diff --git a/apps/mantine.dev/src/pages/x/modals.mdx b/apps/mantine.dev/src/pages/x/modals.mdx
index 285ed0e7f4..be7498012f 100644
--- a/apps/mantine.dev/src/pages/x/modals.mdx
+++ b/apps/mantine.dev/src/pages/x/modals.mdx
@@ -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:
@@ -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:
@@ -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)
+
+
+
## Context modals
You can define any amount of modals in ModalsProvider context:
diff --git a/packages/@docs/demos/src/demos/modals/Modals.demo.textInput.tsx b/packages/@docs/demos/src/demos/modals/Modals.demo.textInput.tsx
new file mode 100644
index 0000000000..b28c579e61
--- /dev/null
+++ b/packages/@docs/demos/src/demos/modals/Modals.demo.textInput.tsx
@@ -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: Top section is rendered here.,
+ bottomSection: Bottom section is rendered here.,
+ onCancel: () => console.log('Cancel'),
+ onConfirm: (value) => console.log(\`Confirm with value \${value}\`),
+ });
+
+ return ;
+}
+`;
+
+function Demo() {
+ const openModal = () =>
+ modals.openTextInputModal({
+ modalId: 'test-id',
+ title: 'Please enter your name',
+ topSection: Top section is rendered here.,
+ bottomSection: Bottom section is rendered here.,
+ 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 ;
+}
+
+export const textInput: MantineDemo = {
+ type: 'code',
+ centered: true,
+ component: Demo,
+ code,
+};
diff --git a/packages/@docs/demos/src/demos/modals/Modals.demos.story.tsx b/packages/@docs/demos/src/demos/modals/Modals.demos.story.tsx
index 8e52eecc64..36e8eee7c9 100644
--- a/packages/@docs/demos/src/demos/modals/Modals.demos.story.tsx
+++ b/packages/@docs/demos/src/demos/modals/Modals.demos.story.tsx
@@ -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),
diff --git a/packages/@docs/demos/src/demos/modals/index.ts b/packages/@docs/demos/src/demos/modals/index.ts
index 707b7e419e..368e7d0a36 100644
--- a/packages/@docs/demos/src/demos/modals/index.ts
+++ b/packages/@docs/demos/src/demos/modals/index.ts
@@ -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';
diff --git a/packages/@mantine/modals/src/ModalsProvider.tsx b/packages/@mantine/modals/src/ModalsProvider.tsx
index 5fa59d7925..ed60800690 100644
--- a/packages/@mantine/modals/src/ModalsProvider.tsx
+++ b/packages/@mantine/modals/src/ModalsProvider.tsx
@@ -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 */
@@ -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);
@@ -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();
@@ -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,
@@ -169,6 +237,7 @@ export function ModalsProvider({ children, modalProps, labels, modals }: ModalsP
modals: state.modals,
openModal,
openConfirmModal,
+ openTextInputModal,
openContextModal,
closeModal,
closeContextModal: closeModal,
@@ -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: (
+
+ ),
+ };
+ }
case 'content': {
const { children: currentModalChildren, ...rest } = currentModal.props;
diff --git a/packages/@mantine/modals/src/TextInputModal.tsx b/packages/@mantine/modals/src/TextInputModal.tsx
new file mode 100644
index 0000000000..75a406ffc2
--- /dev/null
+++ b/packages/@mantine/modals/src/TextInputModal.tsx
@@ -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 {
+ 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) => {
+ const newValue = event.target.value;
+ setValue(newValue);
+ typeof onInputChange === 'function' && onInputChange(value);
+ };
+
+ return (
+ onConfirm?.(value)}>
+ <>
+ {topSection && {topSection}}
+
+ {bottomSection && {bottomSection}}
+ >
+
+ );
+}
diff --git a/packages/@mantine/modals/src/context.ts b/packages/@mantine/modals/src/context.ts
index 2445481948..4a7662e2f8 100644
--- a/packages/@mantine/modals/src/context.ts
+++ b/packages/@mantine/modals/src/context.ts
@@ -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> & { modalId?: string };
export type ConfirmLabels = Record<'confirm' | 'cancel', ReactNode>;
export interface OpenConfirmModal extends ModalSettings, ConfirmModalProps {}
+export interface OpenTextInputModal extends ModalSettings, TextInputModalProps {}
export interface OpenContextModal = {}>
extends ModalSettings {
innerProps: CustomProps;
@@ -21,12 +23,14 @@ export interface ContextModalProps = {}> {
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: (
modal: TKey,
props: OpenContextModal[0]['innerProps']>
diff --git a/packages/@mantine/modals/src/events.ts b/packages/@mantine/modals/src/events.ts
index 071d840547..5433624241 100644
--- a/packages/@mantine/modals/src/events.ts
+++ b/packages/@mantine/modals/src/events.ts
@@ -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: (
payload: OpenContextModal[0]['innerProps']> & { modal: TKey }
) => string;
@@ -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'] = (
payload: OpenContextModal[0]['innerProps']> & { modal: TKey }
) => {
@@ -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'];
@@ -71,6 +80,7 @@ export const modals: {
close: closeModal,
closeAll: closeAllModals,
openConfirmModal,
+ openTextInputModal,
openContextModal,
updateModal,
updateContextModal,
diff --git a/packages/@mantine/modals/src/index.ts b/packages/@mantine/modals/src/index.ts
index 6129650f59..4eecb698f6 100644
--- a/packages/@mantine/modals/src/index.ts
+++ b/packages/@mantine/modals/src/index.ts
@@ -5,6 +5,7 @@ export {
closeModal,
closeAllModals,
openConfirmModal,
+ openTextInputModal,
openContextModal,
updateModal,
updateContextModal,
diff --git a/packages/@mantine/modals/src/reducer.ts b/packages/@mantine/modals/src/reducer.ts
index abfe6bd9aa..ec09e0e613 100644
--- a/packages/@mantine/modals/src/reducer.ts
+++ b/packages/@mantine/modals/src/reducer.ts
@@ -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?.();
}