Skip to content

Commit

Permalink
Improve the useDisabled hook and Disabled component (#40631)
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad authored Apr 27, 2022
1 parent fd1c672 commit 422933d
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 135 deletions.
95 changes: 5 additions & 90 deletions packages/components/src/disabled/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
/**
* External dependencies
*/
import { includes, debounce } from 'lodash';
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import {
createContext,
useCallback,
useLayoutEffect,
useRef,
} from '@wordpress/element';
import { focus } from '@wordpress/dom';
import { __experimentalUseDisabled as useDisabled } from '@wordpress/compose';
import { createContext } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -23,25 +17,6 @@ import { StyledWrapper } from './styles/disabled-styles';
const Context = createContext( false );
const { Consumer, Provider } = Context;

/**
* Names of control nodes which qualify for disabled behavior.
*
* See WHATWG HTML Standard: 4.10.18.5: "Enabling and disabling form controls: the disabled attribute".
*
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute
*
* @type {string[]}
*/
const DISABLED_ELIGIBLE_NODE_NAMES = [
'BUTTON',
'FIELDSET',
'INPUT',
'OPTGROUP',
'OPTION',
'SELECT',
'TEXTAREA',
];

/**
* @typedef OwnProps
* @property {string} [className] Classname for the disabled element.
Expand All @@ -54,68 +29,8 @@ const DISABLED_ELIGIBLE_NODE_NAMES = [
* @return {JSX.Element} Element wrapping the children to disable them when isDisabled is true.
*/
function Disabled( { className, children, isDisabled = true, ...props } ) {
/** @type {import('react').RefObject<HTMLDivElement>} */
const node = useRef( null );

const disable = () => {
if ( ! node.current ) {
return;
}

focus.focusable.find( node.current ).forEach( ( focusable ) => {
if (
includes( DISABLED_ELIGIBLE_NODE_NAMES, focusable.nodeName )
) {
focusable.setAttribute( 'disabled', '' );
}

if ( focusable.nodeName === 'A' ) {
focusable.setAttribute( 'tabindex', '-1' );
}

const tabIndex = focusable.getAttribute( 'tabindex' );
if ( tabIndex !== null && tabIndex !== '-1' ) {
focusable.removeAttribute( 'tabindex' );
}

if ( focusable.hasAttribute( 'contenteditable' ) ) {
focusable.setAttribute( 'contenteditable', 'false' );
}
} );
};

// Debounce re-disable since disabling process itself will incur
// additional mutations which should be ignored.
const debouncedDisable = useCallback(
debounce( disable, undefined, { leading: true } ),
[]
);

useLayoutEffect( () => {
if ( ! isDisabled ) {
return;
}

disable();

/** @type {MutationObserver | undefined} */
let observer;
if ( node.current ) {
observer = new window.MutationObserver( debouncedDisable );
observer.observe( node.current, {
childList: true,
attributes: true,
subtree: true,
} );
}

return () => {
if ( observer ) {
observer.disconnect();
}
debouncedDisable.cancel();
};
}, [] );
/** @type {import('react').RefCallback<HTMLDivElement>} */
const ref = useDisabled();

if ( ! isDisabled ) {
return <Provider value={ false }>{ children }</Provider>;
Expand All @@ -124,7 +39,7 @@ function Disabled( { className, children, isDisabled = true, ...props } ) {
return (
<Provider value={ true }>
<StyledWrapper
ref={ node }
ref={ ref }
className={ classnames( className, 'components-disabled' ) }
{ ...props }
>
Expand Down
90 changes: 45 additions & 45 deletions packages/compose/src/hooks/use-disabled/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import { includes, debounce } from 'lodash';
/**
* WordPress dependencies
*/
import { useCallback, useLayoutEffect, useRef } from '@wordpress/element';
import { focus } from '@wordpress/dom';

/**
* Internal dependencies
*/
import useRefEffect from '../use-ref-effect';

/**
* Names of control nodes which qualify for disabled behavior.
*
Expand All @@ -33,7 +37,7 @@ const DISABLED_ELIGIBLE_NODE_NAMES = [
* (input fields, links, buttons, etc.) need to be disabled. This hook adds the
* behavior to disable nested DOM elements to the returned ref.
*
* @return {import('react').RefObject<HTMLElement>} Element Ref.
* @return {import('react').RefCallback<HTMLElement>} Element Ref.
*
* @example
* ```js
Expand All @@ -50,56 +54,54 @@ const DISABLED_ELIGIBLE_NODE_NAMES = [
* ```
*/
export default function useDisabled() {
/** @type {import('react').RefObject<HTMLElement>} */
const node = useRef( null );
return useRefEffect( ( node ) => {
const disable = () => {
node.style.setProperty( 'user-select', 'none' );
node.style.setProperty( '-webkit-user-select', 'none' );
focus.focusable.find( node ).forEach( ( focusable ) => {
if (
includes( DISABLED_ELIGIBLE_NODE_NAMES, focusable.nodeName )
) {
focusable.setAttribute( 'disabled', '' );
}

const disable = () => {
if ( ! node.current ) {
return;
}
if ( focusable.nodeName === 'A' ) {
focusable.setAttribute( 'tabindex', '-1' );
}

focus.focusable.find( node.current ).forEach( ( focusable ) => {
if (
includes( DISABLED_ELIGIBLE_NODE_NAMES, focusable.nodeName )
) {
focusable.setAttribute( 'disabled', '' );
}
const tabIndex = focusable.getAttribute( 'tabindex' );
if ( tabIndex !== null && tabIndex !== '-1' ) {
focusable.removeAttribute( 'tabindex' );
}

if ( focusable.nodeName === 'A' ) {
focusable.setAttribute( 'tabindex', '-1' );
}
if ( focusable.hasAttribute( 'contenteditable' ) ) {
focusable.setAttribute( 'contenteditable', 'false' );
}

const tabIndex = focusable.getAttribute( 'tabindex' );
if ( tabIndex !== null && tabIndex !== '-1' ) {
focusable.removeAttribute( 'tabindex' );
}
if (
node.ownerDocument.defaultView?.HTMLElement &&
focusable instanceof
node.ownerDocument.defaultView.HTMLElement
) {
focusable.style.setProperty( 'pointer-events', 'none' );
}
} );
};

if ( focusable.hasAttribute( 'contenteditable' ) ) {
focusable.setAttribute( 'contenteditable', 'false' );
}
// Debounce re-disable since disabling process itself will incur
// additional mutations which should be ignored.
const debouncedDisable = debounce( disable, undefined, {
leading: true,
} );
};

// Debounce re-disable since disabling process itself will incur
// additional mutations which should be ignored.
const debouncedDisable = useCallback(
debounce( disable, undefined, { leading: true } ),
[]
);

useLayoutEffect( () => {
disable();

/** @type {MutationObserver | undefined} */
let observer;
if ( node.current ) {
observer = new window.MutationObserver( debouncedDisable );
observer.observe( node.current, {
childList: true,
attributes: true,
subtree: true,
} );
}
const observer = new window.MutationObserver( debouncedDisable );
observer.observe( node, {
childList: true,
attributes: true,
subtree: true,
} );

return () => {
if ( observer ) {
Expand All @@ -108,6 +110,4 @@ export default function useDisabled() {
debouncedDisable.cancel();
};
}, [] );

return node;
}

0 comments on commit 422933d

Please sign in to comment.