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

Components: Add onFocusOutside replacement to Popover onClickOutside #14851

Merged
merged 4 commits into from
Aug 2, 2019
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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

- The `Button` component will no longer assign default styling (`is-default` class) when explicitly assigned as primary (the `isPrimary` prop). This should resolve potential conflicts affecting a combination of `isPrimary`, `isDefault`, and `isLarge` / `isSmall`, where the busy animation would appear with incorrect coloring.

### Deprecations

- The `Popover` component `onClickOutside` prop has been deprecated. Use `onFocusOutside` instead.

### Internal

- The `Dropdown` component has been refactored to focus changes using the `Popover` component's `onFocusOutside` prop.

## 8.0.0 (2019-06-12)

### New Feature
Expand Down
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@babel/runtime": "^7.4.4",
"@wordpress/a11y": "file:../a11y",
"@wordpress/compose": "file:../compose",
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/dom": "file:../dom",
"@wordpress/element": "file:../element",
"@wordpress/hooks": "file:../hooks",
Expand Down
18 changes: 8 additions & 10 deletions packages/components/src/dropdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Dropdown extends Component {

this.toggle = this.toggle.bind( this );
this.close = this.close.bind( this );
this.closeIfClickOutside = this.closeIfClickOutside.bind( this );
this.closeIfFocusOutside = this.closeIfFocusOutside.bind( this );

this.containerRef = createRef();

Expand Down Expand Up @@ -46,15 +46,13 @@ class Dropdown extends Component {
}

/**
* Closes the dropdown if a click occurs outside the dropdown wrapper. This
* is intentionally distinct from `onClose` in that a click outside the
* popover may occur in the toggling of the dropdown via its toggle button.
* The correct behavior is to keep the dropdown closed.
*
* @param {MouseEvent} event Click event triggering `onClickOutside`.
* Closes the dropdown if a focus leaves the dropdown wrapper. This is
* intentionally distinct from `onClose` since focus loss from the popover
* is expected to occur when using the Dropdown's toggle button, in which
* case the correct behavior is to keep the dropdown closed.
*/
closeIfClickOutside( event ) {
if ( ! this.containerRef.current.contains( event.target ) ) {
closeIfFocusOutside() {
if ( ! this.containerRef.current.contains( document.activeElement ) ) {
this.close();
}
}
Expand Down Expand Up @@ -87,7 +85,7 @@ class Dropdown extends Component {
className={ contentClassName }
position={ position }
onClose={ this.close }
onClickOutside={ this.closeIfClickOutside }
onFocusOutside={ this.closeIfFocusOutside }
expandOnMobile={ expandOnMobile }
headerTitle={ headerTitle }
focusOnMount={ focusOnMount }
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/popover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ A callback invoked when the popover should be closed.
- Type: `Function`
- Required: No

### onClickOutside
### onFocusOutside

A callback invoked when the user clicks outside the opened popover, passing the click event. The popover should be closed in response to this interaction. Defaults to `onClose`.
A callback invoked when the focus leaves the opened popover. This should only be provided in advanced use-cases when a Popover should close under specific circumstances; for example, if the new `document.activeElement` is content of or otherwise controlling Popover visibility.

Defaults to `onClose` when not provided.

- Type: `Function`
- Required: No
Expand Down
17 changes: 7 additions & 10 deletions packages/components/src/popover/detect-outside.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
/**
* External dependencies
* WordPress dependencies
*/
import clickOutside from 'react-click-outside';
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated question to this PR. Can we also refactor Modal component to stop using react-click-outside? :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated question to this PR. Can we also refactor Modal component to stop using react-click-outside? :)

I would like to, yes.

( #6261 (comment) 😞 )

Copy link
Member

Choose a reason for hiding this comment

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

There is still hope then :)

Copy link
Member

Choose a reason for hiding this comment

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

https://unpkg.com/react-click-outside@3.0.1/index.js

Well, it looks like react-click-outside is a simplified version of our own HOC :)

Copy link
Member

Choose a reason for hiding this comment

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

I opened a follow-up to address it – #16878.

import { Component } from '@wordpress/element';

/**
* WordPress dependencies
* Internal dependencies
*/
import { Component } from '@wordpress/element';
import withFocusOutside from '../higher-order/with-focus-outside';

class PopoverDetectOutside extends Component {
handleClickOutside( event ) {
const { onClickOutside } = this.props;
if ( onClickOutside ) {
onClickOutside( event );
}
handleFocusOutside( event ) {
this.props.onFocusOutside( event );
}

render() {
return this.props.children;
}
}

export default clickOutside( PopoverDetectOutside );
export default withFocusOutside( PopoverDetectOutside );
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a nice hook const useOnFocusOutside( ref, handler )

45 changes: 43 additions & 2 deletions packages/components/src/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useRef, useState, useEffect } from '@wordpress/element';
import { focus } from '@wordpress/dom';
import { ESCAPE } from '@wordpress/keycodes';
import isShallowEqual from '@wordpress/is-shallow-equal';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
Expand Down Expand Up @@ -251,7 +252,6 @@ const Popover = ( {
onKeyDown,
children,
className,
onClickOutside = onClose,
noArrow = false,
// Disable reason: We generate the `...contentProps` rest as remainder
// of props which aren't explicitly handled by this component.
Expand All @@ -263,6 +263,8 @@ const Popover = ( {
getAnchorRect,
expandOnMobile,
animate = true,
onClickOutside,
onFocusOutside,
/* eslint-enable no-unused-vars */
...contentProps
} ) => {
Expand Down Expand Up @@ -308,6 +310,45 @@ const Popover = ( {
}
};

/**
* Shims an onFocusOutside callback to be compatible with a deprecated
* onClickOutside prop function, if provided.
*
* @param {FocusEvent} event Focus event from onFocusOutside.
*/
function handleOnFocusOutside( event ) {
// Defer to given `onFocusOutside` if specified. Call `onClose` only if
// both `onFocusOutside` and `onClickOutside` are unspecified. Doing so
// assures backwards-compatibility for prior `onClickOutside` default.
if ( onFocusOutside ) {
onFocusOutside( event );
return;
} else if ( ! onClickOutside ) {
onClose();
return;
}

// Simulate MouseEvent using FocusEvent#relatedTarget as emulated click
// target. MouseEvent constructor is unsupported in Internet Explorer.
let clickEvent;
try {
clickEvent = new window.MouseEvent( 'click' );
} catch ( error ) {
clickEvent = document.createEvent( 'MouseEvent' );
clickEvent.initMouseEvent( 'click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null );
}

Object.defineProperty( clickEvent, 'target', {
get: () => event.relatedTarget,
} );

deprecated( 'Popover onClickOutside prop', {
alternative: 'onFocusOutside',
} );

onClickOutside( clickEvent );
}

// Compute the animation position
const yAxisMapping = {
top: 'bottom',
Expand Down Expand Up @@ -339,7 +380,7 @@ const Popover = ( {

/* eslint-disable jsx-a11y/no-static-element-interactions */
let content = (
<PopoverDetectOutside onClickOutside={ onClickOutside }>
<PopoverDetectOutside onFocusOutside={ handleOnFocusOutside }>
<Animate
type={ animate && isReadyToAnimate ? 'appear' : null }
options={ { origin: animateYAxis + ' ' + animateXAxis } }
Expand Down
34 changes: 19 additions & 15 deletions packages/components/src/popover/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ exports[`Popover should pass additional props to portaled element 1`] = `
tabindex="-1"
>
<div>
<div
class="components-popover is-bottom is-center components-animate__appear is-from-top"
role="tooltip"
style=""
>
<div>
<div
class="components-popover__content"
tabindex="-1"
class="components-popover is-bottom is-center components-animate__appear is-from-top"
role="tooltip"
style=""
>
Hello
<div
class="components-popover__content"
tabindex="-1"
>
Hello
</div>
</div>
</div>
</div>
Expand All @@ -29,15 +31,17 @@ exports[`Popover should render content 1`] = `
tabindex="-1"
>
<div>
<div
class="components-popover is-bottom is-center components-animate__appear is-from-top"
style=""
>
<div>
<div
class="components-popover__content"
tabindex="-1"
class="components-popover is-bottom is-center components-animate__appear is-from-top"
style=""
>
Hello
<div
class="components-popover__content"
tabindex="-1"
>
Hello
</div>
</div>
</div>
</div>
Expand Down
6 changes: 6 additions & 0 deletions packages/format-library/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Internal

- The inline link component has been refactored to focus changes using the `Popover` component's `onFocusOutside` prop.

## 1.2.10 (2019-01-03)

## 1.2.9 (2018-12-18)
Expand Down
10 changes: 5 additions & 5 deletions packages/format-library/src/link/inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class InlineLinkUI extends Component {
this.onKeyDown = this.onKeyDown.bind( this );
this.onChangeInputValue = this.onChangeInputValue.bind( this );
this.setLinkTarget = this.setLinkTarget.bind( this );
this.onClickOutside = this.onClickOutside.bind( this );
this.onFocusOutside = this.onFocusOutside.bind( this );
this.resetState = this.resetState.bind( this );
this.autocompleteRef = createRef();

Expand Down Expand Up @@ -165,13 +165,13 @@ class InlineLinkUI extends Component {
}
}

onClickOutside( event ) {
onFocusOutside() {
// The autocomplete suggestions list renders in a separate popover (in a portal),
// so onClickOutside fails to detect that a click on a suggestion occurred in the
// so onFocusOutside fails to detect that a click on a suggestion occurred in the
// LinkContainer. Detect clicks on autocomplete suggestions using a ref here, and
// return to avoid the popover being closed.
const autocompleteElement = this.autocompleteRef.current;
if ( autocompleteElement && autocompleteElement.contains( event.target ) ) {
if ( autocompleteElement && autocompleteElement.contains( document.activeElement ) ) {
return;
}

Expand All @@ -198,7 +198,7 @@ class InlineLinkUI extends Component {
value={ value }
isActive={ isActive }
addingLink={ addingLink }
onClickOutside={ this.onClickOutside }
onFocusOutside={ this.onFocusOutside }
onClose={ this.resetState }
focusOnMount={ showInput ? 'firstElement' : false }
renderSettings={ () => (
Expand Down