Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Ensure connector card modals and links are accessible.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
margin: 0;
}

.wpcom-connector__details-link:focus:not(:active) {
.wpcom-connector__details-link:focus:not(:focus-visible) {
box-shadow: none;
outline: none;
}
Expand All @@ -74,6 +74,12 @@
margin-inline-start: auto;
}

.wpcom-connector__modal .components-button:focus:not(:focus-visible),
.wpcom-connector__confirm-modal .components-button:focus:not(:focus-visible) {
box-shadow: none;
outline: none;
}

.wpcom-connector__modal.components-modal__frame,
.wpcom-connector__modal {
max-inline-size: 480px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const registerConnector =
connectors.__experimentalRegisterConnector || connectors.registerConnector;
const ConnectorItem = connectors.__experimentalConnectorItem || connectors.ConnectorItem;

const { createElement, useState } = window.wp.element;
const { createElement, useState, useRef } = window.wp.element;
const { __ } = window.wp.i18n;
const { Button, Modal } = window.wp.components;
const HStack = window.wp.components.__experimentalHStack || window.wp.components.HStack;
Expand Down Expand Up @@ -136,6 +136,35 @@ function addSkipPricing( url ) {
}
}

/**
* Focus an element once #wpwrap no longer has aria-hidden.
*
* The blur-before-open pattern on modal triggers and this helper are both
* workarounds for a Gutenberg bug where aria-hidden is applied to #wpwrap
* before focus has moved into the modal portal, causing a browser console warning.
*
* @see https://github.com/WordPress/gutenberg/issues/41503
*
* @param {HTMLElement|null} element - Element to focus.
*/
function focusWhenReady( element ) {
if ( ! element ) {
return;
}
const wpwrap = document.getElementById( 'wpwrap' );
if ( ! wpwrap || ! wpwrap.hasAttribute( 'aria-hidden' ) ) {
element.focus();
return;
}
const observer = new MutationObserver( () => {
if ( ! wpwrap.hasAttribute( 'aria-hidden' ) ) {
observer.disconnect();
element.focus();
}
} );
observer.observe( wpwrap, { attributes: true, attributeFilter: [ 'aria-hidden' ] } );
}

/* ── Small presentational components ────────────────────────────── */

/**
Expand Down Expand Up @@ -342,7 +371,13 @@ function ConnectPrompt( { onConnect, isConnecting, isDisconnecting } ) {
function ConfirmationModal( { title, message, onConfirm, onCancel } ) {
return createElement(
Modal,
{ title, onRequestClose: onCancel, size: 'small' },
{
title,
onRequestClose: onCancel,
size: 'small',
role: 'alertdialog',
className: 'wpcom-connector__confirm-modal',
},
createElement(
VStack,
{ spacing: 5 },
Expand All @@ -352,15 +387,14 @@ function ConfirmationModal( { title, message, onConfirm, onCancel } ) {
{ spacing: 3, justify: 'flex-end' },
createElement(
Button,
{ variant: 'tertiary', size: 'compact', onClick: onCancel },
{ variant: 'tertiary', size: 'compact', onClick: onCancel, autoFocus: true },
__( 'Cancel', 'jetpack-connection' )
),
createElement(
Button,
{
variant: 'primary',
isDestructive: true,
size: 'compact',
onClick: onConfirm,
},
__( 'Disconnect', 'jetpack-connection' )
Expand Down Expand Up @@ -398,6 +432,7 @@ function SiteDetailsModal( { onClose } ) {
title: __( 'Connection details', 'jetpack-connection' ),
onRequestClose: onClose,
size: 'small',
focusOnMount: 'firstElement',
},
createElement(
'div',
Expand Down Expand Up @@ -433,6 +468,10 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
const [ showDetailsModal, setShowDetailsModal ] = useState( false );
const [ pendingConfirm, setPendingConfirm ] = useState( null );
const [ actionError, setActionError ] = useState( null );
const detailsLinkRef = useRef( null );
const confirmTriggerRef = useRef( null );
const disconnectSiteRef = useRef( null );
const disconnectAccountRef = useRef( null );

const executeDisconnect = async () => {
setPendingConfirm( null );
Expand Down Expand Up @@ -469,6 +508,7 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
};

const handleDisconnect = () => {
confirmTriggerRef.current = disconnectSiteRef.current;
setPendingConfirm( {
title: __( 'Disconnect site', 'jetpack-connection' ),
message: __(
Expand Down Expand Up @@ -519,6 +559,7 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
};

const handleUnlinkUser = () => {
confirmTriggerRef.current = disconnectAccountRef.current;
const message =
currentUser?.isOwner && currentUser?.hasOtherConnectedUsers
? __(
Expand Down Expand Up @@ -554,6 +595,7 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
: createElement(
Button,
{
ref: disconnectAccountRef,
variant: 'link',
isDestructive: true,
isBusy: isUnlinking,
Expand Down Expand Up @@ -610,9 +652,12 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
? createElement(
Button,
{
ref: detailsLinkRef,
variant: 'link',
size: 'compact',
onClick: () => setShowDetailsModal( true ),
onClick: e => {
e.currentTarget.blur();
setShowDetailsModal( true );
},
className: 'wpcom-connector__details-link',
},
__( 'Connection details', 'jetpack-connection' )
Expand All @@ -623,6 +668,7 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
: createElement(
Button,
{
ref: disconnectSiteRef,
variant: 'secondary',
isDestructive: true,
size: 'compact',
Expand All @@ -638,15 +684,21 @@ function ExpandedDetails( { isConnecting = false, onConnect = null } ) {
// Modals (rendered but visually hidden until triggered).
showDetailsModal && siteDetails
? createElement( SiteDetailsModal, {
onClose: () => setShowDetailsModal( false ),
onClose: () => {
setShowDetailsModal( false );
focusWhenReady( detailsLinkRef.current );
},
} )
: null,
pendingConfirm
? createElement( ConfirmationModal, {
title: pendingConfirm.title,
message: pendingConfirm.message,
onConfirm: pendingConfirm.onConfirm,
onCancel: () => setPendingConfirm( null ),
onCancel: () => {
setPendingConfirm( null );
focusWhenReady( confirmTriggerRef.current );
},
} )
: null
);
Expand Down
Loading