Skip to content

Commit 4348559

Browse files
authored
Add relevant aria attribute for selected emoji in the emoji picker (#31125)
* Add relevant aria attribute for selected emoji in the emoji picker Signed-off-by: Michael Telatynski <[email protected]> * Add aria-multiselectable Signed-off-by: Michael Telatynski <[email protected]> * Do not specify aria-selected/pressed when element is disabled Signed-off-by: Michael Telatynski <[email protected]> * Use checkbox role for reaction picker as gridcell + aria-selected has very inconsistent screenreader support Signed-off-by: Michael Telatynski <[email protected]> * Fix keyboard handling for modified DOM structure Signed-off-by: Michael Telatynski <[email protected]> * Fix enter behaviour Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent fdf54dd commit 4348559

File tree

4 files changed

+57
-29
lines changed

4 files changed

+57
-29
lines changed

src/components/views/emojipicker/Category.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,17 @@ class Category extends React.PureComponent<IProps> {
6161
return (
6262
<div key={rowIndex} role="row">
6363
{emojisForRow.map((emoji) => (
64-
<Emoji
65-
key={emoji.hexcode}
66-
emoji={emoji}
67-
selectedEmojis={selectedEmojis}
68-
onClick={onClick}
69-
onMouseEnter={onMouseEnter}
70-
onMouseLeave={onMouseLeave}
71-
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
72-
id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
73-
role="gridcell"
74-
/>
64+
<div role="gridcell" className="mx_EmojiPicker_item_wrapper" key={emoji.hexcode}>
65+
<Emoji
66+
emoji={emoji}
67+
selectedEmojis={selectedEmojis}
68+
onClick={onClick}
69+
onMouseEnter={onMouseEnter}
70+
onMouseLeave={onMouseLeave}
71+
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
72+
id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
73+
/>
74+
</div>
7575
))}
7676
</div>
7777
);
@@ -118,6 +118,7 @@ class Category extends React.PureComponent<IProps> {
118118
overflowMargin={0}
119119
renderItem={this.renderEmojiRow}
120120
role="grid"
121+
aria-multiselectable
121122
/>
122123
</section>
123124
);

src/components/views/emojipicker/Emoji.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
1515

1616
interface IProps {
1717
emoji: IEmoji;
18+
/**
19+
* Set of which emojis are already selected and should be decorated as such.
20+
* If specified, emoji will use a checkbox role with aria-checked set appropriately.
21+
*/
1822
selectedEmojis?: Set<string>;
1923
onClick(ev: ButtonEvent, emoji: IEmoji): void;
2024
onMouseEnter(emoji: IEmoji): void;
2125
onMouseLeave(emoji: IEmoji): void;
2226
disabled?: boolean;
2327
id?: string;
24-
role?: string;
28+
className?: string;
2529
}
2630

2731
class Emoji extends React.PureComponent<IProps> {
@@ -34,9 +38,10 @@ class Emoji extends React.PureComponent<IProps> {
3438
onClick={(ev: ButtonEvent) => onClick(ev, emoji)}
3539
onMouseEnter={() => onMouseEnter(emoji)}
3640
onMouseLeave={() => onMouseLeave(emoji)}
37-
className="mx_EmojiPicker_item_wrapper"
38-
disabled={this.props.disabled}
39-
role={this.props.role}
41+
className={this.props.className}
42+
disabled={this.props.disabled || undefined}
43+
role={selectedEmojis ? "checkbox" : undefined}
44+
aria-checked={this.props.disabled ? undefined : isSelected}
4045
focusOnMouseOver
4146
>
4247
<div className={`mx_EmojiPicker_item ${isSelected ? "mx_EmojiPicker_item_selected" : ""}`}>

src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -152,36 +152,57 @@ class EmojiPicker extends React.Component<IProps, IState> {
152152
this.updateVisibility();
153153
};
154154

155+
// Given a roving emoji button returns the role=row element containing it
156+
private getRow(rovingNode?: Element): Element | undefined {
157+
return this.getGridcell(rovingNode)?.parentElement ?? undefined;
158+
}
159+
160+
// Given a roving emoji button returns the role=gridcell element containing it
161+
private getGridcell(rovingNode?: Element): Element | undefined {
162+
return rovingNode?.parentElement ?? undefined;
163+
}
164+
165+
// Given a role=gridcell node returns the roving emoji button contained within
166+
private getRovingNode(gridcellNode?: Element): Element | undefined {
167+
return gridcellNode?.children[0];
168+
}
169+
155170
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
156-
const node = state.activeNode;
157-
const parent = node?.parentElement;
158-
if (!parent || !state.activeNode) return;
159-
const rowIndex = Array.from(parent.children).indexOf(node);
171+
const rowElement = this.getRow(state.activeNode);
172+
const gridcellNode = this.getGridcell(state.activeNode);
173+
if (!rowElement || !gridcellNode || !state.activeNode) return;
174+
175+
// Index of element within row container
176+
const columnIndex = Array.from(rowElement.children).indexOf(gridcellNode);
177+
// Index of element within the list of roving nodes
160178
const refIndex = state.nodes.indexOf(state.activeNode);
161179

162180
let focusNode: HTMLElement | undefined;
163-
let newParent: HTMLElement | undefined;
181+
let newRowElement: Element | undefined;
164182
switch (ev.key) {
165183
case Key.ARROW_LEFT:
166184
focusNode = state.nodes[refIndex - 1];
167-
newParent = focusNode?.parentElement ?? undefined;
185+
newRowElement = this.getRow(focusNode);
168186
break;
169187

170188
case Key.ARROW_RIGHT:
171189
focusNode = state.nodes[refIndex + 1];
172-
newParent = focusNode?.parentElement ?? undefined;
190+
newRowElement = this.getRow(focusNode);
173191
break;
174192

175193
case Key.ARROW_UP:
176194
case Key.ARROW_DOWN: {
177195
// For up/down we find the prev/next parent by inspecting the refs either side of our row
178196
const node =
179197
ev.key === Key.ARROW_UP
180-
? state.nodes[refIndex - rowIndex - 1]
181-
: state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW];
182-
newParent = node?.parentElement ?? undefined;
183-
const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
184-
focusNode = state.nodes.find((r) => r === newTarget);
198+
? state.nodes[refIndex - columnIndex - 1]
199+
: state.nodes[refIndex - columnIndex + EMOJIS_PER_ROW];
200+
newRowElement = this.getRow(node);
201+
if (newRowElement) {
202+
const newColumnIndex = clamp(columnIndex, 0, newRowElement.children.length - 1);
203+
const newTarget = this.getRovingNode(newRowElement?.children[newColumnIndex]);
204+
focusNode = state.nodes.find((r) => r === newTarget);
205+
}
185206
break;
186207
}
187208
}
@@ -197,7 +218,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
197218
payload: { node: focusNode },
198219
});
199220

200-
if (parent !== newParent) {
221+
if (rowElement !== newRowElement) {
201222
focusNode?.scrollIntoView({
202223
behavior: "auto",
203224
block: "center",
@@ -315,7 +336,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
315336

316337
private onEnterFilter = (): void => {
317338
const btn = this.scrollRef.current?.containerRef.current?.querySelector<HTMLButtonElement>(
318-
'.mx_EmojiPicker_item_wrapper[tabindex="0"]',
339+
'.mx_EmojiPicker_item_wrapper [tabindex="0"]',
319340
);
320341
btn?.click();
321342
this.props.onFinished();

src/components/views/emojipicker/QuickReactions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class QuickReactions extends React.Component<IProps, IState> {
7373
onMouseEnter={this.onMouseEnter}
7474
onMouseLeave={this.onMouseLeave}
7575
selectedEmojis={this.props.selectedEmojis}
76+
className="mx_EmojiPicker_item_wrapper"
7677
/>
7778
))}
7879
</Toolbar>

0 commit comments

Comments
 (0)