This commit is contained in:
rizky 2024-07-24 00:26:54 -07:00
parent 032d9187ce
commit 36ece9ec6d
2 changed files with 265 additions and 0 deletions

261
comps/custom/Modal.tsx Executable file
View File

@ -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)} />
);
});

View File

@ -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
);