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, filter): fix sync between list items and filtered data #10342

Merged
merged 7 commits into from
Sep 25, 2024
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 @@ -222,6 +222,14 @@ describe("calcite-filter", () => {

assertMatchingItems(await filter.getProperty("filteredItems"), ["jon"]);
expect(filterChangeSpy).toHaveReceivedEventTimes(1);

await page.$eval("calcite-filter", (filter: HTMLCalciteFilterElement): void => {
filter.items = [];
});
await page.waitForTimeout(DEBOUNCE.filter);
await page.waitForChanges();
assertMatchingItems(await filter.getProperty("filteredItems"), []);
expect(filterChangeSpy).toHaveReceivedEventTimes(1);
});

it("searches recursively in items and works and matches on a partial string ignoring case", async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/calcite-components/src/components/filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,7 @@ export class Filter

async componentWillLoad(): Promise<void> {
setUpLoadableComponent(this);
if (this.items.length) {
this.updateFiltered(filter(this.items, this.value, this.filterProps));
}
this.updateFiltered(filter(this.items ?? [], this.value, this.filterProps));
driskull marked this conversation as resolved.
Show resolved Hide resolved
await setUpMessages(this);
}

Expand Down Expand Up @@ -230,8 +228,7 @@ export class Filter

private filterDebounced = debounce(
(value: string, emit = false, onFilter?: () => void): void =>
this.items.length &&
this.updateFiltered(filter(this.items, value, this.filterProps), emit, onFilter),
this.updateFiltered(filter(this.items ?? [], value, this.filterProps), emit, onFilter),
DEBOUNCE.filter,
);

Expand Down
47 changes: 47 additions & 0 deletions packages/calcite-components/src/components/list/list.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,53 @@ describe("calcite-list", () => {
expect(selectedItemValues[1]).toBe("three");
});

it("updating items after filtering", async () => {
const matchingFont = "Courier";

const page = await newE2EPage();
await page.setContent(html`
<calcite-list filter-enabled filter-text="">
<calcite-list-item value="item1" label="${matchingFont}" description="list1"></calcite-list-item>
<calcite-list-item value="item2" label="${matchingFont} 2" description="list1"></calcite-list-item>
<calcite-list-item value="item3" label="Other Font" description="list1"></calcite-list-item>
</calcite-list>
`);
await page.waitForChanges();

const list = await page.find("calcite-list");
let visibleItems = await page.findAll("calcite-list-item:not([filter-hidden])");

expect(visibleItems).toHaveLength(3);
visibleItems.forEach(async (item) => {
expect(await item.getProperty("description")).toBe("list1");
});

list.setProperty("filterText", matchingFont);
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.filter);

visibleItems = await page.findAll("calcite-list-item:not([filter-hidden])");
expect(visibleItems).toHaveLength(2);
visibleItems.forEach(async (item) => {
expect(await item.getProperty("description")).toBe("list1");
});

list.innerHTML = html`
<calcite-list-item value="item4" label="${matchingFont}" description="list2"></calcite-list-item>
<calcite-list-item value="item5" label="${matchingFont} 2" description="list2"></calcite-list-item>
<calcite-list-item value="item6" label="Other Font" description="list2"></calcite-list-item>
`;
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.filter);

expect(await list.getProperty("filterText")).toBe(matchingFont);
visibleItems = await page.findAll("calcite-list-item:not([filter-hidden])");
expect(visibleItems).toHaveLength(2);
visibleItems.forEach(async (item) => {
expect(await item.getProperty("description")).toBe("list2");
});
});

