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 component #6261

Merged
merged 77 commits into from
Jul 4, 2018
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
bf9f287
Initial implementation modal
Apr 18, 2018
aba9636
removed style prop assignment causing error
Apr 19, 2018
b0700fb
Set default mount node to #wpwrap
Apr 23, 2018
66367dd
Implemented default styling
Apr 25, 2018
aca49d0
Improved styling
Apr 25, 2018
3a5d75c
Applied code review feedback to with-focus-contain HOC
Apr 25, 2018
371e66b
Added eslint ignore for jsx-a11y/no-static-element-interactions
Apr 25, 2018
bd48b5a
Implemented withGlobalEvents HOC
Apr 25, 2018
809bfdd
withGlobalEvents HOC now forwards refs
Apr 30, 2018
47219be
Replace lodash defer with withSafeTimeout
Apr 30, 2018
9b93633
Removed unnecessary return statements
Apr 30, 2018
de29a46
Created separate styling rules
May 1, 2018
4a1b2c4
Added documentation for forwardRef function
May 1, 2018
1092fbd
Made mount location for modal configurable
May 1, 2018
44d91d3
Renamed elementId to appElementId for clarity
May 1, 2018
4aef9b6
Added noop for when no reg is provided in forwardRef
May 1, 2018
5c0568c
Fixed error in EditorProvider
May 1, 2018
887193d
Fix eslint errors
May 1, 2018
b0c4d82
Fixed incorrectly bound function
May 1, 2018
f25b989
Modal now by default mounts to the body and hides all other elements
May 2, 2018
c360742
hideApp no longer unhides elements that already had a aria-hidden=tru…
May 2, 2018
984ef8e
Improved a11y and updated documentation
May 2, 2018
6563c63
Updated documentation
May 2, 2018
59255ff
Removed forwardRef from element|
May 2, 2018
c6ee481
Changed default close label to Close dialog
May 2, 2018
17b7957
Add modal-open className to body when modal is opened
May 2, 2018
c4b433b
Added documentation to modal/index.js
May 2, 2018
cca2a0e
Removed aria-modal=true and explained why in aria-helper.js
May 2, 2018
21a722c
Documented modal/frame.js
May 2, 2018
9313ecb
Merge branch 'master' into add/modal
xyfi May 8, 2018
f487f22
Merge branch 'master' into add/modal
atimmer May 23, 2018
a66f5b8
Merge branch 'master' into add/modal
abotteram May 29, 2018
359774d
Merge branch 'add/modal' of https://github.com/WordPress/gutenberg in…
abotteram May 29, 2018
e7a8f4f
Addressed eslint issues
abotteram May 29, 2018
94a3216
Polish the visuals a bit.
Jun 6, 2018
064adbc
Merge branch 'master' into add/modal
abotteram Jun 7, 2018
832b1ac
Merge branch 'add/modal' of https://github.com/WordPress/gutenberg in…
abotteram Jun 7, 2018
6ac30dd
Disabled jsx-a11y/no-static-element-interactions in render function o…
abotteram Jun 7, 2018
349b068
Merge branch 'master' into add/modal
abotteram Jun 13, 2018
d1d2ba6
Merge branch 'master' into add/modal
abotteram Jun 18, 2018
eda32a6
Addressed CR concerns
abotteram Jun 18, 2018
dde71f2
Removed unused variable
abotteram Jun 18, 2018
ca8512a
Replaced focus.tabbables.find from @wordpress/utils with @wordpress/dom
abotteram Jun 18, 2018
afdaa2c
Merge branch 'master' into add/modal
abotteram Jun 20, 2018
b6ef31f
Fixed failing tests after updating react-test-renderer to version 16.…
abotteram Jun 20, 2018
b74d1e6
CSS Tweaks
abotteram Jun 21, 2018
dbac197
Fixed error when clicking outside of the modal
abotteram Jun 26, 2018
61ac955
Move focus to first element with tabindex=-1 on mount
abotteram Jun 26, 2018
d94e4ef
Make sure the dic the modal is renderd in is apprended to the documen…
abotteram Jun 27, 2018
6eb3886
Addressed minor codestyle issues in frame.js
abotteram Jun 27, 2018
ee6f520
Fixed bug when opening modal the second time
abotteram Jun 27, 2018
ca59960
Removed unused import
abotteram Jun 27, 2018
e2a50a1
replaced react-click-outside with internal withFocusOutside HOC
abotteram Jun 27, 2018
010f223
Merge branch 'master' into add/modal
abotteram Jul 2, 2018
9968113
Replaced withFocusContain with withConstrainedTabbing
abotteram Jul 2, 2018
5dc9b58
Replaced withFocusOutside with react-click-outside again
abotteram Jul 2, 2018
7f82944
Replaced @wordpress/utils keycodes with @wordpress/keycodes in frame.js
abotteram Jul 3, 2018
146f562
don't pass props.aria.labelledby to frame div when props.contentLabel…
abotteram Jul 3, 2018
30ab946
Added logic and tests for aria-helper to not hide (implicitely) live …
abotteram Jul 3, 2018
bd1050b
Removed isOpen props from the modal
abotteram Jul 3, 2018
d5f50d1
Removed the ability to add inline styles to the modal
abotteram Jul 3, 2018
2a0c916
Removed useless css
abotteram Jul 3, 2018
9400adc
Removed redundant z-index
abotteram Jul 3, 2018
a956732
Add full page overlay
abotteram Jul 3, 2018
0679405
Don't render h1 tag when no title is provided
abotteram Jul 3, 2018
4e3bb1a
Made modal screen-verlay full screen
abotteram Jul 3, 2018
12dd5fe
generate unique id for modal labelledby attribute
abotteram Jul 3, 2018
f607330
Replaced function scoped array liveRegionAriaRoles with file scoped c…
abotteram Jul 3, 2018
40cc3f9
Removed check whtehr forwardedRef is defined
abotteram Jul 3, 2018
0590e39
Minor JSDoc improvement
abotteram Jul 3, 2018
6f57d08
Removed styles from defaultProps
abotteram Jul 3, 2018
c547404
Documentation improvements
abotteram Jul 3, 2018
4eba6c7
don't add labelledBy attribute to modal frame when no title is present
abotteram Jul 3, 2018
9ee5b83
Don't add unique id to headingId when aria.labelledby prop is provide…
abotteram Jul 3, 2018
1a0bf75
Components: Reorder component lifecycle as first class members
aduth Jul 4, 2018
8778706
Components: Use withInstanceId to generate modal heading id
aduth Jul 4, 2018
6a43d9f
Components: Fix modal withInstanceId import reference
aduth Jul 4, 2018
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
54 changes: 54 additions & 0 deletions components/higher-order/with-focus-contain/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should almost never want to disable a rule for an entire file, since while it may be valid to disable it for the specific usage in place now, file-wide disabling will take effect for any future additions which themselves should be subject to the same scrutiny as the original rule intends.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also consider there's #6987 proposing to implement this HoC separately.

