From cc82fecb5cafd7b504767161702136793ecd9bea Mon Sep 17 00:00:00 2001 From: Pavel Klibani Date: Tue, 2 Jul 2024 10:55:02 +0200 Subject: [PATCH] Feat(web, web-twig, web-react): Hide close button in Modal --- .../src/components/Dialog/Dialog.tsx | 8 ++- .../src/components/Modal/ModalHeader.tsx | 9 ++- .../web-react/src/components/Modal/README.md | 39 ++++++++---- .../Modal/__tests__/ModalHeader.test.tsx | 22 +++++++ .../Modal/demo/ModalHiddenCloseButton.tsx | 48 +++++++++++++++ .../src/components/Modal/demo/index.tsx | 4 ++ .../web-react/src/hooks/useCancelEvent.ts | 50 +++++++++++++-- packages/web-react/src/types/modal.ts | 4 +- .../components/Modal/Modal.stories.twig | 4 ++ .../components/Modal/ModalHeader.twig | 25 ++++---- .../src/Resources/components/Modal/README.md | 41 +++++++++---- .../__tests__/__fixtures__/modalDefault.twig | 17 ++++++ .../__snapshots__/modalDefault.twig.snap.html | 23 +++++++ .../Modal/stories/ModalHiddenCloseButton.twig | 33 ++++++++++ .../web/src/scss/components/Modal/README.md | 17 +++--- .../web/src/scss/components/Modal/index.html | 61 +++++++++++++++++++ 16 files changed, 352 insertions(+), 53 deletions(-) create mode 100644 packages/web-react/src/components/Modal/demo/ModalHiddenCloseButton.tsx create mode 100644 packages/web-twig/src/Resources/components/Modal/stories/ModalHiddenCloseButton.twig diff --git a/packages/web-react/src/components/Dialog/Dialog.tsx b/packages/web-react/src/components/Dialog/Dialog.tsx index ed886dcc31..d970abb129 100644 --- a/packages/web-react/src/components/Dialog/Dialog.tsx +++ b/packages/web-react/src/components/Dialog/Dialog.tsx @@ -7,7 +7,7 @@ import { useDialog } from './useDialog'; // Solved using `as MutableRefObject` but I do not like it const Dialog = (props: DialogProps, ref: ForwardedRef): JSX.Element => { - const { children, isOpen, onClose, closeOnBackdropClick = true, ...restProps } = props; + const { children, isOpen, onClose, closeOnBackdropClick = true, closeOnEscapeKeyDown, ...restProps } = props; const dialogElementRef: MutableRefObject> = useRef(ref); const contentElementRef: MutableRefObject = useRef(null); @@ -28,7 +28,11 @@ const Dialog = (props: DialogProps, ref: ForwardedRef) }); // handles closing using Escape key - useCancelEvent(dialogElementRef as MutableRefObject, onClose); + useCancelEvent( + dialogElementRef as MutableRefObject, + onClose, + closeOnEscapeKeyDown as boolean, + ); /** * Make sure that there is only one child wrapped in dialog element. diff --git a/packages/web-react/src/components/Modal/ModalHeader.tsx b/packages/web-react/src/components/Modal/ModalHeader.tsx index e8f28e5f7a..a085978f3a 100644 --- a/packages/web-react/src/components/Modal/ModalHeader.tsx +++ b/packages/web-react/src/components/Modal/ModalHeader.tsx @@ -6,8 +6,13 @@ import ModalCloseButton from './ModalCloseButton'; import { useModalContext } from './ModalContext'; import { useModalStyleProps } from './useModalStyleProps'; +const defaultProps: ModalHeaderProps = { + hideCloseButton: false, +}; + const ModalHeader = (props: ModalHeaderProps) => { - const { children, closeLabel = 'Close', ...restProps } = props; + const propsWithDefaults = { ...defaultProps, ...props }; + const { children, closeLabel, hideCloseButton, ...restProps } = propsWithDefaults; const { classProps } = useModalStyleProps(); const { styleProps, props: otherProps } = useStyleProps(restProps); @@ -20,7 +25,7 @@ const ModalHeader = (props: ModalHeaderProps) => { {children} )} - + {!hideCloseButton && } ); }; diff --git a/packages/web-react/src/components/Modal/README.md b/packages/web-react/src/components/Modal/README.md index 1694439bcf..eda3901e4c 100644 --- a/packages/web-react/src/components/Modal/README.md +++ b/packages/web-react/src/components/Modal/README.md @@ -60,14 +60,15 @@ Example: ### API -| Name | Type | Default | Required | Description | -| ---------------------- | ---------------------------------------------- | -------- | -------- | ----------------------------------------------------- | -| `alignmentY` | [AlignmentY dictionary][dictionary-alignment] | `center` | ✕ | Vertical alignment of modal | -| `children` | `ReactNode` | — | ✕ | Children node | -| `closeOnBackdropClick` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked | -| `id` | `string` | — | ✓ | Modal ID | -| `isOpen` | `bool` | `false` | ✓ | Open state | -| `onClose` | `(event: ClickEvent or KeyboardEvent) => void` | — | ✓ | Callback on dialog closed | +| Name | Type | Default | Required | Description | +| ---------------------- | ---------------------------------------------- | -------- | -------- | ------------------------------------------------------- | +| `alignmentY` | [AlignmentY dictionary][dictionary-alignment] | `center` | ✕ | Vertical alignment of modal | +| `children` | `ReactNode` | — | ✕ | Children node | +| `closeOnBackdropClick` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked | +| `closeOnEscapeKeyDown` | `bool` | `true` | ✕ | Whether the modal will close when escape key is pressed | +| `id` | `string` | — | ✓ | Modal ID | +| `isOpen` | `bool` | `false` | ✓ | Open state | +| `onClose` | `(event: ClickEvent or KeyboardEvent) => void` | — | ✓ | Callback on dialog closed | Also, all properties of the [`` element][mdn-dialog] are supported. @@ -165,12 +166,26 @@ accessible name for the dialog, e.g. using the `aria-label` attribute on ``` +### Hidden Close Button + +The close button can by hidden by adding `hideCloseButton` prop to `ModalHeader` component. + +```jsx + + + + + + +``` + ### API -| Name | Type | Default | Required | Description | -| ------------ | ----------- | ------- | -------- | ------------------ | -| `children` | `ReactNode` | — | ✕ | Children node | -| `closeLabel` | `string` | — | ✕ | Close button label | +| Name | Type | Default | Required | Description | +| ----------------- | ----------- | ------- | -------- | ------------------------------ | +| `children` | `ReactNode` | — | ✕ | Children node | +| `closeLabel` | `string` | `Close` | ✕ | Close button label | +| `hideCloseButton` | `bool` | `false` | ✕ | Whether close button is hidden | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] diff --git a/packages/web-react/src/components/Modal/__tests__/ModalHeader.test.tsx b/packages/web-react/src/components/Modal/__tests__/ModalHeader.test.tsx index 575d00188d..fc14ffc77c 100644 --- a/packages/web-react/src/components/Modal/__tests__/ModalHeader.test.tsx +++ b/packages/web-react/src/components/Modal/__tests__/ModalHeader.test.tsx @@ -1,4 +1,6 @@ import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; @@ -10,4 +12,24 @@ describe('ModalHeader', () => { stylePropsTest(ModalHeader); restPropsTest(ModalHeader, 'header'); + + it('should have close button', () => { + render(Modal Title); + + const closeButton = screen.getByRole('button'); + + expect(closeButton).toBeInTheDocument(); + }); + + it('should not have close button', () => { + render( + + Modal Title + , + ); + + const closeButton = screen.queryByRole('button'); + + expect(closeButton).not.toBeInTheDocument(); + }); }); diff --git a/packages/web-react/src/components/Modal/demo/ModalHiddenCloseButton.tsx b/packages/web-react/src/components/Modal/demo/ModalHiddenCloseButton.tsx new file mode 100644 index 0000000000..2ee952520a --- /dev/null +++ b/packages/web-react/src/components/Modal/demo/ModalHiddenCloseButton.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { Button } from '../../Button'; +import Modal from '../Modal'; +import ModalBody from '../ModalBody'; +import ModalDialog from '../ModalDialog'; +import ModalFooter from '../ModalFooter'; +import ModalHeader from '../ModalHeader'; + +const ModalHiddenCloseButton = () => { + const [isOpen, setOpen] = useState(false); + const toggleModal = () => setOpen(!isOpen); + const handleClose = () => setOpen(false); + + return ( + <> + + + + + + Modal Title + + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam at excepturi laudantium magnam mollitia + perferendis reprehenderit, voluptate. Cum delectus dicta ducimus eligendi excepturi natus perferendis + provident unde. Eveniet, iste, molestiae? +

