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

IgniteUI / igniteui-angular / 28011544764

23 Jun 2026 08:02AM UTC coverage: 90.139%. Remained the same
28011544764

Pull #17344

github

web-flow
Merge 0a5adb4ba into 01244911c
Pull Request #17344: feat(select): switch default to AutoPositionStrategy; export IgxSelectOverlapPositionStrategy as opt-in

14880 of 17339 branches covered (85.82%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 2 files covered. (100.0%)

4 existing lines in 1 file now uncovered.

29948 of 32393 relevant lines covered (92.45%)

34495.87 hits per line

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

95.22
/projects/igniteui-angular/select/src/select/select.component.ts
1
import { AfterContentChecked, AfterContentInit, AfterViewInit, booleanAttribute, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, HostBinding, Injector, Input, OnDestroy, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, inject, ChangeDetectionStrategy } from '@angular/core';
2
import { NgTemplateOutlet } from '@angular/common';
3
import { AbstractControl, ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
4
import { noop } from 'rxjs';
5
import { takeUntil } from 'rxjs/operators';
6

7
import {
8
    EditorProvider,
9
    IBaseCancelableBrowserEventArgs,
10
    IBaseEventArgs,
11
    AbsoluteScrollStrategy,
12
    AutoPositionStrategy,
13
    HorizontalAlignment,
14
    VerticalAlignment,
15
    OverlaySettings
16
} from 'igniteui-angular/core';
17
import { fadeIn, fadeOut } from 'igniteui-angular/animations';
18
import { IgxSelectItemComponent } from './select-item.component';
19
import { IgxSelectBase } from './select.common';
20
import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE, IgxInputGroupComponent, IgxInputDirective, IgxInputState, IgxLabelDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
21
import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from 'igniteui-angular/directives';
22
import { IgxOverlayService } from 'igniteui-angular/core';
23
import { IgxIconComponent } from 'igniteui-angular/icon';
24
import { IgxSelectItemNavigationDirective } from './select-navigation.directive';
25
import { IGX_DROPDOWN_BASE, IgxDropDownComponent, IgxDropDownItemBaseDirective, ISelectionEventArgs, Navigate } from 'igniteui-angular/drop-down';
26

27
/** @hidden @internal */
28
@Directive({
29
    selector: '[igxSelectToggleIcon]',
30
    standalone: true
31
})
32
export class IgxSelectToggleIconDirective {
3✔
33
}
34

35
/** @hidden @internal */
36
@Directive({
37
    selector: '[igxSelectHeader]',
38
    standalone: true
39
})
40
export class IgxSelectHeaderDirective {
3✔
41
}
42

43
/** @hidden @internal */
44
@Directive({
45
    selector: '[igxSelectFooter]',
46
    standalone: true
47
})
48
export class IgxSelectFooterDirective {
3✔
49
}
50

51
/**
52
 * **Ignite UI for Angular Select** -
53
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)
54
 *
55
 * The `igxSelect` provides an input with dropdown list allowing selection of a single item.
56
 *
57
 * Example:
58
 * ```html
59
 * <igx-select #select1 [placeholder]="'Pick One'">
60
 *   <label igxLabel>Select Label</label>
61
 *   <igx-select-item *ngFor="let item of items" [value]="item.field">
62
 *     {{ item.field }}
63
 *   </igx-select-item>
64
 * </igx-select>
65
 * ```
66
 */
67
@Component({
68
    selector: 'igx-select',
69
    templateUrl: './select.component.html',
70
    providers: [
71
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSelectComponent, multi: true },
72
        { provide: IGX_DROPDOWN_BASE, useExisting: IgxSelectComponent }
73
    ],
74
    styles: [`
75
        :host {
76
            display: block;
77
        }
78
    `],
79
    changeDetection: ChangeDetectionStrategy.Eager,
80
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxSelectItemNavigationDirective, IgxSuffixDirective, IgxReadOnlyInputDirective, NgTemplateOutlet, IgxIconComponent, IgxToggleDirective]
81
})
82
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
3✔
83
    AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
84
    protected overlayService = inject<IgxOverlayService>(IgxOverlayService);
1,191✔
85
    private _inputGroupType = inject<IgxInputGroupType>(IGX_INPUT_GROUP_TYPE, { optional: true });
1,191✔
86
    private _injector = inject(Injector);
1,191✔
87

88

89
    /** @hidden @internal */
90
    @ViewChild('inputGroup', { read: IgxInputGroupComponent, static: true }) public inputGroup: IgxInputGroupComponent;
91

