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

ResizableFrame: Make keyboard accessible #52443

Merged
merged 14 commits into from
Jul 13, 2023
131 changes: 100 additions & 31 deletions packages/edit-site/src/components/resizable-frame/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import classnames from 'classnames';
import { useState, useRef, useEffect } from '@wordpress/element';
import {
ResizableBox,
Tooltip,
__unstableMotion as motion,
} from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { useInstanceId } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
Expand All @@ -33,7 +36,7 @@ const HANDLE_STYLES_OVERRIDE = {
};

// The minimum width of the frame (in px) while resizing.
const FRAME_MIN_WIDTH = 340;
const FRAME_MIN_WIDTH = 320;
// The reference width of the frame (in px) used to calculate the aspect ratio.
const FRAME_REFERENCE_WIDTH = 1300;
// 9 : 19.5 is the target aspect ratio enforced (when possible) while resizing.
Expand All @@ -42,6 +45,8 @@ const FRAME_TARGET_ASPECT_RATIO = 9 / 19.5;
// viewport's edge. If the frame is resized to be closer to the viewport's edge
// than this distance, then "canvas mode" will be enabled.
const SNAP_TO_EDIT_CANVAS_MODE_THRESHOLD = 200;
// Default size for the `frameSize` state.
const INITIAL_FRAME_SIZE = { width: '100%', height: '100%' };

