@progress/kendo-angular-buttons
Version:
Buttons Package for Angular
333 lines (332 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 { 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']
}] } });