diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js
index 20ef22b5f0eb2..88d0cd1588f03 100644
--- a/packages/edit-post/src/components/layout/index.js
+++ b/packages/edit-post/src/components/layout/index.js
@@ -31,6 +31,7 @@ import {
useRef,
useState,
} from '@wordpress/element';
+import { chevronDown, chevronUp } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { store as preferencesStore } from '@wordpress/preferences';
import {
@@ -43,6 +44,7 @@ import { addQueryArgs } from '@wordpress/url';
import { decodeEntities } from '@wordpress/html-entities';
import { store as coreStore } from '@wordpress/core-data';
import {
+ Icon,
ResizableBox,
SlotFillProvider,
Tooltip,
@@ -76,7 +78,7 @@ import useNavigateToEntityRecord from '../../hooks/use-navigate-to-entity-record
const { getLayoutStyles } = unlock( blockEditorPrivateApis );
const { useCommands } = unlock( coreCommandsPrivateApis );
const { useCommandContext } = unlock( commandsPrivateApis );
-const { Editor, FullscreenMode } = unlock( editorPrivateApis );
+const { Editor, FullscreenMode, NavigableRegion } = unlock( editorPrivateApis );
const { BlockKeyboardShortcuts } = unlock( blockLibraryPrivateApis );
const DESIGN_POST_TYPES = [
'wp_template',
@@ -163,7 +165,7 @@ function MetaBoxesMain( { isLegacy } ) {
];
}, [] );
const { set: setPreference } = useDispatch( preferencesStore );
- const resizableBoxRef = useRef();
+ const metaBoxesMainRef = useRef();
const isShort = useMediaQuery( '(max-height: 549px)' );
const [ { min, max }, setHeightConstraints ] = useState( () => ( {} ) );
@@ -178,9 +180,9 @@ function MetaBoxesMain( { isLegacy } ) {
':scope > .components-notice-list'
);
const resizeHandle = container.querySelector(
- '.edit-post-meta-boxes-main__resize-handle'
+ '.edit-post-meta-boxes-main__presenter'
);
- const actualize = () => {
+ const deriveConstraints = () => {
const fullHeight = container.offsetHeight;
let nextMax = fullHeight;
for ( const element of noticeLists ) {
@@ -189,7 +191,7 @@ function MetaBoxesMain( { isLegacy } ) {
const nextMin = resizeHandle.offsetHeight;
setHeightConstraints( { min: nextMin, max: nextMax } );
};
- const observer = new window.ResizeObserver( actualize );
+ const observer = new window.ResizeObserver( deriveConstraints );
observer.observe( container );
for ( const element of noticeLists ) {
observer.observe( element );
@@ -201,12 +203,33 @@ function MetaBoxesMain( { isLegacy } ) {
const separatorHelpId = useId();
const [ isUntouched, setIsUntouched ] = useState( true );
+ const applyHeight = ( candidateHeight, isPersistent, isInstant ) => {
+ const nextHeight = Math.min( max, Math.max( min, candidateHeight ) );
+ if ( isPersistent ) {
+ setPreference(
+ 'core/edit-post',
+ 'metaBoxesMainOpenHeight',
+ nextHeight
+ );
+ } else {
+ separatorRef.current.ariaValueNow = getAriaValueNow( nextHeight );
+ }
+ if ( isInstant ) {
+ metaBoxesMainRef.current.updateSize( {
+ height: nextHeight,
+ // Oddly, when the event that triggered this was not from the mouse (e.g. keydown),
+ // if `width` is left unspecified a subsequent drag gesture applies a fixed
+ // width and the pane fails to widen/narrow with parent width changes from
+ // sidebars opening/closing or window resizes.
+ width: 'auto',
+ } );
+ }
+ };
if ( ! hasAnyVisible ) {
return;
}
- const className = 'edit-post-meta-boxes-main';
const contents = (
@@ -236,59 +260,39 @@ function MetaBoxesMain( { isLegacy } ) {
const usedAriaValueNow =
max === undefined || isAutoHeight ? 50 : getAriaValueNow( openHeight );
- if ( isShort ) {
- return (
-
{
- setPreference(
- 'core/edit-post',
- 'metaBoxesMainIsOpen',
- target.open
- );
- } }
- >
- { __( 'Meta Boxes' ) }
- { contents }
-
- );
- }
+ const toggle = () =>
+ setPreference( 'core/edit-post', 'metaBoxesMainIsOpen', ! isOpen );
// 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
- );
+ const pane = metaBoxesMainRef.current.resizable;
+ const fromHeight = isAutoHeight ? pane.offsetHeight : openHeight;
+ const nextHeight = delta + fromHeight;
+ applyHeight( nextHeight, true, true );
+ event.preventDefault();
}
};
-
- return (
-
[0]} */ ( {
+ as: NavigableRegion,
+ ref: metaBoxesMainRef,
+ className: clsx( className, 'is-resizable' ),
+ defaultSize: { height: openHeight },
+ minHeight: min,
+ maxHeight: usedMax,
+ enable: {
top: true,
right: false,
bottom: false,
@@ -297,72 +301,66 @@ function MetaBoxesMain( { isLegacy } ) {
topRight: false,
bottomRight: false,
bottomLeft: false,
- } }
- minHeight={ min }
- maxHeight={ usedMax }
- bounds="parent"
- boundsByDirection
- // Avoids hiccups while dragging over objects like iframes and ensures that
- // the event to end the drag is captured by the target (resize handle)
- // whether or not it’s under the pointer.
- onPointerDown={ ( { pointerId, target } ) => {
- target.setPointerCapture( pointerId );
- } }
- onResizeStart={ ( event, direction, elementRef ) => {
- 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',
- nextHeight
- );
- } }
- handleClasses={ {
- top: 'edit-post-meta-boxes-main__resize-handle',
- } }
- handleComponent={ {
+ },
+ handleClasses: { top: 'edit-post-meta-boxes-main__presenter' },
+ handleComponent: {
top: (
<>
- { /* Disable reason: aria-valuenow is supported by separator role. */ }
- { /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */ }
-
{ __(
- 'Use up and down arrow keys to resize the metabox panel.'
+ 'Use up and down arrow keys to resize the metabox pane.'
) }
>
),
- } }
- >
-
+ },
+ // Avoids hiccups while dragging over objects like iframes and ensures that
+ // the event to end the drag is captured by the target (resize handle)
+ // whether or not it’s under the pointer.
+ onPointerDown: ( { pointerId, target } ) => {
+ target.setPointerCapture( pointerId );
+ },
+ onResizeStart: ( event, direction, elementRef ) => {
+ if ( isAutoHeight ) {
+ // Sets the starting height to avoid visual jumps in height and
+ // aria-valuenow being `NaN` for the first (few) resize events.
+ applyHeight( elementRef.offsetHeight, false, true );
+ setIsUntouched( false );
+ }
+ },
+ onResize: () =>
+ applyHeight( metaBoxesMainRef.current.state.height ),
+ onResizeStop: () =>
+ applyHeight( metaBoxesMainRef.current.state.height, true ),
+ } );
+ }
+
+ return (
+
+ { isShort ? (
+
+ ) : (
+
+ ) }
{ contents }
-
+
);
}
diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss
index 5d0a9a29c329c..18f12c1dbfbb9 100644
--- a/packages/edit-post/src/components/layout/style.scss
+++ b/packages/edit-post/src/components/layout/style.scss
@@ -1,71 +1,111 @@
-$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.
+ // Windows High Contrast mode will show this outline, but not the shadow.
+ outline: 1px solid transparent;
background-color: $white;
- clear: both; // This is seemingly only needed in case the canvas is not iframe’d.
-
- &:not(details) {
- padding-top: $resize-handle-height;
- }
-
- // The component renders as a details element in short viewports.
- &:is(details) {
- & > summary {
- cursor: pointer;
- color: $gray-900;
- background-color: $white;
- height: $button-size-compact;
- line-height: $button-size-compact;
- font-size: 13px;
- padding-left: $grid-unit-30;
- box-shadow: 0 $border-width $gray-300;
- }
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
- &[open] > summary {
- position: sticky;
- top: 0;
- z-index: 1;
- }
+ &.is-resizable {
+ padding-block-start: $grid-unit-30;
}
}
-.edit-post-meta-boxes-main__resize-handle {
+.edit-post-meta-boxes-main__presenter {
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;
+ // Windows High Contrast mode will show this outline, but not the shadow.
+ outline: 1px solid transparent;
+ position: relative;
+ z-index: 1;
- & > button {
+ // Button style reset for both toggle or resizable.
+ .is-toggle-only > &,
+ .is-resizable.edit-post-meta-boxes-main & > button {
appearance: none;
- cursor: inherit;
- margin: auto;
padding: 0;
border: none;
outline: none;
- background-color: $gray-300;
- width: $grid-unit-80;
- height: $grid-unit-05;
- border-radius: $radius-small;
- transition: width 0.3s ease-out;
- @include reduce-motion("transition");
+ background-color: transparent;
}
- &:hover > button,
- > button:focus {
- background-color: var(--wp-admin-theme-color);
- width: $grid-unit-80 + $grid-unit-20;
+ .is-toggle-only > & {
+ flex-shrink: 0;
+ cursor: pointer;
+ height: $button-size-compact;
+ justify-content: space-between;
+ align-items: center;
+ padding-inline: $grid-unit-30 $grid-unit-15;
+
+ &:is(:hover, :focus-visible) {
+ color: var(--wp-admin-theme-color);
+ }
+ &:focus-visible::after {
+ content: "";
+ position: absolute;
+ inset: var(--wp-admin-border-width-focus);
+ @include button-style__focus();
+ }
+ > svg {
+ fill: currentColor;
+ }
+ }
+
+ .is-resizable.edit-post-meta-boxes-main & {
+ inset: 0 0 auto;
+
+ > button {
+ cursor: inherit;
+ width: $grid-unit-80;
+ height: $grid-unit-30;
+ margin: auto;
+
+ &::before {
+ content: "";
+ background-color: $gray-300;
+ // Windows High Contrast mode will show this outline, but not the background-color.
+ outline: 2px solid transparent;
+ outline-offset: -2px;
+ position: absolute;
+ inset-block: calc(50% - #{$grid-unit-05} / 2) auto;
+ transform: translateX(-50%);
+ width: inherit;
+ height: $grid-unit-05;
+ border-radius: $radius-small;
+ transition: width 0.3s ease-out;
+ @include reduce-motion("transition");
+ }
+ }
+
+ &:is(:hover, :focus-within) > button::before {
+ background-color: var(--wp-admin-theme-color);
+ width: $grid-unit-80 + $grid-unit-20;
+ }
+ }
+}
+
+@media (pointer: coarse) {
+ .is-resizable.edit-post-meta-boxes-main {
+ padding-block-start: $button-size-compact;
+
+ .edit-post-meta-boxes-main__presenter > button {
+ height: $button-size-compact;
+ }
}
}
.edit-post-meta-boxes-main__liner {
overflow: auto;
- max-height: 100%;
// Keep the contents behind the resize handle or details summary.
isolation: isolate;
}
+// In case the canvas is not iframe’d.
+.edit-post-layout__metaboxes {
+ clear: both;
+}
+
.has-metaboxes .editor-visual-editor {
flex: 1;
diff --git a/packages/interface/src/components/navigable-region/index.js b/packages/interface/src/components/navigable-region/index.js
index a4c051185b63a..50b98d49070d4 100644
--- a/packages/interface/src/components/navigable-region/index.js
+++ b/packages/interface/src/components/navigable-region/index.js
@@ -1,24 +1,29 @@
+/**
+ * WordPress dependencies
+ */
+import { forwardRef } from '@wordpress/element';
+
/**
* External dependencies
*/
import clsx from 'clsx';
-export default function NavigableRegion( {
- children,
- className,
- ariaLabel,
- as: Tag = 'div',
- ...props
-} ) {
- return (
-
- { children }
-
- );
-}
+const NavigableRegion = forwardRef(
+ ( { children, className, ariaLabel, as: Tag = 'div', ...props }, ref ) => {
+ return (
+
+ { children }
+
+ );
+ }
+);
+
+NavigableRegion.displayName = 'NavigableRegion';
+export default NavigableRegion;