Skip to content

Commit 6c846e2

Browse files
authored
fix(material/radio): clear selected radio button from group (#27466)
In #18081 the radio group was changed so that deselected buttons receive `tabindex="-1"` when there's a selected button. The problem is that we weren't clearing the reference to the selected button so it gets removed, the deselected buttons become unfocusable using the keyboard. These changes clear the selected radio button on destroy. Fixes #27461.
1 parent 6045a0c commit 6c846e2

File tree

3 files changed

+41
-7
lines changed

3 files changed

+41
-7
lines changed

src/material/radio/radio.spec.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,19 @@ describe('MDC-based MatRadio', () => {
471471
}),
472472
).toEqual(['-1', '-1', '0']);
473473
});
474+
475+
it('should clear the selected radio button but preserve the value on destroy', () => {
476+
radioLabelElements[0].click();
477+
fixture.detectChanges();
478+
expect(groupInstance.selected).toBe(radioInstances[0]);
479+
expect(groupInstance.value).toBe('fire');
480+
481+
fixture.componentInstance.isFirstShown = false;
482+
fixture.detectChanges();
483+
484+
expect(groupInstance.selected).toBe(null);
485+
expect(groupInstance.value).toBe('fire');
486+
});
474487
});
475488

476489
describe('group with ngModel', () => {
@@ -995,7 +1008,7 @@ describe('MatRadioDefaultOverrides', () => {
9951008
[value]="groupValue"
9961009
name="test-name">
9971010
<mat-radio-button value="fire" [disableRipple]="disableRipple" [disabled]="isFirstDisabled"
998-
[color]="color">
1011+
[color]="color" *ngIf="isFirstShown">
9991012
Charmander
10001013
</mat-radio-button>
10011014
<mat-radio-button value="water" [disableRipple]="disableRipple" [color]="color">
@@ -1009,12 +1022,13 @@ describe('MatRadioDefaultOverrides', () => {
10091022
})
10101023
class RadiosInsideRadioGroup {
10111024
labelPos: 'before' | 'after';
1012-
isFirstDisabled: boolean = false;
1013-
isGroupDisabled: boolean = false;
1014-
isGroupRequired: boolean = false;
1025+
isFirstDisabled = false;
1026+
isGroupDisabled = false;
1027+
isGroupRequired = false;
10151028
groupValue: string | null = null;
1016-
disableRipple: boolean = false;
1029+
disableRipple = false;
10171030
color: string | null;
1031+
isFirstShown = true;
10181032
}
10191033

10201034
@Component({

src/material/radio/radio.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {BooleanInput, coerceBooleanProperty, coerceNumberProperty} from '@angula
4242
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
4343
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
4444
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
45+
import {Subscription} from 'rxjs';
4546

4647
// Increasing integer for generating unique ids for radio components.
4748
let nextUniqueId = 0;
@@ -100,7 +101,7 @@ export function MAT_RADIO_DEFAULT_OPTIONS_FACTORY(): MatRadioDefaultOptions {
100101
*/
101102
@Directive()
102103
export abstract class _MatRadioGroupBase<T extends _MatRadioButtonBase>
103-
implements AfterContentInit, ControlValueAccessor
104+
implements AfterContentInit, OnDestroy, ControlValueAccessor
104105
{
105106
/** Selected value for the radio group. */
106107
private _value: any = null;
@@ -123,6 +124,9 @@ export abstract class _MatRadioGroupBase<T extends _MatRadioButtonBase>
123124
/** Whether the radio group is required. */
124125
private _required: boolean = false;
125126

127+
/** Subscription to changes in amount of radio buttons. */
128+
private _buttonChanges: Subscription;
129+
126130
/** The method to be called in order to update ngModel */
127131
_controlValueAccessorChangeFn: (value: any) => void = () => {};
128132

@@ -236,6 +240,20 @@ export abstract class _MatRadioGroupBase<T extends _MatRadioButtonBase>
236240
// possibly be set by NgModel on MatRadioGroup, and it is possible that the OnInit of the
237241
// NgModel occurs *after* the OnInit of the MatRadioGroup.
238242
this._isInitialized = true;
243+
244+
// Clear the `selected` button when it's destroyed since the tabindex of the rest of the
245+
// buttons depends on it. Note that we don't clear the `value`, because the radio button
246+
// may be swapped out with a similar one and there are some internal apps that depend on
247+
// that behavior.
248+
this._buttonChanges = this._radios.changes.subscribe(() => {
249+
if (this.selected && !this._radios.find(radio => radio === this.selected)) {
250+
this._selected = null;
251+
}
252+
});
253+
}
254+
255+
ngOnDestroy() {
256+
this._buttonChanges?.unsubscribe();
239257
}
240258

241259
/**

tools/public_api_guard/material/radio.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class MatRadioGroup extends _MatRadioGroupBase<MatRadioButton> {
124124
}
125125

126126
// @public
127-
export abstract class _MatRadioGroupBase<T extends _MatRadioButtonBase> implements AfterContentInit, ControlValueAccessor {
127+
export abstract class _MatRadioGroupBase<T extends _MatRadioButtonBase> implements AfterContentInit, OnDestroy, ControlValueAccessor {
128128
constructor(_changeDetector: ChangeDetectorRef);
129129
readonly change: EventEmitter<MatRadioChange>;
130130
// (undocumented)
@@ -141,6 +141,8 @@ export abstract class _MatRadioGroupBase<T extends _MatRadioButtonBase> implemen
141141
get name(): string;
142142
set name(value: string);
143143
ngAfterContentInit(): void;
144+
// (undocumented)
145+
ngOnDestroy(): void;
144146
onTouched: () => any;
145147
abstract _radios: QueryList<T>;
146148
registerOnChange(fn: (value: any) => void): void;

0 commit comments

Comments
 (0)