Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modal: Convert component to TypeScript #42949

Merged
merged 10 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
### Internal

- `Flex`, `FlexItem`, `FlexBlock`: Convert to TypeScript ([#42537](https://github.com/WordPress/gutenberg/pull/42537)).
- `Modal`: Convert to TypeScript ([#42949](https://github.com/WordPress/gutenberg/pull/42949)).
- `InputControl`: Fix incorrect `size` prop passing ([#42793](https://github.com/WordPress/gutenberg/pull/42793)).

## 19.16.0 (2022-07-27)
Expand Down Expand Up @@ -80,7 +81,6 @@
- `Popover`: call `getAnchorRect` callback prop even if `anchorRefFallback` has no value. ([#42329](https://github.com/WordPress/gutenberg/pull/42329)).
- Fix `ToolTip` position to ensure it is always positioned relative to the first child of the ToolTip. ([#41268](https://github.com/WordPress/gutenberg/pull/41268))


### Enhancements

- `ToggleGroupControl`: Add large size variant ([#42008](https://github.com/WordPress/gutenberg/pull/42008/)).
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/confirm-dialog/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
*/
import type { MouseEvent, KeyboardEvent, ReactNode } from 'react';

/**
* Internal dependencies
*/
import type { ModalProps } from '../modal/types';

export type DialogInputEvent =
| Parameters< ModalProps[ 'onRequestClose' ] >[ 0 ]
| KeyboardEvent< HTMLDivElement >
| MouseEvent< HTMLButtonElement >;

Expand Down
107 changes: 53 additions & 54 deletions packages/components/src/modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,118 +150,117 @@ const MyModal = () => {
The set of props accepted by the component will be specified below.
Props not included in this set will be applied to the input elements.

#### title
#### `aria.describedby`: `string`

This property is used as the modal header's title.

Titles are required for accessibility reasons, see `aria.labelledby` and `contentLabel` for other ways to provide a title.
If this property is added, it will be added to the modal content `div` as `aria-describedby`.

- Type: `String`
- Required: No

#### onRequestClose
#### `aria.labelledby`: `string`

This function is called to indicate that the modal should be closed.
If this property is added, it will be added to the modal content `div` as `aria-labelledby`.
Use this when you are rendering the title yourself within the modal's content area instead of using the `title` prop. This ensures the title is usable by assistive technology.

- Type: `function`
- Required: Yes
Titles are required for accessibility reasons, see `contentLabel` and `title` for other ways to provide a title.

#### contentLabel
- Required: No
- Default: if the `title` prop is provided, this will default to the id of the element that renders `title`

If this property is added, it will be added to the modal content `div` as `aria-label`.
#### `bodyOpenClassName`: `string`

Titles are required for accessibility reasons, see `aria.labelledby` and `title` for other ways to provide a title.
Class name added to the body element when the modal is open.

- Type: `String`
- Required: No
- Default: `modal-open`

#### aria.labelledby
#### `className`: `string`

If this property is added, it will be added to the modal content `div` as `aria-labelledby`.
Use this when you are rendering the title yourself within the modal's content area instead of using the `title` prop. This ensures the title is usable by assistive technology.

Titles are required for accessibility reasons, see `contentLabel` and `title` for other ways to provide a title.
If this property is added, it will an additional class name to the modal content `div`.

- Type: `String`
- Required: No
- Default: if the `title` prop is provided, this will default to the id of the element that renders `title`

#### aria.describedby
#### `contentLabel`: `string`

If this property is added, it will be added to the modal content `div` as `aria-describedby`.
If this property is added, it will be added to the modal content `div` as `aria-label`.

Titles are required for accessibility reasons, see `aria.labelledby` and `title` for other ways to provide a title.

- Type: `String`
- Required: No

#### focusOnMount
#### `focusOnMount`: `boolean | 'firstElement'`

If this property is true, it will focus the first tabbable element rendered in the modal.

- Type: `boolean`
- Required: No
- Default: true
- Default: `true`

#### shouldCloseOnEsc
#### `isDismissible`: `boolean`

If this property is added, it will determine whether the modal requests to close when the escape key is pressed.
If this property is set to false, the modal will not display a close icon and cannot be dismissed.

- Type: `boolean`
- Required: No
- Default: true
- Default: `true`

#### shouldCloseOnClickOutside
#### `isFullScreen`: `boolean`

If this property is added, it will determine whether the modal requests to close when a mouse click occurs outside of the modal content.
This property when set to `true` will render a full screen modal.

- Type: `boolean`
- Required: No
- Default: true
- Default: `false`

#### isDismissible
#### `onRequestClose`: ``

If this property is set to false, the modal will not display a close icon and cannot be dismissed.
This function is called to indicate that the modal should be closed.

- Type: `boolean`
- Required: No
- Default: true
- Required: Yes

#### className
#### `overlayClassName`: `string`

If this property is added, it will an additional class name to the modal content `div`.
If this property is added, it will an additional class name to the modal overlay `div`.

- Type: `String`
- Required: No

#### role
#### `role`: `AriaRole`

If this property is added, it will override the default role of the modal.

- Type: `String`
- Required: No
- Default: `dialog`

#### overlayClassName
#### `shouldCloseOnClickOutside`: `boolean`

If this property is added, it will an additional class name to the modal overlay `div`.
If this property is added, it will determine whether the modal requests to close when a mouse click occurs outside of the modal content.

- Type: `String`
- Required: No
- Default: `true`

#### isFullScreen
#### `shouldCloseOnEsc`: `boolean`

This property when set to `true` will render a full screen modal.
If this property is added, it will determine whether the modal requests to close when the escape key is pressed.

- Required: No
- Default: `true`

#### `style`: `CSSProperties`

If this property is added, it will be added to the modal frame `div`.

- Required: No

#### `title`: `string`

This property is used as the modal header's title.

Titles are required for accessibility reasons, see `aria.labelledby` and `contentLabel` for other ways to provide a title.

- Type: `boolean`
- Required: No
- Default: `false`

#### __experimentalHideHeader
#### `__experimentalHideHeader`: `boolean`

When set to `true`, the Modal's header (including the icon, title and close button) will not be rendered.

*Warning*: This property is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
_Warning_: This property is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.

- Type: `boolean`
- Required: No
- Default: `false`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// @ts-nocheck

/**
* External dependencies
*/
Expand All @@ -13,7 +11,7 @@ const LIVE_REGION_ARIA_ROLES = new Set( [
'timer',
] );

let hiddenElements = [],
let hiddenElements: Element[] = [],
isHidden = false;

/**
Expand All @@ -26,9 +24,9 @@ let hiddenElements = [],
* we should consider removing these helper functions in favor of
* `aria-modal="true"`.
*
* @param {Element} unhiddenElement The element that should not be hidden.
* @param {HTMLDivElement} unhiddenElement The element that should not be hidden.
*/
export function hideApp( unhiddenElement ) {
export function hideApp( unhiddenElement?: HTMLDivElement ) {
if ( isHidden ) {
return;
}
Expand All @@ -52,13 +50,13 @@ export function hideApp( unhiddenElement ) {
*
* @return {boolean} Whether the element should not be hidden from screen-readers.
*/
export function elementShouldBeHidden( element ) {
export function elementShouldBeHidden( element: Element ) {
const role = element.getAttribute( 'role' );
return ! (
element.tagName === 'SCRIPT' ||
element.hasAttribute( 'aria-hidden' ) ||
element.hasAttribute( 'aria-live' ) ||
LIVE_REGION_ARIA_ROLES.has( role )
( role && LIVE_REGION_ARIA_ROLES.has( role ) )
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// @ts-nocheck

/**
* External dependencies
*/
import classnames from 'classnames';
import type { ForwardedRef, KeyboardEvent, UIEvent } from 'react';

/**
* WordPress dependencies
Expand Down Expand Up @@ -34,11 +33,15 @@ import { close } from '@wordpress/icons';
import * as ariaHelper from './aria-helper';
import Button from '../button';
import StyleProvider from '../style-provider';
import type { ModalProps } from './types';

// Used to count the number of open modals.
let openModalCount = 0;

function Modal( props, forwardedRef ) {
function UnforwardedModal(
props: ModalProps,
forwardedRef: ForwardedRef< HTMLDivElement >
) {
const {
bodyOpenClassName = 'modal-open',
role = 'dialog',
Expand All @@ -49,8 +52,8 @@ function Modal( props, forwardedRef ) {
isDismissible = true,
/* Accessibility. */
aria = {
labelledby: null,
describedby: null,
labelledby: undefined,
describedby: undefined,
},
onRequestClose,
icon,
Expand All @@ -65,7 +68,7 @@ function Modal( props, forwardedRef ) {
__experimentalHideHeader = false,
} = props;

const ref = useRef();
const ref = useRef< HTMLDivElement >();
const instanceId = useInstanceId( Modal );
const headingId = title
? `components-modal-header-${ instanceId }`
Expand Down Expand Up @@ -95,7 +98,7 @@ function Modal( props, forwardedRef ) {
};
}, [ bodyOpenClassName ] );

function handleEscapeKeyDown( event ) {
function handleEscapeKeyDown( event: KeyboardEvent< HTMLDivElement > ) {
if (
shouldCloseOnEsc &&
event.keyCode === ESCAPE &&
Expand All @@ -109,8 +112,8 @@ function Modal( props, forwardedRef ) {
}

const onContentContainerScroll = useCallback(
( e ) => {
const scrollY = e?.target?.scrollTop ?? -1;
( e: UIEvent< HTMLDivElement > ) => {
const scrollY = e?.currentTarget?.scrollTop ?? -1;

if ( ! hasScrolledContent && scrollY > 0 ) {
setHasScrolledContent( true );
Expand Down Expand Up @@ -148,9 +151,9 @@ function Modal( props, forwardedRef ) {
] ) }
role={ role }
aria-label={ contentLabel }
aria-labelledby={ contentLabel ? null : headingId }
aria-labelledby={ contentLabel ? undefined : headingId }
aria-describedby={ aria.describedby }
tabIndex="-1"
tabIndex={ -1 }
{ ...( shouldCloseOnClickOutside
? focusOutsideProps
: {} ) }
Expand Down Expand Up @@ -205,4 +208,37 @@ function Modal( props, forwardedRef ) {
);
}

export default forwardRef( Modal );
/**
* Modals give users information and choices related to a task they’re trying to
* accomplish. They can contain critical information, require decisions, or
* involve multiple tasks.
*
* ```jsx
* import { Button, Modal } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const MyModal = () => {
* const [ isOpen, setOpen ] = useState( false );
* const openModal = () => setOpen( true );
* const closeModal = () => setOpen( false );
*
* return (
* <>
* <Button variant="secondary" onClick={ openModal }>
* Open Modal
* </Button>
* { isOpen && (
* <Modal title="This is my modal" onRequestClose={ closeModal }>
* <Button variant="secondary" onClick={ closeModal }>
* My custom close button
* </Button>
* </Modal>
* ) }
* </>
* );
* };
* ```
*/
export const Modal = forwardRef( UnforwardedModal );

export default Modal;
Loading