Skip to content

Commit

Permalink
Add a useDialog hook and replace the duplicated PopoverWrapper (#27643)
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad authored Dec 11, 2020
1 parent 7cc1ba1 commit f9c2fa3
Show file tree
Hide file tree
Showing 15 changed files with 250 additions and 339 deletions.
52 changes: 28 additions & 24 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/compose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"clipboard": "^2.0.1",
"lodash": "^4.17.19",
"mousetrap": "^1.6.5",
"react-merge-refs": "^1.0.0",
"react-resize-aware": "^3.0.1",
"use-memo-one": "^1.1.1"
},
Expand Down
44 changes: 44 additions & 0 deletions packages/compose/src/hooks/use-dialog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
`useDialog`
===========

React hook to be used on a dialog wrapper to enable the following behaviors:

- constrained tabbing.
- focus on mount.
- return focus on unmount.
- focus outside.

## Returned value

The hooks returns an array composed of the two following values:

### `ref`

- Type: `Object|Function`

A React ref that must be passed to the DOM element where the behavior should be attached.

### `props`

- Type: `Object`

Extra props to apply to the wrapper.

## Usage

```jsx
import { __experimentalUseDialog as useDialog } from '@wordpress/compose';

const MyDialog = () => {
const [ ref, extraProps ] = useDialog( {
onClose: () => console.log('do something to close the dialog')
} );

return (
<div ref={ ref } {...extraProps}>
<Button />
<Button />
</div>
);
};
```
68 changes: 68 additions & 0 deletions packages/compose/src/hooks/use-dialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* External dependencies
*/
import mergeRefs from 'react-merge-refs';

/**
* WordPress dependencies
*/
import { useRef, useCallback, useEffect } from '@wordpress/element';
import { ESCAPE } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import useConstrainedTabbing from '../use-constrained-tabbing';
import useFocusOnMount from '../use-focus-on-mount';
import useFocusReturn from '../use-focus-return';
import useFocusOutside from '../use-focus-outside';

/**
* Returns a ref and props to apply to a dialog wrapper to enable the following behaviors:
* - constrained tabbing.
* - focus on mount.
* - return focus on unmount.
* - focus outside.
*
* @param {Object} options Dialog Options.
*/
function useDialog( options ) {
const onClose = useRef();
useEffect( () => {
onClose.current = options.onClose;
}, [ options.onClose ] );
const constrainedTabbingRef = useConstrainedTabbing();
const focusOnMountRef = useFocusOnMount();
const focusReturnRef = useFocusReturn();
const focusOutsideProps = useFocusOutside( options.onClose );
const closeOnEscapeRef = useCallback( ( node ) => {
if ( ! node ) {
return;
}

node.addEventListener( 'keydown', ( event ) => {
// Close on escape
if ( event.keyCode === ESCAPE && onClose.current ) {
event.stopPropagation();
onClose.current();
}
} );

node.addEventListener( 'mousedown', ( event ) => {
// I'm not really certain what this is for but it matches the previous behavior.
event.stopPropagation();
} );
}, [] );

return [
mergeRefs( [
constrainedTabbingRef,
focusOnMountRef,
focusReturnRef,
closeOnEscapeRef,
] ),
focusOutsideProps,
];
}

export default useDialog;
26 changes: 16 additions & 10 deletions packages/compose/src/hooks/use-focus-on-mount/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { focus } from '@wordpress/dom';
import { useEffect, useRef } from '@wordpress/element';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';