/**
* WordPress dependencies
*/
import { Component, createRef } from '@wordpress/element';
import { focus, keycodes } from '@wordpress/utils';

const {
TAB,
} = keycodes;

const withFocusContain = ( WrappedComponent ) => {
return class extends Component {
constructor() {
super( ...arguments );

this.focusContainRef = createRef();
this.handleTabBehaviour = this.handleTabBehaviour.bind( this );
}

handleTabBehaviour( event ) {
if ( ! event.keyCode === TAB ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is false for any key pressed, please double check. It should be if ( event.keyCode !== TAB )

Copy link
Member

@aduth aduth May 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify what's happening here, the negation occurs before the equality check, so this is effectively:

if ( false === TAB ) {

Makes me wish this ESLint rule were expanded to cover this case: https://eslint.org/docs/rules/no-unsafe-negation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea how I made this mistake... :/

return;
}

const tabbables = focus.tabbable.find( this.focusContainRef.current );
if ( ! tabbables.length ) {
return;
}
const firstTabbable = tabbables[ 0 ];
const lastTabbable = tabbables[ tabbables.length - 1 ];

if ( event.shiftKey && event.target === firstTabbable ) {
event.preventDefault();
lastTabbable.focus();
} else if ( ! event.shiftKey && event.target === lastTabbable ) {
event.preventDefault();
firstTabbable.focus();
}
}

render() {
return (
<div
onKeyDown={ this.handleTabBehaviour }
ref={ this.focusContainRef } >
<WrappedComponent { ...this.props } />
</div>
);
}
};
};

export default withFocusContain;
26 changes: 17 additions & 9 deletions components/higher-order/with-global-events/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* External dependencies
*/
import { forEach } from 'lodash';
import { forEach, noop } from 'lodash';

/**
* WordPress dependencies
*/
import {
Component,
createRef,
forwardRef,
createHigherOrderComponent,
} from '@wordpress/element';

Expand All @@ -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() {
Expand All @@ -49,15 +48,24 @@ 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;
this.props.forwardedRef( el );
}

render() {
return <WrappedComponent ref={ this.ref } { ...this.props } />;
return <WrappedComponent { ...this.props } ref={ this.handleRef } />;
}
};
}

