From 820e243e08f4f150e2e549b933da228cb60f65f3 Mon Sep 17 00:00:00 2001 From: jonathan Date: Fri, 9 Jan 2026 18:18:04 -0300 Subject: [PATCH 1/2] feat(organisms): modal multiple --- src/documentation/pages/Organisms/Modals.tsx | 3 + src/organisms/Modals/Modal/Modal.tsx | 102 ++--------------- .../Modals/Modal/ModalContentBase.tsx | 103 +++++++++++++++++ .../Modals/ModalAlert/ModalAlert.tsx | 60 +--------- .../Modals/ModalAlert/ModalAlertContent.tsx | 62 +++++++++++ .../Modals/ModalMultiple/ModalMultiple.tsx | 105 ++++++++++++++++++ src/organisms/Modals/types.d.ts | 18 +++ 7 files changed, 308 insertions(+), 145 deletions(-) create mode 100644 src/organisms/Modals/Modal/ModalContentBase.tsx create mode 100644 src/organisms/Modals/ModalAlert/ModalAlertContent.tsx create mode 100644 src/organisms/Modals/ModalMultiple/ModalMultiple.tsx diff --git a/src/documentation/pages/Organisms/Modals.tsx b/src/documentation/pages/Organisms/Modals.tsx index a4d148d0..c065a1ad 100644 --- a/src/documentation/pages/Organisms/Modals.tsx +++ b/src/documentation/pages/Organisms/Modals.tsx @@ -4,6 +4,9 @@ import { ModalAlertDemo, ModalDemo } from '@/documentation/components/Organisms/ export const ViewModals = (): JSX.Element => { return ( <> +
+

ejemplo multiple