/**
* Hook used to focus the first tabbable element on mount.
Expand All @@ -27,48 +27,54 @@ import { useEffect, useRef } from '@wordpress/element';
*/
function useFocusOnMount( focusOnMount = 'firstElement' ) {
const focusOnMountRef = useRef( focusOnMount );
const [ node, setNode ] = useState();
useEffect( () => {
focusOnMountRef.current = focusOnMount;
}, [ focusOnMount ] );

// Ideally we should be using a function ref (useCallback)
// Ideally we should be running the focus behavior in the useCallback directly
// Right now we have some issues where the link popover remounts
// prevents us from doing that.
// The downside of the current implementation is that it doesn't update if the "ref" changes.
const ref = useRef();
const ref = useCallback( setNode, [] );

// Focus handling
useEffect( () => {
if ( ! node ) {
return;
}
/*
* Without the setTimeout, the dom node is not being focused. Related:
* https://stackoverflow.com/questions/35522220/react-ref-with-focus-doesnt-work-without-settimeout-my-example
*
* TODO: Treat the cause, not the symptom.
*/
const focusTimeout = setTimeout( () => {
if ( ! focusOnMountRef.current || ! ref.current ) {
if ( focusOnMountRef.current === false || ! node ) {
return;
}

if ( focusOnMountRef.current === 'firstElement' ) {
const firstTabbable = focus.tabbable.find( ref.current )[ 0 ];
const firstTabbable = focus.tabbable.find( node )[ 0 ];

if ( firstTabbable ) {
firstTabbable.focus();
} else {
ref.current.focus();
node.focus();
}

return;
}

if ( focusOnMountRef.current === 'container' ) {
ref.current.focus();
if (
focusOnMountRef.current === 'container' ||
focusOnMountRef.current === true
) {
node.focus();
}
}, 0 );

return () => clearTimeout( focusTimeout );
}, [] );
}, [ node ] );

return ref;
}
Expand Down
1 change: 1 addition & 0 deletions packages/compose/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as withState } from './higher-order/with-state';
// Hooks
export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing';
export { default as useCopyOnClick } from './hooks/use-copy-on-click';
export { default as __experimentalUseDialog } from './hooks/use-dialog';
export { default as __experimentalUseDragging } from './hooks/use-dragging';
export { default as useFocusOnMount } from './hooks/use-focus-on-mount';
export { default as __experimentalUseFocusOutside } from './hooks/use-focus-outside';
Expand Down
56 changes: 29 additions & 27 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
Popover,
__unstableUseDrop as useDrop,
} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import {
useViewportMatch,
__experimentalUseDialog as useDialog,
} from '@wordpress/compose';
import { PluginArea } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import {
Expand All @@ -52,7 +55,6 @@ import SettingsSidebar from '../sidebar/settings-sidebar';
import MetaBoxes from '../meta-boxes';
import WelcomeGuide from '../welcome-guide';
import ActionsPanel from './actions-panel';
import PopoverWrapper from './popover-wrapper';

const interfaceLabels = {
secondarySidebar: __( 'Block library' ),
Expand Down Expand Up @@ -169,6 +171,9 @@ function Layout( { settings } ) {

useDrop( ref );
useEditorStyles( ref, settings.styles );
const [ inserterDialogRef, inserterDialogProps ] = useDialog( {
onClose: () => setIsInserterOpened( false ),
} );

return (
<>
Expand All @@ -194,34 +199,31 @@ function Layout( { settings } ) {
secondarySidebar={
mode === 'visual' &&
isInserterOpened && (
<PopoverWrapper
className="edit-post-layout__inserter-panel-popover-wrapper"
onClose={ () => setIsInserterOpened( false ) }
<div
ref={ inserterDialogRef }
{ ...inserterDialogProps }
className="edit-post-layout__inserter-panel"
>
<div className="edit-post-layout__inserter-panel">
<div className="edit-post-layout__inserter-panel-header">
<Button
icon={ close }
onClick={ () =>
setIsInserterOpened( false )
}
/>
</div>
<div className="edit-post-layout__inserter-panel-content">
<Library
showMostUsedBlocks={
showMostUsedBlocks
<div className="edit-post-layout__inserter-panel-header">
<Button
icon={ close }
onClick={ () =>
setIsInserterOpened( false )
}
/>
</div>
<div className="edit-post-layout__inserter-panel-content">
<Library
showMostUsedBlocks={ showMostUsedBlocks }
showInserterHelpPanel
onSelect={ () => {
if ( isMobileViewport ) {
setIsInserterOpened( false );
}
showInserterHelpPanel
onSelect={ () => {
if ( isMobileViewport ) {
setIsInserterOpened( false );
}
} }
/>
</div>
} }
/>
</div>
</PopoverWrapper>
</div>
)
}
sidebar={
Expand Down
Loading

0 comments on commit f9c2fa3

Please sign in to comment.