Skip to content

Commit

Permalink
fix(tree): Add missing aria-selected attribute
Browse files Browse the repository at this point in the history
Closes #1388
  • Loading branch information
mlaursen committed Apr 1, 2022
1 parent ca27b19 commit e578ea0
Show file tree
Hide file tree
Showing 4 changed files with 1,257 additions and 10 deletions.
4 changes: 3 additions & 1 deletion packages/tree/src/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ export const Tree = forwardRef<ListElement, TreeProps<any>>(function Tree( // es
selected,
expanded,
focused,
onClick() {
onClick(event) {
event.stopPropagation();

setActiveId(itemId);
onItemSelect(itemId);
if (childItems) {
Expand Down
33 changes: 25 additions & 8 deletions packages/tree/src/TreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ export const TreeItem = forwardRef<
disabled = false,
readOnly,
onFocus,
onKeyUp,
onKeyDown,
onClick,
onMouseUp,
onMouseDown,
onMouseLeave,
onTouchMove,
onTouchEnd,
onTouchStart,
...props
},
ref
Expand All @@ -70,7 +79,17 @@ export const TreeItem = forwardRef<
const { ripples, className, handlers } = useInteractionStates({
disabled,
className: propClassName,
handlers: isLink ? props : undefined,
handlers: {
onKeyUp,
onKeyDown,
onClick,
onMouseUp,
onMouseDown,
onMouseLeave,
onTouchMove,
onTouchEnd,
onTouchStart,
},
disableSpacebarClick: isLink,
});

Expand Down Expand Up @@ -99,27 +118,25 @@ export const TreeItem = forwardRef<
}

event.preventDefault();
const tree = event.currentTarget.closest('[role="tree"]');
if (tree) {
(tree as ListElement).focus();
}
event.currentTarget.closest<ListElement>('[role="tree"]')?.focus();
},
[onFocus]
);

const a11y = {
"aria-expanded": renderChildItems ? expanded : undefined,
"aria-selected": selected,
"aria-level": depth + 1,
"aria-setsize": listSize,
"aria-posinset": itemIndex + 1,
"aria-disabled": disabled ? "true" : undefined,
"aria-disabled": disabled || undefined,
id,
role: "treeitem",
tabIndex: -1,
...handlers,
onFocus: handleFocus,
};
const noA11y = { role: "none" };
} as const;
const noA11y = { role: "none" } as const;

return (
<li
Expand Down
141 changes: 140 additions & 1 deletion packages/tree/src/__tests__/Tree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { render } from "@testing-library/react";
import type { ReactElement } from "react";
import { fireEvent, render } from "@testing-library/react";

import { Tree } from "../Tree";
import type { TreeData, TreeItemIds } from "../types";
import { useTreeItemExpansion } from "../useTreeItemExpansion";
import { useTreeItemSelection } from "../useTreeItemSelection";

const PROPS = {
id: "tree",
Expand Down Expand Up @@ -85,4 +89,139 @@ describe("Tree", () => {
expect(item.className).toContain("item-1-li-class-name");
expect(item.children[0].className).toContain("item-1-class-name");
});

describe("Real World Demos", () => {
interface Folder extends TreeItemIds {
name: string;
}

const folders: TreeData<Folder> = {
"folder-1": {
name: "Folder 1",
itemId: "folder-1",
parentId: null,
},
"folder-2": {
name: "Folder 2",
itemId: "folder-2",
parentId: null,
},
"folder-3": {
name: "Folder 3",
itemId: "folder-3",
parentId: null,
},
"folder-2-1": {
name: "Folder 2 Child 1",
itemId: "folder-2-1",
parentId: "folder-2",
},
"folder-2-2": {
name: "Folder 2 Child 2",
itemId: "folder-2-2",
parentId: "folder-2",
},
"folder-2-3": {
name: "Folder 2 Child 3",
itemId: "folder-2-3",
parentId: "folder-2",
},
};

it("should work with the tree item hooks for single selection", () => {
function Test(): ReactElement {
const selection = useTreeItemSelection([], false);
const expansion = useTreeItemExpansion([]);

return (
<Tree
id="single-select-tree"
data={folders}
aria-label="Tree"
{...selection}
{...expansion}
/>
);
}

const { getByRole, container } = render(<Test />);
const tree = getByRole("tree", { name: "Tree" });
expect(tree).not.toHaveAttribute("aria-multiselectable");

const folder1 = getByRole("treeitem", { name: "Folder 1" });
const folder2 = getByRole("treeitem", { name: "Folder 2" });
const folder3 = getByRole("treeitem", { name: "Folder 3" });
expect(folder1).toHaveAttribute("aria-selected", "false");
expect(folder2).toHaveAttribute("aria-selected", "false");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();

fireEvent.click(folder1);
expect(folder1).toHaveAttribute("aria-selected", "true");
expect(folder2).toHaveAttribute("aria-selected", "false");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();

fireEvent.click(folder2);
expect(folder1).toHaveAttribute("aria-selected", "false");
expect(folder2).toHaveAttribute("aria-selected", "true");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();

// does not support having an unselected tree item at this time
fireEvent.click(folder2);
expect(folder1).toHaveAttribute("aria-selected", "false");
expect(folder2).toHaveAttribute("aria-selected", "true");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();
});

it("should work with the tree item hooks for multi selection", () => {
function Test(): ReactElement {
const selection = useTreeItemSelection([], true);
const expansion = useTreeItemExpansion([]);

return (
<Tree
id="multi-select-tree"
data={folders}
aria-label="Tree"
{...selection}
{...expansion}
/>
);
}

const { getByRole, container } = render(<Test />);

const tree = getByRole("tree", { name: "Tree" });
expect(tree).toHaveAttribute("aria-multiselectable", "true");
expect(container).toMatchSnapshot();

const folder1 = getByRole("treeitem", { name: "Folder 1" });
const folder2 = getByRole("treeitem", { name: "Folder 2" });
const folder3 = getByRole("treeitem", { name: "Folder 3" });
expect(folder1).toHaveAttribute("aria-selected", "false");
expect(folder2).toHaveAttribute("aria-selected", "false");
expect(folder3).toHaveAttribute("aria-selected", "false");

fireEvent.click(folder1);
expect(folder1).toHaveAttribute("aria-selected", "true");
expect(folder2).toHaveAttribute("aria-selected", "false");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();

fireEvent.click(folder2);
expect(folder1).toHaveAttribute("aria-selected", "true");
expect(folder2).toHaveAttribute("aria-selected", "true");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();

fireEvent.click(folder2);
expect(folder1).toHaveAttribute("aria-selected", "true");
expect(folder2).toHaveAttribute("aria-selected", "false");
expect(folder3).toHaveAttribute("aria-selected", "false");
expect(container).toMatchSnapshot();
});
});
});
Loading

0 comments on commit e578ea0

Please sign in to comment.