+
+ + + + +
+
+ + ); +}; + +export default ModalHiddenCloseButton; diff --git a/packages/web-react/src/components/Modal/demo/index.tsx b/packages/web-react/src/components/Modal/demo/index.tsx index 02fb0c8c6e..ec008df1a0 100644 --- a/packages/web-react/src/components/Modal/demo/index.tsx +++ b/packages/web-react/src/components/Modal/demo/index.tsx @@ -9,6 +9,7 @@ import DocsSection from '../../../../docs/DocsSections'; import { IconsProvider } from '../../../context'; import ModalDefault from './ModalDefault'; import ModalDisabledBackdropClick from './ModalDisabledBackdropClick'; +import ModalHiddenCloseButton from './ModalHiddenCloseButton'; import ModalScrollingLongContent from './ModalScrollingLongContent'; import ModalStacking from './ModalStacking'; @@ -27,6 +28,9 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + , ); diff --git a/packages/web-react/src/hooks/useCancelEvent.ts b/packages/web-react/src/hooks/useCancelEvent.ts index 95e99616fc..fc7515cfc1 100644 --- a/packages/web-react/src/hooks/useCancelEvent.ts +++ b/packages/web-react/src/hooks/useCancelEvent.ts @@ -1,25 +1,65 @@ -import { useCallback, useEffect, MutableRefObject } from 'react'; +import { useCallback, MutableRefObject } from 'react'; +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; const EVENT_CANCEL = 'cancel'; +const EVENT_KEYDOWN = 'keydown'; -export const useCancelEvent = (ref: MutableRefObject, callback: (event: Event) => void) => { +export const useCancelEvent = ( + ref: MutableRefObject, + callback: (event: Event) => void, + closeOnEscapeKeyDown: boolean = true, +) => { const handleCancel = useCallback( (event: Event) => { + // Do nothing if there is no reference or no callback + if (!ref || !callback) { + return; + } + + // Do nothing if the event was already processed. + if (event.defaultPrevented) { + return; + } + event.preventDefault(); - callback(event); + console.log('Cancel event called'); + + if (callback && closeOnEscapeKeyDown) { + console.log('Cancel event called2'); + callback(event); + } + }, + [ref, callback, closeOnEscapeKeyDown], + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if ( + event.key === 'Escape' && + !closeOnEscapeKeyDown + // ref.current?.hasAttribute('open') && + // ref.current?.getAttribute('open') === 'true' + ) { + console.log('Escape key pressed'); + event.preventDefault(); + } }, - [callback], + [closeOnEscapeKeyDown], ); - useEffect(() => { + useIsomorphicLayoutEffect(() => { const node = ref?.current; if (node) { node.addEventListener(EVENT_CANCEL, handleCancel); + // node.addEventListener(EVENT_KEYDOWN, handleKeyDown); + // document.addEventListener(EVENT_KEYDOWN, handleKeyDown); return () => { node.removeEventListener(EVENT_CANCEL, handleCancel); + // node.removeEventListener(EVENT_KEYDOWN, handleKeyDown); + // document.removeEventListener(EVENT_KEYDOWN, handleKeyDown); }; } diff --git a/packages/web-react/src/types/modal.ts b/packages/web-react/src/types/modal.ts index 987be739cc..4301b1a6f1 100644 --- a/packages/web-react/src/types/modal.ts +++ b/packages/web-react/src/types/modal.ts @@ -17,11 +17,12 @@ export type ModalDialogHandlingProps = { isOpen: boolean; onClose: (event: ClickEvent) => void; closeOnBackdropClick?: boolean; + closeOnEscapeKeyDown?: boolean; }; export interface ModalCloseButtonProps extends ModalDialogHandlingProps { id: string; - label: string; + label?: string; } export type ModalDialogBaseProps = { @@ -49,6 +50,7 @@ export interface ModalBodyProps extends SpiritDivElementProps, ChildrenProps {} export interface ModalHeaderProps extends SpiritElementProps, ChildrenProps { closeLabel?: string; + hideCloseButton?: boolean; } export interface ModalFooterProps extends SpiritElementProps, ChildrenProps { diff --git a/packages/web-twig/src/Resources/components/Modal/Modal.stories.twig b/packages/web-twig/src/Resources/components/Modal/Modal.stories.twig index 94c8bf852f..0b2ba9faf1 100644 --- a/packages/web-twig/src/Resources/components/Modal/Modal.stories.twig +++ b/packages/web-twig/src/Resources/components/Modal/Modal.stories.twig @@ -60,4 +60,8 @@ {% include '@components/Modal/stories/ModalDisabledBackdropClick.twig' %} + + {% include '@components/Modal/stories/ModalHiddenCloseButton.twig' %} + + {% endblock %} diff --git a/packages/web-twig/src/Resources/components/Modal/ModalHeader.twig b/packages/web-twig/src/Resources/components/Modal/ModalHeader.twig index f549ff838c..4a6c08e930 100644 --- a/packages/web-twig/src/Resources/components/Modal/ModalHeader.twig +++ b/packages/web-twig/src/Resources/components/Modal/ModalHeader.twig @@ -2,6 +2,7 @@ {%- set props = props | default([]) -%} {%- set _closeLabel = props.closeLabel | default('Close') -%} {%- set _enableDismiss = props.enableDismiss ?? true -%} +{%- set _hideCloseButton = props.hideCloseButton | default(false) -%} {%- set _modalId = props.modalId -%} {%- set _titleId = props.titleId | default(null) -%} @@ -29,15 +30,17 @@ {% block content %}{% endblock %} {% endif %} - + {% if _hideCloseButton is not same as(true) %} + + {% endif %} diff --git a/packages/web-twig/src/Resources/components/Modal/README.md b/packages/web-twig/src/Resources/components/Modal/README.md index 5c18ce9db4..d3fa8a7e5e 100644 --- a/packages/web-twig/src/Resources/components/Modal/README.md +++ b/packages/web-twig/src/Resources/components/Modal/README.md @@ -43,12 +43,13 @@ Example: ### API -| Name | Type | Default | Required | Description | -| ---------------------- | --------------------------------------------- | -------- | -------- | ----------------------------------------------------- | -| `alignmentY` | [AlignmentY dictionary][dictionary-alignment] | `center` | ✕ | Vertical alignment of modal | -| `closeOnBackdropClick` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked | -| `id` | `string` | — | ✓ | Modal ID | -| `titleId` | `string` | `null` | ✕ | ID of the title inside ModalHeader | +| Name | Type | Default | Required | Description | +| ---------------------- | --------------------------------------------- | -------- | -------- | ------------------------------------------------------- | +| `alignmentY` | [AlignmentY dictionary][dictionary-alignment] | `center` | ✕ | Vertical alignment of modal | +| `closeOnBackdropClick` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked | +| `closeOnEscapeKeyDown` | `bool` | `true` | ✕ | Whether the modal will close when escape key is pressed | +| `id` | `string` | — | ✓ | Modal ID | +| `titleId` | `string` | `null` | ✕ | ID of the title inside ModalHeader | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] @@ -158,14 +159,30 @@ using the `aria-label` attribute on `` component: ``` +### Hidden Close Button + +The close button can by hidden by adding `hideCloseButton` prop to `ModalHeader` component. + +```twig + + + + + … + + + +``` + ### API -| Name | Type | Default | Required | Description | -| --------------- | -------- | ------- | -------- | ----------------------- | -| `closeLabel` | `string` | `Close` | ✕ | Custom close label | -| `enableDismiss` | `bool` | `true` | ✕ | Enable JS Modal dismiss | -| `modalId` | `string` | — | ✓ | Modal ID | -| `titleId` | `string` | `null` | ✕ | ID of the title | +| Name | Type | Default | Required | Description | +| ----------------- | -------- | ------- | -------- | ------------------------------ | +| `closeLabel` | `string` | `Close` | ✕ | Custom close label | +| `enableDismiss` | `bool` | `true` | ✕ | Enable JS Modal dismiss | +| `hideCloseButton` | `bool` | `false` | ✕ | Whether close button is hidden | +| `modalId` | `string` | — | ✓ | Modal ID | +| `titleId` | `string` | `null` | ✕ | ID of the title | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] diff --git a/packages/web-twig/src/Resources/components/Modal/__tests__/__fixtures__/modalDefault.twig b/packages/web-twig/src/Resources/components/Modal/__tests__/__fixtures__/modalDefault.twig index aa26079b5a..51e97846d1 100644 --- a/packages/web-twig/src/Resources/components/Modal/__tests__/__fixtures__/modalDefault.twig +++ b/packages/web-twig/src/Resources/components/Modal/__tests__/__fixtures__/modalDefault.twig @@ -51,6 +51,23 @@ + + + + + Title of the Modal + + +

Modal Body

+
+ + + +
+
+
+ + + +
+
+

+ Title of the Modal +

+
+ +
+

+ Modal Body +

+
+ +
+
+ +
+
+
+
diff --git a/packages/web-twig/src/Resources/components/Modal/stories/ModalHiddenCloseButton.twig b/packages/web-twig/src/Resources/components/Modal/stories/ModalHiddenCloseButton.twig new file mode 100644 index 0000000000..3e3429c617 --- /dev/null +++ b/packages/web-twig/src/Resources/components/Modal/stories/ModalHiddenCloseButton.twig @@ -0,0 +1,33 @@ + + + + + + Modal Title + + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam at excepturi laudantium magnam mollitia + perferendis reprehenderit, voluptate. Cum delectus dicta ducimus eligendi excepturi natus perferendis + provident unde. Eveniet, iste, molestiae? +

+
+ + + + +
+
diff --git a/packages/web/src/scss/components/Modal/README.md b/packages/web/src/scss/components/Modal/README.md index 4713ec0b2e..ac0edea521 100644 --- a/packages/web/src/scss/components/Modal/README.md +++ b/packages/web/src/scss/components/Modal/README.md @@ -429,14 +429,15 @@ When you put it all together: Both trigger and close buttons use `data` attributes to open and close the Modal. -| Name | Type | Default | Required | Description | -| ------------------------------------- | -------- | ------- | -------- | ----------------------------------------------------- | -| `aria-controls` | `string` | — | ✕ | Aria controls state (auto) | -| `aria-expanded` | `string` | — | ✕ | Aria expanded state (auto) | -| `data-spirit-close-on-backdrop-click` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked | -| `data-spirit-dismiss` | `string` | `modal` | ✕ | Iterable selector | -| `data-spirit-target` | `string` | — | ✓ | Target selector | -| `data-spirit-toggle` | `string` | `modal` | ✕ | Iterable selector | +| Name | Type | Default | Required | Description | +| -------------------------------------- | -------- | ------- | -------- | ------------------------------------------------------- | +| `aria-controls` | `string` | — | ✕ | Aria controls state (auto) | +| `aria-expanded` | `string` | — | ✕ | Aria expanded state (auto) | +| `data-spirit-close-on-backdrop-click` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked | +| `data-spirit-close-on-escape-key-down` | `bool` | `true` | ✕ | Whether the modal will close when escape key is pressed | +| `data-spirit-dismiss` | `string` | `modal` | ✕ | Iterable selector | +| `data-spirit-target` | `string` | — | ✓ | Target selector | +| `data-spirit-toggle` | `string` | `modal` | ✕ | Iterable selector | ## JavaScript Plugin diff --git a/packages/web/src/scss/components/Modal/index.html b/packages/web/src/scss/components/Modal/index.html index 3cf718a0bd..7a58845b9c 100644 --- a/packages/web/src/scss/components/Modal/index.html +++ b/packages/web/src/scss/components/Modal/index.html @@ -1197,4 +1197,65 @@

Modal +
+

Hidden Close Button

+ +
+ + + + + +
+ +
+

Modal Title

+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam at excepturi laudantium magnam mollitia + perferendis reprehenderit, voluptate. Cum delectus dicta ducimus eligendi excepturi natus perferendis + provident unde. Eveniet, iste, molestiae? +

+
+ + + +
+ +
+ +
+ +
+ {{/web/layout/plain }}