ARIA Accessibility improvements (#10674)

* Add missing aria-expanded attributes

* Improve autoComplete for phone numbers & email addresses

* Fix room summary card heading order

* Fix missing label on timeline search field

* Use appropriate semantic elements for dropdown listbox

* Use semantic list elements in keyboard settings tab

* Use semantic list elements in spotlight

* Fix types and i18n

* Improve types

* Update tests

* Add snapshot test
This commit is contained in:
Michael Telatynski 2023-04-20 18:13:30 +01:00 committed by GitHub
parent 2da52372d4
commit 782060a26e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 611 additions and 157 deletions

View file

@ -164,6 +164,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
searchEnabled={true}
disabled={this.props.disabled}
label={_t("Country Dropdown")}
autoComplete="tel-country-code"
>
{options}
</Dropdown>

View file

@ -15,18 +15,21 @@ limitations under the License.
*/
import classNames from "classnames";
import React, { ComponentProps, ReactNode } from "react";
import React, { ReactNode, RefObject } from "react";
import { RovingAccessibleButton } from "../../../../accessibility/roving/RovingAccessibleButton";
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton";
interface OptionProps extends ComponentProps<typeof RovingAccessibleButton> {
interface OptionProps {
inputRef?: RefObject<HTMLLIElement>;
endAdornment?: ReactNode;
id?: string;
className?: string;
onClick: ((ev: ButtonEvent) => void) | null;
}
export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment, className, ...props }) => {
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLLIElement>(inputRef);
return (
<AccessibleButton
{...props}
@ -36,6 +39,7 @@ export const Option: React.FC<OptionProps> = ({ inputRef, children, endAdornment
tabIndex={-1}
aria-selected={isActive}
role="option"
element="li"
>
{children}
<div className="mx_SpotlightDialog_option--endAdornment">

View file

@ -30,7 +30,7 @@ interface IMenuOptionProps {
highlighted?: boolean;
dropdownKey: string;
id?: string;
inputRef?: Ref<HTMLDivElement>;
inputRef?: Ref<HTMLLIElement>;
onClick(dropdownKey: string): void;
onMouseEnter(dropdownKey: string): void;
}
@ -57,7 +57,7 @@ class MenuOption extends React.Component<IMenuOptionProps> {
});
return (
<div
<li
id={this.props.id}
className={optClasses}
onClick={this.onClick}
@ -67,7 +67,7 @@ class MenuOption extends React.Component<IMenuOptionProps> {
ref={this.props.inputRef}
>
{this.props.children}
</div>
</li>
);
}
}
@ -78,6 +78,7 @@ export interface DropdownProps {
label: string;
value?: string;
className?: string;
autoComplete?: string;
children: NonEmptyArray<ReactElement & { key: string }>;
// negative for consistency with HTML
disabled?: boolean;
@ -318,21 +319,21 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
});
if (!options?.length) {
return [
<div key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
<li key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
{_t("No results")}
</div>,
</li>,
];
}
return options;
}
public render(): React.ReactNode {
let currentValue;
let currentValue: JSX.Element | undefined;
const menuStyle: CSSProperties = {};
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
let menu;
let menu: JSX.Element | undefined;
if (this.state.expanded) {
if (this.props.searchEnabled) {
currentValue = (
@ -340,6 +341,7 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
id={`${this.props.id}_input`}
type="text"
autoFocus={true}
autoComplete={this.props.autoComplete}
className="mx_Dropdown_option"
onChange={this.onInputChange}
value={this.state.searchQuery}
@ -355,9 +357,9 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
);
}
menu = (
<div className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
<ul className="mx_Dropdown_menu" style={menuStyle} role="listbox" id={`${this.props.id}_listbox`}>
{this.getMenuOptions()}
</div>
</ul>
);
}

View file

@ -47,7 +47,7 @@ interface IGroupProps {
export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
return (
<div className={classNames("mx_BaseCard_Group", className)}>
<h1>{title}</h1>
<h2>{title}</h2>
{children}
</div>
);

View file

@ -318,7 +318,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose })
/>
</div>
<RoomName room={room}>{(name) => <h2 title={name}>{name}</h2>}</RoomName>
<RoomName room={room}>{(name) => <h1 title={name}>{name}</h1>}</RoomName>
<div className="mx_RoomSummaryCard_alias" title={alias}>
{alias}
</div>

