[MM-62973][MM-63028][MM-63034][MM-63027] Various accessibility fixes around Browse Channels modal (#30922)

* [MM-62973] Fix order of Close button and other buttons in the header

* [MM-63028] Add aria-live status area when searching for channels

* [MM-63027] Add announcement that channel is joined

* [MM-63034] Add role=checkbox and reformat HTML to have correct custom checkbox code

* Update snapshots from generic_modal change

* PR feedback

* PR feedback

* PR feedback
Этот коммит содержится в:
Devin Binnie
2025-05-12 09:14:20 -04:00
коммит произвёл GitHub
родитель c82a24f396
Коммит 883711c72d
9 изменённых файлов: 310 добавлений и 121 удалений

Просмотреть файл

@@ -20,12 +20,12 @@ function verifyNoChannelToJoinMessage(isVisible) {
function ensureHideJoinedCheckboxEnabled(shouldBeChecked) {
cy.get('#hideJoinedPreferenceCheckbox').then(($checkbox) => {
cy.wrap($checkbox).findByText('Hide Joined').should('be.visible');
cy.wrap($checkbox).find('button').invoke('attr', 'class').then(($classList) => {
cy.wrap($checkbox).find('div.get-app__checkbox').invoke('attr', 'class').then(($classList) => {
if ($classList.split(' ').includes('checked') ^ shouldBeChecked) {
// We click on the button only when the XOR operands do not match
// e.g. checkbox is checked, but should not be checked; and vice-versa
cy.wrap($checkbox).click();
cy.wrap($checkbox).find('button').should(`${shouldBeChecked ? '' : 'not.'}have.class`, 'checked');
cy.wrap($checkbox).find('div.get-app__checkbox').should(`${shouldBeChecked ? '' : 'not.'}have.class`, 'checked');
}
});
});

Просмотреть файл

@@ -120,8 +120,8 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
// # Hide already joined channels
cy.findByText('Hide Joined').click();
// # Focus on the Create Channel button and TAB four time
cy.get('#createNewChannelButton').focus().tab().tab().tab().tab();
// # Focus on the Create Channel button and TAB five time
cy.get('#createNewChannelButton').focus().tab().tab().tab().tab().tab();
// * Verify channel name is highlighted and reader reads the channel name and channel description
cy.get('#moreChannelsList').within(() => {

Просмотреть файл

@@ -121,11 +121,15 @@ exports[`components/SearchableChannelList should match init snapshot 1`] = `
/>
</Menu>
<div
aria-checked={false}
aria-label="Hide joined channels"
id="hideJoinedPreferenceCheckbox"
onClick={[Function]}
onKeyDown={[Function]}
role="checkbox"
tabIndex={0}
>
<button
aria-label="Hide joined channels checkbox, not checked"
<div
className="get-app__checkbox"
/>
<MemoizedFormattedMessage

Просмотреть файл

@@ -40,22 +40,6 @@ Object {
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
>
@@ -87,6 +71,22 @@ Object {
</h1>
</div>
</div>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
</div>
<div
class="modal-body"
@@ -522,22 +522,6 @@ Object {
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
>
@@ -569,6 +553,22 @@ Object {
</h1>
</div>
</div>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
</div>
<div
class="modal-body"
@@ -1164,22 +1164,6 @@ Object {
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
>
@@ -1211,6 +1195,22 @@ Object {
</h1>
</div>
</div>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
</div>
<div
class="modal-body"
@@ -1525,22 +1525,6 @@ Object {
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
>
@@ -1572,6 +1556,22 @@ Object {
</h1>
</div>
</div>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
</div>
<div
class="modal-body"
@@ -2105,22 +2105,6 @@ Object {
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
>
@@ -2152,6 +2136,22 @@ Object {
</h1>
</div>
</div>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
</div>
<div
class="modal-body"
@@ -2685,22 +2685,6 @@ Object {
<div
class="modal-header"
>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
>
@@ -2732,6 +2716,22 @@ Object {
</h1>
</div>
</div>
<button
aria-label="Close"
class="close"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
class="sr-only"
>
Close
</span>
</button>
</div>
<div
class="modal-body"

Просмотреть файл

@@ -40,7 +40,7 @@ exports[`components/marketplace/ doesn't show web marketplace banner in FeatureF
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -57,6 +57,26 @@ exports[`components/marketplace/ doesn't show web marketplace banner in FeatureF
</h1>
</div>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"
@@ -132,7 +152,7 @@ exports[`components/marketplace/ hides search, shows web marketplace banner in F
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -149,6 +169,26 @@ exports[`components/marketplace/ hides search, shows web marketplace banner in F
</h1>
</div>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"
@@ -225,7 +265,7 @@ exports[`components/marketplace/ should render default 1`] = `
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -262,6 +302,26 @@ exports[`components/marketplace/ should render default 1`] = `
value=""
/>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"
@@ -369,7 +429,7 @@ exports[`components/marketplace/ should render with error banner 1`] = `
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -406,6 +466,26 @@ exports[`components/marketplace/ should render with error banner 1`] = `
value=""
/>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"
@@ -530,7 +610,7 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -567,6 +647,26 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
value=""
/>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"
@@ -682,7 +782,7 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -719,6 +819,26 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
value=""
/>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"
@@ -852,7 +972,7 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeButton={false}
closeLabel="Close"
>
<div
@@ -889,6 +1009,26 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
value=""
/>
</div>
<button
aria-label="Close"
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<MemoizedFormattedMessage
defaultMessage="Close"
id="generic_modal.close"
/>
</span>
</button>
</ModalHeader>
<ModalBody
bsClass="modal-body"

Просмотреть файл

@@ -223,6 +223,20 @@ export class SearchableChannelList extends React.PureComponent<Props, State> {
</div>
<div className='more-modal__actions'>
{joinViewChannelButton}
{this.state.joiningChannel === channel.id && !this.isMemberOfChannel(channel.id) && (
<span
className='sr-only'
role='alert'
>
<FormattedMessage
id='more_channels.joinedChannel'
defaultMessage='Joined channel {channelName}'
values={{
channelName: channel.display_name,
}}
/>
</span>
)}
</div>
</div>
);
@@ -530,13 +544,20 @@ export class SearchableChannelList extends React.PureComponent<Props, State> {
<div
id={'hideJoinedPreferenceCheckbox'}
onClick={this.handleChecked}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === 'Enter' || e.key === ' ') {
this.handleChecked();
}
}}
role='checkbox'
aria-checked={this.props.rememberHideJoinedChannelsChecked}
aria-label={this.props.intl.formatMessage({id: 'more_channels.hide_joined_channels', defaultMessage: 'Hide joined channels'})}
tabIndex={0}
>
<button
className={hideJoinedButtonClass}
aria-label={this.props.rememberHideJoinedChannelsChecked ? this.props.intl.formatMessage({id: 'more_channels.hide_joined_checked', defaultMessage: 'Hide joined channels checkbox, checked'}) : this.props.intl.formatMessage({id: 'more_channels.hide_joined_not_checked', defaultMessage: 'Hide joined channels checkbox, not checked'})}
>
<div className={hideJoinedButtonClass}>
{this.props.rememberHideJoinedChannelsChecked ? <CheckboxCheckedIcon/> : null}
</button>
</div>
<FormattedMessage
id='more_channels.hide_joined'
defaultMessage='Hide Joined'
@@ -558,6 +579,15 @@ export class SearchableChannelList extends React.PureComponent<Props, State> {
const dropDownContainer = (
<div className='more-modal__dropdown'>
<span id='channelCountLabel'>{channelCountLabel}</span>
{this.props.isSearch &&
<span
className='sr-only'
role='status'
aria-live='polite'
>
{channelCountLabel}
</span>
}
<div id='modalPreferenceContainer'>
{channelDropdown}
{hideJoinedPreferenceCheckbox}

Просмотреть файл

@@ -4499,9 +4499,9 @@
"more_channels.count_zero": "0 Results",
"more_channels.create": "Create New Channel",
"more_channels.hide_joined": "Hide Joined",
"more_channels.hide_joined_checked": "Hide joined channels checkbox, checked",
"more_channels.hide_joined_not_checked": "Hide joined channels checkbox, not checked",
"more_channels.hide_joined_channels": "Hide joined channels",
"more_channels.joined": "Joined",
"more_channels.joinedChannel": "Joined channel {channelName}",
"more_channels.membership_indicator": "Membership Indicator: Joined",
"more_channels.next": "Next",
"more_channels.noArchived": "No archived channels",

Просмотреть файл

@@ -39,6 +39,9 @@ exports[`GenericModal should match snapshot for base case 1`] = `
<div
class="modal-header"
>
<div
class="GenericModal__header__text_container"
/>
<button
aria-label="Close"
class="close"
@@ -55,9 +58,6 @@ exports[`GenericModal should match snapshot for base case 1`] = `
Close
</span>
</button>
<div
class="GenericModal__header__text_container"
/>
</div>
<div
class="modal-body"

Просмотреть файл

@@ -4,7 +4,7 @@
import classNames from 'classnames';
import React, {useState, useEffect, useCallback, useRef} from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import {useFocusTrap} from '../hooks/useFocusTrap';
import './generic_modal.scss';
@@ -128,6 +128,8 @@ export const GenericModal: React.FC<Props> = ({
dataTestId,
delayFocusTrap,
}) => {
const intl = useIntl();
// Create a ref for the modal container
const containerRef = useRef<HTMLDivElement>(null);
@@ -290,7 +292,7 @@ export const GenericModal: React.FC<Props> = ({
className='GenericModal__wrapper GenericModal__wrapper-enter-key-press-catcher'
>
{showHeader && (
<Modal.Header closeButton={showCloseButton}>
<Modal.Header closeButton={false}>
<div className='GenericModal__header__text_container'>
{compassDesign && (
<>
@@ -306,6 +308,19 @@ export const GenericModal: React.FC<Props> = ({
</div>
)}
</div>
{showCloseButton && (
<button
type='button'
className='close'
onClick={onHideCallback}
aria-label={intl.formatMessage({id: 'generic_modal.close', defaultMessage: 'Close'})}
>
<span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>
<FormattedMessage id='generic_modal.close' defaultMessage='Close' />
</span>
</button>
)}
</Modal.Header>
)}
<Modal.Body className={classNames({divider: bodyDivider, 'overflow-visible': bodyOverflowVisible})}>