From bf2346d33d4ee7b02130d76586682a91524c3f62 Mon Sep 17 00:00:00 2001 From: Daniel Dudzic Date: Tue, 1 Apr 2025 16:10:32 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=81=EF=B8=8F=E2=80=8D=F0=9F=97=A8?= =?UTF-8?q?=EF=B8=8F=20Enhance=20the=20accessibility=20of=20the=20new=20Se?= =?UTF-8?q?ttings=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reusable-components/_button.scss | 21 ++++- .../reusable-components/_fields.scss | 76 +++++++++++++++++-- .../reusable-components/_navigation.scss | 5 ++ .../css/components/screens/_modals.scss | 15 ++++ .../css/components/screens/_settings.scss | 2 +- .../ReusableComponents/AccordionSection.js | 36 +++++---- .../SettingsBlocks/PaymentMethodItemBlock.js | 13 +++- .../ReusableComponents/SettingsCard.js | 26 ++++++- .../ReusableComponents/SpinnerOverlay.js | 22 +++--- .../Components/ReusableComponents/TabBar.js | 10 ++- .../Screens/Settings/Components/Navigation.js | 63 ++++++++++----- .../Components/Overview/Features/Features.js | 12 ++- .../Components/Overview/Todos/Todos.js | 5 +- .../Screens/Settings/Tabs/TabOverview.js | 20 ++++- 14 files changed, 268 insertions(+), 58 deletions(-) diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss index 8116068a9..884493ada 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_button.scss @@ -70,6 +70,15 @@ button.components-button, a.components-button { --button-disabled-color: #{$color-gray-100}; --button-disabled-background: #{$color-gray-500}; + + &:hover:not(:disabled) { + background: #{$color-blue}; + } + + + &:not(.components-tab-panel__tabs-item):focus-visible:not(:disabled) { + outline: 2px solid #{$color-gray-500}; + } } &.is-secondary { @@ -86,7 +95,7 @@ button.components-button, a.components-button { --button-color: #{$color-blueberry}; --button-hover-color: #{$color-gradient-dark}; - &:focus:not(:disabled) { + &:focus-visible:not(:disabled) { border: none; box-shadow: none; } @@ -95,6 +104,16 @@ button.components-button, a.components-button { &.small-button { @include small-button; } + + &:focus:not(:disabled) { + outline: none; + } + + &:focus-visible:not(:disabled), + &:not(.components-tab-panel__tabs-item):focus-visible:not(:disabled) + &[data-focus-visible="true"] { + outline: 2px solid #{$color-blueberry}; + } } .ppcp--is-loading { diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss index 195367dfb..7dba93d17 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_fields.scss @@ -111,12 +111,7 @@ margin: 0; } } - // Custom styles. - .components-form-toggle.is-checked > .components-form-toggle__track { - background-color: $color-blueberry; - } - .ppcp-r-vertical-text-control { .components-base-control__field { display: flex; @@ -126,3 +121,74 @@ } } } + +.ppcp-r-app, .ppcp-r-modal__container { + // Form toggle styling. + .components-form-toggle { + &.is-checked { + > .components-form-toggle__track { + background-color: $color-blueberry; + } + .components-form-toggle__track { + border-color: $color-blueberry; + } + } + .components-form-toggle__input { + &:focus { + + .components-form-toggle__track { + box-shadow: none; + } + } + &:focus-visible + .components-form-toggle__track { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, + 0 0 0 calc(var(--wp-admin-border-width-focus)*2) $color-blueberry; + } + } + } + + // Form inputs. + .components-text-control__input { + &:focus, + &[type="color"]:focus, + &[type="date"]:focus, + &[type="datetime-local"]:focus, + &[type="datetime"]:focus, + &[type="email"]:focus, + &[type="month"]:focus, + &[type="number"]:focus, + &[type="password"]:focus, + &[type="tel"]:focus, + &[type="text"]:focus, + &[type="time"]:focus, + &[type="url"]:focus, + &[type="week"]:focus { + border-color: $color-blueberry; + } + } + + // Radio inputs. + .components-radio-control__input[type="radio"] { + &:checked { + background-color: $color-blueberry; + border-color: $color-blueberry; + } + &:focus { + box-shadow: none; + } + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, + 0 0 0 calc(var(--wp-admin-border-width-focus)*2) $color-blueberry; + } + } + + // Checkbox inputs. + .components-checkbox-control__input[type="checkbox"] { + &:focus { + box-shadow: none; + } + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) #fff, + 0 0 0 calc(var(--wp-admin-border-width-focus)*2) $color-blueberry; + } + } + } \ No newline at end of file diff --git a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss index b3188b461..50c798438 100644 --- a/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss +++ b/modules/ppcp-settings/resources/css/components/reusable-components/_navigation.scss @@ -103,6 +103,11 @@ $margin_bottom: 48px; .components-tab-panel__tabs-item { height: var(--subnavigation-height); + + &:focus-visible:not(:disabled), + &[data-focus-visible="true"]:focus:not(:disabled) { + outline: none; + } } } diff --git a/modules/ppcp-settings/resources/css/components/screens/_modals.scss b/modules/ppcp-settings/resources/css/components/screens/_modals.scss index 5dbdf7652..0b628556e 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_modals.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_modals.scss @@ -17,3 +17,18 @@ } } } + +.ppcp-r-modal { + button.components-button, + a.components-button { + &:focus:not(:disabled) { + outline: none; + } + + &:focus-visible:not(:disabled), + &:not(.components-tab-panel__tabs-item):focus-visible:not(:disabled) + &[data-focus-visible="true"] { + outline: 2px solid #{$color-blueberry}; + } + } +} diff --git a/modules/ppcp-settings/resources/css/components/screens/_settings.scss b/modules/ppcp-settings/resources/css/components/screens/_settings.scss index 63952ed1f..232dbabd8 100644 --- a/modules/ppcp-settings/resources/css/components/screens/_settings.scss +++ b/modules/ppcp-settings/resources/css/components/screens/_settings.scss @@ -103,7 +103,7 @@ &__dismiss { position: absolute; - right: 0; + right: 2px; top: 50%; transform: translateY(-50%); background-color: transparent; diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js index ca73ab531..228dafc5f 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/AccordionSection.js @@ -1,7 +1,6 @@ import { Icon } from '@wordpress/components'; import { chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; - import { useToggleState } from '../../hooks/useToggleState'; import { Content, @@ -22,33 +21,44 @@ const Accordion = ( { className = '', } ) => { const { isOpen, toggleOpen } = useToggleState( id, initiallyOpen ); - const wrapperClasses = classNames( 'ppcp-r-accordion', className, { - 'ppcp--is-open': isOpen, - } ); - const contentClass = classNames( 'ppcp--accordion-content', { - 'ppcp--is-open': isOpen, - } ); - - const icon = isOpen ? chevronUp : chevronDown; + const contentId = id + ? `${ id }-content` + : `accordion-${ title.replace( /\s+/g, '-' ).toLowerCase() }-content`; return ( -
+
-
+
{ children }
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js index 2a2b52b41..a415ae7f1 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsBlocks/PaymentMethodItemBlock.js @@ -31,10 +31,15 @@ const PaymentMethodItemBlock = ( { id={ paymentMethod.id } className={ methodItemClasses } separatorAndGap={ false } + aria-disabled={ isDisabled ? 'true' : 'false' } > { isDisabled && ( -
-

+

+

{ disabledMessage }

@@ -60,6 +65,8 @@ const PaymentMethodItemBlock = ( { __nextHasNoMarginBottom checked={ isSelected } onChange={ onSelect } + disabled={ isDisabled } + aria-label={ `Enable ${ paymentMethod.itemTitle }` } /> { hasWarning && ! isDisabled && isSelected && ( diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js index 97db00c2f..9cb809635 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SettingsCard.js @@ -2,6 +2,18 @@ import classNames from 'classnames'; import { Content } from './Elements'; +/** + * Renders a settings card. + * + * @param {Object} props Component properties + * @param {string} [props.id] Unique identifier for the card + * @param {string} [props.className] Additional CSS classes + * @param {string} props.title Card title + * @param {*} props.description Card description content + * @param {*} props.children Card content + * @param {boolean} [props.contentContainer=true] Whether to wrap content in a container + * @return {JSX.Element} The settings card component + */ const SettingsCard = ( { id, className, @@ -16,14 +28,20 @@ const SettingsCard = ( { id, }; + const titleId = id ? `${ id }-title` : undefined; + const descriptionId = id ? `${ id }-description` : undefined; + return ( -
+
- +

{ title } - -
+

+
{ description }
diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js index a3b16c07c..44810871d 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/SpinnerOverlay.js @@ -2,20 +2,24 @@ import { __ } from '@wordpress/i18n'; import { Spinner } from '@wordpress/components'; import classnames from 'classnames'; -const SpinnerOverlay = ( { asModal = false, message = null } ) => { +/** + * Renders a loading spinner. + * + * @param {Object} props Component properties. + * @param {boolean} [props.asModal=false] Whether to display the spinner as a modal overlay. + * @param {string} [props.ariaLabel] Accessible label for screen readers. + * @return {JSX.Element} The spinner overlay component. + */ +const SpinnerOverlay = ( { + asModal = false, + ariaLabel = __( 'Loading…', 'woocommerce-paypal-payments' ), +} ) => { const className = classnames( 'ppcp-r-spinner-overlay', { 'ppcp--is-modal': asModal, } ); - if ( null === message ) { - message = __( 'Loading…', 'woocommerce-paypal-payments' ); - } - return ( -
- { message && ( - { message } - ) } +
); diff --git a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js index 0d902e12c..f5387b617 100644 --- a/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js +++ b/modules/ppcp-settings/resources/js/Components/ReusableComponents/TabBar.js @@ -30,8 +30,16 @@ const TabBar = ( { tabs, activePanel, setActivePanel } ) => { initialTabName={ activePanel } onSelect={ updateActivePanel } tabs={ tabs } + orientation="horizontal" + selectOnMove={ false } > - { () => '' } + { ( tab ) => ( +
+ { tab.render ? tab.render() : '' } +
+ ) } ); }; diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js index ad72cbfe1..a1f18e7e7 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js @@ -1,5 +1,6 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import TopNavigation from '../../../ReusableComponents/TopNavigation'; @@ -21,8 +22,17 @@ const SettingsNavigation = ( { setActivePanel = () => {}, } ) => { const { persistAll } = useStoreManager(); - const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); + const [ isSaving, setIsSaving ] = useState( false ); + + const handleSave = () => { + setIsSaving( true ); + speak( + __( 'Saving settings…', 'woocommerce-paypal-payments' ), + 'assertive' + ); + persistAll(); + }; return ( { canSave && ( <> - - + ) } @@ -50,24 +69,26 @@ const SettingsNavigation = ( { export default SettingsNavigation; -const SaveStateMessage = () => { - const [ isSaving, setIsSaving ] = useState( false ); +const SaveStateMessage = ( { setIsSaving, isSaving } ) => { const [ isVisible, setIsVisible ] = useState( false ); const [ isAnimating, setIsAnimating ] = useState( false ); const { onStarted, onFinished } = CommonHooks.useActivityObserver(); const timerRef = useRef( null ); - const handleActivityStart = useCallback( ( started ) => { - if ( started.startsWith( 'persist' ) ) { - setIsSaving( true ); - setIsVisible( false ); - setIsAnimating( false ); + const handleActivityStart = useCallback( + ( started ) => { + if ( started.startsWith( 'persist' ) ) { + setIsSaving( true ); + setIsVisible( false ); + setIsAnimating( false ); - if ( timerRef.current ) { - clearTimeout( timerRef.current ); + if ( timerRef.current ) { + clearTimeout( timerRef.current ); + } } - } - }, [] ); + }, + [ setIsSaving ] + ); const handleActivityDone = useCallback( ( done, remaining ) => { @@ -76,6 +97,14 @@ const SaveStateMessage = () => { setIsVisible( true ); setTimeout( () => setIsAnimating( true ), 50 ); + speak( + __( + 'Settings saved successfully.', + 'woocommerce-paypal-payments' + ), + 'assertive' + ); + timerRef.current = setTimeout( () => { setIsAnimating( false ); setTimeout( @@ -85,7 +114,7 @@ const SaveStateMessage = () => { }, SAVE_CONFIRMATION_DURATION ); } }, - [ isSaving ] + [ isSaving, setIsSaving ] ); useEffect( () => { @@ -102,7 +131,7 @@ const SaveStateMessage = () => { } ); return ( - + { __( 'Completed', 'woocommerce-paypal-payments' ) } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js index c8a20186d..cd5215b0f 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Features/Features.js @@ -43,7 +43,10 @@ const Features = () => { 'Features refreshed successfully.', 'woocommerce-paypal-payments' ), - { icon: NOTIFICATION_SUCCESS } + { + icon: NOTIFICATION_SUCCESS, + speak: true, + } ); } else { throw new Error( @@ -58,7 +61,10 @@ const Features = () => { error.message || __( 'Unknown error', 'woocommerce-paypal-payments' ) ), - { icon: NOTIFICATION_ERROR } + { + icon: NOTIFICATION_ERROR, + speak: true, + } ); } finally { setIsRefreshing( false ); @@ -76,6 +82,8 @@ const Features = () => { /> } contentContainer={ false } + aria-live="polite" + aria-busy={ isRefreshing } > { features.map( ( { id, enabled, ...feature } ) => ( diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js index 6be06ab6b..cc7ce7c7d 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Overview/Todos/Todos.js @@ -33,7 +33,10 @@ const Todos = () => { 'Dismissed items restored successfully.', 'woocommerce-paypal-payments' ), - { icon: NOTIFICATION_SUCCESS } + { + icon: NOTIFICATION_SUCCESS, + speak: true, + } ); } finally { setIsResetting( false ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js index 0d16c707c..ee864ed34 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js @@ -1,3 +1,4 @@ +import { __ } from '@wordpress/i18n'; import Todos from '../Components/Overview/Todos/Todos'; import Features from '../Components/Overview/Features/Features'; import Help from '../Components/Overview/Help/Help'; @@ -14,11 +15,26 @@ const TabOverview = () => { usePaymentGatewaySync(); if ( ! areTodosReady || ! merchantIsReady || ! featuresIsReady ) { - return ; + return ( + + ); } return ( -
+