Skip to content

Commit d558fa7

Browse files
authored
Emoji Picker: Focused emoji does not move with the arrow keys (#30893)
* We should focus the node in the DOM so that the browser focus(with outline) matches the our internal RovingIndex state * Don't move focus from the input if we are in "virtual" focus(via active descendant)
1 parent 2ab42df commit d558fa7

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ class EmojiPicker extends React.Component<IProps, IState> {
187187
}
188188

189189
if (focusNode) {
190+
// Only move actual DOM focus if an emoji already has focus
191+
// If the input has focus, keep using aria-activedescendant for virtual focus
192+
if (document.activeElement !== document.querySelector(".mx_EmojiPicker_search input")) {
193+
focusNode?.focus();
194+
}
190195
dispatch({
191196
type: Type.SetFocus,
192197
payload: { node: focusNode },

test/unit-tests/components/views/emojipicker/EmojiPicker-test.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,23 @@ import userEvent from "@testing-library/user-event";
1212

1313
import EmojiPicker from "../../../../../src/components/views/emojipicker/EmojiPicker";
1414
import { stubClient } from "../../../../test-utils";
15+
import SettingsStore from "../../../../../src/settings/SettingsStore";
1516

1617
describe("EmojiPicker", function () {
1718
stubClient();
1819

20+
beforeEach(() => {
21+
// Clear recent emojis to prevent test pollution
22+
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
23+
if (settingName === "recent_emoji") return [] as any;
24+
return jest.requireActual("../../../../../src/settings/SettingsStore").default.getValue(settingName);
25+
});
26+
});
27+
28+
afterEach(() => {
29+
jest.restoreAllMocks();
30+
});
31+
1932
it("should not mangle default order after filtering", async () => {
2033
const ref = createRef<EmojiPicker>();
2134
const { container } = render(
@@ -90,4 +103,64 @@ describe("EmojiPicker", function () {
90103
expect(onChoose).toHaveBeenCalledWith("📫️");
91104
expect(onFinished).toHaveBeenCalled();
92105
});
106+
107+
it("should move actual focus when navigating between emojis after Tab", async () => {
108+
// mock offsetParent
109+
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
110+
get() {
111+
return this.parentNode;
112+
},
113+
});
114+
115+
const onChoose = jest.fn();
116+
const onFinished = jest.fn();
117+
const { container } = render(<EmojiPicker onChoose={onChoose} onFinished={onFinished} />);
118+
119+
const input = container.querySelector("input")!;
120+
expect(input).toHaveFocus();
121+
122+
// Wait for emojis to render
123+
await waitFor(() => {
124+
expect(container.querySelector('[role="gridcell"]')).toBeInTheDocument();
125+
});
126+
127+
function getEmoji(): string {
128+
return document.activeElement?.textContent || "";
129+
}
130+
131+
function getVirtuallyFocusedEmoji(): string {
132+
const activeDescendant = input.getAttribute("aria-activedescendant");
133+
if (!activeDescendant) return "";
134+
return container.querySelector("#" + activeDescendant)?.textContent || "";
135+
}
136+
137+
// Initially, arrow keys use virtual focus (aria-activedescendant)
138+
// The first emoji is virtually focused by default
139+
expect(input).toHaveFocus();
140+
expect(getVirtuallyFocusedEmoji()).toEqual("😀");
141+
expect(getEmoji()).toEqual(""); // No actual emoji has focus
142+
143+
await userEvent.keyboard("[ArrowDown]");
144+
expect(input).toHaveFocus(); // Input still has focus
145+
expect(getVirtuallyFocusedEmoji()).toEqual("🙂"); // Virtual focus moved
146+
expect(getEmoji()).toEqual(""); // No actual emoji has focus
147+
148+
// Tab to move actual focus to the emoji
149+
await userEvent.keyboard("[Tab]");
150+
expect(input).not.toHaveFocus();
151+
expect(getEmoji()).toEqual("🙂"); // Now emoji has actual focus
152+
153+
// Arrow keys now move actual DOM focus between emojis
154+
await userEvent.keyboard("[ArrowDown]");
155+
expect(getEmoji()).toEqual("🤩"); // Actual focus moved down one row
156+
expect(input).not.toHaveFocus();
157+
158+
await userEvent.keyboard("[ArrowUp]");
159+
expect(getEmoji()).toEqual("🙂"); // Actual focus moved back up
160+
expect(input).not.toHaveFocus();
161+
162+
await userEvent.keyboard("[ArrowRight]");
163+
expect(getEmoji()).toEqual("🙃"); // Actual focus moved right
164+
expect(input).not.toHaveFocus();
165+
});
93166
});

0 commit comments

Comments
 (0)