Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tree): fix tree keyboard selection issue #7908

Merged
merged 1 commit into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,12 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent

@Listen("keydown")
keyDownHandler(event: KeyboardEvent): void {
if (this.isActionEndEvent(event)) {
if (this.isActionEndEvent(event) || event.defaultPrevented) {
return;
}

switch (event.key) {
case " ":
if (this.selectionMode === "none") {
return;
}
this.userChangedValue = true;
this.calciteInternalTreeItemSelect.emit({
modifyCurrentSelection: this.isSelectionMultiLike,
Expand All @@ -375,9 +372,6 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
event.preventDefault();
break;
case "Enter":
if (this.selectionMode === "none") {
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 = Array.from(this.el.children).find((el) =>
el.matches("a")
Expand Down
299 changes: 192 additions & 107 deletions packages/calcite-components/src/components/tree/tree.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ describe("calcite-tree", () => {
expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);
});

it("does not prevent space/enter keyboard event on actions with selectionMode of none", async () => {
it("does prevent space/enter keyboard event on actions with selectionMode of none", async () => {
const page = await newE2EPage();
await page.setContent(html`<div id="container">
<calcite-tree selection-mode="none">
Expand All @@ -1058,12 +1058,12 @@ describe("calcite-tree", () => {
await page.keyboard.press("Enter");

expect(keydownSpy).toHaveReceivedEventTimes(1);
expect(keydownSpy.lastEvent.defaultPrevented).toBe(false);
expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);

await page.keyboard.press("Space");

expect(keydownSpy).toHaveReceivedEventTimes(2);
expect(keydownSpy.lastEvent.defaultPrevented).toBe(false);
expect(keydownSpy.lastEvent.defaultPrevented).toBe(true);
});
});

Expand Down Expand Up @@ -1101,128 +1101,213 @@ describe("calcite-tree", () => {
});

describe("parent node expansion", () => {
describe("keyboard", () => {
assertSelectionModes(
false,
async (_page, item) => {
await item.press("Enter");
},
async (_page, item, toggle) => {
const itemExpanded = await item.getProperty("expanded");
const toggleKey = itemExpanded ? "ArrowLeft" : "ArrowRight";
await toggle.press(toggleKey);
},
async (_page, item) => {
await item.press("Enter");
}
);
});

describe("mouse", () => {
assertSelectionModes(
true,
async (page, item) => {
await directItemClick(page, item);
},
async (page, _item, toggle) => {
await directItemClick(page, toggle);
},
async (page, item) => {
await directItemClick(page, item);
}
);
});

interface SelectionModeTest {
selectionMode: SelectionMode;
canDeselect: boolean;
canDeselect: {
item: boolean;
child: boolean;
};
expandableItemClick: {
selectsItem: boolean;
selectsChildren: boolean;
};
}

const selectionModesTests: SelectionModeTest[] = [
{
selectionMode: "ancestors",
canDeselect: true,
expandableItemClick: {
selectsItem: true,
selectsChildren: true,
async function assertSelectionModes(
childToggleTraversesParent: boolean,
selectItem: (page: E2EPage, item: E2EElement) => Promise<void>,
toggleItem: (page: E2EPage, item: E2EElement, toggle: E2EElement) => Promise<void>,
selectItemChild: (page: E2EPage, item: E2EElement) => Promise<void>
): Promise<void> {
const selectionModesTests: SelectionModeTest[] = [
{
selectionMode: "ancestors",
canDeselect: { item: true, child: true },
expandableItemClick: {
selectsItem: true,
selectsChildren: true,
},
},
},
{
selectionMode: "multichildren",
canDeselect: true,
expandableItemClick: {
selectsItem: true,
selectsChildren: false,
{
selectionMode: "children",
canDeselect: { item: false, child: false },
expandableItemClick: {
selectsItem: true,
selectsChildren: false,
},
},
},
{
selectionMode: "multiple",
canDeselect: true,
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
{
selectionMode: "multichildren",
canDeselect: { item: true, child: true },
expandableItemClick: {
selectsItem: true,
selectsChildren: false,
},
},
},
{
selectionMode: "none",
canDeselect: false,
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
{
selectionMode: "multiple",
canDeselect: { item: true, child: true },
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
},
},
},
{
selectionMode: "single",
canDeselect: true,
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
{
selectionMode: "none",
canDeselect: { item: false, child: false },
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
},
},
},
{
selectionMode: "single-persist",
canDeselect: false,
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
{
selectionMode: "single",
canDeselect: { item: true, child: false },
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
},
},
},
];

selectionModesTests.forEach(
({ selectionMode, canDeselect, expandableItemClick: { selectsItem, selectsChildren } }) => {
it(`selection-mode = ${selectionMode}`, async () => {
const expandableItemId = "expandable-item";
const page = await newE2EPage();
await page.setContent(html`
<calcite-tree selection-mode="${selectionMode}">
<calcite-tree-item>Child 1</calcite-tree-item>
<calcite-tree-item id="${expandableItemId}">
Child 2
<calcite-tree slot="children">
<calcite-tree-item>Grandchild 1</calcite-tree-item>
<calcite-tree-item>Grandchild 2</calcite-tree-item>
<calcite-tree-item>
Grandchild 3
<calcite-tree slot="children">
<calcite-tree-item>Great-Grandchild 1</calcite-tree-item>
<calcite-tree-item>Great-Grandchild 2</calcite-tree-item>
<calcite-tree-item>Great-Grandchild 3</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>
`);

const tree = await page.find("calcite-tree");
expect(await tree.getProperty("selectedItems")).toHaveLength(0);

const expandableParentItem = await page.find(`#${expandableItemId}`);
const childItems = await expandableParentItem.findAll("calcite-tree-item");
expect(await expandableParentItem.getProperty("expanded")).toBe(false);

await directItemClick(page, expandableParentItem);

expect(await expandableParentItem.getProperty("expanded")).toBe(!selectsItem);
const expectedSelectedItemsAfterExpanding = (selectsItem ? 1 : 0) + (selectsChildren ? childItems.length : 0);
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterExpanding);

await directItemClick(page, expandableParentItem);

expect(await expandableParentItem.getProperty("expanded")).toBe(false);
const expectedSelectedItemsAfterCollapsing = canDeselect ? 0 : expectedSelectedItemsAfterExpanding;
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterCollapsing);
{
selectionMode: "single-persist",
canDeselect: { item: false, child: false },
expandableItemClick: {
selectsItem: false,
selectsChildren: false,
},
},
];

const expandableParentToggle = await page.find(`#${expandableItemId} >>> .${CSS.chevron}`);
selectionModesTests.forEach(
({ selectionMode, canDeselect, expandableItemClick: { selectsItem, selectsChildren } }) => {
it(`selection-mode = ${selectionMode}`, async () => {
const expandableItemId = "expandable-item";
const expandableItemChildId = "expandable-item-child";
const page = await newE2EPage();
await page.setContent(html`
<calcite-tree selection-mode="${selectionMode}">
<calcite-tree-item>Child 1</calcite-tree-item>
await directItemClick(page, expandableParentToggle);
<calcite-tree-item id="${expandableItemId}">
Child 2
expect(await expandableParentItem.getProperty("expanded")).toBe(true);
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterCollapsing);
<calcite-tree slot="children">
<calcite-tree-item id="${expandableItemChildId}">Grandchild 1</calcite-tree-item>
await directItemClick(page, expandableParentToggle);
<calcite-tree-item>Grandchild 2</calcite-tree-item>
expect(await expandableParentItem.getProperty("expanded")).toBe(false);
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterCollapsing);
});
}
);
<calcite-tree-item>
Grandchild 3
<calcite-tree slot="children">
<calcite-tree-item>Great-Grandchild 1</calcite-tree-item>
<calcite-tree-item>Great-Grandchild 2</calcite-tree-item>
<calcite-tree-item>Great-Grandchild 3</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>
`);

const tree = await page.find("calcite-tree");
expect(await tree.getProperty("selectedItems")).toHaveLength(0);

const expandableParentItem = await page.find(`#${expandableItemId}`);
const childItems = await expandableParentItem.findAll("calcite-tree-item");
expect(await expandableParentItem.getProperty("expanded")).toBe(false);

await selectItem(page, expandableParentItem);

expect(await expandableParentItem.getProperty("expanded")).toBe(!selectsItem);
const expectedSelectedItemsAfterExpanding =
(selectsItem ? 1 : 0) + (selectsChildren ? childItems.length : 0);
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterExpanding);

await selectItem(page, expandableParentItem);

expect(await expandableParentItem.getProperty("expanded")).toBe(false);
const expectedSelectedItemsAfterCollapsing = canDeselect.item ? 0 : expectedSelectedItemsAfterExpanding;
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterCollapsing);

const expandableParentToggle = await page.find(`#${expandableItemId} >>> .${CSS.chevron}`);

await toggleItem(page, expandableParentItem, expandableParentToggle);

expect(await expandableParentItem.getProperty("expanded")).toBe(true);
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterCollapsing);

await toggleItem(page, expandableParentItem, expandableParentToggle);

expect(await expandableParentItem.getProperty("expanded")).toBe(false);
expect(await tree.getProperty("selectedItems")).toHaveLength(expectedSelectedItemsAfterCollapsing);

const expandableChildItem = await page.find(`#${expandableItemChildId}`);

await selectItemChild(page, expandableChildItem);

expect(await expandableParentItem.getProperty("expanded")).toBe(
!selectsItem && !childToggleTraversesParent
);
expect(await tree.getProperty("selectedItems")).toHaveLength(
selectionMode === "none"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to clean up these assertion conditions, but might need to do in a follow-up PR.

? 0
: selectionMode === "ancestors" && !childToggleTraversesParent
? 7
: !childToggleTraversesParent &&
(selectionMode === "multiple" || selectionMode === "single" || selectionMode === "single-persist")
? 0
: 1
);

await selectItemChild(page, expandableChildItem);

expect(await expandableParentItem.getProperty("expanded")).toBe(
!selectsItem && !childToggleTraversesParent
);
expect(await tree.getProperty("selectedItems")).toHaveLength(
selectionMode === "none"
? 0
: !childToggleTraversesParent && selectionMode === "multiple"
? 1
: canDeselect.child
? 0
: 1
);
});
}
);
}
});
});
Loading