diff --git a/packages/calcite-components/src/components/list-item/interfaces.ts b/packages/calcite-components/src/components/list-item/interfaces.ts index a5fb2de378a..124774f6969 100644 --- a/packages/calcite-components/src/components/list-item/interfaces.ts +++ b/packages/calcite-components/src/components/list-item/interfaces.ts @@ -5,4 +5,5 @@ export type ItemData = { description: string; metadata: Record; el: ListItem["el"]; -}[]; + heading?: string[]; +}; diff --git a/packages/calcite-components/src/components/list/interfaces.ts b/packages/calcite-components/src/components/list/interfaces.ts index a87fe1a669a..2c87229e1bd 100644 --- a/packages/calcite-components/src/components/list/interfaces.ts +++ b/packages/calcite-components/src/components/list/interfaces.ts @@ -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"; @@ -16,3 +17,5 @@ export interface ListMoveDetail extends MoveDetail { dragEl: ListItem["el"]; relatedEl: ListItem["el"]; } + +export type ListElement = ListItem["el"] | ListItemGroup["el"]; diff --git a/packages/calcite-components/src/components/list/list.e2e.ts b/packages/calcite-components/src/components/list/list.e2e.ts index e90d2b89ed3..f5f753fe878 100755 --- a/packages/calcite-components/src/components/list/list.e2e.ts +++ b/packages/calcite-components/src/components/list/list.e2e.ts @@ -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, @@ -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` + + + + + + + + + + + + + + + + + + + `); + + 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`, { @@ -1886,4 +1993,9 @@ describe("calcite-list", () => { }); }); }); + + async function assertDescendantItems(page: E2EPage, groupSelector: string, visibility: boolean): Promise { + const items = await page.findAll(`calcite-list-item-group${groupSelector} > calcite-list-item`); + items.forEach(async (item: E2EElement) => expect(await item.isVisible()).toBe(visibility)); + } }); diff --git a/packages/calcite-components/src/components/list/list.stories.ts b/packages/calcite-components/src/components/list/list.stories.ts index 4d9e9a49e93..3b83164cded 100644 --- a/packages/calcite-components/src/components/list/list.stories.ts +++ b/packages/calcite-components/src/components/list/list.stories.ts @@ -5088,3 +5088,27 @@ export const sortableListWithSingleItem = (): string => html` `; + +export const filterGroups = (): string => + html` + + + + + + + + + `; diff --git a/packages/calcite-components/src/components/list/list.tsx b/packages/calcite-components/src/components/list/list.tsx index 633f75abea1..0b033ab54a1 100755 --- a/packages/calcite-components/src/components/list/list.tsx +++ b/packages/calcite-components/src/components/list/list.tsx @@ -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"; @@ -83,6 +84,8 @@ export class List private listItems: ListItem["el"][] = []; + private listItemGroups: ListItemGroup["el"][] = []; + mutationObserver = createObserver("mutation", () => { this.willPerformFilter = true; this.updateListItems(); @@ -161,7 +164,7 @@ export class List @state() assistiveText: string; - @state() dataForFilter: ItemData = []; + @state() dataForFilter: ItemData[] = []; @state() hasFilterActionsEnd = false; @@ -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. */ @@ -236,7 +239,7 @@ export class List * * @readonly */ - @property() filteredData: ItemData = []; + @property() filteredData: ItemData[] = []; /** * The currently filtered `calcite-list-item`s. @@ -409,6 +412,7 @@ export class List this.updateListItems(); this.setUpSorting(); this.setParentList(); + this.setListItemGroups(); } async load(): Promise { @@ -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); } @@ -686,17 +694,15 @@ export class List filteredItems, visibleParents, }: { - el: ListItem["el"] | ListItemGroup["el"]; + el: ListElement; filteredItems: ListItem["el"][]; - visibleParents: WeakSet; + visibleParents: WeakSet; }): void { const filterHidden = !visibleParents.has(el) && !filteredItems.includes(el as ListItem["el"]); el.filterHidden = filterHidden; - const closestParent = el.parentElement.closest( - parentSelector, - ); + const closestParent = el.parentElement.closest(parentSelector); if (!closestParent) { return; @@ -770,7 +776,7 @@ export class List } if (filterEl.filteredItems) { - this.filteredData = filterEl.filteredItems as ItemData; + this.filteredData = filterEl.filteredItems as ItemData[]; } this.updateListItems(); @@ -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"); @@ -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;