Skip to content

Commit 7e85ab6

Browse files
committed
feat: Added highlight container
1 parent 690c85f commit 7e85ab6

File tree

9 files changed

+843
-8
lines changed

9 files changed

+843
-8
lines changed

src/components/common/definitions/defineAllComponents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import IgcDropdownGroupComponent from '../../dropdown/dropdown-group.js';
3030
import IgcDropdownHeaderComponent from '../../dropdown/dropdown-header.js';
3131
import IgcDropdownItemComponent from '../../dropdown/dropdown-item.js';
3232
import IgcExpansionPanelComponent from '../../expansion-panel/expansion-panel.js';
33+
import IgcHighlightComponent from '../../highlight/highlight.js';
3334
import IgcIconComponent from '../../icon/icon.js';
3435
import IgcInputComponent from '../../input/input.js';
3536
import IgcListComponent from '../../list/list.js';
@@ -102,6 +103,7 @@ const allComponents: IgniteComponent[] = [
102103
IgcDividerComponent,
103104
IgcSwitchComponent,
104105
IgcExpansionPanelComponent,
106+
IgcHighlightComponent,
105107
IgcIconComponent,
106108
IgcInputComponent,
107109
IgcListHeaderComponent,

src/components/common/util.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ export function roundByDPR(value: number): number {
379379
}
380380

381381
export function scrollIntoView(
382-
element?: HTMLElement,
382+
element?: HTMLElement | null,
383383
config?: ScrollIntoViewOptions
384384
): void {
385385
if (!element) {
@@ -516,6 +516,19 @@ export function equal<T>(a: unknown, b: T, visited = new WeakSet()): boolean {
516516
return false;
517517
}
518518

519+
/**
520+
* Escapes any potential regex syntax characters in a string, and returns a new string
521+
* that can be safely used as a literal pattern for the `RegExp()` constructor.
522+
*
523+
* @remarks
524+
* Substitute with `RegExp.escape` once it has enough support:
525+
*
526+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape#browser_compatibility
527+
*/
528+
export function escapeRegex(value: string): string {
529+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
530+
}
531+
519532
/** Required utility type for specific props */
520533
export type RequiredProps<T, K extends keyof T> = T & {
521534
[P in K]-?: T[P];

src/components/date-time-input/date-util.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { parseISODate } from '../calendar/helpers.js';
2-
import { clamp } from '../common/util.js';
2+
import { clamp, escapeRegex } from '../common/util.js';
33

44
export enum FormatDesc {
55
Numeric = 'numeric',
@@ -847,13 +847,9 @@ export abstract class DateTimeUtil {
847847
);
848848
}
849849

850-
private static escapeRegExp(string: string) {
851-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
852-
}
853-
854850
private static trimEmptyPlaceholders(value: string, prompt?: string): string {
855851
const result = value.replace(
856-
new RegExp(DateTimeUtil.escapeRegExp(prompt ?? '_'), 'g'),
852+
new RegExp(escapeRegex(prompt ?? '_'), 'g'),
857853
''
858854
);
859855
return result;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
2+
3+
import { defineComponents } from '../common/definitions/defineComponents.js';
4+
import IgcHighlightComponent from './highlight.js';
5+
6+
describe('Highlight', () => {
7+
before(() => defineComponents(IgcHighlightComponent));
8+
9+
let highlight: IgcHighlightComponent;
10+
11+
function createHighlightWithInitialMatch() {
12+
return html`<igc-highlight search-text="lorem">
13+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sapiente in
14+
recusandae aliquam placeat! Saepe hic reiciendis quae, dolorum totam ab
15+
mollitia, tempora excepturi blanditiis repellat dolore nemo cumque illum
16+
quas.
17+
</igc-highlight>`;
18+
}
19+
20+
function createHighlight() {
21+
return html`<igc-highlight>
22+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sapiente in
23+
recusandae aliquam placeat! Saepe hic reiciendis quae, dolorum totam ab
24+
mollitia, tempora excepturi blanditiis repellat dolore nemo cumque illum
25+
quas.
26+
</igc-highlight>`;
27+
}
28+
29+
describe('Initial render', () => {
30+
beforeEach(async () => {
31+
highlight = await fixture(createHighlightWithInitialMatch());
32+
});
33+
34+
it('is correctly matched', async () => {
35+
expect(highlight.size).to.equal(1);
36+
});
37+
});
38+
39+
describe('DOM', () => {
40+
beforeEach(async () => {
41+
highlight = await fixture(createHighlight());
42+
});
43+
44+
it('is defined', async () => {
45+
expect(highlight).to.not.be.undefined;
46+
});
47+
48+
it('is accessible', async () => {
49+
await expect(highlight).shadowDom.to.be.accessible();
50+
await expect(highlight).lightDom.to.be.accessible();
51+
});
52+
});
53+
54+
describe('API', () => {
55+
beforeEach(async () => {
56+
highlight = await fixture(createHighlight());
57+
});
58+
59+
it('matches on changing `search` value', async () => {
60+
expect(highlight.size).to.equal(0);
61+
62+
highlight.searchText = 'lorem';
63+
await elementUpdated(highlight);
64+
65+
expect(highlight.size).to.equal(1);
66+
67+
highlight.searchText = '';
68+
await elementUpdated(highlight);
69+
70+
expect(highlight.size).to.equal(0);
71+
});
72+
73+
it('matches with case sensitivity', async () => {
74+
highlight.caseSensitive = true;
75+
highlight.searchText = 'lorem';
76+
await elementUpdated(highlight);
77+
78+
expect(highlight.size).to.equal(0);
79+
80+
highlight.searchText = 'Lorem';
81+
await elementUpdated(highlight);
82+
83+
expect(highlight.size).to.equal(1);
84+
});
85+
86+
it('moves to the next match when `next()` is invoked', async () => {
87+
highlight.searchText = 'e';
88+
await elementUpdated(highlight);
89+
90+
expect(highlight.size).greaterThan(0);
91+
expect(highlight.current).to.equal(0);
92+
93+
highlight.next();
94+
expect(highlight.current).to.equal(1);
95+
});
96+
97+
it('moves to the previous when `previous()` is invoked', async () => {
98+
highlight.searchText = 'e';
99+
await elementUpdated(highlight);
100+
101+
expect(highlight.size).greaterThan(0);
102+
expect(highlight.current).to.equal(0);
103+
104+
// Wrap around to the last one
105+
highlight.previous();
106+
expect(highlight.current).to.equal(highlight.size - 1);
107+
});
108+
109+
it('setActive called', async () => {
110+
highlight.searchText = 'e';
111+
await elementUpdated(highlight);
112+
113+
highlight.setActive(15);
114+
expect(highlight.current).to.equal(15);
115+
});
116+
117+
it('refresh called', async () => {
118+
highlight.searchText = 'lorem';
119+
await elementUpdated(highlight);
120+
121+
expect(highlight.size).to.equal(1);
122+
123+
const node = document.createElement('div');
124+
node.textContent = 'Lorem '.repeat(9);
125+
126+
highlight.append(node);
127+
highlight.search();
128+
129+
expect(highlight.size).to.equal(10);
130+
131+
node.remove();
132+
highlight.search();
133+
134+
expect(highlight.size).to.equal(1);
135+
});
136+
});
137+
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { css, html, LitElement } from 'lit';
2+
import { property } from 'lit/decorators.js';
3+
import { registerComponent } from '../common/definitions/register.js';
4+
import {
5+
createHighlightController,
6+
type HighlightNavigation,
7+
} from './service.js';
8+
9+
/**
10+
* The highlight component provides a way for efficient searching and highlighting of
11+
* text projected into it.
12+
*
13+
* @element igc-highlight
14+
*
15+
* @slot - The default slot of the component.
16+
*
17+
* @cssproperty --resting-color - The text color for a highlighted text node.
18+
* @cssproperty --resting-background - The background color for a highlighted text node.
19+
* @cssproperty --active-color - The text color for the active highlighted text node.
20+
* @cssproperty --active-background - The background color for the active highlighted text node.
21+
*/
22+
export default class IgcHighlightComponent extends LitElement {
23+
public static readonly tagName = 'igc-highlight';
24+
25+
public static override styles = css`
26+
:host {
27+
display: contents;
28+
}
29+
`;
30+
31+
/* blazorSuppress */
32+
public static register(): void {
33+
registerComponent(IgcHighlightComponent);
34+
}
35+
36+
private readonly _service = createHighlightController(this);
37+
38+
private _caseSensitive = false;
39+
private _searchText = '';
40+
41+
/**
42+
* Whether to match the searched text with case sensitivity in mind.
43+
* @attr case-sensitive
44+
*/
45+
@property({ type: Boolean, reflect: true, attribute: 'case-sensitive' })
46+
public set caseSensitive(value: boolean) {
47+
this._caseSensitive = value;
48+
this.search();
49+
}
50+
51+
public get caseSensitive(): boolean {
52+
return this._caseSensitive;
53+
}
54+
55+
/**
56+
* The string to search and highlight in the DOM content of the component.
57+
* @attr search-text
58+
*/
59+
@property({ attribute: 'search-text' })
60+
public set searchText(value: string) {
61+
this._searchText = value;
62+
this.search();
63+
}
64+
65+
public get searchText(): string {
66+
return this._searchText;
67+
}
68+
69+
/** The number of matches. */
70+
public get size(): number {
71+
return this._service.size;
72+
}
73+
74+
/** The index of the currently active match. */
75+
public get current(): number {
76+
return this._service.current;
77+
}
78+
79+
/** Moves the active state to the next match. */
80+
public next(options?: HighlightNavigation): void {
81+
this._service.next(options);
82+
}
83+
84+
/** Moves the active state to the previous match. */
85+
public previous(options?: HighlightNavigation): void {
86+
this._service.previous(options);
87+
}
88+
89+
/** Moves the active state to the given index. */
90+
public setActive(index: number, options?: HighlightNavigation): void {
91+
this._service.setActive(index, options);
92+
}
93+
94+
/**
95+
* Executes the highlight logic again based on the current `searchText` and
96+
* `caseSensitive` values.
97+
*
98+
* Useful when the slotted content is dynamic.
99+
*/
100+
public search(): void {
101+
if (this.hasUpdated) {
102+
this._service.clear();
103+
this._service.find(this.searchText);
104+
}
105+
}
106+
107+
protected override render() {
108+
return html`<slot></slot>`;
109+
}
110+
}
111+
112+
declare global {
113+
interface HTMLElementTagNameMap {
114+
'igc-highlight': IgcHighlightComponent;
115+
}
116+
}

0 commit comments

Comments
 (0)