From 8e6ead264603edea75acae891cd2556a45908772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 3 May 2023 13:01:57 +0200 Subject: [PATCH 01/10] Remove unused styles --- src/BlockTypes/MiniCartContents.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/BlockTypes/MiniCartContents.php b/src/BlockTypes/MiniCartContents.php index c176188d17a..37ee9ee31ec 100644 --- a/src/BlockTypes/MiniCartContents.php +++ b/src/BlockTypes/MiniCartContents.php @@ -74,15 +74,6 @@ protected function enqueue_assets( array $attributes ) { $bg_color = StyleAttributesUtils::get_background_color_class_and_style( $attributes ); $styles = array( - array( - 'selector' => '.wc-block-mini-cart__drawer .components-modal__header', - 'properties' => array( - array( - 'property' => 'color', - 'value' => $text_color ? $text_color['value'] : false, - ), - ), - ), array( 'selector' => array( '.wc-block-mini-cart__footer .wc-block-mini-cart__footer-actions .wc-block-mini-cart__footer-checkout', From f177530a9740f6e1b95eebe77cd1e82e86ace473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 3 May 2023 13:02:04 +0200 Subject: [PATCH 02/10] Replace usage of Modal component with custom Drawer --- assets/js/base/components/drawer/index.tsx | 139 +++++++++++++++--- assets/js/base/components/drawer/style.scss | 72 ++++----- .../components/drawer/utils/aria-helper.ts | 74 ++++++++++ assets/js/blocks/mini-cart/block.tsx | 1 - assets/js/blocks/mini-cart/style.scss | 56 ++----- 5 files changed, 241 insertions(+), 101 deletions(-) create mode 100644 assets/js/base/components/drawer/utils/aria-helper.ts diff --git a/assets/js/base/components/drawer/index.tsx b/assets/js/base/components/drawer/index.tsx index 5c27fbb5717..938db7d3bae 100644 --- a/assets/js/base/components/drawer/index.tsx +++ b/assets/js/base/components/drawer/index.tsx @@ -1,14 +1,35 @@ +/** + * Some code of the Drawer component is based on the Modal component from Gutenberg: + * https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/modal/index.tsx + */ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { Modal } from 'wordpress-components'; -import { useDebounce } from 'use-debounce'; import classNames from 'classnames'; +import { useDebounce } from 'use-debounce'; +import type { ForwardedRef, KeyboardEvent } from 'react'; +import { __ } from '@wordpress/i18n'; +import { + createPortal, + useEffect, + useRef, + forwardRef, +} from '@wordpress/element'; +import { close } from '@wordpress/icons'; +import { + useFocusReturn, + useFocusOnMount, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseFocusOutside as useFocusOutside, + useConstrainedTabbing, + useMergeRefs, +} from '@wordpress/compose'; /** * Internal dependencies */ +import Button from '../button'; +import * as ariaHelper from './utils/aria-helper'; import './style.scss'; interface DrawerProps { @@ -18,32 +39,77 @@ interface DrawerProps { onClose: () => void; slideIn?: boolean; slideOut?: boolean; - title: string; } -const Drawer = ( { - children, - className, - isOpen, - onClose, - slideIn = true, - slideOut = true, - title, -}: DrawerProps ): JSX.Element | null => { +const UnforwardedDrawer = ( + { + children, + className, + isOpen, + onClose, + slideIn = true, + slideOut = true, + }: DrawerProps, + forwardedRef: ForwardedRef< HTMLDivElement > +): JSX.Element | null => { const [ debouncedIsOpen ] = useDebounce< boolean >( isOpen, 300 ); const isClosing = ! isOpen && debouncedIsOpen; + const bodyOpenClassName = 'drawer-open'; + + const onRequestClose = () => { + document.body.classList.remove( bodyOpenClassName ); + ariaHelper.showApp(); + onClose(); + }; + + const ref = useRef< HTMLDivElement >(); + const focusOnMountRef = useFocusOnMount(); + const constrainedTabbingRef = useConstrainedTabbing(); + const focusReturnRef = useFocusReturn(); + const focusOutsideProps = useFocusOutside( onRequestClose ); + const contentRef = useRef< HTMLDivElement >( null ); + + useEffect( () => { + if ( isOpen ) { + ariaHelper.hideApp( ref.current ); + document.body.classList.add( bodyOpenClassName ); + } + }, [ isOpen, bodyOpenClassName ] ); + + const overlayRef = useMergeRefs( [ ref, forwardedRef ] ); + const drawerRef = useMergeRefs( [ + constrainedTabbingRef, + focusReturnRef, + focusOnMountRef, + ] ); if ( ! isOpen && ! isClosing ) { return null; } - return ( - ) { + if ( + // Ignore keydowns from IMEs + event.nativeEvent.isComposing || + // Workaround for Mac Safari where the final Enter/Backspace of an IME composition + // is `isComposing=false`, even though it's technically still part of the composition. + // These can only be detected by keyCode. + event.keyCode === 229 + ) { + return; + } + + if ( event.code === 'Escape' && ! event.defaultPrevented ) { + event.preventDefault(); + onRequestClose(); + } + } + + return createPortal( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
- { children } - +
+
+
+
+
, + document.body ); }; +export const Drawer = forwardRef( UnforwardedDrawer ); + export default Drawer; diff --git a/assets/js/base/components/drawer/style.scss b/assets/js/base/components/drawer/style.scss index 25812f579f0..57aee03476c 100644 --- a/assets/js/base/components/drawer/style.scss +++ b/assets/js/base/components/drawer/style.scss @@ -111,44 +111,48 @@ $drawer-animation-duration: 0.3s; } } -.wc-block-components-drawer .components-modal__content { - padding: $gap-largest $gap; +// Important rules are needed to reset button styles. +.wc-block-components-drawer__close { + @include reset-box(); + background: transparent !important; + color: inherit !important; + position: absolute !important; + top: $gap-small; + right: $gap-small; + opacity: 0.6; + z-index: 2; + // Increase clickable area. + padding: 1em !important; + margin: -1em; + + &:hover, + &:focus, + &:active { + opacity: 1; + } + + > span { + @include visually-hidden(); + } + svg { + fill: currentColor; + display: block; + } } -.wc-block-components-drawer .components-modal__header { +.wc-block-components-drawer__content { + height: 100dvh; position: relative; +} - // Close button. - .components-button { - @include reset-box(); - background: transparent; - color: inherit; - position: absolute; - opacity: 0.6; - z-index: 2; - // The SVG has some white spacing around, thus we have to set this magic number. - right: 8px; - top: 0; - // Increase clickable area. - padding: 1em; - margin: -1em; - - &:hover, - &:focus, - &:active { - opacity: 1; - } - - > span { - @include visually-hidden(); - } - } +.admin-bar .wc-block-components-drawer__content { + margin-top: 46px; + height: calc(100dvh - 46px); } -// Same styles as `Title` component. -.wc-block-components-drawer .components-modal__header-heading { - @include reset-box(); - // We need the font size to be in rem so it doesn't change depending on the parent element. - @include font-size(large, 1rem); - word-break: break-word; +@media only screen and (min-width: 783px) { + .admin-bar .wc-block-components-drawer__content { + margin-top: 32px; + height: calc(100dvh - 32px); + } } diff --git a/assets/js/base/components/drawer/utils/aria-helper.ts b/assets/js/base/components/drawer/utils/aria-helper.ts new file mode 100644 index 00000000000..dbb91aaa67f --- /dev/null +++ b/assets/js/base/components/drawer/utils/aria-helper.ts @@ -0,0 +1,74 @@ +/** + * Copied from https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/modal/aria-helper.ts + */ +const LIVE_REGION_ARIA_ROLES = new Set( [ + 'alert', + 'status', + 'log', + 'marquee', + 'timer', +] ); + +let hiddenElements: Element[] = [], + isHidden = false; + +/** + * Determines if the passed element should not be hidden from screen readers. + * + * @param {HTMLElement} element The element that should be checked. + * + * @return {boolean} Whether the element should not be hidden from screen-readers. + */ +export function elementShouldBeHidden( element: Element ) { + const role = element.getAttribute( 'role' ); + return ! ( + element.tagName === 'SCRIPT' || + element.hasAttribute( 'aria-hidden' ) || + element.hasAttribute( 'aria-live' ) || + ( role && LIVE_REGION_ARIA_ROLES.has( role ) ) + ); +} + +/** + * Hides all elements in the body element from screen-readers except + * the provided element and elements that should not be hidden from + * screen-readers. + * + * The reason we do this is because `aria-modal="true"` currently is bugged + * in Safari, and support is spotty in other browsers overall. In the future + * we should consider removing these helper functions in favor of + * `aria-modal="true"`. + * + * @param {HTMLDivElement} unhiddenElement The element that should not be hidden. + */ +export function hideApp( unhiddenElement?: HTMLDivElement ) { + if ( isHidden ) { + return; + } + const elements = Array.from( document.body.children ); + elements.forEach( ( element ) => { + if ( element === unhiddenElement ) { + return; + } + if ( elementShouldBeHidden( element ) ) { + element.setAttribute( 'aria-hidden', 'true' ); + hiddenElements.push( element ); + } + } ); + isHidden = true; +} + +/** + * Makes all elements in the body that have been hidden by `hideApp` + * visible again to screen-readers. + */ +export function showApp() { + if ( ! isHidden ) { + return; + } + hiddenElements.forEach( ( element ) => { + element.removeAttribute( 'aria-hidden' ); + } ); + hiddenElements = []; + isHidden = false; +} diff --git a/assets/js/blocks/mini-cart/block.tsx b/assets/js/blocks/mini-cart/block.tsx index a3fa2cee748..6d0539d8976 100644 --- a/assets/js/blocks/mini-cart/block.tsx +++ b/assets/js/blocks/mini-cart/block.tsx @@ -257,7 +257,6 @@ const MiniCartBlock = ( attributes: Props ): JSX.Element => { 'is-loading': cartIsLoading, } ) } - title="" isOpen={ isOpen } onClose={ () => { setIsOpen( false ); diff --git a/assets/js/blocks/mini-cart/style.scss b/assets/js/blocks/mini-cart/style.scss index 057ff58f950..8b8008b9488 100644 --- a/assets/js/blocks/mini-cart/style.scss +++ b/assets/js/blocks/mini-cart/style.scss @@ -2,6 +2,11 @@ display: inline-block; } +.wc-block-mini-cart__template-part, +.wp-block-woocommerce-mini-cart-contents { + height: 100%; +} + .wc-block-mini-cart__button { align-items: center; background-color: transparent; @@ -41,7 +46,7 @@ } } -.modal-open .wc-block-mini-cart__button { +.drawer-open .wc-block-mini-cart__button { pointer-events: none; } @@ -50,6 +55,10 @@ font-size: 1rem; .wp-block-woocommerce-mini-cart-contents { + box-sizing: border-box; + padding: 0; + justify-content: center; + .wc-block-components-notices { margin: #{$gap} #{$gap-largest} -#{$gap} #{$gap}; margin-bottom: unset; @@ -57,40 +66,13 @@ .wc-block-components-notices__notice { margin-bottom: unset; } - } - } - - .components-modal__content { - padding: 0; - position: relative; - } - .components-modal__header { - position: relative; - height: calc($gap-largest + $gap); - position: absolute; - top: $gap-largest; - right: $gap-smallest; - - button { - margin: 0; - right: 0; - transform: translateY(-50%); - } - - svg { - fill: currentColor; - display: block; + &:empty { + display: none; + } } } } - -.wp-block-woocommerce-mini-cart-contents { - box-sizing: border-box; - height: 100dvh; - padding: 0; - justify-content: center; -} :where(.wp-block-woocommerce-mini-cart-contents) { background: #fff; } @@ -224,15 +206,3 @@ h2.wc-block-mini-cart__title { } } } - -.admin-bar .wp-block-woocommerce-mini-cart-contents { - margin-top: 46px; - height: calc(100dvh - 46px); -} - -@media only screen and (min-width: 783px) { - .admin-bar .wp-block-woocommerce-mini-cart-contents { - margin-top: 32px; - height: calc(100dvh - 32px); - } -} From 11c0a1b4b77b82fcb0cc99d9216368a5ca377f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Thu, 4 May 2023 09:49:23 +0200 Subject: [PATCH 03/10] Update MiniCart.php class structure --- src/BlockTypes/MiniCart.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/BlockTypes/MiniCart.php b/src/BlockTypes/MiniCart.php index 7baaf4d6b09..205b4bc6614 100644 --- a/src/BlockTypes/MiniCart.php +++ b/src/BlockTypes/MiniCart.php @@ -496,12 +496,9 @@ protected function get_markup( $attributes ) { return '
-