Skip to content

Commit

Permalink
fix(list): include groups in filtering (#10664)
Browse files Browse the repository at this point in the history
**Related Issue:** #7702 

## Summary

- Include `calcite-list-item-group` in filtering.
- group's `heading` can be used as `filterProp`.
  • Loading branch information
anveshmekala authored Jan 2, 2025
1 parent 57ccde6 commit 904623b
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export type ItemData = {
description: string;
metadata: Record<string, unknown>;
el: ListItem["el"];
}[];
heading?: string[];
};
3 changes: 3 additions & 0 deletions packages/calcite-components/src/components/list/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DragDetail, MoveDetail } from "../../utils/sortableComponent";
import type { ListItem } from "../list-item/list-item";
import type { ListItemGroup } from "../list-item-group/list-item-group";
import type { List } from "./list";

export type ListDisplayMode = "flat" | "nested";
Expand All @@ -16,3 +17,5 @@ export interface ListMoveDetail extends MoveDetail {
dragEl: ListItem["el"];
relatedEl: ListItem["el"];
}

export type ListElement = ListItem["el"] | ListItemGroup["el"];
116 changes: 114 additions & 2 deletions packages/calcite-components/src/components/list/list.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @ts-strict-ignore
import { newE2EPage, E2EPage } from "@arcgis/lumina-compiler/puppeteerTesting";
import { newE2EPage, E2EPage, E2EElement } from "@arcgis/lumina-compiler/puppeteerTesting";
import { describe, expect, it } from "vitest";
import {
accessible,
Expand Down Expand Up @@ -1876,6 +1875,114 @@ describe("calcite-list", () => {
});
});

describe("group filtering", () => {
it("should include groups while filtering", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-list filter-enabled filter-placeholder="typing 'recreation' should show 1st group with all items">
<calcite-list-item-group heading="Outdoor recreation" id="recreation">
<calcite-list-item
label="Hiking trails"
description="Designated routes for hikers to use."
value="hiking-trails"
></calcite-list-item>
<calcite-list-item
label="Waterfalls"
description="Vertical drops from a river."
value="waterfalls"
></calcite-list-item>
<calcite-list-item-group heading="Beaches" id="beaches">
<calcite-list-item label="Surfing" description="Surfing" value="Surfing"></calcite-list-item>
<calcite-list-item label="Paragliding" description="Paragliding" value="Paragliding"></calcite-list-item>
<calcite-list-item-group heading="Underwater" id="underwater">
<calcite-list-item label="Snorkeling" description="Snorkeling" value="Snorkeling"></calcite-list-item>
<calcite-list-item
label="Scuba diving"
description="Scuba diving"
value="Scuba diving"
></calcite-list-item>
</calcite-list-item-group>
</calcite-list-item-group>
</calcite-list-item-group>
<calcite-list-item-group heading="Buildings" id="buildings">
<calcite-list-item
label="Park offices"
description="Home base for park staff to converse with visitors."
value="offices"
></calcite-list-item>
<calcite-list-item
label="Guest lodges"
description="Small houses available for visitors to book for stays."
value="lodges"
></calcite-list-item>
</calcite-list-item-group>
</calcite-list>
`);

const filter = await page.find(`calcite-list >>> calcite-filter`);
const list = await page.find("calcite-list");
await filter.callMethod("setFocus");
await page.waitForChanges();
expect(await list.getProperty("filteredItems")).toHaveLength(8);

const group1 = await page.find("#recreation");
const group2 = await page.find("#buildings");
const group3 = await page.find("#beaches");
const group4 = await page.find("#underwater");

await page.keyboard.type("Bui");
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.filter);
expect(await list.getProperty("filterText")).toBe("Bui");
expect(await list.getProperty("filteredItems")).toHaveLength(2);

expect(await group1.isVisible()).toBe(false);
await assertDescendantItems(page, "#recreation", false);
expect(await group2.isVisible()).toBe(true);
await assertDescendantItems(page, `#buildings`, true);
expect(await group3.isVisible()).toBe(false);
await assertDescendantItems(page, `#beaches`, false);
expect(await group4.isVisible()).toBe(false);
await assertDescendantItems(page, `#underwater`, false);

await page.keyboard.press("Escape");
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.filter);
expect(await list.getProperty("filterText")).toBe("");
expect(await list.getProperty("filteredItems")).toHaveLength(8);

expect(await group1.isVisible()).toBe(true);
await assertDescendantItems(page, "#recreation", true);
expect(await group2.isVisible()).toBe(true);
await assertDescendantItems(page, "#buildings", true);
expect(await group3.isVisible()).toBe(true);
await assertDescendantItems(page, "#beaches", true);
expect(await group4.isVisible()).toBe(true);
await assertDescendantItems(page, `#underwater`, true);

await page.keyboard.type("Bea");
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.filter);
expect(await list.getProperty("filterText")).toBe("Bea");
expect(await list.getProperty("filteredItems")).toHaveLength(4);

expect(await group1.isVisible()).toBe(true);
await assertDescendantItems(page, "#recreation", false);
expect(await group2.isVisible()).toBe(false);
await assertDescendantItems(page, "#buildings", false);
expect(await group3.isVisible()).toBe(true);
await assertDescendantItems(page, "#beaches", true);
expect(await group4.isVisible()).toBe(true);
await assertDescendantItems(page, `#underwater`, true);

await page.keyboard.press("Backspace");
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.filter);
expect(await list.getProperty("filterText")).toBe("Be");
expect(await list.getProperty("filteredItems")).toHaveLength(4);
});
});

