From 65fc4a70b1149b2bb71e9ada4fce5057f251acc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 10 Feb 2026 15:26:24 -0300 Subject: [PATCH] feat: add optional fullscreen button to Modal --- src/Modal/ModalDialog.tsx | 45 +++++++++--- src/Modal/_ModalDialog.scss | 2 +- src/Modal/messages.ts | 5 ++ src/Modal/modal-dialog.mdx | 1 + src/Modal/tests/ModalDialog.test.tsx | 105 +++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 13 deletions(-) diff --git a/src/Modal/ModalDialog.tsx b/src/Modal/ModalDialog.tsx index 0d0f71c1f4..c73bd01ea2 100644 --- a/src/Modal/ModalDialog.tsx +++ b/src/Modal/ModalDialog.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { useMediaQuery } from 'react-responsive'; import { useIntl } from 'react-intl'; import ModalLayer from './ModalLayer'; + // @ts-ignore for now - this needs to be converted to TypeScript import ModalCloseButton from './ModalCloseButton'; import ModalDialogHeader from './ModalDialogHeader'; @@ -17,7 +18,8 @@ import ModalDialogHero from './ModalDialogHero'; import Icon from '../Icon'; import IconButton from '../IconButton'; -import { Close } from '../../icons'; +import useToggle from '../hooks/useToggleHook'; +import { Close, FullscreenExit, Fullscreen } from '../../icons'; import messages from './messages'; interface Props { @@ -39,6 +41,10 @@ interface Props { closeLabel?: string; /** Specifies class name to append to the base element */ className?: string; + /** Renders the fullscreen icon button in the top right of the dialog box. It will not be redered (even if set to + * true) if the dialog is fullscreen for another reason (e.g. isFullscreenOnMobile is true and the viewport is + * mobile, or the dialog size is already set to fullscreen) */ + hasFullscreenButton?: boolean; /** * Determines where a scrollbar should appear if a modal is too large for the * viewport. When false, the ``ModalDialog``. Body receives a scrollbar, when true @@ -69,6 +75,7 @@ function ModalDialog({ variant = 'default', hasCloseButton = true, closeLabel, + hasFullscreenButton = false, isFullscreenScroll = false, className, isFullscreenOnMobile = false, @@ -79,7 +86,10 @@ function ModalDialog({ const intl = useIntl(); const closeButtonText = closeLabel || intl.formatMessage(messages.closeButtonText); const isMobile = useMediaQuery({ query: '(max-width: 767.98px)' }); - const showFullScreen = (isFullscreenOnMobile && isMobile); + const alwaysFullscreen = (isFullscreenOnMobile && isMobile) || size === 'fullscreen'; + + const [isFullscreen, , , toggleFullscreen] = useToggle(alwaysFullscreen); + return (
- {hasCloseButton && ( -
- + {(hasCloseButton || hasFullscreenButton) && ( +
+ {hasFullscreenButton && !alwaysFullscreen && ( + + )} + {hasCloseButton && ( + + )}
)} {children} diff --git a/src/Modal/_ModalDialog.scss b/src/Modal/_ModalDialog.scss index 16b38e2907..939c8c054e 100644 --- a/src/Modal/_ModalDialog.scss +++ b/src/Modal/_ModalDialog.scss @@ -113,7 +113,7 @@ // Subcomponents -.pgn__modal-close-container { +.pgn__modal-title-buttons-container { position: absolute; z-index: 10; top: var(--pgn-spacing-dropdown-close-container-top); diff --git a/src/Modal/messages.ts b/src/Modal/messages.ts index fa54a15c36..c5486b3659 100644 --- a/src/Modal/messages.ts +++ b/src/Modal/messages.ts @@ -6,6 +6,11 @@ const messages = defineMessages({ defaultMessage: 'Close', description: 'Accessible name for the close button in the modal dialog', }, + fullscreenButtonText: { + id: 'pgn.Modal.fullscreenButton', + defaultMessage: 'Fullscreen', + description: 'Accessible name for the fullscreen button in the modal dialog', + }, }); export default messages; diff --git a/src/Modal/modal-dialog.mdx b/src/Modal/modal-dialog.mdx index 5d4993abfb..0c5b345d13 100644 --- a/src/Modal/modal-dialog.mdx +++ b/src/Modal/modal-dialog.mdx @@ -46,6 +46,7 @@ label for the dialog element. size={modalSize} variant={modalVariant} hasCloseButton + hasFullscreenButton isFullscreenOnMobile isOverflowVisible={false} > diff --git a/src/Modal/tests/ModalDialog.test.tsx b/src/Modal/tests/ModalDialog.test.tsx index 21ff3c92a9..e02fed1054 100644 --- a/src/Modal/tests/ModalDialog.test.tsx +++ b/src/Modal/tests/ModalDialog.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { IntlProvider } from 'react-intl'; import ModalDialog from '../ModalDialog'; @@ -58,6 +59,110 @@ describe('ModalDialog', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); + + it('renders a dialog with hasFullscreenButton', async () => { + const user = userEvent.setup(); + const onClose = jest.fn(); + render( + + + +

The content

+
+
+
, + ); + const dialogNode = screen.getByRole('dialog'); + + expect(dialogNode).toBeInTheDocument(); + + const fullscreenButton = screen.getByRole('button', { name: 'Fullscreen' }); + expect(fullscreenButton).toBeInTheDocument(); + expect(fullscreenButton).toHaveAttribute('aria-label', 'Fullscreen'); + + await user.click(fullscreenButton); + + expect(dialogNode).toHaveClass('pgn__modal-fullscreen'); + expect(dialogNode).not.toHaveClass('pgn__modal-md'); + + await user.click(fullscreenButton); + + expect(dialogNode).toHaveClass('pgn__modal-md'); + expect(dialogNode).not.toHaveClass('pgn__modal-fullscreen'); + }); + + it('should not render fullscreen button if isFullscreenOnMobile is true and viewport is mobile', async () => { + const onClose = jest.fn(); + + // Mock useMediaQuery + // eslint-disable-next-line global-require + const reactResponsiveUseMediaQuery = require('react-responsive'); + jest.spyOn(reactResponsiveUseMediaQuery, 'useMediaQuery').mockImplementation(() => true); + + render( + + + +

The content

+
+
+
, + ); + const dialogNode = screen.getByRole('dialog'); + + expect(dialogNode).toBeInTheDocument(); + + const fullscreenButton = screen.queryByRole('button', { name: 'Fullscreen' }); + expect(fullscreenButton).not.toBeInTheDocument(); + }); + + it('should not render fullscreen button if size is fullscreen', async () => { + const onClose = jest.fn(); + + render( + + + +

The content

+
+
+
, + ); + const dialogNode = screen.getByRole('dialog'); + + expect(dialogNode).toBeInTheDocument(); + + const fullscreenButton = screen.queryByRole('button', { name: 'Fullscreen' }); + expect(fullscreenButton).not.toBeInTheDocument(); + }); }); describe('ModalDialog with Hero', () => {