Skip to content

Commit

Permalink
feat(autocomplete): highlight text matching the inputValue (#11155)
Browse files Browse the repository at this point in the history
**Related Issue:** #11154

## Summary

- add highlight styles to `autocomplete-item` elements
- add utility to highlight text to promote code reuse
- add story screenshot test

BEGIN_COMMIT_OVERRIDE
END_COMMIT_OVERRIDE
  • Loading branch information
driskull authored Dec 31, 2024
1 parent a1e868c commit a43982b
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
line-height: var(--calcite-font-line-height-relative-snug);
}

.text-match {
font-weight: var(--calcite-font-weight-bold);
background-color: transparent;
color: inherit;
}

:host(:hover:not([disabled])) .container {
background-color: var(--calcite-autocomplete-background-color, var(--calcite-color-foreground-2));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../../utils/interactive";
import { IconNameOrString } from "../icon/interfaces";
import { guid } from "../../utils/guid";
import { highlightText } from "../../utils/text";
import { CSS, SLOTS } from "./resources";
import { styles } from "./autocomplete-item.scss";

Expand Down Expand Up @@ -74,6 +75,13 @@ export class AutocompleteItem
/** Specifies an icon to display at the start of the component. */
@property({ reflect: true }) iconStart: IconNameOrString;

/**
* Pattern for highlighting text matches.
*
* @private
*/
@property({ reflect: true }) inputValueMatchPattern: RegExp;

/** Accessible name for the component. */
@property() label: string;

Expand Down Expand Up @@ -128,7 +136,7 @@ export class AutocompleteItem
// #region Rendering

override render(): JsxNode {
const { active, description, heading, disabled } = this;
const { active, description, heading, disabled, inputValueMatchPattern } = this;

return (
<InteractiveContainer disabled={disabled}>
Expand All @@ -143,8 +151,18 @@ export class AutocompleteItem
{this.renderIcon("start")}
<slot name={SLOTS.contentStart} />
<div class={CSS.contentCenter}>
<div class={CSS.heading}>{heading}</div>
<div class={CSS.description}>{description}</div>
<div class={CSS.heading}>
{highlightText({
text: heading,
pattern: inputValueMatchPattern,
})}
</div>
<div class={CSS.description}>
{highlightText({
text: description,
pattern: inputValueMatchPattern,
})}
</div>
</div>
<slot name={SLOTS.contentEnd} />
{this.renderIcon("end")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ export const simple = (args: AutocompleteStoryArgs): string => html`
</div>
`;

export const matchResults = (): string =>
html`<div style="width:350px; height: 600px;">
<calcite-autocomplete label="Item list" id="myAutocomplete" input-value="item" open>
<calcite-autocomplete-item-group heading="items">
<calcite-autocomplete-item label="Item one" value="one" heading="Item one"></calcite-autocomplete-item>
<calcite-autocomplete-item label="Item two" value="two" heading="Item two"></calcite-autocomplete-item>
<calcite-autocomplete-item label="Item three" value="three" heading="Item three"></calcite-autocomplete-item>
<calcite-autocomplete-item label="Item four" value="four" heading="Item four"></calcite-autocomplete-item>
<calcite-autocomplete-item
disabled
label="Item five"
value="five"
heading="Item five"
></calcite-autocomplete-item>
<calcite-autocomplete-item label="Item six" value="six" heading="Item six"></calcite-autocomplete-item>
<calcite-autocomplete-item label="Item seven" value="seven" heading="Item seven"></calcite-autocomplete-item>
</calcite-autocomplete-item-group>
</calcite-autocomplete>
</div>`;

const kitchenSinkHTML = html`
<style>
.container {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
LuminaJsx,
} from "@arcgis/lumina";
import { useWatchAttributes } from "@arcgis/components-controllers";
import { debounce } from "lodash-es";
import { debounce, escapeRegExp } from "lodash-es";
import {
FlipPlacement,
FloatingCSS,
Expand Down Expand Up @@ -130,6 +130,8 @@ export class Autocomplete

transitionEl: HTMLDivElement;

private inputValueMatchPattern: RegExp;

// #endregion

// #region State Properties
Expand Down Expand Up @@ -444,12 +446,27 @@ export class Autocomplete
this.reposition(true);
}

if (changes.has("scale") && (this.hasUpdated || this.scale !== "m")) {
let itemsAndGroupsUpdated = false;

if (changes.has("inputValue") && (this.hasUpdated || this.inputValue)) {
this.inputValueMatchPattern =
this.inputValue && new RegExp(`(${escapeRegExp(this.inputValue)})`, "i");
this.updateItems();
this.updateGroups();
itemsAndGroupsUpdated = true;
}

if (!itemsAndGroupsUpdated && changes.has("scale") && (this.hasUpdated || this.scale !== "m")) {
this.updateItems();
this.updateGroups();
itemsAndGroupsUpdated = true;
}

if (changes.has("activeIndex") && (this.hasUpdated || this.activeIndex !== -1)) {
if (
!itemsAndGroupsUpdated &&
changes.has("activeIndex") &&
(this.hasUpdated || this.activeIndex !== -1)
) {
this.updateItems();
}
}
Expand Down Expand Up @@ -591,6 +608,7 @@ export class Autocomplete

item.active = isActive;
item.scale = this.scale;
item.inputValueMatchPattern = this.inputValueMatchPattern;
});

this.activeDescendant = activeDescendant;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ ul:focus {
color: theme("backgroundColor.brand");
}

.filter-match {
.text-match {
font-weight: var(--calcite-font-weight-bold);
color: var(--calcite-color-text-1);
background-color: var(--calcite-color-foreground-current);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Scale, SelectionMode } from "../interfaces";
import { getIconScale, warnIfMissingRequiredProp } from "../../utils/component";
import { IconNameOrString } from "../icon/interfaces";
import { slotChangeHasContent } from "../../utils/dom";
import { highlightText } from "../../utils/text";
import { CSS, SLOTS } from "./resources";
import { styles } from "./combobox-item.scss";

Expand Down Expand Up @@ -241,7 +242,16 @@ export class ComboboxItem extends LitElement implements InteractiveComponent {
}

override render(): JsxNode {
const { disabled, heading, label, textLabel, value } = this;
const {
disabled,
heading,
label,
textLabel,
value,
filterTextMatchPattern,
description,
shortHeading,
} = this;
const isSingleSelect = isSingleLike(this.selectionMode);
const icon = disabled || isSingleSelect ? undefined : "check";
const selectionIcon = isSingleSelect ? "bullet-point" : "check";
Expand Down Expand Up @@ -274,13 +284,28 @@ export class ComboboxItem extends LitElement implements InteractiveComponent {
{this.renderSelectIndicator(selectionIcon)}
{this.renderIcon(icon)}
<div class={CSS.centerContent}>
<div class={CSS.title}>{this.renderTextContent(headingText)}</div>
{this.description ? (
<div class={CSS.description}>{this.renderTextContent(this.description)}</div>
<div class={CSS.title}>
{highlightText({
text: headingText,
pattern: filterTextMatchPattern,
})}
</div>
{description ? (
<div class={CSS.description}>
{highlightText({
text: description,
pattern: filterTextMatchPattern,
})}
</div>
) : null}
</div>
{this.shortHeading ? (
<div class={CSS.shortText}>{this.renderTextContent(this.shortHeading)}</div>
{shortHeading ? (
<div class={CSS.shortText}>
{highlightText({
text: shortHeading,
pattern: filterTextMatchPattern,
})}
</div>
) : null}
<slot name={SLOTS.contentEnd} />
</li>
Expand All @@ -290,22 +315,5 @@ export class ComboboxItem extends LitElement implements InteractiveComponent {
);
}

private renderTextContent(text: string): string | (string | JsxNode)[] {
const pattern = this.filterTextMatchPattern;

if (!pattern || !text) {
return text;
}

const parts: (string | JsxNode)[] = text.split(pattern);

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

return parts;
}

// #endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const CSS = {
container: "container",
custom: "icon--custom",
description: "description",
filterMatch: "filter-match",
icon: "icon",
iconSelected: "icon--selected",
label: "label",
Expand Down
22 changes: 22 additions & 0 deletions packages/calcite-components/src/utils/text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { h, JsxNode } from "@arcgis/lumina";

export function highlightText({
text,
pattern,
}: {
text: string;
pattern: RegExp;
}): string | (string | JsxNode)[] {
if (!pattern || !text) {
return text;
}

const parts: (string | JsxNode)[] = text.split(pattern);

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

return parts;
}

0 comments on commit a43982b

Please sign in to comment.