mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-07 23:18:21 +00:00
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 <7t3chguy@gmail.com> * Add aria-multiselectable Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Do not specify aria-selected/pressed when element is disabled Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use checkbox role for reaction picker as gridcell + aria-selected has very inconsistent screenreader support Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix keyboard handling for modified DOM structure Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix enter behaviour Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
committed by
GitHub
parent
fdf54dd9c2
commit
43485594b5
@@ -61,17 +61,17 @@ class Category extends React.PureComponent<IProps> {
|
|||||||
return (
|
return (
|
||||||
<div key={rowIndex} role="row">
|
<div key={rowIndex} role="row">
|
||||||
{emojisForRow.map((emoji) => (
|
{emojisForRow.map((emoji) => (
|
||||||
<Emoji
|
<div role="gridcell" className="mx_EmojiPicker_item_wrapper" key={emoji.hexcode}>
|
||||||
key={emoji.hexcode}
|
<Emoji
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
selectedEmojis={selectedEmojis}
|
selectedEmojis={selectedEmojis}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
|
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
|
||||||
id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
|
id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
|
||||||
role="gridcell"
|
/>
|
||||||
/>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -118,6 +118,7 @@ class Category extends React.PureComponent<IProps> {
|
|||||||
overflowMargin={0}
|
overflowMargin={0}
|
||||||
renderItem={this.renderEmojiRow}
|
renderItem={this.renderEmojiRow}
|
||||||
role="grid"
|
role="grid"
|
||||||
|
aria-multiselectable
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
emoji: IEmoji;
|
emoji: IEmoji;
|
||||||
|
/**
|
||||||
|
* Set of which emojis are already selected and should be decorated as such.
|
||||||
|
* If specified, emoji will use a checkbox role with aria-checked set appropriately.
|
||||||
|
*/
|
||||||
selectedEmojis?: Set<string>;
|
selectedEmojis?: Set<string>;
|
||||||
onClick(ev: ButtonEvent, emoji: IEmoji): void;
|
onClick(ev: ButtonEvent, emoji: IEmoji): void;
|
||||||
onMouseEnter(emoji: IEmoji): void;
|
onMouseEnter(emoji: IEmoji): void;
|
||||||
onMouseLeave(emoji: IEmoji): void;
|
onMouseLeave(emoji: IEmoji): void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
role?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Emoji extends React.PureComponent<IProps> {
|
class Emoji extends React.PureComponent<IProps> {
|
||||||
@@ -34,9 +38,10 @@ class Emoji extends React.PureComponent<IProps> {
|
|||||||
onClick={(ev: ButtonEvent) => onClick(ev, emoji)}
|
onClick={(ev: ButtonEvent) => onClick(ev, emoji)}
|
||||||
onMouseEnter={() => onMouseEnter(emoji)}
|
onMouseEnter={() => onMouseEnter(emoji)}
|
||||||
onMouseLeave={() => onMouseLeave(emoji)}
|
onMouseLeave={() => onMouseLeave(emoji)}
|
||||||
className="mx_EmojiPicker_item_wrapper"
|
className={this.props.className}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled || undefined}
|
||||||
role={this.props.role}
|
role={selectedEmojis ? "checkbox" : undefined}
|
||||||
|
aria-checked={this.props.disabled ? undefined : isSelected}
|
||||||
focusOnMouseOver
|
focusOnMouseOver
|
||||||
>
|
>
|
||||||
<div className={`mx_EmojiPicker_item ${isSelected ? "mx_EmojiPicker_item_selected" : ""}`}>
|
<div className={`mx_EmojiPicker_item ${isSelected ? "mx_EmojiPicker_item_selected" : ""}`}>
|
||||||
|
|||||||
@@ -152,24 +152,42 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||||||
this.updateVisibility();
|
this.updateVisibility();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Given a roving emoji button returns the role=row element containing it
|
||||||
|
private getRow(rovingNode?: Element): Element | undefined {
|
||||||
|
return this.getGridcell(rovingNode)?.parentElement ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a roving emoji button returns the role=gridcell element containing it
|
||||||
|
private getGridcell(rovingNode?: Element): Element | undefined {
|
||||||
|
return rovingNode?.parentElement ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a role=gridcell node returns the roving emoji button contained within
|
||||||
|
private getRovingNode(gridcellNode?: Element): Element | undefined {
|
||||||
|
return gridcellNode?.children[0];
|
||||||
|
}
|
||||||
|
|
||||||
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
|
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
|
||||||
const node = state.activeNode;
|
const rowElement = this.getRow(state.activeNode);
|
||||||
const parent = node?.parentElement;
|
const gridcellNode = this.getGridcell(state.activeNode);
|
||||||
if (!parent || !state.activeNode) return;
|
if (!rowElement || !gridcellNode || !state.activeNode) return;
|
||||||
const rowIndex = Array.from(parent.children).indexOf(node);
|
|
||||||
|
// Index of element within row container
|
||||||
|
const columnIndex = Array.from(rowElement.children).indexOf(gridcellNode);
|
||||||
|
// Index of element within the list of roving nodes
|
||||||
const refIndex = state.nodes.indexOf(state.activeNode);
|
const refIndex = state.nodes.indexOf(state.activeNode);
|
||||||
|
|
||||||
let focusNode: HTMLElement | undefined;
|
let focusNode: HTMLElement | undefined;
|
||||||
let newParent: HTMLElement | undefined;
|
let newRowElement: Element | undefined;
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.ARROW_LEFT:
|
case Key.ARROW_LEFT:
|
||||||
focusNode = state.nodes[refIndex - 1];
|
focusNode = state.nodes[refIndex - 1];
|
||||||
newParent = focusNode?.parentElement ?? undefined;
|
newRowElement = this.getRow(focusNode);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Key.ARROW_RIGHT:
|
case Key.ARROW_RIGHT:
|
||||||
focusNode = state.nodes[refIndex + 1];
|
focusNode = state.nodes[refIndex + 1];
|
||||||
newParent = focusNode?.parentElement ?? undefined;
|
newRowElement = this.getRow(focusNode);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Key.ARROW_UP:
|
case Key.ARROW_UP:
|
||||||
@@ -177,11 +195,14 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||||||
// For up/down we find the prev/next parent by inspecting the refs either side of our row
|
// For up/down we find the prev/next parent by inspecting the refs either side of our row
|
||||||
const node =
|
const node =
|
||||||
ev.key === Key.ARROW_UP
|
ev.key === Key.ARROW_UP
|
||||||
? state.nodes[refIndex - rowIndex - 1]
|
? state.nodes[refIndex - columnIndex - 1]
|
||||||
: state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW];
|
: state.nodes[refIndex - columnIndex + EMOJIS_PER_ROW];
|
||||||
newParent = node?.parentElement ?? undefined;
|
newRowElement = this.getRow(node);
|
||||||
const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
|
if (newRowElement) {
|
||||||
focusNode = state.nodes.find((r) => r === newTarget);
|
const newColumnIndex = clamp(columnIndex, 0, newRowElement.children.length - 1);
|
||||||
|
const newTarget = this.getRovingNode(newRowElement?.children[newColumnIndex]);
|
||||||
|
focusNode = state.nodes.find((r) => r === newTarget);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +218,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||||||
payload: { node: focusNode },
|
payload: { node: focusNode },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parent !== newParent) {
|
if (rowElement !== newRowElement) {
|
||||||
focusNode?.scrollIntoView({
|
focusNode?.scrollIntoView({
|
||||||
behavior: "auto",
|
behavior: "auto",
|
||||||
block: "center",
|
block: "center",
|
||||||
@@ -315,7 +336,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
private onEnterFilter = (): void => {
|
private onEnterFilter = (): void => {
|
||||||
const btn = this.scrollRef.current?.containerRef.current?.querySelector<HTMLButtonElement>(
|
const btn = this.scrollRef.current?.containerRef.current?.querySelector<HTMLButtonElement>(
|
||||||
'.mx_EmojiPicker_item_wrapper[tabindex="0"]',
|
'.mx_EmojiPicker_item_wrapper [tabindex="0"]',
|
||||||
);
|
);
|
||||||
btn?.click();
|
btn?.click();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class QuickReactions extends React.Component<IProps, IState> {
|
|||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
onMouseLeave={this.onMouseLeave}
|
onMouseLeave={this.onMouseLeave}
|
||||||
selectedEmojis={this.props.selectedEmojis}
|
selectedEmojis={this.props.selectedEmojis}
|
||||||
|
className="mx_EmojiPicker_item_wrapper"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
Reference in New Issue
Block a user