Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@mantine/modals] TextInput modal #7203

Closed
Closed
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
23 changes: 19 additions & 4 deletions apps/mantine.dev/src/pages/x/modals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ function Demo() {

## Confirm modal

@mantine/modals package includes special modal that can be used for confirmations.
Component includes confirm and cancel buttons and supports children to display additional
information about action. Use `openConfirmModal` function to open a confirm modal:
@mantine/modals package includes a special modal that can be used for confirmations.
This component includes confirm and cancel buttons and supports children to display additional
information about action. Use the `openConfirmModal` function to open a confirm modal:

<Demo data={ModalsDemos.confirm} />

Expand All @@ -46,7 +46,7 @@ information about action. Use `openConfirmModal` function to open a confirm moda
- `groupProps` – buttons [Group](/core/group/) props
- `labels` – cancel and confirm buttons labels, can be defined on ModalsProvider

Using this properties you can customize confirm modal to match current context requirements:
Using these properties you can customize confirm modal to match current context requirements:

<Demo data={ModalsDemos.confirmCustomize} />

Expand All @@ -64,6 +64,21 @@ function Demo() {
}
```

## TextInput modal
@mantine/modals package also includes special modal that can be used to capture user text input.
This component is an extension of the confirm modal, supporting the same properties as well as a few additional
properties:

- `onConfirm` - same as confirm modal, but also includes the user's input from the text field as an argument
- `topSection` - used to render content above the text input
- `bottomSection` - used to render content below the text input
- `inputProps` – text input props
- `onInputChange` - called when the text input value changes
- `initialValue` - initial value of the text input
- `autofocus` - whether to autofocus the input field when the modal opens (true by default)

<Demo data={ModalsDemos.textInput} />

## Context modals

You can define any amount of modals in ModalsProvider context:
Expand Down
54 changes: 54 additions & 0 deletions packages/@docs/demos/src/demos/modals/Modals.demo.textInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Button, Text } from '@mantine/core';
import { modals } from '@mantine/modals';
import { notifications } from '@mantine/notifications';
import { MantineDemo } from '@mantinex/demo';

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

function Demo() {
const openModal = () =>
modals.openTextInputModal({
modalId: 'test-id',
title: 'Please enter your name',
topSection: <Text size="sm">Top section is rendered here.</Text>,
bottomSection: <Text size="sm">Bottom section is rendered here.</Text>,
onCancel: () => console.log('Cancel'),
onConfirm: (value) => console.log(\`Confirm with value \${value}\`),
});

return <Button onClick={openModal}>Open text input modal</Button>;
}
`;

function Demo() {
const openModal = () =>
modals.openTextInputModal({
modalId: 'test-id',
title: 'Please enter your name',
topSection: <Text size="sm">Top section is rendered here.</Text>,
bottomSection: <Text size="sm">Bottom section is rendered here.</Text>,
onCancel: () =>
notifications.show({
title: 'Canceled',
message: 'TextInput modal was canceled',
color: 'gray',
}),
onConfirm: (value) =>
notifications.show({
title: 'Confirmed',
message: `TextInputModal was confirmed with input ${value}`,
color: 'teal',
}),
});

return <Button onClick={openModal}>Open text input modal</Button>;
}

export const textInput: MantineDemo = {
type: 'code',
centered: true,
component: Demo,
code,
};
5 changes: 5 additions & 0 deletions packages/@docs/demos/src/demos/modals/Modals.demos.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const Demo_confirm = {
render: renderDemo(demos.confirm),
};

export const Demo_textInput = {
name: '⭐ Demo: textInput',
render: renderDemo(demos.textInput),
};

export const Demo_context = {
name: '⭐ Demo: context',
render: renderDemo(demos.context),
Expand Down
1 change: 1 addition & 0 deletions packages/@docs/demos/src/demos/modals/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { confirm } from './Modals.demo.confirm';
export { textInput } from './Modals.demo.textInput';
export { context } from './Modals.demo.context';
export { confirmCustomize } from './Modals.demo.confirmCustomize';
export { multipleSteps } from './Modals.demo.multipleSteps';
Expand Down
84 changes: 84 additions & 0 deletions packages/@mantine/modals/src/ModalsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
ModalSettings,
OpenConfirmModal,
OpenContextModal,
OpenTextInputModal,
} from './context';
import { useModalsEvents } from './events';
import { modalsReducer } from './reducer';
import { TextInputModal } from './TextInputModal';

export interface ModalsProviderProps {
/** Your app */
Expand Down Expand Up @@ -67,6 +69,55 @@ function separateConfirmModalProps(props: OpenConfirmModal) {
};
}

function separateTextInputModalProps(props: OpenTextInputModal) {
if (!props) {
return { textInputProps: {}, modalProps: {} };
}

const {
id,
topSection,
bottomSection,
onCancel,
onConfirm,
closeOnConfirm,
closeOnCancel,
cancelProps,
confirmProps,
groupProps,
labels,
inputProps,
onInputChange,
initialValue,
autofocus,
...others
} = props;

return {
textInputProps: {
id,
topSection,
bottomSection,
onCancel,
onConfirm,
closeOnConfirm,
closeOnCancel,
cancelProps,
confirmProps,
groupProps,
labels,
inputProps,
onInputChange,
initialValue,
autofocus,
},
modalProps: {
id,
...others,
},
};
}

export function ModalsProvider({ children, modalProps, labels, modals }: ModalsProviderProps) {
const [state, dispatch] = useReducer(modalsReducer, { modals: [], current: null });
const stateRef = useRef(state);
Expand Down Expand Up @@ -112,6 +163,22 @@ export function ModalsProvider({ children, modalProps, labels, modals }: ModalsP
[dispatch]
);

const openTextInputModal = useCallback(
({ modalId, ...props }: OpenTextInputModal) => {
const id = modalId || randomId();
dispatch({
type: 'OPEN',
modal: {
id,
type: 'textInput',
props,
},
});
return id;
},
[dispatch]
);

const openContextModal = useCallback(
(modal: string, { modalId, ...props }: OpenContextModal) => {
const id = modalId || randomId();
Expand Down Expand Up @@ -157,6 +224,7 @@ export function ModalsProvider({ children, modalProps, labels, modals }: ModalsP
useModalsEvents({
openModal,
openConfirmModal,
openTextInputModal,
openContextModal: ({ modal, ...payload }: any) => openContextModal(modal, payload),
closeModal,
closeContextModal: closeModal,
Expand All @@ -169,6 +237,7 @@ export function ModalsProvider({ children, modalProps, labels, modals }: ModalsP
modals: state.modals,
openModal,
openConfirmModal,
openTextInputModal,
openContextModal,
closeModal,
closeContextModal: closeModal,
Expand Down Expand Up @@ -204,6 +273,21 @@ export function ModalsProvider({ children, modalProps, labels, modals }: ModalsP
),
};
}
case 'textInput': {
const { modalProps: separatedModalProps, textInputProps: separatedConfirmProps } =
separateTextInputModalProps(currentModal.props);

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

Expand Down
48 changes: 48 additions & 0 deletions packages/@mantine/modals/src/TextInputModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { Box, TextInput, TextInputProps } from '@mantine/core';
import { ConfirmModal, ConfirmModalProps } from './ConfirmModal';

export interface TextInputModalProps extends Omit<ConfirmModalProps, 'onConfirm' | 'children'> {
topSection?: React.ReactNode;
bottomSection?: React.ReactNode;
onConfirm?: (value: string) => void;
inputProps?: TextInputProps & React.ComponentPropsWithoutRef<'input'>;
onInputChange?: (value: string) => void;
initialValue?: string;
autofocus?: boolean;
}

export function TextInputModal({
topSection,
bottomSection,
onConfirm,
inputProps,
onInputChange,
initialValue = '',
autofocus = true,
...confirmModalProps
}: TextInputModalProps) {
const [value, setValue] = useState(initialValue);

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setValue(newValue);
typeof onInputChange === 'function' && onInputChange(value);
};

return (
<ConfirmModal {...confirmModalProps} onConfirm={() => onConfirm?.(value)}>
<>
{topSection && <Box mb="md">{topSection}</Box>}
<TextInput
mb="sm"
value={value}
onChange={handleInputChange}
{...(autofocus ? { 'data-autofocus': true } : {})}
{...inputProps}
/>
{bottomSection && <Box mb="md">{bottomSection}</Box>}
</>
</ConfirmModal>
);
}
4 changes: 4 additions & 0 deletions packages/@mantine/modals/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createContext, ReactNode } from 'react';
import { ModalProps } from '@mantine/core';
import type { ConfirmModalProps } from './ConfirmModal';
import type { TextInputModalProps } from './TextInputModal';

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

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

export interface OpenConfirmModal extends ModalSettings, ConfirmModalProps {}
export interface OpenTextInputModal extends ModalSettings, TextInputModalProps {}
export interface OpenContextModal<CustomProps extends Record<string, any> = {}>
extends ModalSettings {
innerProps: CustomProps;
Expand All @@ -21,12 +23,14 @@ export interface ContextModalProps<T extends Record<string, any> = {}> {
export type ModalState =
| { id: string; props: ModalSettings; type: 'content' }
| { id: string; props: OpenConfirmModal; type: 'confirm' }
| { id: string; props: OpenTextInputModal; type: 'textInput' }
| { id: string; props: OpenContextModal; type: 'context'; ctx: string };

export interface ModalsContextProps {
modals: ModalState[];
openModal: (props: ModalSettings) => string;
openConfirmModal: (props: OpenConfirmModal) => string;
openTextInputModal: (props: OpenTextInputModal) => string;
openContextModal: <TKey extends MantineModal>(
modal: TKey,
props: OpenContextModal<Parameters<MantineModals[TKey]>[0]['innerProps']>
Expand Down
10 changes: 10 additions & 0 deletions packages/@mantine/modals/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
ModalSettings,
OpenConfirmModal,
OpenContextModal,
OpenTextInputModal,
} from './context';

type ModalsEvents = {
openModal: (payload: ModalSettings) => string;
openConfirmModal: (payload: OpenConfirmModal) => string;
openTextInputModal: (payload: OpenTextInputModal) => string;
openContextModal: <TKey extends MantineModal>(
payload: OpenContextModal<Parameters<MantineModals[TKey]>[0]['innerProps']> & { modal: TKey }
) => string;
Expand All @@ -36,6 +38,12 @@ export const openConfirmModal: ModalsEvents['openConfirmModal'] = (payload) => {
return id;
};

export const openTextInputModal: ModalsEvents['openTextInputModal'] = (payload) => {
const id = payload.modalId || randomId();
createEvent('openTextInputModal')({ ...payload, modalId: id });
return id;
};

export const openContextModal: ModalsEvents['openContextModal'] = <TKey extends MantineModal>(
payload: OpenContextModal<Parameters<MantineModals[TKey]>[0]['innerProps']> & { modal: TKey }
) => {
Expand Down Expand Up @@ -63,6 +71,7 @@ export const modals: {
close: ModalsEvents['closeModal'];
closeAll: ModalsEvents['closeAllModals'];
openConfirmModal: ModalsEvents['openConfirmModal'];
openTextInputModal: ModalsEvents['openTextInputModal'];
openContextModal: ModalsEvents['openContextModal'];
updateModal: ModalsEvents['updateModal'];
updateContextModal: ModalsEvents['updateContextModal'];
Expand All @@ -71,6 +80,7 @@ export const modals: {
close: closeModal,
closeAll: closeAllModals,
openConfirmModal,
openTextInputModal,
openContextModal,
updateModal,
updateContextModal,
Expand Down
1 change: 1 addition & 0 deletions packages/@mantine/modals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
closeModal,
closeAllModals,
openConfirmModal,
openTextInputModal,
openContextModal,
updateModal,
updateContextModal,
Expand Down
2 changes: 1 addition & 1 deletion packages/@mantine/modals/src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface UpdateAction {
}

function handleCloseModal(modal: ModalState, canceled?: boolean) {
if (canceled && modal.type === 'confirm') {
if (canceled && (modal.type === 'confirm' || modal.type === 'textInput')) {
modal.props.onCancel?.();
}

Expand Down
Loading