it("filters initially", async () => {
const page = await newE2EPage();
await page.setContent(html`
Expand Down
84 changes: 49 additions & 35 deletions packages/calcite-components/src/components/list/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class List
@Prop() filterProps: string[];

@Watch("filterProps")
async handlefilterPropsChange(): Promise<void> {
async handleFilterPropsChange(): Promise<void> {
this.performFilter();
}

Expand Down Expand Up @@ -232,7 +232,7 @@ export class List
@Watch("selectionMode")
@Watch("selectionAppearance")
handleListItemChange(): void {
this.updateListItems();
this.updateListItems({ performFilter: true });
}

//--------------------------------------------------------------------------
Expand Down Expand Up @@ -409,7 +409,7 @@ export class List
connectLocalized(this);
connectMessages(this);
this.connectObserver();
this.updateListItems();
this.updateListItems({ performFilter: true });
this.setUpSorting();
this.setParentList();
}
Expand Down Expand Up @@ -471,7 +471,9 @@ export class List

listItems: HTMLCalciteListItemElement[] = [];

mutationObserver = createObserver("mutation", () => this.updateListItems());
mutationObserver = createObserver("mutation", () =>
this.updateListItems({ performFilter: true }),
);

visibleItems: HTMLCalciteListItemElement[] = [];

Expand Down Expand Up @@ -807,10 +809,15 @@ export class List
this.filteredData = filterEl.filteredItems as ItemData;
}

this.updateListItems(emit);
this.updateListItems({ emitFilterChange: emit });
}

private async filterAndUpdateData(): Promise<void> {
await this.filterEl?.filter(this.filterText);
this.updateFilteredData();
}

private async performFilter(): Promise<void> {
private performFilter(): void {
const { filterEl, filterText, filterProps } = this;

if (!filterEl) {
Expand All @@ -819,8 +826,7 @@ export class List

filterEl.value = filterText;
filterEl.filterProps = filterProps;
await filterEl.filter(filterText);
this.updateFilteredData();
this.filterAndUpdateData();
}

private setFilterEl = (el: HTMLCalciteFilterElement): void => {
Expand All @@ -844,39 +850,47 @@ export class List
}));
};

private updateListItems = debounce((emitFilterChange = false): void => {
const { selectionAppearance, selectionMode, dragEnabled, el } = this;
private updateListItems = debounce(
(options?: { emitFilterChange?: boolean; performFilter?: boolean }): void => {
const emitFilterChange = options?.emitFilterChange ?? false;
const performFilter = options?.performFilter ?? false;

const { selectionAppearance, selectionMode, dragEnabled, el, filterEl, filterEnabled } = this;

const items = Array.from(this.el.querySelectorAll(listItemSelector));

const items = Array.from(this.el.querySelectorAll(listItemSelector));
items.forEach((item) => {
item.selectionAppearance = selectionAppearance;
item.selectionMode = selectionMode;
if (item.closest("calcite-list") === el) {
item.dragHandle = dragEnabled;
}
});

items.forEach((item) => {
item.selectionAppearance = selectionAppearance;
item.selectionMode = selectionMode;
if (item.closest("calcite-list") === el) {
item.dragHandle = dragEnabled;
if (this.parentListEl) {
this.setUpSorting();
return;
}
});

if (this.parentListEl) {
this.setUpSorting();
return;
}
this.listItems = items;
if (filterEnabled && performFilter) {
this.dataForFilter = this.getItemData();

this.listItems = items;
if (this.filterEnabled) {
this.dataForFilter = this.getItemData();
if (this.filterEl) {
this.filterEl.items = this.dataForFilter;
if (filterEl) {
filterEl.items = this.dataForFilter;
this.filterAndUpdateData();
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be awaited on to avoid synchronization issues?

Copy link
Member Author

Choose a reason for hiding this comment

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

@jcfranco It doesn't. The filter component will fire an event once the filter actually occurs.

This is part of the oddness of having a filter component that does the filtering. The syncing is odd.

}
}
}
this.visibleItems = this.listItems.filter((item) => !item.closed && !item.hidden);
this.updateFilteredItems(emitFilterChange);
this.borderItems();
this.focusableItems = this.filteredItems.filter((item) => !item.disabled);
this.setActiveListItem();
this.updateSelectedItems();
this.setUpSorting();
}, debounceTimeout);
this.visibleItems = this.listItems.filter((item) => !item.closed && !item.hidden);
this.updateFilteredItems(emitFilterChange);
this.borderItems();
this.focusableItems = this.filteredItems.filter((item) => !item.disabled);
this.setActiveListItem();
this.updateSelectedItems();
this.setUpSorting();
},
debounceTimeout,
);

private focusRow = (focusEl: HTMLCalciteListItemElement): void => {
const { focusableItems } = this;
Expand Down
Loading