View file

@ -121,6 +121,9 @@ export default class SearchBar extends React.Component<IProps, IState> {
type="text"
autoFocus={true}
placeholder={_t("Search…")}
aria-label={
this.state.scope === SearchScope.Room ? _t("Search this room") : _t("Search all rooms")
}
onKeyDown={this.onSearchChange}
/>
<AccessibleButton

View file

@ -280,7 +280,7 @@ export default class EmailAddresses extends React.Component<IProps, IState> {
<Field
type="text"
label={_t("Email Address")}
autoComplete="off"
autoComplete="email"
disabled={this.state.verifying}
value={this.state.newEmailAddress}
onChange={this.onChangeNewEmailAddress}

View file

@ -310,7 +310,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
<Field
type="text"
label={_t("Phone Number")}
autoComplete="off"
autoComplete="tel-national"
disabled={this.state.verifying}
prefixComponent={phoneCountry}
value={this.state.newPhoneNumber}

View file

@ -443,6 +443,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
onClick={this.toggleAdvancedSection}
kind="link"
className="mx_SettingsTab_showAdvanced"
aria-expanded={this.state.showAdvancedSection}
>
{this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")}
</AccessibleButton>

View file

@ -88,7 +88,11 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
const brand = SdkConfig.get().brand;
const toggle = (
<AccessibleButton kind="link" onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}>
<AccessibleButton
kind="link"
onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}
aria-expanded={this.state.showAdvanced}
>
{this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
</AccessibleButton>
);

View file

@ -41,10 +41,10 @@ const KeyboardShortcutRow: React.FC<IKeyboardShortcutRowProps> = ({ name }) => {
if (!displayName || !value) return null;
return (
<div className="mx_KeyboardShortcut_shortcutRow">
<li className="mx_KeyboardShortcut_shortcutRow">
{displayName}
<KeyboardShortcut value={value} />
</div>
</li>
);
};
@ -59,12 +59,12 @@ const KeyboardShortcutSection: React.FC<IKeyboardShortcutSectionProps> = ({ cate
return (
<div className="mx_SettingsTab_section" key={categoryName}>
<div className="mx_SettingsTab_subheading">{_t(category.categoryLabel)}</div>
<div>
<ul>
{" "}
{category.settingNames.map((shortcutName) => {
return <KeyboardShortcutRow key={shortcutName} name={shortcutName} />;
})}{" "}
</div>
</ul>
</div>
);
};

View file

@ -134,6 +134,7 @@ const QuickSettingsButton: React.FC<{
title={_t("Quick settings")}
inputRef={handle}
forceHide={!isPanelCollapsed}
aria-expanded={!isPanelCollapsed}
>
{!isPanelCollapsed ? _t("Settings") : null}
</AccessibleTooltipButton>

View file

@ -97,6 +97,7 @@ const SpaceSettingsVisibilityTab: React.FC<IProps> = ({ matrixClient: cli, space
onClick={toggleAdvancedSection}
kind="link"
className="mx_SettingsTab_showAdvanced"
aria-expanded={showAdvancedSection}
>
{showAdvancedSection ? _t("Hide advanced") : _t("Show advanced")}
</AccessibleButton>

View file

@ -2141,6 +2141,8 @@
"This Room": "This Room",
"All Rooms": "All Rooms",
"Search…": "Search…",
"Search this room": "Search this room",
"Search all rooms": "Search all rooms",
"Failed to connect to integration manager": "Failed to connect to integration manager",
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
"Add some now": "Add some now",