802 lines
26 KiB
TypeScript
802 lines
26 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, FocusOrigin} from '@angular/cdk/a11y';
|
|
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
|
|
import {
|
|
ANIMATION_MODULE_TYPE,
|
|
AfterContentInit,
|
|
AfterViewInit,
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
ContentChildren,
|
|
Directive,
|
|
DoCheck,
|
|
ElementRef,
|
|
EventEmitter,
|
|
InjectionToken,
|
|
Injector,
|
|
Input,
|
|
NgZone,
|
|
OnDestroy,
|
|
OnInit,
|
|
Output,
|
|
QueryList,
|
|
ViewChild,
|
|
ViewEncapsulation,
|
|
afterNextRender,
|
|
booleanAttribute,
|
|
forwardRef,
|
|
inject,
|
|
numberAttribute,
|
|
HostAttributeToken,
|
|
} from '@angular/core';
|
|
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
|
|
import {
|
|
MatRipple,
|
|
ThemePalette,
|
|
_MatInternalFormField,
|
|
_StructuralStylesLoader,
|
|
} from '@angular/material/core';
|
|
import {Subscription} from 'rxjs';
|
|
import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
|
|
|
|
/** Change event object emitted by radio button and radio group. */
|
|
export class MatRadioChange {
|
|
constructor(
|
|
/** The radio button that emits the change event. */
|
|
public source: MatRadioButton,
|
|
/** The value of the radio button. */
|
|
public value: any,
|
|
) {}
|
|
}
|
|
|
|
/**
|
|
* Provider Expression that allows mat-radio-group to register as a ControlValueAccessor. This
|
|
* allows it to support [(ngModel)] and ngControl.
|
|
* @docs-private
|
|
*/
|
|
export const MAT_RADIO_GROUP_CONTROL_VALUE_ACCESSOR: any = {
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => MatRadioGroup),
|
|
multi: true,
|
|
};
|
|
|
|
/**
|
|
* Injection token that can be used to inject instances of `MatRadioGroup`. It serves as
|
|
* alternative token to the actual `MatRadioGroup` class which could cause unnecessary
|
|
* retention of the class and its component metadata.
|
|
*/
|
|
export const MAT_RADIO_GROUP = new InjectionToken<MatRadioGroup>('MatRadioGroup');
|
|
|
|
export interface MatRadioDefaultOptions {
|
|
/**
|
|
* Theme color of the radio button. This API is supported in M2 themes only, it
|
|
* has no effect in M3 themes.
|
|
*
|
|
* For information on applying color variants in M3, see
|
|
* https://material.angular.io/guide/theming#using-component-color-variants.
|
|
*/
|
|
color: ThemePalette;
|
|
|
|
/** Whether disabled radio buttons should be interactive. */
|
|
disabledInteractive?: boolean;
|
|
}
|
|
|
|
export const MAT_RADIO_DEFAULT_OPTIONS = new InjectionToken<MatRadioDefaultOptions>(
|
|
'mat-radio-default-options',
|
|
{
|
|
providedIn: 'root',
|
|
factory: MAT_RADIO_DEFAULT_OPTIONS_FACTORY,
|
|
},
|
|
);
|
|
|
|
export function MAT_RADIO_DEFAULT_OPTIONS_FACTORY(): MatRadioDefaultOptions {
|
|
return {
|
|
color: 'accent',
|
|
disabledInteractive: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A group of radio buttons. May contain one or more `<mat-radio-button>` elements.
|
|
*/
|
|
@Directive({
|
|
selector: 'mat-radio-group',
|
|
exportAs: 'matRadioGroup',
|
|
providers: [
|
|
MAT_RADIO_GROUP_CONTROL_VALUE_ACCESSOR,
|
|
{provide: MAT_RADIO_GROUP, useExisting: MatRadioGroup},
|
|
],
|
|
host: {
|
|
'role': 'radiogroup',
|
|
'class': 'mat-mdc-radio-group',
|
|
},
|
|
})
|
|
export class MatRadioGroup implements AfterContentInit, OnDestroy, ControlValueAccessor {
|
|
private _changeDetector = inject(ChangeDetectorRef);
|
|
|
|
/** Selected value for the radio group. */
|
|
private _value: any = null;
|
|
|
|
/** The HTML name attribute applied to radio buttons in this group. */
|
|
private _name: string = inject(_IdGenerator).getId('mat-radio-group-');
|
|
|
|
/** The currently selected radio button. Should match value. */
|
|
private _selected: MatRadioButton | null = null;
|
|
|
|
/** Whether the `value` has been set to its initial value. */
|
|
private _isInitialized: boolean = false;
|
|
|
|
/** Whether the labels should appear after or before the radio-buttons. Defaults to 'after' */
|
|
private _labelPosition: 'before' | 'after' = 'after';
|
|
|
|
/** Whether the radio group is disabled. */
|
|
private _disabled: boolean = false;
|
|
|
|
/** Whether the radio group is required. */
|
|
private _required: boolean = false;
|
|
|
|
/** Subscription to changes in amount of radio buttons. */
|
|
private _buttonChanges: Subscription;
|
|
|
|
/** The method to be called in order to update ngModel */
|
|
_controlValueAccessorChangeFn: (value: any) => void = () => {};
|
|
|
|
/**
|
|
* onTouch function registered via registerOnTouch (ControlValueAccessor).
|
|
* @docs-private
|
|
*/
|
|
onTouched: () => any = () => {};
|
|
|
|
/**
|
|
* Event emitted when the group value changes.
|
|
* Change events are only emitted when the value changes due to user interaction with
|
|
* a radio button (the same behavior as `<input type-"radio">`).
|
|
*/
|
|
@Output() readonly change: EventEmitter<MatRadioChange> = new EventEmitter<MatRadioChange>();
|
|
|
|
/** Child radio buttons. */
|
|
@ContentChildren(forwardRef(() => MatRadioButton), {descendants: true})
|
|
_radios: QueryList<MatRadioButton>;
|
|
|
|
/**
|
|
* Theme color of the radio buttons in the group. This API is supported in M2
|
|
* themes only, it has no effect in M3 themes.
|
|
*
|
|
* For information on applying color variants in M3, see
|
|
* https://material.angular.io/guide/theming#using-component-color-variants.
|
|
*/
|
|
@Input() color: ThemePalette;
|
|
|
|
/** Name of the radio button group. All radio buttons inside this group will use this name. */
|
|
@Input()
|
|
get name(): string {
|
|
return this._name;
|
|
}
|
|
set name(value: string) {
|
|
this._name = value;
|
|
this._updateRadioButtonNames();
|
|
}
|
|
|
|
/** Whether the labels should appear after or before the radio-buttons. Defaults to 'after' */
|
|
@Input()
|
|
get labelPosition(): 'before' | 'after' {
|
|
return this._labelPosition;
|
|
}
|
|
set labelPosition(v) {
|
|
this._labelPosition = v === 'before' ? 'before' : 'after';
|
|
this._markRadiosForCheck();
|
|
}
|
|
|
|
/**
|
|
* Value for the radio-group. Should equal the value of the selected radio button if there is
|
|
* a corresponding radio button with a matching value. If there is not such a corresponding
|
|
* radio button, this value persists to be applied in case a new radio button is added with a
|
|
* matching value.
|
|
*/
|
|
@Input()
|
|
get value(): any {
|
|
return this._value;
|
|
}
|
|
set value(newValue: any) {
|
|
if (this._value !== newValue) {
|
|
// Set this before proceeding to ensure no circular loop occurs with selection.
|
|
this._value = newValue;
|
|
|
|
this._updateSelectedRadioFromValue();
|
|
this._checkSelectedRadioButton();
|
|
}
|
|
}
|
|
|
|
_checkSelectedRadioButton() {
|
|
if (this._selected && !this._selected.checked) {
|
|
this._selected.checked = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The currently selected radio button. If set to a new radio button, the radio group value
|
|
* will be updated to match the new selected button.
|
|
*/
|
|
@Input()
|
|
get selected() {
|
|
return this._selected;
|
|
}
|
|
set selected(selected: MatRadioButton | null) {
|
|
this._selected = selected;
|
|
this.value = selected ? selected.value : null;
|
|
this._checkSelectedRadioButton();
|
|
}
|
|
|
|
/** Whether the radio group is disabled */
|
|
@Input({transform: booleanAttribute})
|
|
get disabled(): boolean {
|
|
return this._disabled;
|
|
}
|
|
set disabled(value: boolean) {
|
|
this._disabled = value;
|
|
this._markRadiosForCheck();
|
|
}
|
|
|
|
/** Whether the radio group is required */
|
|
@Input({transform: booleanAttribute})
|
|
get required(): boolean {
|
|
return this._required;
|
|
}
|
|
set required(value: boolean) {
|
|
this._required = value;
|
|
this._markRadiosForCheck();
|
|
}
|
|
|
|
/** 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._markRadiosForCheck();
|
|
}
|
|
private _disabledInteractive = false;
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {}
|
|
|
|
/**
|
|
* Initialize properties once content children are available.
|
|
* This allows us to propagate relevant attributes to associated buttons.
|
|
*/
|
|
ngAfterContentInit() {
|
|
// Mark this component as initialized in AfterContentInit because the initial value can
|
|
// possibly be set by NgModel on MatRadioGroup, and it is possible that the OnInit of the
|
|
// NgModel occurs *after* the OnInit of the MatRadioGroup.
|
|
this._isInitialized = true;
|
|
|
|
// Clear the `selected` button when it's destroyed since the tabindex of the rest of the
|
|
// buttons depends on it. Note that we don't clear the `value`, because the radio button
|
|
// may be swapped out with a similar one and there are some internal apps that depend on
|
|
// that behavior.
|
|
this._buttonChanges = this._radios.changes.subscribe(() => {
|
|
if (this.selected && !this._radios.find(radio => radio === this.selected)) {
|
|
this._selected = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this._buttonChanges?.unsubscribe();
|
|
}
|
|
|
|
/**
|
|
* Mark this group as being "touched" (for ngModel). Meant to be called by the contained
|
|
* radio buttons upon their blur.
|
|
*/
|
|
_touch() {
|
|
if (this.onTouched) {
|
|
this.onTouched();
|
|
}
|
|
}
|
|
|
|
private _updateRadioButtonNames(): void {
|
|
if (this._radios) {
|
|
this._radios.forEach(radio => {
|
|
radio.name = this.name;
|
|
radio._markForCheck();
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Updates the `selected` radio button from the internal _value state. */
|
|
private _updateSelectedRadioFromValue(): void {
|
|
// If the value already matches the selected radio, do nothing.
|
|
const isAlreadySelected = this._selected !== null && this._selected.value === this._value;
|
|
|
|
if (this._radios && !isAlreadySelected) {
|
|
this._selected = null;
|
|
this._radios.forEach(radio => {
|
|
radio.checked = this.value === radio.value;
|
|
if (radio.checked) {
|
|
this._selected = radio;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Dispatch change event with current selection and group value. */
|
|
_emitChangeEvent(): void {
|
|
if (this._isInitialized) {
|
|
this.change.emit(new MatRadioChange(this._selected!, this._value));
|
|
}
|
|
}
|
|
|
|
_markRadiosForCheck() {
|
|
if (this._radios) {
|
|
this._radios.forEach(radio => radio._markForCheck());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the model value. Implemented as part of ControlValueAccessor.
|
|
* @param value
|
|
*/
|
|
writeValue(value: any) {
|
|
this.value = value;
|
|
this._changeDetector.markForCheck();
|
|
}
|
|
|
|
/**
|
|
* Registers a callback to be triggered when the model value changes.
|
|
* Implemented as part of ControlValueAccessor.
|
|
* @param fn Callback to be registered.
|
|
*/
|
|
registerOnChange(fn: (value: any) => void) {
|
|
this._controlValueAccessorChangeFn = fn;
|
|
}
|
|
|
|
/**
|
|
* Registers a callback to be triggered when the control is touched.
|
|
* Implemented as part of ControlValueAccessor.
|
|
* @param fn Callback to be registered.
|
|
*/
|
|
registerOnTouched(fn: any) {
|
|
this.onTouched = fn;
|
|
}
|
|
|
|
/**
|
|
* Sets the disabled state of the control. Implemented as a part of ControlValueAccessor.
|
|
* @param isDisabled Whether the control should be disabled.
|
|
*/
|
|
setDisabledState(isDisabled: boolean) {
|
|
this.disabled = isDisabled;
|
|
this._changeDetector.markForCheck();
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
selector: 'mat-radio-button',
|
|
templateUrl: 'radio.html',
|
|
styleUrl: 'radio.css',
|
|
host: {
|
|
'class': 'mat-mdc-radio-button',
|
|
'[attr.id]': 'id',
|
|
'[class.mat-primary]': 'color === "primary"',
|
|
'[class.mat-accent]': 'color === "accent"',
|
|
'[class.mat-warn]': 'color === "warn"',
|
|
'[class.mat-mdc-radio-checked]': 'checked',
|
|
'[class.mat-mdc-radio-disabled]': 'disabled',
|
|
'[class.mat-mdc-radio-disabled-interactive]': 'disabledInteractive',
|
|
'[class._mat-animation-noopable]': '_noopAnimations',
|
|
// Needs to be removed since it causes some a11y issues (see #21266).
|
|
'[attr.tabindex]': 'null',
|
|
'[attr.aria-label]': 'null',
|
|
'[attr.aria-labelledby]': 'null',
|
|
'[attr.aria-describedby]': 'null',
|
|
// Note: under normal conditions focus shouldn't land on this element, however it may be
|
|
// programmatically set, for example inside of a focus trap, in this case we want to forward
|
|
// the focus to the native element.
|
|
'(focus)': '_inputElement.nativeElement.focus()',
|
|
},
|
|
exportAs: 'matRadioButton',
|
|
encapsulation: ViewEncapsulation.None,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [MatRipple, _MatInternalFormField],
|
|
})
|
|
export class MatRadioButton implements OnInit, AfterViewInit, DoCheck, OnDestroy {
|
|
protected _elementRef = inject(ElementRef);
|
|
private _changeDetector = inject(ChangeDetectorRef);
|
|
private _focusMonitor = inject(FocusMonitor);
|
|
private _radioDispatcher = inject(UniqueSelectionDispatcher);
|
|
private _defaultOptions = inject<MatRadioDefaultOptions>(MAT_RADIO_DEFAULT_OPTIONS, {
|
|
optional: true,
|
|
});
|
|
|
|
private _ngZone = inject(NgZone);
|
|
private _uniqueId: string = inject(_IdGenerator).getId('mat-radio-');
|
|
|
|
/** The unique ID for the radio button. */
|
|
@Input() id: string = this._uniqueId;
|
|
|
|
/** Analog to HTML 'name' attribute used to group radios for unique selection. */
|
|
@Input() name: string;
|
|
|
|
/** Used to set the 'aria-label' attribute on the underlying input element. */
|
|
@Input('aria-label') ariaLabel: string;
|
|
|
|
/** The 'aria-labelledby' attribute takes precedence as the element's text alternative. */
|
|
@Input('aria-labelledby') ariaLabelledby: string;
|
|
|
|
/** The 'aria-describedby' attribute is read after the element's label and field type. */
|
|
@Input('aria-describedby') ariaDescribedby: string;
|
|
|
|
/** Whether ripples are disabled inside the radio button */
|
|
@Input({transform: booleanAttribute})
|
|
disableRipple: boolean = false;
|
|
|
|
/** Tabindex of the radio button. */
|
|
@Input({
|
|
transform: (value: unknown) => (value == null ? 0 : numberAttribute(value)),
|
|
})
|
|
tabIndex: number = 0;
|
|
|
|
/** Whether this radio button is checked. */
|
|
@Input({transform: booleanAttribute})
|
|
get checked(): boolean {
|
|
return this._checked;
|
|
}
|
|
set checked(value: boolean) {
|
|
if (this._checked !== value) {
|
|
this._checked = value;
|
|
if (value && this.radioGroup && this.radioGroup.value !== this.value) {
|
|
this.radioGroup.selected = this;
|
|
} else if (!value && this.radioGroup && this.radioGroup.value === this.value) {
|
|
// When unchecking the selected radio button, update the selected radio
|
|
// property on the group.
|
|
this.radioGroup.selected = null;
|
|
}
|
|
|
|
if (value) {
|
|
// Notify all radio buttons with the same name to un-check.
|
|
this._radioDispatcher.notify(this.id, this.name);
|
|
}
|
|
this._changeDetector.markForCheck();
|
|
}
|
|
}
|
|
|
|
/** The value of this radio button. */
|
|
@Input()
|
|
get value(): any {
|
|
return this._value;
|
|
}
|
|
set value(value: any) {
|
|
if (this._value !== value) {
|
|
this._value = value;
|
|
if (this.radioGroup !== null) {
|
|
if (!this.checked) {
|
|
// Update checked when the value changed to match the radio group's value
|
|
this.checked = this.radioGroup.value === value;
|
|
}
|
|
if (this.checked) {
|
|
this.radioGroup.selected = this;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Whether the label should appear after or before the radio button. Defaults to 'after' */
|
|
@Input()
|
|
get labelPosition(): 'before' | 'after' {
|
|
return this._labelPosition || (this.radioGroup && this.radioGroup.labelPosition) || 'after';
|
|
}
|
|
set labelPosition(value) {
|
|
this._labelPosition = value;
|
|
}
|
|
private _labelPosition: 'before' | 'after';
|
|
|
|
/** Whether the radio button is disabled. */
|
|
@Input({transform: booleanAttribute})
|
|
get disabled(): boolean {
|
|
return this._disabled || (this.radioGroup !== null && this.radioGroup.disabled);
|
|
}
|
|
set disabled(value: boolean) {
|
|
this._setDisabled(value);
|
|
}
|
|
|
|
/** Whether the radio button is required. */
|
|
@Input({transform: booleanAttribute})
|
|
get required(): boolean {
|
|
return this._required || (this.radioGroup && this.radioGroup.required);
|
|
}
|
|
set required(value: boolean) {
|
|
this._required = value;
|
|
}
|
|
|
|
/**
|
|
* Theme color of the radio button. This API is supported in M2 themes only, it
|
|
* has no effect in M3 themes.
|
|
*
|
|
* For information on applying color variants in M3, see
|
|
* https://material.angular.io/guide/theming#using-component-color-variants.
|
|
*/
|
|
@Input()
|
|
get color(): ThemePalette {
|
|
// As per M2 design specifications the selection control radio should use the accent color
|
|
// palette by default. https://m2.material.io/components/radio-buttons#specs
|
|
return (
|
|
this._color ||
|
|
(this.radioGroup && this.radioGroup.color) ||
|
|
(this._defaultOptions && this._defaultOptions.color) ||
|
|
'accent'
|
|
);
|
|
}
|
|
set color(newValue: ThemePalette) {
|
|
this._color = newValue;
|
|
}
|
|
private _color: ThemePalette;
|
|
|
|
/** Whether the radio button should remain interactive when it is disabled. */
|
|
@Input({transform: booleanAttribute})
|
|
get disabledInteractive(): boolean {
|
|
return (
|
|
this._disabledInteractive || (this.radioGroup !== null && this.radioGroup.disabledInteractive)
|
|
);
|
|
}
|
|
set disabledInteractive(value: boolean) {
|
|
this._disabledInteractive = value;
|
|
}
|
|
private _disabledInteractive: boolean;
|
|
|
|
/**
|
|
* Event emitted when the checked state of this radio button changes.
|
|
* Change events are only emitted when the value changes due to user interaction with
|
|
* the radio button (the same behavior as `<input type-"radio">`).
|
|
*/
|
|
@Output() readonly change: EventEmitter<MatRadioChange> = new EventEmitter<MatRadioChange>();
|
|
|
|
/** The parent radio group. May or may not be present. */
|
|
radioGroup: MatRadioGroup;
|
|
|
|
/** ID of the native input element inside `<mat-radio-button>` */
|
|
get inputId(): string {
|
|
return `${this.id || this._uniqueId}-input`;
|
|
}
|
|
|
|
/** Whether this radio is checked. */
|
|
private _checked: boolean = false;
|
|
|
|
/** Whether this radio is disabled. */
|
|
private _disabled: boolean;
|
|
|
|
/** Whether this radio is required. */
|
|
private _required: boolean;
|
|
|
|
/** Value assigned to this radio. */
|
|
private _value: any = null;
|
|
|
|
/** Unregister function for _radioDispatcher */
|
|
private _removeUniqueSelectionListener: () => void = () => {};
|
|
|
|
/** Previous value of the input's tabindex. */
|
|
private _previousTabIndex: number | undefined;
|
|
|
|
/** The native `<input type=radio>` element */
|
|
@ViewChild('input') _inputElement: ElementRef<HTMLInputElement>;
|
|
|
|
/** Trigger elements for the ripple events. */
|
|
@ViewChild('formField', {read: ElementRef, static: true})
|
|
_rippleTrigger: ElementRef<HTMLElement>;
|
|
|
|
/** Whether animations are disabled. */
|
|
_noopAnimations: boolean;
|
|
|
|
private _injector = inject(Injector);
|
|
|
|
constructor(...args: unknown[]);
|
|
|
|
constructor() {
|
|
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
|
|
const radioGroup = inject<MatRadioGroup>(MAT_RADIO_GROUP, {optional: true})!;
|
|
const animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
|
|
const tabIndex = inject(new HostAttributeToken('tabindex'), {optional: true});
|
|
|
|
// Assertions. Ideally these should be stripped out by the compiler.
|
|
// TODO(jelbourn): Assert that there's no name binding AND a parent radio group.
|
|
this.radioGroup = radioGroup;
|
|
this._noopAnimations = animationMode === 'NoopAnimations';
|
|
this._disabledInteractive = this._defaultOptions?.disabledInteractive ?? false;
|
|
|
|
if (tabIndex) {
|
|
this.tabIndex = numberAttribute(tabIndex, 0);
|
|
}
|
|
}
|
|
|
|
/** Focuses the radio button. */
|
|
focus(options?: FocusOptions, origin?: FocusOrigin): void {
|
|
if (origin) {
|
|
this._focusMonitor.focusVia(this._inputElement, origin, options);
|
|
} else {
|
|
this._inputElement.nativeElement.focus(options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks the radio button as needing checking for change detection.
|
|
* This method is exposed because the parent radio group will directly
|
|
* update bound properties of the radio button.
|
|
*/
|
|
_markForCheck() {
|
|
// When group value changes, the button will not be notified. Use `markForCheck` to explicit
|
|
// update radio button's status
|
|
this._changeDetector.markForCheck();
|
|
}
|
|
|
|
ngOnInit() {
|
|
if (this.radioGroup) {
|
|
// If the radio is inside a radio group, determine if it should be checked
|
|
this.checked = this.radioGroup.value === this._value;
|
|
|
|
if (this.checked) {
|
|
this.radioGroup.selected = this;
|
|
}
|
|
|
|
// Copy name from parent radio group
|
|
this.name = this.radioGroup.name;
|
|
}
|
|
|
|
this._removeUniqueSelectionListener = this._radioDispatcher.listen((id, name) => {
|
|
if (id !== this.id && name === this.name) {
|
|
this.checked = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
ngDoCheck(): void {
|
|
this._updateTabIndex();
|
|
}
|
|
|
|
ngAfterViewInit() {
|
|
this._updateTabIndex();
|
|
this._focusMonitor.monitor(this._elementRef, true).subscribe(focusOrigin => {
|
|
if (!focusOrigin && this.radioGroup) {
|
|
this.radioGroup._touch();
|
|
}
|
|
});
|
|
|
|
// We bind this outside of the zone, because:
|
|
// 1. Its logic is completely DOM-related so we can avoid some change detections.
|
|
// 2. There appear to be some internal tests that break when this triggers a change detection.
|
|
this._ngZone.runOutsideAngular(() => {
|
|
this._inputElement.nativeElement.addEventListener('click', this._onInputClick);
|
|
});
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
// We need to null check in case the button was destroyed before `ngAfterViewInit`.
|
|
this._inputElement?.nativeElement.removeEventListener('click', this._onInputClick);
|
|
this._focusMonitor.stopMonitoring(this._elementRef);
|
|
this._removeUniqueSelectionListener();
|
|
}
|
|
|
|
/** Dispatch change event with current value. */
|
|
private _emitChangeEvent(): void {
|
|
this.change.emit(new MatRadioChange(this, this._value));
|
|
}
|
|
|
|
_isRippleDisabled() {
|
|
return this.disableRipple || this.disabled;
|
|
}
|
|
|
|
/** Triggered when the radio button receives an interaction from the user. */
|
|
_onInputInteraction(event: Event) {
|
|
// We always have to stop propagation on the change event.
|
|
// Otherwise the change event, from the input element, will bubble up and
|
|
// emit its event object to the `change` output.
|
|
event.stopPropagation();
|
|
|
|
if (!this.checked && !this.disabled) {
|
|
const groupValueChanged = this.radioGroup && this.value !== this.radioGroup.value;
|
|
this.checked = true;
|
|
this._emitChangeEvent();
|
|
|
|
if (this.radioGroup) {
|
|
this.radioGroup._controlValueAccessorChangeFn(this.value);
|
|
if (groupValueChanged) {
|
|
this.radioGroup._emitChangeEvent();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Triggered when the user clicks on the touch target. */
|
|
_onTouchTargetClick(event: Event) {
|
|
this._onInputInteraction(event);
|
|
|
|
if (!this.disabled || this.disabledInteractive) {
|
|
// Normally the input should be focused already, but if the click
|
|
// comes from the touch target, then we might have to focus it ourselves.
|
|
this._inputElement?.nativeElement.focus();
|
|
}
|
|
}
|
|
|
|
/** Sets the disabled state and marks for check if a change occurred. */
|
|
protected _setDisabled(value: boolean) {
|
|
if (this._disabled !== value) {
|
|
this._disabled = value;
|
|
this._changeDetector.markForCheck();
|
|
}
|
|
}
|
|
|
|
/** Called when the input is clicked. */
|
|
private _onInputClick = (event: Event) => {
|
|
// If the input is disabled while interactive, we need to prevent the
|
|
// selection from happening in this event handler. Note that even though
|
|
// this happens on `click` events, the logic applies when the user is
|
|
// navigating with the keyboard as well. An alternative way of doing
|
|
// this is by resetting the `checked` state in the `change` callback but
|
|
// it isn't optimal, because it can allow a pre-checked disabled button
|
|
// to be un-checked. This approach seems to cover everything.
|
|
if (this.disabled && this.disabledInteractive) {
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
/** Gets the tabindex for the underlying input element. */
|
|
private _updateTabIndex() {
|
|
const group = this.radioGroup;
|
|
let value: number;
|
|
|
|
// Implement a roving tabindex if the button is inside a group. For most cases this isn't
|
|
// necessary, because the browser handles the tab order for inputs inside a group automatically,
|
|
// but we need an explicitly higher tabindex for the selected button in order for things like
|
|
// the focus trap to pick it up correctly.
|
|
if (!group || !group.selected || this.disabled) {
|
|
value = this.tabIndex;
|
|
} else {
|
|
value = group.selected === this ? this.tabIndex : -1;
|
|
}
|
|
|
|
if (value !== this._previousTabIndex) {
|
|
// We have to set the tabindex directly on the DOM node, because it depends on
|
|
// the selected state which is prone to "changed after checked errors".
|
|
const input: HTMLInputElement | undefined = this._inputElement?.nativeElement;
|
|
|
|
if (input) {
|
|
input.setAttribute('tabindex', value + '');
|
|
this._previousTabIndex = value;
|
|
// Wait for any pending tabindex changes to be applied
|
|
afterNextRender(
|
|
() => {
|
|
queueMicrotask(() => {
|
|
// The radio group uses a "selection follows focus" pattern for tab management, so if this
|
|
// radio button is currently focused and another radio button in the group becomes
|
|
// selected, we should move focus to the newly selected radio button to maintain
|
|
// consistency between the focused and selected states.
|
|
if (
|
|
group &&
|
|
group.selected &&
|
|
group.selected !== this &&
|
|
document.activeElement === input
|
|
) {
|
|
group.selected?._inputElement.nativeElement.focus();
|
|
// If this radio button still has focus, the selected one must be disabled. In this
|
|
// case the radio group as a whole should lose focus.
|
|
if (document.activeElement === input) {
|
|
this._inputElement.nativeElement.blur();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{injector: this._injector},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|