From d558fa79e0073d1f68f1e3ce5ed4921367cb6f0e Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 29 Oct 2025 16:16:03 +0000 Subject: [PATCH] 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) --- .../views/emojipicker/EmojiPicker.tsx | 5 ++ .../views/emojipicker/EmojiPicker-test.tsx | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index c225bfcfae..f8ab405f5d 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -187,6 +187,11 @@ class EmojiPicker extends React.Component { } if (focusNode) { + // Only move actual DOM focus if an emoji already has focus + // If the input has focus, keep using aria-activedescendant for virtual focus + if (document.activeElement !== document.querySelector(".mx_EmojiPicker_search input")) { + focusNode?.focus(); + } dispatch({ type: Type.SetFocus, payload: { node: focusNode }, diff --git a/test/unit-tests/components/views/emojipicker/EmojiPicker-test.tsx b/test/unit-tests/components/views/emojipicker/EmojiPicker-test.tsx index a7dcadc9f7..ad56b76adc 100644 --- a/test/unit-tests/components/views/emojipicker/EmojiPicker-test.tsx +++ b/test/unit-tests/components/views/emojipicker/EmojiPicker-test.tsx @@ -12,10 +12,23 @@ import userEvent from "@testing-library/user-event"; import EmojiPicker from "../../../../../src/components/views/emojipicker/EmojiPicker"; import { stubClient } from "../../../../test-utils"; +import SettingsStore from "../../../../../src/settings/SettingsStore"; describe("EmojiPicker", function () { stubClient(); + beforeEach(() => { + // Clear recent emojis to prevent test pollution + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => { + if (settingName === "recent_emoji") return [] as any; + return jest.requireActual("../../../../../src/settings/SettingsStore").default.getValue(settingName); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should not mangle default order after filtering", async () => { const ref = createRef(); const { container } = render( @@ -90,4 +103,64 @@ describe("EmojiPicker", function () { expect(onChoose).toHaveBeenCalledWith("📫️"); expect(onFinished).toHaveBeenCalled(); }); + + it("should move actual focus when navigating between emojis after Tab", async () => { + // mock offsetParent + Object.defineProperty(HTMLElement.prototype, "offsetParent", { + get() { + return this.parentNode; + }, + }); + + const onChoose = jest.fn(); + const onFinished = jest.fn(); + const { container } = render(); + + const input = container.querySelector("input")!; + expect(input).toHaveFocus(); + + // Wait for emojis to render + await waitFor(() => { + expect(container.querySelector('[role="gridcell"]')).toBeInTheDocument(); + }); + + function getEmoji(): string { + return document.activeElement?.textContent || ""; + } + + function getVirtuallyFocusedEmoji(): string { + const activeDescendant = input.getAttribute("aria-activedescendant"); + if (!activeDescendant) return ""; + return container.querySelector("#" + activeDescendant)?.textContent || ""; + } + + // Initially, arrow keys use virtual focus (aria-activedescendant) + // The first emoji is virtually focused by default + expect(input).toHaveFocus(); + expect(getVirtuallyFocusedEmoji()).toEqual("😀"); + expect(getEmoji()).toEqual(""); // No actual emoji has focus + + await userEvent.keyboard("[ArrowDown]"); + expect(input).toHaveFocus(); // Input still has focus + expect(getVirtuallyFocusedEmoji()).toEqual("🙂"); // Virtual focus moved + expect(getEmoji()).toEqual(""); // No actual emoji has focus + + // Tab to move actual focus to the emoji + await userEvent.keyboard("[Tab]"); + expect(input).not.toHaveFocus(); + expect(getEmoji()).toEqual("🙂"); // Now emoji has actual focus + + // Arrow keys now move actual DOM focus between emojis + await userEvent.keyboard("[ArrowDown]"); + expect(getEmoji()).toEqual("🤩"); // Actual focus moved down one row + expect(input).not.toHaveFocus(); + + await userEvent.keyboard("[ArrowUp]"); + expect(getEmoji()).toEqual("🙂"); // Actual focus moved back up + expect(input).not.toHaveFocus(); + + await userEvent.keyboard("[ArrowRight]"); + expect(getEmoji()).toEqual("🙃"); // Actual focus moved right + expect(input).not.toHaveFocus(); + }); });