Skip to content

Commit

Permalink
feat(combobox): highlight filter matches (#9425)
Browse files Browse the repository at this point in the history
**Related Issue:** #9026

## Summary

Adds highlighting to combobox for the first occurrence of an item's
label that matches the filter text.

### Next steps

For consistency, we can update `list` with similar functionality.
  • Loading branch information
jcfranco committed Jun 24, 2024
1 parent e094bdf commit 0d538c8
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 14 deletions.
8 changes: 8 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,10 @@ export namespace Components {
* When `true`, omits the component from the `calcite-combobox` filtered search results.
*/
"filterDisabled": boolean;
/**
* Pattern for highlighting filter text matches.
*/
"filterTextMatchPattern": RegExp;
/**
* The `id` attribute of the component. When omitted, a globally unique identifier is used.
*/
Expand Down Expand Up @@ -9081,6 +9085,10 @@ declare namespace LocalJSX {
* When `true`, omits the component from the `calcite-combobox` filtered search results.
*/
"filterDisabled"?: boolean;
/**
* Pattern for highlighting filter text matches.
*/
"filterTextMatchPattern"?: RegExp;
/**
* The `id` attribute of the component. When omitted, a globally unique identifier is used.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/calcite-components/src/components/alert/alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ $border-style: 1px solid var(--calcite-color-border-3);
inset-block-start: -2px;
block-size: 2px;
border-radius: var(--calcite-border-radius) var(--calcite-border-radius) 0 0;
&:after {
&::after {
@apply absolute
top-0
block;
Expand Down Expand Up @@ -299,7 +299,7 @@ $alertDurations:
}
}

.container.focused .dismiss-progress:after {
.container.focused .dismiss-progress::after {
animation-play-state: paused;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,9 @@ ul:focus {
:host(:hover[disabled]) .icon {
@apply opacity-100;
}

.filter-match {
font-weight: var(--calcite-font-weight-bold);
color: var(--calcite-color-text-1);
background-color: var(--calcite-color-foreground-current);
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
/** The component's text. */
@Prop({ reflect: true }) textLabel!: string;

/**
* Pattern for highlighting filter text matches.
*
* @internal
*/
@Prop({ reflect: true }) filterTextMatchPattern: RegExp;

/** The component's value. */
@Prop() value!: any;

Expand Down Expand Up @@ -252,12 +259,27 @@ export class ComboboxItem implements ConditionalSlotComponent, InteractiveCompon
<li class={classes} id={this.guid} onClick={this.itemClickHandler}>
{this.renderSelectIndicator(showDot, iconPath)}
{this.renderIcon(iconPath)}
<span class="title">{this.textLabel}</span>
<span class="title">{this.renderTextContent()}</span>
</li>
{this.renderChildren()}
</div>
</InteractiveContainer>
</Host>
);
}

private renderTextContent(): string | (string | VNode)[] {
if (!this.filterTextMatchPattern) {
return this.textLabel;
}

const parts: (string | VNode)[] = this.textLabel.split(this.filterTextMatchPattern);

if (parts.length > 1) {
// we only highlight the first match
parts[1] = <mark class={CSS.filterMatch}>{parts[1]}</mark>;
}

return parts;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const CSS = {
selected: "label--selected",
title: "title",
textContainer: "text-container",
filterMatch: "filter-match",
};
Original file line number Diff line number Diff line change
Expand Up @@ -811,3 +811,28 @@ export const readOnlyAllModes = (): string => html`
</calcite-combobox-item>
</calcite-combobox>
`;

export const filterHighlighting = (): string => html`
<calcite-combobox filter-text="Susan" max-items="6" open>
<calcite-combobox-item value="Trees" text-label="Trees">
<calcite-combobox-item value="Pine" text-label="Pine">
<calcite-combobox-item value="Pine Nested" text-label="Pine Nested"></calcite-combobox-item>
</calcite-combobox-item>
<calcite-combobox-item value="Sequoia" disabled text-label="Sequoia"></calcite-combobox-item>
<calcite-combobox-item value="Douglas Fir" text-label="Douglas Fir"></calcite-combobox-item>
</calcite-combobox-item>
<calcite-combobox-item value="Flowers" text-label="Flowers">
<calcite-combobox-item value="Daffodil" text-label="Daffodil"></calcite-combobox-item>
<calcite-combobox-item value="Black Eyed Susan" text-label="Black Eyed Susan"></calcite-combobox-item>
<calcite-combobox-item value="Nasturtium" text-label="Nasturtium"></calcite-combobox-item>
</calcite-combobox-item>
<calcite-combobox-item value="Animals" text-label="Animals">
<calcite-combobox-item value="Birds" text-label="Birds"></calcite-combobox-item>
<calcite-combobox-item value="Reptiles" text-label="Reptiles"></calcite-combobox-item>
<calcite-combobox-item value="Amphibians" text-label="Amphibians"></calcite-combobox-item>
</calcite-combobox-item>
<calcite-combobox-item value="Rocks" text-label="Rocks"></calcite-combobox-item>
<calcite-combobox-item value="Insects" text-label="Insects"></calcite-combobox-item>
<calcite-combobox-item value="Rivers" text-label="Rivers"></calcite-combobox-item>
</calcite-combobox>
`;
12 changes: 10 additions & 2 deletions packages/calcite-components/src/components/combobox/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
VNode,
Watch,
} from "@stencil/core";
import { debounce } from "lodash-es";
import { debounce, escapeRegExp } from "lodash-es";
import { calciteSize48 } from "@esri/calcite-design-tokens/dist/es6/core.js";
import { filter } from "../../utils/filter";
import { getElementWidth, getTextWidth, toAriaBoolean } from "../../utils/dom";
Expand Down Expand Up @@ -529,9 +529,11 @@ export class Combobox
//
//--------------------------------------------------------------------------

@Element() el: HTMLCalciteComboboxElement;

private allSelectedIndicatorChipEl: HTMLCalciteChipElement;

@Element() el: HTMLCalciteComboboxElement;
private filterTextMatchPattern: RegExp;

placement: LogicalPlacement = defaultMenuPlacement;

Expand Down Expand Up @@ -1103,7 +1105,13 @@ export class Combobox
}
});

