Files
Homarr/packages/modals/src/index.tsx
homarr-renovate[bot] a87c937b69 fix(deps): update dependency eslint-plugin-react-hooks to v5 (#1280)
* fix(deps): update dependency eslint-plugin-react-hooks to v5

* fix: lint issues after reenabling hook rules

* fix: format issues

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-10-16 21:43:51 +02:00

152 lines
4.4 KiB
TypeScript

"use client";
import type { PropsWithChildren } from "react";
import { createContext, useCallback, useContext, useEffect, useReducer, useRef, useState } 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 type { ModalsState, ModalStateWithReference } from "./reducer";
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 [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 });
},
[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]);
const activeModals = state.modals.filter((modal) => modal.id === state.current?.id || modal.props.keepMounted);
return (
<ModalContext.Provider value={{ openModalInner, closeModal }}>
{activeModals.map((modal) => (
<ActiveModal key={modal.id} modal={modal} state={state} handleCloseModal={handleCloseModal} />
))}
{children}
</ModalContext.Provider>
);
};
interface ActiveModalProps {
modal: ModalStateWithReference;
state: ModalsState;
handleCloseModal: () => void;
}
const ActiveModal = ({ modal, state, handleCloseModal }: ActiveModalProps) => {
const t = useI18n();
// The below code is used to animate the modal when it opens (using the transition)
// It is necessary as transition is not working when the modal is directly mounted and run
const [opened, setOpened] = useState(false);
useEffect(() => {
setTimeout(() => setOpened(true), 0);
}, []);
const { defaultTitle: _ignored, ...otherModalProps } = modal.reference.modalProps;
return (
<Modal
key={modal.id}
zIndex={getDefaultZIndex("modal") + 1}
style={{
userSelect: modal.id === state.current?.id ? undefined : "none",
}}
styles={{
title: {
fontSize: "1.25rem",
fontWeight: 500,
},
inner: {
display: modal.id === state.current?.id ? undefined : "none",
},
}}
trapFocus={modal.id === state.current?.id}
{...otherModalProps}
title={translateIfNecessary(t, modal.props.defaultTitle)}
opened={opened}
onClose={handleCloseModal}
>
{modal.reference.content}
</Modal>
);
};
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) => {
// void actually is undefined
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
context.openModalInner({ modal, innerProps, options: options ?? {} });
},
};
};
export const useConfirmModal = () => {
const { openModal } = useModalAction(ConfirmModal);
return {
openConfirmModal: (props: ConfirmModalProps) => openModal(props, { title: props.title }),
};
};