799 lines
25 KiB
TypeScript
799 lines
25 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright Google LLC All Rights Reserved.
|
||
|
|
*
|
||
|
|
* Use of this source code is governed by an MIT-style license that can be
|
||
|
|
* found in the LICENSE file at https://angular.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {_IdGenerator, FocusMonitor} from '@angular/cdk/a11y';
|
||
|
|
import {SelectionModel} from '@angular/cdk/collections';
|
||
|
|
import {DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW, SPACE, ENTER} from '@angular/cdk/keycodes';
|
||
|
|
import {
|
||
|
|
AfterContentInit,
|
||
|
|
ChangeDetectionStrategy,
|
||
|
|
ChangeDetectorRef,
|
||
|
|
Component,
|
||
|
|
ContentChildren,
|
||
|
|
Directive,
|
||
|
|
ElementRef,
|
||
|
|
EventEmitter,
|
||
|
|
forwardRef,
|
||
|
|
Input,
|
||
|
|
OnDestroy,
|
||
|
|
OnInit,
|
||
|
|
Output,
|
||
|
|
QueryList,
|
||
|
|
ViewChild,
|
||
|
|
ViewEncapsulation,
|
||
|
|
InjectionToken,
|
||
|
|
AfterViewInit,
|
||
|
|
booleanAttribute,
|
||
|
|
inject,
|
||
|
|
HostAttributeToken,
|
||
|
|
ANIMATION_MODULE_TYPE,
|
||
|
|
} from '@angular/core';
|
||
|
|
import {Direction, Directionality} from '@angular/cdk/bidi';
|
||
|
|
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
||
|
|
import {MatRipple, MatPseudoCheckbox, _StructuralStylesLoader} from '@angular/material/core';
|
||
|
|
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated No longer used.
|
||
|
|
* @breaking-change 11.0.0
|
||
|
|
*/
|
||
|
|
export type ToggleType = 'checkbox' | 'radio';
|
||
|
|
|
||
|
|
/** Possible appearance styles for the button toggle. */
|
||
|
|
export type MatButtonToggleAppearance = 'legacy' | 'standard';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Represents the default options for the button toggle that can be configured
|
||
|
|
* using the `MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS` injection token.
|
||
|
|
*/
|
||
|
|
export interface MatButtonToggleDefaultOptions {
|
||
|
|
/**
|
||
|
|
* Default appearance to be used by button toggles. Can be overridden by explicitly
|
||
|
|
* setting an appearance on a button toggle or group.
|
||
|
|
*/
|
||
|
|
appearance?: MatButtonToggleAppearance;
|
||
|
|
/** Whether icon indicators should be hidden for single-selection button toggle groups. */
|
||
|
|
hideSingleSelectionIndicator?: boolean;
|
||
|
|
/** Whether icon indicators should be hidden for multiple-selection button toggle groups. */
|
||
|
|
hideMultipleSelectionIndicator?: boolean;
|
||
|
|
/** Whether disabled toggle buttons should be interactive. */
|
||
|
|
disabledInteractive?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Injection token that can be used to configure the
|
||
|
|
* default options for all button toggles within an app.
|
||
|
|
*/
|
||
|
|
export const MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS = new InjectionToken<MatButtonToggleDefaultOptions>(
|
||
|
|
'MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS',
|
||
|
|
{
|
||
|
|
providedIn: 'root',
|
||
|
|
factory: MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
export function MAT_BUTTON_TOGGLE_GROUP_DEFAULT_OPTIONS_FACTORY(): MatButtonToggleDefaultOptions {
|
||
|
|
return {
|
||
|
|
hideSingleSelectionIndicator: false,
|
||
|
|
hideMultipleSelectionIndicator: false,
|
||
|
|
disabledInteractive: false,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Injection token that can be used to reference instances of `MatButtonToggleGroup`.
|
||
|
|
* It serves as alternative token to the actual `MatButtonToggleGroup` class which
|
||
|
|
* could cause unnecessary retention of the class and its component metadata.
|
||
|
|
*/
|
||
|
|
export const MAT_BUTTON_TOGGLE_GROUP = new InjectionToken<MatButtonToggleGroup>(
|
||
|
|
'MatButtonToggleGroup',
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Provider Expression that allows mat-button-toggle-group to register as a ControlValueAccessor.
|
||
|
|
* This allows it to support [(ngModel)].
|
||
|
|
* @docs-private
|
||
|
|
*/
|
||
|
|
export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = {
|
||
|
|
provide: NG_VALUE_ACCESSOR,
|
||
|
|
useExisting: forwardRef(() => MatButtonToggleGroup),
|
||
|
|
multi: true,
|
||
|
|
};
|
||
|
|
|
||
|
|
/** Change event object emitted by button toggle. */
|
||
|
|
export class MatButtonToggleChange {
|
||
|
|
constructor(
|
||
|
|
/** The button toggle that emits the event. */
|
||
|
|
public source: MatButtonToggle,
|
||
|
|
|
||
|
|
/** The value assigned to the button toggle. */
|
||
|
|
public value: any,
|
||
|
|
) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Exclusive selection button toggle group that behaves like a radio-button group. */
|
||
|
|
@Directive({
|
||
|
|
selector: 'mat-button-toggle-group',
|
||
|
|
providers: [
|
||
|
|
MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR,
|
||
|
|
{provide: MAT_BUTTON_TOGGLE_GROUP, useExisting: MatButtonToggleGroup},
|
||
|
|
],
|
||
|
|
host: {
|
||
|
|
'class': 'mat-button-toggle-group',
|
||
|
|
'(keydown)': '_keydown($event)',
|
||
|
|
'[attr.role]': "multiple ? 'group' : 'radiogroup'",
|
||
|
|
'[attr.aria-disabled]': 'disabled',
|
||
|
|
'[class.mat-button-toggle-vertical]': 'vertical',
|
||
|
|
'[class.mat-button-toggle-group-appearance-standard]': 'appearance === "standard"',
|
||
|
|
},
|
||
|
|
exportAs: 'matButtonToggleGroup',
|
||
|
|
})
|
||
|
|
export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, AfterContentInit {
|
||
|
|
private _changeDetector = inject(ChangeDetectorRef);
|
||
|
|
private _dir = inject(Directionality, {optional: true});
|
||
|
|
|
||
|
|
private _multiple = false;
|
||
|
|
private _disabled = false;
|
||
|
|
private _disabledInteractive = false;
|
||
|
|
private _selectionModel: SelectionModel<MatButtonToggle>;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reference to the raw value that the consumer tried to assign. The real
|
||
|
|
* value will exclude any values from this one that don't correspond to a
|
||
|
|
* toggle. Useful for the cases where the value is assigned before the toggles
|
||
|
|
* have been initialized or at the same that they're being swapped out.
|
||
|
|
*/
|
||
|
|
private _rawValue: any;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The method to be called in order to update ngModel.
|
||
|
|
* Now `ngModel` binding is not supported in multiple selection mode.
|
||
|
|
*/
|
||
|
|
_controlValueAccessorChangeFn: (value: any) => void = () => {};
|
||
|
|
|
||
|
|
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
|
||
|
|
_onTouched: () => any = () => {};
|
||
|
|
|
||
|
|
/** Child button toggle buttons. */
|
||
|
|
@ContentChildren(forwardRef(() => MatButtonToggle), {
|
||
|
|
// Note that this would technically pick up toggles
|
||
|
|
// from nested groups, but that's not a case that we support.
|
||
|
|
descendants: true,
|
||
|
|
})
|
||
|
|
_buttonToggles: QueryList<MatButtonToggle>;
|
||
|
|
|
||
|
|
/** The appearance for all the buttons in the group. */
|
||
|
|
@Input() appearance: MatButtonToggleAppearance;
|
||
|
|
|
||
|
|
/** `name` attribute for the underlying `input` element. */
|
||
|
|
@Input()
|
||
|
|
get name(): string {
|
||
|
|
return this._name;
|
||
|
|
}
|
||
|
|
set name(value: string) {
|
||
|
|
this._name = value;
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
private _name = inject(_IdGenerator).getId('mat-button-toggle-group-');
|
||
|
|
|
||
|
|
/** Whether the toggle group is vertical. */
|
||
|
|
@Input({transform: booleanAttribute}) vertical: boolean;
|
||
|
|
|
||
|
|
/** Value of the toggle group. */
|
||
|
|
@Input()
|
||
|
|
get value(): any {
|
||
|
|
const selected = this._selectionModel ? this._selectionModel.selected : [];
|
||
|
|
|
||
|
|
if (this.multiple) {
|
||
|
|
return selected.map(toggle => toggle.value);
|
||
|
|
}
|
||
|
|
|
||
|
|
return selected[0] ? selected[0].value : undefined;
|
||
|
|
}
|
||
|
|
set value(newValue: any) {
|
||
|
|
this._setSelectionByValue(newValue);
|
||
|
|
this.valueChange.emit(this.value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Event that emits whenever the value of the group changes.
|
||
|
|
* Used to facilitate two-way data binding.
|
||
|
|
* @docs-private
|
||
|
|
*/
|
||
|
|
@Output() readonly valueChange = new EventEmitter<any>();
|
||
|
|
|
||
|
|
/** Selected button toggles in the group. */
|
||
|
|
get selected(): MatButtonToggle | MatButtonToggle[] {
|
||
|
|
const selected = this._selectionModel ? this._selectionModel.selected : [];
|
||
|
|
return this.multiple ? selected : selected[0] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Whether multiple button toggles can be selected. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get multiple(): boolean {
|
||
|
|
return this._multiple;
|
||
|
|
}
|
||
|
|
set multiple(value: boolean) {
|
||
|
|
this._multiple = value;
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Whether multiple button toggle group is disabled. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get disabled(): boolean {
|
||
|
|
return this._disabled;
|
||
|
|
}
|
||
|
|
set disabled(value: boolean) {
|
||
|
|
this._disabled = value;
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Whether buttons in the group should be interactive while they're disabled. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get disabledInteractive(): boolean {
|
||
|
|
return this._disabledInteractive;
|
||
|
|
}
|
||
|
|
set disabledInteractive(value: boolean) {
|
||
|
|
this._disabledInteractive = value;
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The layout direction of the toggle button group. */
|
||
|
|
get dir(): Direction {
|
||
|
|
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Event emitted when the group's value changes. */
|
||
|
|
@Output() readonly change: EventEmitter<MatButtonToggleChange> =
|
||
|
|
new EventEmitter<MatButtonToggleChange>();
|
||
|
|
|
||
|
|
/** Whether checkmark indicator for single-selection button toggle groups is hidden. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get hideSingleSelectionIndicator(): boolean {
|
||
|
|
return this._hideSingleSelectionIndicator;
|
||
|
|
}
|
||
|
|
set hideSingleSelectionIndicator(value: boolean) {
|
||
|
|
this._hideSingleSelectionIndicator = value;
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
private _hideSingleSelectionIndicator: boolean;
|
||
|
|
|
||
|
|
/** Whether checkmark indicator for multiple-selection button toggle groups is hidden. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get hideMultipleSelectionIndicator(): boolean {
|
||
|
|
return this._hideMultipleSelectionIndicator;
|
||
|
|
}
|
||
|
|
set hideMultipleSelectionIndicator(value: boolean) {
|
||
|
|
this._hideMultipleSelectionIndicator = value;
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
private _hideMultipleSelectionIndicator: boolean;
|
||
|
|
|
||
|
|
constructor(...args: unknown[]);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
const defaultOptions = inject<MatButtonToggleDefaultOptions>(
|
||
|
|
MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS,
|
||
|
|
{optional: true},
|
||
|
|
);
|
||
|
|
|
||
|
|
this.appearance =
|
||
|
|
defaultOptions && defaultOptions.appearance ? defaultOptions.appearance : 'standard';
|
||
|
|
this.hideSingleSelectionIndicator = defaultOptions?.hideSingleSelectionIndicator ?? false;
|
||
|
|
this.hideMultipleSelectionIndicator = defaultOptions?.hideMultipleSelectionIndicator ?? false;
|
||
|
|
}
|
||
|
|
|
||
|
|
ngOnInit() {
|
||
|
|
this._selectionModel = new SelectionModel<MatButtonToggle>(this.multiple, undefined, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
ngAfterContentInit() {
|
||
|
|
this._selectionModel.select(...this._buttonToggles.filter(toggle => toggle.checked));
|
||
|
|
if (!this.multiple) {
|
||
|
|
this._initializeTabIndex();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sets the model value. Implemented as part of ControlValueAccessor.
|
||
|
|
* @param value Value to be set to the model.
|
||
|
|
*/
|
||
|
|
writeValue(value: any) {
|
||
|
|
this.value = value;
|
||
|
|
this._changeDetector.markForCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implemented as part of ControlValueAccessor.
|
||
|
|
registerOnChange(fn: (value: any) => void) {
|
||
|
|
this._controlValueAccessorChangeFn = fn;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implemented as part of ControlValueAccessor.
|
||
|
|
registerOnTouched(fn: any) {
|
||
|
|
this._onTouched = fn;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implemented as part of ControlValueAccessor.
|
||
|
|
setDisabledState(isDisabled: boolean): void {
|
||
|
|
this.disabled = isDisabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Handle keydown event calling to single-select button toggle. */
|
||
|
|
protected _keydown(event: KeyboardEvent) {
|
||
|
|
if (this.multiple || this.disabled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const target = event.target as HTMLButtonElement;
|
||
|
|
const buttonId = target.id;
|
||
|
|
const index = this._buttonToggles.toArray().findIndex(toggle => {
|
||
|
|
return toggle.buttonId === buttonId;
|
||
|
|
});
|
||
|
|
|
||
|
|
let nextButton: MatButtonToggle | null = null;
|
||
|
|
switch (event.keyCode) {
|
||
|
|
case SPACE:
|
||
|
|
case ENTER:
|
||
|
|
nextButton = this._buttonToggles.get(index) || null;
|
||
|
|
break;
|
||
|
|
case UP_ARROW:
|
||
|
|
nextButton = this._getNextButton(index, -1);
|
||
|
|
break;
|
||
|
|
case LEFT_ARROW:
|
||
|
|
nextButton = this._getNextButton(index, this.dir === 'ltr' ? -1 : 1);
|
||
|
|
break;
|
||
|
|
case DOWN_ARROW:
|
||
|
|
nextButton = this._getNextButton(index, 1);
|
||
|
|
break;
|
||
|
|
case RIGHT_ARROW:
|
||
|
|
nextButton = this._getNextButton(index, this.dir === 'ltr' ? 1 : -1);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nextButton) {
|
||
|
|
event.preventDefault();
|
||
|
|
nextButton._onButtonClick();
|
||
|
|
nextButton.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Dispatch change event with current selection and group value. */
|
||
|
|
_emitChangeEvent(toggle: MatButtonToggle): void {
|
||
|
|
const event = new MatButtonToggleChange(toggle, this.value);
|
||
|
|
this._rawValue = event.value;
|
||
|
|
this._controlValueAccessorChangeFn(event.value);
|
||
|
|
this.change.emit(event);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Syncs a button toggle's selected state with the model value.
|
||
|
|
* @param toggle Toggle to be synced.
|
||
|
|
* @param select Whether the toggle should be selected.
|
||
|
|
* @param isUserInput Whether the change was a result of a user interaction.
|
||
|
|
* @param deferEvents Whether to defer emitting the change events.
|
||
|
|
*/
|
||
|
|
_syncButtonToggle(
|
||
|
|
toggle: MatButtonToggle,
|
||
|
|
select: boolean,
|
||
|
|
isUserInput = false,
|
||
|
|
deferEvents = false,
|
||
|
|
) {
|
||
|
|
// Deselect the currently-selected toggle, if we're in single-selection
|
||
|
|
// mode and the button being toggled isn't selected at the moment.
|
||
|
|
if (!this.multiple && this.selected && !toggle.checked) {
|
||
|
|
(this.selected as MatButtonToggle).checked = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._selectionModel) {
|
||
|
|
if (select) {
|
||
|
|
this._selectionModel.select(toggle);
|
||
|
|
} else {
|
||
|
|
this._selectionModel.deselect(toggle);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
deferEvents = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// We need to defer in some cases in order to avoid "changed after checked errors", however
|
||
|
|
// the side-effect is that we may end up updating the model value out of sequence in others
|
||
|
|
// The `deferEvents` flag allows us to decide whether to do it on a case-by-case basis.
|
||
|
|
if (deferEvents) {
|
||
|
|
Promise.resolve().then(() => this._updateModelValue(toggle, isUserInput));
|
||
|
|
} else {
|
||
|
|
this._updateModelValue(toggle, isUserInput);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Checks whether a button toggle is selected. */
|
||
|
|
_isSelected(toggle: MatButtonToggle) {
|
||
|
|
return this._selectionModel && this._selectionModel.isSelected(toggle);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Determines whether a button toggle should be checked on init. */
|
||
|
|
_isPrechecked(toggle: MatButtonToggle) {
|
||
|
|
if (typeof this._rawValue === 'undefined') {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.multiple && Array.isArray(this._rawValue)) {
|
||
|
|
return this._rawValue.some(value => toggle.value != null && value === toggle.value);
|
||
|
|
}
|
||
|
|
|
||
|
|
return toggle.value === this._rawValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Initializes the tabindex attribute using the radio pattern. */
|
||
|
|
private _initializeTabIndex() {
|
||
|
|
this._buttonToggles.forEach(toggle => {
|
||
|
|
toggle.tabIndex = -1;
|
||
|
|
});
|
||
|
|
if (this.selected) {
|
||
|
|
(this.selected as MatButtonToggle).tabIndex = 0;
|
||
|
|
} else {
|
||
|
|
for (let i = 0; i < this._buttonToggles.length; i++) {
|
||
|
|
const toggle = this._buttonToggles.get(i)!;
|
||
|
|
|
||
|
|
if (!toggle.disabled) {
|
||
|
|
toggle.tabIndex = 0;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._markButtonsForCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Obtain the subsequent toggle to which the focus shifts. */
|
||
|
|
private _getNextButton(startIndex: number, offset: number): MatButtonToggle | null {
|
||
|
|
const items = this._buttonToggles;
|
||
|
|
|
||
|
|
for (let i = 1; i <= items.length; i++) {
|
||
|
|
const index = (startIndex + offset * i + items.length) % items.length;
|
||
|
|
const item = items.get(index);
|
||
|
|
|
||
|
|
if (item && !item.disabled) {
|
||
|
|
return item;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Updates the selection state of the toggles in the group based on a value. */
|
||
|
|
private _setSelectionByValue(value: any | any[]) {
|
||
|
|
this._rawValue = value;
|
||
|
|
|
||
|
|
if (!this._buttonToggles) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggles = this._buttonToggles.toArray();
|
||
|
|
|
||
|
|
if (this.multiple && value) {
|
||
|
|
if (!Array.isArray(value) && (typeof ngDevMode === 'undefined' || ngDevMode)) {
|
||
|
|
throw Error('Value must be an array in multiple-selection mode.');
|
||
|
|
}
|
||
|
|
|
||
|
|
this._clearSelection();
|
||
|
|
value.forEach((currentValue: any) => this._selectValue(currentValue, toggles));
|
||
|
|
} else {
|
||
|
|
this._clearSelection();
|
||
|
|
this._selectValue(value, toggles);
|
||
|
|
}
|
||
|
|
|
||
|
|
// In single selection mode we need at least one enabled toggle to always be focusable.
|
||
|
|
if (!this.multiple && toggles.every(toggle => toggle.tabIndex === -1)) {
|
||
|
|
for (const toggle of toggles) {
|
||
|
|
if (!toggle.disabled) {
|
||
|
|
toggle.tabIndex = 0;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Clears the selected toggles. */
|
||
|
|
private _clearSelection() {
|
||
|
|
this._selectionModel.clear();
|
||
|
|
this._buttonToggles.forEach(toggle => {
|
||
|
|
toggle.checked = false;
|
||
|
|
// If the button toggle is in single select mode, initialize the tabIndex.
|
||
|
|
if (!this.multiple) {
|
||
|
|
toggle.tabIndex = -1;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Selects a value if there's a toggle that corresponds to it. */
|
||
|
|
private _selectValue(value: any, toggles: MatButtonToggle[]) {
|
||
|
|
for (const toggle of toggles) {
|
||
|
|
if (toggle.value != null && toggle.value === value) {
|
||
|
|
toggle.checked = true;
|
||
|
|
this._selectionModel.select(toggle);
|
||
|
|
if (!this.multiple) {
|
||
|
|
// If the button toggle is in single select mode, reset the tabIndex.
|
||
|
|
toggle.tabIndex = 0;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Syncs up the group's value with the model and emits the change event. */
|
||
|
|
private _updateModelValue(toggle: MatButtonToggle, isUserInput: boolean) {
|
||
|
|
// Only emit the change event for user input.
|
||
|
|
if (isUserInput) {
|
||
|
|
this._emitChangeEvent(toggle);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Note: we emit this one no matter whether it was a user interaction, because
|
||
|
|
// it is used by Angular to sync up the two-way data binding.
|
||
|
|
this.valueChange.emit(this.value);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Marks all of the child button toggles to be checked. */
|
||
|
|
private _markButtonsForCheck() {
|
||
|
|
this._buttonToggles?.forEach(toggle => toggle._markForCheck());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Single button inside of a toggle group. */
|
||
|
|
@Component({
|
||
|
|
selector: 'mat-button-toggle',
|
||
|
|
templateUrl: 'button-toggle.html',
|
||
|
|
styleUrl: 'button-toggle.css',
|
||
|
|
encapsulation: ViewEncapsulation.None,
|
||
|
|
exportAs: 'matButtonToggle',
|
||
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||
|
|
host: {
|
||
|
|
'[class.mat-button-toggle-standalone]': '!buttonToggleGroup',
|
||
|
|
'[class.mat-button-toggle-checked]': 'checked',
|
||
|
|
'[class.mat-button-toggle-disabled]': 'disabled',
|
||
|
|
'[class.mat-button-toggle-disabled-interactive]': 'disabledInteractive',
|
||
|
|
'[class.mat-button-toggle-appearance-standard]': 'appearance === "standard"',
|
||
|
|
'class': 'mat-button-toggle',
|
||
|
|
'[attr.aria-label]': 'null',
|
||
|
|
'[attr.aria-labelledby]': 'null',
|
||
|
|
'[attr.id]': 'id',
|
||
|
|
'[attr.name]': 'null',
|
||
|
|
'(focus)': 'focus()',
|
||
|
|
'role': 'presentation',
|
||
|
|
},
|
||
|
|
imports: [MatRipple, MatPseudoCheckbox],
|
||
|
|
})
|
||
|
|
export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
|
||
|
|
private _changeDetectorRef = inject(ChangeDetectorRef);
|
||
|
|
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
|
||
|
|
private _focusMonitor = inject(FocusMonitor);
|
||
|
|
private _idGenerator = inject(_IdGenerator);
|
||
|
|
private _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
|
||
|
|
private _checked = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Attached to the aria-label attribute of the host element. In most cases, aria-labelledby will
|
||
|
|
* take precedence so this may be omitted.
|
||
|
|
*/
|
||
|
|
@Input('aria-label') ariaLabel: string;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Users can specify the `aria-labelledby` attribute which will be forwarded to the input element
|
||
|
|
*/
|
||
|
|
@Input('aria-labelledby') ariaLabelledby: string | null = null;
|
||
|
|
|
||
|
|
/** Underlying native `button` element. */
|
||
|
|
@ViewChild('button') _buttonElement: ElementRef<HTMLButtonElement>;
|
||
|
|
|
||
|
|
/** The parent button toggle group (exclusive selection). Optional. */
|
||
|
|
buttonToggleGroup: MatButtonToggleGroup;
|
||
|
|
|
||
|
|
/** Unique ID for the underlying `button` element. */
|
||
|
|
get buttonId(): string {
|
||
|
|
return `${this.id}-button`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The unique ID for this button toggle. */
|
||
|
|
@Input() id: string;
|
||
|
|
|
||
|
|
/** HTML's 'name' attribute used to group radios for unique selection. */
|
||
|
|
@Input() name: string;
|
||
|
|
|
||
|
|
/** MatButtonToggleGroup reads this to assign its own value. */
|
||
|
|
@Input() value: any;
|
||
|
|
|
||
|
|
/** Tabindex of the toggle. */
|
||
|
|
@Input()
|
||
|
|
get tabIndex(): number | null {
|
||
|
|
return this._tabIndex;
|
||
|
|
}
|
||
|
|
set tabIndex(value: number | null) {
|
||
|
|
if (value !== this._tabIndex) {
|
||
|
|
this._tabIndex = value;
|
||
|
|
this._markForCheck();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private _tabIndex: number | null;
|
||
|
|
|
||
|
|
/** Whether ripples are disabled on the button toggle. */
|
||
|
|
@Input({transform: booleanAttribute}) disableRipple: boolean;
|
||
|
|
|
||
|
|
/** The appearance style of the button. */
|
||
|
|
@Input()
|
||
|
|
get appearance(): MatButtonToggleAppearance {
|
||
|
|
return this.buttonToggleGroup ? this.buttonToggleGroup.appearance : this._appearance;
|
||
|
|
}
|
||
|
|
set appearance(value: MatButtonToggleAppearance) {
|
||
|
|
this._appearance = value;
|
||
|
|
}
|
||
|
|
private _appearance: MatButtonToggleAppearance;
|
||
|
|
|
||
|
|
/** Whether the button is checked. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get checked(): boolean {
|
||
|
|
return this.buttonToggleGroup ? this.buttonToggleGroup._isSelected(this) : this._checked;
|
||
|
|
}
|
||
|
|
set checked(value: boolean) {
|
||
|
|
if (value !== this._checked) {
|
||
|
|
this._checked = value;
|
||
|
|
|
||
|
|
if (this.buttonToggleGroup) {
|
||
|
|
this.buttonToggleGroup._syncButtonToggle(this, this._checked);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._changeDetectorRef.markForCheck();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Whether the button is disabled. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get disabled(): boolean {
|
||
|
|
return this._disabled || (this.buttonToggleGroup && this.buttonToggleGroup.disabled);
|
||
|
|
}
|
||
|
|
set disabled(value: boolean) {
|
||
|
|
this._disabled = value;
|
||
|
|
}
|
||
|
|
private _disabled: boolean = false;
|
||
|
|
|
||
|
|
/** Whether the button should remain interactive when it is disabled. */
|
||
|
|
@Input({transform: booleanAttribute})
|
||
|
|
get disabledInteractive(): boolean {
|
||
|
|
return (
|
||
|
|
this._disabledInteractive ||
|
||
|
|
(this.buttonToggleGroup !== null && this.buttonToggleGroup.disabledInteractive)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
set disabledInteractive(value: boolean) {
|
||
|
|
this._disabledInteractive = value;
|
||
|
|
}
|
||
|
|
private _disabledInteractive: boolean;
|
||
|
|
|
||
|
|
/** Event emitted when the group value changes. */
|
||
|
|
@Output() readonly change: EventEmitter<MatButtonToggleChange> =
|
||
|
|
new EventEmitter<MatButtonToggleChange>();
|
||
|
|
|
||
|
|
constructor(...args: unknown[]);
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
|
||
|
|
const toggleGroup = inject<MatButtonToggleGroup>(MAT_BUTTON_TOGGLE_GROUP, {optional: true})!;
|
||
|
|
const defaultTabIndex = inject(new HostAttributeToken('tabindex'), {optional: true}) || '';
|
||
|
|
const defaultOptions = inject<MatButtonToggleDefaultOptions>(
|
||
|
|
MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS,
|
||
|
|
{optional: true},
|
||
|
|
);
|
||
|
|
|
||
|
|
this._tabIndex = parseInt(defaultTabIndex) || 0;
|
||
|
|
this.buttonToggleGroup = toggleGroup;
|
||
|
|
this.appearance =
|
||
|
|
defaultOptions && defaultOptions.appearance ? defaultOptions.appearance : 'standard';
|
||
|
|
this.disabledInteractive = defaultOptions?.disabledInteractive ?? false;
|
||
|
|
}
|
||
|
|
|
||
|
|
ngOnInit() {
|
||
|
|
const group = this.buttonToggleGroup;
|
||
|
|
this.id = this.id || this._idGenerator.getId('mat-button-toggle-');
|
||
|
|
|
||
|
|
if (group) {
|
||
|
|
if (group._isPrechecked(this)) {
|
||
|
|
this.checked = true;
|
||
|
|
} else if (group._isSelected(this) !== this._checked) {
|
||
|
|
// As side effect of the circular dependency between the toggle group and the button,
|
||
|
|
// we may end up in a state where the button is supposed to be checked on init, but it
|
||
|
|
// isn't, because the checked value was assigned too early. This can happen when Ivy
|
||
|
|
// assigns the static input value before the `ngOnInit` has run.
|
||
|
|
group._syncButtonToggle(this, this._checked);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ngAfterViewInit() {
|
||
|
|
// This serves two purposes:
|
||
|
|
// 1. We don't want the animation to fire on the first render for pre-checked toggles so we
|
||
|
|
// delay adding the class until the view is rendered.
|
||
|
|
// 2. We don't want animation if the `NoopAnimationsModule` is provided.
|
||
|
|
if (this._animationMode !== 'NoopAnimations') {
|
||
|
|
this._elementRef.nativeElement.classList.add('mat-button-toggle-animations-enabled');
|
||
|
|
}
|
||
|
|
|
||
|
|
this._focusMonitor.monitor(this._elementRef, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
ngOnDestroy() {
|
||
|
|
const group = this.buttonToggleGroup;
|
||
|
|
|
||
|
|
this._focusMonitor.stopMonitoring(this._elementRef);
|
||
|
|
|
||
|
|
// Remove the toggle from the selection once it's destroyed. Needs to happen
|
||
|
|
// on the next tick in order to avoid "changed after checked" errors.
|
||
|
|
if (group && group._isSelected(this)) {
|
||
|
|
group._syncButtonToggle(this, false, false, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Focuses the button. */
|
||
|
|
focus(options?: FocusOptions): void {
|
||
|
|
this._buttonElement.nativeElement.focus(options);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Checks the button toggle due to an interaction with the underlying native button. */
|
||
|
|
_onButtonClick() {
|
||
|
|
if (this.disabled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const newChecked = this.isSingleSelector() ? true : !this._checked;
|
||
|
|
|
||
|
|
if (newChecked !== this._checked) {
|
||
|
|
this._checked = newChecked;
|
||
|
|
if (this.buttonToggleGroup) {
|
||
|
|
this.buttonToggleGroup._syncButtonToggle(this, this._checked, true);
|
||
|
|
this.buttonToggleGroup._onTouched();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.isSingleSelector()) {
|
||
|
|
const focusable = this.buttonToggleGroup._buttonToggles.find(toggle => {
|
||
|
|
return toggle.tabIndex === 0;
|
||
|
|
});
|
||
|
|
// Modify the tabindex attribute of the last focusable button toggle to -1.
|
||
|
|
if (focusable) {
|
||
|
|
focusable.tabIndex = -1;
|
||
|
|
}
|
||
|
|
// Modify the tabindex attribute of the presently selected button toggle to 0.
|
||
|
|
this.tabIndex = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Emit a change event when it's the single selector
|
||
|
|
this.change.emit(new MatButtonToggleChange(this, this.value));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Marks the button toggle as needing checking for change detection.
|
||
|
|
* This method is exposed because the parent button toggle group will directly
|
||
|
|
* update bound properties of the radio button.
|
||
|
|
*/
|
||
|
|
_markForCheck() {
|
||
|
|
// When the group value changes, the button will not be notified.
|
||
|
|
// Use `markForCheck` to explicit update button toggle's status.
|
||
|
|
this._changeDetectorRef.markForCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Gets the name that should be assigned to the inner DOM node. */
|
||
|
|
_getButtonName(): string | null {
|
||
|
|
if (this.isSingleSelector()) {
|
||
|
|
return this.buttonToggleGroup.name;
|
||
|
|
}
|
||
|
|
return this.name || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Whether the toggle is in single selection mode. */
|
||
|
|
isSingleSelector(): boolean {
|
||
|
|
return this.buttonToggleGroup && !this.buttonToggleGroup.multiple;
|
||
|
|
}
|
||
|
|
}
|