Fix screen readers not indicating the emoji picker search field is focused. (#31128)

* Only set active descendant when the user starts typing.

* Fix jest tests.

* Remove aria-hidden

It was failing code quality checks and it actually wasn't addressing the issue.

* Only show highlight on arrow key navigation or updating the search query.

* Update screenshots

* Enter should not select an emoji if it is not highlighted.

* On clearing a query and using arrow kets again the highlighted emoji should be reset to the first.

* Update selector in picker tests
This commit is contained in:
David Langley
2025-10-31 17:10:02 +00:00
committed by GitHub
parent 017aee9a8f
commit 36ccc1ae9a
6 changed files with 148 additions and 8 deletions

View File

@@ -17,6 +17,10 @@ import SettingsStore from "../../../../../src/settings/SettingsStore";
describe("EmojiPicker", function () {
stubClient();
// Helper to get the currently active emoji's text content from the grid
const getActiveEmojiText = (container: HTMLElement): string =>
container.querySelector('.mx_EmojiPicker_body .mx_EmojiPicker_item_wrapper [tabindex="0"]')?.textContent || "";
beforeEach(() => {
// Clear recent emojis to prevent test pollution
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
@@ -77,11 +81,14 @@ describe("EmojiPicker", function () {
expect(input).toHaveFocus();
function getEmoji(): string {
const activeDescendant = input.getAttribute("aria-activedescendant");
return container.querySelector("#" + activeDescendant)!.textContent!;
return getActiveEmojiText(container);
}
expect(getEmoji()).toEqual("😀");
// First arrow key press shows highlight without navigating
await userEvent.keyboard("[ArrowDown]");
expect(getEmoji()).toEqual("😀");
// Subsequent arrow keys navigate
await userEvent.keyboard("[ArrowDown]");
expect(getEmoji()).toEqual("🙂");
await userEvent.keyboard("[ArrowUp]");
@@ -129,9 +136,7 @@ describe("EmojiPicker", function () {
}
function getVirtuallyFocusedEmoji(): string {
const activeDescendant = input.getAttribute("aria-activedescendant");
if (!activeDescendant) return "";
return container.querySelector("#" + activeDescendant)?.textContent || "";
return getActiveEmojiText(container);
}
// Initially, arrow keys use virtual focus (aria-activedescendant)
@@ -140,6 +145,13 @@ describe("EmojiPicker", function () {
expect(getVirtuallyFocusedEmoji()).toEqual("😀");
expect(getEmoji()).toEqual(""); // No actual emoji has focus
// First arrow key press shows highlight without navigating
await userEvent.keyboard("[ArrowDown]");
expect(input).toHaveFocus(); // Input still has focus
expect(getVirtuallyFocusedEmoji()).toEqual("😀"); // Virtual focus stayed on first emoji
expect(getEmoji()).toEqual(""); // No actual emoji has focus
// Second arrow key press navigates
await userEvent.keyboard("[ArrowDown]");
expect(input).toHaveFocus(); // Input still has focus
expect(getVirtuallyFocusedEmoji()).toEqual("🙂"); // Virtual focus moved
@@ -163,4 +175,98 @@ describe("EmojiPicker", function () {
expect(getEmoji()).toEqual("🙃"); // Actual focus moved right
expect(input).not.toHaveFocus();
});
it("should not select emoji on Enter press before highlight is shown", async () => {
// mock offsetParent
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
get() {
return this.parentNode;
},
});
const onChoose = jest.fn();
const onFinished = jest.fn();
const { container } = render(<EmojiPicker onChoose={onChoose} onFinished={onFinished} />);
const input = container.querySelector("input")!;
expect(input).toHaveFocus();
// Wait for emojis to render
await waitFor(() => {
expect(container.querySelector('[role="gridcell"]')).toBeInTheDocument();
});
// Press Enter immediately without interacting with arrow keys or search
await userEvent.keyboard("[Enter]");
// onChoose and onFinished should not be called
expect(onChoose).not.toHaveBeenCalled();
expect(onFinished).not.toHaveBeenCalled();
// Now press arrow key to show highlight
await userEvent.keyboard("[ArrowDown]");
// Press Enter again - now it should work
await userEvent.keyboard("[Enter]");
// onChoose and onFinished should be called
expect(onChoose).toHaveBeenCalledWith("😀");
expect(onFinished).toHaveBeenCalled();
});
it("should reset to first emoji when filter is cleared after navigation", async () => {
// mock offsetParent
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
get() {
return this.parentNode;
},
});
const onChoose = jest.fn();
const onFinished = jest.fn();
const { container } = render(<EmojiPicker onChoose={onChoose} onFinished={onFinished} />);
const input = container.querySelector("input")!;
expect(input).toHaveFocus();
function getEmoji(): string {
return getActiveEmojiText(container);
}
// Initially on first emoji
expect(getEmoji()).toEqual("😀");
// Show highlight with first arrow press
await userEvent.keyboard("[ArrowDown]");
expect(getEmoji()).toEqual("😀");
// Navigate to a different emoji
await userEvent.keyboard("[ArrowDown]");
expect(getEmoji()).toEqual("🙂");
await userEvent.keyboard("[ArrowDown]");
expect(getEmoji()).toEqual("🤩");
// Type a search query to filter emojis (this sets showHighlight=true)
await userEvent.type(input, "think");
await waitFor(() => {
// After filtering, we should be on the "thinking" emoji
expect(getEmoji()).toEqual("🤔");
});
// Clear the search filter
await userEvent.clear(input);
// After clearing, showHighlight is false, so the highlight is hidden
// The activeNode might still be on 🤔, but we can't see it
// Press arrow key - this should reset to first emoji AND show highlight
await userEvent.keyboard("[ArrowDown]");
await waitFor(() => {
expect(getEmoji()).toEqual("😀"); // Should now be on first emoji with highlight shown
});
// Next arrow key should navigate from first emoji
await userEvent.keyboard("[ArrowDown]");
expect(getEmoji()).toEqual("🙂");
});
});