import * as React from "react"; import { render, focus, press, click, type } from "reakit-test-utils"; import { useCompositeState, Composite, CompositeGroup, CompositeItem, unstable_CompositeItemWidget as CompositeItemWidget, } from ".."; const emojiMap = { "^": ["ArrowUp"], ">": ["ArrowRight"], v: ["ArrowDown"], "<": ["ArrowLeft"], "^^": ["PageUp"], vv: ["PageDown"], "<<": ["Home"], ">>": ["End"], "<<<": ["Home", { ctrlKey: true }], ">>>": ["End", { ctrlKey: true }], } as const; function active() { const { activeElement } = document; const activeDescendant = activeElement?.getAttribute("aria-activedescendant"); if (activeDescendant) { return document.getElementById(activeDescendant); } return activeElement?.hasAttribute("data-item") ? activeElement : undefined; } function key(char: keyof typeof emojiMap) { const [k, options] = emojiMap[char]; press[k](null, options); return active(); } function template(value: string) { const items = Array.from(document.querySelectorAll("[data-item]")); const withoutSpaces = value.replace(/\s/gm, ""); return items[withoutSpaces.indexOf("0")]; } [true, false].forEach((virtual) => { describe(virtual ? "aria-activedescendant" : "roving-tabindex", () => { test("warning when there's no label", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( item1 item2 item3 ); }; render(); expect(console).toHaveWarned(); }); test("first list item is active", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( item1 item2 item3 ); }; const { getByText } = render(); const item1 = getByText("item1"); expect(item1).not.toHaveFocus(); press.Tab(); expect(item1).toHaveFocus(); }); test("list item is active when currentId is set", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, currentId: "item2", }); return ( item1 item2 item3 ); }; const { getByText } = render(); const item2 = getByText("item2"); expect(item2).not.toHaveFocus(); press.Tab(); expect(item2).toHaveFocus(); }); test("composite becomes the first item when currentId is null", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, currentId: null, }); return ( item1 item2 item3 ); }; const { getByText, getByLabelText } = render(); const composite = getByLabelText("composite"); const item1 = getByText("item1"); const item2 = getByText("item2"); const item3 = getByText("item3"); expect(composite).not.toHaveFocus(); press.Tab(); expect(composite).toHaveFocus(); expect(item1).not.toHaveFocus(); press.ArrowDown(); expect(item1).toHaveFocus(); press.ArrowRight(); expect(item2).toHaveFocus(); press.ArrowDown(); expect(item3).toHaveFocus(); press.ArrowDown(); expect(item3).toHaveFocus(); press.ArrowUp(); expect(item2).toHaveFocus(); press.ArrowLeft(); expect(item1).toHaveFocus(); press.ArrowUp(); expect(item1).not.toHaveFocus(); expect(composite).toHaveFocus(); press.ArrowUp(); expect(item3).toHaveFocus(); press.Home(); press.ArrowUp(); expect(composite).toHaveFocus(); press.PageDown(); expect(item3).toHaveFocus(); press.PageUp(); press.ArrowLeft(); expect(composite).toHaveFocus(); press.Home(); expect(item1).toHaveFocus(); press.ArrowUp(); expect(composite).toHaveFocus(); press.End(); expect(item3).toHaveFocus(); }); test("composite becomes the first item when currentId is null and loop is true", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, currentId: null, loop: true, }); return ( item1 item2 item3 ); }; const { getByText, getByLabelText } = render(); const composite = getByLabelText("composite"); const item1 = getByText("item1"); const item2 = getByText("item2"); const item3 = getByText("item3"); expect(composite).not.toHaveFocus(); press.Tab(); expect(composite).toHaveFocus(); expect(item1).not.toHaveFocus(); press.ArrowRight(); expect(item1).toHaveFocus(); press.ArrowDown(); expect(item2).toHaveFocus(); press.ArrowRight(); expect(item3).toHaveFocus(); press.ArrowRight(); expect(item1).not.toHaveFocus(); expect(item3).not.toHaveFocus(); expect(composite).toHaveFocus(); press.ArrowDown(); expect(item1).toHaveFocus(); press.ArrowLeft(); expect(item1).not.toHaveFocus(); expect(item3).not.toHaveFocus(); expect(composite).toHaveFocus(); press.PageDown(); expect(item3).toHaveFocus(); press.ArrowDown(); expect(composite).toHaveFocus(); press.Home(); expect(item1).toHaveFocus(); press.ArrowUp(); expect(composite).toHaveFocus(); press.End(); expect(item3).toHaveFocus(); }); test("click item", () => { const onClick = jest.fn(); const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( item1 item2 item3 ); }; const { getByText } = render(); const item2 = getByText("item2"); expect(item2).not.toHaveFocus(); expect(onClick).toHaveBeenCalledTimes(0); click(item2); expect(item2).toHaveFocus(); expect(onClick).toHaveBeenCalledTimes(1); press.Enter(); expect(onClick).toHaveBeenCalledTimes(2); press.Space(); expect(onClick).toHaveBeenCalledTimes(3); }); test("composite is a single tab stop", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( <> item1 item2 item3 ); }; const { getByText } = render(); const button1 = getByText("button1"); const item1 = getByText("item1"); const button2 = getByText("button2"); focus(button1); press.Tab(); expect(item1).toHaveFocus(); press.Tab(); expect(button2).toHaveFocus(); press.ShiftTab(); expect(item1).toHaveFocus(); }); test("remember the last focused item", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( <> item1 item2 item3 ); }; const { getByText } = render(); const button = getByText("button"); const item2 = getByText("item2"); focus(item2); press.Tab(); expect(button).toHaveFocus(); press.ShiftTab(); expect(item2).toHaveFocus(); }); test("move focus with arrow keys", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( ); }; render(); press.Tab(); expect(active()).toBe(template("0x--")); expect(key(">")).toBe(template("-x0-")); expect(key("v")).toBe(template("-x-0")); expect(key("<")).toBe(template("-x0-")); expect(key("^")).toBe(template("0x--")); expect(key("<")).toBe(template("0x--")); expect(key(">>")).toBe(template("-x-0")); expect(key("<<")).toBe(template("0x--")); expect(key("vv")).toBe(template("-x-0")); expect(key("^^")).toBe(template("0x--")); }); test("move focus with arrow keys rtl", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, rtl: true, }); return ( ); }; render(); press.Tab(); expect(active()).toBe(template("0x--")); expect(key("<")).toBe(template("-x0-")); expect(key("v")).toBe(template("-x-0")); expect(key(">")).toBe(template("-x0-")); expect(key("^")).toBe(template("0x--")); expect(key(">")).toBe(template("0x--")); expect(key(">>")).toBe(template("-x-0")); expect(key("<<")).toBe(template("0x--")); expect(key("vv")).toBe(template("-x-0")); expect(key("^^")).toBe(template("0x--")); }); test("move focus with arrow keys loop", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, loop: true, }); return ( ); }; render(); press.Tab(); expect(active()).toBe(template("0--x")); expect(key(">")).toBe(template("-0-x")); expect(key("v")).toBe(template("--0x")); expect(key("<")).toBe(template("-0-x")); expect(key("^")).toBe(template("0--x")); expect(key("<")).toBe(template("--0x")); expect(key(">")).toBe(template("0--x")); expect(key(">>")).toBe(template("--0x")); expect(key("<<")).toBe(template("0--x")); expect(key("vv")).toBe(template("--0x")); expect(key("^^")).toBe(template("0--x")); }); test("move focus with arrow keys horizontal", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, orientation: "horizontal", }); return ( ); }; render(); press.Tab(); expect(active()).toBe(template("0--")); expect(key(">")).toBe(template("-0-")); expect(key("v")).toBe(template("-0-")); expect(key(">")).toBe(template("--0")); expect(key(">")).toBe(template("--0")); expect(key("<")).toBe(template("-0-")); expect(key("^")).toBe(template("-0-")); expect(key("<")).toBe(template("0--")); expect(key("<")).toBe(template("0--")); expect(key(">>")).toBe(template("--0")); expect(key("<<")).toBe(template("0--")); expect(key("vv")).toBe(template("--0")); expect(key("^^")).toBe(template("0--")); }); test("move focus with arrow keys vertical", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, orientation: "vertical", }); return ( ); }; render(); press.Tab(); expect(active()).toBe(template("0--")); expect(key("v")).toBe(template("-0-")); expect(key(">")).toBe(template("-0-")); expect(key("v")).toBe(template("--0")); expect(key("v")).toBe(template("--0")); expect(key("^")).toBe(template("-0-")); expect(key("<")).toBe(template("-0-")); expect(key("^")).toBe(template("0--")); expect(key("^")).toBe(template("0--")); expect(key(">>")).toBe(template("--0")); expect(key("<<")).toBe(template("0--")); expect(key("vv")).toBe(template("--0")); expect(key("^^")).toBe(template("0--")); }); test("block intermediate focus/blur events", () => { const stack: string[] = []; const returnTarget = (event: React.SyntheticEvent) => { const target = event.target as HTMLElement; const currentTarget = event.currentTarget as HTMLElement; stack.push( `${event.type} ${currentTarget.getAttribute( "aria-label" )} ${target.getAttribute("aria-label")}` ); }; const onCompositeFocus = jest.fn(returnTarget); const onCompositeBlur = jest.fn(returnTarget); const onItemFocus = jest.fn(returnTarget); const onItemBlur = jest.fn(returnTarget); const Test = () => { const composite = useCompositeState({ currentId: null, unstable_virtual: virtual, }); return ( ); }; const { getByLabelText: $, baseElement } = render(); press.Tab(); expect(stack.splice(0)).toEqual(["focus composite composite"]); press.ArrowDown(); if (virtual) { expect(stack.splice(0)).toEqual([ "focus item1 item1", "focus composite item1", ]); } else { expect(stack.splice(0)).toEqual([ "blur composite composite", "focus item1 item1", "focus composite item1", ]); } press.ArrowDown(); expect(stack.splice(0)).toEqual([ "blur item1 item1", "blur composite item1", "focus item2 item2", "focus composite item2", ]); click($("item3")); expect(stack.splice(0)).toEqual([ "blur item2 item2", "blur composite item2", "focus item3 item3", "focus composite item3", ]); click(baseElement); if (virtual) { expect(stack.splice(0)).toEqual([ "blur item3 item3", "blur composite item3", "blur composite composite", ]); } else { expect(stack.splice(0)).toEqual([ "blur item3 item3", "blur composite item3", ]); } press.Tab(); if (virtual) { expect(stack.splice(0)).toEqual([ "focus composite composite", "focus item3 item3", "focus composite item3", ]); } else { expect(stack.splice(0)).toEqual([ "focus item3 item3", "focus composite item3", ]); } }); test("keep DOM order", () => { const Test = ({ renderItem2 = false }) => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( item1 {renderItem2 && item2} item3 ); }; const { getByText, rerender } = render(); const item1 = getByText("item1"); const item3 = getByText("item3"); focus(item1); expect(item1).toHaveFocus(); press.ArrowRight(); expect(item3).toHaveFocus(); rerender(); expect(item3).toHaveFocus(); press.ArrowLeft(); expect(getByText("item2")).toHaveFocus(); }); ["disabled", "unmounted"].forEach((state) => { test(`move to the past item when the current active item is ${state}`, () => { const Test = () => { const [disabled, setDisabled] = React.useState(false); const composite = useCompositeState({ unstable_virtual: virtual }); return ( <> item1 {(!disabled || state !== "unmounted") && ( item2 )} item3 item4 ); }; const { getByText: $ } = render(); focus($("item2")); expect($("item2")).toHaveFocus(); click($("toggle")); press.Tab(); expect($("item1")).toHaveFocus(); click($("toggle")); press.Tab(); press.ArrowRight(); expect($("item2")).toHaveFocus(); press.ArrowRight(); expect($("item4")).toHaveFocus(); press.ArrowLeft(); expect($("item2")).toHaveFocus(); click($("toggle")); press.Tab(); expect($("item4")).toHaveFocus(); }); test(`move to the past item when the current active item is ${state} and currentId is set`, () => { const Test = () => { const [disabled, setDisabled] = React.useState(false); const composite = useCompositeState({ unstable_virtual: virtual, currentId: "item2", }); return ( <> item1 {(!disabled || state !== "unmounted") && ( item2 )} item3 item4 ); }; const { getByText: $ } = render(); expect($("item2")).not.toHaveFocus(); click($("toggle")); expect($("item1")).not.toHaveFocus(); press.Tab(); expect($("item1")).toHaveFocus(); click($("toggle")); press.Tab(); press.ArrowRight(); expect($("item2")).toHaveFocus(); press.ArrowRight(); expect($("item4")).toHaveFocus(); press.ArrowLeft(); expect($("item2")).toHaveFocus(); click($("toggle")); press.Tab(); expect($("item4")).toHaveFocus(); }); test(`move to the past item when the current active item is ${state} and id is set`, () => { const Test = () => { const [disabled, setDisabled] = React.useState(false); const composite = useCompositeState({ unstable_virtual: virtual }); return ( <> item1 {(!disabled || state !== "unmounted") && ( item2 )} item3 item4 ); }; const { getByText: $ } = render(); focus($("item2")); expect($("item2")).toHaveFocus(); click($("toggle")); press.Tab(); expect($("item1")).toHaveFocus(); click($("toggle")); press.Tab(); press.ArrowRight(); expect($("item2")).toHaveFocus(); press.ArrowRight(); expect($("item4")).toHaveFocus(); press.ArrowLeft(); expect($("item2")).toHaveFocus(); click($("toggle")); press.Tab(); expect($("item4")).toHaveFocus(); }); }); test("list item with tabbable content inside", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); return ( <> item1 innerButton ); }; const { getByLabelText, getByText } = render(); const item2 = getByLabelText("item2"); const item3 = getByLabelText("item3"); const input = getByLabelText("input"); const innerButton = getByText("innerButton"); const outerButton = getByText("outerButton"); click(item2); expect(item2).not.toHaveFocus(); expect(input).toHaveFocus(); press.Escape(); expect(item2).toHaveFocus(); press.Tab(); expect(outerButton).toHaveFocus(); press.ShiftTab(); expect(item2).toHaveFocus(); press.Enter(); expect(input).toHaveFocus(); press.ArrowDown(); press.ArrowRight(); expect(input).toHaveFocus(); press.Tab(); expect(innerButton).toHaveFocus(); press.Tab(); expect(outerButton).toHaveFocus(); press.ShiftTab(); expect(item3).toHaveFocus(); press("a"); expect(item3).toHaveFocus(); press.Enter(); expect(innerButton).toHaveFocus(); press.Escape(); expect(item3).toHaveFocus(); press.Space(); expect(innerButton).toHaveFocus(); press.ShiftTab(); expect(input).toHaveFocus(); press.Escape(); expect(input).not.toHaveFocus(); press.Space(); expect(input).toHaveFocus(); expect(input).toHaveValue(""); press.Escape(); expect(input).not.toHaveFocus(); type("a"); expect(input).toHaveFocus(); expect(input).toHaveValue("a"); type("bc d"); expect(input).toHaveValue("abc d"); press.Escape(); expect(input).not.toHaveFocus(); expect(input).toHaveValue(""); type("b"); expect(input).toHaveFocus(); expect(input).toHaveValue("b"); type("c"); expect(input).toHaveValue("bc"); press.Enter(); expect(item2).toHaveFocus(); expect(input).toHaveValue("bc"); type("b"); expect(input).toHaveFocus(); expect(input).toHaveValue("b"); press.Escape(); expect(item2).toHaveFocus(); expect(input).toHaveValue("bc"); press.Backspace(); expect(item2).toHaveFocus(); expect(input).toHaveValue(""); type("#"); expect(input).toHaveFocus(); expect(input).toHaveValue("#"); press.Escape(); press.Delete(); expect(item2).toHaveFocus(); expect(input).toHaveValue(""); }); test("move grid focus with arrow keys", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 2, 1, 2], [2, 0, 0, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 - - - - x x - - - - x `) ); expect(key(">")).toBe( template(` - 0 - - - x x - - - - x `) ); expect(key(">")).toBe( template(` - - 0 - - x x - - - - x `) ); expect(key(">")).toBe( template(` - - - 0 - x x - - - - x `) ); expect(key(">")).toBe( template(` - - - 0 - x x - - - - x `) ); expect(key("v")).toBe( template(` - - - - - x x 0 - - - x `) ); expect(key(">")).toBe( template(` - - - - - x x 0 - - - x `) ); expect(key("<")).toBe( template(` - - - - 0 x x - - - - x `) ); expect(key("v")).toBe( template(` - - - - - x x - 0 - - x `) ); expect(key(">>")).toBe( template(` - - - - - x x - - - 0 x `) ); expect(key("<<")).toBe( template(` - - - - - x x - 0 - - x `) ); expect(key(">")).toBe( template(` - - - - - x x - - 0 - x `) ); expect(key("^^")).toBe( template(` - 0 - - - x x - - - - x `) ); expect(key("^")).toBe( template(` - 0 - - - x x - - - - x `) ); expect(key("<<<")).toBe( template(` 0 - - - - x x - - - - x `) ); expect(key("vv")).toBe( template(` - - - - - x x - 0 - - x `) ); expect(key("^")).toBe( template(` - - - - 0 x x - - - - x `) ); expect(key(">>>")).toBe( template(` - - - - - x x - - - 0 x `) ); }); test("move grid focus with arrow keys rtl", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, rtl: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("v")).toBe( template(` - x x - - - - 0 - - - x `) ); expect(key(">")).toBe( template(` - x x - - - 0 - - - - x `) ); expect(key(">>")).toBe( template(` - x x - - - - 0 - - - x `) ); expect(key(">>>")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("<<<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("vv")).toBe( template(` - x x - - - - - 0 - - x `) ); }); test("move grid focus with arrow keys wrap", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("v")).toBe( template(` - x x 0 - - - - - - - x `) ); }); test("move grid focus with arrow keys wrap horizontal", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: "horizontal", }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x 0 - - - - - - - x `) ); }); test("move grid focus with arrow keys wrap vertical", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: "vertical", }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("v")).toBe( template(` - x x 0 - - - - - - - x `) ); }); test("move grid focus with arrow keys loop", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, loop: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("v")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key("v")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("^")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key("^")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - 0 - - - x `) ); expect(key("v")).toBe( template(` - x x 0 - - - - - - - x `) ); }); test("move grid focus with arrow keys loop horizontal", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, loop: "horizontal", }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x 0 - - - - - - - x `) ); }); test("move grid focus with arrow keys loop vertical", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, loop: "vertical", }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); }); test("move grid focus with arrow keys wrap loop", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: true, loop: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key(">>>")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key(">")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("v")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("v")).toBe( template(` - x x - - - - 0 - - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - 0 - - - x `) ); }); test("move grid focus with arrow keys wrap horizontal loop vertical", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: "horizontal", loop: "vertical", }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key(">>>")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key(">")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("v")).toBe( template(` - x x - - - 0 - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - 0 x `) ); }); test("move grid focus with arrow keys rtl wrap loop", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, rtl: true, wrap: true, loop: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x - 0 - - - - - - x `) ); expect(key(">>>")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("v")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("v")).toBe( template(` - x x - - - - 0 - - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); }); test("move grid focus with arrow keys different number of cells", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 0, 1], [2, 2, 0], [2, 2, 2, 2, 1], [2, 2, 2], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x x - - - x - - - - - - - - `) ); expect(key(">")).toBe( template(` - x x x 0 - - x - - - - - - - - `) ); expect(key(">")).toBe( template(` - x x x 0 - - x - - - - - - - - `) ); expect(key("^")).toBe( template(` - x x x 0 - - x - - - - - - - - `) ); expect(key("v")).toBe( template(` - x x x - - - x - - - - 0 - - - `) ); expect(key("v")).toBe( template(` - x x x - - - x - - - - 0 - - - `) ); expect(key("^")).toBe( template(` - x x x 0 - - x - - - - - - - - `) ); expect(key(">>>")).toBe( template(` - x x x - - - x - - - - - - - 0 `) ); expect(key("^")).toBe( template(` - x x x - - - x - - 0 - - - - - `) ); expect(key("<<")).toBe( template(` - x x x - - - x 0 - - - - - - - `) ); expect(key("v")).toBe( template(` - x x x - - - x - - - - - 0 - - `) ); expect(key(">")).toBe( template(` - x x x - - - x - - - - - - 0 - `) ); expect(key(">")).toBe( template(` - x x x - - - x - - - - - - - 0 `) ); expect(key(">")).toBe( template(` - x x x - - - x - - - - - - - 0 `) ); expect(key("^^")).toBe( template(` - x x x - - - x - - 0 - - - - - `) ); expect(key(">>")).toBe( template(` - x x x - - - x - - - - 0 - - - `) ); }); test("move grid focus with arrow keys different number of cells wrap", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 0, 1], [2, 2, 0], [2, 2, 2, 2, 1], [2, 2, 2], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; render(); press.Tab(); expect(active()).toBe( template(` 0 x x x - - - x - - - - - - - - `) ); expect(key(">")).toBe( template(` - x x x 0 - - x - - - - - - - - `) ); expect(key(">")).toBe( template(` - x x x - 0 - x - - - - - - - - `) ); expect(key(">>>")).toBe( template(` - x x x - - - x - - - - - - - 0 `) ); expect(key("^")).toBe( template(` - x x x - - - x - - 0 - - - - - `) ); expect(key(">")).toBe( template(` - x x x - - - x - - - 0 - - - - `) ); expect(key("^")).toBe( template(` - x x x - - - x - - - - - - - 0 `) ); expect(key("v")).toBe( template(` - x x x - - - x - - - 0 - - - - `) ); }); test("grid item with tabbable content inside", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, orientation: "vertical", wrap: true, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled, .5 - has widget const rows = [ [2, 0.5, 0, 2], [2, 2.5, 1, 2], [2, 2, 1.5, 0.5], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( {item % 1 !== 0 && ( )} ))} ))} ); }; const { getByLabelText } = render(); press.Tab(); expect(getByLabelText("0-0")).toHaveFocus(); press.ArrowDown(); press.ArrowRight(); expect(getByLabelText("1-1")).toHaveFocus(); press.Enter(); expect(getByLabelText("1-1")).not.toHaveFocus(); expect(getByLabelText("input-1-1")).toHaveFocus(); press.Enter(null, { shiftKey: true }); expect(getByLabelText("input-1-1")).toHaveFocus(); press.Tab(); expect(getByLabelText("2-2")).not.toHaveFocus(); expect(getByLabelText("input-2-2")).toHaveFocus(); press.ShiftTab(); expect(getByLabelText("1-1")).not.toHaveFocus(); expect(getByLabelText("input-1-1")).toHaveFocus(); press.Escape(); expect(getByLabelText("1-1")).toHaveFocus(); expect(getByLabelText("input-1-1")).not.toHaveFocus(); }); test("move to the past item when the current group is unmounted", () => { const Test = () => { const [disableGroup, setDisableGroup] = React.useState(false); const [disableItems, setDisableItems] = React.useState(false); const composite = useCompositeState({ unstable_virtual: virtual, }); const [groups, setGroups] = React.useState([[]]); React.useEffect(() => { if (disableGroup) { setGroups([ ["1-1", "1-2", "1-3"], ["3-1", "3-2", "3-3"], ]); } else { setGroups([ ["1-1", "1-2", "1-3"], ["2-1", "2-2", "2-3"], ["3-1", "3-2", "3-3"], ]); } }, [disableGroup]); return ( <> {groups.map((items, i) => ( {items.map((item) => ( ))} ))} ); }; const { getByLabelText, getByText } = render(); press.Tab(); press.Tab(); press.Tab(); expect(getByLabelText("1-1")).toHaveFocus(); press.ArrowDown(); press.ArrowRight(); expect(getByLabelText("2-2")).toHaveFocus(); click(getByText("toggle group")); press.Tab(); press.Tab(); expect(getByLabelText("1-1")).toHaveFocus(); click(getByText("toggle group")); press.Tab(); press.Tab(); expect(getByLabelText("1-1")).toHaveFocus(); press.ArrowDown(); expect(getByLabelText("2-1")).toHaveFocus(); click(getByText("toggle items")); press.Tab(); expect(getByLabelText("1-1")).toHaveFocus(); }); test("composite grid becomes the first item when currentId is null", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, currentId: null, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; const { getByLabelText: $ } = render(); press.Tab(); expect($("composite")).toHaveFocus(); expect(active()).toBe( template(` - x x - - - - - - - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("vv")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("v")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("^^")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect($("composite")).toHaveFocus(); expect(key(">")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key(">")).toBe( template(` - x x 0 - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect($("composite")).toHaveFocus(); expect(key("<")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("<<<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect(key("^^")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect(key("<<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect(key("<<<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect(key(">>")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("<<<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect(key(">>>")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key("<<<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - 0 - - x `) ); }); test("composite grid becomes the first item when currentId is null and wrap", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: true, currentId: null, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; const { getByLabelText: $ } = render(); press.Tab(); expect($("composite")).toHaveFocus(); expect(active()).toBe( template(` - x x - - - - - - - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("vv")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("v")).toBe( template(` - x x - - 0 - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect($("composite")).toHaveFocus(); }); test("composite grid becomes the first item when currentId is null, wrap and loop", () => { const Test = () => { const composite = useCompositeState({ unstable_virtual: virtual, wrap: true, loop: true, currentId: null, }); // 2 - enabled, 1 - disabled focusable, 0 - disabled const rows = [ [2, 0, 0, 2], [2, 2, 1, 2], [2, 2, 2, 0], ]; return ( {rows.map((items, i) => ( {items.map((item, j) => ( ))} ))} ); }; const { getByLabelText: $ } = render(); press.Tab(); expect($("composite")).toHaveFocus(); expect(active()).toBe( template(` - x x - - - - - - - - x `) ); expect(key("v")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("<")).toBe( template(` - x x - - - - - - - 0 x `) ); expect(key(">")).toBe( template(` 0 x x - - - - - - - - x `) ); expect(key("^")).toBe( template(` - x x - - - - - - - - x `) ); expect($("composite")).toHaveFocus(); expect(key("^")).toBe( template(` - x x - - - - - 0 - - x `) ); expect(key("v")).toBe( template(` - x x - - - - - - - - x `) ); expect($("composite")).toHaveFocus(); }); }); }); test("block intermediate focus/blur events when composite container is not the parent", () => { const stack: string[] = []; const returnTarget = (event: React.SyntheticEvent) => { const target = event.target as HTMLElement; const currentTarget = event.currentTarget as HTMLElement; stack.push( `${event.type} ${currentTarget.getAttribute( "aria-label" )} ${target.getAttribute("aria-label")}` ); }; const onCompositeFocus = jest.fn(returnTarget); const onCompositeBlur = jest.fn(returnTarget); const onItemFocus = jest.fn(returnTarget); const onItemBlur = jest.fn(returnTarget); const Test = () => { const composite = useCompositeState({ unstable_virtual: true }); return ( <> <> ); }; const { getByLabelText: $, baseElement } = render(); press.Tab(); expect(stack.splice(0)).toEqual([ "focus composite composite", "focus item1 item1", ]); press.ArrowDown(); expect(stack.splice(0)).toEqual(["blur item1 item1", "focus item2 item2"]); click($("item3")); expect(stack.splice(0)).toEqual(["blur item2 item2", "focus item3 item3"]); click(baseElement); expect(stack.splice(0)).toEqual([ "blur item3 item3", "blur composite composite", ]); press.Tab(); expect(stack.splice(0)).toEqual([ "focus composite composite", "focus item3 item3", ]); });