From 4df10308883c2510329163549736f32db8eefc0e Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Thu, 13 Jul 2023 21:18:18 +0900 Subject: [PATCH] ResizableFrame: Make keyboard accessible (#52443) * ResizableFrame: Make keyboard accessible * Fix outline in Safari * Use proper CSS modifier * Add aria-label to button * Keep handle enlarged when resizing (Safari) * Add back visually hidden help text * Don't switch to edit mode * Make the handle a role="separator" * Revert to `button` * Switch description text to `div hidden` * Prevent keydown event default when right/left arrow * Change minimum frame width to 320px * Mention shift key in description text * Only render resize handle when in View mode --- .../src/components/resizable-frame/index.js | 131 +++++++++++++----- .../src/components/resizable-frame/style.scss | 18 +-- 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js index a13881e7905ff..1bb9315a07e38 100644 --- a/packages/edit-site/src/components/resizable-frame/index.js +++ b/packages/edit-site/src/components/resizable-frame/index.js @@ -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 @@ -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. @@ -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 ) => { @@ -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( () => { @@ -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; + } + + event.preventDefault(); + + const step = 20 * ( event.shiftKey ? 5 : 1 ); + 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, @@ -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 ( setIsHovering( true ) } - onMouseOut={ () => setIsHovering( false ) } + onFocus={ () => setShouldShowHandle( true ) } + onBlur={ () => setShouldShowHandle( false ) } + onMouseOver={ () => setShouldShowHandle( true ) } + onMouseOut={ () => setShouldShowHandle( false ) } handleComponent={ { - left: - isHovering || isResizing ? ( - - ) : null, + left: canvasMode === 'view' && ( + <> + + { /* Disable reason: role="separator" does in fact support aria-valuenow */ } + { /* eslint-disable-next-line jsx-a11y/role-supports-aria-props */ } + + + + + ), } } onResizeStart={ handleResizeStart } onResize={ handleResize } diff --git a/packages/edit-site/src/components/resizable-frame/style.scss b/packages/edit-site/src/components/resizable-frame/style.scss index b1da6e1c39399..596304be8d6b9 100644 --- a/packages/edit-site/src/components/resizable-frame/style.scss +++ b/packages/edit-site/src/components/resizable-frame/style.scss @@ -47,11 +47,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; position: absolute; top: calc(50% - #{$grid-unit-40}); width: $grid-unit-05; @@ -73,16 +75,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; + &:hover, + &:focus, + &.is-resizing { + background-color: var(--wp-admin-theme-color); } }