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(list): include groups in filtering #10664

Merged
merged 33 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c679a94
fix(list): include groups in filtering
anveshmekala Oct 31, 2024
becaffd
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Nov 1, 2024
053a27f
resolve merge conflicts
anveshmekala Nov 1, 2024
1f02133
resolve merge conflicts in e2e
anveshmekala Nov 1, 2024
85a0e4e
fix test failures
anveshmekala Nov 4, 2024
204f9e4
fix filteredItems prop
anveshmekala Nov 4, 2024
363863a
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Nov 18, 2024
c36591c
refactor code to lit
anveshmekala Nov 18, 2024
9c93432
linting
anveshmekala Nov 18, 2024
cd3c545
deprecate filteredItems and add filteredResults
anveshmekala Nov 20, 2024
246de6f
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 12, 2024
15cd55d
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 18, 2024
d6f7c24
refactor
anveshmekala Dec 18, 2024
8ea0e96
include heading in filterProps
anveshmekala Dec 19, 2024
977e982
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 19, 2024
52dd50c
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 19, 2024
56cad5f
clean up
anveshmekala Dec 19, 2024
59a933e
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 20, 2024
beebb86
remove console.log
anveshmekala Dec 20, 2024
635385a
add role=group when group is filtered
anveshmekala Dec 20, 2024
ca9c3b6
doc update
anveshmekala Dec 20, 2024
26f0d83
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 20, 2024
6aa62f7
rename
anveshmekala Dec 20, 2024
bdd2543
fix a11y test
anveshmekala Dec 21, 2024
23cd88d
drop filteredResults & do not include groups in filteredItems
anveshmekala Dec 31, 2024
4bfefb6
cleanup
anveshmekala Dec 31, 2024
be935d9
revert doc update
anveshmekala Dec 31, 2024
40d2c6f
update types
anveshmekala Dec 31, 2024
e107542
handle edge case when visible items are empty
anveshmekala Dec 31, 2024
cab883a
feedback changes
anveshmekala Dec 31, 2024
bbfbc6b
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Dec 31, 2024
723d02c
store heading as array
anveshmekala Dec 31, 2024
adef085
Merge branch 'dev' into anveshmekala/7702-fix-list-filtering-groups
anveshmekala Jan 2, 2025
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 @@ -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));
Copy link
Member

Choose a reason for hiding this comment

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

The other alternative would be something like this:

let ancestor = el.closest("calcite-list-item-group");
const headings: string[] = [];
while (ancestor) {
  headings.push(ancestor.heading);
  ancestor = ancestor.closest("calcite-list-item-group");
}

I'm not sure which would be more performant, calling closest() a few times on connectedCallback or calling setListItemGroups on each mutation. Thoughts @jcfranco?

}

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
Loading