Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/mantine.dev/src/pages/x/modals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ And then open one of these modals with `modals.openContextModal` function.
By default `innerProps` and `modal` are not typesafe. You can add typesafety with a Typescript module declaration.

```tsx
import { ContextModalProps } from '@mantine/modals';
const TestModal = ({
context,
id,
Expand Down Expand Up @@ -165,6 +166,12 @@ To close all opened modals call `modals.closeAll()` function:

<Demo data={ModalsDemos.multipleSteps} />

By default, opening a new modal using `modals manager` replaces the currently opened modal. You can have multiple modals
open at the same time by setting `shouldReplaceExistingModal` to `false` on subsequent calls.

<Demo data={ModalsDemos.multiple} />


## Modal props

You can pass props down to the [Modal](/core/modal) component by adding them to the
Expand Down
81 changes: 81 additions & 0 deletions packages/@docs/demos/src/demos/modals/Modals.demo.multiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Button, Text } from '@mantine/core';
import { modals } from '@mantine/modals';
import { MantineDemo } from '@mantinex/demo';

const code = `
import { Button, Group, Text } from '@mantine/core';
import { modals } from '@mantine/modals';

function Demo() {
const openMultipleModals = () => {
modals.open({
title: 'First Modal',
children: (
<>
<Text size="sm" mb="md">
This modal will stay open when you open the next one.
</Text>
<Button
onClick={() => {
modals.open({
title: 'Second Modal',
children: (
<Text size="sm">
This modal is opened on top of the first one!
</Text>
),
shouldReplaceExistingModal: false,
});
}}
>
Open Second Modal
</Button>
</>
),
shouldReplaceExistingModal: false,
});
};

return (
<Button onClick={openMultipleModals}>
Open Multiple Modals
</Button>
);
}
`;

function Demo() {
const openMultipleModals = () => {
modals.open({
title: 'First Modal',
children: (
<>
<Text size="sm" mb="md">
This modal will stay open when you open the next one.
</Text>
<Button
onClick={() => {
modals.open({
title: 'Second Modal',
children: <Text size="sm">This modal is open on top of the first one!</Text>,
shouldReplaceExistingModal: false,
});
}}
>
Open Second Modal
</Button>
</>
),
shouldReplaceExistingModal: false,
});
};

return <Button onClick={openMultipleModals}>Open Multiple Modals</Button>;
}

export const multiple: MantineDemo = {
type: 'code',
centered: true,
component: Demo,
code,
};
1 change: 1 addition & 0 deletions packages/@docs/demos/src/demos/modals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { content } from './Modals.demo.content';
export { modalProps } from './Modals.demo.modalProps';
export { updateModal } from './Modals.demo.updateModal';
export { updateContextModal } from './Modals.demo.updateContextModal';
export { multiple } from './Modals.demo.multiple';
107 changes: 107 additions & 0 deletions packages/@mantine/modals/src/Modals.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,110 @@ export function UpdateExample() {
</ModalsProvider>
);
}

function MultipleModalsDemo() {
const modals = useModals();

const openMultipleModals = () => {
// Open first modal without replacing
modals.openModal({
title: 'First Modal',
children: (
<div>
<Text mb="md">
This is the first modal. Notice it stays open when you click the button below.
</Text>
<Button
onClick={() => {
// Open second modal on top
modals.openModal({
title: 'Second Modal',
children: (
<div>
<Text size="sm" mb="md">
This is the second modal, stacked on top of the first one!
</Text>
<Button
onClick={() => {
// Open third modal on top
modals.openModal({
title: 'Third Modal',
children: (
<Text size="sm">Three modals stacked! Each has its own overlay.</Text>
),
shouldReplaceExistingModal: false,
});
}}
>
Open Third Modal
</Button>
</div>
),
shouldReplaceExistingModal: false,
});
}}
>
Open Second Modal (Stacked)
</Button>
</div>
),
shouldReplaceExistingModal: false,
});
};

const openMixedModals = () => {
// Open regular modal
modals.openModal({
title: 'Step 1: Information',
children: (
<div>
<Text size="sm" mb="md">
This is a regular content modal.
</Text>
<Button
onClick={() => {
// Stack a confirm modal on top
modals.openConfirmModal({
title: 'Step 2: Confirm Action',
children: <Text size="sm">Do you want to proceed with this action?</Text>,
labels: { confirm: 'Yes, Continue', cancel: 'No, Go Back' },
onConfirm: () => {
modals.openModal({
title: 'Step 3: Complete',
children: <Text size="sm">Action completed successfully!</Text>,
shouldReplaceExistingModal: false,
});
},
shouldReplaceExistingModal: false,
});
}}
>
Next Step
</Button>
</div>
),
shouldReplaceExistingModal: false,
});
};

return (
<Group p={40}>
<Button onClick={openMultipleModals} color="blue">
Multiple Content Modals
</Button>
<Button onClick={openMixedModals} color="violet">
Confirmation modal opened within another modal
</Button>
</Group>
);
}

export function MultipleModals() {
return (
<ModalsProvider>
<MultipleModalsDemo />
</ModalsProvider>
);
}

MultipleModals.storyName = 'Multiple Modals (New Feature)';
118 changes: 64 additions & 54 deletions packages/@mantine/modals/src/ModalsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ModalsContext,
ModalsContextProps,
ModalSettings,
ModalState,
OpenConfirmModal,
OpenContextModal,
} from './context';
Expand Down Expand Up @@ -68,7 +69,7 @@ function separateConfirmModalProps(props: OpenConfirmModal) {
}

export function ModalsProvider({ children, modalProps, labels, modals }: ModalsProviderProps) {
const [state, dispatch] = useReducer(modalsReducer, { modals: [], current: null });
const [state, dispatch] = useReducer(modalsReducer, { modals: [], current: [] });
const stateRef = useRef(state);
stateRef.current = state;

Expand Down Expand Up @@ -178,63 +179,72 @@ export function ModalsProvider({ children, modalProps, labels, modals }: ModalsP
updateContextModal,
};

const getCurrentModal = () => {
const currentModal = stateRef.current.current;
switch (currentModal?.type) {
case 'context': {
const { innerProps, ...rest } = currentModal.props;
const ContextModal = modals![currentModal.ctx];

return {
modalProps: rest,
content: <ContextModal innerProps={innerProps} context={ctx} id={currentModal.id} />,
};
const getModalContent = useCallback(
(modal: ModalState) => {
switch (modal.type) {
case 'context': {
const { innerProps, ...rest } = modal.props;
const ContextModal = modals![modal.ctx];

return {
modalProps: rest,
content: <ContextModal innerProps={innerProps} context={ctx} id={modal.id} />,
};
}
case 'confirm': {
const { modalProps: separatedModalProps, confirmProps: separatedConfirmProps } =
separateConfirmModalProps(modal.props);

return {
modalProps: separatedModalProps,
content: (
<ConfirmModal
{...separatedConfirmProps}
id={modal.id}
labels={modal.props.labels || labels}
/>
),
};
}
case 'content': {
const { children: currentModalChildren, ...rest } = modal.props;

return {
modalProps: rest,
content: currentModalChildren,
};
}
default: {
return {
modalProps: {},
content: null,
};
}
}
case 'confirm': {
const { modalProps: separatedModalProps, confirmProps: separatedConfirmProps } =
separateConfirmModalProps(currentModal.props);

return {
modalProps: separatedModalProps,
content: (
<ConfirmModal
{...separatedConfirmProps}
id={currentModal.id}
labels={currentModal.props.labels || labels}
/>
),
};
}
case 'content': {
const { children: currentModalChildren, ...rest } = currentModal.props;

return {
modalProps: rest,
content: currentModalChildren,
};
}
default: {
return {
modalProps: {},
content: null,
};
}
}
};

const { modalProps: currentModalProps, content } = getCurrentModal();
},
[ctx, labels, modals]
);

return (
<ModalsContext.Provider value={ctx}>
<Modal
zIndex={getDefaultZIndex('modal') + 1}
{...modalProps}
{...currentModalProps}
opened={state.modals.length > 0}
onClose={() => closeModal(state.current?.id as any)}
>
{content}
</Modal>
{state.current.map((modal, index) => {
const { modalProps: currentModalProps, content } = getModalContent(modal);
const baseZIndex = getDefaultZIndex('modal');
const opened = true; // always opened because this will only render for current modals

return (
<Modal
key={modal.id}
zIndex={baseZIndex + index + 1}
{...modalProps}
{...currentModalProps}
opened={opened}
onClose={() => closeModal(modal.id)}
>
{content}
</Modal>
);
})}

{children}
</ModalsContext.Provider>
Expand Down
5 changes: 4 additions & 1 deletion packages/@mantine/modals/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { createContext, ReactNode } from 'react';
import { ModalProps } from '@mantine/core';
import type { ConfirmModalProps } from './ConfirmModal';

export type ModalSettings = Partial<Omit<ModalProps, 'opened'>> & { modalId?: string };
export type ModalSettings = Partial<Omit<ModalProps, 'opened'>> & {
modalId?: string;
shouldReplaceExistingModal?: boolean;
};

export type ConfirmLabels = Record<'confirm' | 'cancel', ReactNode>;

Expand Down
Loading
Loading