diff --git a/lib/compat/wordpress-6.2/script-loader.php b/lib/compat/wordpress-6.2/script-loader.php new file mode 100644 index 00000000000000..06f01e03b6121a --- /dev/null +++ b/lib/compat/wordpress-6.2/script-loader.php @@ -0,0 +1,30 @@ +query( 'wp-inert-polyfill', 'registered' ); + if ( ! $script ) { + $scripts->add( 'wp-inert-polyfill', gutenberg_url( 'build/vendors/inert-polyfill' . $extension ), array() ); + } + + $script = $scripts->query( 'wp-polyfill', 'registered' ); + $script->deps = array_merge( $script->deps, array( 'wp-inert-polyfill' ) ); +} +add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts_62' ); diff --git a/lib/load.php b/lib/load.php index 7ece2d359b5bfa..65e8e3e2cb220b 100644 --- a/lib/load.php +++ b/lib/load.php @@ -95,6 +95,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.1/template-parts-screen.php'; require __DIR__ . '/compat/wordpress-6.1/theme.php'; +// WordPress 6.2 compat. +require __DIR__ . '/compat/wordpress-6.2/script-loader.php'; + // Experimental features. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WP 6.0's stopgap handler for Webfonts API. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/package-lock.json b/package-lock.json index 209fcbb32ff033..69e567321bc604 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60048,6 +60048,11 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wicg-inert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.2.tgz", + "integrity": "sha512-Ba9tGNYxXwaqKEi9sJJvPMKuo063umUPsHN0JJsjrs2j8KDSzkWLMZGZ+MH1Jf1Fq4OWZ5HsESJID6nRza2ang==" + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index cdf826f2e735b7..9fd2335be231ca 100755 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "@wordpress/viewport": "file:packages/viewport", "@wordpress/warning": "file:packages/warning", "@wordpress/widgets": "file:packages/widgets", - "@wordpress/wordcount": "file:packages/wordcount" + "@wordpress/wordcount": "file:packages/wordcount", + "wicg-inert": "3.1.2" }, "devDependencies": { "@actions/core": "1.8.0", diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js index 48d19553059a74..ddf4ee0ec75abd 100644 --- a/packages/block-editor/src/components/block-preview/auto.js +++ b/packages/block-editor/src/components/block-preview/auto.js @@ -1,10 +1,10 @@ /** * WordPress dependencies */ -import { Disabled } from '@wordpress/components'; import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; +import { Disabled } from '@wordpress/components'; /** * Internal dependencies diff --git a/packages/block-editor/src/components/block-preview/live.js b/packages/block-editor/src/components/block-preview/live.js index 792740bb2c02b6..5015c778defe78 100644 --- a/packages/block-editor/src/components/block-preview/live.js +++ b/packages/block-editor/src/components/block-preview/live.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { Disabled } from '@wordpress/components'; - /** * Internal dependencies */ @@ -16,9 +11,9 @@ export default function LiveBlockPreview( { onClick } ) { onClick={ onClick } onKeyPress={ onClick } > - +
- +
); } diff --git a/packages/block-editor/src/components/block-preview/test/index.js b/packages/block-editor/src/components/block-preview/test/index.js index 119660e7575153..b83bef8a1453b1 100644 --- a/packages/block-editor/src/components/block-preview/test/index.js +++ b/packages/block-editor/src/components/block-preview/test/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * WordPress dependencies @@ -99,12 +99,6 @@ describe( 'useBlockPreview', () => { ); expect( previewedBlockContents ).toBeInTheDocument(); - // Test elements within block contents are disabled. - await waitFor( () => { - const button = screen.getByText( 'Button' ); - expect( button.hasAttribute( 'disabled' ) ).toBe( true ); - } ); - // Ensure the block preview class names are merged with the component's class name. expect( container.firstChild.className ).toBe( 'test-container-classname block-editor-block-preview__live-content components-disabled' diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js index eb8ba03cbf0468..3597bc07b07f79 100644 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ b/packages/block-editor/src/components/block-tools/back-compat.js @@ -23,6 +23,7 @@ export default function BlockToolsBackCompat( { children } ) { deprecated( 'wp.components.Popover.Slot name="block-toolbar"', { alternative: 'wp.blockEditor.BlockTools', since: '5.8', + version: '6.3', } ); return ( diff --git a/packages/block-library/src/comments/edit/placeholder.js b/packages/block-library/src/comments/edit/placeholder.js index 837561aad15c00..1f0c44e8984611 100644 --- a/packages/block-library/src/comments/edit/placeholder.js +++ b/packages/block-library/src/comments/edit/placeholder.js @@ -5,7 +5,6 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { __, sprintf } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { useEntityProp } from '@wordpress/core-data'; -import { useDisabled } from '@wordpress/compose'; /** * Internal dependencies @@ -22,13 +21,8 @@ export default function PostCommentsPlaceholder( { postType, postId } ) { .__experimentalDiscussionSettings ); - const disabledRef = useDisabled(); - return ( -
+

{ /* translators: %s: Post title. */ diff --git a/packages/block-library/src/post-comments-form/form.js b/packages/block-library/src/post-comments-form/form.js index a71f8231acafad..0fc858c178a6e8 100644 --- a/packages/block-library/src/post-comments-form/form.js +++ b/packages/block-library/src/post-comments-form/form.js @@ -13,18 +13,17 @@ import { __experimentalGetElementClassName, } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; -import { useDisabled, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; const CommentsFormPlaceholder = () => { - const disabledFormRef = useDisabled(); const instanceId = useInstanceId( CommentsFormPlaceholder ); return (

{ __( 'Leave a Reply' ) }

-
+

diff --git a/packages/components/src/disabled/index.tsx b/packages/components/src/disabled/index.tsx index 3cda56a27b5b23..6ef627e8350fa3 100644 --- a/packages/components/src/disabled/index.tsx +++ b/packages/components/src/disabled/index.tsx @@ -1,13 +1,7 @@ -/** - * External dependencies - */ -import type { HTMLProps } from 'react'; - /** * WordPress dependencies */ -import { useDisabled } from '@wordpress/compose'; -import { createContext, forwardRef } from '@wordpress/element'; +import { createContext } from '@wordpress/element'; /** * Internal dependencies @@ -20,15 +14,6 @@ import { useCx } from '../utils'; const Context = createContext< boolean >( false ); const { Consumer, Provider } = Context; -// Extracting this ContentWrapper component in order to make it more explicit -// the same 'ContentWrapper' component is needed so that React can reconcile -// the dom correctly when switching between disabled/non-disabled (instead -// of thrashing the previous DOM and therefore losing the form fields values). -const ContentWrapper = forwardRef< - HTMLDivElement, - HTMLProps< HTMLDivElement > ->( ( props, ref ) =>
); - /** * `Disabled` is a component which disables descendant tabbable elements and prevents pointer interaction. * @@ -65,29 +50,22 @@ function Disabled( { isDisabled = true, ...props }: WordPressComponentProps< DisabledProps, 'div' > ) { - const ref = useDisabled(); const cx = useCx(); - if ( ! isDisabled ) { - return ( - - { children } - - ); - } return ( - - +
{ children } - +
); } diff --git a/packages/components/src/disabled/test/index.tsx b/packages/components/src/disabled/test/index.tsx index 7b55b0f79c3832..4d11bcd77d0e9d 100644 --- a/packages/components/src/disabled/test/index.tsx +++ b/packages/components/src/disabled/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * Internal dependencies @@ -9,35 +9,6 @@ import { render, screen, waitFor } from '@testing-library/react'; import Disabled from '../'; import userEvent from '@testing-library/user-event'; -jest.mock( '@wordpress/dom', () => { - const focus = jest.requireActual( '../../../../dom/src' ).focus; - return { - focus: { - ...focus, - focusable: { - ...focus.focusable, - find( context: Element, options = { sequential: false } ) { - // In JSDOM, all elements have zero'd widths and height. - // This is a metric for focusable's `isVisible`, so find - // and apply an arbitrary non-zero width. - Array.from( context.querySelectorAll( '*' ) ).forEach( - ( element ) => { - Object.defineProperties( element, { - offsetWidth: { - get: () => 1, - configurable: true, - }, - } ); - } - ); - - return focus.focusable.find( context, options ); - }, - }, - }, - }; -} ); - describe( 'Disabled', () => { const Form = () => ( @@ -47,18 +18,14 @@ describe( 'Disabled', () => { ); it( 'will disable all fields', () => { - render( + const { container } = render( ); - const input = screen.getByRole( 'textbox' ); - const contentEditable = screen.getByTitle( 'edit my content' ); - expect( input ).toBeDisabled(); - expect( contentEditable ).toHaveAttribute( 'contenteditable', 'false' ); - expect( contentEditable ).not.toHaveAttribute( 'tabindex' ); - expect( contentEditable ).not.toHaveAttribute( 'disabled' ); + // @ts-ignore + expect( container.firstChild.hasAttribute( 'inert' ) ).toBe( true ); } ); it( 'should cleanly un-disable via reconciliation', () => { @@ -71,19 +38,15 @@ describe( 'Disabled', () => { ); - const { rerender } = render( ); + const { container, rerender } = render( ); - const input = screen.getByRole( 'textbox' ); - const contentEditable = screen.getByTitle( 'edit my content' ); - - expect( input ).toBeDisabled(); - expect( contentEditable ).toHaveAttribute( 'contenteditable', 'false' ); + // @ts-ignore + expect( container.firstChild.hasAttribute( 'inert' ) ).toBe( true ); rerender( ); - expect( input ).not.toBeDisabled(); - expect( contentEditable ).toHaveAttribute( 'contenteditable', 'true' ); - expect( contentEditable ).toHaveAttribute( 'tabindex' ); + // @ts-ignore + expect( container.firstChild.hasAttribute( 'inert' ) ).toBe( false ); } ); it( 'will disable or enable descendant fields based on the isDisabled prop value', () => { @@ -93,46 +56,15 @@ describe( 'Disabled', () => { ); - const { rerender } = render( ); - - const input = screen.getByRole( 'textbox' ); - const contentEditable = screen.getByTitle( 'edit my content' ); + const { rerender, container } = render( ); - expect( input ).toBeDisabled(); - expect( contentEditable ).toHaveAttribute( 'contenteditable', 'false' ); + // @ts-ignore + expect( container.firstChild.hasAttribute( 'inert' ) ).toBe( true ); rerender( ); - expect( input ).not.toBeDisabled(); - expect( contentEditable ).toHaveAttribute( 'contenteditable', 'true' ); - } ); - - it( 'will disable all fields on sneaky DOM manipulation', async () => { - render( - - - - ); - - const form = screen.getByTitle( 'form' ); - form.insertAdjacentHTML( - 'beforeend', - '' - ); - form.insertAdjacentHTML( - 'beforeend', - '
' - ); - const sneakyInput = screen.getByTitle( 'sneaky input' ); - const sneakyEditable = screen.getByTitle( 'sneaky editable content' ); - - await waitFor( () => expect( sneakyInput ).toBeDisabled() ); - await waitFor( () => - expect( sneakyEditable ).toHaveAttribute( - 'contenteditable', - 'false' - ) - ); + // @ts-ignore + expect( container.firstChild.hasAttribute( 'inert' ) ).toBe( false ); } ); it( 'should preserve input values when toggling the isDisabled prop', async () => { diff --git a/packages/compose/README.md b/packages/compose/README.md index c11f4d76f9a369..6cc928be13096b 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -289,10 +289,13 @@ In some circumstances, such as block previews, all focusable DOM elements (input fields, links, buttons, etc.) need to be disabled. This hook adds the behavior to disable nested DOM elements to the returned ref. +If you can, prefer the use of the inert HTML attribute. + _Usage_ ```js import { useDisabled } from '@wordpress/compose'; + const DisabledExample = () => { const disabledRef = useDisabled(); return ( diff --git a/packages/compose/src/hooks/use-disabled/index.js b/packages/compose/src/hooks/use-disabled/index.js deleted file mode 100644 index fa8848046147a7..00000000000000 --- a/packages/compose/src/hooks/use-disabled/index.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * WordPress dependencies - */ -import { focus } from '@wordpress/dom'; - -/** - * Internal dependencies - */ -import { debounce } from '../../utils/debounce'; -import useRefEffect from '../use-ref-effect'; - -/** - * 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', -]; - -/** - * In some circumstances, such as block previews, all focusable DOM elements - * (input fields, links, buttons, etc.) need to be disabled. This hook adds the - * behavior to disable nested DOM elements to the returned ref. - * - * @param {Object} config Configuration object. - * @param {boolean=} config.isDisabled Whether the element should be disabled. - * @return {import('react').RefCallback} Element Ref. - * - * @example - * ```js - * import { useDisabled } from '@wordpress/compose'; - * const DisabledExample = () => { - * const disabledRef = useDisabled(); - * return ( - * - * ); - * }; - * ``` - */ -export default function useDisabled( { - isDisabled: isDisabledProp = false, -} = {} ) { - return useRefEffect( - ( node ) => { - if ( isDisabledProp ) { - return; - } - - /** A variable keeping track of the previous updates in order to restore them. */ - /** @type {Function[]} */ - const updates = []; - - const disable = () => { - if ( node.style.getPropertyValue( 'user-select' ) !== 'none' ) { - const previousValue = - node.style.getPropertyValue( 'user-select' ); - node.style.setProperty( 'user-select', 'none' ); - node.style.setProperty( '-webkit-user-select', 'none' ); - updates.push( () => { - if ( ! node.isConnected ) { - return; - } - node.style.setProperty( 'user-select', previousValue ); - node.style.setProperty( - '-webkit-user-select', - previousValue - ); - } ); - } - - focus.focusable.find( node ).forEach( ( focusable ) => { - if ( - DISABLED_ELIGIBLE_NODE_NAMES.includes( - focusable.nodeName - ) && - // @ts-ignore - ! focusable.disabled - ) { - focusable.setAttribute( 'disabled', '' ); - updates.push( () => { - if ( ! focusable.isConnected ) { - return; - } - // @ts-ignore - focusable.disabled = false; - } ); - } - - if ( - focusable.nodeName === 'A' && - focusable.getAttribute( 'tabindex' ) !== '-1' - ) { - const previousValue = - focusable.getAttribute( 'tabindex' ); - focusable.setAttribute( 'tabindex', '-1' ); - updates.push( () => { - if ( ! focusable.isConnected ) { - return; - } - if ( ! previousValue ) { - focusable.removeAttribute( 'tabindex' ); - } else { - focusable.setAttribute( - 'tabindex', - previousValue - ); - } - } ); - } - - const tabIndex = focusable.getAttribute( 'tabindex' ); - if ( tabIndex !== null && tabIndex !== '-1' ) { - focusable.removeAttribute( 'tabindex' ); - updates.push( () => { - if ( ! focusable.isConnected ) { - return; - } - focusable.setAttribute( 'tabindex', tabIndex ); - } ); - } - - if ( - focusable.hasAttribute( 'contenteditable' ) && - focusable.getAttribute( 'contenteditable' ) !== 'false' - ) { - focusable.setAttribute( 'contenteditable', 'false' ); - updates.push( () => { - if ( ! focusable.isConnected ) { - return; - } - focusable.setAttribute( 'contenteditable', 'true' ); - } ); - } - - if ( - node.ownerDocument.defaultView?.HTMLElement && - focusable instanceof - node.ownerDocument.defaultView.HTMLElement - ) { - const previousValue = - focusable.style.getPropertyValue( - 'pointer-events' - ); - focusable.style.setProperty( 'pointer-events', 'none' ); - updates.push( () => { - if ( ! focusable.isConnected ) { - return; - } - focusable.style.setProperty( - 'pointer-events', - previousValue - ); - } ); - } - } ); - }; - - // Debounce re-disable since disabling process itself will incur - // additional mutations which should be ignored. - const debouncedDisable = debounce( disable, 0, { - leading: true, - } ); - disable(); - - /** @type {MutationObserver | undefined} */ - const observer = new window.MutationObserver( debouncedDisable ); - observer.observe( node, { - childList: true, - attributes: true, - subtree: true, - } ); - - return () => { - if ( observer ) { - observer.disconnect(); - } - debouncedDisable.cancel(); - updates.forEach( ( update ) => update() ); - }; - }, - [ isDisabledProp ] - ); -} diff --git a/packages/compose/src/hooks/use-disabled/index.ts b/packages/compose/src/hooks/use-disabled/index.ts new file mode 100644 index 00000000000000..7a384cbcc546f7 --- /dev/null +++ b/packages/compose/src/hooks/use-disabled/index.ts @@ -0,0 +1,81 @@ +/** + * Internal dependencies + */ +import { debounce } from '../../utils/debounce'; +import useRefEffect from '../use-ref-effect'; + +/** + * In some circumstances, such as block previews, all focusable DOM elements + * (input fields, links, buttons, etc.) need to be disabled. This hook adds the + * behavior to disable nested DOM elements to the returned ref. + * + * If you can, prefer the use of the inert HTML attribute. + * + * @param {Object} config Configuration object. + * @param {boolean=} config.isDisabled Whether the element should be disabled. + * @return {import('react').RefCallback} Element Ref. + * + * @example + * ```js + * import { useDisabled } from '@wordpress/compose'; + * + * const DisabledExample = () => { + * const disabledRef = useDisabled(); + * return ( + * + * ); + * }; + * ``` + */ +export default function useDisabled( { + isDisabled: isDisabledProp = false, +} = {} ) { + return useRefEffect( + ( node ) => { + if ( isDisabledProp ) { + return; + } + + /** A variable keeping track of the previous updates in order to restore them. */ + const updates: Function[] = []; + const disable = () => { + node.childNodes.forEach( ( child ) => { + if ( ! ( child instanceof HTMLElement ) ) { + return; + } + if ( ! child.getAttribute( 'inert' ) ) { + child.setAttribute( 'inert', 'true' ); + updates.push( () => { + child.removeAttribute( 'inert' ); + } ); + } + } ); + }; + + // Debounce re-disable since disabling process itself will incur + // additional mutations which should be ignored. + const debouncedDisable = debounce( disable, 0, { + leading: true, + } ); + disable(); + + /** @type {MutationObserver | undefined} */ + const observer = new window.MutationObserver( debouncedDisable ); + observer.observe( node, { + childList: true, + } ); + + return () => { + if ( observer ) { + observer.disconnect(); + } + debouncedDisable.cancel(); + updates.forEach( ( update ) => update() ); + }; + }, + [ isDisabledProp ] + ); +} diff --git a/packages/compose/src/hooks/use-disabled/test/index.js b/packages/compose/src/hooks/use-disabled/test/index.js index 9b5e7171678195..edf01c3ee602cc 100644 --- a/packages/compose/src/hooks/use-disabled/test/index.js +++ b/packages/compose/src/hooks/use-disabled/test/index.js @@ -13,36 +13,6 @@ import { forwardRef } from '@wordpress/element'; */ import useDisabled from '../'; -jest.mock( '@wordpress/dom', () => { - const focus = jest.requireActual( '../../../../../dom/src' ).focus; - - return { - focus: { - ...focus, - focusable: { - ...focus.focusable, - find( context ) { - // In JSDOM, all elements have zero'd widths and height. - // This is a metric for focusable's `isVisible`, so find - // and apply an arbitrary non-zero width. - Array.from( context.querySelectorAll( '*' ) ).forEach( - ( element ) => { - Object.defineProperties( element, { - offsetWidth: { - get: () => 1, - configurable: true, - }, - } ); - } - ); - - return focus.focusable.find( ...arguments ); - }, - }, - }, - }; -} ); - jest.useRealTimers(); describe( 'useDisabled', () => { @@ -69,11 +39,9 @@ describe( 'useDisabled', () => { const link = screen.getByRole( 'link' ); const p = container.querySelector( 'p' ); - expect( input.hasAttribute( 'disabled' ) ).toBe( true ); - expect( link.getAttribute( 'tabindex' ) ).toBe( '-1' ); - expect( p.getAttribute( 'contenteditable' ) ).toBe( 'false' ); - expect( p.hasAttribute( 'tabindex' ) ).toBe( false ); - expect( p.hasAttribute( 'disabled' ) ).toBe( false ); + expect( input.hasAttribute( 'inert' ) ).toBe( true ); + expect( link.hasAttribute( 'inert' ) ).toBe( true ); + expect( p.hasAttribute( 'inert' ) ).toBe( true ); } ); it( 'will disable an element rendered in an update to the component', async () => { @@ -86,7 +54,7 @@ describe( 'useDisabled', () => { const button = screen.getByText( 'Button' ); await waitFor( () => { - expect( button.hasAttribute( 'disabled' ) ).toBe( true ); + expect( button.hasAttribute( 'inert' ) ).toBe( true ); } ); } ); } ); diff --git a/packages/scripts/scripts/check-licenses.js b/packages/scripts/scripts/check-licenses.js index 5f0526b6752385..ad34c06e268a39 100644 --- a/packages/scripts/scripts/check-licenses.js +++ b/packages/scripts/scripts/check-licenses.js @@ -60,6 +60,7 @@ const gpl2CompatibleLicenses = [ 'ODC-By-1.0', 'Public Domain', 'Unlicense', + 'W3C-20150513', 'WTFPL', 'Zlib', ]; diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 3159f4a4cc4268..30f73a82fa0da2 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -99,6 +99,10 @@ const vendors = { 'react-dom/umd/react-dom.development.js', 'react-dom/umd/react-dom.production.min.js', ], + 'inert-polyfill': [ + 'wicg-inert/dist/inert.js', + 'wicg-inert/dist/inert.min.js', + ], }; const vendorsCopyConfig = Object.entries( vendors ).flatMap( ( [ key, [ devFilename, prodFilename ] ] ) => {