+
Modales Para los modales, tenemos dos tipos de componentes: Modal y ModalAlert. Cada uno tiene sus{' '} diff --git a/src/organisms/Modals/Modal/Modal.tsx b/src/organisms/Modals/Modal/Modal.tsx index 127b1495..24a27903 100644 --- a/src/organisms/Modals/Modal/Modal.tsx +++ b/src/organisms/Modals/Modal/Modal.tsx @@ -1,15 +1,7 @@ -import { - Box, - Modal as ChakraModal, - ModalCloseButton, - ModalContent as ChakraModalContent, - ModalHeader, - ModalOverlay, - useMediaQuery, -} from '@chakra-ui/react' +import { Modal as ChakraModal, ModalOverlay } from '@chakra-ui/react' -import { vars } from '@/theme' import { IModal } from '../types' +import { ModalContentBase } from './ModalContentBase' export const uiKitModalIsDesktop = 641 @@ -25,11 +17,6 @@ export const Modal = ({ fixedButtons = false, autoFocus = false, }: IModal): JSX.Element => { - const py = '32px' - const px = '24px' - - const [isDesktop] = useMediaQuery(`(min-width: ${uiKitModalIsDesktop}px)`) - const isInside = scrollBehavior === 'inside' || fixedButtons return ( @@ -42,83 +29,18 @@ export const Modal = ({ onClose={onClose} scrollBehavior={isInside ? 'inside' : 'outside'} autoFocus={autoFocus} + blockScrollOnMount={false} > - - - {title} - - {closeOnOverlayClick && ( - - )} - {fixedSubtitle?.trim() && ( - - {fixedSubtitle} - - )} - {children} - + ) diff --git a/src/organisms/Modals/Modal/ModalContentBase.tsx b/src/organisms/Modals/Modal/ModalContentBase.tsx new file mode 100644 index 00000000..d454a894 --- /dev/null +++ b/src/organisms/Modals/Modal/ModalContentBase.tsx @@ -0,0 +1,103 @@ +import { + Box, + ModalCloseButton, + ModalContent as ChakraModalContent, + ModalHeader, + useMediaQuery, +} from '@chakra-ui/react' + +import { vars } from '@/theme' +import { IModalContentBase } from '../types' +import { uiKitModalIsDesktop } from './Modal' + +export const ModalContentBase = ({ + isInside, + fixedButtons, + withoutMargin, + title, + closeOnOverlayClick, + fixedSubtitle, + children, +}: IModalContentBase): JSX.Element => { + const [isDesktop] = useMediaQuery(`(min-width: ${uiKitModalIsDesktop}px)`) + const py = '32px' + const px = '24px' + + return ( + + + {title} + + {closeOnOverlayClick && ( + + )} + {fixedSubtitle?.trim() && ( + + {fixedSubtitle} + + )} + {children} + + ) +} diff --git a/src/organisms/Modals/ModalAlert/ModalAlert.tsx b/src/organisms/Modals/ModalAlert/ModalAlert.tsx index a450610f..535e1f8d 100644 --- a/src/organisms/Modals/ModalAlert/ModalAlert.tsx +++ b/src/organisms/Modals/ModalAlert/ModalAlert.tsx @@ -1,17 +1,9 @@ -import { - Box, - Modal as ChakraModal, - ModalBody, - ModalContent, - ModalOverlay, - useMediaQuery, -} from '@chakra-ui/react' +import { Box, Modal as ChakraModal, ModalOverlay } from '@chakra-ui/react' import { IModalAlert } from '../types' -import { Loading } from './Loading' -import { alertColorStates } from '@/organisms/Alerts/utils/alertStates' import { vars } from '@/theme' +import { ModalAlertContent } from './ModalAlertContent' export const ModalAlertNew = ({ autoFocus = false, @@ -23,8 +15,6 @@ export const ModalAlertNew = ({ description, status, }: IModalAlert): JSX.Element => { - const [isDesktop] = useMediaQuery('(min-width: 641px)') - return ( <> - - - {type === 'loading' ? ( - - ) : ( - - {alertColorStates[status ?? 'info'].icon} - - )} - {title && ( - - {title} - - )} - {description && ( - - {description} - - )} - - {type !== 'loading' && children ? children : <>} - + + {children} + ) diff --git a/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx b/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx new file mode 100644 index 00000000..0606742c --- /dev/null +++ b/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx @@ -0,0 +1,62 @@ +import { Box, ModalBody, ModalContent, useMediaQuery } from '@chakra-ui/react' + +import { IModalAlertContent } from '../types' + +import { Loading } from './Loading' +import { alertColorStates } from '@/organisms/Alerts/utils/alertStates' + +export const ModalAlertContent = ({ + type, + title, + description, + status, + children, +}: IModalAlertContent): JSX.Element => { + const [isDesktop] = useMediaQuery('(min-width: 641px)') + + return ( + + + {type === 'loading' ? ( + + ) : ( + + {alertColorStates[status ?? 'info'].icon} + + )} + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + + {type !== 'loading' && children ? children : <>} + + ) +} diff --git a/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx b/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx new file mode 100644 index 00000000..5cf15afd --- /dev/null +++ b/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx @@ -0,0 +1,105 @@ +import { Modal as ChakraModal, ModalOverlay } from '@chakra-ui/react' + +import { ModalContentBase } from '../Modal/ModalContentBase' +import { ModalAlertContent } from '../ModalAlert/ModalAlertContent' +import { IModalAlert, IModal } from '../types' + +export interface ModalDefaultProps { + closeOnOverlayClick?: IModal['closeOnOverlayClick'] + fixedSubtitle?: IModal['fixedSubtitle'] + withoutMargin?: IModal['withoutMargin'] + scrollBehavior?: IModal['scrollBehavior'] + fixedButtons?: IModal['fixedButtons'] +} + +export interface ModalAlertProps { + status?: IModalAlert['status'] + description?: IModalAlert['description'] +} + +export interface ModalMultipleProps extends ModalDefaultProps, ModalAlertProps { + type: 'modal' | 'modalAlert' | 'modalLoading' + isOpen: boolean + onClose: () => void + autoFocus?: boolean + children?: React.ReactNode + title?: string +} + +export const ModalMultiple = ({ + autoFocus = false, + type, + isOpen, + onClose, + children, + title, + description, + closeOnOverlayClick = true, + fixedSubtitle, + withoutMargin = false, + scrollBehavior = 'outside', + fixedButtons = false, + status, +}: ModalMultipleProps): JSX.Element => { + const isInside = scrollBehavior === 'inside' || fixedButtons + + const configDifferent: Record< + 'modal' | 'modalAlert' | 'modalLoading', + { + closeOnOverlayClick: boolean + closeOnEsc: boolean + scrollBehavior?: 'outside' | 'inside' + } + > = { + modal: { + closeOnOverlayClick, + closeOnEsc: closeOnOverlayClick, + scrollBehavior: isInside ? 'inside' : 'outside', + }, + modalAlert: { + closeOnOverlayClick: false, + closeOnEsc: false, + scrollBehavior: 'outside', + }, + modalLoading: { + closeOnOverlayClick: false, + closeOnEsc: false, + scrollBehavior: 'outside', + }, + } + + return ( + <> + + + {type === 'modal' ? ( + + ) : ( + + )} + + + ) +} diff --git a/src/organisms/Modals/types.d.ts b/src/organisms/Modals/types.d.ts index 4a0afb12..e3e4455b 100644 --- a/src/organisms/Modals/types.d.ts +++ b/src/organisms/Modals/types.d.ts @@ -16,6 +16,16 @@ export interface IModal { autoFocus?: boolean } +export interface IModalContentBase { + isInside: boolean + fixedButtons: IModal['fixedButtons'] + withoutMargin: IModal['withoutMargin'] + title?: IModal['title'] + closeOnOverlayClick: IModal['closeOnOverlayClick'] + fixedSubtitle?: IModal['fixedSubtitle'] + children: React.ReactNode +} + export interface IModalButtons { children: React.ReactNode buttonsCenter?: boolean @@ -35,3 +45,11 @@ export interface IModalAlert { type: 'info' | 'loading' status?: 'success' | 'error' | 'warning' | 'info' } + +export interface IModalAlertContent { + type: IModalAlert['type'] + title?: IModalAlert['title'] + description?: IModalAlert['description'] + status?: IModalAlert['status'] + children: React.ReactNode +} From 96c9e6c1f0ee49739b64b1f269775d3b41d27170 Mon Sep 17 00:00:00 2001 From: jonathan Date: Wed, 14 Jan 2026 15:45:38 -0300 Subject: [PATCH 2/2] feat(organisms): modal multiple --- .../components/Organisms/Modals.tsx | 39 ++++ src/documentation/pages/Organisms/Modals.tsx | 75 ++++++- src/index.ts | 2 + src/organisms/Modals/Modal/Modal.tsx | 48 +++-- .../Modals/Modal/ModalContentBase.tsx | 53 +---- src/organisms/Modals/Modal/useModalConfig.ts | 69 +++++++ .../Modals/ModalAlert/ModalAlert.tsx | 13 +- .../Modals/ModalAlert/ModalAlertContent.tsx | 13 +- .../Modals/ModalAlert/useModalAlertConfig.ts | 28 +++ .../ModalMultiple/ModalMultiple.test.tsx | 181 +++++++++++++++++ .../Modals/ModalMultiple/ModalMultiple.tsx | 185 +++++++++++------- .../ModalMultiple/useModalMultipleConfig.ts | 45 +++++ src/organisms/Modals/index.ts | 1 + src/organisms/Modals/types.d.ts | 9 +- 14 files changed, 601 insertions(+), 160 deletions(-) create mode 100644 src/organisms/Modals/Modal/useModalConfig.ts create mode 100644 src/organisms/Modals/ModalAlert/useModalAlertConfig.ts create mode 100644 src/organisms/Modals/ModalMultiple/ModalMultiple.test.tsx create mode 100644 src/organisms/Modals/ModalMultiple/useModalMultipleConfig.ts diff --git a/src/documentation/components/Organisms/Modals.tsx b/src/documentation/components/Organisms/Modals.tsx index d2411f6e..8abe6e27 100644 --- a/src/documentation/components/Organisms/Modals.tsx +++ b/src/documentation/components/Organisms/Modals.tsx @@ -8,6 +8,8 @@ import { ModalButtons, ModalContent, } from '@/organisms/Modals/' +import { ModalMultiple, ModalMultipleProps } from '@/organisms/Modals/ModalMultiple/ModalMultiple' +import { useState } from 'react' export const ModalDemo = ({ type, @@ -298,3 +300,40 @@ export const ModalAlertDemo = ({ ) } + +export const ModalMultipleDemo = (): JSX.Element => { + const { isOpen, onOpen, onClose } = useDisclosure() + const [type, setType] = useState('modal') + return ( + <> + ModalMultiple + + {type === 'modal' ? ( + +

alumnos, además de definir el uso de la plataforma de estudio.

+ + setType('modal')}>Guardar + onClose()}>Cancelar + +
+ ) : ( + + setType('modal')}> + Aceptar + + onClose()}> + Cancelar + + + )} +
+ + ) +} diff --git a/src/documentation/pages/Organisms/Modals.tsx b/src/documentation/pages/Organisms/Modals.tsx index c065a1ad..a05480ba 100644 --- a/src/documentation/pages/Organisms/Modals.tsx +++ b/src/documentation/pages/Organisms/Modals.tsx @@ -1,12 +1,13 @@ import { MyHeading, MyText, MyTitle, Code, ListComponent } from '@/documentation/components' -import { ModalAlertDemo, ModalDemo } from '@/documentation/components/Organisms/Modals' +import { + ModalAlertDemo, + ModalDemo, + ModalMultipleDemo, +} from '@/documentation/components/Organisms/Modals' export const ViewModals = (): JSX.Element => { return ( <> -
-

ejemplo multiple

-
Modales Para los modales, tenemos dos tipos de componentes: Modal y ModalAlert. Cada uno tiene sus{' '} @@ -134,6 +135,72 @@ export function View(){ withoutDescription /> + Tipo ModalMultiple + + Es un componente unificador que permite renderizar dos tipos de modal distintos dentro de un + mismo flujo:
+
modal → Modal tradicional (contenido libre, cabecera, footer, + botones, scroll). +
modalAlert / modalLoading → Modal de alerta o de carga, con + contenido reducido y foco en la acción del usuario.
+
Está pensado para casos donde el estado del modal cambia (por ejemplo, + confirmaciones, advertencias o pasos intermedios) sin necesidad de cerrar y volver a abrir + otro modal. +
+ + + + ('modal') + + return ( + <> + ModalMultiple + + {type === 'modal' ? ( + +

alumnos, además de definir el uso de la plataforma de estudio.

+ + setType('modalAlert')}>Guardar + onClose()}>Cancelar + +
+ ) : ( + + setType('modal')}> + Aceptar + + onClose()}> + Cancelar + + + )} +
+ ) +}`} + /> ) } diff --git a/src/index.ts b/src/index.ts index a3358a90..015fff88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,8 @@ export { ModalButtons, ModalContent, ModalAlertButtons, + ModalMultiple, + ModalMultipleProps, } from './organisms/Modals' export { ModalAlert } from './organisms/ModalAlert' export { Eventos } from './organisms/Events' diff --git a/src/organisms/Modals/Modal/Modal.tsx b/src/organisms/Modals/Modal/Modal.tsx index 24a27903..704572dd 100644 --- a/src/organisms/Modals/Modal/Modal.tsx +++ b/src/organisms/Modals/Modal/Modal.tsx @@ -1,9 +1,14 @@ -import { Modal as ChakraModal, ModalOverlay } from '@chakra-ui/react' +import { Modal as ChakraModal, ModalOverlay, ModalContent } from '@chakra-ui/react' import { IModal } from '../types' import { ModalContentBase } from './ModalContentBase' +import { useModalConfig } from './useModalConfig' export const uiKitModalIsDesktop = 641 +export const ModalPadding = { + py: '32px', + px: '24px', +} export const Modal = ({ children, @@ -19,29 +24,36 @@ export const Modal = ({ }: IModal): JSX.Element => { const isInside = scrollBehavior === 'inside' || fixedButtons + const modalConfig = useModalConfig({ + closeOnOverlayClick, + scrollBehavior, + fixedButtons, + withoutMargin, + }) + return ( - <> - - + + + - - + > + {children} +
+ + ) } diff --git a/src/organisms/Modals/Modal/ModalContentBase.tsx b/src/organisms/Modals/Modal/ModalContentBase.tsx index d454a894..0667990c 100644 --- a/src/organisms/Modals/Modal/ModalContentBase.tsx +++ b/src/organisms/Modals/Modal/ModalContentBase.tsx @@ -1,63 +1,22 @@ -import { - Box, - ModalCloseButton, - ModalContent as ChakraModalContent, - ModalHeader, - useMediaQuery, -} from '@chakra-ui/react' +import { Box, ModalCloseButton, ModalHeader, useMediaQuery } from '@chakra-ui/react' import { vars } from '@/theme' import { IModalContentBase } from '../types' import { uiKitModalIsDesktop } from './Modal' export const ModalContentBase = ({ - isInside, - fixedButtons, - withoutMargin, - title, + children, closeOnOverlayClick, fixedSubtitle, - children, + title, + withoutMargin, }: IModalContentBase): JSX.Element => { const [isDesktop] = useMediaQuery(`(min-width: ${uiKitModalIsDesktop}px)`) const py = '32px' const px = '24px' return ( - + <> )} {children} - + ) } diff --git a/src/organisms/Modals/Modal/useModalConfig.ts b/src/organisms/Modals/Modal/useModalConfig.ts new file mode 100644 index 00000000..7114eeed --- /dev/null +++ b/src/organisms/Modals/Modal/useModalConfig.ts @@ -0,0 +1,69 @@ +import { BoxProps, useMediaQuery } from '@chakra-ui/react' +import { vars } from '@/theme' +import { ModalPadding, uiKitModalIsDesktop } from '../Modal/Modal' + +interface ModalConfig { + closeOnOverlayClick: boolean + closeOnEsc: boolean + scrollBehavior?: 'outside' | 'inside' + contentProps: BoxProps +} + +interface UseModalConfigParams { + closeOnOverlayClick: boolean + scrollBehavior: 'outside' | 'inside' + fixedButtons: boolean + withoutMargin: boolean +} + +export const useModalConfig = ({ + closeOnOverlayClick, + scrollBehavior, + fixedButtons, + withoutMargin, +}: UseModalConfigParams): ModalConfig => { + const [isDesktop] = useMediaQuery(`(min-width: ${uiKitModalIsDesktop}px)`) + // fixedButtons requiere scroll interno + const isInsideScroll = scrollBehavior === 'inside' + const shouldForceInsideScroll = fixedButtons + const isInside = isInsideScroll || shouldForceInsideScroll + return { + closeOnOverlayClick, + closeOnEsc: closeOnOverlayClick, + scrollBehavior: isInside ? 'inside' : 'outside', + contentProps: { + maxH: isInside ? '100dvh' : 'auto', + minH: isDesktop ? '300px' : '100dvh', + padding: 0, + width: '100%', + animation: 'none', + sx: { + bgColor: vars('colors-neutral-white'), + borderRadius: isDesktop ? '8px' : 0, + mt: isDesktop ? '48px' : 0, + mb: isDesktop ? '48px' : 0, + marginX: isDesktop ? 'auto' : 0, + maxH: isInside ? 'calc(100dvh - 96px)' : 'auto', + maxWidth: isDesktop ? '690px' : '100%', + + ...(fixedButtons && { + '.uikit-modalContent': { + pb: 0, + }, + '.uikit-modalButtons': { + ...ModalPadding, + }, + }), + ...(withoutMargin && { + '.uikit-modalContent': { + pt: 0, + px: 0, + }, + '.uikit-modalButtons': { + px: ModalPadding.px, + }, + }), + }, + }, + } +} diff --git a/src/organisms/Modals/ModalAlert/ModalAlert.tsx b/src/organisms/Modals/ModalAlert/ModalAlert.tsx index 535e1f8d..b2c1df61 100644 --- a/src/organisms/Modals/ModalAlert/ModalAlert.tsx +++ b/src/organisms/Modals/ModalAlert/ModalAlert.tsx @@ -1,9 +1,9 @@ -import { Box, Modal as ChakraModal, ModalOverlay } from '@chakra-ui/react' +import { Box, Modal as ChakraModal, ModalContent, ModalOverlay } from '@chakra-ui/react' import { IModalAlert } from '../types' - import { vars } from '@/theme' import { ModalAlertContent } from './ModalAlertContent' +import { useModalAlertConfig } from './useModalAlertConfig' export const ModalAlertNew = ({ autoFocus = false, @@ -15,6 +15,7 @@ export const ModalAlertNew = ({ description, status, }: IModalAlert): JSX.Element => { + const modalConfig = useModalAlertConfig() return ( <> - - {children} - + + + {children} + + ) diff --git a/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx b/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx index 0606742c..74827029 100644 --- a/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx +++ b/src/organisms/Modals/ModalAlert/ModalAlertContent.tsx @@ -1,4 +1,4 @@ -import { Box, ModalBody, ModalContent, useMediaQuery } from '@chakra-ui/react' +import { Box, ModalBody, useMediaQuery } from '@chakra-ui/react' import { IModalAlertContent } from '../types' @@ -15,14 +15,7 @@ export const ModalAlertContent = ({ const [isDesktop] = useMediaQuery('(min-width: 641px)') return ( - + <> {type !== 'loading' && children ? children : <>} - + ) } diff --git a/src/organisms/Modals/ModalAlert/useModalAlertConfig.ts b/src/organisms/Modals/ModalAlert/useModalAlertConfig.ts new file mode 100644 index 00000000..50a96e37 --- /dev/null +++ b/src/organisms/Modals/ModalAlert/useModalAlertConfig.ts @@ -0,0 +1,28 @@ +import { BoxProps, useMediaQuery } from '@chakra-ui/react' + +import { uiKitModalIsDesktop } from '../Modal/Modal' + +interface ModalConfig { + closeOnOverlayClick: boolean + closeOnEsc: boolean + scrollBehavior?: 'outside' | 'inside' + contentProps: BoxProps +} + +export const useModalAlertConfig = (): ModalConfig => { + const [isDesktop] = useMediaQuery(`(min-width: ${uiKitModalIsDesktop}px)`) + + return { + closeOnOverlayClick: false, + closeOnEsc: false, + scrollBehavior: 'outside', + contentProps: { + borderRadius: '8px', + p: 0, + m: '10vh auto 0', + sx: { + maxWidth: isDesktop ? '589px' : '343px', + }, + }, + } +} diff --git a/src/organisms/Modals/ModalMultiple/ModalMultiple.test.tsx b/src/organisms/Modals/ModalMultiple/ModalMultiple.test.tsx new file mode 100644 index 00000000..d61f9e12 --- /dev/null +++ b/src/organisms/Modals/ModalMultiple/ModalMultiple.test.tsx @@ -0,0 +1,181 @@ +import { ChakraProvider } from '@chakra-ui/react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { ModalMultiple } from './ModalMultiple' + +// Mock de useMediaQuery para controlar el comportamiento responsivo +jest.mock('@chakra-ui/react', () => { + const originalModule = jest.requireActual('@chakra-ui/react') + return { + ...originalModule, + useMediaQuery: jest.fn(() => [true]), // Desktop por defecto + } +}) + +const renderWithChakra = (ui: React.ReactElement): any => { + return render({ui}) +} + +describe('ModalMultiple Component', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + /* -------------------------------------------------------------------------- */ + /* RENDERING */ + /* -------------------------------------------------------------------------- */ + + describe('Rendering', () => { + it('renders modal type correctly', () => { + renderWithChakra( + +
Modal Content
+
+ ) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByText('Modal Title')).toBeInTheDocument() + expect(screen.getByText('Modal Content')).toBeInTheDocument() + }) + + it('renders modalAlert type correctly', () => { + renderWithChakra( + + ) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByText('Alert Title')).toBeInTheDocument() + expect(screen.getByText('Alert Description')).toBeInTheDocument() + }) + + it('renders modalLoading type correctly', () => { + renderWithChakra( + + ) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByText('Loading Title')).toBeInTheDocument() + expect(screen.getByText('Loading Description')).toBeInTheDocument() + }) + + it('does not render when isOpen is false', () => { + renderWithChakra( + +
Modal Multiple
+
+ ) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(screen.queryByText('Hidden Modal')).not.toBeInTheDocument() + }) + }) + + /* -------------------------------------------------------------------------- */ + /* INTERACTION */ + /* -------------------------------------------------------------------------- */ + + describe('Interaction', () => { + it('calls onClose when close button is clicked (modal)', async () => { + const user = userEvent.setup() + const onCloseMock = jest.fn() + + renderWithChakra( + +
Modal Multiple
+
+ ) + + await user.click(screen.getByLabelText('Close')) + expect(onCloseMock).toHaveBeenCalledTimes(1) + }) + + it('does not close on escape when type is modalLoading', async () => { + const user = userEvent.setup() + const onCloseMock = jest.fn() + + renderWithChakra( + + ) + + await user.keyboard('{Escape}') + expect(onCloseMock).not.toHaveBeenCalled() + }) + }) + + /* -------------------------------------------------------------------------- */ + /* CHILDREN BEHAVIOR */ + /* -------------------------------------------------------------------------- */ + + describe('Children rendering', () => { + it('renders children for modal type', () => { + renderWithChakra( + +
Children Content
+
+ ) + + expect(screen.getByText('Children Content')).toBeInTheDocument() + }) + + it('renders children for modalLoading type', () => { + renderWithChakra( + +
Loading Children
+
+ ) + + expect(screen.getByText('Loading')).toBeInTheDocument() + }) + }) + + /* -------------------------------------------------------------------------- */ + /* RESPONSIVE BEHAVIOR */ + /* -------------------------------------------------------------------------- */ + + describe('Responsive behavior', () => { + it('applies desktop behavior', () => { + const useMediaQuery = jest.requireMock('@chakra-ui/react').useMediaQuery + useMediaQuery.mockReturnValue([true]) + + renderWithChakra( + +
Modal Multiple
+
+ ) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + }) + + it('applies mobile behavior', () => { + const useMediaQuery = jest.requireMock('@chakra-ui/react').useMediaQuery + useMediaQuery.mockReturnValue([false]) + + renderWithChakra( + +
Modal Multiple
+
+ ) + + expect(screen.getByRole('dialog')).toBeInTheDocument() + }) + }) +}) diff --git a/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx b/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx index 5cf15afd..5ff5c41f 100644 --- a/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx +++ b/src/organisms/Modals/ModalMultiple/ModalMultiple.tsx @@ -1,8 +1,13 @@ -import { Modal as ChakraModal, ModalOverlay } from '@chakra-ui/react' +import { Modal as ChakraModal, ModalOverlay, ModalContent } from '@chakra-ui/react' import { ModalContentBase } from '../Modal/ModalContentBase' import { ModalAlertContent } from '../ModalAlert/ModalAlertContent' import { IModalAlert, IModal } from '../types' +import { useModalMultipleConfig } from './useModalMultipleConfig' + +/* -------------------------------------------------------------------------- */ +/* TYPES */ +/* -------------------------------------------------------------------------- */ export interface ModalDefaultProps { closeOnOverlayClick?: IModal['closeOnOverlayClick'] @@ -12,94 +17,132 @@ export interface ModalDefaultProps { fixedButtons?: IModal['fixedButtons'] } -export interface ModalAlertProps { - status?: IModalAlert['status'] - description?: IModalAlert['description'] -} - -export interface ModalMultipleProps extends ModalDefaultProps, ModalAlertProps { - type: 'modal' | 'modalAlert' | 'modalLoading' +type BaseProps = ModalDefaultProps & { isOpen: boolean onClose: () => void autoFocus?: boolean - children?: React.ReactNode +} + +type ModalProps = BaseProps & { + type: 'modal' title?: string + children: React.ReactNode +} + +type ModalAlertProps = BaseProps & { + type: 'modalAlert' | 'modalLoading' + title?: string + description?: string + status?: IModalAlert['status'] + children?: React.ReactNode } -export const ModalMultiple = ({ - autoFocus = false, - type, - isOpen, - onClose, - children, - title, - description, - closeOnOverlayClick = true, - fixedSubtitle, - withoutMargin = false, - scrollBehavior = 'outside', - fixedButtons = false, - status, -}: ModalMultipleProps): JSX.Element => { - const isInside = scrollBehavior === 'inside' || fixedButtons +export type ModalMultipleProps = ModalProps | ModalAlertProps - const configDifferent: Record< - 'modal' | 'modalAlert' | 'modalLoading', - { - closeOnOverlayClick: boolean - closeOnEsc: boolean - scrollBehavior?: 'outside' | 'inside' - } - > = { - modal: { - closeOnOverlayClick, - closeOnEsc: closeOnOverlayClick, - scrollBehavior: isInside ? 'inside' : 'outside', - }, - modalAlert: { - closeOnOverlayClick: false, - closeOnEsc: false, - scrollBehavior: 'outside', - }, - modalLoading: { - closeOnOverlayClick: false, - closeOnEsc: false, - scrollBehavior: 'outside', - }, - } +/* -------------------------------------------------------------------------- */ +/* COMPONENT */ +/* -------------------------------------------------------------------------- */ +/** + * + * @example + * setOpen(false)} + title={type === 'modal' ? 'Confirmación' : '¿Seguro que deseas borrar esta pregunta?'} + status="info" + description="Por favor escoge otro horario." + > + {type === 'modal' ? ( + +

+ alumnos, además de definir el uso de la plataforma de estudio. +

+ + setType('modalAlert')}>Guardar + setOpen(false)}>Cancelar + +
+ ) : ( + + setType('modal')}> + Aceptar + + setOpen(false)}> + Cancelar + + + )} +
+ */ +export const ModalMultiple = (props: ModalMultipleProps): JSX.Element => { + const { + type, + isOpen, + onClose, + autoFocus = false, + children, + title, + closeOnOverlayClick = true, + fixedSubtitle, + withoutMargin = false, + scrollBehavior = 'outside', + fixedButtons = false, + } = props - return ( - <> - - - {type === 'modal' ? ( + const modalConfig = useModalMultipleConfig({ + type, + closeOnOverlayClick, + scrollBehavior, + fixedButtons, + withoutMargin, + }) + + const renderContent = (): JSX.Element | null => { + switch (type) { + case 'modal': + return ( - ) : ( + > + {children} + + ) + + case 'modalAlert': + case 'modalLoading': { + const { description, status } = props + + return ( - )} - - + > + {children} + + ) + } + } + } + + return ( + + + {renderContent()} + ) } diff --git a/src/organisms/Modals/ModalMultiple/useModalMultipleConfig.ts b/src/organisms/Modals/ModalMultiple/useModalMultipleConfig.ts new file mode 100644 index 00000000..1d5f92cb --- /dev/null +++ b/src/organisms/Modals/ModalMultiple/useModalMultipleConfig.ts @@ -0,0 +1,45 @@ +import { BoxProps } from '@chakra-ui/react' + +import { ModalMultipleProps } from './ModalMultiple' +import { useModalConfig } from '../Modal/useModalConfig' +import { useModalAlertConfig } from '../ModalAlert/useModalAlertConfig' + +interface ModalConfig { + closeOnOverlayClick: boolean + closeOnEsc: boolean + scrollBehavior?: 'outside' | 'inside' + contentProps: BoxProps +} + +interface UseModalMultipleConfigParams { + type: ModalMultipleProps['type'] + closeOnOverlayClick: boolean + scrollBehavior: 'outside' | 'inside' + fixedButtons: boolean + withoutMargin: boolean +} + +export const useModalMultipleConfig = ({ + type, + closeOnOverlayClick, + scrollBehavior, + fixedButtons, + withoutMargin, +}: UseModalMultipleConfigParams): ModalConfig => { + const modalConfig = useModalConfig({ + closeOnOverlayClick, + scrollBehavior, + fixedButtons, + withoutMargin, + }) + + const modalAlertConfig = useModalAlertConfig() + + switch (type) { + case 'modal': + return modalConfig + case 'modalAlert': + case 'modalLoading': + return modalAlertConfig + } +} diff --git a/src/organisms/Modals/index.ts b/src/organisms/Modals/index.ts index ea82de3a..6e91dc2e 100644 --- a/src/organisms/Modals/index.ts +++ b/src/organisms/Modals/index.ts @@ -1,3 +1,4 @@ export { Modal } from './Modal/Modal' export { ModalButtons, ModalContent } from './Modal/ModalButtons' export { ModalAlertNew, ModalAlertButtons } from './ModalAlert/ModalAlert' +export { ModalMultiple, ModalMultipleProps } from './ModalMultiple/ModalMultiple' diff --git a/src/organisms/Modals/types.d.ts b/src/organisms/Modals/types.d.ts index e3e4455b..952bf808 100644 --- a/src/organisms/Modals/types.d.ts +++ b/src/organisms/Modals/types.d.ts @@ -17,13 +17,12 @@ export interface IModal { } export interface IModalContentBase { - isInside: boolean - fixedButtons: IModal['fixedButtons'] - withoutMargin: IModal['withoutMargin'] - title?: IModal['title'] + children: React.ReactNode closeOnOverlayClick: IModal['closeOnOverlayClick'] + fixedButtons: IModal['fixedButtons'] fixedSubtitle?: IModal['fixedSubtitle'] - children: React.ReactNode + title?: IModal['title'] + withoutMargin: IModal['withoutMargin'] } export interface IModalButtons {