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:
parent
2da52372d4
commit
782060a26e
24 changed files with 611 additions and 157 deletions
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -134,6 +134,7 @@ const QuickSettingsButton: React.FC<{
|
|||
title={_t("Quick settings")}
|
||||
inputRef={handle}
|
||||
forceHide={!isPanelCollapsed}
|
||||
aria-expanded={!isPanelCollapsed}
|
||||
>
|
||||
{!isPanelCollapsed ? _t("Settings") : null}
|
||||
</AccessibleTooltipButton>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue