/* Copyright 2024 New Vector Ltd. Copyright 2020, 2021 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React, { HTMLAttributes } from "react"; import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { IState, reducer, RovingTabIndexProvider, RovingTabIndexWrapper, Type, useRovingTabIndex, } from "../../src/accessibility/RovingTabIndex"; const Button = (props: HTMLAttributes) => { const [onFocus, isActive, ref] = useRovingTabIndex(); return ; const button2 = ; const button3 = ; const button4 = ; // mock offsetParent Object.defineProperty(HTMLElement.prototype, "offsetParent", { get() { return this.parentNode; }, }); describe("RovingTabIndex", () => { it("RovingTabIndexProvider renders children as expected", () => { const { container } = render( {() => (
Test
)}
, ); expect(container.textContent).toBe("Test"); expect(container.innerHTML).toBe("
Test
"); }); it("RovingTabIndexProvider works as expected with useRovingTabIndex", () => { const { container, rerender } = render( {() => ( {button1} {button2} {button3} )} , ); // should begin with 0th being active checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); // focus on 2nd button and test it is the only active one container.querySelectorAll("button")[2].focus(); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]); // focus on 1st button and test it is the only active one container.querySelectorAll("button")[1].focus(); checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]); // check that the active button does not change even on an explicit blur event container.querySelectorAll("button")[1].blur(); checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]); // update the children, it should remain on the same button rerender( {() => ( {button1} {button4} {button2} {button3} )} , ); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0, -1]); // update the children, remove the active button, it should move to the next one rerender( {() => ( {button1} {button4} {button3} )} , ); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]); }); it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => { const { container } = render( {() => ( {button1} {button2} {({ onFocus, isActive, ref }) => ( )} )} , ); // should begin with 0th being active checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); // focus on 2nd button and test it is the only active one container.querySelectorAll("button")[2].focus(); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]); }); describe("reducer functions as expected", () => { it("SetFocus works as expected", () => { const ref1 = React.createRef(); const ref2 = React.createRef(); expect( reducer( { activeRef: ref1, refs: [ref1, ref2], }, { type: Type.SetFocus, payload: { ref: ref2, }, }, ), ).toStrictEqual({ activeRef: ref2, refs: [ref1, ref2], }); }); it("Unregister works as expected", () => { const ref1 = React.createRef(); const ref2 = React.createRef(); const ref3 = React.createRef(); const ref4 = React.createRef(); let state: IState = { refs: [ref1, ref2, ref3, ref4], }; state = reducer(state, { type: Type.Unregister, payload: { ref: ref2, }, }); expect(state).toStrictEqual({ refs: [ref1, ref3, ref4], }); state = reducer(state, { type: Type.Unregister, payload: { ref: ref3, }, }); expect(state).toStrictEqual({ refs: [ref1, ref4], }); state = reducer(state, { type: Type.Unregister, payload: { ref: ref4, }, }); expect(state).toStrictEqual({ refs: [ref1], }); state = reducer(state, { type: Type.Unregister, payload: { ref: ref1, }, }); expect(state).toStrictEqual({ refs: [], }); }); it("Register works as expected", () => { const ref1 = React.createRef(); const ref2 = React.createRef(); const ref3 = React.createRef(); const ref4 = React.createRef(); render( , ); let state: IState = { refs: [], }; state = reducer(state, { type: Type.Register, payload: { ref: ref1, }, }); expect(state).toStrictEqual({ activeRef: ref1, refs: [ref1], }); state = reducer(state, { type: Type.Register, payload: { ref: ref2, }, }); expect(state).toStrictEqual({ activeRef: ref1, refs: [ref1, ref2], }); state = reducer(state, { type: Type.Register, payload: { ref: ref3, }, }); expect(state).toStrictEqual({ activeRef: ref1, refs: [ref1, ref2, ref3], }); state = reducer(state, { type: Type.Register, payload: { ref: ref4, }, }); expect(state).toStrictEqual({ activeRef: ref1, refs: [ref1, ref2, ref3, ref4], }); // test that the automatic focus switch works for unmounting state = reducer(state, { type: Type.SetFocus, payload: { ref: ref2, }, }); expect(state).toStrictEqual({ activeRef: ref2, refs: [ref1, ref2, ref3, ref4], }); state = reducer(state, { type: Type.Unregister, payload: { ref: ref2, }, }); expect(state).toStrictEqual({ activeRef: ref3, refs: [ref1, ref3, ref4], }); // test that the insert into the middle works as expected state = reducer(state, { type: Type.Register, payload: { ref: ref2, }, }); expect(state).toStrictEqual({ activeRef: ref3, refs: [ref1, ref2, ref3, ref4], }); // test that insertion at the edges works state = reducer(state, { type: Type.Unregister, payload: { ref: ref1, }, }); state = reducer(state, { type: Type.Unregister, payload: { ref: ref4, }, }); expect(state).toStrictEqual({ activeRef: ref3, refs: [ref2, ref3], }); state = reducer(state, { type: Type.Register, payload: { ref: ref1, }, }); state = reducer(state, { type: Type.Register, payload: { ref: ref4, }, }); expect(state).toStrictEqual({ activeRef: ref3, refs: [ref1, ref2, ref3, ref4], }); }); }); describe("handles arrow keys", () => { it("should handle up/down arrow keys work when handleUpDown=true", async () => { const { container } = render( {({ onKeyDownHandler }) => (
{button1} {button2} {button3}
)}
, ); container.querySelectorAll("button")[0].focus(); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); await userEvent.keyboard("[ArrowDown]"); checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]); await userEvent.keyboard("[ArrowDown]"); checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]); await userEvent.keyboard("[ArrowUp]"); checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]); await userEvent.keyboard("[ArrowUp]"); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); // Does not loop without await userEvent.keyboard("[ArrowUp]"); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); }); it("should call scrollIntoView if specified", async () => { const { container } = render( {({ onKeyDownHandler }) => (
{button1} {button2} {button3}
)}
, ); container.querySelectorAll("button")[0].focus(); checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]); const button = container.querySelectorAll("button")[1]; const mock = jest.spyOn(button, "scrollIntoView"); await userEvent.keyboard("[ArrowDown]"); expect(mock).toHaveBeenCalled(); }); }); });