diff --git a/packages/calcite-components/src/components.d.ts b/packages/calcite-components/src/components.d.ts
index f4d58e1375f..685befd3652 100644
--- a/packages/calcite-components/src/components.d.ts
+++ b/packages/calcite-components/src/components.d.ts
@@ -1236,6 +1236,10 @@ export namespace Components {
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
"disabled": boolean;
+ /**
+ * Text for the component's filter input field.
+ */
+ "filterText": string;
/**
* Specifies the component's filtered items.
* @readonly
@@ -9018,6 +9022,10 @@ declare namespace LocalJSX {
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
"disabled"?: boolean;
+ /**
+ * Text for the component's filter input field.
+ */
+ "filterText"?: string;
/**
* Specifies the component's filtered items.
* @readonly
diff --git a/packages/calcite-components/src/components/combobox/combobox.e2e.ts b/packages/calcite-components/src/components/combobox/combobox.e2e.ts
index 99d905f7144..6b195d744a7 100644
--- a/packages/calcite-components/src/components/combobox/combobox.e2e.ts
+++ b/packages/calcite-components/src/components/combobox/combobox.e2e.ts
@@ -179,177 +179,304 @@ describe("calcite-combobox", () => {
openClose(simpleComboboxHTML);
});
- it("should toggle the combobox when typing within the input", async () => {
- const page = await newE2EPage();
+ describe("filtering", () => {
+ it("should toggle the combobox when typing within the input", async () => {
+ const page = await newE2EPage();
- await page.setContent(html`
-
-
-
-
-
-
- `);
+ await page.setContent(html`
+
+
+
+
+
+
+ `);
- const combobox = await page.find("calcite-combobox");
- await combobox.callMethod("setFocus");
- await page.waitForChanges();
- expect(await combobox.getProperty("open")).toBe(false);
+ const combobox = await page.find("calcite-combobox");
+ await combobox.callMethod("setFocus");
+ await page.waitForChanges();
+ expect(await combobox.getProperty("open")).toBe(false);
- const text = "Arizona";
+ const text = "Arizona";
- await combobox.type(text);
- await page.waitForChanges();
+ await combobox.type(text);
+ await page.waitForChanges();
- expect(await combobox.getProperty("open")).toBe(true);
+ expect(await combobox.getProperty("open")).toBe(true);
- for (let i = 0; i < text.length; i++) {
- await combobox.press("Backspace");
- }
+ for (let i = 0; i < text.length; i++) {
+ await combobox.press("Backspace");
+ }
- await page.waitForChanges();
- expect(await combobox.getProperty("open")).toBe(false);
- });
+ await page.waitForChanges();
+ expect(await combobox.getProperty("open")).toBe(false);
+ });
- it("should not toggle the combobox when typing within the input does not match any results", async () => {
- const page = await newE2EPage();
+ it("should not toggle the combobox when typing within the input does not match any results", async () => {
+ const page = await newE2EPage();
- await page.setContent(html`
-
-
-
-
-
-
- `);
+ await page.setContent(html`
+
+
+
+
+
+
+ `);
- const combobox = await page.find("calcite-combobox");
- await combobox.callMethod("setFocus");
- await page.waitForChanges();
- expect(await combobox.getProperty("open")).toBe(false);
+ const combobox = await page.find("calcite-combobox");
+ await combobox.callMethod("setFocus");
+ await page.waitForChanges();
+ expect(await combobox.getProperty("open")).toBe(false);
- const text = "nomatchingtexthere";
+ const text = "nomatchingtexthere";
- await combobox.type(text);
- await page.waitForChanges();
+ await combobox.type(text);
+ await page.waitForChanges();
- expect(await combobox.getProperty("open")).toBe(false);
- });
+ expect(await combobox.getProperty("open")).toBe(false);
+ });
- it("filtering does not match property with value of undefined", async () => {
- const page = await newE2EPage();
+ it("filtering does not match property with value of undefined", async () => {
+ const page = await newE2EPage();
- await page.setContent(html`
-
-
-
-
-
-
- `);
+ await page.setContent(html`
+
+
+
+
+
+
+ `);
- const combobox = await page.find("calcite-combobox");
- const input = await page.find("calcite-combobox >>> input");
- const items = await page.findAll("calcite-combobox-item");
- await combobox.click();
- await page.waitForChanges();
+ const combobox = await page.find("calcite-combobox");
+ const input = await page.find("calcite-combobox >>> input");
+ const items = await page.findAll("calcite-combobox-item");
+ await combobox.click();
+ await page.waitForChanges();
- await input.type("undefined");
- await page.waitForChanges();
+ await input.type("undefined");
+ await page.waitForChanges();
- expect(await items[0].isVisible()).toBe(false);
- expect(await items[1].isVisible()).toBe(false);
- expect(await items[2].isVisible()).toBe(false);
- expect(await items[3].isVisible()).toBe(false);
- });
+ expect(await items[0].isVisible()).toBe(false);
+ expect(await items[1].isVisible()).toBe(false);
+ expect(await items[2].isVisible()).toBe(false);
+ expect(await items[3].isVisible()).toBe(false);
+ });
- it("should filter the items in listbox when typing into the input", async () => {
- const page = await newE2EPage();
+ it("should filter the items in listbox when typing into the input", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+
+
+
+
+ `);
- await page.setContent(html`
-
-
-
-
-
-
- `);
+ const combobox = await page.find("calcite-combobox");
+ const items = await page.findAll("calcite-combobox-item");
- const combobox = await page.find("calcite-combobox");
- const input = await page.find("calcite-combobox >>> input");
- const items = await page.findAll("calcite-combobox-item");
+ const openEvent = await combobox.spyOnEvent("calciteComboboxOpen");
+ const filterEventSpy = await combobox.spyOnEvent("calciteComboboxFilterChange");
- const openEvent = await combobox.spyOnEvent("calciteComboboxOpen");
- const filterEventSpy = await combobox.spyOnEvent("calciteComboboxFilterChange");
+ await combobox.click();
+ await page.waitForChanges();
+ expect(openEvent).toHaveReceivedEventTimes(1);
- await combobox.click();
- await page.waitForChanges();
- expect(openEvent).toHaveReceivedEventTimes(1);
+ await combobox.press("s");
+ await page.waitForChanges();
+ expect(filterEventSpy).toHaveReceivedEventTimes(1);
- await input.press("s");
- await page.waitForChanges();
- expect(filterEventSpy).toHaveReceivedEventTimes(1);
+ expect(await items[0].isVisible()).toBe(true);
+ expect(await items[1].isVisible()).toBe(true);
+ expect(await items[2].isVisible()).toBe(true);
+ expect(await items[3].isVisible()).toBe(true);
- expect(await items[0].isVisible()).toBe(true);
- expect(await items[1].isVisible()).toBe(true);
- expect(await items[2].isVisible()).toBe(true);
- expect(await items[3].isVisible()).toBe(true);
+ expect(await combobox.getProperty("filterText")).toBe("s");
+ expect((await combobox.getProperty("filteredItems")).length).toBe(4);
- expect((await combobox.getProperty("filteredItems")).length).toBe(4);
+ await combobox.press("i");
+ await page.waitForChanges();
+ expect(filterEventSpy).toHaveReceivedEventTimes(2);
- await input.press("i");
- await page.waitForChanges();
- expect(filterEventSpy).toHaveReceivedEventTimes(2);
+ expect(await items[0].isVisible()).toBe(true);
+ expect(await items[1].isVisible()).toBe(true);
+ expect(await items[2].isVisible()).toBe(false);
+ expect(await items[3].isVisible()).toBe(true);
- expect(await items[0].isVisible()).toBe(true);
- expect(await items[1].isVisible()).toBe(true);
- expect(await items[2].isVisible()).toBe(false);
- expect(await items[3].isVisible()).toBe(true);
+ expect(await combobox.getProperty("filterText")).toBe("si");
+ expect((await combobox.getProperty("filteredItems")).length).toBe(3);
- expect((await combobox.getProperty("filteredItems")).length).toBe(3);
+ await combobox.press("n");
+ await page.waitForChanges();
+ expect(filterEventSpy).toHaveReceivedEventTimes(3);
- await input.press("n");
- await page.waitForChanges();
- expect(filterEventSpy).toHaveReceivedEventTimes(3);
+ expect(await items[0].isVisible()).toBe(true);
+ expect(await items[1].isVisible()).toBe(true);
+ expect(await items[2].isVisible()).toBe(false);
+ expect(await items[3].isVisible()).toBe(false);
- expect(await items[0].isVisible()).toBe(true);
- expect(await items[1].isVisible()).toBe(true);
- expect(await items[2].isVisible()).toBe(false);
- expect(await items[3].isVisible()).toBe(false);
+ expect(await combobox.getProperty("filterText")).toBe("sin");
+ expect((await combobox.getProperty("filteredItems")).length).toBe(2);
+ });
- expect((await combobox.getProperty("filteredItems")).length).toBe(2);
- });
+ it("does not clear filter if pointer down/up on an item has a delay in between events", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html`
+
+
+
+
+
+
+ `);
- it("does not clear filter if pointer down/up on an item has a delay in between events", async () => {
- const page = await newE2EPage();
- await page.setContent(html`
-
-
-
-
-
+ const combobox = await page.find("calcite-combobox");
+ await combobox.click();
+ await page.waitForChanges();
+ await combobox.type("Algeria");
+ await page.waitForChanges();
+
+ const [lastItemX, lastItemY] = await getElementXY(page, "#item-4");
+
+ await page.mouse.move(lastItemX, lastItemY);
+ await page.mouse.down();
+ await page.waitForChanges();
+ await page.mouse.up();
+ await page.waitForChanges();
+
+ expect(await combobox.getProperty("value")).toBe("Libya/Algeria");
+ });
+
+ it("respects the filterDisabled item property", async () => {
+ const page = await newE2EPage();
+ await page.setContent(`
+
+
+
+
`);
- const combobox = await page.find("calcite-combobox");
- await combobox.click();
- await page.waitForChanges();
- await combobox.type("Algeria");
- await page.waitForChanges();
+ const combobox = await page.find("calcite-combobox");
+ await combobox.click();
+ await page.waitForChanges();
+ await combobox.type("two");
+ await page.waitForChanges();
+ const one = await (await page.find("#one")).isVisible();
+ const two = await (await page.find("#two")).isVisible();
+ const three = await (await page.find("#three")).isVisible();
- const [lastItemX, lastItemY] = await getElementXY(page, "#item-4");
+ expect(one).toBeFalsy();
+ expect(two).toBeTruthy();
+ expect(three).toBeTruthy();
+ });
- await page.mouse.move(lastItemX, lastItemY);
- await page.mouse.down();
- await page.waitForChanges();
- await page.mouse.up();
- await page.waitForChanges();
+ const nestedComboboxChildren = html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ it("should filter on initial load", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html` ${nestedComboboxChildren} `);
+ await page.waitForChanges();
+
+ const visibleItemsAndGroups = await page.findAll(
+ "calcite-combobox-item:not([hidden]), calcite-combobox-item-group:not([hidden])",
+ );
+ const visibleItemAndGroupIds = await Promise.all(visibleItemsAndGroups.map((item) => item.getProperty("id")));
+
+ expect(visibleItemAndGroupIds).toEqual([
+ "group-1",
+ "item-1-2",
+ "subgroup-1-1",
+ "subgroup-1-1-2",
+ "item-1-1-2-1",
+ "item-1-1-2-2",
+ "group-2",
+ "item-2-1",
+ "item-2-1-2",
+ ]);
+ });
+
+ it("should display all groups/items when filter is cleared", async () => {
+ const page = await newE2EPage();
+ await page.setContent(html` ${nestedComboboxChildren} `);
+ await page.waitForChanges();
- expect(await combobox.getProperty("value")).toBe("Libya/Algeria");
+ const combobox = await page.find("calcite-combobox");
+ combobox.setProperty("filterText", "1.2");
+ await page.waitForChanges();
+
+ const filteredItemsAndGroups = await page.findAll(
+ "calcite-combobox-item:not([hidden]), calcite-combobox-item-group:not([hidden])",
+ );
+ const filteredItemAndGroupIds = await Promise.all(filteredItemsAndGroups.map((item) => item.getProperty("id")));
+
+ expect(filteredItemAndGroupIds).toEqual([
+ "group-1",
+ "item-1-2",
+ "subgroup-1-1",
+ "subgroup-1-1-2",
+ "item-1-1-2-1",
+ "item-1-1-2-2",
+ "group-2",
+ "item-2-1",
+ "item-2-1-2",
+ ]);
+
+ combobox.setProperty("filterText", "");
+ await page.waitForChanges();
+
+ const allVisibleItemAndGroups = await page.findAll(
+ "calcite-combobox-item:not([hidden]), calcite-combobox-item-group:not([hidden])",
+ );
+ const allVisibleItemAndGroupIds = await Promise.all(
+ allVisibleItemAndGroups.map((item) => item.getProperty("id")),
+ );
+ expect(allVisibleItemAndGroupIds).toEqual([
+ "group-1",
+ "item-1-1",
+ "item-1-2",
+ "subgroup-1-1",
+ "item-1-1-1",
+ "subgroup-1-1-1",
+ "subgroup-1-1-2",
+ "item-1-1-2-1",
+ "item-1-1-2-2",
+ "group-2",
+ "item-2-1",
+ "item-2-1-1",
+ "item-2-1-2",
+ ]);
+ });
});
it("should control max items displayed", async () => {
@@ -1731,29 +1858,6 @@ describe("calcite-combobox", () => {
});
});
- it("respects the filterDisabled item property", async () => {
- const page = await newE2EPage();
- await page.setContent(`
-
-
-
-
-
- `);
-
- await page.waitForChanges();
- const input = await page.find("calcite-combobox >>> .wrapper");
- await input.click();
- await page.keyboard.type("two");
- await page.waitForChanges();
- const one = await (await page.find("#one")).isVisible();
- const two = await (await page.find("#two")).isVisible();
- const three = await (await page.find("#three")).isVisible();
- expect(one).toBeFalsy();
- expect(two).toBeTruthy();
- expect(three).toBeTruthy();
- });
-
it("works correctly inside a shadowRoot", async () => {
const page = await newE2EPage();
await page.setContent(`
diff --git a/packages/calcite-components/src/components/combobox/combobox.tsx b/packages/calcite-components/src/components/combobox/combobox.tsx
index 540fdc9a1c8..54804ba4313 100644
--- a/packages/calcite-components/src/components/combobox/combobox.tsx
+++ b/packages/calcite-components/src/components/combobox/combobox.tsx
@@ -116,6 +116,17 @@ export class Combobox
*/
@Prop({ reflect: true }) clearDisabled = false;
+ /**
+ * Text for the component's filter input field.
+ */
+ @Prop({ reflect: true, mutable: true }) filterText = "";
+
+ @Watch("filterText")
+ filterTextChange(value: string): void {
+ this.updateActiveItemIndex(-1);
+ this.filterItems(value, true);
+ }
+
/**
* When `selectionMode` is `"ancestors"` or `"multiple"`, specifies the display of multiple `calcite-combobox-item` selections, where:
*
@@ -350,14 +361,14 @@ export class Combobox
await componentOnReady(this.el);
- if (!this.allowCustomValues && this.text) {
+ if (!this.allowCustomValues && this.filterText) {
this.clearInputValue();
this.filterItems("");
this.updateActiveItemIndex(-1);
}
- if (this.allowCustomValues && this.text.trim().length) {
- this.addCustomChip(this.text);
+ if (this.allowCustomValues && this.filterText.trim().length) {
+ this.addCustomChip(this.filterText);
}
this.open = false;
@@ -481,6 +492,7 @@ export class Combobox
setUpLoadableComponent(this);
this.updateItems();
await setUpMessages(this);
+ this.filterItems(this.filterText, false, false);
}
componentDidLoad(): void {
@@ -551,14 +563,6 @@ export class Combobox
@State() selectedVisibleChipsCount = 0;
- @State() text = "";
-
- /** when search text is cleared, reset active to */
- @Watch("text")
- textHandler(): void {
- this.updateActiveItemIndex(-1);
- }
-
@State() effectiveLocale: string;
@Watch("effectiveLocale")
@@ -622,7 +626,7 @@ export class Combobox
private clearInputValue(): void {
this.textInput.value = "";
- this.text = "";
+ this.filterText = "";
}
setFilteredPlacements = (): void => {
@@ -663,13 +667,13 @@ export class Combobox
case "Tab":
this.activeChipIndex = -1;
this.activeItemIndex = -1;
- if (this.allowCustomValues && this.text) {
- this.addCustomChip(this.text, true);
+ if (this.allowCustomValues && this.filterText) {
+ this.addCustomChip(this.filterText, true);
event.preventDefault();
} else if (this.open) {
this.open = false;
event.preventDefault();
- } else if (!this.allowCustomValues && this.text) {
+ } else if (!this.allowCustomValues && this.filterText) {
this.clearInputValue();
this.filterItems("");
this.updateActiveItemIndex(-1);
@@ -760,8 +764,8 @@ export class Combobox
} else if (this.activeChipIndex > -1) {
this.removeActiveChip();
event.preventDefault();
- } else if (this.allowCustomValues && this.text) {
- this.addCustomChip(this.text, true);
+ } else if (this.allowCustomValues && this.filterText) {
+ this.addCustomChip(this.filterText, true);
event.preventDefault();
} else if (!event.defaultPrevented) {
if (submitForm(this)) {
@@ -780,7 +784,7 @@ export class Combobox
if (this.activeChipIndex > -1) {
event.preventDefault();
this.removeActiveChip();
- } else if (!this.text && this.isMulti()) {
+ } else if (!this.filterText && this.isMulti()) {
event.preventDefault();
this.removeLastChip();
}
@@ -1060,11 +1064,7 @@ export class Combobox
inputHandler = (event: Event): void => {
const value = (event.target as HTMLInputElement).value;
- this.text = value;
- this.filterItems(value, true);
- if (value) {
- this.activeChipIndex = -1;
- }
+ this.filterText = value;
};
getItemsAndGroups(): ComboboxChildElement[] {
@@ -1078,11 +1078,18 @@ export class Combobox
isGroup(item) ? label === item.label : value === item.value && label === item.textLabel,
);
- return debounce((text: string, setOpenToEmptyState = false): void => {
+ return debounce((text: string, setOpenToEmptyState = false, emit = true): void => {
const filteredData = filter(this.data, text);
const itemsAndGroups = this.getItemsAndGroups();
+ const matchAll = text === "";
+
itemsAndGroups.forEach((item) => {
+ if (matchAll) {
+ item.hidden = false;
+ return;
+ }
+
const hidden = !find(item, filteredData);
item.hidden = hidden;
const [parent, grandparent] = item.ancestors;
@@ -1099,10 +1106,12 @@ export class Combobox
this.filteredItems = this.getFilteredItems();
if (setOpenToEmptyState) {
- this.open = this.text.trim().length > 0 && this.filteredItems.length > 0;
+ this.open = this.filterText.trim().length > 0 && this.filteredItems.length > 0;
}
- this.calciteComboboxFilterChange.emit();
+ if (emit) {
+ this.calciteComboboxFilterChange.emit();
+ }
}, 100);
})();
@@ -1165,7 +1174,7 @@ export class Combobox
}
getFilteredItems(): HTMLCalciteComboboxItemElement[] {
- return this.items.filter((item) => !item.hidden);
+ return this.filterText === "" ? this.items : this.items.filter((item) => !item.hidden);
}
private getSelectedItems = (): HTMLCalciteComboboxItemElement[] => {
@@ -1238,7 +1247,7 @@ export class Combobox
if (this.textInput) {
this.textInput.value = "";
}
- this.text = "";
+ this.filterText = "";
}
getItems(): HTMLCalciteComboboxItemElement[] {
@@ -1618,6 +1627,7 @@ export class Combobox
role="combobox"
tabindex={this.activeChipIndex === -1 ? 0 : -1}
type="text"
+ value={this.filterText}
/>
);