Skip to content
Merged
46 changes: 46 additions & 0 deletions packages/calcite-components/src/components/filter/filter.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,52 @@ describe("calcite-filter", () => {
});
});

describe("filter method", () => {
let page: E2EPage;

beforeEach(async () => {
page = await newE2EPage();
await page.setContent(`<calcite-filter></calcite-filter>`);
await page.evaluate(() => {
const filter = document.querySelector("calcite-filter");
filter.items = [
{
name: "Harry",
description: "developer",
value: "harry",
metadata: { haircolor: "red", favoriteBand: "MetallicA" }
},
{
name: "Matt",
description: "developer",
value: "matt",
metadata: { haircolor: "black", favoriteBand: "Radiohead" }
Comment thread
driskull marked this conversation as resolved.
}
];
});
});

it("should filter with value argument", async () => {
const filter = await page.find("calcite-filter");
const filterChangeSpy = await page.spyOnEvent("calciteFilterChange");
await filter.callMethod("filter", "Matt");
await page.waitForChanges();
expect(filterChangeSpy).toHaveReceivedEventTimes(0);
assertMatchingItems(await filter.getProperty("filteredItems"), ["matt"]);
});

it("should filter without value argument", async () => {
const filter = await page.find("calcite-filter");
filter.setProperty("value", "harry");
await page.waitForChanges();
const filterChangeSpy = await page.spyOnEvent("calciteFilterChange");
await filter.callMethod("filter");
await page.waitForChanges();
expect(filterChangeSpy).toHaveReceivedEventTimes(0);
assertMatchingItems(await filter.getProperty("filteredItems"), ["harry"]);
});
});

describe("translation support", () => {
t9n("calcite-filter");
});
Expand Down
36 changes: 27 additions & 9 deletions packages/calcite-components/src/components/filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class Filter

@Watch("items")
watchItemsHandler(): void {
this.filter(this.value);
this.filterDebounced(this.value);
}

/**
Expand Down Expand Up @@ -112,7 +112,7 @@ export class Filter

@Watch("value")
valueHandler(value: string): void {
this.filter(value);
this.filterDebounced(value);
}

// --------------------------------------------------------------------------
Expand Down Expand Up @@ -169,6 +169,7 @@ export class Filter
disconnectedCallback(): void {
disconnectLocalized(this);
disconnectMessages(this);
this.filterDebounced.cancel();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice!

Would you need to filter again on connectedCallback? Might be an edge case, but I'm thinking of the case where filtering is running as the component is moved between parents.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe we could address that if it ever comes up? Since we don't know if a component is moved vs removed we would have to maintain state like this that isn't set via a prop. It would be something to discuss across all components that have some async method or state to maintain.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For sure, we can keep an eye out for it. I think this is a very edge use case anyways since the filter is a supporting component and it'd be odd for a component to move it around during filtering. 🤔

}

componentDidLoad(): void {
Expand All @@ -181,6 +182,22 @@ export class Filter
//
// --------------------------------------------------------------------------

/**
* Performs a filter on the component.
*
* This method can be useful because filtering is delayed and asynchronous.
*
* @param {string} value - The filter text value.
* @returns {Promise<void>}
*/
@Method()
async filter(value: string = this.value): Promise<void> {
return new Promise((resolve) => {
this.value = value;
this.filterDebounced(value, false, resolve);
});
}

/** Sets focus on the component. */
@Method()
async setFocus(): Promise<void> {
Expand All @@ -195,15 +212,16 @@ export class Filter
//
// --------------------------------------------------------------------------

private filter = debounce(
(value: string, emit = false): void => this.updateFiltered(filter(this.items, value), emit),
private filterDebounced = debounce(
(value: string, emit = false, onFilter?: () => void): void =>
this.updateFiltered(filter(this.items, value), emit, onFilter),
DEBOUNCE_TIMEOUT
);

inputHandler = (event: CustomEvent): void => {
const target = event.target as HTMLCalciteInputElement;
this.value = target.value;
this.filter(target.value, true);
this.filterDebounced(target.value, true);
};

keyDownHandler = (event: KeyboardEvent): void => {
Expand All @@ -219,16 +237,16 @@ export class Filter

clear = (): void => {
this.value = "";
this.filter("", true);
this.filterDebounced("", true);
this.setFocus();
};

updateFiltered(filtered: any[], emit = false): void {
this.filteredItems.length = 0;
this.filteredItems = this.filteredItems.concat(filtered);
updateFiltered(filtered: object[], emit = false, callback?: () => void): void {
Comment thread
driskull marked this conversation as resolved.
this.filteredItems = filtered;
if (emit) {
this.calciteFilterChange.emit();
}
callback?.();
}

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