Skip to content

Commit

Permalink
Feat(web, web-twig, web-react): Hide close button in Modal
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelklibani committed Jul 2, 2024
1 parent 86ba5ff commit 2e55023
Show file tree
Hide file tree
Showing 16 changed files with 349 additions and 53 deletions.
9 changes: 7 additions & 2 deletions packages/web-react/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useDialog } from './useDialog';
// Solved using `as MutableRefObject<HTMLDialogElement | null>` but I do not like it

const Dialog = (props: DialogProps, ref: ForwardedRef<HTMLDialogElement | null>): JSX.Element => {
const { children, isOpen, onClose, closeOnBackdropClick = true, ...restProps } = props;
const { children, isOpen, onClose, closeOnBackdropClick = true, closeOnEscapeKeyDown, ...restProps } = props;
const dialogElementRef: MutableRefObject<ForwardedRef<HTMLDialogElement | null>> = useRef(ref);
const contentElementRef: MutableRefObject<HTMLElement | null> = useRef(null);

Expand All @@ -28,7 +28,12 @@ const Dialog = (props: DialogProps, ref: ForwardedRef<HTMLDialogElement | null>)
});

// handles closing using Escape key
useCancelEvent(dialogElementRef as MutableRefObject<HTMLDialogElement | null>, onClose);
useCancelEvent(
dialogElementRef as MutableRefObject<HTMLDialogElement | null>,
onClose,
closeOnEscapeKeyDown as boolean,
isOpen,
);

