diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx index 9ffe1dce14..75734a5d16 100644 --- a/src/components/views/emojipicker/Category.tsx +++ b/src/components/views/emojipicker/Category.tsx @@ -61,17 +61,17 @@ class Category extends React.PureComponent { return (
{emojisForRow.map((emoji) => ( - +
+ +
))}
); @@ -118,6 +118,7 @@ class Category extends React.PureComponent { overflowMargin={0} renderItem={this.renderEmojiRow} role="grid" + aria-multiselectable /> ); diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index 26d15d4dc6..8cc104f9a5 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -15,13 +15,17 @@ import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex"; interface IProps { 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; onClick(ev: ButtonEvent, emoji: IEmoji): void; onMouseEnter(emoji: IEmoji): void; onMouseLeave(emoji: IEmoji): void; disabled?: boolean; id?: string; - role?: string; + className?: string; } class Emoji extends React.PureComponent { @@ -34,9 +38,10 @@ class Emoji extends React.PureComponent { onClick={(ev: ButtonEvent) => onClick(ev, emoji)} onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} - className="mx_EmojiPicker_item_wrapper" - disabled={this.props.disabled} - role={this.props.role} + className={this.props.className} + disabled={this.props.disabled || undefined} + role={selectedEmojis ? "checkbox" : undefined} + aria-checked={this.props.disabled ? undefined : isSelected} focusOnMouseOver >
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index f8ab405f5d..5311e84c91 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -152,24 +152,42 @@ class EmojiPicker extends React.Component { 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): void { - const node = state.activeNode; - const parent = node?.parentElement; - if (!parent || !state.activeNode) return; - const rowIndex = Array.from(parent.children).indexOf(node); + const rowElement = this.getRow(state.activeNode); + const gridcellNode = this.getGridcell(state.activeNode); + if (!rowElement || !gridcellNode || !state.activeNode) return; + + // 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); let focusNode: HTMLElement | undefined; - let newParent: HTMLElement | undefined; + let newRowElement: Element | undefined; switch (ev.key) { case Key.ARROW_LEFT: focusNode = state.nodes[refIndex - 1]; - newParent = focusNode?.parentElement ?? undefined; + newRowElement = this.getRow(focusNode); break; case Key.ARROW_RIGHT: focusNode = state.nodes[refIndex + 1]; - newParent = focusNode?.parentElement ?? undefined; + newRowElement = this.getRow(focusNode); break; case Key.ARROW_UP: @@ -177,11 +195,14 @@ class EmojiPicker extends React.Component { // For up/down we find the prev/next parent by inspecting the refs either side of our row const node = ev.key === Key.ARROW_UP - ? state.nodes[refIndex - rowIndex - 1] - : state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW]; - newParent = node?.parentElement ?? undefined; - const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)]; - focusNode = state.nodes.find((r) => r === newTarget); + ? state.nodes[refIndex - columnIndex - 1] + : state.nodes[refIndex - columnIndex + EMOJIS_PER_ROW]; + newRowElement = this.getRow(node); + if (newRowElement) { + const newColumnIndex = clamp(columnIndex, 0, newRowElement.children.length - 1); + const newTarget = this.getRovingNode(newRowElement?.children[newColumnIndex]); + focusNode = state.nodes.find((r) => r === newTarget); + } break; } } @@ -197,7 +218,7 @@ class EmojiPicker extends React.Component { payload: { node: focusNode }, }); - if (parent !== newParent) { + if (rowElement !== newRowElement) { focusNode?.scrollIntoView({ behavior: "auto", block: "center", @@ -315,7 +336,7 @@ class EmojiPicker extends React.Component { private onEnterFilter = (): void => { const btn = this.scrollRef.current?.containerRef.current?.querySelector( - '.mx_EmojiPicker_item_wrapper[tabindex="0"]', + '.mx_EmojiPicker_item_wrapper [tabindex="0"]', ); btn?.click(); this.props.onFinished(); diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index a814a6b206..4b337cd5f5 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -73,6 +73,7 @@ class QuickReactions extends React.Component { onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} selectedEmojis={this.props.selectedEmojis} + className="mx_EmojiPicker_item_wrapper" /> ))}