• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 7098961215

05 Dec 2023 09:50AM UTC coverage: 97.383% (+0.1%) from 97.251%
7098961215

push

github

web-flow
feat: Refactor toggle directive into popover component (#983)

* refactor(dropdown): Migrated to popover component
* extracted a base option-like class for dropdown item and select item
* utility classes refactoring
* refactor(select): Use the underlying popover
* Improved behavior to mimic the native select
* WAI-ARIA improvements
* refactor(dropdown): Detached target behavior
* refactor: Moved keybindings to handleEvent pattern
* chore: Improved storybook samples
* refactor: RootClickControllerConfig as type
Co-authored-by: Simeon Simeonoff <sim.simeonoff@gmail.com>

3281 of 3501 branches covered (0.0%)

Branch coverage included in aggregate %.

1514 of 1532 new or added lines in 16 files covered. (98.83%)

4 existing lines in 2 files now uncovered.

20569 of 20990 relevant lines covered (97.99%)

432.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.19
/src/components/select/select.ts
1
import { html } from 'lit';
32✔
2
import {
32✔
3
  property,
32✔
4
  query,
32✔
5
  queryAssignedElements,
32✔
6
  state,
32✔
7
} from 'lit/decorators.js';
32✔
8
import { ifDefined } from 'lit/directives/if-defined.js';
32✔
9

32✔
10
import IgcSelectGroupComponent from './select-group.js';
32✔
11
import IgcSelectHeaderComponent from './select-header.js';
32✔
12
import IgcSelectItemComponent from './select-item.js';
32✔
13
import { styles } from './themes/select.base.css.js';
32✔
14
import { all } from './themes/themes.js';
32✔
15
import { themeSymbol, themes } from '../../theming/theming-decorator.js';
32✔
16
import type { Theme } from '../../theming/types.js';
32✔
17
import {
32✔
18
  addKeybindings,
32✔
19
  altKey,
32✔
20
  arrowDown,
32✔
21
  arrowLeft,
32✔
22
  arrowRight,
32✔
23
  arrowUp,
32✔
24
  endKey,
32✔
25
  enterKey,
32✔
26
  escapeKey,
32✔
27
  homeKey,
32✔
28
  spaceBar,
32✔
29
  tabKey,
32✔
30
} from '../common/controllers/key-bindings.js';
32✔
31
import { addRootClickHandler } from '../common/controllers/root-click.js';
32✔
32
import { addRootScrollHandler } from '../common/controllers/root-scroll.js';
32✔
33
import { alternateName } from '../common/decorators/alternateName.js';
32✔
34
import { blazorAdditionalDependencies } from '../common/decorators/blazorAdditionalDependencies.js';
32✔
35
import { blazorSuppress } from '../common/decorators/blazorSuppress.js';
32✔
36
import { watch } from '../common/decorators/watch.js';
32✔
37
import { registerComponent } from '../common/definitions/register.js';
32✔
38
import {
32✔
39
  IgcBaseComboBoxLikeComponent,
32✔
40
  getActiveItems,
32✔
41
  getItems,
32✔
42
  getNextActiveItem,
32✔
43
  getPreviousActiveItem,
32✔
44
  setInitialSelectionState,
32✔
45
} from '../common/mixins/combo-box.js';
32✔
46
import type { Constructor } from '../common/mixins/constructor.js';
32✔
47
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
32✔
48
import { FormAssociatedRequiredMixin } from '../common/mixins/form-associated-required.js';
32✔
49
import { partNameMap } from '../common/util.js';
32✔
50
import { Validator, requiredValidator } from '../common/validators.js';
32✔
51
import IgcIconComponent from '../icon/icon.js';
32✔
52
import IgcInputComponent from '../input/input.js';
32✔
53
import IgcPopoverComponent, { type IgcPlacement } from '../popover/popover.js';
32✔
54

32✔
55
export interface IgcSelectEventMap {
32✔
56
  igcChange: CustomEvent<IgcSelectItemComponent>;
32✔
57
  igcBlur: CustomEvent<void>;
32✔
58
  igcFocus: CustomEvent<void>;
32✔
59
  igcOpening: CustomEvent<void>;
32✔
60
  igcOpened: CustomEvent<void>;
32✔
61
  igcClosing: CustomEvent<void>;
32✔
62
  igcClosed: CustomEvent<void>;
32✔
63
}
32✔
64

32✔
65
/**
32✔
66
 * Represents a control that provides a menu of options.
32✔
67
 *
32✔
68
 * @element igc-select
32✔
69
 *
32✔
70
 * @slot - Renders the list of select items.
32✔
71
 * @slot prefix - Renders content before the input.
32✔
72
 * @slot suffix - Renders content after input.
32✔
73
 * @slot header - Renders a container before the list of options.
32✔
74
 * @slot footer - Renders a container after the list of options.
32✔
75
 * @slot helper-text - Renders content below the input.
32✔
76
 * @slot toggle-icon - Renders content inside the suffix container.
32✔
77
 * @slot toggle-icon-expanded - Renders content for the toggle icon when the component is in open state.
32✔
78
 *
32✔
79
 * @fires igcFocus - Emitted when the select gains focus.
32✔
80
 * @fires igcBlur - Emitted when the select loses focus.
32✔
81
 * @fires igcChange - Emitted when the control's checked state changes.
32✔
82
 * @fires igcOpening - Emitted just before the list of options is opened.
32✔
83
 * @fires igcOpened - Emitted after the list of options is opened.
32✔
84
 * @fires igcClosing - Emitter just before the list of options is closed.
32✔
85
 * @fires igcClosed - Emitted after the list of options is closed.
32✔
86
 *
32✔
87
 * @csspart list - The list of options wrapper.
32✔
88
 * @csspart input - The encapsulated igc-input.
32✔
89
 * @csspart label - The encapsulated text label.
32✔
90
 * @csspart prefix - The prefix wrapper.
32✔
91
 * @csspart suffix - The suffix wrapper.
32✔
92
 * @csspart toggle-icon - The toggle icon wrapper.
32✔
93
 * @csspart helper-text - The helper text wrapper.
32✔
94
 */
32✔
95
@themes(all, true)
32✔
96
@blazorAdditionalDependencies(
32✔
97
  'IgcIconComponent, IgcInputComponent, IgcPopoverComponent, IgcSelectGroupComponent, IgcSelectHeaderComponent, IgcSelectItemComponent'
32✔
98
)
32✔
99
export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
32✔
100
  EventEmitterMixin<
32✔
101
    IgcSelectEventMap,
32✔
102
    Constructor<IgcBaseComboBoxLikeComponent>
32✔
103
  >(IgcBaseComboBoxLikeComponent)
32✔
104
) {
32✔
105
  public static readonly tagName = 'igc-select';
32✔
106
  public static styles = styles;
32✔
107

32✔
108
  public static register() {
32✔
109
    registerComponent(
2✔
110
      this,
2✔
111
      IgcIconComponent,
2✔
112
      IgcInputComponent,
2✔
113
      IgcPopoverComponent,
2✔
114
      IgcSelectGroupComponent,
2✔
115
      IgcSelectHeaderComponent,
2✔
116
      IgcSelectItemComponent
2✔
117
    );
2✔
118
  }
2✔
119

32✔
120
  private declare readonly [themeSymbol]: Theme;
32✔
121
  private _value!: string;
32✔
122
  private _searchTerm = '';
32✔
123
  private _lastKeyTime = 0;
32✔
124

32✔
125
  private _rootClickController = addRootClickHandler(this, {
32✔
126
    hideCallback: () => this._hide(true),
32✔
127
  });
32✔
128

32✔
129
  private _rootScrollController = addRootScrollHandler(this, {
32✔
130
    hideCallback: () => this._hide(true),
32✔
131
  });
32✔
132

32✔
133
  private get isMaterialTheme() {
32✔
134
    return this[themeSymbol] === 'material';
374✔
135
  }
374✔
136

32✔
137
  private get _activeItems() {
32✔
138
    return Array.from(
34✔
139
      getActiveItems<IgcSelectItemComponent>(
34✔
140
        this,
34✔
141
        IgcSelectItemComponent.tagName
34✔
142
      )
34✔
143
    );
34✔
144
  }
34✔
145

32✔
146
  @state()
32✔
147
  protected _selectedItem: IgcSelectItemComponent | null = null;
32✔
148

32✔
149
  @state()
32✔
150
  protected _activeItem!: IgcSelectItemComponent;
32✔
151

32✔
152
  protected override validators: Validator<this>[] = [requiredValidator];
32✔
153

32✔
154
  @query(IgcInputComponent.tagName, true)
32✔
155
  protected input!: IgcInputComponent;
32✔
156

32✔
157
  @queryAssignedElements({ slot: 'helper-text' })
32✔
158
  protected helperText!: Array<HTMLElement>;
32✔
159

32✔
160
  @queryAssignedElements({ slot: 'suffix' })
32✔
161
  protected inputSuffix!: Array<HTMLElement>;
32✔
162

32✔
163
  @queryAssignedElements({ slot: 'prefix' })
32✔
164
  protected inputPrefix!: Array<HTMLElement>;
32✔
165

32✔
166
  @queryAssignedElements({ slot: 'toggle-icon-expanded' })
32✔
167
  protected _expandedIconSlot!: Array<HTMLElement>;
32✔
168

32✔
169
  protected get hasExpandedIcon() {
32✔
170
    return this._expandedIconSlot.length > 0;
241✔
171
  }
241✔
172

32✔
173
  protected get hasPrefixes() {
32✔
174
    return this.inputPrefix.length > 0;
187✔
175
  }
187✔
176

32✔
177
  protected get hasSuffixes() {
32✔
178
    return this.inputSuffix.length > 0;
187✔
179
  }
187✔
180

32✔
181
  protected get hasHelperText() {
32✔
182
    return this.helperText.length > 0;
187✔
183
  }
187✔
184

32✔
185
  /**
32✔
186
   * The value attribute of the control.
32✔
187
   * @attr
32✔
188
   */
32✔
189
  @property()
32✔
190
  public get value(): string {
32✔
191
    return this._value;
643✔
192
  }
643✔
193

32✔
194
  public set value(value: string) {
32✔
195
    this._updateValue(value);
21✔
196
    const item = this.getItem(this._value);
21✔
197
    item ? this.setSelectedItem(item) : this.clearSelectedItem();
21✔
198
  }
21✔
199

32✔
200
  /**
32✔
201
   * The outlined attribute of the control.
32✔
202
   * @attr
32✔
203
   */
32✔
204
  @property({ reflect: true, type: Boolean })
32✔
205
  public outlined = false;
32✔
206

32✔
207
  /**
32✔
208
   * The autofocus attribute of the control.
32✔
209
   * @attr
32✔
210
   */
32✔
211
  @property({ type: Boolean })
32✔
212
  public override autofocus!: boolean;
32✔
213

32✔
214
  /**
32✔
215
   * The distance of the select dropdown from its input.
32✔
216
   * @attr
32✔
217
   */
32✔
218
  @property({ type: Number })
32✔
219
  public distance = 0;
32✔
220

32✔
221
  /**
32✔
222
   * The label attribute of the control.
32✔
223
   * @attr
32✔
224
   */
32✔
225
  @property()
32✔
226
  public label!: string;
32✔
227

32✔
228
  /**
32✔
229
   * The placeholder attribute of the control.
32✔
230
   * @attr
32✔
231
   */
32✔
232
  @property()
32✔
233
  public placeholder!: string;
32✔
234

32✔
235
  /** The preferred placement of the select dropdown around its input.
32✔
236
   * @type {'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'right' | 'right-start' | 'right-end' | 'left' | 'left-start' | 'left-end'}
32✔
237
   * @attr
32✔
238
   */
32✔
239
  @property()
32✔
240
  public placement: IgcPlacement = 'bottom-start';
32✔
241

32✔
242
  /**
32✔
243
   * @deprecated since version 4.3.0
32✔
244
   * @hidden @internal @private
32✔
245
   */
32✔
246
  public positionStrategy: 'absolute' | 'fixed' = 'fixed';
32✔
247

32✔
248
  /**
32✔
249
   * Determines the behavior of the component during scrolling of the parent container.
32✔
250
   * @attr scroll-strategy
32✔
251
   */
32✔
252
  @property({ attribute: 'scroll-strategy' })
32✔
253
  public scrollStrategy: 'scroll' | 'block' | 'close' = 'scroll';
32✔
254

32✔
255
  /**
32✔
256
   * Whether the dropdown's width should be the same as the target's one.
32✔
257
   * @deprecated since version 4.3.0
32✔
258
   * @hidden @internal @private
32✔
259
   * @attr same-width
32✔
260
   */
32✔
261
  @property({ type: Boolean, attribute: 'same-width' })
32✔
262
  public sameWidth = true;
32✔
263

32✔
264
  /**
32✔
265
   * Whether the component should be flipped to the opposite side of the target once it's about to overflow the visible area.
32✔
266
   * When true, once enough space is detected on its preferred side, it will flip back.
32✔
267
   * @deprecated since version 4.3.0
32✔
268
   * @hidden @internal @private
32✔
269
   * @attr
32✔
270
   */
32✔
271
  @property({ type: Boolean })
32✔
272
  public flip = true;
32✔
273

32✔
274
  /** Returns the items of the igc-select component. */
32✔
275
  public get items() {
32✔
276
    return Array.from(
280✔
277
      getItems<IgcSelectItemComponent>(this, IgcSelectItemComponent.tagName)
280✔
278
    );
280✔
279
  }
280✔
280

32✔
281
  /** Returns the groups of the igc-select component. */
32✔
282
  public get groups() {
32✔
283
    return Array.from(
4✔
284
      getItems<IgcSelectGroupComponent>(this, IgcSelectGroupComponent.tagName)
4✔
285
    );
4✔
286
  }
4✔
287

32✔
288
  /** Returns the selected item from the dropdown or null.  */
32✔
289
  public get selectedItem() {
32✔
290
    return this._selectedItem;
254✔
291
  }
254✔
292

32✔
293
  @watch('scrollStrategy', { waitUntilFirstUpdate: true })
32✔
294
  protected scrollStrategyChanged() {
32✔
295
    this._rootScrollController.update({ resetListeners: true });
2✔
296
  }
2✔
297

32✔
298
  @watch('open', { waitUntilFirstUpdate: true })
32✔
299
  protected openChange() {
32✔
300
    this._rootClickController.update();
48✔
301
    this._rootScrollController.update();
48✔
302
  }
48✔
303

32✔
304
  constructor() {
32✔
305
    super();
72✔
306

72✔
307
    addKeybindings(this, {
72✔
308
      skip: () => this.disabled,
72✔
309
      bindingDefaults: { preventDefault: true, triggers: ['keydownRepeat'] },
72✔
310
    })
72✔
311
      .set([altKey, arrowDown], this.altArrowDown)
72✔
312
      .set([altKey, arrowUp], this.altArrowUp)
72✔
313
      .set(arrowDown, this.onArrowDown)
72✔
314
      .set(arrowUp, this.onArrowUp)
72✔
315
      .set(arrowLeft, this.onArrowUp)
72✔
316
      .set(arrowRight, this.onArrowDown)
72✔
317
      .set(tabKey, this.onTabKey, { preventDefault: false })
72✔
318
      .set(escapeKey, this.onEscapeKey)
72✔
319
      .set(homeKey, this.onHomeKey)
72✔
320
      .set(endKey, this.onEndKey)
72✔
321
      .set(spaceBar, this.onSpaceBarKey)
72✔
322
      .set(enterKey, this.onEnterKey);
72✔
323

72✔
324
    this.addEventListener('keydown', this.handleSearch);
72✔
325
    this.addEventListener('focusin', this.handleFocusIn);
72✔
326
    this.addEventListener('focusout', this.handleFocusOut);
72✔
327
  }
72✔
328

32✔
329
  protected override async firstUpdated() {
32✔
330
    await this.updateComplete;
72✔
331
    const selected = setInitialSelectionState(this.items);
72✔
332

72✔
333
    if (this.value && !selected) {
72✔
334
      this._selectItem(this.getItem(this.value), false);
1✔
335
    }
1✔
336

72✔
337
    if (selected && selected.value !== this.value) {
72✔
338
      this._defaultValue = selected.value;
3✔
339
      this._selectItem(selected, false);
3✔
340
    }
3✔
341

72✔
342
    if (this.autofocus) {
72✔
343
      this.focus();
1✔
344
    }
1✔
345

72✔
346
    this.updateValidity();
72✔
347
  }
72✔
348

32✔
349
  private handleFocusIn({ relatedTarget }: FocusEvent) {
32✔
350
    this._dirty = true;
34✔
351

34✔
352
    if (this.contains(relatedTarget as Node) || this.open) {
34✔
353
      return;
28✔
354
    }
28✔
355

6✔
356
    this.emitEvent('igcFocus');
6✔
357
  }
6✔
358

32✔
359
  private handleFocusOut({ relatedTarget }: FocusEvent) {
32✔
360
    if (this.contains(relatedTarget as Node)) {
34✔
361
      return;
14✔
362
    }
14✔
363

20✔
364
    this.checkValidity();
20✔
365
    this.emitEvent('igcBlur');
20✔
366
  }
20✔
367

32✔
368
  private handleClick(event: MouseEvent) {
32✔
369
    const item = event.target as IgcSelectItemComponent;
8✔
370
    if (this._activeItems.includes(item)) {
8✔
371
      this._selectItem(item);
3✔
372
    }
3✔
373
  }
8✔
374

32✔
375
  private handleChange(item: IgcSelectItemComponent) {
32✔
376
    return this.emitEvent('igcChange', { detail: item });
28✔
377
  }
28✔
378

32✔
379
  private handleSearch(event: KeyboardEvent) {
32✔
380
    if (!/^.$/u.test(event.key)) {
93✔
381
      return;
71✔
382
    }
71✔
383

22✔
384
    event.preventDefault();
22✔
385
    const now = Date.now();
22✔
386

22✔
387
    if (now - this._lastKeyTime > 500) {
93✔
388
      this._searchTerm = '';
7✔
389
    }
7✔
390

22✔
391
    this._lastKeyTime = now;
22✔
392
    this._searchTerm += event.key.toLocaleLowerCase();
22✔
393

22✔
394
    const item = this._activeItems.find(
22✔
395
      (item) =>
22✔
396
        item.textContent
80✔
397
          ?.trim()
80✔
398
          .toLocaleLowerCase()
80✔
399
          .startsWith(this._searchTerm)
80✔
400
    );
22✔
401

22✔
402
    if (item) {
93✔
403
      this.open ? this.activateItem(item) : this._selectItem(item);
10✔
404
    }
10✔
405
  }
93✔
406

32✔
407
  private onEnterKey() {
32✔
408
    this.open && this._activeItem
5✔
409
      ? this._selectItem(this._activeItem)
5✔
410
      : this.handleAnchorClick();
5✔
411
  }
5✔
412

32✔
413
  private onSpaceBarKey() {
32✔
414
    if (!this.open) {
2✔
415
      this.handleAnchorClick();
1✔
416
    }
1✔
417
  }
2✔
418

32✔
419
  private onArrowDown() {
32✔
420
    const item = getNextActiveItem(this.items, this._activeItem);
45✔
421
    this.open ? this._navigateToActiveItem(item) : this._selectItem(item);
45✔
422
  }
45✔
423

32✔
424
  private onArrowUp() {
32✔
425
    const item = getPreviousActiveItem(this.items, this._activeItem);
11✔
426
    this.open ? this._navigateToActiveItem(item) : this._selectItem(item);
11✔
427
  }
11✔
428

32✔
429
  private altArrowDown() {
32✔
430
    if (!this.open) {
1✔
431
      this._show(true);
1✔
432
    }
1✔
433
  }
1✔
434

32✔
435
  private async altArrowUp() {
32✔
436
    if (this.open && (await this._hide(true))) {
1✔
437
      this.input.focus();
1✔
438
    }
1✔
439
  }
1✔
440

32✔
441
  protected async onEscapeKey() {
32✔
442
    if (await this._hide(true)) {
2✔
443
      this.input.focus();
1✔
444
    }
1✔
445
  }
2✔
446

32✔
447
  private onTabKey(event: KeyboardEvent) {
32✔
448
    if (this.open) {
2✔
449
      event.preventDefault();
2✔
450
      this._selectItem(this._activeItem);
2✔
451
      this._hide(true);
2✔
452
    }
2✔
453
  }
2✔
454

32✔
455
  protected onHomeKey() {
32✔
456
    const item = this._activeItems.at(0);
2✔
457
    this.open ? this._navigateToActiveItem(item) : this._selectItem(item);
2✔
458
  }
2✔
459

32✔
460
  protected onEndKey() {
32✔
461
    const item = this._activeItems.at(-1);
2✔
462
    this.open ? this._navigateToActiveItem(item) : this._selectItem(item);
2✔
463
  }
2✔
464

32✔
465
  /** Monitor input slot changes and request update */
32✔
466
  protected inputSlotChanged() {
32✔
467
    this.requestUpdate();
10✔
468
  }
10✔
469

32✔
470
  private activateItem(item: IgcSelectItemComponent) {
32✔
471
    if (this._activeItem) {
83✔
472
      this._activeItem.active = false;
53✔
473
    }
53✔
474

83✔
475
    this._activeItem = item;
83✔
476
    this._activeItem.active = true;
83✔
477
  }
83✔
478

32✔
479
  private setSelectedItem(item: IgcSelectItemComponent) {
32✔
480
    if (this._selectedItem) {
48✔
481
      this._selectedItem.selected = false;
15✔
482
    }
15✔
483

48✔
484
    this._selectedItem = item;
48✔
485
    this._selectedItem.selected = true;
48✔
486

48✔
487
    return this._selectedItem;
48✔
488
  }
48✔
489

32✔
490
  private _selectItem(item?: IgcSelectItemComponent, emit = true) {
32✔
491
    if (!item) {
43!
NEW
492
      this.clearSelectedItem();
×
NEW
493
      this._updateValue();
×
NEW
494
      return null;
×
NEW
495
    }
×
496

43✔
497
    const items = this.items;
43✔
498
    const [previous, current] = [
43✔
499
      items.indexOf(this._selectedItem!),
43✔
500
      items.indexOf(item),
43✔
501
    ];
43✔
502

43✔
503
    if (previous === current) {
43✔
504
      return this._selectedItem;
7✔
505
    }
7✔
506

36✔
507
    const newItem = this.setSelectedItem(item);
36✔
508
    this.activateItem(newItem);
36✔
509
    this._updateValue(newItem.value);
36✔
510

36✔
511
    if (emit) this.handleChange(newItem);
43✔
512
    if (emit && this.open) this.input.focus();
43✔
513
    if (emit && !this.keepOpenOnSelect) this._hide(true);
43✔
514

36✔
515
    return this._selectedItem;
36✔
516
  }
36✔
517

32✔
518
  private _navigateToActiveItem(item?: IgcSelectItemComponent) {
32✔
519
    if (item) {
44✔
520
      this.activateItem(item);
44✔
521
      item.scrollIntoView({ behavior: 'auto', block: 'nearest' });
44✔
522
    }
44✔
523
  }
44✔
524

32✔
525
  private _updateValue(value?: string) {
32✔
526
    this._value = value as string;
60✔
527
    this.setFormValue(this._value ? this._value : null);
60✔
528
    this.updateValidity();
60✔
529
    this.setInvalidState();
60✔
530
  }
60✔
531

32✔
532
  private clearSelectedItem() {
32✔
533
    if (this._selectedItem) {
12✔
534
      this._selectedItem.selected = false;
6✔
535
    }
6✔
536
    this._selectedItem = null;
12✔
537
  }
12✔
538

32✔
539
  protected getItem(value: string) {
32✔
540
    return this.items.find((item) => item.value === value);
31✔
541
  }
31✔
542

32✔
543
  private _stopPropagation(e: Event) {
32✔
544
    e.stopPropagation();
28✔
545
  }
28✔
546

32✔
547
  /** Sets focus on the component. */
32✔
548
  @alternateName('focusComponent')
32✔
549
  public override focus(options?: FocusOptions) {
32✔
550
    this.input.focus(options);
3✔
551
  }
3✔
552

32✔
553
  /** Removes focus from the component. */
32✔
554
  @alternateName('blurComponent')
32✔
555
  public override blur() {
32✔
556
    this.input.blur();
1✔
557
    super.blur();
1✔
558
  }
1✔
559

32✔
560
  /** Checks the validity of the control and moves the focus to it if it is not valid. */
32✔
561
  public override reportValidity() {
32✔
562
    const valid = super.reportValidity();
4✔
563
    if (!valid) this.input.focus();
4✔
564
    return valid;
4✔
565
  }
4✔
566

32✔
567
  /** Navigates to the item with the specified value. If it exists, returns the found item, otherwise - null. */
32✔
568
  public navigateTo(value: string): IgcSelectItemComponent | null;
32✔
569
  /** Navigates to the item at the specified index. If it exists, returns the found item, otherwise - null. */
32✔
570
  public navigateTo(index: number): IgcSelectItemComponent | null;
32✔
571
  /** Navigates to the specified item. If it exists, returns the found item, otherwise - null. */
32✔
572
  @blazorSuppress()
32✔
573
  public navigateTo(value: string | number): IgcSelectItemComponent | null {
32✔
574
    const item =
7✔
575
      typeof value === 'string' ? this.getItem(value) : this.items[value];
7✔
576

7✔
577
    if (item) {
7✔
578
      this._navigateToActiveItem(item);
4✔
579
    }
4✔
580

7✔
581
    return item ?? null;
7✔
582
  }
7✔
583

32✔
584
  /** Selects the item with the specified value. If it exists, returns the found item, otherwise - null. */
32✔
585
  public select(value: string): IgcSelectItemComponent | null;
32✔
586
  /** Selects the item at the specified index. If it exists, returns the found item, otherwise - null. */
32✔
587
  public select(index: number): IgcSelectItemComponent | null;
32✔
588
  /** Selects the specified item. If it exists, returns the found item, otherwise - null. */
32✔
589
  @blazorSuppress()
32✔
590
  public select(value: string | number): IgcSelectItemComponent | null {
32✔
591
    const item =
8✔
592
      typeof value === 'string' ? this.getItem(value) : this.items[value];
8✔
593
    return item ? this._selectItem(item, false) : null;
8✔
594
  }
8✔
595

32✔
596
  /**  Resets the current value and selection of the component. */
32✔
597
  public clearSelection() {
32✔
598
    this._updateValue();
3✔
599
    this.clearSelectedItem();
3✔
600
  }
3✔
601

32✔
602
  protected renderInputSlots() {
32✔
603
    const prefixName = this.hasPrefixes ? 'prefix' : '';
187✔
604
    const suffixName = this.hasSuffixes ? 'suffix' : '';
187✔
605

187✔
606
    return html`
187✔
607
      <span slot=${prefixName}>
187✔
608
        <slot name="prefix" @slotchange=${this.inputSlotChanged}></slot>
187✔
609
      </span>
187✔
610

187✔
611
      <span slot=${suffixName}>
187✔
612
        <slot name="suffix" @slotchange=${this.inputSlotChanged}></slot>
187✔
613
      </span>
187✔
614
    `;
187✔
615
  }
187✔
616

32✔
617
  protected renderToggleIcon() {
32✔
618
    const parts = partNameMap({ 'toggle-icon': true, filled: this.value! });
187✔
619
    const iconHidden = this.open && this.hasExpandedIcon;
187✔
620
    const iconExpandedHidden = !this.hasExpandedIcon || !this.open;
187✔
621

187✔
622
    const openIcon = this.isMaterialTheme
187✔
623
      ? 'keyboard_arrow_up'
187!
624
      : 'arrow_drop_up';
187✔
625
    const closeIcon = this.isMaterialTheme
187✔
626
      ? 'keyboard_arrow_down'
187!
627
      : 'arrow_drop_down';
187✔
628

187✔
629
    return html`
187✔
630
      <span slot="suffix" part=${parts} aria-hidden="true">
187✔
631
        <slot
187✔
632
          name="toggle-icon"
187✔
633
          ?hidden=${iconHidden}
187✔
634
          @slotchange=${this.inputSlotChanged}
187✔
635
        >
187✔
636
          <igc-icon
187✔
637
            name=${this.open ? openIcon : closeIcon}
187✔
638
            collection="internal"
187✔
639
          ></igc-icon>
187✔
640
        </slot>
187✔
641
        <slot
187✔
642
          name="toggle-icon-expanded"
187✔
643
          ?hidden=${iconExpandedHidden}
187✔
644
          @slotchange=${this.inputSlotChanged}
187✔
645
        ></slot>
187✔
646
      </span>
187✔
647
    `;
187✔
648
  }
187✔
649

32✔
650
  protected renderHelperText() {
32✔
651
    return html`
187✔
652
      <div
187✔
653
        id="helper-text"
187✔
654
        part="helper-text"
187✔
655
        slot="anchor"
187✔
656
        ?hidden=${!this.hasHelperText}
187✔
657
      >
187✔
658
        <slot name="helper-text" @slotchange=${this.inputSlotChanged}></slot>
187✔
659
      </div>
187✔
660
    `;
187✔
661
  }
187✔
662

32✔
663
  protected renderInputAnchor() {
32✔
664
    const value = this.selectedItem?.textContent?.trim();
187✔
665

187✔
666
    return html`
187✔
667
      <igc-input
187✔
668
        id="input"
187✔
669
        slot="anchor"
187✔
670
        role="combobox"
187✔
671
        readonly
187✔
672
        aria-controls="dropdown"
187✔
673
        aria-describedby="helper-text"
187✔
674
        aria-expanded=${this.open ? 'true' : 'false'}
187✔
675
        exportparts="container: input, input: native-input, label, prefix, suffix"
187✔
676
        tabIndex=${this.disabled ? -1 : 0}
187✔
677
        value=${ifDefined(value)}
187✔
678
        placeholder=${ifDefined(this.placeholder)}
187✔
679
        label=${ifDefined(this.label)}
187✔
680
        .disabled=${this.disabled}
187✔
681
        .required=${this.required}
187✔
682
        .invalid=${this.invalid}
187✔
683
        .outlined=${this.outlined}
187✔
684
        @click=${this.handleAnchorClick}
187✔
685
        @igcFocus=${this._stopPropagation}
187✔
686
        @igcBlur=${this._stopPropagation}
187✔
687
      >
187✔
688
        ${this.renderInputSlots()} ${this.renderToggleIcon()}
187✔
689
      </igc-input>
187✔
690

187✔
691
      ${this.renderHelperText()}
187✔
692
    `;
187✔
693
  }
187✔
694

32✔
695
  protected renderDropdown() {
32✔
696
    return html`<div part="base" .inert=${!this.open}>
187✔
697
      <div
187✔
698
        id="dropdown"
187✔
699
        role="listbox"
187✔
700
        part="list"
187✔
701
        aria-labelledby="input"
187✔
702
        @click=${this.handleClick}
187✔
703
      >
187✔
704
        <slot name="header"></slot>
187✔
705
        <slot></slot>
187✔
706
        <slot name="footer"></slot>
187✔
707
      </div>
187✔
708
    </div>`;
187✔
709
  }
187✔
710

32✔
711
  protected override render() {
32✔
712
    return html`<igc-popover
187✔
713
      ?open=${this.open}
187✔
714
      flip
187✔
715
      shift
187✔
716
      same-width
187✔
717
      strategy="fixed"
187✔
718
      .offset=${this.distance}
187✔
719
      .placement=${this.placement}
187✔
720
      >${this.renderInputAnchor()} ${this.renderDropdown()}
187✔
721
    </igc-popover>`;
187✔
722
  }
187✔
723
}
32✔
724

32✔
725
declare global {
32✔
726
  interface HTMLElementTagNameMap {
32✔
727
    'igc-select': IgcSelectComponent;
32✔
728
  }
32✔
729
}
32✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc