diff --git a/packages/calcite-components/src/components/combobox/combobox.e2e.ts b/packages/calcite-components/src/components/combobox/combobox.e2e.ts index b20ae4a6fad..05ddf88d38e 100644 --- a/packages/calcite-components/src/components/combobox/combobox.e2e.ts +++ b/packages/calcite-components/src/components/combobox/combobox.e2e.ts @@ -2219,12 +2219,15 @@ describe("calcite-combobox", () => { it("should allow enter unknown tag when tabbing away", async () => { const page = await newE2EPage(); await page.setContent(html` - - - - - - +
+ + + + + + +
+
`); const chip = await page.find("calcite-combobox >>> calcite-chip"); const eventSpy = await page.spyOnEvent("calciteComboboxChange"); @@ -3338,5 +3341,36 @@ describe("calcite-combobox", () => { }, }); }); + + describe("no-matches", () => { + themed( + async () => { + const page = await newE2EPage(); + await page.setContent(` + + + + + `); + + const combobox = await page.find("calcite-combobox"); + combobox.setProperty("filterText", "Oak"); + await page.waitForChanges(); + await page.waitForTimeout(DEBOUNCE.filter); + + return { tag: "calcite-combobox", page }; + }, + { + "--calcite-combobox-background-color": { + shadowSelector: `.${CSS.noMatches}`, + targetProp: "backgroundColor", + }, + "--calcite-combobox-input-text-color": { + shadowSelector: `.${CSS.noMatches} >>> mark`, + targetProp: "color", + }, + }, + ); + }); }); }); diff --git a/packages/calcite-components/src/components/combobox/combobox.scss b/packages/calcite-components/src/components/combobox/combobox.scss index 6980026544b..a4033dc2d33 100644 --- a/packages/calcite-components/src/components/combobox/combobox.scss +++ b/packages/calcite-components/src/components/combobox/combobox.scss @@ -13,6 +13,15 @@ * @prop --calcite-combobox-input-text-color: When `selectionDisplay` is `"single"`, specifies the text color of the component's input. */ +// AUTO-GENERATED — do not modify. Changes will be overwritten. +// +// Internal CSS custom properties for component use only. Overwriting is not recommended. +// +// --calcite-internal-close-size +// --calcite-internal-combobox-input-margin-block +// --calcite-internal-combobox-spacing-unit-l +// --calcite-internal-combobox-spacing-unit-s + :host { @apply relative block; } @@ -250,6 +259,20 @@ calcite-chip { z-index: var(--calcite-z-index-sticky); } +.no-matches { + padding-block: var(--calcite-internal-combobox-spacing-unit-s); + padding-inline: var(--calcite-internal-combobox-spacing-unit-l); + + color: var(--calcite-combobox-input-text-color, var(--calcite-color-text-1)); + background: var(--calcite-combobox-background-color, var(--calcite-color-foreground-1)); + cursor: pointer; +} + +.no-matches-placeholder { + color: var(--calcite-combobox-icon-color, var(--calcite-color-text-3)); + cursor: default; +} + @include disabled(); @include x-button( $background-color: "var(--calcite-close-background-color, var(--calcite-color-foreground-2))", @@ -259,6 +282,7 @@ calcite-chip { @include form-validation-message(); @include hidden-form-input(); @include base-component(); +@include text-highlight-item(); ::slotted(calcite-combobox-item-group:not(:first-child)) { padding-block-start: var(--calcite-internal-combobox-spacing-unit-l); diff --git a/packages/calcite-components/src/components/combobox/combobox.stories.ts b/packages/calcite-components/src/components/combobox/combobox.stories.ts index b3fd698ce55..2a65bd0dbbf 100644 --- a/packages/calcite-components/src/components/combobox/combobox.stories.ts +++ b/packages/calcite-components/src/components/combobox/combobox.stories.ts @@ -1030,6 +1030,35 @@ export const withDescriptionShortLabelAndContentSlots = (): string => html` `; + +export const noMatchesScaledOrAddCustomValue = (): string => html` +
+
+ + + + + + + + + + + + + + +
+ +
+ + + + +
+
+`; + withDescriptionShortLabelAndContentSlots.args = { selectionMode: ["single", "multiple"], }; diff --git a/packages/calcite-components/src/components/combobox/combobox.tsx b/packages/calcite-components/src/components/combobox/combobox.tsx index 6b7003688ef..779d150cda8 100644 --- a/packages/calcite-components/src/components/combobox/combobox.tsx +++ b/packages/calcite-components/src/components/combobox/combobox.tsx @@ -56,6 +56,7 @@ import { useT9n } from "../../controllers/useT9n"; import type { Chip } from "../chip/chip"; import type { ComboboxItemGroup as HTMLCalciteComboboxItemGroupElement } from "../combobox-item-group/combobox-item-group"; import type { ComboboxItem as HTMLCalciteComboboxItemElement } from "../combobox-item/combobox-item"; +import { highlightText } from "../../utils/text"; import type { Label } from "../label/label"; import { useSetFocus } from "../../controllers/useSetFocus"; import { useCancelable } from "../../controllers/useCancelable"; @@ -142,6 +143,8 @@ export class Combobox } }); + this.noMatchesFound = this.filteredItems.length === 0 && !!this.filterText; + this.filterTextMatchPattern = this.filterText && new RegExp(`(${escapeRegExp(this.filterText)})`, "i"); @@ -150,7 +153,7 @@ export class Combobox }); if (setOpenToEmptyState) { - this.open = this.filterText.trim().length > 0 && this.keyboardNavItems.length > 0; + this.open = this.filterText.trim().length > 0; } if (emit) { @@ -247,6 +250,29 @@ export class Combobox private focusSetter = useSetFocus()(this); + private get effectiveFilterProps(): string[] { + if (!this.filterProps) { + return ["description", "label", "metadata", "shortHeading", "textLabel"]; + } + + return this.filterProps.filter((prop) => prop !== "el"); + } + + private get showingInlineIcon(): boolean { + const { placeholderIcon, selectionMode, selectedItems, open } = this; + const selectedItem = selectedItems[0]; + const selectedIcon = selectedItem?.icon; + const singleSelectionMode = isSingleLike(selectionMode); + + return !open && selectedItem + ? !!selectedIcon && singleSelectionMode + : !!placeholderIcon && (!selectedItem || singleSelectionMode); + } + + private customChipAddHandler = (): void => { + this.addCustomChip(this.filterText, true); + }; + //#endregion //#region State Properties @@ -288,6 +314,8 @@ export class Combobox return filteredItems; } + @state() noMatchesFound: boolean; + //#endregion //#region Public Properties @@ -636,29 +664,10 @@ export class Combobox //#region Private Methods - private get effectiveFilterProps(): string[] { - if (!this.filterProps) { - return ["description", "label", "metadata", "shortHeading", "textLabel"]; - } - - return this.filterProps.filter((prop) => prop !== "el"); - } - private emitComboboxChange(): void { this.calciteComboboxChange.emit(); } - private get showingInlineIcon(): boolean { - const { placeholderIcon, selectionMode, selectedItems, open } = this; - const selectedItem = selectedItems[0]; - const selectedIcon = selectedItem?.icon; - const singleSelectionMode = isSingleLike(selectionMode); - - return !open && selectedItem - ? !!selectedIcon && singleSelectionMode - : !!placeholderIcon && (!selectedItem || singleSelectionMode); - } - private filterTextChange(value: string): void { this.updateActiveItemIndex(-1); this.filterItems(value, true); @@ -1789,13 +1798,15 @@ export class Combobox } private renderFloatingUIContainer(): JsxNode { - const { setFloatingEl, setContainerEl, open, scale } = this; + const { messages, setFloatingEl, setContainerEl, open, scale } = this; const classes = { [CSS.listContainer]: true, [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: open, }; + const label = (this.filterText && messages.add?.replace("{text}", `${this.filterText}`)) ?? ""; + return (
@@ -1807,16 +1818,35 @@ export class Combobox class={CSS.selectAll} id={`${this.guid}-select-all-enabled-interactive`} indeterminate={this.indeterminate} - label={this.messages.selectAll} + label={messages.selectAll} ref={this.selectAllComboboxItemReferenceEl} scale={scale} selected={this.allSelected} tabIndex="-1" - text-label={this.messages.selectAll} + text-label={messages.selectAll} value="select-all" /> )} + {this.noMatchesFound && + (this.allowCustomValues ? ( +
  • + {highlightText({ + text: label, + pattern: new RegExp(`(${escapeRegExp(this.filterText)})`, "i"), + })} +
  • + ) : ( +
  • + {messages.noMatches} +
  • + ))}
    diff --git a/packages/calcite-components/src/components/combobox/resources.ts b/packages/calcite-components/src/components/combobox/resources.ts index 90414cbdf0f..4135247240a 100644 --- a/packages/calcite-components/src/components/combobox/resources.ts +++ b/packages/calcite-components/src/components/combobox/resources.ts @@ -12,6 +12,8 @@ export const CSS = { label: "label", labelIcon: "label--icon", listContainer: "list-container", + noMatches: "no-matches", + noMatchesPlaceholder: "no-matches-placeholder", placeholderIcon: "placeholder-icon", selectAll: "select-all", selectionDisplayFit: "selection-display--fit", diff --git a/packages/calcite-components/src/custom-theme/combobox.ts b/packages/calcite-components/src/custom-theme/combobox.ts index 4a1241dc696..d56ba5c8194 100644 --- a/packages/calcite-components/src/custom-theme/combobox.ts +++ b/packages/calcite-components/src/custom-theme/combobox.ts @@ -38,3 +38,10 @@ export const comboboxWithPlaceHolderIcon = html` `; + +export const noMatches = html` + + + + +`;