92
    /** @hidden @internal */
93
    @ViewChild('input', { read: IgxInputDirective, static: true }) public input: IgxInputDirective;
94

95
    /** @hidden @internal */
96
    @ContentChildren(forwardRef(() => IgxSelectItemComponent), { descendants: true })
6✔
97
    public override children: QueryList<IgxSelectItemComponent>;
98

99
    @ContentChildren(IgxPrefixDirective, { descendants: true })
100
    protected prefixes: QueryList<IgxPrefixDirective>;
101

102
    @ContentChildren(IgxSuffixDirective, { descendants: true })
103
    protected suffixes: QueryList<IgxSuffixDirective>;
104

105
    @ViewChildren(IgxSuffixDirective)
106
    protected internalSuffixes: QueryList<IgxSuffixDirective>;
107

108
    /** @hidden @internal */
109
    @ContentChild(forwardRef(() => IgxLabelDirective), { static: true }) public label: IgxLabelDirective;
6✔
110

111
    /**
112
     * Sets input placeholder.
113
     *
114
     */
115
    @Input() public placeholder;
116

117

118
    /**
119
     * Disables the component.
120
     * ```html
121
     * <igx-select [disabled]="'true'"></igx-select>
122
     * ```
123
     */
124
    @Input({ transform: booleanAttribute }) public disabled = false;
1,191✔
125

126
    /**
127
     * Sets custom overlay settings for the select component.
128
     * ```html
129
     * <igx-select [overlaySettings]="customOverlaySettings"></igx-select>
130
     * ```
131
     */
132
    @Input()
133
    public overlaySettings: OverlaySettings;
134

135
    /** @hidden @internal */
136
    @HostBinding('style.maxHeight')
137
    public override maxHeight = '256px';
1,191✔
138

139
    /**
140
     * Emitted before the dropdown is opened
141
     *
142
     * ```html
143
     * <igx-select opening='handleOpening($event)'></igx-select>
144
     * ```
145
     */
146
    @Output()
147
    public override opening = new EventEmitter<IBaseCancelableBrowserEventArgs>();
1,191✔
148

149
    /**
150
     * Emitted after the dropdown is opened
151
     *
152
     * ```html
153
     * <igx-select (opened)='handleOpened($event)'></igx-select>
154
     * ```
155
     */
156
    @Output()
157
    public override opened = new EventEmitter<IBaseEventArgs>();
1,191✔
158

159
    /**
160
     * Emitted before the dropdown is closed
161
     *
162
     * ```html
163
     * <igx-select (closing)='handleClosing($event)'></igx-select>
164
     * ```
165
     */
166
    @Output()
167
    public override closing = new EventEmitter<IBaseCancelableBrowserEventArgs>();
1,191✔
168

169
    /**
170
     * Emitted after the dropdown is closed
171
     *
172
     * ```html
173
     * <igx-select (closed)='handleClosed($event)'></igx-select>
174
     * ```
175
     */
176
    @Output()
177
    public override closed = new EventEmitter<IBaseEventArgs>();
1,191✔
178

179
    /**
180
     * The custom template, if any, that should be used when rendering the select TOGGLE(open/close) button
181
     *
182
     * ```typescript
183
     * // Set in typescript
184
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
185
     * myComponent.select.toggleIconTemplate = myCustomTemplate;
186
     * ```
187
     * ```html
188
     * <!-- Set in markup -->
189
     *  <igx-select #select>
190
     *      ...
191
     *      <ng-template igxSelectToggleIcon let-collapsed>
192
     *          <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
193
     *      </ng-template>
194
     *  </igx-select>
195
     * ```
196
     */
197
    @ContentChild(IgxSelectToggleIconDirective, { read: TemplateRef })
198
    public toggleIconTemplate: TemplateRef<any> = null;
1,191✔
199

200
    /**
201
     * The custom template, if any, that should be used when rendering the HEADER for the select items list
202
     *
203
     * ```typescript
204
     * // Set in typescript
205
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
206
     * myComponent.select.headerTemplate = myCustomTemplate;
207
     * ```
208
     * ```html
209
     * <!-- Set in markup -->
210
     *  <igx-select #select>
211
     *      ...
212
     *      <ng-template igxSelectHeader>
213
     *          <div class="select__header">
214
     *              This is a custom header
215
     *          </div>
216
     *      </ng-template>
217
     *  </igx-select>
218
     * ```
219
     */
220
    @ContentChild(IgxSelectHeaderDirective, { read: TemplateRef, static: false })
221
    public headerTemplate: TemplateRef<any> = null;
1,191✔
222

223
    /**
224
     * The custom template, if any, that should be used when rendering the FOOTER for the select items list
225
     *
226
     * ```typescript
227
     * // Set in typescript
228
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
229
     * myComponent.select.footerTemplate = myCustomTemplate;
230
     * ```
231
     * ```html
232
     * <!-- Set in markup -->
233
     *  <igx-select #select>
234
     *      ...
235
     *      <ng-template igxSelectFooter>
236
     *          <div class="select__footer">
237
     *              This is a custom footer
238
     *          </div>
239
     *      </ng-template>
240
     *  </igx-select>
241
     * ```
242
     */
243
    @ContentChild(IgxSelectFooterDirective, { read: TemplateRef, static: false })
244
    public footerTemplate: TemplateRef<any> = null;
1,191✔
245

246
    @ContentChild(IgxHintDirective, { read: ElementRef }) private hintElement: ElementRef;
247

248
    /** @hidden @internal */
249
    public override width: string;
250

251
    /** @hidden @internal do not use the drop-down container class */
252
    public override cssClass = false;
1,191✔
253

254
    /** @hidden @internal */
255
    public override allowItemsFocus = false;
1,191✔
256

257
    /** @hidden @internal */
258
    public override height: string;
259

260
    private ngControl: NgControl = null;
1,191✔
261
    private _overlayDefaults: OverlaySettings;
262
    private _value: any;
263
    private _type = null;
1,191✔
264

265
    /**
266
     * Gets/Sets the component value.
267
     *
268
     * ```typescript
269
     * // get
270
     * let selectValue = this.select.value;
271
     * ```
272
     *
273
     * ```typescript
274
     * // set
275
     * this.select.value = 'London';
276
     * ```
277
     * ```html
278
     * <igx-select [value]="value"></igx-select>
279
     * ```
280
     */
281
    @Input()
282
    public get value(): any {
283
        return this._value;
9,845✔
284
    }
285
    public set value(v: any) {
286
        if (this._value === v) {
2,546✔
287
            return;
311✔
288
        }
289
        this._value = v;
2,235✔
290
        this.setSelection(this.items.find(x => x.value === this.value));
3,313✔
291
    }
292

293
    /**
294
     * Sets how the select will be styled.
295
     * The allowed values are `line`, `box` and `border`. Defaults to `box` if no input-group type is set.
296
     * ```html
297
     * <igx-select [type]="'border'"></igx-select>
298
     * ```
299
     */
300
    @Input()
301
    public get type(): IgxInputGroupType {
302
        return this._type || this._inputGroupType || 'box';
59,637✔
303
    }
304

305
    public set type(val: IgxInputGroupType) {
306
        this._type = val;
1,090✔
307
    }
308

309
    /** @hidden @internal */
310
    public get selectionValue() {
311
        const selectedItem = this.selectedItem;
29,817✔
312
        return selectedItem ? selectedItem.itemText : '';
29,817✔
313
    }
314

315
    /** @hidden @internal */
316
    public override get selectedItem(): IgxSelectItemComponent {
317
        return this.selection.first_item(this.id);
31,862✔
318
    }
319

320
    private _onChangeCallback: (_: any) => void = noop;
1,191✔
321
    private _onTouchedCallback: () => void = noop;
1,191✔
322

323
    //#region ControlValueAccessor
324

325
    /** @hidden @internal */
326
    public writeValue = (value: any) => {
1,191✔
327
        this.value = value;
2,530✔
328
    };
329

330
    /** @hidden @internal */
331
    public registerOnChange(fn: any): void {
332
        this._onChangeCallback = fn;
1,102✔
333
    }
334

335
    /** @hidden @internal */
336
    public registerOnTouched(fn: any): void {
337
        this._onTouchedCallback = fn;
1,102✔
338
    }
339

340
    /** @hidden @internal */
341
    public setDisabledState(isDisabled: boolean): void {
342
        this.disabled = isDisabled;
1,260✔
343
    }
344
    //#endregion
345

346
    /** @hidden @internal */
347
    public getEditElement(): HTMLInputElement {
348
        return this.input.nativeElement;
2,039✔
349
    }
350

351
    /** @hidden @internal */
352
    public override selectItem(newSelection: IgxDropDownItemBaseDirective, event?) {
353
        const oldSelection = this.selectedItem ?? <IgxDropDownItemBaseDirective>{};
399✔
354

355
        if (newSelection === null || newSelection.disabled || newSelection.isHeader) {
399✔
356
            return;
2✔
357
        }
358

359
        if (newSelection === oldSelection) {
397✔
360
            this.toggleDirective.close();
24✔
361
            return;
24✔
362
        }
363

364
        const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false, owner: this };
373✔
365
        this.selectionChanging.emit(args);
373✔
366

367
        if (args.cancel) {
373✔
368
            return;
111✔
369
        }
370

371
        this.setSelection(newSelection);
262✔
372
        this._value = newSelection.value;
262✔
373

374
        if (event) {
262✔
375
            this.toggleDirective.close();
111✔
376
        }
377

378
        this.cdr.detectChanges();
262✔
379
        this._onChangeCallback(this.value);
262✔
380
    }
381

382
    /** @hidden @internal */
383
    public getFirstItemElement(): HTMLElement {
384
        return this.children.first.element.nativeElement;
1✔
385
    }
386

387
    /**
388
     * Opens the select
389
     *
390
     * ```typescript
391
     * this.select.open();
392
     * ```
393
     */
394
    public override open(overlaySettings?: OverlaySettings) {
395
        if (this.disabled || this.items.length === 0) {
309✔
396
            return;
3✔
397
        }
398

399
        if (!this.selectedItem) {
306✔
400
            this.navigateFirst();
214✔
401
        }
402

403
        super.open(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
306✔
404
    }
405

406
    public inputGroupClick(event: MouseEvent, overlaySettings?: OverlaySettings) {
407
        const targetElement = event.target as HTMLElement;
234✔
408

409
        if (this.hintElement && targetElement.contains(this.hintElement.nativeElement)) {
234!
UNCOV
410
            return;
×
411
        }
412
        this.toggle(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
234✔
413
    }
414

415
    /** @hidden @internal */
416
    public ngAfterContentInit() {
417
        this._overlayDefaults = {
1,190✔
418
            target: this.getEditElement(),
419
            modal: false,
420
            positionStrategy: new AutoPositionStrategy({
421
                horizontalDirection: HorizontalAlignment.Right,
422
                verticalDirection: VerticalAlignment.Bottom,
423
                horizontalStartPoint: HorizontalAlignment.Left,
424
                verticalStartPoint: VerticalAlignment.Top,
425
                openAnimation: fadeIn,
426
                closeAnimation: fadeOut
427
            }),
428
            scrollStrategy: new AbsoluteScrollStrategy(),
429
            excludeFromOutsideClick: [this.inputGroup.element.nativeElement as HTMLElement]
430
        };
431
        const changes$ = this.children.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
1,190✔
432
            this.setSelection(this.items.find(x => x.value === this.value));
3,273✔
433
            this.cdr.detectChanges();
864✔
434
        });
435
        Promise.resolve().then(() => {
1,190✔
436
            if (!changes$.closed) {
1,190✔
437
                this.children.notifyOnChanges();
828✔
438
            }
439
        });
440
    }
441

442
    /**
443
     * Event handlers
444
     *
445
     * @hidden @internal
446
     */
447
    public handleOpening(e: ToggleViewCancelableEventArgs) {
448
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
306✔
449
        this.opening.emit(args);
306✔
450

451
        e.cancel = args.cancel;
306✔
452
        if (args.cancel) {
306!
UNCOV
453
            return;
×
454
        }
455
    }
456

457
    /** @hidden @internal */
458
    public override onToggleContentAppended(event: ToggleViewEventArgs) {
459
        const info = this.overlayService.getOverlayById(event.id);
301✔
460
        if ((info?.settings?.positionStrategy as { ownsScrollPositioning?: boolean })?.ownsScrollPositioning) {
301!
UNCOV
461
            return;
×
462
        }
463
        super.onToggleContentAppended(event);
301✔
464
    }
465

466
    /** @hidden @internal */
467
    public handleOpened() {
468
        this.updateItemFocus();
294✔
469
        this.opened.emit({ owner: this });
294✔
470
    }
471

472
    /** @hidden @internal */
473
    public handleClosing(e: ToggleViewCancelableEventArgs) {
474
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
255✔
475
        this.closing.emit(args);
255✔
476
        e.cancel = args.cancel;
255✔
477
    }
478

479
    /** @hidden @internal */
480
    public handleClosed() {
481
        this.focusItem(false);
252✔
482
        this.closed.emit({ owner: this });
252✔
483
    }
484

485
    /** @hidden @internal */
486
    public onBlur(): void {
487
        this._onTouchedCallback();
166✔
488
        if (this.ngControl && this.ngControl.invalid) {
166✔
489
            this.input.valid = IgxInputState.INVALID;
3✔
490
        } else {
491
            this.input.valid = IgxInputState.INITIAL;
163✔
492
        }
493
    }
