fix
This commit is contained in:
parent
032d9187ce
commit
36ece9ec6d
|
|
@ -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<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",
|
||||
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<typeof useModal> & {
|
||||
setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setDescriptionId: React.Dispatch<
|
||||
React.SetStateAction<string | undefined>
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
|
||||
const ModalContext = React.createContext<ContextType>(null);
|
||||
|
||||
export const useModalContext = () => {
|
||||
const context = React.useContext(ModalContext);
|
||||
|
||||
if (context == null) {
|
||||
throw new Error("Modal components must be wrapped in <Modal />");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export function Modal({
|
||||
children,
|
||||
...options
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
} & ModalOptions) {
|
||||
const dialog = useModal(options);
|
||||
return (
|
||||
<ModalContext.Provider value={dialog}>
|
||||
<ModalContent fade={options.fade} className={cx("modal", "outline-none")}>
|
||||
{children}
|
||||
</ModalContent>
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModalTriggerProps {
|
||||
children: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const ModalTrigger = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.HTMLProps<HTMLElement> & 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 (
|
||||
<button
|
||||
ref={ref}
|
||||
data-state={context.open ? "open" : "closed"}
|
||||
{...context.getReferenceProps(props as any)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export const ModalContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLProps<HTMLDivElement> & { 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 (
|
||||
<FloatingPortal>
|
||||
<FloatingOverlay
|
||||
className={cx(
|
||||
"modal-overlay",
|
||||
"flex items-center justify-center transition-all ",
|
||||
css`
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
`,
|
||||
local.preview ? "opacity-20 duration-1000" : "duration-300"
|
||||
)}
|
||||
lockScroll
|
||||
>
|
||||
<FloatingFocusManager context={floatingContext}>
|
||||
<div
|
||||
ref={ref}
|
||||
onPointerMove={() => {
|
||||
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}
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
</FloatingOverlay>
|
||||
</FloatingPortal>
|
||||
);
|
||||
});
|
||||
|
||||
export const ModalHeading = React.forwardRef<
|
||||
HTMLHeadingElement,
|
||||
React.HTMLProps<HTMLHeadingElement>
|
||||
>(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 (
|
||||
<h2 {...props} ref={ref} id={id}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
});
|
||||
|
||||
export const ModalDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLProps<HTMLParagraphElement>
|
||||
>(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 (
|
||||
<p {...props} ref={ref} id={id}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
|
||||
export const ModalClose = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
>(function ModalClose(props, ref) {
|
||||
const { setOpen } = useModalContext();
|
||||
return (
|
||||
<button type="button" {...props} ref={ref} onClick={() => setOpen(false)} />
|
||||
);
|
||||
});
|
||||
|
|
@ -12,6 +12,10 @@ export const Popover = lazify(
|
|||
async () => (await import("@/comps/custom/Popover")).Popover
|
||||
);
|
||||
|
||||
export const Modal = lazify(
|
||||
async () => (await import("lib/comps/custom/Modal")).Modal
|
||||
);
|
||||
|
||||
export const Typeahead = lazify(
|
||||
async () => (await import("@/comps/ui/typeahead")).Typeahead
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue