Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post Editor: Support keyboard resizing of meta boxes pane #65325

Merged
merged 9 commits into from
Sep 18, 2024
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' ) }
t-hamano marked this conversation as resolved.
Show resolved Hide resolved
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
Loading