494

495
    /** @hidden @internal */
496
    public onFocus(): void {
497
        this._onTouchedCallback();
234✔
498
    }
499

500
    /**
501
     * @hidden @internal
502
     */
503
    public override ngOnInit() {
504
        this.ngControl = this._injector.get<NgControl>(NgControl, null);
1,191✔
505
    }
506

507
    /**
508
     * @hidden @internal
509
     */
510
    public override ngAfterViewInit() {
511
        super.ngAfterViewInit();
1,190✔
512

513
        if (this.ngControl) {
1,190✔
514
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this));
1,096✔
515
            this.manageRequiredAsterisk();
1,096✔
516
        }
517

518
        this.cdr.detectChanges();
1,190✔
519
    }
520

521
    /** @hidden @internal */
522
    public ngAfterContentChecked() {
523
        if (this.inputGroup && this.prefixes?.length > 0) {
14,082✔
524
            this.inputGroup.prefixes = this.prefixes;
769✔
525
        }
526

527
        if (this.inputGroup) {
14,082✔
528
            const suffixesArray = this.suffixes?.toArray() ?? [];
14,082!
529
            const internalSuffixesArray = this.internalSuffixes?.toArray() ?? [];
14,082✔
530
            const mergedSuffixes = new QueryList<IgxSuffixDirective>();
14,082✔
531
            mergedSuffixes.reset([
14,082✔
532
                ...suffixesArray,
533
                ...internalSuffixesArray
534
            ]);
535
            this.inputGroup.suffixes = mergedSuffixes;
14,082✔
536
        }
537
    }
538

539
    /** @hidden @internal */
540
    public get toggleIcon(): string {
541
        return this.collapsed ? 'input_expand' : 'input_collapse';
29,817✔
542
    }
543

544
    /**
545
     * @hidden @internal
546
     * Prevent input blur - closing the items container on Header/Footer Template click.
547
     */
548
    public mousedownHandler(event) {
UNCOV
549
        event.preventDefault();
×
550
    }
551

552
    protected onStatusChanged() {
553
        this.manageRequiredAsterisk();
1,387✔
554

555
        if (this.ngControl && !this.ngControl.disabled && this.isTouchedOrDirty) {
1,387✔
556
            if (this.hasValidators && this.inputGroup.isFocused) {
516✔
557
                this.input.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
1!
558
            } else {
559
                // B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
560
                this.input.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
515✔
561
            }
562
        } else {
563
            this.input.valid = IgxInputState.INITIAL;
871✔
564
        }
565
    }
566

567
    private get isTouchedOrDirty(): boolean {
568
        return (this.ngControl.control.touched || this.ngControl.control.dirty);
1,197✔
569
    }
570

571
    private get hasValidators(): boolean {
572
        return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
516✔
573
    }
574

575
    protected override navigate(direction: Navigate, currentIndex?: number) {
576
        if (this.collapsed && this.selectedItem) {
327✔
577
            this.navigateItem(this.selectedItem.itemIndex);
41✔
578
        }
579
        super.navigate(direction, currentIndex);
327✔
580
    }
581

582
    protected manageRequiredAsterisk(): void {
583
        const hasRequiredHTMLAttribute = this.elementRef.nativeElement.hasAttribute('required');
2,483✔
584
        let isRequired = false;
2,483✔
585

586
        if (this.ngControl && this.ngControl.control.validator) {
2,483✔
587
            const error = this.ngControl.control.validator({} as AbstractControl);
20✔
588
            isRequired = !!(error && error.required);
20✔
589
        }
590

591
        this.inputGroup.isRequired = isRequired;
2,483✔
592

593
        if (this.input?.nativeElement) {
2,483✔
594
            this.input.nativeElement.setAttribute('aria-required', isRequired.toString());
2,483✔
595
        }
596

597
        // Handle validator removal case
598
        if (!isRequired && !hasRequiredHTMLAttribute) {
2,483✔
599
            this.input.valid = IgxInputState.INITIAL;
2,459✔
600
        }
601

602
        this.cdr.markForCheck();
2,483✔
603
    }
604

605
    private setSelection(item: IgxDropDownItemBaseDirective) {
606
        if (item && item.value !== undefined && item.value !== null) {
3,361✔
607
            this.selection.set(this.id, new Set([item]));
1,777✔
608
        } else {
609
            this.selection.clear(this.id);
1,584✔
610
        }
611
    }
612
}
613

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