Skip to content

Commit

Permalink
Post Editor: Support keyboard resizing of meta boxes pane (#65325)
Browse files Browse the repository at this point in the history
* Use a custom resize handle to support keyboard use

Also computes maximum resize height taking into account notices

* Avoid pane’s `width` becoming fixed

* Fix aria-valuenow updates from `onResize`

* Respect reduced motion preference

* Correct typo

* Revamp resize handle button hover/focus transition

* Use border-radius/width instead of clip-path

* Use variable based on grid unit for resize handle height

* Remove unnecessary max-height rule

Co-authored-by: t-hamano <wildworks@git.wordpress.org>
  • Loading branch information
stokesman and t-hamano authored Sep 18, 2024
1 parent d67b83f commit 07c6a1f
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 37 deletions.
150 changes: 134 additions & 16 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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 (
<details
Expand All @@ -224,6 +274,35 @@ function MetaBoxesMain( { isLegacy } ) {
</details>
);
}

// 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 (
<ResizableBox
className={ className }
Expand All @@ -239,8 +318,8 @@ function MetaBoxesMain( { isLegacy } ) {
bottomRight: false,
bottomLeft: false,
} }
// This is overriden by an !important rule that applies until user resizes.
maxHeight="100%"
minHeight={ min }
maxHeight={ usedMax }
bounds="parent"
boundsByDirection
// Avoids hiccups while dragging over objects like iframes and ensures that
Expand All @@ -250,19 +329,58 @@ function MetaBoxesMain( { isLegacy } ) {
target.setPointerCapture( pointerId );
} }
onResizeStart={ ( event, direction, elementRef ) => {
// 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: (
<>
<Tooltip text={ __( 'Drag to resize' ) }>
{ /* Disable reason: aria-valuenow is supported by separator role. */ }
{ /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */ }
<button
ref={ separatorRef }
aria-label={ __( 'Drag to resize' ) }
aria-describedby={ separatorHelpId }
onKeyDown={ onSeparatorKeyDown }
// Disable reason: buttons are allowed to be separator role.
// eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role
role="separator"
aria-valuenow={ usedAriaValueNow }
/>
</Tooltip>
<VisuallyHidden id={ separatorHelpId }>
{ __(
'Use up and down arrow keys to resize the metabox panel.'
) }
</VisuallyHidden>
</>
),
} }
>
<meta ref={ effectSizeConstraints } />
{ contents }
</ResizableBox>
);
Expand Down
49 changes: 28 additions & 21 deletions packages/edit-post/src/components/layout/style.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
$resize-handle-height: $grid-unit-30;

.edit-post-meta-boxes-main {
filter: drop-shadow(0 -1px rgba($color: #000, $alpha: 0.133)); // 0.133 = $gray-200 but with alpha.
background-color: $white;
clear: both; // This is seemingly only needed in case the canvas is not iframe’d.

&:not(details) {
padding-top: 23px;
max-height: 100%;

&:not(.has-user-size) {
max-height: 50% !important;
}
padding-top: $resize-handle-height;
}

// The component renders as a details element in short viewports.
Expand All @@ -31,24 +28,34 @@
z-index: 1;
}
}
}

& .components-resizable-box__handle-top {
top: 0;
box-shadow: 0 $border-width $gray-300;
}
& .components-resizable-box__side-handle::before {
border-radius: 0;
top: 0;
height: $border-width;
}
& .components-resizable-box__handle::after {
.edit-post-meta-boxes-main__resize-handle {
display: flex;
// The position is absolute by default inline style of ResizableBox.
inset: 0 0 auto 0;
height: $resize-handle-height;
box-shadow: 0 $border-width $gray-300;

& > button {
appearance: none;
cursor: inherit;
margin: auto;
padding: 0;
border: none;
outline: none;
background-color: $gray-300;
box-shadow: none;
border-radius: 4px;
width: $grid-unit-80;
height: $grid-unit-05;
top: calc(50% - #{$grid-unit-05} / 2);
width: 100px;
right: calc(50% - 50px);
border-radius: $radius-small;
transition: width 0.3s ease-out;
@include reduce-motion("transition");
}

&:hover > button,
> button:focus {
background-color: var(--wp-admin-theme-color);
width: $grid-unit-80 + $grid-unit-20;
}
}

Expand Down

0 comments on commit 07c6a1f

Please sign in to comment.