@clr/angular
Version:
Angular components for Clarity
1,215 lines (1,197 loc) • 89.6 kB
JavaScript
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);
}