diff --git a/comps/custom/Modal.tsx b/comps/custom/Modal.tsx new file mode 100755 index 0000000..597abed --- /dev/null +++ b/comps/custom/Modal.tsx @@ -0,0 +1,261 @@ +import { + FloatingFocusManager, + FloatingOverlay, + FloatingPortal, + useClick, + useDismiss, + useFloating, + useId, + useInteractions, + useMergeRefs, + useRole, +} from "@floating-ui/react"; +import { useLocal } from "lib/utils/use-local"; +import * as React from "react"; + +interface ModalOptions { + initialOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + fade?: boolean; +} + +export function useModal({ + initialOpen = true, + open: controlledOpen, + onOpenChange: setControlledOpen, +}: ModalOptions) { + 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", + escapeKey: false, + }); + 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 ModalContext = React.createContext(null); + +export const useModalContext = () => { + const context = React.useContext(ModalContext); + + if (context == null) { + throw new Error("Modal components must be wrapped in "); + } + + return context; +}; + +export function Modal({ + children, + ...options +}: { + children: React.ReactNode; +} & ModalOptions) { + const dialog = useModal(options); + return ( + + + {children} + + + ); +} + +interface ModalTriggerProps { + children: React.ReactNode; + asChild?: boolean; +} + +export const ModalTrigger = React.forwardRef< + HTMLElement, + React.HTMLProps & ModalTriggerProps +>(function ModalTrigger({ children, asChild = false, ...props }, propRef) { + const context = useModalContext(); + const childrenRef = (children as any).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 ModalContent = React.forwardRef< + HTMLDivElement, + React.HTMLProps & { fade?: boolean } +>(function ModalContent(props, propRef) { + const local = useLocal({ preview: false, timeout: null as any }); + const { context: floatingContext, ...context } = useModalContext(); + const ref = useMergeRefs([context.refs.setFloating, propRef]); + + if (!floatingContext.open) return null; + const _props = { ...props }; + if (typeof _props.fade !== "undefined") { + delete _props.fade; + } + + const floatingDivProps = context.getFloatingProps(_props); + return ( + + + +
{ + if (props.fade !== false) { + clearTimeout(local.timeout); + if (local.preview) { + local.preview = false; + local.render(); + } + } + }} + onPointerLeave={(e) => { + // if (Object.keys(w.openedPopupID || {}).length > 0) { + // return; + // } + + if (props.fade !== false) { + clearTimeout(local.timeout); + local.timeout = setTimeout(() => { + local.preview = true; + local.render(); + }, 1000); + } + }} + aria-labelledby={context.labelId} + aria-describedby={context.descriptionId} + {...floatingDivProps} + > + {props.children} +
+
+
+
+ ); +}); + +export const ModalHeading = React.forwardRef< + HTMLHeadingElement, + React.HTMLProps +>(function ModalHeading({ children, ...props }, ref) { + const { setLabelId } = useModalContext(); + const id = useId(); + + // Only sets `aria-labelledby` on the Modal root element + // if this component is mounted inside it. + React.useLayoutEffect(() => { + setLabelId(id); + return () => setLabelId(undefined); + }, [id, setLabelId]); + + return ( +

+ {children} +

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

+ {children} +

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