UNPKG

@progress/kendo-angular-buttons

Version:
333 lines (332 loc) 13.7 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ButtonComponent } from '../button/button.component'; import { Component, EventEmitter, Output, Input, ContentChildren, QueryList, HostBinding, isDevMode, ElementRef, Renderer2 } from '@angular/core'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { isChanged, Keys } from '@progress/kendo-angular-common'; import { KendoButtonService } from '../button/button.service'; import { fromEvent, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import { isPresent } from '../util'; import { PreventableEvent } from '../preventable-event'; import { packageMetadata } from '../package-metadata'; import { validatePackage } from '@progress/kendo-licensing'; import * as i0 from "@angular/core"; import * as i1 from "../button/button.service"; import * as i2 from "@progress/kendo-angular-l10n"; /** * @hidden */ const tabindex = 'tabindex'; /** * Represents the Kendo UI ButtonGroup component for Angular. */ export class ButtonGroupComponent { service; renderer; element; /** * By default, the ButtonGroup is enabled. * To disable the whole group of buttons, set its `disabled` attribute to `true`. * * To disable a specific button, set its own `disabled` attribute to `true` * and leave the `disabled` attribute of the ButtonGroup undefined. * If you define the `disabled` attribute of the ButtonGroup, it will take * precedence over the `disabled` attributes of the underlying buttons and they will be ignored. * * For more information on how to configure the Button, refer to * its [API documentation]({% slug api_buttons_buttoncomponent %}). */ disabled; /** * The selection mode of the ButtonGroup. * @default 'multiple' */ selection = 'multiple'; /** * Sets the width of the ButtonGroup. * If the width of the ButtonGroup is set: * - The buttons resize automatically to fill the full width of the group wrapper. * - The buttons acquire the same width. */ width; /** * Specifies the [`tabIndex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of the component. */ set tabIndex(value) { this._tabIndex = value; this.currentTabIndex = value; } get tabIndex() { return this._tabIndex; } /** * When this option is set to `true` (default), the component is a single tab-stop, * and focus is moved through the inner buttons via the arrow keys. * * When the option is set to `false`, the inner buttons are part of the natural tab sequence of the page. * * @default true */ navigable = true; /** * Fires every time keyboard navigation occurs. */ navigate = new EventEmitter(); buttons; _tabIndex = 0; currentTabIndex = 0; lastFocusedIndex = -1; direction; subs = new Subscription(); wrapperClasses = true; get disabledClass() { return this.disabled; } get stretchedClass() { return !!this.width; } role = 'group'; get dir() { return this.direction; } get ariaDisabled() { return this.disabled; } get wrapperWidth() { return this.width; } get wrapperTabIndex() { return this.disabled ? undefined : this.navigable ? this.currentTabIndex : undefined; } constructor(service, localization, renderer, element) { this.service = service; this.renderer = renderer; this.element = element; validatePackage(packageMetadata); this.subs.add(localization.changes.subscribe(({ rtl }) => this.direction = rtl ? 'rtl' : 'ltr')); } ngOnInit() { this.subs.add(this.service.buttonClicked$.subscribe((button) => { let newSelectionValue; if (this.isSelectionSingle()) { newSelectionValue = true; this.deactivate(this.buttons.filter(current => current !== button)); } else { if (this.navigable) { this.defocus(this.buttons.toArray()); } newSelectionValue = !button.selected; } if (button.togglable) { button.setSelected(newSelectionValue); } if (this.navigable) { this.currentTabIndex = -1; this.renderer.setAttribute(button, tabindex, '0'); } })); this.handleSubs('focus', () => this.navigable, this.focusHandler); this.handleSubs('keydown', () => this.navigable && !this.disabled, (event) => this.navigateFocus(event)); this.handleSubs('focusout', (event) => this.navigable && event.relatedTarget && event.relatedTarget.parentNode !== this.element.nativeElement, () => { this.lastFocusedIndex = this.buttons.toArray().findIndex(button => button.tabIndex !== -1); this.defocus(this.buttons.toArray()); this.currentTabIndex = this.tabIndex; }); this.subs.add(fromEvent(this.element.nativeElement, 'focusout') .pipe(filter((event) => this.navigable && event.relatedTarget && event.relatedTarget.parentNode !== this.element.nativeElement)) .subscribe(() => { this.defocus(this.buttons.toArray()); this.currentTabIndex = this.tabIndex; })); } ngOnChanges(changes) { if (isChanged('disabled', changes)) { this.buttons.forEach((button) => { if (isPresent(this.disabled)) { button.disabled = this.disabled; } }); } if (isChanged('navigable', changes)) { if (changes['navigable'].currentValue) { this.defocus(this.buttons.toArray()); this.currentTabIndex = 0; } else { this.currentTabIndex = -1; this.buttons.forEach((button) => this.renderer.setAttribute(button, tabindex, '0')); } } } ngAfterContentInit() { if (!this.navigable) { return; } this.defocus(this.buttons.toArray()); } ngAfterViewChecked() { if (this.buttons.length) { this.renderer.addClass(this.buttons.first.element, 'k-group-start'); this.renderer.addClass(this.buttons.last.element, 'k-group-end'); } } ngOnDestroy() { this.subs.unsubscribe(); } ngAfterContentChecked() { this.verifySettings(); } navigateFocus(event) { const navigationButtons = this.buttons.toArray().filter(button => !button.disabled); const focusedIndex = navigationButtons.findIndex(current => current.element.tabIndex !== -1); const firstIndex = 0; const lastIndex = navigationButtons.length - 1; const eventArgs = new PreventableEvent(); if (event.keyCode === Keys.ArrowRight && focusedIndex < lastIndex) { this.navigate.emit(eventArgs); if (!eventArgs.isDefaultPrevented()) { this.defocus(navigationButtons); this.focus(navigationButtons.filter((_current, index) => { return index === focusedIndex + 1; })); } } if (event.keyCode === Keys.ArrowLeft && focusedIndex > firstIndex) { this.navigate.emit(eventArgs); if (!eventArgs.isDefaultPrevented()) { this.defocus(navigationButtons); this.focus(navigationButtons.filter((_current, index) => { return index === focusedIndex - 1; })); } } } deactivate(buttons) { buttons.forEach((button) => { button.setSelected(false); if (this.navigable) { this.renderer.setAttribute(button, tabindex, '-1'); } }); } activate(buttons) { buttons.forEach((button) => { button.setSelected(true); if (this.navigable) { this.renderer.setAttribute(button, tabindex, '0'); } button.focus(); }); } defocus(buttons) { buttons.forEach((button) => { this.renderer.setAttribute(button, tabindex, '-1'); }); } focus(buttons) { buttons.forEach((button) => { this.renderer.setAttribute(button, tabindex, '0'); button.focus(); }); } verifySettings() { if (isDevMode()) { if (this.isSelectionSingle() && this.buttons.filter(button => button.selected).length > 1) { throw new Error('Having multiple selected buttons with single selection mode is not supported'); } } } isSelectionSingle() { return this.selection === 'single'; } handleSubs(eventName, predicate, handler) { this.subs.add(fromEvent(this.element.nativeElement, eventName) .pipe(filter(predicate)) .subscribe(handler)); } focusHandler = () => { this.currentTabIndex = -1; this.defocus(this.buttons.toArray()); const firstFocusableIndex = this.buttons.toArray().findIndex(current => !current.disabled); const index = this.lastFocusedIndex === -1 ? firstFocusableIndex : this.lastFocusedIndex; this.focus(this.buttons.filter((_current, i) => { return i === index; })); }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ButtonGroupComponent, deps: [{ token: i1.KendoButtonService }, { token: i2.LocalizationService }, { token: i0.Renderer2 }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ButtonGroupComponent, isStandalone: true, selector: "kendo-buttongroup", inputs: { disabled: "disabled", selection: "selection", width: "width", tabIndex: "tabIndex", navigable: "navigable" }, outputs: { navigate: "navigate" }, host: { properties: { "class.k-button-group": "this.wrapperClasses", "class.k-disabled": "this.disabledClass", "class.k-button-group-stretched": "this.stretchedClass", "attr.role": "this.role", "attr.dir": "this.dir", "attr.aria-disabled": "this.ariaDisabled", "style.width": "this.wrapperWidth", "attr.tabindex": "this.wrapperTabIndex" } }, providers: [ KendoButtonService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.buttongroup' } ], queries: [{ propertyName: "buttons", predicate: ButtonComponent }], exportAs: ["kendoButtonGroup"], usesOnChanges: true, ngImport: i0, template: ` <ng-content select="[kendoButton]"></ng-content> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ButtonGroupComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoButtonGroup', providers: [ KendoButtonService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.buttongroup' } ], selector: 'kendo-buttongroup', template: ` <ng-content select="[kendoButton]"></ng-content> `, standalone: true }] }], ctorParameters: function () { return [{ type: i1.KendoButtonService }, { type: i2.LocalizationService }, { type: i0.Renderer2 }, { type: i0.ElementRef }]; }, propDecorators: { disabled: [{ type: Input, args: ['disabled'] }], selection: [{ type: Input, args: ['selection'] }], width: [{ type: Input, args: ['width'] }], tabIndex: [{ type: Input }], navigable: [{ type: Input }], navigate: [{ type: Output }], buttons: [{ type: ContentChildren, args: [ButtonComponent] }], wrapperClasses: [{ type: HostBinding, args: ['class.k-button-group'] }], disabledClass: [{ type: HostBinding, args: ['class.k-disabled'] }], stretchedClass: [{ type: HostBinding, args: ['class.k-button-group-stretched'] }], role: [{ type: HostBinding, args: ['attr.role'] }], dir: [{ type: HostBinding, args: ['attr.dir'] }], ariaDisabled: [{ type: HostBinding, args: ['attr.aria-disabled'] }], wrapperWidth: [{ type: HostBinding, args: ['style.width'] }], wrapperTabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }] } });