switch from react-spectrum to react-aria custom components
This commit is contained in:
@@ -39,6 +39,11 @@ const logKeyShortcut = (keyCode) => {
|
||||
}
|
||||
};
|
||||
|
||||
const IconStyle = {
|
||||
width: '24px',
|
||||
margin: '0 4px'
|
||||
}
|
||||
|
||||
const ArrowLinkClasses = `hover:underline text-vibrant-light hover:text-muted-light
|
||||
lg:px-4 self-stretch flex items-center hover:bg-black/50 max-h-screen sticky top-0
|
||||
`;
|
||||
@@ -260,37 +265,37 @@ const GalleryImage = ({ data, location: { state } }) => {
|
||||
<div className="flex flex-col items-end">
|
||||
<MetadataItem
|
||||
data={dateTaken.toLocaleDateString()}
|
||||
icon={<Calendar />}
|
||||
icon={<Calendar UNSAFE_style={IconStyle} />}
|
||||
title="date taken"
|
||||
/>
|
||||
<div className="sm:flex justify-end gap-2 border border-vibrant-light pl-2 rounded">
|
||||
<MetadataItem
|
||||
data={shutterSpeed}
|
||||
icon={<Stopwatch />}
|
||||
icon={<Stopwatch UNSAFE_style={IconStyle} />}
|
||||
title="shutter speed"
|
||||
/>
|
||||
{meta.FNumber && (
|
||||
<MetadataItem
|
||||
data={`f/${meta.FNumber}`}
|
||||
icon={<Exposure />}
|
||||
icon={<Exposure UNSAFE_style={IconStyle} />}
|
||||
title="aperture"
|
||||
/>
|
||||
)}
|
||||
<MetadataItem
|
||||
data={meta.ISO}
|
||||
icon={<Filmroll />}
|
||||
icon={<Filmroll UNSAFE_style={IconStyle} />}
|
||||
title="ISO"
|
||||
/>
|
||||
</div>
|
||||
<MetadataItem
|
||||
data={locationString}
|
||||
icon={<Location />}
|
||||
icon={<Location UNSAFE_style={IconStyle} />}
|
||||
title="location"
|
||||
/>
|
||||
{(meta.Make || meta.Model) && (
|
||||
<MetadataItem
|
||||
data={[meta.Make, meta.Model].join(" ")}
|
||||
icon={<Camera />}
|
||||
icon={<Camera UNSAFE_style={IconStyle} />}
|
||||
title="camera"
|
||||
/>
|
||||
)}
|
||||
@@ -302,7 +307,7 @@ const GalleryImage = ({ data, location: { state } }) => {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" @")}
|
||||
icon={<Circle />}
|
||||
icon={<Circle UNSAFE_style={IconStyle} />}
|
||||
title="lens"
|
||||
/>
|
||||
)}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
import Checkmark from "@spectrum-icons/workflow/Checkmark";
|
||||
|
||||
interface KeywordsPickerProps {
|
||||
keywords: string[];
|
||||
@@ -9,7 +10,7 @@ interface KeywordsPickerProps {
|
||||
const KeywordsPicker = ({ keywords, value, onChange }: KeywordsPickerProps) => {
|
||||
return (
|
||||
<div className="mx-2 mt-2">
|
||||
<span className="text-xs text-[var(--spectrum-fieldlabel-text-color,var(--spectrum-alias-label-text-color))]">
|
||||
<span className="text-xs text-black">
|
||||
Collections
|
||||
</span>
|
||||
<ul className="flex gap-1 flex-wrap mt-1 mb-2">
|
||||
@@ -19,20 +20,24 @@ const KeywordsPicker = ({ keywords, value, onChange }: KeywordsPickerProps) => {
|
||||
<li key={keyword}>
|
||||
<button
|
||||
className={classNames(
|
||||
"transition",
|
||||
`py-[5px] px-3 rounded-full`,
|
||||
`text-[var(--spectrum-fieldbutton-text-color,var(--spectrum-alias-text-color))]
|
||||
|
||||
border border-[var(--spectrum-fieldbutton-border-color,var(--spectrum-alias-border-color))]`,
|
||||
`text-black border border-black`,
|
||||
selected
|
||||
? "bg-green-500 hover:bg-green-300"
|
||||
: `bg-[var(--spectrum-fieldbutton-background-color,var(--spectrum-global-color-gray-75))]
|
||||
hover:bg-[var(--spectrum-fieldbutton-background-color-down,var(--spectrum-global-color-gray-200))]`
|
||||
? "bg-transparentblack font-bold"
|
||||
: `bg-white
|
||||
hover:bg-transparentblack`
|
||||
)}
|
||||
onClick={() => (selected ? onChange(null) : onChange(keyword))}
|
||||
type="button"
|
||||
>
|
||||
{keyword}
|
||||
{keyword}{" "}
|
||||
{selected && (
|
||||
<Checkmark
|
||||
UNSAFE_className="mx-1"
|
||||
UNSAFE_style={{ width: "15px" }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
|
105
src/components/ListBox.tsx
Normal file
105
src/components/ListBox.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import * as React from "react";
|
||||
import type { AriaListBoxOptions } from "@react-aria/listbox";
|
||||
import type { ListState } from "react-stately";
|
||||
import type { Node } from "@react-types/shared";
|
||||
import { useListBox, useListBoxSection, useOption } from "react-aria";
|
||||
// import { CheckIcon } from "@heroicons/react/solid";
|
||||
import Checkmark from '@spectrum-icons/workflow/Checkmark'
|
||||
|
||||
interface ListBoxProps extends AriaListBoxOptions<unknown> {
|
||||
listBoxRef?: React.RefObject<HTMLUListElement>;
|
||||
state: ListState<unknown>;
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
section: Node<unknown>;
|
||||
state: ListState<unknown>;
|
||||
}
|
||||
|
||||
interface OptionProps {
|
||||
item: Node<unknown>;
|
||||
state: ListState<unknown>;
|
||||
}
|
||||
|
||||
export function ListBox(props: ListBoxProps) {
|
||||
let ref = React.useRef<HTMLUListElement>(null);
|
||||
let { listBoxRef = ref, state } = props;
|
||||
let { listBoxProps } = useListBox(props, state, listBoxRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...listBoxProps}
|
||||
className="w-full max-h-72 overflow-auto outline-none"
|
||||
ref={listBoxRef}
|
||||
>
|
||||
{[...state.collection].map((item) =>
|
||||
item.type === "section" ? (
|
||||
<ListBoxSection key={item.key} section={item} state={state} />
|
||||
) : (
|
||||
<Option item={item} key={item.key} state={state} />
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function ListBoxSection({ section, state }: SectionProps) {
|
||||
let { itemProps, headingProps, groupProps } = useListBoxSection({
|
||||
heading: section.rendered,
|
||||
"aria-label": section["aria-label"]
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<li {...itemProps} className="pt-2">
|
||||
{section.rendered && (
|
||||
<span
|
||||
{...headingProps}
|
||||
className="text-xs font-bold uppercase text-gray-500 mx-3"
|
||||
>
|
||||
{section.rendered}
|
||||
</span>
|
||||
)}
|
||||
<ul {...groupProps}>
|
||||
{[...section.childNodes].map((node) => (
|
||||
<Option item={node} key={node.key} state={state} />
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Option({ item, state }: OptionProps) {
|
||||
let ref = React.useRef<HTMLLIElement>(null);
|
||||
let { optionProps, isDisabled, isSelected, isFocused } = useOption(
|
||||
{
|
||||
key: item.key
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
let text = "text-black";
|
||||
if (isFocused || isSelected) {
|
||||
text = "text-black";
|
||||
} else if (isDisabled) {
|
||||
text = "text-gray-200";
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
{...optionProps}
|
||||
className={`p-3 outline-none cursor-default flex items-center justify-between ${text} ${
|
||||
isFocused ? "bg-transparentblack" : ""
|
||||
} ${isSelected ? "font-bold" : ""}`}
|
||||
ref={ref}
|
||||
>
|
||||
{item.rendered}
|
||||
{isSelected && (
|
||||
<Checkmark UNSAFE_className="mx-1" UNSAFE_style={{ width: '15px' }} aria-hidden="true" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
@@ -138,7 +138,7 @@ const MasonryGallery = ({
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
"border-4 overflow-hidden",
|
||||
"border-4 border-white overflow-hidden",
|
||||
debugHue && "border-8"
|
||||
)}
|
||||
id={image.base}
|
||||
@@ -159,7 +159,7 @@ const MasonryGallery = ({
|
||||
${image.fields?.imageMeta?.dominantHue?.[1] ?? 0 * 100}%,
|
||||
${image.fields?.imageMeta?.dominantHue?.[2] ?? 0 * 100}%
|
||||
)`
|
||||
: "white",
|
||||
: "",
|
||||
}}
|
||||
to={`/photogallery/${image.base}`}
|
||||
>
|
||||
|
39
src/components/Popover.tsx
Normal file
39
src/components/Popover.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { OverlayTriggerState } from "react-stately";
|
||||
import type { AriaPopoverProps } from "@react-aria/overlays";
|
||||
import * as React from "react";
|
||||
import { usePopover, DismissButton, Overlay } from "@react-aria/overlays";
|
||||
|
||||
interface PopoverProps extends Omit<AriaPopoverProps, "popoverRef"> {
|
||||
children: React.ReactNode;
|
||||
state: OverlayTriggerState;
|
||||
className?: string;
|
||||
popoverRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function Popover(props: PopoverProps) {
|
||||
let ref = React.useRef<HTMLDivElement>(null);
|
||||
let { popoverRef = ref, state, children, className, isNonModal } = props;
|
||||
|
||||
let { popoverProps, underlayProps } = usePopover(
|
||||
{
|
||||
...props,
|
||||
popoverRef
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
return (
|
||||
<Overlay>
|
||||
{!isNonModal && <div {...underlayProps} className="fixed inset-0" />}
|
||||
<div
|
||||
{...popoverProps}
|
||||
className={`z-10 shadow border border-black bg-white rounded mt-1 ${className}`}
|
||||
ref={popoverRef}
|
||||
>
|
||||
{!isNonModal && <DismissButton onDismiss={state.close} />}
|
||||
{children}
|
||||
<DismissButton onDismiss={state.close} />
|
||||
</div>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
89
src/components/Select.tsx
Normal file
89
src/components/Select.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import * as React from "react";
|
||||
import type { AriaSelectProps } from "@react-types/select";
|
||||
import { useSelectState } from "react-stately";
|
||||
import {
|
||||
useSelect,
|
||||
HiddenSelect,
|
||||
useButton,
|
||||
mergeProps,
|
||||
useFocusRing
|
||||
} from "react-aria";
|
||||
// import { SelectorIcon } from "@heroicons/react/solid";
|
||||
import ChevronDown from "@spectrum-icons/workflow/ChevronDown";
|
||||
|
||||
import { ListBox } from "./ListBox";
|
||||
import { Popover } from "./Popover";
|
||||
|
||||
export { Item } from "react-stately";
|
||||
|
||||
export function Select<T extends object>(props: AriaSelectProps<T>) {
|
||||
// Create state based on the incoming props
|
||||
let state = useSelectState(props);
|
||||
|
||||
// Get props for child elements from useSelect
|
||||
let ref = React.useRef(null);
|
||||
let { labelProps, triggerProps, valueProps, menuProps } = useSelect(
|
||||
props,
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
// Get props for the button based on the trigger props from useSelect
|
||||
let { buttonProps } = useButton(triggerProps, ref);
|
||||
|
||||
let { focusProps, isFocusVisible } = useFocusRing();
|
||||
|
||||
return (
|
||||
<div className="inline-flex flex-col relative">
|
||||
<div
|
||||
{...labelProps}
|
||||
className="block text-sm text-left cursor-default mb-1"
|
||||
>
|
||||
{props.label}
|
||||
</div>
|
||||
<HiddenSelect
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
state={state}
|
||||
triggerRef={ref}
|
||||
/>
|
||||
<button
|
||||
{...mergeProps(buttonProps, focusProps)}
|
||||
className={`py-[5px] px-3 w-[150px] flex flex-row items-center justify-between overflow-hidden cursor-default rounded border ${
|
||||
isFocusVisible ? "border-green-500" : "border-black"
|
||||
} ${state.isOpen ? "bg-gray-100" : "bg-white"}`}
|
||||
ref={ref}
|
||||
>
|
||||
<span
|
||||
{...valueProps}
|
||||
// className={`text-md flex-auto ${
|
||||
// state.selectedItem ? "text-gray-800" : "text-gray-500"
|
||||
// }`}
|
||||
>
|
||||
{state.selectedItem
|
||||
? state.selectedItem.rendered
|
||||
: "Select an option"}
|
||||
</span>
|
||||
<ChevronDown
|
||||
UNSAFE_className="mx-1"
|
||||
UNSAFE_style={{
|
||||
width: '15px'
|
||||
}}
|
||||
// UNSAFE_className={`w-[20px] h-5 ${
|
||||
// isFocusVisible ? "text-pink-500" : "text-gray-500"
|
||||
// }`}
|
||||
/>
|
||||
</button>
|
||||
{state.isOpen && (
|
||||
<Popover
|
||||
className="w-[150px]"
|
||||
placement="bottom start"
|
||||
state={state}
|
||||
triggerRef={ref}
|
||||
>
|
||||
<ListBox {...menuProps} state={state} />
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -78,8 +78,8 @@ const IndexPage = ({
|
||||
LightMuted: [0, 0, 0],
|
||||
Vibrant: [0, 0, 0],
|
||||
LightVibrant: [0, 0, 0],
|
||||
DarkMuted: [255, 255, 255],
|
||||
DarkVibrant: [255, 255, 255],
|
||||
DarkMuted: [238, 238, 238],
|
||||
DarkVibrant: [238, 238, 238],
|
||||
})}
|
||||
/>
|
||||
</Helmet>
|
||||
|
@@ -2,12 +2,13 @@ import * as React from "react";
|
||||
import * as R from "ramda";
|
||||
import { graphql, PageProps } from "gatsby";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Picker, Item } from "@adobe/react-spectrum";
|
||||
// import { Picker, Item } from "@adobe/react-spectrum";
|
||||
|
||||
import MasonryGallery from "../components/MasonryGallery";
|
||||
import KeywordsPicker from "../components/KeywordsPicker";
|
||||
import { getGalleryPageUrl, getHelmetSafeBodyStyle } from "../utils";
|
||||
import Nav from "../components/Nav";
|
||||
import { Item, Select } from "../components/Select";
|
||||
|
||||
const SORT_KEYS = {
|
||||
hue: ["fields", "imageMeta", "vibrantHue"],
|
||||
@@ -170,8 +171,8 @@ const GalleryPage = ({ data }: PageProps<Queries.GalleryPageQueryQuery>) => {
|
||||
LightMuted: [0, 0, 0],
|
||||
Vibrant: [0, 0, 0],
|
||||
LightVibrant: [0, 0, 0],
|
||||
DarkMuted: [255, 255, 255],
|
||||
DarkVibrant: [255, 255, 255],
|
||||
DarkMuted: [238, 238, 238],
|
||||
DarkVibrant: [238, 238, 238],
|
||||
})}
|
||||
/>
|
||||
</Helmet>
|
||||
@@ -205,7 +206,7 @@ const GalleryPage = ({ data }: PageProps<Queries.GalleryPageQueryQuery>) => {
|
||||
value={filterKeyword}
|
||||
/>
|
||||
<div className="m-2">
|
||||
<Picker
|
||||
<Select
|
||||
label="Sort by..."
|
||||
// @ts-ignore
|
||||
onSelectionChange={setSortKey}
|
||||
@@ -214,7 +215,7 @@ const GalleryPage = ({ data }: PageProps<Queries.GalleryPageQueryQuery>) => {
|
||||
<Item key="rating">Curated</Item>
|
||||
<Item key="date">Date</Item>
|
||||
<Item key="hue">Hue</Item>
|
||||
</Picker>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user