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:
@@ -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("🙂");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user