/**
* Make sure that there is only one child wrapped in dialog element.
Expand Down
9 changes: 7 additions & 2 deletions packages/web-react/src/components/Modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -20,7 +25,7 @@ const ModalHeader = (props: ModalHeaderProps) => {
{children}
</h2>
)}
<ModalCloseButton id={id} isOpen={isOpen} label={closeLabel} onClose={onClose} />
{!hideCloseButton && <ModalCloseButton id={id} isOpen={isOpen} label={closeLabel} onClose={onClose} />}
</header>
);
};
Expand Down
39 changes: 27 additions & 12 deletions packages/web-react/src/components/Modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [`<dialog>` element][mdn-dialog] are supported.

Expand Down Expand Up @@ -165,12 +166,26 @@ accessible name for the dialog, e.g. using the `aria-label` attribute on
</Modal>
```

### Hidden Close Button

The close button can by hidden by adding `hideCloseButton` prop to `ModalHeader` component.

```jsx
<Modal id="modal-example">
<ModalDialog>
<ModalHeader hideCloseButton />
<ModalBody></ModalBody>
</ModalDialog>
</Modal>
```

### 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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,4 +12,24 @@ describe('ModalHeader', () => {
stylePropsTest(ModalHeader);

restPropsTest(ModalHeader, 'header');

it('should have close button', () => {
render(<ModalHeader id="test">Modal Title</ModalHeader>);

const closeButton = screen.getByRole('button');

expect(closeButton).toBeInTheDocument();
});

it('should not have close button', () => {
render(
<ModalHeader id="test" hideCloseButton>
Modal Title
</ModalHeader>,
);

const closeButton = screen.queryByRole('button');

expect(closeButton).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Button onClick={toggleModal}>Open Modal</Button>

<Modal
id="example-hidden-close-button"
isOpen={isOpen}
onClose={handleClose}
closeOnBackdropClick={false}
closeOnEscapeKeyDown={false}
>
<ModalDialog>
<ModalHeader id="example-hidden-close-button" hideCloseButton>
Modal Title
</ModalHeader>
<ModalBody>
<p>
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?
</p>
</ModalBody>
<ModalFooter>
<Button onClick={handleClose}>Primary action</Button>
<Button color="secondary" onClick={handleClose}>
Secondary action
</Button>
</ModalFooter>
</ModalDialog>
</Modal>
</>
);
};

export default ModalHiddenCloseButton;
4 changes: 4 additions & 0 deletions packages/web-react/src/components/Modal/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -27,6 +28,9 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<DocsSection title="Disabled Backdrop Click">
<ModalDisabledBackdropClick />
</DocsSection>
<DocsSection title="Hidden Close Button">
<ModalHiddenCloseButton />
</DocsSection>
</IconsProvider>
</React.StrictMode>,
);
46 changes: 41 additions & 5 deletions packages/web-react/src/hooks/useCancelEvent.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,61 @@
import { useCallback, useEffect, MutableRefObject } from 'react';
import { useCallback, MutableRefObject } from 'react';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';

// TODO: Remove `isOpen` and listeners with `handleKeyDown` when Chrome fixes the bug,
// right now Chrome is bugged and sends un-cancelable events, so closing modal based on
// `cancel` event is not possible in chrome.
// FireFox and Safari are working fine.

const EVENT_CANCEL = 'cancel';
const EVENT_KEYDOWN = 'keydown'; // ! Removed when bug in chrome will be fixed

export const useCancelEvent = (ref: MutableRefObject<HTMLElement | null>, callback: (event: Event) => void) => {
export const useCancelEvent = (
ref: MutableRefObject<HTMLElement | null>,
callback: (event: Event) => void,
closeOnEscapeKeyDown: boolean = true,
isOpen: boolean = false, // ! Removed when bug in chrome will be fixed
) => {
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);
if (callback && closeOnEscapeKeyDown) {
callback(event);
}
},
[ref, callback, closeOnEscapeKeyDown],
);

// ! Removed when bug in chrome will be fixed
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape' && !closeOnEscapeKeyDown && isOpen) {
event.preventDefault();
}
},
[callback],
[closeOnEscapeKeyDown, isOpen],
);

useEffect(() => {
useIsomorphicLayoutEffect(() => {
const node = ref?.current;

if (node) {
node.addEventListener(EVENT_CANCEL, handleCancel);
document.addEventListener(EVENT_KEYDOWN, handleKeyDown); // ! Removed when bug in chrome will be fixed

return () => {
node.removeEventListener(EVENT_CANCEL, handleCancel);
document.removeEventListener(EVENT_KEYDOWN, handleKeyDown); // ! Removed when bug in chrome will be fixed
};
}

Expand Down
4 changes: 3 additions & 1 deletion packages/web-react/src/types/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<E extends ElementType = ModalDialogElementType> = {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@
{% include '@components/Modal/stories/ModalDisabledBackdropClick.twig' %}
</DocsSection>

<DocsSection title="Hidden Close Button">
{% include '@components/Modal/stories/ModalHiddenCloseButton.twig' %}
</DocsSection>

{% endblock %}
25 changes: 14 additions & 11 deletions packages/web-twig/src/Resources/components/Modal/ModalHeader.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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) -%}

Expand Down Expand Up @@ -29,15 +30,17 @@
{% block content %}{% endblock %}
</h2>
{% endif %}
<Button
aria-controls="{{ _modalId }}"
aria-expanded="false"
color="tertiary"
data-spirit-dismiss="{{ _enableDismiss ? 'modal' : null }}"
data-spirit-target="{{ _enableDismiss ? '#' ~ _modalId : null }}"
isSquare
>
<Icon name="close" />
<VisuallyHidden>{{ _closeLabel }}</VisuallyHidden>
</Button>
{% if _hideCloseButton is not same as(true) %}
<Button
aria-controls="{{ _modalId }}"
aria-expanded="false"
color="tertiary"
data-spirit-dismiss="{{ _enableDismiss ? 'modal' : null }}"
data-spirit-target="{{ _enableDismiss ? '#' ~ _modalId : null }}"
isSquare
>
<Icon name="close" />
<VisuallyHidden>{{ _closeLabel }}</VisuallyHidden>
</Button>
{% endif %}
</header>
41 changes: 29 additions & 12 deletions packages/web-twig/src/Resources/components/Modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -158,14 +159,30 @@ using the `aria-label` attribute on `<Modal>` component:
</Modal>
```

### Hidden Close Button

The close button can by hidden by adding `hideCloseButton` prop to `ModalHeader` component.

```twig
<Modal id="modal-example">
<ModalDialog>
<ModalHeader modalId="modal-example" hideCloseButton />
<ModalBody>
</ModalBody>
</ModalDialog>
</Modal>
```

### 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]
Expand Down
Loading

0 comments on commit 2e55023

Please sign in to comment.