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?.(); }