Introduce `<Noninteractive>` component to disable form elements non-visually (https://github.com/woocommerce/woocommerce-blocks/pull/5157)

* Noninteractive component based on Disabled

* Implement in cart/checkout

* Pass down props

* Update 'use-debounce' library

* aria disabled
This commit is contained in:
Mike Jolley 2021-11-26 14:47:37 +00:00 committed by GitHub
parent cbdbc6c7a1
commit c067e990b4
18 changed files with 176 additions and 52 deletions

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import { useRef, useLayoutEffect } from '@wordpress/element';
import { focus } from '@wordpress/dom';
import { useDebouncedCallback } from 'use-debounce';
/**
* Names of control nodes which need to be disabled.
*/
const FOCUSABLE_NODE_NAMES = [
'BUTTON',
'FIELDSET',
'INPUT',
'OPTGROUP',
'OPTION',
'SELECT',
'TEXTAREA',
'A',
];
/**
* Noninteractive component
*
* Makes children elements Noninteractive, preventing both mouse and keyboard events without affecting how the elements
* appear visually. Used for previews.
*
* Based on the <Disabled> component in WordPress.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/disabled/index.js
*/
const Noninteractive = ( {
children,
style = {},
...props
}: {
children: React.ReactChildren;
style?: Record< string, string >;
} ): JSX.Element => {
const node = useRef< HTMLDivElement >( null );
const disableFocus = () => {
if ( node.current ) {
focus.focusable.find( node.current ).forEach( ( focusable ) => {
if ( FOCUSABLE_NODE_NAMES.includes( focusable.nodeName ) ) {
focusable.setAttribute( 'tabindex', '-1' );
}
if ( focusable.hasAttribute( 'contenteditable' ) ) {
focusable.setAttribute( 'contenteditable', 'false' );
}
} );
}
};
// Debounce re-disable since disabling process itself will incur additional mutations which should be ignored.
const debounced = useDebouncedCallback( disableFocus, 0, {
leading: true,
} );
useLayoutEffect( () => {
let observer: MutationObserver | undefined;
disableFocus();
if ( node.current ) {
observer = new window.MutationObserver( debounced );
observer.observe( node.current, {
childList: true,
attributes: true,
subtree: true,
} );
}
return () => {
if ( observer ) {
observer.disconnect();
}
debounced.cancel();
};
}, [ debounced ] );
return (
<div
ref={ node }
aria-disabled="true"
style={ {
userSelect: 'none',
pointerEvents: 'none',
cursor: 'normal',
...style,
} }
{ ...props }
>
{ children }
</div>
);
};
export default Noninteractive;

View File

@ -161,7 +161,7 @@ const usePaymentMethodRegistration = (
registeredPaymentMethods,
] );
const [ debouncedRefreshCanMakePayments ] = useDebouncedCallback(
const debouncedRefreshCanMakePayments = useDebouncedCallback(
refreshCanMakePayments,
500
);

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -19,9 +19,9 @@ export const Edit = ( {
return (
<div { ...blockProps }>
<Disabled>
<Noninteractive>
<Block className={ className } />
</Disabled>
</Noninteractive>
</div>
);
};

View File

@ -3,8 +3,9 @@
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, ToggleControl } from '@wordpress/components';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -97,13 +98,13 @@ export const Edit = ( {
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Noninteractive>
<Block
className={ className }
showRateAfterTaxName={ showRateAfterTaxName }
isShippingCalculatorEnabled={ isShippingCalculatorEnabled }
/>
</Disabled>
</Noninteractive>
</div>
);
};

View File

@ -6,8 +6,8 @@ import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import PageSelector from '@woocommerce/editor-components/page-selector';
import { Disabled } from '@wordpress/components';
import { CART_PAGE_ID } from '@woocommerce/block-settings';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
*/
@ -60,12 +60,12 @@ export const Edit = ( {
/>
) }
</InspectorControls>
<Disabled>
<Noninteractive>
<Block
checkoutPageId={ checkoutPageId }
className={ className }
/>
</Disabled>
</Noninteractive>
</div>
);
};

View File

@ -6,8 +6,9 @@ import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import PageSelector from '@woocommerce/editor-components/page-selector';
import { PanelBody, ToggleControl, Disabled } from '@wordpress/components';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { CHECKOUT_PAGE_ID } from '@woocommerce/block-settings';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
*/
@ -82,12 +83,12 @@ export const Edit = ( {
/>
) }
</InspectorControls>
<Disabled>
<Noninteractive>
<Block
showReturnToCart={ showReturnToCart }
cartPageId={ cartPageId }
/>
</Disabled>
</Noninteractive>
</div>
);
};

View File

@ -2,13 +2,13 @@
* External dependencies
*/
import { useMemo, useEffect, Fragment } from '@wordpress/element';
import { Disabled } from 'wordpress-components';
import {
useCheckoutAddress,
useStoreEvents,
useEditorContext,
} from '@woocommerce/base-context';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -56,7 +56,7 @@ const Block = ( {
};
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
const AddressFormWrapperComponent = isEditor ? Disabled : Fragment;
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
return (
<AddressFormWrapperComponent>

View File

@ -3,8 +3,8 @@
*/
import classnames from 'classnames';
import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -44,9 +44,9 @@ export const Edit = ( {
) }
>
<Controls />
<Disabled>
<Noninteractive>
<Block allowCreateAccount={ allowCreateAccount } />
</Disabled>
</Noninteractive>
<AdditionalFields block={ innerBlockAreas.CONTACT_INFORMATION } />
</FormStepBlock>
);

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { Disabled } from '@wordpress/components';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -14,9 +14,9 @@ export const Edit = (): JSX.Element => {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<Disabled>
<Noninteractive>
<Block />
</Disabled>
</Noninteractive>
</div>
);
};

