Skip to content

Commit

Permalink
Fill out readings section
Browse files Browse the repository at this point in the history
  • Loading branch information
justinsilvestre committed Nov 20, 2023
1 parent 71377a8 commit cee4c1e
Show file tree
Hide file tree
Showing 8 changed files with 883 additions and 4 deletions.
2 changes: 0 additions & 2 deletions app/components/AppLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ export function DictLink({
}: LinkProps<{ figureId: string; focusOnLoad?: boolean }>) {
const linkRef = useRef<HTMLAnchorElement>(null);
useEffect(() => {
console.log("focusOnLoad?", figureId);
if (focusOnLoad) {
console.log("focusOnLoad!!", figureId);
linkRef.current?.focus();
}
}, [figureId, focusOnLoad]);
Expand Down
214 changes: 214 additions & 0 deletions app/features/dictionary/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -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<string | undefined>();
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<typeof useDialog> & {
setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;
setDescriptionId: React.Dispatch<
React.SetStateAction<string | undefined>
>;
})
| null;

const DialogContext = React.createContext<ContextType>(null);

export const useDialogContext = () => {
const context = React.useContext(DialogContext);

if (context == null) {
throw new Error("Dialog components must be wrapped in <Dialog />");
}

return context;
};

export function Dialog({
children,
...options
}: {
children: React.ReactNode;
} & DialogOptions) {
const dialog = useDialog(options);
return (
<DialogContext.Provider value={dialog}>{children}</DialogContext.Provider>
);
}

interface DialogTriggerProps {
children: React.ReactNode;
asChild?: boolean;
}

export const DialogTrigger = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & DialogTriggerProps
>(function DialogTrigger({ children, asChild = false, ...props }, propRef) {
const context = useDialogContext();
const childrenRef = (children as unknown as { ref: React.Ref<HTMLElement> })
.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 (
<button
ref={ref}
// The user can style the trigger based on the state
data-state={context.open ? "open" : "closed"}
{...context.getReferenceProps(props)}
>
{children}
</button>
);
});

export const DialogContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
>(function DialogContent(props, propRef) {
const { context: floatingContext, ...context } = useDialogContext();
const ref = useMergeRefs([context.refs.setFloating, propRef]);

if (!floatingContext.open) return null;

return (
<FloatingPortal>
<FloatingOverlay className="grid place-items-center">
<FloatingFocusManager context={floatingContext}>
<div
ref={ref}
aria-labelledby={context.labelId}
aria-describedby={context.descriptionId}
{...context.getFloatingProps(props)}
>
{props.children}
</div>
</FloatingFocusManager>
</FloatingOverlay>
</FloatingPortal>
);
});

export const DialogHeading = React.forwardRef<
HTMLHeadingElement,
React.HTMLProps<HTMLHeadingElement>
>(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 (
<h2 {...props} ref={ref} id={id}>
{children}
</h2>
);
});

export const DialogDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLProps<HTMLParagraphElement>
>(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 (
<p {...props} ref={ref} id={id}>
{children}
</p>
);
});

export const DialogClose = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(function DialogClose(props, ref) {
const { setOpen } = useDialogContext();
return (
<button type="button" {...props} ref={ref} onClick={() => setOpen(false)} />
);
});
Loading

0 comments on commit cee4c1e

Please sign in to comment.