Merge pull request #4636 from matrix-org/t3chguy/emojipickera11y

Improve accessibility of the emoji picker
This commit is contained in:
Michael Telatynski 2020-05-26 12:02:07 +01:00 committed by GitHub
commit e049ead25f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 13 deletions

View file

@ -67,7 +67,13 @@ class Category extends React.PureComponent {
const localScrollTop = Math.max(0, scrollTop - listTop);
return (
<section className="mx_EmojiPicker_category" data-category-id={this.props.id}>
<section
id={`mx_EmojiPicker_category_${this.props.id}`}
className="mx_EmojiPicker_category"
data-category-id={this.props.id}
role="tabpanel"
aria-label={name}
>
<h2 className="mx_EmojiPicker_category_label">
{name}
</h2>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {MenuItem} from "../../structures/ContextMenu";
class Emoji extends React.PureComponent {
static propTypes = {
@ -30,14 +31,18 @@ class Emoji extends React.PureComponent {
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
return (
<li onClick={() => onClick(emoji)}
<MenuItem
element="li"
onClick={() => onClick(emoji)}
onMouseEnter={() => onMouseEnter(emoji)}
onMouseLeave={() => onMouseLeave(emoji)}
className="mx_EmojiPicker_item_wrapper">
className="mx_EmojiPicker_item_wrapper"
label={emoji.unicode}
>
<div className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
{emoji.unicode}
</div>
</li>
</MenuItem>
);
}
}

View file

@ -147,8 +147,12 @@ class EmojiPicker extends React.Component {
// We update this here instead of through React to avoid re-render on scroll.
if (cat.visible) {
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", true);
cat.ref.current.setAttribute("tabindex", 0);
} else {
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
cat.ref.current.setAttribute("aria-selected", false);
cat.ref.current.setAttribute("tabindex", -1);
}
}
}

View file

@ -16,23 +16,89 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import classNames from "classnames";
import {_t} from "../../../languageHandler";
import {Key} from "../../../Keyboard";
class Header extends React.PureComponent {
static propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
onAnchorClick: PropTypes.func.isRequired,
refs: PropTypes.object,
};
findNearestEnabled(index, delta) {
index += this.props.categories.length;
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
while (index < cats.length && index >= 0) {
if (cats[index].enabled) return index % this.props.categories.length;
index += delta > 0 ? 1 : -1;
}
}
changeCategoryRelative(delta) {
const current = this.props.categories.findIndex(c => c.visible);
this.changeCategoryAbsolute(current + delta, delta);
}
changeCategoryAbsolute(index, delta=1) {
const category = this.props.categories[this.findNearestEnabled(index, delta)];
if (category) {
this.props.onAnchorClick(category.id);
category.ref.current.focus();
}
}
// Implements ARIA Tabs with Automatic Activation pattern
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
onKeyDown = (ev) => {
let handled = true;
switch (ev.key) {
case Key.ARROW_LEFT:
this.changeCategoryRelative(-1);
break;
case Key.ARROW_RIGHT:
this.changeCategoryRelative(1);
break;
case Key.HOME:
this.changeCategoryAbsolute(0);
break;
case Key.END:
this.changeCategoryAbsolute(this.props.categories.length - 1, -1);
break;
default:
handled = false;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
};
render() {
return (
<nav className="mx_EmojiPicker_header">
{this.props.categories.map(category => (
<button disabled={!category.enabled} key={category.id} ref={category.ref}
className={`mx_EmojiPicker_anchor ${category.visible ? 'mx_EmojiPicker_anchor_visible' : ''}
mx_EmojiPicker_anchor_${category.id}`}
onClick={() => this.props.onAnchorClick(category.id)} title={category.name} />
))}
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
{this.props.categories.map(category => {
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
mx_EmojiPicker_anchor_visible: category.visible,
});
// Properties of this button are also modified by EmojiPicker's updateVisibility in DOM.
return <button
disabled={!category.enabled}
key={category.id}
ref={category.ref}
className={classes}
onClick={() => this.props.onAnchorClick(category.id)}
title={category.name}
role="tab"
tabIndex={category.visible ? 0 : -1} // roving
aria-selected={category.visible}
aria-controls={`mx_EmojiPicker_category_${category.id}`}
/>;
})}
</nav>
);
}

View file

@ -72,7 +72,7 @@ class QuickReactions extends React.Component {
</React.Fragment>
}
</h2>
<ul className="mx_EmojiPicker_list">
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
{QUICK_REACTIONS.map(emoji => <Emoji
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}

View file

@ -1408,6 +1408,7 @@
"Objects": "Objects",
"Symbols": "Symbols",
"Flags": "Flags",
"Categories": "Categories",
"Quick Reactions": "Quick Reactions",
"Cancel search": "Cancel search",
"Unknown Address": "Unknown Address",