diff --git a/packages/calcite-components/src/components/tree-item/tree-item.e2e.ts b/packages/calcite-components/src/components/tree-item/tree-item.e2e.ts
index c0d3a7bf04f..1f7e070d9aa 100644
--- a/packages/calcite-components/src/components/tree-item/tree-item.e2e.ts
+++ b/packages/calcite-components/src/components/tree-item/tree-item.e2e.ts
@@ -285,8 +285,7 @@ describe("calcite-tree-item", () => {
const item = await page.find("#newbie");
expect(item).toEqualAttribute("aria-hidden", "false");
- expect(item).not.toHaveAttribute("calcite-hydrated-hidden");
- expect(item.tabIndex).toBe(0);
+ expect(item.tabIndex).toBe(-1); // items are programmatically focused
});
});
@@ -362,61 +361,6 @@ describe("calcite-tree-item", () => {
expect(isVisible).toBe(true);
});
- it("right arrow key expands subtree and left arrow collapses it", async () => {
- const page = await newE2EPage({
- html: `
-
-
- Cables
-
- XLR Cable
- Instrument Cable
-
-
-
- `,
- });
-
- await page.keyboard.press("Tab");
-
- expect(await page.evaluate(() => document.activeElement.id)).toBe("cables");
- expect(await page.evaluate(() => (document.activeElement as HTMLCalciteTreeItemElement).expanded)).toBe(false);
-
- await page.keyboard.press("ArrowRight");
-
- expect(await page.evaluate(() => document.activeElement.id)).toBe("cables");
- expect(await page.evaluate(() => (document.activeElement as HTMLCalciteTreeItemElement).expanded)).toBe(true);
-
- await page.keyboard.press("ArrowLeft");
-
- expect(await page.evaluate(() => document.activeElement.id)).toBe("cables");
- expect(await page.evaluate(() => (document.activeElement as HTMLCalciteTreeItemElement).expanded)).toBe(false);
- });
-
- it("right arrow key focuses first item in expanded subtree", async () => {
- const page = await newE2EPage({
- html: `
-
-
- Cables
-
- XLR Cable
- Instrument Cable
-
-
-
- `,
- });
-
- await page.keyboard.press("Tab");
-
- expect(await page.evaluate(() => document.activeElement.id)).toBe("cables");
-
- await page.keyboard.press("ArrowRight");
-
- expect(await page.evaluate(() => document.activeElement.id)).toBe("xlr");
- });
-
it("displaying an expanded item is visible", async () => {
const page = await newE2EPage();
await page.setContent(
diff --git a/packages/calcite-components/src/components/tree-item/tree-item.tsx b/packages/calcite-components/src/components/tree-item/tree-item.tsx
index 30791bb652b..3b0cb8dfbc8 100644
--- a/packages/calcite-components/src/components/tree-item/tree-item.tsx
+++ b/packages/calcite-components/src/components/tree-item/tree-item.tsx
@@ -12,11 +12,10 @@ import {
Watch,
} from "@stencil/core";
import {
- slotChangeHasAssignedElement,
filterDirectChildren,
getElementDir,
getSlotted,
- nodeListToArray,
+ slotChangeHasAssignedElement,
toAriaBoolean,
} from "../../utils/dom";
import {
@@ -186,7 +185,10 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
}
componentDidRender(): void {
- updateHostInteraction(this, () => this.parentExpanded || this.depth === 1);
+ updateHostInteraction(
+ this,
+ () => false // programmatically focusable
+ );
}
//--------------------------------------------------------------------------
@@ -358,8 +360,6 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
@Listen("keydown")
keyDownHandler(event: KeyboardEvent): void {
- let root;
-
if (this.isActionEndEvent(event)) {
return;
}
@@ -382,7 +382,7 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
return;
}
// activates a node, i.e., performs its default action. For parent nodes, one possible default action is to open or close the node. In single-select trees where selection does not follow focus (see note below), the default action is typically to select the focused node.
- const link = nodeListToArray(this.el.children).find((el) =>
+ const link = Array.from(this.el.children).find((el) =>
el.matches("a")
) as HTMLAnchorElement;
@@ -398,33 +398,8 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
updateItem: true,
});
}
-
event.preventDefault();
- break;
- case "Home":
- root = this.el.closest("calcite-tree:not([child])") as HTMLCalciteTreeElement;
-
- const firstNode = root.querySelector("calcite-tree-item");
-
- firstNode?.focus();
-
- break;
- case "End":
- root = this.el.closest("calcite-tree:not([child])");
-
- let currentNode = root.children[root.children.length - 1]; // last child
- let currentTree = nodeListToArray(currentNode.children).find((el) =>
- el.matches("calcite-tree")
- );
- while (currentTree) {
- currentNode = currentTree.children[root.children.length - 1];
- currentTree = nodeListToArray(currentNode.children).find((el) =>
- el.matches("calcite-tree")
- );
- }
- currentNode?.focus();
- break;
- }
+ }
}
//--------------------------------------------------------------------------
@@ -533,3 +508,4 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
this.hasEndActions = slotChangeHasAssignedElement(event);
};
}
+
diff --git a/packages/calcite-components/src/components/tree/tree.e2e.ts b/packages/calcite-components/src/components/tree/tree.e2e.ts
index db976677981..a0139e067a2 100644
--- a/packages/calcite-components/src/components/tree/tree.e2e.ts
+++ b/packages/calcite-components/src/components/tree/tree.e2e.ts
@@ -1,8 +1,9 @@
-import { E2EPage, newE2EPage } from "@stencil/core/testing";
+import { newE2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
import { accessible, defaults, hidden, renders } from "../../tests/commonTests";
import { CSS } from "../tree-item/resources";
import SpyInstance = jest.SpyInstance;
+import { getFocusedElementProp } from "../../tests/utils";
describe("calcite-tree", () => {
describe("renders", () => {
@@ -549,17 +550,17 @@ describe("calcite-tree", () => {
});
describe("keyboard support", () => {
- it("should allow space keydown events to propagate outside the root tree", async () => {
+ it("does not stop propagation of handled keyboard events", async () => {
const page = await newE2EPage({
- html: html`
+ html: html`
-
+
One
-
+
Child 1
-
+
Grandchild 1
@@ -567,396 +568,243 @@ describe("calcite-tree", () => {
-
`,
+ `,
});
- const container = await page.find("#container");
- const grandchild = await page.find("#grandchild-one");
- const keydownSpy = await container.spyOnEvent("keydown");
+ const keyDownSpy = await page.spyOnEvent("keydown");
+ const item = await page.find("#middle-item");
+ await item.focus();
- expect(keydownSpy).toHaveReceivedEventTimes(0);
+ expect(keyDownSpy).toHaveReceivedEventTimes(0);
- await grandchild.focus();
- await page.keyboard.press("Space");
+ await page.keyboard.press("Space"); // open
+ await page.keyboard.press("Enter"); // close
+ await page.keyboard.press("ArrowRight"); // open
+ await page.keyboard.press("ArrowLeft"); // close
+ await page.keyboard.press("Home");
+ await page.keyboard.press("End");
+ await page.keyboard.press("Tab");
- expect(keydownSpy).toHaveReceivedEventTimes(1);
+ expect(keyDownSpy).toHaveReceivedEventTimes(7);
});
- it("should allow enter keydown events to propagate outside the root tree", async () => {
- const page = await newE2EPage({
- html: html`
-
-
- One
-
-
- Child 1
-
-
- Grandchild 1
-
-
-
-
+ it("supports navigating the entire tree structure", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+ Root Item 1
+
+
+ Parent
+
+
+ Child
-
-
`,
- });
-
- const container = await page.find("#container");
- const grandchild = await page.find("#grandchild-one");
- const keydownSpy = await container.spyOnEvent("keydown");
-
- expect(keydownSpy).toHaveReceivedEventTimes(0);
-
- await grandchild.focus();
- await page.keyboard.press("Enter");
-
- expect(keydownSpy).toHaveReceivedEventTimes(1);
- });
-
- it.skip("should allow ArrowRight and ArrowLeft keydown events to propagate outside the root tree", async () => {
- const page = await newE2EPage({
- html: html`
-
-
- One
+
+ Child 2
-
- Child 1
-
-
- Grandchild 1
-
-
+
+ Grandchild
-
-
-
-
`,
- });
-
- const container = await page.find("#container");
- const one = await page.find("#one");
- const keydownSpy = await container.spyOnEvent("keydown");
-
- expect(keydownSpy).toHaveReceivedEventTimes(0);
-
- await one.focus();
- await page.keyboard.press("ArrowRight");
-
- expect(keydownSpy).toHaveReceivedEventTimes(1);
-
- await page.keyboard.press("ArrowRight");
-
- expect(keydownSpy).toHaveReceivedEventTimes(2);
-
- await page.keyboard.press("ArrowRight");
-
- expect(keydownSpy).toHaveReceivedEventTimes(3);
-
- await page.keyboard.press("ArrowLeft");
-
- expect(keydownSpy).toHaveReceivedEventTimes(4);
-
- await page.keyboard.press("ArrowLeft");
-
- expect(keydownSpy).toHaveReceivedEventTimes(5);
-
- await page.keyboard.press("ArrowLeft");
-
- expect(keydownSpy).toHaveReceivedEventTimes(6);
- });
-
- async function getActiveElementId(page: E2EPage): Promise {
- return page.evaluate(() => document.activeElement.id);
- }
-
- it("ArrowRight and ArrowLeft keys expand and collapse nested trees", async () => {
- const parent = "parent";
- const child = "child";
- const grandchild = "grandchild";
-
- const page = await newE2EPage({
- html: html`
-
-
- Parent
-
-
- Child
-
-
- Grandchild
-
-
+
+ Grandchild 2
+
+ Child 3
+
-
`,
- });
-
- const parentEl = await page.find(`#${parent}`);
- const childEl = await page.find(`#${child}`);
+
+
+ Root Item 3
+
+ `);
- expect(await parentEl.getProperty("expanded")).toBe(false);
- expect(await childEl.getProperty("expanded")).toBe(false);
+ const root = await page.find("#root");
+ const parent = await page.find("#parent");
+ const child2 = await page.find("#child2");
- await parentEl.focus();
- await page.keyboard.press("ArrowRight");
+ await root.focus();
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(parent);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(false);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-1");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(child);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(false);
+ expect(await getFocusedElementProp(page, "id")).toEqual("parent");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(child);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(true);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-3");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("Home");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(grandchild);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(true);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-1");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("End");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(grandchild);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(true);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-3");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(grandchild);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(true);
+ expect(await getFocusedElementProp(page, "id")).toEqual("parent");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowLeft");
+ await page.keyboard.press("ArrowRight");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(child);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(true);
+ expect(await getFocusedElementProp(page, "id")).toEqual("parent");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowLeft");
+ await page.keyboard.press("ArrowRight");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(child);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(false);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowLeft");
+ await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(parent);
- expect(await parentEl.getProperty("expanded")).toBe(true);
- expect(await childEl.getProperty("expanded")).toBe(false);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child2");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowLeft");
+ await page.keyboard.press("ArrowRight");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual(parent);
- expect(await parentEl.getProperty("expanded")).toBe(false);
- expect(await childEl.getProperty("expanded")).toBe(false);
- });
-
- it("ArrowUp and ArrowDown keys move focus between adjacent tree items at all 3 levels of depth and allow keydown events to propagate outside the tree root", async () => {
- const page = await newE2EPage({
- html: html`
-
-
- Root Item 1
-
-
- Parent
-
-
- Child
-
-
- Child 2
-
-
- Grandchild
-
-
- Grandchild 2
-
-
-
-
- Child 3
-
-
-
-
- Root Item 3
-
-
-
`,
- });
-
- const container = await page.find("#container");
- const root = await page.find("#root");
- const keydownSpy = await container.spyOnEvent("keydown");
-
- expect(keydownSpy).toHaveReceivedEventTimes(0);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child2");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(true);
- await root.focus();
+ await page.keyboard.press("ArrowRight");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("root-item-1");
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(true);
await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("parent");
- expect(keydownSpy).toHaveReceivedEventTimes(1);
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild2");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(true);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child");
- expect(keydownSpy).toHaveReceivedEventTimes(2);
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(true);
- await page.keyboard.press("ArrowDown");
+ await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child2");
- expect(keydownSpy).toHaveReceivedEventTimes(3);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child2");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(true);
- await page.keyboard.press("ArrowRight");
+ await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("grandchild");
- expect(keydownSpy).toHaveReceivedEventTimes(4);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child2");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("grandchild2");
- expect(keydownSpy).toHaveReceivedEventTimes(5);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child3");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("grandchild");
- expect(keydownSpy).toHaveReceivedEventTimes(6);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child2");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowLeft");
- await page.keyboard.press("ArrowDown");
+ await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child3");
- expect(keydownSpy).toHaveReceivedEventTimes(8);
+ expect(await getFocusedElementProp(page, "id")).toEqual("child");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowUp");
+ await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child2");
- expect(keydownSpy).toHaveReceivedEventTimes(9);
+ expect(await getFocusedElementProp(page, "id")).toEqual("parent");
+ expect(await parent.getProperty("expanded")).toBe(true);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowUp");
+ await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child");
- expect(keydownSpy).toHaveReceivedEventTimes(10);
+ expect(await getFocusedElementProp(page, "id")).toEqual("parent");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("ArrowLeft");
await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("root-item-3");
- expect(keydownSpy).toHaveReceivedEventTimes(12);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-3");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("parent");
- expect(keydownSpy).toHaveReceivedEventTimes(13);
+ expect(await getFocusedElementProp(page, "id")).toEqual("parent");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("root-item-1");
- expect(keydownSpy).toHaveReceivedEventTimes(14);
- });
-
- it("does prevent space/enter keyboard event on actions with selectionMode of single", async () => {
- const page = await newE2EPage();
- await page.setContent(html`
-
-
-
-
-
-
`);
-
- const container = await page.find("#container");
- const button = await page.find("button");
- const keydownSpy = await container.spyOnEvent("keydown");
-
- expect(keydownSpy).toHaveReceivedEventTimes(0);
-
- await button.focus();
- await page.keyboard.press("Enter");
-
- expect(keydownSpy).toHaveReceivedEventTimes(1);
- expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);
-
- await page.keyboard.press("Space");
-
- expect(keydownSpy).toHaveReceivedEventTimes(2);
- expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);
- });
-
- it("does not prevent space/enter keyboard event on actions with selectionMode of none", async () => {
- const page = await newE2EPage();
- await page.setContent(html`
-
-
-
-
-
-
`);
-
- const container = await page.find("#container");
- const button = await page.find("button");
- const keydownSpy = await container.spyOnEvent("keydown");
-
- expect(keydownSpy).toHaveReceivedEventTimes(0);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-1");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await button.focus();
- await page.keyboard.press("Enter");
+ await page.keyboard.press("End");
+ await page.waitForChanges();
- expect(keydownSpy).toHaveReceivedEventTimes(1);
- expect(keydownSpy.lastEvent.defaultPrevented).toBe(false);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-3");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
- await page.keyboard.press("Space");
+ await page.keyboard.press("Home");
+ await page.waitForChanges();
- expect(keydownSpy).toHaveReceivedEventTimes(2);
- expect(keydownSpy.lastEvent.defaultPrevented).toBe(false);
+ expect(await getFocusedElementProp(page, "id")).toEqual("root-item-1");
+ expect(await parent.getProperty("expanded")).toBe(false);
+ expect(await child2.getProperty("expanded")).toBe(false);
});
it("honors disabled items when navigating the tree", async () => {
const page = await newE2EPage();
await page.setContent(
- html`
+ html`
Child 1
@@ -1003,60 +851,211 @@ describe("calcite-tree", () => {
`
);
- await page.click("#child-1");
+ const root = await page.find("#child-1");
+ const child3 = await page.find("#child-3");
+ const grandchild2 = await page.find("#grandchild-2");
+
+ await root.focus();
- expect(await getActiveElementId(page)).toEqual("child-1");
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-1");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child-3");
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-3");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowRight");
await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-3");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
+
await page.keyboard.press("ArrowRight");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("grandchild-2");
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild-2");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowRight");
await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild-2");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
+
await page.keyboard.press("ArrowRight");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("great-grandchild-1");
+ expect(await getFocusedElementProp(page, "id")).toEqual("great-grandchild-1");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
await page.keyboard.press("ArrowDown");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("great-grandchild-3");
+ expect(await getFocusedElementProp(page, "id")).toEqual("great-grandchild-3");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("great-grandchild-1");
+ expect(await getFocusedElementProp(page, "id")).toEqual("great-grandchild-1");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
+
+ await page.keyboard.press("End");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-4");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
+
+ await page.keyboard.press("Home");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-1");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
+
+ await page.keyboard.press("ArrowDown");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-3");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
+
+ await page.keyboard.press("ArrowDown");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild-2");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
+
+ await page.keyboard.press("ArrowDown");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("great-grandchild-1");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("grandchild-2");
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild-2");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(true);
- await page.keyboard.press("ArrowUp");
+ await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("grandchild-2");
+ expect(await getFocusedElementProp(page, "id")).toEqual("grandchild-2");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-3");
+ expect(await child3.getProperty("expanded")).toBe(true);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
+
await page.keyboard.press("ArrowLeft");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child-3");
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-3");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
+
+ await page.keyboard.press("Home");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-1");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
+
+ await page.keyboard.press("End");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-4");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
await page.keyboard.press("ArrowUp");
await page.waitForChanges();
- expect(await getActiveElementId(page)).toEqual("child-1");
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-3");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
+
+ await page.keyboard.press("Home");
+ await page.waitForChanges();
+
+ expect(await getFocusedElementProp(page, "id")).toEqual("child-1");
+ expect(await child3.getProperty("expanded")).toBe(false);
+ expect(await grandchild2.getProperty("expanded")).toBe(false);
+ });
+
+ it("does prevent space/enter keyboard event on actions with selectionMode of single", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+
+
+
+
`);
+
+ const container = await page.find("#container");
+ const button = await page.find("button");
+ const keydownSpy = await container.spyOnEvent("keydown");
+
+ expect(keydownSpy).toHaveReceivedEventTimes(0);
+
+ await button.focus();
+ await page.keyboard.press("Enter");
+
+ expect(keydownSpy).toHaveReceivedEventTimes(1);
+ expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);
+
+ await page.keyboard.press("Space");
+
+ expect(keydownSpy).toHaveReceivedEventTimes(2);
+ expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);
+ });
+
+ it("does not prevent space/enter keyboard event on actions with selectionMode of none", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+
+
+
+
`);
+
+ const container = await page.find("#container");
+ const button = await page.find("button");
+ const keydownSpy = await container.spyOnEvent("keydown");
+
+ expect(keydownSpy).toHaveReceivedEventTimes(0);
+
+ await button.focus();
+ await page.keyboard.press("Enter");
+
+ expect(keydownSpy).toHaveReceivedEventTimes(1);
+ expect(keydownSpy.lastEvent.defaultPrevented).toBe(false);
+
+ await page.keyboard.press("Space");
+
+ expect(keydownSpy).toHaveReceivedEventTimes(2);
+ expect(keydownSpy.lastEvent.defaultPrevented).toBe(false);
});
});
diff --git a/packages/calcite-components/src/components/tree/tree.tsx b/packages/calcite-components/src/components/tree/tree.tsx
index bc603b8aa15..1d00c9920a4 100644
--- a/packages/calcite-components/src/components/tree/tree.tsx
+++ b/packages/calcite-components/src/components/tree/tree.tsx
@@ -9,10 +9,10 @@ import {
Prop,
VNode,
} from "@stencil/core";
-import { focusElement, getRootNode, nodeListToArray } from "../../utils/dom";
+import { focusElement, nodeListToArray } from "../../utils/dom";
import { Scale, SelectionMode } from "../interfaces";
import { TreeItemSelectDetail } from "../tree-item/interfaces";
-import { getEnabledSiblingItem } from "./utils";
+import { getTraversableItems, isTreeItem } from "./utils";
/**
* @slot - A slot for `calcite-tree-item` elements.
@@ -85,6 +85,7 @@ export class Tree {
this.selectionMode === "multiple" || this.selectionMode === "multichildren"
).toString()
}
+ onKeyDown={this.keyDownHandler}
role={!this.child ? "tree" : undefined}
tabIndex={this.getRootTabIndex()}
>
@@ -244,72 +245,96 @@ export class Tree {
event.stopPropagation();
}
- @Listen("keydown")
- keyDownHandler(event: KeyboardEvent): void {
- const root = this.el.closest("calcite-tree:not([child])") as HTMLCalciteTreeElement;
+ private keyDownHandler = (event: KeyboardEvent): void => {
+ if (this.child) {
+ return;
+ }
+
+ const root = this.el;
const target = event.target as HTMLCalciteTreeItemElement;
- if (!(root === this.el && target.tagName === "CALCITE-TREE-ITEM" && this.el.contains(target))) {
+ const supportedKeys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End", "Tab"];
+
+ if (!(isTreeItem(target) && this.el.contains(target)) || !supportedKeys.includes(event.key)) {
return;
}
- if (event.key === "ArrowDown") {
- const next = getEnabledSiblingItem(target.nextElementSibling, "down");
- if (next) {
- next.focus();
- event.preventDefault();
- }
+ const traversableItems = getTraversableItems(root);
+ if (event.key === "Tab") {
+ // root tabindex will be restored when blurred/focused
+ traversableItems.forEach((item) => (item.tabIndex = -1));
+ return;
+ }
+
+ if (event.key === "ArrowDown") {
+ const currentItemIndex = traversableItems.indexOf(target);
+ const nextItem = traversableItems[currentItemIndex + 1];
+ nextItem?.focus();
+ event.preventDefault();
return;
}
if (event.key === "ArrowUp") {
- const previous = getEnabledSiblingItem(target.previousElementSibling, "up");
- if (previous) {
- previous.focus();
- event.preventDefault();
- }
+ const currentItemIndex = traversableItems.indexOf(target);
+ const previousItem = traversableItems[currentItemIndex - 1];
+ previousItem?.focus();
+ event.preventDefault();
+ return;
}
- if (event.key === "ArrowLeft" && !target.disabled) {
- // When focus is on an open node, closes the node.
+ if (event.key === "ArrowLeft") {
if (target.hasChildren && target.expanded) {
target.expanded = false;
event.preventDefault();
return;
}
- // When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
- const parentItem = target.parentElement.closest("calcite-tree-item");
+ const rootToItemPath = traversableItems.slice(0, traversableItems.indexOf(target)).reverse();
+ const parentItem = rootToItemPath.find((item) => item.depth === target.depth - 1);
- if (parentItem && (!target.hasChildren || target.expanded === false)) {
- parentItem.focus();
- event.preventDefault();
- return;
- }
+ parentItem?.focus();
+ event.preventDefault();
- // When focus is on a root node that is also either an end node or a closed node, does nothing.
return;
}
- if (event.key === "ArrowRight" && !target.disabled) {
- if (target.hasChildren) {
- if (target.expanded && getRootNode(this.el).activeElement === target) {
- // When focus is on an open node, moves focus to the first child node.
- getEnabledSiblingItem(target.querySelector("calcite-tree-item"), "down")?.focus();
+ if (event.key === "ArrowRight") {
+ if (!target.disabled && target.hasChildren) {
+ if (!target.expanded) {
+ target.expanded = true;
event.preventDefault();
} else {
- // When focus is on a closed node, opens the node; focus does not move.
- target.expanded = true;
+ const currentItemIndex = traversableItems.indexOf(target);
+ const nextItem = traversableItems[currentItemIndex + 1];
+ nextItem?.focus();
event.preventDefault();
}
}
return;
}
- }
- updateAncestorTree(event: CustomEvent): void {
+ if (event.key === "Home") {
+ const firstNode = traversableItems.shift();
+ if (firstNode) {
+ firstNode.focus();
+ event.preventDefault();
+ }
+ return;
+ }
+
+ if (event.key === "End") {
+ const lastNode = traversableItems.pop();
+ if (lastNode) {
+ lastNode.focus();
+ event.preventDefault();
+ }
+ return;
+ }
+ };
+
+ private updateAncestorTree(event: CustomEvent): void {
const item = event.target as HTMLCalciteTreeItemElement;
const updateItem = event.detail.updateItem;
diff --git a/packages/calcite-components/src/components/tree/utils.ts b/packages/calcite-components/src/components/tree/utils.ts
index 81e392e8d86..f7537bd9480 100644
--- a/packages/calcite-components/src/components/tree/utils.ts
+++ b/packages/calcite-components/src/components/tree/utils.ts
@@ -1,20 +1,24 @@
-function isTreeItem(element: Element): element is HTMLCalciteTreeItemElement {
- return element?.matches("calcite-tree-item");
+export function isTreeItem(element: Element): element is HTMLCalciteTreeItemElement {
+ return element?.tagName === "CALCITE-TREE-ITEM";
}
-export function getEnabledSiblingItem(el: Element, direction: "up" | "down"): HTMLCalciteTreeItemElement {
- const directionProp = direction === "down" ? "nextElementSibling" : "previousElementSibling";
- let currentEl: Element = el;
- let enabledEl: HTMLCalciteTreeItemElement | null = null;
+export function getTraversableItems(root: HTMLCalciteTreeElement): HTMLCalciteTreeItemElement[] {
+ return Array.from(root.querySelectorAll("calcite-tree-item:not([disabled])")).filter(
+ (item): boolean => {
+ let currentItem: HTMLElement = item;
- while (isTreeItem(currentEl)) {
- if (!currentEl.disabled) {
- enabledEl = currentEl;
- break;
- }
+ while (currentItem !== root && currentItem !== undefined) {
+ const parent = currentItem.parentElement;
+ const traversable = !isTreeItem(parent) || !parent.hasChildren || parent.expanded;
+
+ if (!traversable) {
+ return false;
+ }
- currentEl = currentEl[directionProp];
- }
+ currentItem = currentItem.parentElement;
+ }
- return enabledEl;
+ return true;
+ }
+ );
}