Files
homarr/packages/modals/src/index.tsx
Meier Lukas b78d32b81c fix: nextjs is slow dev server (#364)
* fix: nextjs slow compile time

* fix: change optimized package imports and transpile packages

* fix: format issue
2024-04-25 22:06:15 +02:00

146 lines
3.8 KiB
TypeScript

"use client";
import type { PropsWithChildren } from "react";
import {
createContext,
useCallback,
useContext,
useReducer,
useRef,
} from "react";
import { getDefaultZIndex, Modal } from "@mantine/core";
import { randomId } from "@mantine/hooks";
import type { stringOrTranslation } from "@homarr/translation";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { ConfirmModalProps } from "./confirm-modal";
import { ConfirmModal } from "./confirm-modal";
import { modalReducer } from "./reducer";
import type { inferInnerProps, ModalDefinition } from "./type";
interface ModalContextProps {
openModalInner: <TModal extends ModalDefinition>(props: {
modal: TModal;
innerProps: inferInnerProps<TModal>;
options: OpenModalOptions;
}) => void;
closeModal: (id: string) => void;
}
export const ModalContext = createContext<ModalContextProps | null>(null);
export const ModalProvider = ({ children }: PropsWithChildren) => {
const t = useI18n();
const [state, dispatch] = useReducer(modalReducer, {
modals: [],
current: null,
});
const stateRef = useRef(state);
stateRef.current = state;
const closeModal = useCallback(
(id: string, canceled?: boolean) => {
dispatch({ type: "CLOSE", modalId: id, canceled });
},
[stateRef, dispatch],
);
const openModalInner: ModalContextProps["openModalInner"] = useCallback(
({ modal, innerProps, options }) => {
const id = randomId();
const { title, ...rest } = options;
dispatch({
type: "OPEN",
modal: {
id,
modal,
props: {
...modal.options,
...rest,
defaultTitle: title ?? modal.options.defaultTitle,
innerProps,
},
},
});
return id;
},
[dispatch],
);
const handleCloseModal = useCallback(
() => state.current && closeModal(state.current.id),
[closeModal, state.current?.id],
);
const activeModals = state.modals.filter(
(modal) => modal.id === state.current?.id || modal.props.keepMounted,
);
return (
<ModalContext.Provider value={{ openModalInner, closeModal }}>
{activeModals.map((modal) => {
const { defaultTitle: _ignored, ...otherModalProps } =
modal.reference.modalProps;
return (
<Modal
key={modal.id}
zIndex={getDefaultZIndex("modal") + 1}
display={modal.id === state.current?.id ? undefined : "none"}
style={{
userSelect: modal.id === state.current?.id ? undefined : "none",
}}
styles={{
title: {
fontSize: "1.25rem",
fontWeight: 500,
},
}}
trapFocus={modal.id === state.current?.id}
{...otherModalProps}
title={translateIfNecessary(t, modal.props.defaultTitle)}
opened={state.modals.length > 0}
onClose={handleCloseModal}
>
{modal.reference.content}
</Modal>
);
})}
{children}
</ModalContext.Provider>
);
};
interface OpenModalOptions {
keepMounted?: boolean;
title?: stringOrTranslation;
}
export const useModalAction = <TModal extends ModalDefinition>(
modal: TModal,
) => {
const context = useContext(ModalContext);
if (!context) throw new Error("ModalContext is not provided");
return {
openModal: (
innerProps: inferInnerProps<TModal>,
options: OpenModalOptions | void,
) => {
context.openModalInner({ modal, innerProps, options: options ?? {} });
},
};
};
export const useConfirmModal = () => {
const { openModal } = useModalAction(ConfirmModal);
return {
openConfirmModal: (props: ConfirmModalProps) =>
openModal(props, { title: props.title }),
};
};