this.filterTextMatchPattern =
this.filterText && new RegExp(`(${escapeRegExp(this.filterText)})`, "i");

this.filteredItems = this.getFilteredItems();
this.filteredItems.forEach((item) => {
item.filterTextMatchPattern = this.filterTextMatchPattern;
});

if (setOpenToEmptyState) {
this.open = this.filterText.trim().length > 0 && this.filteredItems.length > 0;
Expand Down
18 changes: 9 additions & 9 deletions packages/calcite-components/src/components/link/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ You can programmatically focus a `calcite-link` with the `setFocus()` method:

## Properties

| Property | Attribute | Description | Type | Default |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------- |
| `disabled` | `disabled` | When `true`, interaction is prevented and the component is displayed with lower opacity. | `boolean` | `false` |
| Property | Attribute | Description | Type | Default |
| ------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------- |
| `disabled` | `disabled` | When `true`, interaction is prevented and the component is displayed with lower opacity. | `boolean` | `false` |
| `download` | `download` | Prompts the user to save the linked URL instead of navigating to it. Can be used with or without a value: Without a value, the browser will suggest a filename/extension See <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-download>. | `boolean \| string` | `false` |
| `href` | `href` | Specifies the URL of the linked resource, which can be set as an absolute or relative path. | `string` | `undefined` |
| `iconEnd` | `icon-end` | Specifies an icon to display at the end of the component. | `string` | `undefined` |
| `iconFlipRtl` | `icon-flip-rtl` | Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). | `"both" \| "end" \| "start"` | `undefined` |
| `iconStart` | `icon-start` | Specifies an icon to display at the start of the component. | `string` | `undefined` |
| `rel` | `rel` | Specifies the relationship to the linked document defined in `href`. | `string` | `undefined` |
| `target` | `target` | Specifies the frame or window to open the linked document. | `string` | `undefined` |
| `href` | `href` | Specifies the URL of the linked resource, which can be set as an absolute or relative path. | `string` | `undefined` |
| `iconEnd` | `icon-end` | Specifies an icon to display at the end of the component. | `string` | `undefined` |
| `iconFlipRtl` | `icon-flip-rtl` | Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). | `"both" \| "end" \| "start"` | `undefined` |
| `iconStart` | `icon-start` | Specifies an icon to display at the start of the component. | `string` | `undefined` |
| `rel` | `rel` | Specifies the relationship to the linked document defined in `href`. | `string` | `undefined` |
| `target` | `target` | Specifies the frame or window to open the linked document. | `string` | `undefined` |

## Methods

Expand Down

0 comments on commit 0d538c8

Please sign in to comment.