diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 2cbf5c32e814b..e6451c6a4a408 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -26,9 +26,10 @@ import { PluginArea } from '@wordpress/plugins'; import { __, sprintf } from '@wordpress/i18n'; import { useCallback, - useLayoutEffect, useMemo, + useId, useRef, + useState, } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -41,8 +42,17 @@ import { privateApis as blockLibraryPrivateApis } from '@wordpress/block-library import { addQueryArgs } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; -import { ResizableBox, SlotFillProvider } from '@wordpress/components'; -import { useMediaQuery, useViewportMatch } from '@wordpress/compose'; +import { + ResizableBox, + SlotFillProvider, + Tooltip, + VisuallyHidden, +} from '@wordpress/components'; +import { + useMediaQuery, + useRefEffect, + useViewportMatch, +} from '@wordpress/compose'; /** * Internal dependencies @@ -176,13 +186,41 @@ function MetaBoxesMain( { isLegacy } ) { const resizableBoxRef = useRef(); const isShort = useMediaQuery( '(max-height: 549px)' ); - const isAutoHeight = openHeight === undefined; - // In case a user size is set stops the default max-height from applying. - useLayoutEffect( () => { - if ( ! isLegacy && hasAnyVisible && ! isShort && ! isAutoHeight ) { - resizableBoxRef.current.resizable.classList.add( 'has-user-size' ); + const [ { min, max }, setHeightConstraints ] = useState( () => ( {} ) ); + // Keeps the resizable area’s size constraints updated taking into account + // editor notices. The constraints are also used to derive the value for the + // aria-valuenow attribute on the seperator. + const effectSizeConstraints = useRefEffect( ( node ) => { + const container = node.closest( + '.interface-interface-skeleton__content' + ); + const noticeLists = container.querySelectorAll( + ':scope > .components-notice-list' + ); + const resizeHandle = container.querySelector( + '.edit-post-meta-boxes-main__resize-handle' + ); + const actualize = () => { + const fullHeight = container.offsetHeight; + let nextMax = fullHeight; + for ( const element of noticeLists ) { + nextMax -= element.offsetHeight; + } + const nextMin = resizeHandle.offsetHeight; + setHeightConstraints( { min: nextMin, max: nextMax } ); + }; + const observer = new window.ResizeObserver( actualize ); + observer.observe( container ); + for ( const element of noticeLists ) { + observer.observe( element ); } - }, [ isAutoHeight, isShort, hasAnyVisible, isLegacy ] ); + return () => observer.disconnect(); + }, [] ); + + const separatorRef = useRef(); + const separatorHelpId = useId(); + + const [ isUntouched, setIsUntouched ] = useState( true ); if ( ! hasAnyVisible ) { return; @@ -206,6 +244,18 @@ function MetaBoxesMain( { isLegacy } ) { return contents; } + const isAutoHeight = openHeight === undefined; + let usedMax = '50%'; // Approximation before max has a value. + if ( max !== undefined ) { + // Halves the available max height until a user height is set. + usedMax = isAutoHeight && isUntouched ? max / 2 : max; + } + + const getAriaValueNow = ( height ) => + Math.round( ( ( height - min ) / ( max - min ) ) * 100 ); + const usedAriaValueNow = + max === undefined || isAutoHeight ? 50 : getAriaValueNow( openHeight ); + if ( isShort ) { return (
); } + + // TODO: Support more/all keyboard interactions from the window splitter pattern: + // https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ + const onSeparatorKeyDown = ( event ) => { + const delta = { ArrowUp: 20, ArrowDown: -20 }[ event.key ]; + if ( delta ) { + const { resizable } = resizableBoxRef.current; + const fromHeight = isAutoHeight + ? resizable.offsetHeight + : openHeight; + const nextHeight = Math.min( + max, + Math.max( min, delta + fromHeight ) + ); + resizableBoxRef.current.updateSize( { + height: nextHeight, + // Oddly, if left unspecified a subsequent drag gesture applies a fixed + // width and the pane fails to shrink/grow with parent width changes from + // sidebars opening/closing or window resizes. + width: 'auto', + } ); + setPreference( + 'core/edit-post', + 'metaBoxesMainOpenHeight', + nextHeight + ); + } + }; + return ( { - // Avoids height jumping in case it’s limited by max-height. - elementRef.style.height = `${ elementRef.offsetHeight }px`; - // Stops initial max-height from being applied. - elementRef.classList.add( 'has-user-size' ); + if ( isAutoHeight ) { + const heightNow = elementRef.offsetHeight; + // Sets the starting height to avoid visual jumps in height and + // aria-valuenow being `NaN` for the first (few) resize events. + resizableBoxRef.current.updateSize( { height: heightNow } ); + // Causes `maxHeight` to update to full `max` value instead of half. + setIsUntouched( false ); + } + } } + onResize={ () => { + const { height } = resizableBoxRef.current.state; + const separator = separatorRef.current; + separator.ariaValueNow = getAriaValueNow( height ); } } onResizeStop={ () => { + const nextHeight = resizableBoxRef.current.state.height; setPreference( 'core/edit-post', 'metaBoxesMainOpenHeight', - resizableBoxRef.current.state.height + nextHeight ); } } + handleClasses={ { + top: 'edit-post-meta-boxes-main__resize-handle', + } } + handleComponent={ { + top: ( + <> + + { /* Disable reason: aria-valuenow is supported by separator role. */ } + { /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */ } +