Skip to content

Commit

Permalink
fix(tree-item): updates state when selection changes programmatically…
Browse files Browse the repository at this point in the history
… for `selection-mode='ancestors'` (#7572)

**Related Issue:** #7291

## Summary

Updates state of `calcite-tree-item` when `selected` prop is changed
programmatically.
  • Loading branch information
anveshmekala authored Aug 25, 2023
1 parent 3b85467 commit 40758c5
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ export interface TreeItemSelectDetail {
* Indicate if an item should be collapsed/expanded even if child selection is enabled.
*/
forceToggle: boolean;
/**
* Indicates if an item selected & indeterminate properties should be updated.
* This will be set to true for user interaction changes and false for programmatic changes.
*/
updateItem?: boolean;
}
45 changes: 38 additions & 7 deletions packages/calcite-components/src/components/tree-item/tree-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
/** When `true`, the component is expanded. */
@Prop({ mutable: true, reflect: true }) expanded = false;

@Watch("expanded")
expandedHandler(newValue: boolean): void {
this.updateParentIsExpanded(this.el, newValue);
}

/** When `true`, the icon will be flipped when the element direction is right-to-left (`"rtl"`). */
@Prop({ reflect: true }) iconFlipRtl: FlipContext;

Expand All @@ -69,9 +74,18 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
/** When `true`, the component is selected. */
@Prop({ mutable: true, reflect: true }) selected = false;

@Watch("expanded")
expandedHandler(newValue: boolean): void {
this.updateParentIsExpanded(this.el, newValue);
@Watch("selected")
handleSelectedChange(value: boolean): void {
if (this.selectionMode === "ancestors" && !this.userChangedValue) {
if (value) {
this.indeterminate = false;
}
this.calciteInternalTreeItemSelect.emit({
modifyCurrentSelection: true,
forceToggle: false,
updateItem: false,
});
}
}

/**
Expand Down Expand Up @@ -330,7 +344,9 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
this.calciteInternalTreeItemSelect.emit({
modifyCurrentSelection: this.selectionMode === "ancestors" || this.isSelectionMultiLike,
forceToggle: false,
updateItem: true,
});
this.userChangedValue = true;
}

iconClickHandler = (event: MouseEvent): void => {
Expand All @@ -353,9 +369,11 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
if (this.selectionMode === "none") {
return;
}
this.userChangedValue = true;
this.calciteInternalTreeItemSelect.emit({
modifyCurrentSelection: this.isSelectionMultiLike,
forceToggle: false,
updateItem: true,
});
event.preventDefault();
break;
Expand All @@ -368,13 +386,16 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
el.matches("a")
) as HTMLAnchorElement;

this.userChangedValue = true;

if (link) {
link.click();
this.selected = true;
} else {
this.calciteInternalTreeItemSelect.emit({
modifyCurrentSelection: this.isSelectionMultiLike,
forceToggle: false,
updateItem: true,
});
}

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

@State() hasEndActions = false;

/**
* Used to make sure initially expanded tree-item can properly
* transition and emit events from closed state when rendered.
Expand All @@ -437,15 +460,15 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
*/
@State() updateAfterInitialRender = false;

actionSlotWrapper!: HTMLElement;

childrenSlotWrapper!: HTMLElement;

defaultSlotWrapper!: HTMLElement;

actionSlotWrapper!: HTMLElement;

private parentTreeItem?: HTMLCalciteTreeItemElement;

@State() hasEndActions = false;
private userChangedValue = false;

//--------------------------------------------------------------------------
//
Expand Down Expand Up @@ -474,7 +497,6 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
*/
private updateAncestorTree(): void {
const parentItem = this.parentTreeItem;

if (this.selectionMode !== "ancestors" || !parentItem) {
return;
}
Expand All @@ -492,6 +514,15 @@ export class TreeItem implements ConditionalSlotComponent, InteractiveComponent
} else if (selectedSiblings.length > 0) {
parentItem.indeterminate = true;
}

const childItems = Array.from(
this.el.querySelectorAll<HTMLCalciteTreeItemElement>("calcite-tree-item:not([disabled])")
);

childItems.forEach((item: HTMLCalciteTreeItemElement) => {
item.selected = true;
item.indeterminate = false;
});
} else if (this.indeterminate) {
const parentItem = this.parentTreeItem;
parentItem.indeterminate = true;
Expand Down
90 changes: 90 additions & 0 deletions packages/calcite-components/src/components/tree/tree.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,96 @@ describe("calcite-tree", () => {
expect(await page.findAll("calcite-tree-item[selected]")).toHaveLength(0);
});
});

