UNPKG

@clr/angular

Version:

Angular components for Clarity

1,215 lines (1,197 loc) 89.6 kB
import * as i8 from '@angular/common'; import { isPlatformBrowser, NgForOf, CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, ViewChild, Optional, Component, Input, Directive, PLATFORM_ID, Inject, HostListener, HostBinding, DOCUMENT, ContentChildren, EventEmitter, booleanAttribute, ContentChild, ViewChildren, Output, Self, Host, NgModule } from '@angular/core'; import * as i1$1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import * as i1 from '@clr/angular/forms/common'; import { ClrAbstractContainer, NgControlService, ControlIdService, ControlClassService, WrappedFormControl, ClrCommonFormsModule } from '@clr/angular/forms/common'; import * as i9 from '@clr/angular/icon'; import { ClarityIcons, successStandardIcon, errorStandardIcon, angleIcon, windowCloseIcon, ClrIcon } from '@clr/angular/icon'; import * as i4 from '@clr/angular/popover/common'; import { POPOVER_HOST_ORIGIN, ClrPopoverPosition, ClrPopoverType, ClrPopoverHostDirective, ClrPopoverModuleNext } from '@clr/angular/popover/common'; import * as i5 from '@clr/angular/progress/spinner'; import { ClrSpinnerModule } from '@clr/angular/progress/spinner'; import * as i3 from '@clr/angular/utils'; import { ArrowKeyDirection, Keys, customFocusableItemProvider, uniqueIdFactory, ClrLoadingState, IF_ACTIVE_ID, LoadingListener, IF_ACTIVE_ID_PROVIDER, FOCUS_SERVICE_PROVIDER, ClrConditionalModule, ClrKeyFocusModule } from '@clr/angular/utils'; import { BehaviorSubject, ReplaySubject, Subject, debounceTime } from 'rxjs'; import { take } from 'rxjs/operators'; /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ComboboxContainerService { constructor() { this.labelOffset = 0; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ComboboxContainerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ComboboxContainerService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ComboboxContainerService, decorators: [{ type: Injectable }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrComboboxContainer extends ClrAbstractContainer { constructor(layoutService, controlClassService, ngControlService, containerService, el) { super(layoutService, controlClassService, ngControlService); this.containerService = containerService; this.el = el; } ngAfterContentInit() { if (this.label) { this.containerService.labelText = this.label.labelText; } } ngAfterViewInit() { this.containerService.labelOffset = this.controlContainer.nativeElement.offsetHeight - this.el.nativeElement.offsetHeight; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrComboboxContainer, deps: [{ token: i1.LayoutService, optional: true }, { token: i1.ControlClassService }, { token: i1.NgControlService }, { token: ComboboxContainerService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrComboboxContainer, isStandalone: false, selector: "clr-combobox-container", host: { properties: { "class.clr-form-control": "true", "class.clr-combobox-form-control": "true", "class.clr-form-control-disabled": "control?.disabled", "class.clr-row": "addGrid()" } }, providers: [NgControlService, ControlIdService, ControlClassService, ComboboxContainerService], viewQueries: [{ propertyName: "controlContainer", first: true, predicate: ["controlContainer"], descendants: true }], usesInheritance: true, ngImport: i0, template: ` <ng-content select="label"></ng-content> @if (!label && addGrid()) { <label></label> } <div class="clr-control-container" [ngClass]="controlClass()" #controlContainer> <ng-content select="clr-combobox"></ng-content> @if (showHelper) { <ng-content select="clr-control-helper"></ng-content> } @if (showInvalid) { <ng-content select="clr-control-error"></ng-content> } @if (showValid) { <ng-content select="clr-control-success"></ng-content> } </div> `, isInline: true, dependencies: [{ kind: "directive", type: i8.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.ClrControlLabel, selector: "label", inputs: ["id", "for"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrComboboxContainer, decorators: [{ type: Component, args: [{ selector: 'clr-combobox-container', template: ` <ng-content select="label"></ng-content> @if (!label && addGrid()) { <label></label> } <div class="clr-control-container" [ngClass]="controlClass()" #controlContainer> <ng-content select="clr-combobox"></ng-content> @if (showHelper) { <ng-content select="clr-control-helper"></ng-content> } @if (showInvalid) { <ng-content select="clr-control-error"></ng-content> } @if (showValid) { <ng-content select="clr-control-success"></ng-content> } </div> `, host: { '[class.clr-form-control]': 'true', '[class.clr-combobox-form-control]': 'true', '[class.clr-form-control-disabled]': 'control?.disabled', '[class.clr-row]': 'addGrid()', }, providers: [NgControlService, ControlIdService, ControlClassService, ComboboxContainerService], standalone: false, }] }], ctorParameters: () => [{ type: i1.LayoutService, decorators: [{ type: Optional }] }, { type: i1.ControlClassService }, { type: i1.NgControlService }, { type: ComboboxContainerService }, { type: i0.ElementRef }], propDecorators: { controlContainer: [{ type: ViewChild, args: ['controlContainer'] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ComboboxModel { constructor() { this.identityFn = (item) => item; } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class MultiSelectComboboxModel extends ComboboxModel { containsItem(item) { if (this.model === null || this.model === undefined) { return false; } return this.model.some(m => this.identityFn(m) === this.identityFn(item)); } select(item) { this.addItem(item); } unselect(item) { this.removeItem(item); } isEmpty() { return !(this.model && this.model.length > 0); } pop() { let item; if (this.model && this.model.length > 0) { item = this.model[this.model.length - 1]; this.removeItem(item); } return item; } toString(displayField, index = -1) { let displayString = ''; if (this.model) { // If the model is array, we can use a specific item from it, to retrieve the display value. if (index > -1) { if (this.model[index]) { // If we have a defined display field, we'll use it's value as display value if (displayField && this.model[index][displayField]) { displayString += this.model[index][displayField]; } else { // If we don't have a defined display field, we'll use the toString representation of the // item as display value. displayString += this.model[index].toString(); } } } else { this.model.forEach((model) => { // If we have a defined display field, we'll use it's value as display value if (displayField && model[displayField]) { displayString += model[displayField]; } else { // If we don't have a defined display field, we'll use the toString representation of the // model as display value. displayString += model.toString(); } displayString += ' '; }); } } return displayString.trim(); } addItem(item) { if (!this.containsItem(item)) { this.model = this.model || []; this.model.push(item); } } removeItem(item) { if (this.model === null || this.model === undefined) { return; } const index = this.model.findIndex(m => this.identityFn(m) === this.identityFn(item)); if (index > -1) { this.model.splice(index, 1); } // we intentionally set the model to null for form validation if (this.model.length === 0) { this.model = null; } } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class SingleSelectComboboxModel extends ComboboxModel { containsItem(item) { if (this.model === null || this.model === undefined) { return false; } return this.identityFn(this.model) === this.identityFn(item); } select(item) { this.model = item; } unselect(item) { if (this.containsItem(item)) { this.model = null; } } isEmpty() { return !this.model; } pop() { const item = this.model; this.model = null; return item; } toString(displayField) { if (!this.model) { return ''; } if (displayField && this.model[displayField]) { return this.model[displayField]; } else { return this.model.toString(); } } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrOptionSelected { constructor(template) { this.template = template; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrOptionSelected, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrOptionSelected, isStandalone: false, selector: "[clrOptionSelected]", inputs: { selected: ["clrOptionSelected", "selected"] }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrOptionSelected, decorators: [{ type: Directive, args: [{ selector: '[clrOptionSelected]', standalone: false, }] }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { selected: [{ type: Input, args: ['clrOptionSelected'] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class PseudoFocusModel extends SingleSelectComboboxModel { constructor() { super(...arguments); this._focusChanged = new BehaviorSubject(null); } get focusChanged() { return this._focusChanged.asObservable(); } select(item) { if (this.model !== item) { this.model = item; this._focusChanged.next(item); } } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class OptionSelectionService { constructor() { this.loading = false; this.editable = false; this.showSelectAll = false; // Display all options on first open, even if filter text exists. // https://github.com/vmware-clarity/ng-clarity/issues/386 this.showAllOptions = true; this._currentInput = ''; this._inputChanged = new BehaviorSubject(''); this._selectionChanged = new ReplaySubject(1); this._selectAllRequested = new Subject(); this.editableResolver = (input) => input; this._identityFn = (item) => item; this.inputChanged = this._inputChanged.asObservable(); } get displayField() { return this._displayField; } set displayField(value) { this._displayField = value; if (this.selectionModel) { this.selectionModel.displayField = value; } } get currentInput() { return this._currentInput; } set currentInput(input) { // clear value in single selection model when input is empty if (input === '' && !this.multiselectable) { this.setSelectionValue(null); } this._currentInput = input; this._inputChanged.next(input); } // This observable is for notifying the ClrOption to update its // selection by comparing the value get selectionChanged() { return this._selectionChanged.asObservable(); } get multiselectable() { return this.selectionModel instanceof MultiSelectComboboxModel; } get identityFn() { return this._identityFn; } set identityFn(value) { this._identityFn = value || ((item) => item); if (this.selectionModel) { this.selectionModel.identityFn = this._identityFn; } } get selectAllRequested() { return this._selectAllRequested.asObservable(); } requestSelectAll() { this._selectAllRequested.next(); } select(item) { if (item === null || item === undefined || this.selectionModel.containsItem(item)) { return; } this.selectionModel.select(item); this._selectionChanged.next(this.selectionModel); } toggle(item) { if (item === null || item === undefined) { return; } if (this.selectionModel.containsItem(item)) { this.selectionModel.unselect(item); } else { this.selectionModel.select(item); } this._selectionChanged.next(this.selectionModel); } selectMany(items) { let changed = false; for (const item of items) { if (!this.selectionModel.containsItem(item)) { this.selectionModel.select(item); changed = true; } } if (changed) { this._selectionChanged.next(this.selectionModel); } } unselectMany(items) { if (!this.selectionModel || this.selectionModel.isEmpty()) { return; } let changed = false; for (const item of items) { if (this.selectionModel.containsItem(item)) { this.selectionModel.unselect(item); changed = true; } } if (changed) { this._selectionChanged.next(this.selectionModel); } } unselect(item) { if (item === null || item === undefined || !this.selectionModel.containsItem(item)) { return; } this.selectionModel.unselect(item); this._selectionChanged.next(this.selectionModel); } /** * Checks whether all given items are currently selected, using identityFn for comparison. */ containsAll(items) { if (!items.length || this.selectionModel.isEmpty()) { return false; } return items.every(item => this.selectionModel.containsItem(item)); } setSelectionValue(value) { if (!this.selectionModel) { return; } const current = this.selectionModel.model; if (this.valuesEqualByIdentity(current, value)) { return; } this.selectionModel.model = value; this._selectionChanged.next(this.selectionModel); } valuesEqualByIdentity(current, value) { if (current === value) { return true; } // Check if both are null or undefined or empty string. if ((current === null || current === undefined || current === '') && (value === null || value === undefined || value === '')) { return true; } // Check if one is null or undefined or empty string and the other is not. if (current === null || current === undefined || current === '' || value === null || value === undefined || value === '') { return false; } if (this.multiselectable) { const cur = current; const val = value; if (cur.length !== val.length) { return false; } // We only consider values equal if they are ordered the same way. const curIds = cur.map(this._identityFn); const valIds = val.map(this._identityFn); return curIds.every((id, i) => id === valIds[i]); } else { return this._identityFn(current) === this._identityFn(value); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OptionSelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OptionSelectionService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: OptionSelectionService, decorators: [{ type: Injectable }], ctorParameters: () => [] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ComboboxFocusHandler { constructor(rendererFactory, popoverService, selectionService, platformId) { this.popoverService = popoverService; this.selectionService = selectionService; this.platformId = platformId; this.pseudoFocus = new PseudoFocusModel(); this.optionData = []; this.handleFocusSubscription(); // Direct renderer injection can be problematic and leads to failing tests at least this.renderer = rendererFactory.createRenderer(null, null); } get trigger() { return this._trigger; } set trigger(el) { this._trigger = el; this.addFocusOnBlurListener(el); } get listbox() { return this._listbox; } set listbox(el) { this._listbox = el; this.addFocusOnBlurListener(el); } get textInput() { return this._textInput; } set textInput(el) { this._textInput = el; this.renderer.listen(el, 'keydown', event => !this.handleTextInput(event)); this.addFocusOnBlurListener(el); } focusInput() { if (this.textInput && isPlatformBrowser(this.platformId)) { this.textInput.focus({ preventScroll: true }); } } focusFirstActive() { if (this.optionData.length > 0) { if (this.selectionService.selectionModel.isEmpty()) { this.pseudoFocus.select(this.optionData[0]); } else { let firstActive; if (this.selectionService.multiselectable) { firstActive = this.selectionService.selectionModel.model[0]; } else { firstActive = this.selectionService.selectionModel.model; } const activeProxy = this.optionData.find(option => option.value === firstActive); if (activeProxy) { // active element is visible this.pseudoFocus.select(activeProxy); } else { // we have active element, but it's filtered out this.pseudoFocus.select(this.optionData[0]); } this.scrollIntoSelectedModel('auto'); } } } addOptionValues(options) { this.optionData = options; } focusOption(option) { this.pseudoFocus.select(option); } handleFocusSubscription() { this.popoverService.openChange.subscribe(open => { if (!open) { this.pseudoFocus.model = null; } }); } moveFocusTo(direction) { let index = this.optionData.findIndex(option => option.equals(this.pseudoFocus.model)); if (direction === ArrowKeyDirection.UP) { if (index === -1 || index === 0) { index = this.optionData.length - 1; } else { index--; } } else if (direction === ArrowKeyDirection.DOWN) { if (index === -1 || index === this.optionData.length - 1) { index = 0; } else { index++; } } this.pseudoFocus.select(this.optionData[index]); this.scrollIntoSelectedModel(); } openAndMoveTo(direction) { if (!this.popoverService.open) { this.popoverService.openChange.pipe(take(1)).subscribe(open => { if (open) { this.moveFocusTo(direction); } }); this.popoverService.open = true; } else { this.moveFocusTo(direction); } } // this service is only interested in keys that may move the focus handleTextInput(event) { let preventDefault = false; const key = event.key; if (event) { switch (key) { case Keys.Enter: if (this.popoverService.open && this.pseudoFocus.model) { if (this.selectionService.multiselectable) { if (this.pseudoFocus.model.id === SELECT_ALL_ID) { this.selectionService.requestSelectAll(); } else { this.selectionService.toggle(this.pseudoFocus.model.value); } } else { this.selectionService.select(this.pseudoFocus.model.value); } preventDefault = true; } break; case Keys.Space: if (!this.popoverService.open) { this.popoverService.open = true; preventDefault = true; } break; case Keys.ArrowUp: this.preventViewportScrolling(event); this.openAndMoveTo(ArrowKeyDirection.UP); preventDefault = true; break; case Keys.ArrowDown: this.preventViewportScrolling(event); this.openAndMoveTo(ArrowKeyDirection.DOWN); preventDefault = true; break; default: // Any other keypress if (event.key !== Keys.Tab && !(this.selectionService.multiselectable && event.key === Keys.Backspace) && !(event.key === Keys.Escape) && !this.popoverService.open) { this.popoverService.open = true; } break; } } return preventDefault; } scrollIntoSelectedModel(behavior = 'smooth') { if (this.pseudoFocus.model && this.pseudoFocus.model.el) { this.pseudoFocus.model.el.scrollIntoView({ behavior, block: 'center', inline: 'nearest' }); } } preventViewportScrolling(event) { event.preventDefault(); event.stopImmediatePropagation(); } addFocusOnBlurListener(el) { if (isPlatformBrowser(this.platformId)) { this.renderer.listen(el, 'blur', event => { if (this.focusOutOfComponent(event)) { this.popoverService.open = false; } }); } } focusOutOfComponent(event) { const target = event.relatedTarget; return !(this.textInput.contains(target) || this.trigger.contains(target) || this.listbox.contains(target)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ComboboxFocusHandler, deps: [{ token: i0.RendererFactory2 }, { token: i4.ClrPopoverService }, { token: OptionSelectionService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ComboboxFocusHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ComboboxFocusHandler, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.RendererFactory2 }, { type: i4.ClrPopoverService }, { type: OptionSelectionService }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }] }); const COMBOBOX_FOCUS_HANDLER_PROVIDER = customFocusableItemProvider(ComboboxFocusHandler); class OptionData { constructor(id, value) { this.id = id; this.value = value; } equals(other) { if (!other) { return false; } return this.id === other.id && this.value === other.value; } } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrOption { constructor(elRef, commonStrings, focusHandler, optionSelectionService) { this.elRef = elRef; this.commonStrings = commonStrings; this.focusHandler = focusHandler; this.optionSelectionService = optionSelectionService; // A proxy with only the necessary data to be used for a11y and the focus handler service. this.optionProxy = new OptionData(null, null); this.optionProxy.el = elRef.nativeElement; } get optionId() { return this._id; } set optionId(id) { this._id = id; this.optionProxy.id = this._id; } get value() { return this._value; } set value(value) { this._value = value; this.optionProxy.value = value; } get selected() { return (this.optionSelectionService.selectionModel && this.optionSelectionService.selectionModel.containsItem(this.value)); } get focusClass() { return this.focusHandler.pseudoFocus.containsItem(this.optionProxy); } ngOnInit() { if (!this._id) { this._id = 'clr-option-' + uniqueIdFactory(); this.optionProxy.id = this._id; } } onClick(event) { event.stopPropagation(); if (this.optionSelectionService.multiselectable) { this.optionSelectionService.toggle(this.value); } else { this.optionSelectionService.select(this.value); } // As the popover stays open in multi-select mode now, we have to take focus back to the input // This way we achieve two things: // - do not lose focus // - we're still able to use onBlur for "outside-click" handling this.focusHandler.focusOption(this.optionProxy); this.focusHandler.focusInput(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrOption, deps: [{ token: i0.ElementRef }, { token: i3.ClrCommonStringsService }, { token: ComboboxFocusHandler }, { token: OptionSelectionService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrOption, isStandalone: false, selector: "clr-option", inputs: { optionId: ["id", "optionId"], value: ["clrValue", "value"] }, host: { listeners: { "click": "onClick($event)" }, properties: { "class.clr-combobox-option": "true", "attr.role": "\"option\"", "attr.tabindex": "-1", "attr.id": "optionId", "class.active": "this.selected", "class.clr-focus": "this.focusClass" } }, ngImport: i0, template: ` <ng-content></ng-content> @if (selected) { <span class="clr-sr-only">{{ commonStrings.keys.comboboxSelected }}</span> } `, isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrOption, decorators: [{ type: Component, args: [{ selector: 'clr-option', template: ` <ng-content></ng-content> @if (selected) { <span class="clr-sr-only">{{ commonStrings.keys.comboboxSelected }}</span> } `, host: { '[class.clr-combobox-option]': 'true', '[attr.role]': '"option"', // Do not remove. Or click-selection will not work. '[attr.tabindex]': '-1', '[attr.id]': 'optionId', }, standalone: false, }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i3.ClrCommonStringsService }, { type: ComboboxFocusHandler }, { type: OptionSelectionService }], propDecorators: { optionId: [{ type: Input, args: ['id'] }], value: [{ type: Input, args: ['clrValue'] }], selected: [{ type: HostBinding, args: ['class.active'] }], focusClass: [{ type: HostBinding, args: ['class.clr-focus'] }], onClick: [{ type: HostListener, args: ['click', ['$event']] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ let nbOptionsComponents = 0; const SELECT_ALL_ID = 'select-all-id'; class ClrOptions { constructor(optionSelectionService, id, el, commonStrings, focusHandler, popoverService, parentHost, document) { this.optionSelectionService = optionSelectionService; this.id = id; this.el = el; this.commonStrings = commonStrings; this.focusHandler = focusHandler; this.popoverService = popoverService; this.document = document; this.loading = false; this.subscriptions = []; if (!parentHost) { throw new Error('clr-options should only be used inside of a clr-combobox'); } if (!this.optionsId) { this.optionsId = 'clr-options-' + nbOptionsComponents++; } } set selectAllBtn(value) { if (value) { this._selectAllOption = new OptionData(SELECT_ALL_ID, null); this._selectAllOption.el = value.nativeElement; } else { this._selectAllOption = null; } this.updateFocusableItems(); } get items() { return this._items; } set items(items) { this._items = items; this.updateFocusableItems(); } /** * Tests if the list of options is empty, meaning it doesn't contain any items */ get emptyOptions() { return !this.optionSelectionService.loading && this.items.length === 0; } get editable() { return this.optionSelectionService.editable; } get noResultsElementId() { return `${this.optionsId}-no-results`; } get showSelectAll() { return (this.optionSelectionService.showSelectAll && this.optionSelectionService.multiselectable && !this.optionSelectionService.loading && this.items.length > 0); } get allVisibleSelected() { if (!this.items || this.items.length === 0) { return false; } return this.optionSelectionService.containsAll(this.items.map(option => option.value)); } get isSelectAllFocused() { return this.focusHandler.pseudoFocus.model?.id === SELECT_ALL_ID; } toggleSelectAll(event = null) { if (event) { event.stopPropagation(); this.focusHandler.focusInput(); } const visibleValues = this.items.map(option => option.value); if (this.allVisibleSelected) { this.optionSelectionService.unselectMany(visibleValues); } else { this.optionSelectionService.selectMany(visibleValues); } } ngAfterViewInit() { this.focusHandler.listbox = this.el.nativeElement; this.subscriptions.push(this.items.changes.subscribe(items => { if (items.length) { setTimeout(() => { this.focusHandler.focusFirstActive(); }); } else { this.focusHandler.pseudoFocus.pop(); } }), this.optionSelectionService.selectAllRequested.subscribe(() => { this.toggleSelectAll(); })); } ngOnDestroy() { this.subscriptions.forEach(sub => sub.unsubscribe()); } searchText(input) { return this.commonStrings.parse(this.commonStrings.keys.comboboxSearching, { INPUT: input }); } loadingStateChange(state) { this.loading = state === ClrLoadingState.LOADING; } updateFocusableItems() { const focusList = []; if (this._selectAllOption) { focusList.push(this._selectAllOption); } if (this._items) { const itemOptions = this._items.map(option => option.optionProxy); focusList.push(...itemOptions); } this.focusHandler.addOptionValues(focusList); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrOptions, deps: [{ token: OptionSelectionService }, { token: IF_ACTIVE_ID }, { token: i0.ElementRef }, { token: i3.ClrCommonStringsService }, { token: ComboboxFocusHandler }, { token: i4.ClrPopoverService }, { token: POPOVER_HOST_ORIGIN, optional: true }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrOptions, isStandalone: false, selector: "clr-options", inputs: { optionsId: ["id", "optionsId"] }, host: { properties: { "class.clr-combobox-options": "true", "class.clr-combobox-options-multi": "optionSelectionService.multiselectable", "class.clr-combobox-options-hidden": "emptyOptions && editable", "attr.role": "\"listbox\"", "id": "optionsId" } }, providers: [{ provide: LoadingListener, useExisting: ClrOptions }], queries: [{ propertyName: "items", predicate: ClrOption, descendants: true }], viewQueries: [{ propertyName: "selectAllBtn", first: true, predicate: ["selectAllBtn"], descendants: true }], ngImport: i0, template: ` @if (optionSelectionService.loading) { <div class="clr-combobox-options-loading"> <clr-spinner clrInline> {{ commonStrings.keys.loading }} </clr-spinner> <span class="clr-combobox-options-text"> {{ searchText(optionSelectionService.currentInput) }} </span> </div> } @if (showSelectAll) { <div class="clr-combobox-select-all"> <button #selectAllBtn type="button" tabindex="-1" class="btn btn-link clr-combobox-select-all-btn clr-combobox-option" [class.clr-focus]="isSelectAllFocused" (click)="toggleSelectAll($event)" > {{ allVisibleSelected ? commonStrings.keys.comboboxUnselectAll : commonStrings.keys.comboboxSelectAll }} </button> </div> } <!-- Rendered if data set is empty --> @if (emptyOptions) { <div [id]="noResultsElementId" role="option"> <span class="clr-combobox-options-empty-text"> {{ commonStrings.keys.comboboxNoResults }} </span> </div> } <!--Option Groups and Options will be projected here--> <ng-content></ng-content> `, isInline: true, dependencies: [{ kind: "component", type: i5.ClrSpinner, selector: "clr-spinner", inputs: ["clrInline", "clrInverse", "clrSmall", "clrMedium"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrOptions, decorators: [{ type: Component, args: [{ selector: 'clr-options', template: ` @if (optionSelectionService.loading) { <div class="clr-combobox-options-loading"> <clr-spinner clrInline> {{ commonStrings.keys.loading }} </clr-spinner> <span class="clr-combobox-options-text"> {{ searchText(optionSelectionService.currentInput) }} </span> </div> } @if (showSelectAll) { <div class="clr-combobox-select-all"> <button #selectAllBtn type="button" tabindex="-1" class="btn btn-link clr-combobox-select-all-btn clr-combobox-option" [class.clr-focus]="isSelectAllFocused" (click)="toggleSelectAll($event)" > {{ allVisibleSelected ? commonStrings.keys.comboboxUnselectAll : commonStrings.keys.comboboxSelectAll }} </button> </div> } <!-- Rendered if data set is empty --> @if (emptyOptions) { <div [id]="noResultsElementId" role="option"> <span class="clr-combobox-options-empty-text"> {{ commonStrings.keys.comboboxNoResults }} </span> </div> } <!--Option Groups and Options will be projected here--> <ng-content></ng-content> `, providers: [{ provide: LoadingListener, useExisting: ClrOptions }], host: { '[class.clr-combobox-options]': 'true', '[class.clr-combobox-options-multi]': 'optionSelectionService.multiselectable', '[class.clr-combobox-options-hidden]': 'emptyOptions && editable', '[attr.role]': '"listbox"', '[id]': 'optionsId', }, standalone: false, }] }], ctorParameters: () => [{ type: OptionSelectionService }, { type: undefined, decorators: [{ type: Inject, args: [IF_ACTIVE_ID] }] }, { type: i0.ElementRef }, { type: i3.ClrCommonStringsService }, { type: ComboboxFocusHandler }, { type: i4.ClrPopoverService }, { type: i0.ElementRef, decorators: [{ type: Optional }, { type: Inject, args: [POPOVER_HOST_ORIGIN] }] }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }], propDecorators: { optionsId: [{ type: Input, args: ['id'] }], selectAllBtn: [{ type: ViewChild, args: ['selectAllBtn'] }], items: [{ type: ContentChildren, args: [ClrOption, { descendants: true }] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrCombobox extends WrappedFormControl { constructor(vcr, injector, control, renderer, el, optionSelectionService, commonStrings, popoverService, containerService, platformId, focusHandler, cdr, zone, container) { super(vcr, ClrComboboxContainer, injector, control, renderer, el); this.control = control; this.renderer = renderer; this.el = el; this.optionSelectionService = optionSelectionService; this.commonStrings = commonStrings; this.popoverService = popoverService; this.containerService = containerService; this.platformId = platformId; this.focusHandler = focusHandler; this.cdr = cdr; this.zone = zone; this.container = container; this.placeholder = ''; this.clrInputChange = new EventEmitter(false); this.clrOpenChange = this.popoverService.openChange; /** * This output should be used to set up a live region using aria-live and populate it with updates that reflect each combobox change. */ this.clrSelectionChange = this.optionSelectionService.selectionChanged; this.focused = false; this.popoverPosition = ClrPopoverPosition.BOTTOM_LEFT; this.index = 1; this.popoverType = ClrPopoverType.DROPDOWN; this.containerWidth = null; this.selectionExpanded = false; this.shouldCalculate = true; this.isTotalSelection = false; this.containerWidthChange = new Subject(); this._searchText = ''; if (control) { control.valueAccessor = this; } // default to SingleSelectComboboxModel, in case the optional input [ClrMulti] isn't used this.multiSelect = false; } get showSelectAll() { return this.optionSelectionService.showSelectAll; } set showSelectAll(value) { this.optionSelectionService.showSelectAll = value; } get editable() { return this.optionSelectionService.editable; } set editable(value) { this.optionSelectionService.editable = value; } set editableResolver(value) { this.optionSelectionService.editableResolver = value; } set identityFn(value) { this.optionSelectionService.identityFn = value; } get multiSelect() { return this.optionSelectionService.multiselectable; } set multiSelect(value) { if (value) { this.optionSelectionService.selectionModel = new MultiSelectComboboxModel(); } else { // in theory, setting this again should not cause errors even though we already set it in constructor, // since the initial call to writeValue (caused by [ngModel] input) should happen after this this.optionSelectionService.selectionModel = new SingleSelectComboboxModel(); } this.optionSelectionService.selectionModel.identityFn = this.optionSelectionService.identityFn; this.updateControlValue(); } // Override the id of WrappedFormControl, as we want to move it to the embedded input. // Otherwise, the label/component connection does not work and screen readers do not read the label. get id() { return this.controlIdService.id + '-combobox'; } set id(id) { super.id = id; } get searchText() { return this._searchText; } set searchText(text) { // if input text has changed since last time, fire a change event so application can react to it if (text !== this._searchText) { if (this.popoverService.open) { this.optionSelectionService.showAllOptions = false; } this._searchText = text; this.clrInputChange.emit(this.searchText); } // We need to trigger this even if unchanged, so the option-items directive will update its list // based on the "showAllOptions" variable which may have changed in the openChange subscription below. // The option-items directive does not listen to openChange, but it listens to currentInput changes. this.optionSelectionService.currentInput = this.searchText; } get openState() { return this.popoverService.open; } get multiSelectModel() { if (!this.multiSelect) { throw Error('multiSelectModel is not available in single selection context'); } return this.optionSelectionService.selectionModel.model; } get ariaControls() { return this.options?.optionsId; } get ariaOwns() { return this.options?.optionsId; } get ariaDescribedBySelection() { return 'selection-' + this.id; } get displayField() { return this.optionSelectionService.displayField; } get showAllText() { return this.commonStrings.parse(this.commonStrings.keys.comboboxShowAll, { ITEMS: this.multiSelectModel?.length.toString(), }); } get allSelectedText() { return this.commonStrings.parse(this.commonStrings.keys.comboboxAllSelected, { ITEMS: this.multiSelectModel?.length.toString(), }); } get showIndividualPills() { return !this.isTotalSelection || this.selectionExpanded; } get showTruncationToggle() { return (this.selectionExpanded || this.isTotalSelection || (this.calculatedLimit !== null && this.calculatedLimit < this.multiSelectModel.length)); } get disabled() { return this.control?.disabled; } ngAfterContentInit() { this.initializeSubscriptions(); // Initialize with preselected value if (!this.optionSelectionService.selectionModel.isEmpty()) { this.updateInputValue(this.optionSelectionService.selectionModel); } } ngAfterViewInit() { this.focusHandler.textInput = this.textbox.nativeElement; this.focusHandler.trigger = this.trigger.nativeElement; // The text input is the actual element we are wrapping // This assignment is needed by the wrapper, so it can set // the aria properties on the input element, not on the component. // We calculate on the initial load to prevent flickering this.el = this.textbox; if (this.showSelectAll) { if (this.multiSelect && this.multiSelectModel?.length > 0) { this.calculateLimit(); } this.initialiseObserver(); } } ngOnDestroy() { super.ngOnDestroy(); if (this.resizeObserver) { this.resizeObserver.disconnect(); } } clearSelection() { this.focusHandler.focusInput(); // Clear the array model directly this.optionSelectionService.setSelectionValue([]); } onKeyUp(event) { // if BACKSPACE in multiselect mode, delete the last pill if text is empty if (this.multiSelect) { const multiModel = this.optionSelectionService.selectionModel.model; switch (event.key) { case Keys.Backspace: if (!this._searchText.length) { if (multiModel && multiModel.length > 0) { const lastItem = multiModel[multiModel.length - 1]; this.control?.control.markAsTouched(); this.optionSelectionService.unselect(lastItem); }