return forwardRef( ( props, ref ) => {
Copy link
Member

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)

Copy link
Member

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)

This comment has yet to be addressed or responded.

Copy link
Member

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)

This comment has yet to be addressed or responded.

For future readers, see related effort at #7557 .

return <Wrapper { ...props } forwardedRef={ ref || noop } />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is noop the correct fallback value (a function, where ref is otherwise an object)?

Do we need a fallback value?

} );
}, 'withGlobalEvents' );
}

Expand Down
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { default as KeyboardShortcuts } from './keyboard-shortcuts';
export { default as MenuGroup } from './menu-group';
export { default as MenuItem } from './menu-item';
export { default as MenuItemsChoice } from './menu-items-choice';
export { default as Modal, ModalContextProvider } from './modal';
export { default as ScrollLock } from './scroll-lock';
export { NavigableMenu, TabbableContainer } from './navigable-container';
export { default as Notice } from './notice';
Expand Down
115 changes: 115 additions & 0 deletions components/modal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we link to react-modal documentation, if the intent is to allow people to apply common knowledge between the two projects?

Should we clarify whether the intent is for it to continue to strictly adhere to a compatible API into the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've already diverged from the react-modal api quite a bit, maybe just leaving out the reference to react-modal all together would be a better idea.


## Usage

Render a screen overlay with a modal on top.
```jsx
<ModalContextProvider value={ { elementId: 'wpwrap' } }>
<Modal
aria={ {
labelledby: 'modal-title',
describedby: 'modal-description',
} }
parentSelector={ () => {
return document.getElementById( 'wpwrap' );
} )
>
<ModalContent>
<h2 id="modal-title">My awesome modal!</h2>
<p id="modal-description">This modal is meant to be awesome!</p>
</ModalConent>
</Modal>
</ModalContextProvider>
```

## 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.

### 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

### 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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case of trying to mimic the react-modal API.

Copy link
Member

Choose a reason for hiding this comment

The 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 className which is also listed as one of the props available.


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

### overlayClassName

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

- Type: `String`
- Required: No
19 changes: 19 additions & 0 deletions components/modal/aria-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
let appElement = null;

export function setAppElement( node ) {
if ( ! appElement ) {
appElement = node;
}
}

export function hideApp() {
if ( appElement ) {
appElement.setAttribute( 'aria-hidden', 'true' );
}
}

export function showApp() {
if ( appElement ) {
appElement.removeAttribute( 'aria-hidden' );
}
}
32 changes: 32 additions & 0 deletions components/modal/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* WordPress dependencies
*/
import { createContext, createHigherOrderComponent } from '@wordpress/element';

const { Consumer, Provider } = createContext( {
appElementId: null,
} );

export { Provider as ModalContextProvider };

/**
* A Higher-order Component used to inject Modal context into the wrapped
* component.
*
* @param {Component} OriginalComponent Component to wrap.
*
* @return {Component} Component with Modal context injected.
*/
export const withModalContext = createHigherOrderComponent(
( OriginalComponent ) => ( props ) => (
<Consumer>
{ ( modalContext ) => (
<OriginalComponent
{ ...props }
modalContext={ modalContext }
/>
) }
</Consumer>
),
'withModalContext'
);
Loading