function calculateNewHeight( width, initialAspectRatio ) {
const lerp = ( a, b, amount ) => {
Expand Down Expand Up @@ -78,22 +83,27 @@ function ResizableFrame( {
oversizedClassName,
innerContentStyle,
} ) {
const [ frameSize, setFrameSize ] = useState( {
width: '100%',
height: '100%',
} );
const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE );
// The width of the resizable frame when a new resize gesture starts.
const [ startingWidth, setStartingWidth ] = useState();
const [ isResizing, setIsResizing ] = useState( false );
const [ isHovering, setIsHovering ] = useState( false );
const [ shouldShowHandle, setShouldShowHandle ] = useState( false );
const [ isOversized, setIsOversized ] = useState( false );
const [ resizeRatio, setResizeRatio ] = useState( 1 );
const canvasMode = useSelect(
( select ) => unlock( select( editSiteStore ) ).getCanvasMode(),
[]
);
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
const initialAspectRatioRef = useRef( null );
// The width of the resizable frame on initial render.
const initialComputedWidthRef = useRef( null );
const FRAME_TRANSITION = { type: 'tween', duration: isResizing ? 0 : 0.5 };
const frameRef = useRef( null );
const resizableHandleHelpId = useInstanceId(
ResizableFrame,
'edit-site-resizable-frame-handle-help'
);

// Remember frame dimensions on initial render.
useEffect( () => {
Expand Down Expand Up @@ -154,13 +164,40 @@ function ResizableFrame( {
if ( remainingWidth > SNAP_TO_EDIT_CANVAS_MODE_THRESHOLD ) {
// Reset the initial aspect ratio if the frame is resized slightly
// above the sidebar but not far enough to trigger full screen.
setFrameSize( { width: '100%', height: '100%' } );
setFrameSize( INITIAL_FRAME_SIZE );
} else {
// Trigger full screen if the frame is resized far enough to the left.
setCanvasMode( 'edit' );
}
};

// Handle resize by arrow keys
const handleResizableHandleKeyDown = ( event ) => {
if ( ! [ 'ArrowLeft', 'ArrowRight' ].includes( event.key ) ) {
return;
}

mirka marked this conversation as resolved.
Show resolved Hide resolved
event.preventDefault();

const step = 20 * ( event.shiftKey ? 5 : 1 );
mirka marked this conversation as resolved.
Show resolved Hide resolved
const delta = step * ( event.key === 'ArrowLeft' ? 1 : -1 );
const newWidth = Math.min(
Math.max(
FRAME_MIN_WIDTH,
frameRef.current.resizable.offsetWidth + delta
),
initialComputedWidthRef.current
);

setFrameSize( {
width: newWidth,
height: calculateNewHeight(
newWidth,
initialAspectRatioRef.current
),
} );
};

const frameAnimationVariants = {
default: {
flexGrow: 0,
Expand All @@ -173,16 +210,26 @@ function ResizableFrame( {
};

const resizeHandleVariants = {
default: {
hidden: {
opacity: 0,
left: 0,
},
visible: {
opacity: 1,
left: -16,
},
resizing: {
active: {
opacity: 1,
left: -16,
scaleY: 1.3,
},
};
const currentResizeHandleVariant = ( () => {
if ( isResizing ) {
return 'active';
}
return shouldShowHandle ? 'visible' : 'hidden';
} )();

return (
<ResizableBox
Expand Down Expand Up @@ -217,28 +264,50 @@ function ResizableFrame( {
minWidth={ FRAME_MIN_WIDTH }
maxWidth={ isFullWidth ? '100%' : '150%' }
maxHeight={ '100%' }
onMouseOver={ () => setIsHovering( true ) }
onMouseOut={ () => setIsHovering( false ) }
onFocus={ () => setShouldShowHandle( true ) }
onBlur={ () => setShouldShowHandle( false ) }
onMouseOver={ () => setShouldShowHandle( true ) }
onMouseOut={ () => setShouldShowHandle( false ) }
handleComponent={ {
left:
isHovering || isResizing ? (
<motion.div
key="handle"
className="edit-site-resizable-frame__handle"
variants={ resizeHandleVariants }
animate={ isResizing ? 'resizing' : 'default' }
title="Drag to resize"
initial={ {
opacity: 0,
left: 0,
} }
exit={ {
opacity: 0,
left: 0,
} }
whileHover={ { scaleY: 1.3 } }
/>
) : null,
left: canvasMode === 'view' && (
<>
<Tooltip text={ __( 'Drag to resize' ) }>
{ /* Disable reason: role="separator" does in fact support aria-valuenow */ }
{ /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */ }
<motion.button
key="handle"
role="separator"
aria-orientation="vertical"
className={ classnames(
'edit-site-resizable-frame__handle',
{ 'is-resizing': isResizing }
) }
variants={ resizeHandleVariants }
animate={ currentResizeHandleVariant }
aria-label={ __( 'Drag to resize' ) }
aria-describedby={ resizableHandleHelpId }
aria-valuenow={
frameRef.current?.resizable?.offsetWidth ||
undefined
}
aria-valuemin={ FRAME_MIN_WIDTH }
aria-valuemax={
initialComputedWidthRef.current
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking aria-valuemax should always be the main document offsetWidth. Otherwise this value will be different depending on the editor mode. For example: My screen is a 16-inch retina display. The default scaled resolution is 1792 x 1120 so the actual viewport width is 1792. WHen the editor is in vide mode, this will be aria-valuemax="1408". When the editor is in edit mode, this will be aria-valuemax="1792".
I think the value should always the actual maximum width the iframe can be set to, which is 1792.
Also, we should make sure this value updates when resizing the browser window (I thikn it does update when set to the main document offsetWidth).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a difficult one actually. The values are associated with the separator, and the separator is only functional in View mode. Especially in a keyboard operation context, where I believe the semantics are most relevant, the max value is in fact the initial computed width. I think it would be confusing if you couldn't keyboard resize the frame up to the purported max value.

(In testing this, I noticed that the resize handle is still in the tab sequence when in Edit mode, so I fixed that. 5acbbfd)

}
onKeyDown={ handleResizableHandleKeyDown }
initial="hidden"
exit="hidden"
whileFocus="active"
whileHover="active"
/>
</Tooltip>
<div hidden id={ resizableHandleHelpId }>
{ __(
'Use left and right arrow keys to resize the canvas. Hold shift to resize in larger increments.'
) }
</div>
</>
),
} }
onResizeStart={ handleResizeStart }
onResize={ handleResize }
Expand Down
18 changes: 9 additions & 9 deletions packages/edit-site/src/components/resizable-frame/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@
.edit-site-resizable-frame__handle {
align-items: center;
background-color: rgba($gray-700, 0.4);
border: 0;
border-radius: $grid-unit-05;
cursor: col-resize;
display: flex;
height: $grid-unit-80;
justify-content: flex-end;
padding: 0;
Comment on lines +33 to +39
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resets user agent button styles.

position: absolute;
top: calc(50% - #{$grid-unit-40});
width: $grid-unit-05;
Expand All @@ -56,16 +58,14 @@
width: $grid-unit-40;
}

&:hover,
.is-resizing & {
background-color: var(--wp-admin-theme-color);
&:focus-visible {
// Works with Windows high contrast mode while also hiding weird outline in Safari.
outline: 2px solid transparent;
}

.edit-site-resizable-frame__handle-label {
background: var(--wp-admin-theme-color);
border-radius: 2px;
color: #fff;
margin-right: $grid-unit-10;
padding: 4px 8px;
Comment on lines -64 to -69
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused styles 🧹

&:hover,
&:focus,
&.is-resizing {
background-color: var(--wp-admin-theme-color);
}
}
Loading