diff --git a/.changeset/mean-cherries-press.md b/.changeset/mean-cherries-press.md new file mode 100644 index 0000000000..ecb7905e50 --- /dev/null +++ b/.changeset/mean-cherries-press.md @@ -0,0 +1,7 @@ +--- +"@khanacademy/wonder-blocks-clickable": major +"@khanacademy/wonder-blocks-dropdown": major +"@khanacademy/wonder-blocks-core": major +--- + +Fixes keyboard tests in Dropdown and Clickable with specific key events. We now check `event.key` instead of `event.which` or `event.keyCode` to remove deprecated event properties and match the keys returned from Testing Library/userEvent. diff --git a/__docs__/wonder-blocks-dropdown/single-select.accessibility.stories.tsx b/__docs__/wonder-blocks-dropdown/single-select.accessibility.stories.tsx index 08b81b89bc..2a0db8647a 100644 --- a/__docs__/wonder-blocks-dropdown/single-select.accessibility.stories.tsx +++ b/__docs__/wonder-blocks-dropdown/single-select.accessibility.stories.tsx @@ -57,3 +57,29 @@ export const UsingAriaAttributes = { render: SingleSelectAccessibility.bind({}), name: "Using aria attributes", }; + +// This story exists for debugging automated unit tests. +const SingleSelectKeyboardSelection = () => { + const [selectedValue, setSelectedValue] = React.useState(""); + return ( + + + + + + + + ); +}; + +export const UsingKeyboardSelection = { + render: SingleSelectKeyboardSelection.bind({}), + name: "Using the keyboard", + parameters: { + chromatic: {disableSnapshot: true}, + }, +}; diff --git a/packages/wonder-blocks-clickable/src/components/__tests__/clickable-behavior.test.tsx b/packages/wonder-blocks-clickable/src/components/__tests__/clickable-behavior.test.tsx index 774126f502..16e1fed0f8 100644 --- a/packages/wonder-blocks-clickable/src/components/__tests__/clickable-behavior.test.tsx +++ b/packages/wonder-blocks-clickable/src/components/__tests__/clickable-behavior.test.tsx @@ -4,17 +4,12 @@ import * as React from "react"; import {render, screen, fireEvent, waitFor} from "@testing-library/react"; import {MemoryRouter, Switch, Route} from "react-router-dom"; import {userEvent} from "@testing-library/user-event"; +import {keys} from "@khanacademy/wonder-blocks-core"; import getClickableBehavior from "../../util/get-clickable-behavior"; import ClickableBehavior from "../clickable-behavior"; import type {ClickableState} from "../clickable-behavior"; -const keyCodes = { - tab: 9, - enter: 13, - space: 32, -} as const; - const labelForState = (state: ClickableState): string => { const labels: Array = []; if (state.hovered) { @@ -195,8 +190,8 @@ describe("ClickableBehavior", () => { ); const button = await screen.findByRole("button"); expect(button).not.toHaveTextContent("focused"); - fireEvent.keyDown(button, {keyCode: keyCodes.space}); - fireEvent.keyUp(button, {keyCode: keyCodes.space}); + fireEvent.keyDown(button, {key: keys.space}); + fireEvent.keyUp(button, {key: keys.space}); // NOTE(kevinb): await userEvent.click() fires other events that we don't want // affecting this test case. fireEvent.click(button); @@ -219,8 +214,8 @@ describe("ClickableBehavior", () => { ); const button = await screen.findByRole("button"); expect(button).not.toHaveTextContent("focused"); - fireEvent.keyDown(button, {keyCode: keyCodes.space}); - fireEvent.keyUp(button, {keyCode: keyCodes.space}); + await userEvent.tab(); + await userEvent.keyboard(" "); // NOTE(kevinb): await userEvent.click() fires other events that we don't want // affecting this test case. fireEvent.click(button); @@ -244,14 +239,14 @@ describe("ClickableBehavior", () => { ); const button = await screen.findByRole("button"); expect(button).not.toHaveTextContent("pressed"); - fireEvent.keyDown(button, {keyCode: keyCodes.space}); + fireEvent.keyDown(button, {key: keys.space}); expect(button).toHaveTextContent("pressed"); - fireEvent.keyUp(button, {keyCode: keyCodes.space}); + fireEvent.keyUp(button, {key: keys.space}); expect(button).not.toHaveTextContent("pressed"); - fireEvent.keyDown(button, {keyCode: keyCodes.enter}); + fireEvent.keyDown(button, {key: keys.enter}); expect(button).toHaveTextContent("pressed"); - fireEvent.keyUp(button, {keyCode: keyCodes.enter}); + fireEvent.keyUp(button, {key: keys.enter}); expect(button).not.toHaveTextContent("pressed"); }); @@ -280,14 +275,14 @@ describe("ClickableBehavior", () => { ); const link = await screen.findByRole("link"); expect(link).not.toHaveTextContent("pressed"); - fireEvent.keyDown(link, {keyCode: keyCodes.enter}); + fireEvent.keyDown(link, {key: keys.enter}); expect(link).toHaveTextContent("pressed"); - fireEvent.keyUp(link, {keyCode: keyCodes.enter}); + fireEvent.keyUp(link, {key: keys.enter}); expect(link).not.toHaveTextContent("pressed"); - fireEvent.keyDown(link, {keyCode: keyCodes.space}); + fireEvent.keyDown(link, {key: keys.space}); expect(link).not.toHaveTextContent("pressed"); - fireEvent.keyUp(link, {keyCode: keyCodes.space}); + fireEvent.keyUp(link, {key: keys.space}); expect(link).not.toHaveTextContent("pressed"); }); @@ -462,19 +457,19 @@ describe("ClickableBehavior", () => { expect(button).not.toHaveTextContent("focused"); fireEvent.keyUp(button, { - keyCode: keyCodes.tab, + key: keys.tab, }); expect(button).not.toHaveTextContent("focused"); - fireEvent.keyDown(button, {keyCode: keyCodes.tab}); + fireEvent.keyDown(button, {key: keys.tab}); expect(button).not.toHaveTextContent("focused"); expect(button).not.toHaveTextContent("pressed"); - fireEvent.keyDown(button, {keyCode: keyCodes.space}); + fireEvent.keyDown(button, {key: keys.space}); expect(button).not.toHaveTextContent("pressed"); - fireEvent.keyUp(button, {keyCode: keyCodes.space}); + fireEvent.keyUp(button, {key: keys.space}); expect(button).not.toHaveTextContent("pressed"); - fireEvent.keyDown(button, {keyCode: keyCodes.space}); + fireEvent.keyDown(button, {key: keys.space}); fireEvent.blur(button); expect(button).not.toHaveTextContent("pressed"); @@ -501,9 +496,9 @@ describe("ClickableBehavior", () => { const anchor = await screen.findByRole("link"); expect(anchor).not.toHaveTextContent("pressed"); - fireEvent.keyDown(anchor, {keyCode: keyCodes.enter}); + fireEvent.keyDown(anchor, {key: keys.enter}); expect(anchor).not.toHaveTextContent("pressed"); - fireEvent.keyUp(anchor, {keyCode: keyCodes.enter}); + fireEvent.keyUp(anchor, {key: keys.enter}); expect(anchor).not.toHaveTextContent("pressed"); }); @@ -525,16 +520,16 @@ describe("ClickableBehavior", () => { await userEvent.click(button); expect(onClick).toHaveBeenCalledTimes(1); - fireEvent.keyDown(button, {keyCode: keyCodes.space}); - fireEvent.keyUp(button, {keyCode: keyCodes.space}); + fireEvent.keyDown(button, {key: keys.space}); + fireEvent.keyUp(button, {key: keys.space}); expect(onClick).toHaveBeenCalledTimes(2); - fireEvent.keyDown(button, {keyCode: keyCodes.enter}); - fireEvent.keyUp(button, {keyCode: keyCodes.enter}); + fireEvent.keyDown(button, {key: keys.enter}); + fireEvent.keyUp(button, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(3); - fireEvent.touchStart(button, {keyCode: keyCodes.space}); - fireEvent.touchEnd(button, {keyCode: keyCodes.space}); + fireEvent.touchStart(button, {key: keys.space}); + fireEvent.touchEnd(button, {key: keys.space}); fireEvent.click(button); expect(onClick).toHaveBeenCalledTimes(4); }); @@ -600,21 +595,21 @@ describe("ClickableBehavior", () => { const link = await screen.findByRole("link"); // Space press should not trigger the onClick - fireEvent.keyDown(link, {keyCode: keyCodes.space}); - fireEvent.keyUp(link, {keyCode: keyCodes.space}); + fireEvent.keyDown(link, {key: keys.space}); + fireEvent.keyUp(link, {key: keys.space}); expect(onClick).toHaveBeenCalledTimes(0); // Navigation didn't happen with space expect(window.location.assign).toHaveBeenCalledTimes(0); // Enter press should trigger the onClick after keyup - fireEvent.keyDown(link, {keyCode: keyCodes.enter}); + fireEvent.keyDown(link, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(0); // Navigation doesn't happen until after enter is released expect(window.location.assign).toHaveBeenCalledTimes(0); - fireEvent.keyUp(link, {keyCode: keyCodes.enter}); + fireEvent.keyUp(link, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(1); // Navigation happened after enter click @@ -730,14 +725,14 @@ describe("ClickableBehavior", () => { // Enter press should not do anything const checkbox = await screen.findByRole("checkbox"); - fireEvent.keyDown(checkbox, {keyCode: keyCodes.enter}); + fireEvent.keyDown(checkbox, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(0); - fireEvent.keyUp(checkbox, {keyCode: keyCodes.enter}); + fireEvent.keyUp(checkbox, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(0); // Space press should trigger the onClick - fireEvent.keyDown(checkbox, {keyCode: keyCodes.space}); - fireEvent.keyUp(checkbox, {keyCode: keyCodes.space}); + fireEvent.keyDown(checkbox, {key: keys.space}); + fireEvent.keyUp(checkbox, {key: keys.space}); expect(onClick).toHaveBeenCalledTimes(1); }); @@ -757,15 +752,15 @@ describe("ClickableBehavior", () => { // Enter press const button = await screen.findByRole("button"); - fireEvent.keyDown(button, {keyCode: keyCodes.enter}); + fireEvent.keyDown(button, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(0); - fireEvent.keyUp(button, {keyCode: keyCodes.enter}); + fireEvent.keyUp(button, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(1); // Space press - fireEvent.keyDown(button, {keyCode: keyCodes.space}); + fireEvent.keyDown(button, {key: keys.space}); expect(onClick).toHaveBeenCalledTimes(1); - fireEvent.keyUp(button, {keyCode: keyCodes.space}); + fireEvent.keyUp(button, {key: keys.space}); expect(onClick).toHaveBeenCalledTimes(2); }); @@ -794,10 +789,10 @@ describe("ClickableBehavior", () => { } // Enter press on a div - fireEvent.keyDown(clickableDiv, {keyCode: keyCodes.enter}); + fireEvent.keyDown(clickableDiv, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled); fireEvent.keyUp(clickableDiv, { - keyCode: keyCodes.enter, + key: keys.enter, }); expectedNumberTimesCalled += 1; expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled); @@ -808,9 +803,9 @@ describe("ClickableBehavior", () => { expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled); // Space press on a div - fireEvent.keyDown(clickableDiv, {keyCode: keyCodes.space}); + fireEvent.keyDown(clickableDiv, {key: keys.space}); expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled); - fireEvent.keyUp(clickableDiv, {keyCode: keyCodes.space}); + fireEvent.keyUp(clickableDiv, {key: keys.space}); expectedNumberTimesCalled += 1; expect(onClick).toHaveBeenCalledTimes(expectedNumberTimesCalled); @@ -885,10 +880,10 @@ describe("ClickableBehavior", () => { const checkbox = await screen.findByRole("checkbox"); // Enter press should not do anything - fireEvent.keyDown(checkbox, {keyCode: keyCodes.enter}); + fireEvent.keyDown(checkbox, {key: keys.enter}); // This element still wants to have a click on enter press fireEvent.click(checkbox); - fireEvent.keyUp(checkbox, {keyCode: keyCodes.enter}); + fireEvent.keyUp(checkbox, {key: keys.enter}); expect(onClick).toHaveBeenCalledTimes(0); }); diff --git a/packages/wonder-blocks-clickable/src/components/clickable-behavior.ts b/packages/wonder-blocks-clickable/src/components/clickable-behavior.ts index da76b31338..aea9e7d5e3 100644 --- a/packages/wonder-blocks-clickable/src/components/clickable-behavior.ts +++ b/packages/wonder-blocks-clickable/src/components/clickable-behavior.ts @@ -1,4 +1,5 @@ import * as React from "react"; +import {keys} from "@khanacademy/wonder-blocks-core"; // NOTE: Potentially add to this as more cases come up. export type ClickableRole = @@ -229,11 +230,6 @@ const disabledHandlers = { onKeyUp: () => void 0, } as const; -const keyCodes = { - enter: 13, - space: 32, -} as const; - const startState: ClickableState = { hovered: false, focused: false, @@ -560,13 +556,12 @@ export default class ClickableBehavior extends React.Component< if (onKeyDown) { onKeyDown(e); } - - const keyCode = e.which || e.keyCode; + const keyName = e.key; const {triggerOnEnter, triggerOnSpace} = getAppropriateTriggersForRole(role); if ( - (triggerOnEnter && keyCode === keyCodes.enter) || - (triggerOnSpace && keyCode === keyCodes.space) + (triggerOnEnter && keyName === keys.enter) || + (triggerOnSpace && keyName === keys.space) ) { // This prevents space from scrolling down. It also prevents the // space and enter keys from triggering click events. We manually @@ -574,7 +569,7 @@ export default class ClickableBehavior extends React.Component< // handleKeyUp instead. e.preventDefault(); this.setState({pressed: true}); - } else if (!triggerOnEnter && keyCode === keyCodes.enter) { + } else if (!triggerOnEnter && keyName === keys.enter) { // If the component isn't supposed to trigger on enter, we have to // keep track of the enter keydown to negate the onClick callback this.enterClick = true; @@ -587,17 +582,17 @@ export default class ClickableBehavior extends React.Component< onKeyUp(e); } - const keyCode = e.which || e.keyCode; + const keyName = e.key; const {triggerOnEnter, triggerOnSpace} = getAppropriateTriggersForRole(role); if ( - (triggerOnEnter && keyCode === keyCodes.enter) || - (triggerOnSpace && keyCode === keyCodes.space) + (triggerOnEnter && keyName === keys.enter) || + (triggerOnSpace && keyName === keys.space) ) { this.setState({pressed: false, focused: true}); this.runCallbackAndMaybeNavigate(e); - } else if (!triggerOnEnter && keyCode === keyCodes.enter) { + } else if (!triggerOnEnter && keyName === keys.enter) { this.enterClick = false; } }; diff --git a/packages/wonder-blocks-core/src/index.ts b/packages/wonder-blocks-core/src/index.ts index be714657cb..758a117a7c 100644 --- a/packages/wonder-blocks-core/src/index.ts +++ b/packages/wonder-blocks-core/src/index.ts @@ -15,3 +15,4 @@ export {RenderStateRoot} from "./components/render-state-root"; export {RenderState} from "./components/render-state-context"; export type {AriaRole, AriaAttributes} from "./util/aria-types"; export type {AriaProps, StyleType, PropsFor} from "./util/types"; +export {keys} from "./util/keys"; diff --git a/packages/wonder-blocks-core/src/util/keys.ts b/packages/wonder-blocks-core/src/util/keys.ts new file mode 100644 index 0000000000..d953433cfe --- /dev/null +++ b/packages/wonder-blocks-core/src/util/keys.ts @@ -0,0 +1,12 @@ +/** + * Key value mapping reference: + * https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values + */ +export const keys = { + enter: "Enter", + escape: "Escape", + tab: "Tab", + space: " ", + up: "ArrowUp", + down: "ArrowDown", +} as const; diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/action-menu.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/action-menu.test.tsx index 61388ddaa0..cdbc18bd88 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/action-menu.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/action-menu.test.tsx @@ -38,9 +38,7 @@ describe("ActionMenu", () => { ).toBeInTheDocument(); }); - // TODO(FEI-5533): Key press events aren't working correctly with - // user-event v14. We need to investigate and fix this. - it.skip("opens the menu on enter", async () => { + it("opens the menu on enter", async () => { // Arrange render( { // Act await userEvent.tab(); - await userEvent.keyboard("{enter}"); + await userEvent.keyboard("{Enter}"); // Assert expect( @@ -65,9 +63,7 @@ describe("ActionMenu", () => { ).toBeInTheDocument(); }); - // TODO(FEI-5533): Key press events aren't working correctly with - // user-event v14. We need to investigate and fix this. - it.skip("closes itself on escape", async () => { + it("closes itself on escape", async () => { // Arrange render( { ); await userEvent.tab(); - await userEvent.keyboard("{enter}"); + await userEvent.keyboard("{Enter}"); // Act - await userEvent.keyboard("{escape}"); + await userEvent.keyboard("{Escape}"); // Assert expect(screen.queryByRole("menu")).not.toBeInTheDocument(); }); - // TODO(FEI-5533): Key press events aren't working correctly with - // user-event v14. We need to investigate and fix this. - it.skip("closes itself on tab", async () => { + it("closes itself on tab", async () => { // Arrange render( { ); await userEvent.tab(); - await userEvent.keyboard("{enter}"); + await userEvent.keyboard("{Enter}"); // Act await userEvent.tab(); diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/combobox.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/combobox.test.tsx index cb33014611..2c8e207f95 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/combobox.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/combobox.test.tsx @@ -280,7 +280,7 @@ describe("Combobox", () => { await userEvent.type(screen.getByRole("combobox"), "3"); // Act - await userEvent.keyboard("{enter}"); + await userEvent.keyboard("{Enter}"); // Assert expect(screen.getByRole("combobox")).toHaveValue("option 3"); diff --git a/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx b/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx index 3f247da6fb..e54972b673 100644 --- a/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx +++ b/packages/wonder-blocks-dropdown/src/components/__tests__/dropdown-core.test.tsx @@ -73,9 +73,7 @@ describe("DropdownCore", () => { expect(item).toHaveFocus(); }); - // TODO(FEI-5533): Key press events aren't working correctly with - // user-event v14. We need to investigate and fix this. - it.skip("handles basic keyboard navigation as expected", async () => { + it("handles basic keyboard navigation as expected", async () => { // Arrange const dummyOpener =