describe("selection changes programmatically in ancestors selection-mode", () => {
const pageContent = html`<calcite-tree selection-mode="ancestors">
<calcite-tree-item id="grandparent">
Grandparent
<calcite-tree slot="children">
<calcite-tree-item id="parent1">
Parent1
<calcite-tree slot="children">
<calcite-tree-item id="child1"
>Child1
<calcite-tree slot="children">
<calcite-tree-item id="grandchild1">GrandChild1</calcite-tree-item>
<calcite-tree-item id="grandchild2">GrandChild2</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
<calcite-tree-item id="child2">Child2</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
<calcite-tree-item id="parent2">Parent2</calcite-tree-item>
</calcite-tree>
</calcite-tree-item>
</calcite-tree>`;

it("should update selection of ancestors and descendants", async () => {
const page = await newE2EPage();
await page.setContent(pageContent);

const tree = await page.find("calcite-tree");
const selectEventSpy = await tree.spyOnEvent("calciteTreeSelect");
const child1 = await page.find("calcite-tree-item[id='child1']");
const child2 = await page.find("calcite-tree-item[id='child2']");
const parent1 = await page.find("calcite-tree-item[id='parent1']");
const parent2 = await page.find("calcite-tree-item[id='parent2']");
const grandparent = await page.find("calcite-tree-item[id='grandparent']");
const grandchild1 = await page.find("calcite-tree-item[id='grandchild1']");
const grandchild2 = await page.find("calcite-tree-item[id='grandchild2']");

child1.setProperty("selected", true);
await page.waitForChanges();
expect(selectEventSpy).toHaveReceivedEventTimes(0);
expect(await tree.getProperty("selectedItems")).toHaveLength(3);
expect(await page.findAll("calcite-tree-item[selected]")).toHaveLength(3);
expect(parent1).toHaveAttribute("indeterminate");
expect(grandparent).toHaveAttribute("indeterminate");

child2.setProperty("selected", true);
await page.waitForChanges();
expect(selectEventSpy).toHaveReceivedEventTimes(0);
expect(await tree.getProperty("selectedItems")).toHaveLength(5);
expect(await page.findAll("calcite-tree-item[selected]")).toHaveLength(5);
expect(parent1).not.toHaveAttribute("indeterminate");
expect(parent1).toHaveAttribute("selected");
expect(grandparent).toHaveAttribute("indeterminate");

parent2.setProperty("selected", true);
await page.waitForChanges();
expect(selectEventSpy).toHaveReceivedEventTimes(0);
expect(await tree.getProperty("selectedItems")).toHaveLength(7);
expect(await page.findAll("calcite-tree-item[selected]")).toHaveLength(7);
expect(grandparent).not.toHaveAttribute("indeterminate");
expect(grandparent).toHaveAttribute("selected");

grandchild2.setProperty("selected", false);
await page.waitForChanges();
expect(selectEventSpy).toHaveReceivedEventTimes(0);
expect(await tree.getProperty("selectedItems")).toHaveLength(3);
expect(await page.findAll("calcite-tree-item[selected]")).toHaveLength(3);
expect(grandparent).toHaveAttribute("indeterminate");
expect(grandparent).not.toHaveAttribute("selected");
expect(parent1).toHaveAttribute("indeterminate");
expect(grandchild1).toHaveAttribute("selected");
expect(grandchild2).not.toHaveAttribute("selected");
});

it("should select all descendants when root level element is selected", async () => {
const page = await newE2EPage();
await page.setContent(pageContent);
const tree = await page.find("calcite-tree");
const selectEventSpy = await tree.spyOnEvent("calciteTreeSelect");
const grandparent = await page.find("calcite-tree-item[id='grandparent']");
grandparent.setProperty("selected", true);
await page.waitForChanges();
expect(selectEventSpy).toHaveReceivedEventTimes(0);
expect(await tree.getProperty("selectedItems")).toHaveLength(7);
expect(await page.findAll("calcite-tree-item[selected]")).toHaveLength(7);
expect(grandparent).not.toHaveAttribute("indeterminate");
expect(grandparent).toHaveAttribute("selected");
});
});
});

describe("keyboard support", () => {
Expand Down
29 changes: 19 additions & 10 deletions packages/calcite-components/src/components/tree/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,9 @@ export class Tree {

updateAncestorTree(event: CustomEvent<TreeItemSelectDetail>): void {
const item = event.target as HTMLCalciteTreeItemElement;
const updateItem = event.detail.updateItem;

if (item.disabled) {
if (item.disabled || (item.indeterminate && !updateItem)) {
return;
}

Expand All @@ -329,9 +330,12 @@ export class Tree {
const childItemsWithNoChildren = childItems.filter((child) => !child.hasChildren);
const childItemsWithChildren = childItems.filter((child) => child.hasChildren);

const futureSelected = item.hasChildren
? !(item.selected || item.indeterminate)
: !item.selected;
let futureSelected;
if (updateItem) {
futureSelected = item.hasChildren ? !(item.selected || item.indeterminate) : !item.selected;
} else {
futureSelected = item.selected;
}

childItemsWithNoChildren.forEach((el) => {
el.selected = futureSelected;
Expand All @@ -357,11 +361,13 @@ export class Tree {
updateItemState(directChildItems, el);
});

if (item.hasChildren) {
updateItemState(childItems, item);
} else {
item.selected = futureSelected;
item.indeterminate = false;
if (updateItem) {
if (item.hasChildren) {
updateItemState(childItems, item);
} else {
item.selected = futureSelected;
item.indeterminate = false;
}
}

ancestors.forEach((ancestor) => {
Expand All @@ -382,8 +388,11 @@ export class Tree {
nodeListToArray(this.el.querySelectorAll("calcite-tree-item")) as HTMLCalciteTreeItemElement[]
).filter((i) => i.selected);

this.calciteTreeSelect.emit();
if (updateItem) {
this.calciteTreeSelect.emit();
}
}

//--------------------------------------------------------------------------
//
// Events
Expand Down

0 comments on commit 40758c5

Please sign in to comment.