From cee4c1ebab552a5560f39ac1fb945205fada0ca9 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Mon, 20 Nov 2023 17:05:26 +0100 Subject: [PATCH] Fill out readings section --- app/components/AppLink.tsx | 2 - app/features/dictionary/Dialog.tsx | 214 +++++++++++ app/features/dictionary/DictEntryReadings.tsx | 359 ++++++++++++++++++ .../SingleFigureDictionaryEntry.tsx | 9 +- .../dictionary/kanjidicKanaToRomaji.ts | 191 ++++++++++ app/routes/dict.$figureId.sino.tsx | 50 +++ package-lock.json | 60 +++ package.json | 2 + 8 files changed, 883 insertions(+), 4 deletions(-) create mode 100644 app/features/dictionary/Dialog.tsx create mode 100644 app/features/dictionary/DictEntryReadings.tsx create mode 100644 app/features/dictionary/kanjidicKanaToRomaji.ts create mode 100644 app/routes/dict.$figureId.sino.tsx diff --git a/app/components/AppLink.tsx b/app/components/AppLink.tsx index 290a198de..798d88e77 100644 --- a/app/components/AppLink.tsx +++ b/app/components/AppLink.tsx @@ -32,9 +32,7 @@ export function DictLink({ }: LinkProps<{ figureId: string; focusOnLoad?: boolean }>) { const linkRef = useRef(null); useEffect(() => { - console.log("focusOnLoad?", figureId); if (focusOnLoad) { - console.log("focusOnLoad!!", figureId); linkRef.current?.focus(); } }, [figureId, focusOnLoad]); diff --git a/app/features/dictionary/Dialog.tsx b/app/features/dictionary/Dialog.tsx new file mode 100644 index 000000000..23302bcfb --- /dev/null +++ b/app/features/dictionary/Dialog.tsx @@ -0,0 +1,214 @@ +import { + useFloating, + useClick, + useDismiss, + useRole, + useInteractions, + useMergeRefs, + FloatingPortal, + FloatingFocusManager, + FloatingOverlay, + useId, +} from "@floating-ui/react"; +import * as React from "react"; + +interface DialogOptions { + initialOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +export function useDialog({ + initialOpen = false, + open: controlledOpen, + onOpenChange: setControlledOpen, +}: DialogOptions = {}) { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); + const [labelId, setLabelId] = React.useState(); + const [descriptionId, setDescriptionId] = React.useState< + string | undefined + >(); + + const open = controlledOpen ?? uncontrolledOpen; + const setOpen = setControlledOpen ?? setUncontrolledOpen; + + const data = useFloating({ + open, + onOpenChange: setOpen, + }); + + const context = data.context; + + const click = useClick(context, { + enabled: controlledOpen == null, + }); + const dismiss = useDismiss(context, { outsidePressEvent: "mousedown" }); + const role = useRole(context); + + const interactions = useInteractions([click, dismiss, role]); + + return React.useMemo( + () => ({ + open, + setOpen, + ...interactions, + ...data, + labelId, + descriptionId, + setLabelId, + setDescriptionId, + }), + [open, setOpen, interactions, data, labelId, descriptionId], + ); +} + +type ContextType = + | (ReturnType & { + setLabelId: React.Dispatch>; + setDescriptionId: React.Dispatch< + React.SetStateAction + >; + }) + | null; + +const DialogContext = React.createContext(null); + +export const useDialogContext = () => { + const context = React.useContext(DialogContext); + + if (context == null) { + throw new Error("Dialog components must be wrapped in "); + } + + return context; +}; + +export function Dialog({ + children, + ...options +}: { + children: React.ReactNode; +} & DialogOptions) { + const dialog = useDialog(options); + return ( + {children} + ); +} + +interface DialogTriggerProps { + children: React.ReactNode; + asChild?: boolean; +} + +export const DialogTrigger = React.forwardRef< + HTMLElement, + React.HTMLProps & DialogTriggerProps +>(function DialogTrigger({ children, asChild = false, ...props }, propRef) { + const context = useDialogContext(); + const childrenRef = (children as unknown as { ref: React.Ref }) + .ref; + const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]); + + // `asChild` allows the user to pass any element as the anchor + if (asChild && React.isValidElement(children)) { + return React.cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...children.props, + "data-state": context.open ? "open" : "closed", + }), + ); + } + + return ( + + ); +}); + +export const DialogContent = React.forwardRef< + HTMLDivElement, + React.HTMLProps +>(function DialogContent(props, propRef) { + const { context: floatingContext, ...context } = useDialogContext(); + const ref = useMergeRefs([context.refs.setFloating, propRef]); + + if (!floatingContext.open) return null; + + return ( + + + +
+ {props.children} +
+
+
+
+ ); +}); + +export const DialogHeading = React.forwardRef< + HTMLHeadingElement, + React.HTMLProps +>(function DialogHeading({ children, ...props }, ref) { + const { setLabelId } = useDialogContext(); + const id = useId(); + + // Only sets `aria-labelledby` on the Dialog root element + // if this component is mounted inside it. + React.useLayoutEffect(() => { + setLabelId(id); + return () => setLabelId(undefined); + }, [id, setLabelId]); + + return ( +

+ {children} +

+ ); +}); + +export const DialogDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLProps +>(function DialogDescription({ children, ...props }, ref) { + const { setDescriptionId } = useDialogContext(); + const id = useId(); + + // Only sets `aria-describedby` on the Dialog root element + // if this component is mounted inside it. + React.useLayoutEffect(() => { + setDescriptionId(id); + return () => setDescriptionId(undefined); + }, [id, setDescriptionId]); + + return ( +

+ {children} +

+ ); +}); + +export const DialogClose = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function DialogClose(props, ref) { + const { setOpen } = useDialogContext(); + return ( +