@progress/kendo-angular-buttons
Version:
Buttons Package for Angular
338 lines (337 loc) • 13.7 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, Input, HostBinding, ContentChildren, QueryList, EventEmitter, Output, HostListener, Renderer2, ElementRef, NgZone } from '@angular/core';
import { Subscription } from 'rxjs';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { ChipComponent } from './chip.component';
import { closest, getStylingClasses } from '../util';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { isDocumentAvailable, Keys } from '@progress/kendo-angular-common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
export class ChipListComponent {
localizationService;
renderer;
element;
ngZone;
hostClass = true;
orientation = 'horizontal';
/**
* @hidden
*/
direction;
/**
* Sets the selection mode of the ChipList.
*
* The available values are:
* * `none` (default)
* * `single`
* * `multiple`
*/
selection = 'none';
/**
* The size property specifies the gap between the Chips in the ChipList
* ([see example]({% slug appearance_chiplist %}#toc-size)).
*
* The possible values are:
* * `'small'`
* * `'medium'` (default)
* * `'large'`
* * `none`
*/
set size(size) {
const sizeValue = size ? size : 'medium';
this.handleClasses(sizeValue, 'size');
this.chips?.forEach(chip => this.setChipSize(chip, sizeValue));
this._size = sizeValue;
}
get size() {
return this._size;
}
/**
* Fires each time when the ChipList selection is changed.
*/
selectedChange = new EventEmitter();
/**
* Fires each time the user clicks on the remove icon of the Chip.
*/
remove = new EventEmitter();
chips;
get single() {
return this.selection === 'single';
}
get multiple() {
return this.selection === 'multiple';
}
/**
* @hidden
*/
role = 'listbox';
dynamicRTLSubscription;
_size = 'medium';
subs = new Subscription();
_navigable = true;
/**
* @hidden
*/
onClick($event) {
const target = $event.target;
const isRemoveClicked = closest(target, '.k-chip-remove-action');
const clickedChip = closest(target, '.k-chip');
const chip = this.chips.find((chip) => clickedChip === chip.element.nativeElement);
this.currentActiveIndex = this.chips.toArray().indexOf(chip);
chip && chip.focus();
if (chip && this.navigable) {
this.chips.forEach((c) => {
this.renderer.setAttribute(c.element.nativeElement, 'tabindex', '-1');
});
this.renderer.setAttribute(chip.element.nativeElement, 'tabindex', '0');
}
if (isRemoveClicked && clickedChip) {
const removeEventArgs = { sender: this, originalEvent: $event, removedChip: chip };
this.remove.emit(removeEventArgs);
}
if (this.selection !== 'none' && clickedChip && !isRemoveClicked) {
this.setSelection(chip);
}
}
/**
* By default, keyboard navigation is available through arrow keys and roving tabindex.
* When set to `false`, all chips are part of the default tabbing sequence of the page.
*
* @default true
*/
set navigable(value) {
this._navigable = value;
this.chips?.forEach(c => this.renderer.setAttribute(c.element.nativeElement, 'tabindex', value ? '-1' : '0'));
this.chips?.first && this.renderer.setAttribute(this.chips.first.element.nativeElement, 'tabindex', '0');
}
get navigable() {
return this._navigable;
}
currentActiveIndex = 0;
constructor(localizationService, renderer, element, ngZone) {
this.localizationService = localizationService;
this.renderer = renderer;
this.element = element;
this.ngZone = ngZone;
validatePackage(packageMetadata);
this.direction = localizationService.rtl ? 'rtl' : 'ltr';
}
ngOnInit() {
this.dynamicRTLSubscription = this.localizationService.changes
.subscribe(({ rtl }) => this.direction = rtl ? 'rtl' : 'ltr');
}
ngAfterViewInit() {
const stylingInputs = ['size'];
stylingInputs.forEach(input => {
this.handleClasses(this[input], input);
});
this.attachElementEventHandlers();
this.updateChips();
}
ngAfterContentInit() {
this.subs.add(this.chips?.changes.subscribe(() => this.updateChips()));
}
ngOnDestroy() {
if (this.dynamicRTLSubscription) {
this.dynamicRTLSubscription.unsubscribe();
}
this.subs.unsubscribe();
}
selectedChips() {
return this.chips.reduce((acc, cur, idx) => { return cur.selected ? acc.concat(idx) : acc; }, []);
}
/**
* Updates the selection on click of a Chip. Emits events.
*/
setSelection(chip) {
if (this.selection === 'single') {
this.clearSelection(chip);
}
chip.selected = !chip.selected;
const chipEl = chip.element.nativeElement;
this.renderer.setAttribute(chipEl, 'aria-selected', `${chip.selected}`);
this.selectedChange.emit(this.selectedChips());
}
clearSelection(chip) {
this.chips.forEach((c) => {
if (chip !== c) {
c.selected = false;
this.renderer.setAttribute(c.element.nativeElement, 'aria-selected', 'false');
}
});
}
handleClasses(value, input) {
const elem = this.element.nativeElement;
const classes = getStylingClasses('chip-list', input, this[input], value);
if (classes.toRemove) {
this.renderer.removeClass(elem, classes.toRemove);
}
if (classes.toAdd) {
this.renderer.addClass(elem, classes.toAdd);
}
}
attachElementEventHandlers() {
const chiplist = this.element.nativeElement;
this.ngZone.runOutsideAngular(() => {
this.subs.add(this.renderer.listen(chiplist, 'keydown', this.keyDownHandler.bind(this)));
});
}
keyDownHandler(e) {
const isEnterOrSpace = e.keyCode === Keys.Enter || e.keyCode === Keys.Space;
const isDeleteOrBackspace = e.keyCode === Keys.Delete || e.keyCode === Keys.Backspace;
const isLeftArrow = e.keyCode === Keys.ArrowLeft;
const isRightArrow = e.keyCode === Keys.ArrowRight;
if (isEnterOrSpace) {
const target = e.target;
const clickedChip = closest(target, '.k-chip');
const chip = this.chips.find((chip) => clickedChip === chip.element.nativeElement);
this.currentActiveIndex = this.chips.toArray().findIndex((chip) => clickedChip === chip.element.nativeElement);
if (this.selection !== 'none' && clickedChip) {
this.ngZone.run(() => {
this.setSelection(chip);
});
}
}
else if (isDeleteOrBackspace) {
const target = e.target;
const clickedChip = closest(target, '.k-chip');
const chip = this.chips.find((chip) => clickedChip === chip.element.nativeElement);
if (clickedChip) {
const removeEventArgs = { sender: this, originalEvent: e, removedChip: chip };
this.ngZone.run(() => {
this.remove.emit(removeEventArgs);
});
}
}
else if (isLeftArrow) {
this.handleArrowKeys('left');
}
else if (isRightArrow) {
this.handleArrowKeys('right');
}
}
handleArrowKeys(direction) {
if (!this.navigable) {
return;
}
const directionDelta = direction === 'left' ? -1 : 1;
this.currentActiveIndex = this.currentActiveIndex + directionDelta;
if (this.currentActiveIndex >= this.chips.length) {
this.currentActiveIndex = 0;
}
else if (this.currentActiveIndex < 0) {
this.currentActiveIndex = this.chips.length - 1;
}
this.chips.forEach((chip, idx) => {
this.renderer.setAttribute(chip.element.nativeElement, 'tabindex', '-1');
if (idx === this.currentActiveIndex) {
this.renderer.setAttribute(chip.element.nativeElement, 'tabindex', '0');
chip.focus();
}
});
}
updateChips() {
this.normalizeActiveIndex();
this.chips.forEach((chip, idx) => {
const chipEl = chip.element.nativeElement;
this.renderer.removeAttribute(chipEl, 'aria-pressed');
this.renderer.setAttribute(chipEl, 'aria-selected', `${chip.selected}`);
this.role === 'listbox' && this.renderer.setAttribute(chipEl, 'role', 'option');
if (!this.navigable) {
return;
}
this.renderer.setAttribute(chipEl, 'tabindex', '-1');
if (idx === this.currentActiveIndex) {
this.renderer.setAttribute(chipEl, 'tabindex', '0');
if (isDocumentAvailable() && document.activeElement.closest('.k-chip-list')) {
chip.focus();
}
}
if (chip.removable) {
this.renderer.setAttribute(chipEl, 'aria-keyshortcuts', 'Enter Delete');
}
this.setChipSize(chip, this.size);
});
}
normalizeActiveIndex() {
if (this.currentActiveIndex >= this.chips.length) {
this.currentActiveIndex = Math.max(this.chips.length - 1, 0);
}
}
setChipSize(chip, size) {
const hasSize = chip.sizeIsSet;
!hasSize && chip.size !== size && (chip.size = size);
!hasSize && (chip.sizeIsSet = false);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChipListComponent, deps: [{ token: i1.LocalizationService }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ChipListComponent, isStandalone: true, selector: "kendo-chiplist, kendo-chip-list", inputs: { selection: "selection", size: "size", role: "role", navigable: "navigable" }, outputs: { selectedChange: "selectedChange", remove: "remove" }, host: { listeners: { "click": "onClick($event)" }, properties: { "class.k-chip-list": "this.hostClass", "attr.aria-orientation": "this.orientation", "attr.dir": "this.direction", "class.k-selection-single": "this.single", "attr.aria-multiselectable": "this.multiple", "class.k-selection-multiple": "this.multiple", "attr.role": "this.role" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.chiplist'
}
], queries: [{ propertyName: "chips", predicate: ChipComponent }], ngImport: i0, template: `
<ng-content></ng-content>
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ChipListComponent, decorators: [{
type: Component,
args: [{
selector: 'kendo-chiplist, kendo-chip-list',
template: `
<ng-content></ng-content>
`,
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.chiplist'
}
],
standalone: true
}]
}], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { hostClass: [{
type: HostBinding,
args: ['class.k-chip-list']
}], orientation: [{
type: HostBinding,
args: ['attr.aria-orientation']
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}], selection: [{
type: Input
}], size: [{
type: Input
}], selectedChange: [{
type: Output
}], remove: [{
type: Output
}], chips: [{
type: ContentChildren,
args: [ChipComponent]
}], single: [{
type: HostBinding,
args: ['class.k-selection-single']
}], multiple: [{
type: HostBinding,
args: ['attr.aria-multiselectable']
}, {
type: HostBinding,
args: ['class.k-selection-multiple']
}], role: [{
type: HostBinding,
args: ['attr.role']
}, {
type: Input
}], onClick: [{
type: HostListener,
args: ['click', ['$event']]
}], navigable: [{
type: Input
}] } });