View File

@ -3,8 +3,9 @@
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { Disabled, PanelBody, ToggleControl } from '@wordpress/components';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -65,11 +66,11 @@ export const Edit = ( {
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Noninteractive>
<Block
showRateAfterTaxName={ attributes.showRateAfterTaxName }
/>
</Disabled>
</Noninteractive>
</div>
);
};

View File

@ -4,10 +4,11 @@
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, Disabled, ExternalLink } from '@wordpress/components';
import { PanelBody, ExternalLink } from '@wordpress/components';
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -85,9 +86,9 @@ export const Edit = ( {
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Noninteractive>
<Block />
</Disabled>
</Noninteractive>
<AdditionalFields block={ innerBlockAreas.PAYMENT_METHODS } />
</FormStepBlock>
);

View File

@ -3,7 +3,6 @@
*/
import { __ } from '@wordpress/i18n';
import { useMemo, useEffect, Fragment } from '@wordpress/element';
import { Disabled } from 'wordpress-components';
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutAddress,
@ -11,6 +10,7 @@ import {
useEditorContext,
} from '@woocommerce/base-context';
import { CheckboxControl } from '@woocommerce/blocks-checkout';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -60,7 +60,7 @@ const Block = ( {
};
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
const AddressFormWrapperComponent = isEditor ? Disabled : Fragment;
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
return (
<>

View File

@ -4,10 +4,11 @@
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, Disabled, ExternalLink } from '@wordpress/components';
import { PanelBody, ExternalLink } from '@wordpress/components';
import { ADMIN_URL, getSetting } from '@woocommerce/settings';
import ExternalLinkCard from '@woocommerce/editor-components/external-link-card';
import { innerBlockAreas } from '@woocommerce/blocks-checkout';
import Noninteractive from '@woocommerce/base-components/noninteractive';
/**
* Internal dependencies
@ -119,9 +120,9 @@ export const Edit = ( {
</PanelBody>
) }
</InspectorControls>
<Disabled>
<Noninteractive>
<Block />
</Disabled>
</Noninteractive>
<AdditionalFields block={ innerBlockAreas.SHIPPING_METHODS } />
</FormStepBlock>
);

View File

@ -69,7 +69,7 @@ const PriceFilterBlock = ( { attributes, isEditor = false } ) => {
);
// Updates the query after a short delay.
const [ debouncedUpdateQuery ] = useDebouncedCallback( onSubmit, 500 );
const debouncedUpdateQuery = useDebouncedCallback( onSubmit, 500 );
// Callback when slider or input fields are changed.
const onChange = useCallback(

View File

@ -28,9 +28,7 @@ mockUtils.getProducts = jest.fn().mockImplementation( () =>
// Add a mock implementation of debounce for testing so we can spy on the onSearch call.
mockUseDebounce.useDebouncedCallback = jest
.fn()
.mockImplementation( ( search ) => [
() => mockUtils.getProducts( search ),
] );
.mockImplementation( ( search ) => () => mockUtils.getProducts( search ) );
describe( 'withSearchedProducts Component', () => {
const { getProducts } = mockUtils;

View File

@ -50,17 +50,14 @@ const withSearchedProducts = (
.catch( setErrorState );
}, [ selected ] );
const [ debouncedSearch ] = useDebouncedCallback(
( search: string ) => {
getProducts( { selected, search } )
.then( ( results ) => {
setProductsList( results as ProductResponseItem[] );
setIsLoading( false );
} )
.catch( setErrorState );
},
400
);
const debouncedSearch = useDebouncedCallback( ( search: string ) => {
getProducts( { selected, search } )
.then( ( results ) => {
setProductsList( results as ProductResponseItem[] );
setIsLoading( false );
} )
.catch( setErrorState );
}, 400 );
const onSearch = useCallback(
( search: string ) => {

View File

@ -15795,6 +15795,16 @@
"dev": true,
"optional": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -21430,6 +21440,13 @@
}
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"filelist": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz",
@ -29326,6 +29343,13 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@ -38756,9 +38780,9 @@
}
},
"use-debounce": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-3.4.3.tgz",
"integrity": "sha512-nxy+opOxDccWfhMl36J5BSCTpvcj89iaQk2OZWLAtBJQj7ISCtx1gh+rFbdjGfMl6vtCZf6gke/kYvrkVfHMoA=="
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-7.0.1.tgz",
"integrity": "sha512-fOrzIw2wstbAJuv8PC9Vg4XgwyTLEOdq4y/Z3IhVl8DAE4svRcgyEUvrEXu+BMNgMoc3YND6qLT61kkgEKXh7Q=="
},
"use-enhanced-state": {
"version": "0.0.13",
@ -39105,7 +39129,11 @@
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",

View File

@ -207,7 +207,7 @@
"react-number-format": "4.4.3",
"reakit": "1.3.11",
"trim-html": "0.1.9",
"use-debounce": "3.4.3",
"use-debounce": "7.0.1",
"wordpress-components": "npm:@wordpress/components@11.1.6"
},
"husky": {