-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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 component #6261
Modal component #6261
Changes from all commits
bf9f287
aba9636
b0700fb
66367dd
aca49d0
3a5d75c
371e66b
bd48b5a
809bfdd
47219be
9b93633
de29a46
4a1b2c4
1092fbd
44d91d3
4aef9b6
5c0568c
887193d
b0c4d82
f25b989
c360742
984ef8e
6563c63
59255ff
c6ee481
17b7957
c4b433b
cca2a0e
21a722c
9313ecb
f487f22
a66f5b8
359774d
e7a8f4f
94a3216
064adbc
832b1ac
6ac30dd
349b068
d1d2ba6
eda32a6
dde71f2
ca8512a
afdaa2c
b6ef31f
b74d1e6
dbac197
61ac955
d94e4ef
6eb3886
ee6f520
ca59960
e2a50a1
010f223
9968113
5dc9b58
7f82944
146f562
30ab946
bd1050b
d5f50d1
2a0c916
9400adc
a956732
0679405
4e3bb1a
12dd5fe
f607330
40cc3f9
0590e39
6f57d08
c547404
4eba6c7
9ee5b83
1a0bf75
8778706
6a43d9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
Modal | ||
======= | ||
|
||
The modal is used to create an accessible modal over an application. | ||
|
||
**Note:** The API for this modal has been mimicked to resemble [`react-modal`](https://github.com/reactjs/react-modal). | ||
|
||
## Usage | ||
|
||
Render a screen overlay with a modal on top. | ||
```jsx | ||
<Modal | ||
title="My Modal" | ||
onRequestClose={ closeFunction } | ||
aria={ { | ||
describedby: "modal-description", | ||
} } | ||
> | ||
<ModalContent> | ||
<p id="modal-description">This modal is meant to be awesome!</p> | ||
</ModalConent> | ||
</Modal> | ||
``` | ||
|
||
## Implement close logic | ||
|
||
For the modal to properly work it's important you implement the close logic for the modal properly. The following example shows you how to properly implement a modal. | ||
|
||
```js | ||
const { Component, Fragment } = wp.element; | ||
const { Modal } = wp.components; | ||
|
||
class MyModalWrapper extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.state = { | ||
isOpen: true, | ||
} | ||
|
||
this.openModal = this.openModal.bind( this ); | ||
this.closeModal = this.closeModal.bind( this ); | ||
} | ||
|
||
openModal() { | ||
if ( ! this.state.isOpen ) { | ||
this.setState( { isOpen: true } ); | ||
} | ||
} | ||
|
||
closeModal() { | ||
if ( this.state.isOpen ) { | ||
this.setState( { isOpen: false } ); | ||
} | ||
} | ||
|
||
render() { | ||
return ( | ||
<Fragment> | ||
<button onClick={ this.openModal }>Open Modal</button> | ||
{ this.state.isOpen ? | ||
<Modal | ||
title="This is my modal" | ||
onRequestClose={ this.closeModal }> | ||
<button onClick={ this.closeModal }> | ||
My custom close button | ||
</button> | ||
</Modal> | ||
: null } | ||
</Fragment> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
## Props | ||
|
||
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 | ||
|
||
This property is used as the modal header's title. It is required for accessibility reasons. | ||
|
||
- Type: `String` | ||
- Required: Yes | ||
|
||
### onRequestClose | ||
|
||
This function is called to indicate that the modal should be closed. | ||
|
||
- Type: `function` | ||
- Required: Yes | ||
|
||
### contentLabel | ||
|
||
If this property is added, it will be added to the modal content `div` as `aria-label`. | ||
You are encouraged to use this if `aria.labelledby` is not provided. | ||
|
||
- Type: `String` | ||
- Required: No | ||
|
||
### aria.labelledby | ||
|
||
If this property is added, it will be added to the modal content `div` as `aria-labelledby`. | ||
You are encouraged to use this when the modal is visually labelled. | ||
|
||
- Type: `String` | ||
- Required: No | ||
- Default: `modal-heading` | ||
|
||
### aria.describedby | ||
|
||
If this property is added, it will be added to the modal content `div` as `aria-describedby`. | ||
|
||
- Type: `String` | ||
- Required: No | ||
|
||
### focusOnMount | ||
|
||
If this property is true, it will focus the first tabbable element rendered in the modal. | ||
|
||
- Type: `bool` | ||
- Required: No | ||
- Default: true | ||
|
||
### shouldCloseOnEsc | ||
|
||
If this property is added, it will determine whether the modal requests to close when the escape key is pressed. | ||
|
||
- Type: `bool` | ||
- Required: No | ||
- Default: true | ||
|
||
### shouldCloseOnClickOutside | ||
|
||
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: `bool` | ||
- Required: No | ||
- Default: true | ||
|
||
### className | ||
|
||
If this property is added, it will an additional class name to the modal content `div`. | ||
|
||
- Type: `String` | ||
- Required: No | ||
|
||
### role | ||
|
||
If this property is added, it will override the default role of the modal. | ||
|
||
- Type: `String` | ||
- Required: No | ||
- Default: `dialog` | ||
|
||
### overlayClassName | ||
|
||
If this property is added, it will an additional class name to the modal overlay `div`. | ||
|
||
- Type: `String` | ||
- Required: No |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { forEach } from 'lodash'; | ||
|
||
const LIVE_REGION_ARIA_ROLES = new Set( [ | ||
'alert', | ||
'status', | ||
'log', | ||
'marquee', | ||
'timer', | ||
] ); | ||
|
||
let hiddenElements = [], | ||
isHidden = false; | ||
|
||
/** | ||
* Hides all elements in the body element from screen-readers except | ||
* the provided element and elements that should not be hidden from | ||
* screen-readers. | ||
* | ||
* The reason we do this is because `aria-modal="true"` currently is bugged | ||
* in Safari, and support is spotty in other browsers overall. In the future | ||
* we should consider removing these helper functions in favor of | ||
* `aria-modal="true"`. | ||
* | ||
* @param {Element} unhiddenElement The element that should not be hidden. | ||
*/ | ||
export function hideApp( unhiddenElement ) { | ||
if ( isHidden ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the condition (and the associated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be called multiple time in case there are multiple modals. Although this is not desired, various plugins could open modals making this check a worthwhile check in my opinion. |
||
return; | ||
} | ||
const elements = document.body.children; | ||
forEach( elements, ( element ) => { | ||
if ( | ||
element === unhiddenElement | ||
) { | ||
return; | ||
} | ||
if ( elementShouldBeHidden( element ) ) { | ||
element.setAttribute( 'aria-hidden', 'true' ); | ||
hiddenElements.push( element ); | ||
} | ||
} ); | ||
isHidden = true; | ||
} | ||
|
||
/** | ||
* Determines if the passed element should not be hidden from screen readers. | ||
* | ||
* @param {HTMLElement} element The element that should be checked. | ||
* | ||
* @return {boolean} Whether the element should not be hidden from screen-readers. | ||
*/ | ||
export function elementShouldBeHidden( element ) { | ||
const role = element.getAttribute( 'role' ); | ||
return ! ( | ||
element.tagName === 'SCRIPT' || | ||
element.hasAttribute( 'aria-hidden' ) || | ||
element.hasAttribute( 'aria-live' ) || | ||
LIVE_REGION_ARIA_ROLES.has( role ) | ||
); | ||
} | ||
|
||
/** | ||
* Makes all elements in the body that have been hidden by `hideApp` | ||
* visible again to screen-readers. | ||
*/ | ||
export function showApp() { | ||
if ( ! isHidden ) { | ||
return; | ||
} | ||
forEach( hiddenElements, ( element ) => { | ||
element.removeAttribute( 'aria-hidden' ); | ||
} ); | ||
hiddenElements = []; | ||
isHidden = false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be built-in to
createHigherOrderComponent
?See similar mention at #6480 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment has yet to be addressed or responded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For future readers, see related effort at #7557 .