-
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 56 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 |
---|---|---|
|
@@ -8,7 +8,7 @@ import { forEach } from 'lodash'; | |
*/ | ||
import { | ||
Component, | ||
createRef, | ||
forwardRef, | ||
createHigherOrderComponent, | ||
} from '@wordpress/element'; | ||
|
||
|
@@ -26,13 +26,12 @@ const listener = new Listener(); | |
|
||
function withGlobalEvents( eventTypesToHandlers ) { | ||
return createHigherOrderComponent( ( WrappedComponent ) => { | ||
return class extends Component { | ||
class Wrapper extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.handleEvent = this.handleEvent.bind( this ); | ||
|
||
this.ref = createRef(); | ||
this.handleRef = this.handleRef.bind( this ); | ||
} | ||
|
||
componentDidMount() { | ||
|
@@ -49,15 +48,26 @@ function withGlobalEvents( eventTypesToHandlers ) { | |
|
||
handleEvent( event ) { | ||
const handler = eventTypesToHandlers[ event.type ]; | ||
if ( typeof this.ref.current[ handler ] === 'function' ) { | ||
this.ref.current[ handler ]( event ); | ||
if ( typeof this.wrappedRef[ handler ] === 'function' ) { | ||
this.wrappedRef[ handler ]( event ); | ||
} | ||
} | ||
|
||
handleRef( el ) { | ||
this.wrappedRef = el; | ||
if ( this.props.forwardedRef ) { | ||
this.props.forwardedRef( el ); | ||
} | ||
} | ||
|
||
render() { | ||
return <WrappedComponent ref={ this.ref } { ...this.props } />; | ||
return <WrappedComponent { ...this.props } ref={ this.handleRef } />; | ||
} | ||
}; | ||
} | ||
|
||
return forwardRef( ( props, ref ) => { | ||
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. Should this be built-in to See similar mention at #6480 (comment) 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.
This comment has yet to be addressed or responded. 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.
For future readers, see related effort at #7557 . |
||
return <Wrapper { ...props } forwardedRef={ ref } />; | ||
} ); | ||
}, 'withGlobalEvents' ); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
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 } | ||
isOpen={ openState } | ||
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. This example is out of date. The |
||
aria={ { | ||
describedby: "modal-description", | ||
} } | ||
> | ||
<ModalContent> | ||
<p id="modal-description">This modal is meant to be awesome!</p> | ||
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. We're encouraging a bad practice here with applying a static 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. I've added the 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. We still should never encourage / allow static https://github.com/WordPress/gutenberg/tree/master/components/higher-order/with-instance-id Alternatively, since there's decent overhead in doing so, maybe this is something we should bake as behavior on behalf of the developer? |
||
</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> | ||
<Modal | ||
title="This is my modal" | ||
onRequestClose={ this.closeModal } | ||
isOpen={ this.state.isOpen }> | ||
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. This is something we used to have with Popover, and then was later removed in #5015 because there's a performance impact in allowing the element and its children to be created wastefully (with no intention of ever rendering except by basis of boolean value). Further, there's a point toward consistency between usage of the two components. Would it be possible to remove this boolean and have the modal render simply by its presence? 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. Also, this 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. Yes, it is possible to omit the 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. Yes, I was about to propose the same given that this component is useless when |
||
<button onClick={ this.closeModal }> | ||
My custom close button | ||
</button> | ||
</Modal> | ||
</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 | ||
|
||
### style.content | ||
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. For what purpose would someone be adding inline styles? Should we want to encourage this, vs. styling by an assigned class name? 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. Another case of trying to mimic the 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. I share a similar concern, what would be the advantage of using inline styles over |
||
|
||
If this property is added, it will add inline styles to the modal content `div`. | ||
|
||
- Type: `Object` | ||
- Required: No | ||
|
||
### style.overlay | ||
|
||
If this property is added, it will add inline styles to the modal overlay `div`. | ||
|
||
- Type: `Object` | ||
- Required: No | ||
|
||
### 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,53 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { forEach } from 'lodash'; | ||
|
||
let hiddenElements = [], | ||
isHidden = false; | ||
|
||
/** | ||
* Hides all elements in the body element from screen-readers except | ||
* the provided element, script elements and elements that already have | ||
* an `aria-hidden="true"` attribute. | ||
* | ||
* 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 || | ||
element.tagName === 'SCRIPT' || | ||
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. On what basis did you decide script (and not any other node types) should be exempt from 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. I did a simple DOM inspection after running this function without any additional checks, and having script tags with 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. @afercia Commented: Well |
||
element.hasAttribute( 'aria-hidden', 'true' ) | ||
) { | ||
return; | ||
} | ||
element.setAttribute( 'aria-hidden', 'true' ); | ||
hiddenElements.push( element ); | ||
} ); | ||
isHidden = true; | ||
} | ||
|
||
/** | ||
* 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.
When will this condition not be satisfied?
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 some reason when
react-test-renderer
is used this condition is not satisfied. I know we shouldn't implement logic to satisfy tests, but this was a quick and easy fix.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.
Please make an issue to correct this, ideally self-assigned.
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 has been fixed.
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.
I don't think it's been fixed; this line is causing #7707 and adding the check back prevents the error.
Though it would indicate these refs aren't being forwarded at all.