describe("themed", () => {
describe("default", () => {
themed(html`calcite-list`, {
Expand All @@ -1886,4 +1993,9 @@ describe("calcite-list", () => {
});
});
});

async function assertDescendantItems(page: E2EPage, groupSelector: string, visibility: boolean): Promise<void> {
const items = await page.findAll(`calcite-list-item-group${groupSelector} > calcite-list-item`);
items.forEach(async (item: E2EElement) => expect(await item.isVisible()).toBe(visibility));
}
});
24 changes: 24 additions & 0 deletions packages/calcite-components/src/components/list/list.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5088,3 +5088,27 @@ export const sortableListWithSingleItem = (): string =>
html`<calcite-list drag-enabled label="test">
<calcite-list-item label="small" value="small" description="small hello world"></calcite-list-item>
</calcite-list>`;

export const filterGroups = (): string =>
html` <calcite-list
filter-enabled
filter-placeholder="typing 'recreation' should show 1st group with all items"
filter-text="Beaches"
>
<calcite-list-item-group heading="Outdoor recreation" id="recreation">
<calcite-list-item
label="Hiking trails"
description="Designated routes for hikers to use."
value="hiking-trails"
></calcite-list-item>
<calcite-list-item
label="Waterfalls"
description="Vertical drops from a river."
value="waterfalls"
></calcite-list-item>
<calcite-list-item-group heading="Beaches" id="beaches">
<calcite-list-item label="Surfing" description="Surfing" value="Surfing"></calcite-list-item>
<calcite-list-item label="Paragliding" description="Paragliding" value="Paragliding"></calcite-list-item>
</calcite-list-item-group>
</calcite-list-item-group>
</calcite-list>`;
37 changes: 26 additions & 11 deletions packages/calcite-components/src/components/list/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { Filter } from "../filter/filter";
import type { ListItemGroup } from "../list-item-group/list-item-group";
import { CSS, debounceTimeout, SelectionAppearance, SLOTS } from "./resources";
import T9nStrings from "./assets/t9n/messages.en.json";
import { ListElement } from "./interfaces";
import { ListDragDetail, ListDisplayMode, ListMoveDetail } from "./interfaces";
import { styles } from "./list.scss";

Expand Down Expand Up @@ -83,6 +84,8 @@ export class List

private listItems: ListItem["el"][] = [];

private listItemGroups: ListItemGroup["el"][] = [];

mutationObserver = createObserver("mutation", () => {
this.willPerformFilter = true;
this.updateListItems();
Expand Down Expand Up @@ -161,7 +164,7 @@ export class List

@state() assistiveText: string;

@state() dataForFilter: ItemData = [];
@state() dataForFilter: ItemData[] = [];

@state() hasFilterActionsEnd = false;

Expand Down Expand Up @@ -225,7 +228,7 @@ export class List
/** Placeholder text for the component's filter input field. */
@property({ reflect: true }) filterPlaceholder: string;

/** Specifies the properties to match against when filtering. If not set, all properties will be matched (label, description, metadata, value). */
/** Specifies the properties to match against when filtering. If not set, all properties will be matched (label, description, metadata, value, group heading). */
@property() filterProps: string[];

/** Text for the component's filter input field. */
Expand All @@ -236,7 +239,7 @@ export class List
*
* @readonly
*/
@property() filteredData: ItemData = [];
@property() filteredData: ItemData[] = [];

/**
* The currently filtered `calcite-list-item`s.
Expand Down Expand Up @@ -409,6 +412,7 @@ export class List
this.updateListItems();
this.setUpSorting();
this.setParentList();
this.setListItemGroups();
}

async load(): Promise<void> {
Expand Down Expand Up @@ -650,6 +654,10 @@ export class List
}
}

private setListItemGroups(): void {
this.listItemGroups = Array.from(this.el.querySelectorAll(listItemGroupSelector));
}

private handleFilterActionsStartSlotChange(event: Event): void {
this.hasFilterActionsStart = slotChangeHasAssignedElement(event);
}
Expand Down Expand Up @@ -686,17 +694,15 @@ export class List
filteredItems,
visibleParents,
}: {
el: ListItem["el"] | ListItemGroup["el"];
el: ListElement;
filteredItems: ListItem["el"][];
visibleParents: WeakSet<ListItem["el"] | ListItemGroup["el"]>;
visibleParents: WeakSet<ListElement>;
}): void {
const filterHidden = !visibleParents.has(el) && !filteredItems.includes(el as ListItem["el"]);

el.filterHidden = filterHidden;

const closestParent = el.parentElement.closest<ListItem["el"] | ListItemGroup["el"]>(
parentSelector,
);
const closestParent = el.parentElement.closest<ListElement>(parentSelector);

if (!closestParent) {
return;
Expand Down Expand Up @@ -770,7 +776,7 @@ export class List
}

if (filterEl.filteredItems) {
this.filteredData = filterEl.filteredItems as ItemData;
this.filteredData = filterEl.filteredItems as ItemData[];
}

this.updateListItems();
Expand All @@ -783,7 +789,7 @@ export class List

private get effectiveFilterProps(): string[] {
if (!this.filterProps) {
return ["description", "label", "metadata"];
return ["description", "label", "metadata", "heading"];
}

return this.filterProps.filter((prop) => prop !== "el");
Expand Down Expand Up @@ -814,15 +820,24 @@ export class List
this.updateFilteredData();
}

private getItemData(): ItemData {
private getItemData(): ItemData[] {
return this.listItems.map((item) => ({
label: item.label,
description: item.description,
metadata: item.metadata,
heading: this.getGroupHeading(item),
el: item,
}));
}

private getGroupHeading(item: ListItem["el"]): string[] {
const heading = this.listItemGroups
.filter((group) => group.contains(item))
.map((group) => group.heading);

return heading;
}

private updateGroupItems(): void {
const { el, group, scale } = this;

Expand Down

0 comments on commit 904623b

Please sign in to comment.