mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-09 21:07:49 +00:00
chore: add initial dropdown for new vs old
This commit is contained in:
29
package-lock.json
generated
29
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "^3.7.0",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@floating-ui/react": "^0.24.8",
|
||||
"@tanstack/react-query": "^4.29.19",
|
||||
"medium-zoom": "^1.0.8",
|
||||
"preact": "^10.16.0"
|
||||
@@ -66,7 +66,7 @@
|
||||
"postcss-csso": "^6.0.1",
|
||||
"preact-render-to-string": "^6.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.10.0",
|
||||
"prettier-plugin-astro": "^0.11.0",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"rehype-retext": "^3.0.2",
|
||||
@@ -25141,13 +25141,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-astro": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.10.0.tgz",
|
||||
"integrity": "sha512-dPzop0gKZyVGpTDQmfy+e7FKXC9JT3mlpfYA2diOVz+Ui+QR1U4G/s+OesKl2Hib2JJOtAYJs/l+ovgT0ljlFA==",
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.11.0.tgz",
|
||||
"integrity": "sha512-rl2hJ4Kty/aEfGjk3i4JS+bpng9MjgvwqLRNzeb9NqYhqKoWNwOR39cIJXFjU1vR3zYOPnwWNRMelKb0orunYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@astrojs/compiler": "^1.5.0",
|
||||
"prettier": "^2.8.8",
|
||||
"@astrojs/compiler": "^1.5.5",
|
||||
"prettier": "^3.0.0",
|
||||
"sass-formatter": "^0.7.6"
|
||||
},
|
||||
"engines": {
|
||||
@@ -25155,6 +25155,21 @@
|
||||
"pnpm": ">=7.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-astro/node_modules/prettier": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
|
||||
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"postcss-csso": "^6.0.1",
|
||||
"preact-render-to-string": "^6.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-astro": "^0.10.0",
|
||||
"prettier-plugin-astro": "^0.11.0",
|
||||
"probe-image-size": "^7.2.3",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"rehype-retext": "^3.0.2",
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/vercel": "^3.7.0",
|
||||
"@floating-ui/react": "^0.24.3",
|
||||
"@floating-ui/react": "^0.24.8",
|
||||
"@tanstack/react-query": "^4.29.19",
|
||||
"medium-zoom": "^1.0.8",
|
||||
"preact": "^10.16.0"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { JSXNode, PropsWithChildren } from "../types";
|
||||
import { createElement, Ref } from "preact";
|
||||
import { createElement, Ref, VNode } from "preact";
|
||||
import { JSX } from "preact";
|
||||
import { forwardRef } from "preact/compat";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
type AllowedTags = "a" | "button";
|
||||
|
||||
@@ -34,7 +35,7 @@ const ButtonWrapper = forwardRef(
|
||||
}: ButtonProps<T>,
|
||||
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>
|
||||
) => {
|
||||
const Wrapper = (props: any) => createElement(tag, props, props.children);
|
||||
const Wrapper: any = tag;
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
@@ -81,7 +82,10 @@ export const LargeButton = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
type IconOnlyButtonProps<T extends AllowedTags = "a"> = Omit<ButtonProps<T>, "leftIcon" | "rightIcon">;
|
||||
type IconOnlyButtonProps<T extends AllowedTags = "a"> = Omit<
|
||||
ButtonProps<T>,
|
||||
"leftIcon" | "rightIcon"
|
||||
>;
|
||||
|
||||
export const IconOnlyButton = forwardRef(
|
||||
<T extends AllowedTags = "a">(
|
||||
|
||||
12
src/components/select/select.module.scss
Normal file
12
src/components/select/select.module.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@import "src/tokens/index";
|
||||
|
||||
.downSpan,
|
||||
.downSpan svg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selectDropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
187
src/components/select/select.tsx
Normal file
187
src/components/select/select.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* TODO: Migrate this to be controlled at some point. Right now, it's uncontrolled
|
||||
*/
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
useFloating,
|
||||
useInteractions,
|
||||
useListNavigation,
|
||||
useTypeahead,
|
||||
useClick,
|
||||
useListItem,
|
||||
useDismiss,
|
||||
useRole,
|
||||
FloatingFocusManager,
|
||||
FloatingList,
|
||||
} from "@floating-ui/react";
|
||||
import { createContext } from "preact";
|
||||
import { PropsWithChildren, useContext } from "preact/compat";
|
||||
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
|
||||
import down from "src/icons/chevron_down.svg?raw";
|
||||
import { Button } from "components/button/button";
|
||||
import styles from "./select.module.scss";
|
||||
|
||||
interface SelectContextValue {
|
||||
activeIndex: number | null;
|
||||
selectedIndex: number | null;
|
||||
getItemProps: ReturnType<typeof useInteractions>["getItemProps"];
|
||||
handleSelect: (index: number | null) => void;
|
||||
}
|
||||
|
||||
const SelectContext = createContext<SelectContextValue>(
|
||||
{} as SelectContextValue
|
||||
);
|
||||
|
||||
const rightIcon = (
|
||||
<span
|
||||
className={styles.downSpan}
|
||||
dangerouslySetInnerHTML={{ __html: down }}
|
||||
></span>
|
||||
);
|
||||
|
||||
interface SelectProps {
|
||||
initial?: {
|
||||
selectedIndex: number;
|
||||
selectedLabel: string;
|
||||
};
|
||||
class?: string;
|
||||
className?: string;
|
||||
onChangeVal: (val: string) => void;
|
||||
}
|
||||
|
||||
export function Select({
|
||||
children,
|
||||
initial,
|
||||
class: className = "",
|
||||
className: classNameName = "",
|
||||
onChangeVal,
|
||||
}: PropsWithChildren<SelectProps>) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(
|
||||
initial?.selectedIndex ?? null
|
||||
);
|
||||
const [selectedLabel, setSelectedLabel] = useState<string | null>(
|
||||
initial?.selectedLabel ?? null
|
||||
);
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
placement: "bottom",
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [flip()],
|
||||
});
|
||||
|
||||
const elementsRef = useRef<Array<HTMLElement | null>>([]);
|
||||
const labelsRef = useRef<Array<string | null>>([]);
|
||||
|
||||
const handleSelect = useCallback((index: number | null) => {
|
||||
setSelectedIndex(index);
|
||||
setIsOpen(false);
|
||||
if (index !== null) {
|
||||
const newLabel = labelsRef.current[index];
|
||||
setSelectedLabel(newLabel);
|
||||
onChangeVal(newLabel);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function handleTypeaheadMatch(index: number | null) {
|
||||
if (isOpen) {
|
||||
setActiveIndex(index);
|
||||
} else {
|
||||
handleSelect(index);
|
||||
}
|
||||
}
|
||||
|
||||
const listNav = useListNavigation(context, {
|
||||
listRef: elementsRef,
|
||||
activeIndex,
|
||||
selectedIndex,
|
||||
onNavigate: setActiveIndex,
|
||||
});
|
||||
const typeahead = useTypeahead(context, {
|
||||
listRef: labelsRef,
|
||||
activeIndex,
|
||||
selectedIndex,
|
||||
onMatch: handleTypeaheadMatch,
|
||||
});
|
||||
const click = useClick(context);
|
||||
const dismiss = useDismiss(context);
|
||||
const role = useRole(context, { role: "listbox" });
|
||||
|
||||
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
||||
[listNav, typeahead, click, dismiss, role]
|
||||
);
|
||||
|
||||
const selectContext = useMemo(
|
||||
() => ({
|
||||
activeIndex,
|
||||
selectedIndex,
|
||||
getItemProps,
|
||||
handleSelect,
|
||||
}),
|
||||
[activeIndex, selectedIndex, getItemProps, handleSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
class={`${className} ${classNameName}`}
|
||||
tag="button"
|
||||
type="button"
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
rightIcon={rightIcon}
|
||||
>
|
||||
{selectedLabel ?? "Select..."}
|
||||
</Button>
|
||||
|
||||
<SelectContext.Provider value={selectContext}>
|
||||
{isOpen && (
|
||||
<FloatingFocusManager context={context} modal={false}>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
class={styles.selectDropdown}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
|
||||
{children}
|
||||
</FloatingList>
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
)}
|
||||
</SelectContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Option({ label }: { label: string }) {
|
||||
const { activeIndex, selectedIndex, getItemProps, handleSelect } =
|
||||
useContext(SelectContext);
|
||||
|
||||
const { ref, index } = useListItem({ label });
|
||||
|
||||
const isActive = activeIndex === index;
|
||||
const isSelected = selectedIndex === index;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
role="option"
|
||||
aria-selected={isActive && isSelected}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
style={{
|
||||
background: isActive ? "cyan" : "",
|
||||
fontWeight: isSelected ? "bold" : "",
|
||||
}}
|
||||
{...getItemProps({
|
||||
onClick: () => handleSelect(index),
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,11 @@
|
||||
display: grid;
|
||||
gap: var(--spc-4x);
|
||||
|
||||
grid-template-rows: repeat(5, auto);
|
||||
grid-template-columns: repeat(2, auto);
|
||||
|
||||
@include from($tabletSmall) {
|
||||
grid-template-rows: repeat(3, auto);
|
||||
grid-template-rows: repeat(4, auto);
|
||||
grid-template-columns: repeat(2, auto);
|
||||
}
|
||||
|
||||
@@ -21,8 +24,12 @@
|
||||
width: 100%;
|
||||
background: var(--background_disabled);
|
||||
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 2;
|
||||
|
||||
@include from($tabletSmall) {
|
||||
grid-column: 1 / 3;
|
||||
grid-row: unset;
|
||||
}
|
||||
|
||||
@include from($tabletLarge) {
|
||||
@@ -32,12 +39,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.orderSelectContainer {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
@include from($tabletSmall) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabletSmallTopBarDivider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: var(--background_disabled);
|
||||
|
||||
grid-column: 1 / 3;
|
||||
|
||||
@include from($tabletLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.topBarButtonsContentToDisplay {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spc-2x);
|
||||
justify-self: start;
|
||||
align-items: center;
|
||||
|
||||
@include from($tabletSmall) {
|
||||
grid-column: 1;
|
||||
@@ -47,6 +74,7 @@
|
||||
@include from($tabletLarge) {
|
||||
grid-row: unset;
|
||||
grid-column: unset;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +124,8 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
grid-column: 1 / 3;
|
||||
|
||||
@include from($tabletSmall) {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SearchInput } from "components/input/input";
|
||||
import { Button, IconOnlyButton } from "components/button/button";
|
||||
import filter from "src/icons/filter.svg?raw";
|
||||
import forward from "src/icons/arrow_right.svg?raw";
|
||||
import { Option, Select } from "components/select/select";
|
||||
|
||||
interface SearchTopbarProps {
|
||||
onSearch: (search: string) => void;
|
||||
@@ -88,6 +89,18 @@ export const SearchTopbar = ({
|
||||
Collections
|
||||
</Button>
|
||||
</div>
|
||||
<div class={style.orderSelectContainer}>
|
||||
<Select
|
||||
initial={{
|
||||
selectedIndex: 1,
|
||||
selectedLabel: "Newest",
|
||||
}}
|
||||
onChangeVal={(val) => alert(val)}
|
||||
>
|
||||
<Option label={"Newest"} />
|
||||
<Option label={"Oldest"} />
|
||||
</Select>
|
||||
</div>
|
||||
<div className={style.topBarSmallTabletButtons}>
|
||||
<div role="group" className={style.topBarSmallTabletButtonsToggle}>
|
||||
<Button
|
||||
@@ -114,6 +127,7 @@ export const SearchTopbar = ({
|
||||
></span>
|
||||
</IconOnlyButton>
|
||||
</div>
|
||||
<div class={style.tabletSmallTopBarDivider} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user