Skip to content

Commit

Permalink
fix(tree): improve keyboard navigation (#7618)
Browse files Browse the repository at this point in the history
**Related Issue:** #6861 

## Summary

Improves tree's keyboard navigation to move across all items and users
can now tab to the next focusable element without having to traverse all
items.

### Notes

* items are now programmatically-focusable only (e.g., `tabindex="-1"`)
since the tree moves focus
* keyboard navigation based on
https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-1a/#kbd_label
* consolidated tree tests and improved coverage
* moved all navigation logic to the tree-level
* refactored item traversal logic to cover all cases
* moved tree `keyDownHandler` to use VDOM event.
  • Loading branch information
jcfranco authored Aug 29, 2023
1 parent 41d7d97 commit 826a5cb
Show file tree
Hide file tree
Showing 5 changed files with 396 additions and 448 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});

Expand Down Expand Up @@ -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: `
<calcite-tree>
<calcite-tree-item id="cables">
Cables
<calcite-tree slot="children">
<calcite-tree-item id="xlr">XLR Cable</calcite-tree-item>
<calcite-tree-item id="instrument">Instrument Cable</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>
`,
});

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: `
<calcite-tree>
<calcite-tree-item id="cables" expanded>
Cables
<calcite-tree slot="children">
<calcite-tree-item id="xlr">XLR Cable</calcite-tree-item>
<calcite-tree-item id="instrument">Instrument Cable</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>
`,
});

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(
Expand Down
40 changes: 8 additions & 32 deletions packages/calcite-components/src/components/tree-item/tree-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import {
Watch,
} from "@stencil/core";
import {
slotChangeHasAssignedElement,
filterDirectChildren,
getElementDir,
getSlotted,
nodeListToArray,
slotChangeHasAssignedElement,
toAriaBoolean,
} from "../../utils/dom";
import {
Expand Down Expand Up @@ -186,7 +185,10 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
}

componentDidRender(): void {
updateHostInteraction(this, () => this.parentExpanded || this.depth === 1);
updateHostInteraction(
this,
() => false // programmatically focusable
);
}

//--------------------------------------------------------------------------
Expand Down Expand Up @@ -358,8 +360,6 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent

@Listen("keydown")
keyDownHandler(event: KeyboardEvent): void {
let root;

if (this.isActionEndEvent(event)) {
return;
}
Expand All @@ -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;

Expand All @@ -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;
}
}
}

//--------------------------------------------------------------------------
Expand Down Expand Up @@ -533,3 +508,4 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
this.hasEndActions = slotChangeHasAssignedElement(event);
};
}

Loading

0 comments on commit 826a5cb

Please sign in to comment.