1304 lines
46 KiB
TypeScript
1304 lines
46 KiB
TypeScript
|
|
import {dispatchFakeEvent} from '@angular/cdk/testing/private';
|
||
|
|
import {Component, DebugElement, ViewChild} from '@angular/core';
|
||
|
|
import {ComponentFixture, TestBed, fakeAsync, tick, waitForAsync} from '@angular/core/testing';
|
||
|
|
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
|
||
|
|
import {By} from '@angular/platform-browser';
|
||
|
|
import {
|
||
|
|
MAT_RADIO_DEFAULT_OPTIONS,
|
||
|
|
MatRadioButton,
|
||
|
|
MatRadioChange,
|
||
|
|
MatRadioGroup,
|
||
|
|
MatRadioModule,
|
||
|
|
} from './index';
|
||
|
|
|
||
|
|
describe('MatRadio', () => {
|
||
|
|
beforeEach(waitForAsync(() => {
|
||
|
|
TestBed.configureTestingModule({
|
||
|
|
imports: [
|
||
|
|
MatRadioModule,
|
||
|
|
FormsModule,
|
||
|
|
ReactiveFormsModule,
|
||
|
|
DisableableRadioButton,
|
||
|
|
FocusableRadioButton,
|
||
|
|
RadiosInsideRadioGroup,
|
||
|
|
RadioGroupWithNgModel,
|
||
|
|
RadioGroupWithFormControl,
|
||
|
|
StandaloneRadioButtons,
|
||
|
|
InterleavedRadioGroup,
|
||
|
|
TranscludingWrapper,
|
||
|
|
RadioButtonWithPredefinedTabindex,
|
||
|
|
RadioButtonWithPredefinedAriaAttributes,
|
||
|
|
RadiosInsidePreCheckedRadioGroup,
|
||
|
|
PreselectedRadioWithStaticValueAndNgIf,
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}));
|
||
|
|
|
||
|
|
describe('inside of a group', () => {
|
||
|
|
let fixture: ComponentFixture<RadiosInsideRadioGroup>;
|
||
|
|
let groupDebugElement: DebugElement;
|
||
|
|
let radioDebugElements: DebugElement[];
|
||
|
|
let radioNativeElements: HTMLElement[];
|
||
|
|
let radioLabelElements: HTMLLabelElement[];
|
||
|
|
let radioInputElements: HTMLInputElement[];
|
||
|
|
let radioFormFieldElements: HTMLInputElement[];
|
||
|
|
let groupInstance: MatRadioGroup;
|
||
|
|
let radioInstances: MatRadioButton[];
|
||
|
|
let testComponent: RadiosInsideRadioGroup;
|
||
|
|
|
||
|
|
beforeEach(waitForAsync(() => {
|
||
|
|
fixture = TestBed.createComponent(RadiosInsideRadioGroup);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
testComponent = fixture.debugElement.componentInstance;
|
||
|
|
|
||
|
|
groupDebugElement = fixture.debugElement.query(By.directive(MatRadioGroup))!;
|
||
|
|
groupInstance = groupDebugElement.injector.get<MatRadioGroup>(MatRadioGroup);
|
||
|
|
|
||
|
|
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
|
||
|
|
radioNativeElements = radioDebugElements.map(debugEl => debugEl.nativeElement);
|
||
|
|
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
|
||
|
|
|
||
|
|
radioLabelElements = radioDebugElements.map(
|
||
|
|
debugEl => debugEl.query(By.css('label'))!.nativeElement,
|
||
|
|
);
|
||
|
|
radioInputElements = radioDebugElements.map(
|
||
|
|
debugEl => debugEl.query(By.css('input'))!.nativeElement,
|
||
|
|
);
|
||
|
|
radioFormFieldElements = radioDebugElements.map(
|
||
|
|
debugEl => debugEl.query(By.css('.mdc-form-field'))!.nativeElement,
|
||
|
|
);
|
||
|
|
}));
|
||
|
|
|
||
|
|
it('should set individual radio names based on the group name', () => {
|
||
|
|
expect(groupInstance.name).toBeTruthy();
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.name).toBe(groupInstance.name);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should coerce the disabled binding on the radio group', () => {
|
||
|
|
(testComponent as any).isGroupDisabled = '';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioInstances[0].checked).toBe(false);
|
||
|
|
expect(groupInstance.disabled).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should disable click interaction when the group is disabled', () => {
|
||
|
|
testComponent.isGroupDisabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioInstances[0].checked).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should set label position based on the group labelPosition', () => {
|
||
|
|
testComponent.labelPos = 'before';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.labelPosition).toBe('before');
|
||
|
|
}
|
||
|
|
|
||
|
|
testComponent.labelPos = 'after';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.labelPosition).toBe('after');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should disable each individual radio when the group is disabled', () => {
|
||
|
|
testComponent.isGroupDisabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.disabled).toBe(true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should make all disabled buttons interactive if the group is marked as disabledInteractive', () => {
|
||
|
|
testComponent.isGroupDisabledInteractive = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(radioInstances.every(radio => radio.disabledInteractive)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should prevent the click action when disabledInteractive and disabled', () => {
|
||
|
|
testComponent.isGroupDisabled = true;
|
||
|
|
testComponent.isGroupDisabledInteractive = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
// We can't monitor the `defaultPrevented` state on the
|
||
|
|
// native `click` so we dispatch an extra one.
|
||
|
|
const fakeEvent = dispatchFakeEvent(radioInputElements[0], 'click');
|
||
|
|
radioInputElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fakeEvent.defaultPrevented).toBe(true);
|
||
|
|
expect(radioInstances[0].checked).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should set required to each radio button when the group is required', () => {
|
||
|
|
testComponent.isGroupRequired = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.required).toBe(true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should update the group value when one of the radios changes', () => {
|
||
|
|
expect(groupInstance.value).toBeFalsy();
|
||
|
|
|
||
|
|
radioInstances[0].checked = true;
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should update the group and radios when one of the radios is clicked', () => {
|
||
|
|
expect(groupInstance.value).toBeFalsy();
|
||
|
|
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[0]);
|
||
|
|
expect(radioInstances[0].checked).toBe(true);
|
||
|
|
expect(radioInstances[1].checked).toBe(false);
|
||
|
|
|
||
|
|
radioLabelElements[1].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBe('water');
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[1]);
|
||
|
|
expect(radioInstances[0].checked).toBe(false);
|
||
|
|
expect(radioInstances[1].checked).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should check a radio upon interaction with the underlying native radio button', () => {
|
||
|
|
radioInputElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioInstances[0].checked).toBe(true);
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should emit a change event from radio buttons', () => {
|
||
|
|
expect(radioInstances[0].checked).toBe(false);
|
||
|
|
|
||
|
|
const spies = radioInstances.map((radio, index) =>
|
||
|
|
jasmine.createSpy(`onChangeSpy ${index} for ${radio.name}`),
|
||
|
|
);
|
||
|
|
|
||
|
|
spies.forEach((spy, index) => radioInstances[index].change.subscribe(spy));
|
||
|
|
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(spies[0]).toHaveBeenCalled();
|
||
|
|
|
||
|
|
radioLabelElements[1].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
// To match the native radio button behavior, the change event shouldn't
|
||
|
|
// be triggered when the radio got unselected.
|
||
|
|
expect(spies[0]).toHaveBeenCalledTimes(1);
|
||
|
|
expect(spies[1]).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it(`should not emit a change event from the radio group when change group value
|
||
|
|
programmatically`, () => {
|
||
|
|
expect(groupInstance.value).toBeFalsy();
|
||
|
|
|
||
|
|
const changeSpy = jasmine.createSpy('radio-group change listener');
|
||
|
|
groupInstance.change.subscribe(changeSpy);
|
||
|
|
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(changeSpy).toHaveBeenCalledTimes(1);
|
||
|
|
|
||
|
|
groupInstance.value = 'water';
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(changeSpy).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should update the group and radios when updating the group value', () => {
|
||
|
|
expect(groupInstance.value).toBeFalsy();
|
||
|
|
|
||
|
|
testComponent.groupValue = 'fire';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[0]);
|
||
|
|
expect(radioInstances[0].checked).toBe(true);
|
||
|
|
expect(radioInstances[1].checked).toBe(false);
|
||
|
|
|
||
|
|
testComponent.groupValue = 'water';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBe('water');
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[1]);
|
||
|
|
expect(radioInstances[0].checked).toBe(false);
|
||
|
|
expect(radioInstances[1].checked).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should deselect all of the radios when the group value is cleared', () => {
|
||
|
|
radioInstances[0].checked = true;
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBeTruthy();
|
||
|
|
|
||
|
|
groupInstance.value = null;
|
||
|
|
|
||
|
|
expect(radioInstances.every(radio => !radio.checked)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not show ripples on disabled radio buttons', () => {
|
||
|
|
testComponent.isFirstDisabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
dispatchFakeEvent(radioFormFieldElements[0], 'mousedown');
|
||
|
|
dispatchFakeEvent(radioFormFieldElements[0], 'mouseup');
|
||
|
|
|
||
|
|
let rippleAmount = radioNativeElements[0].querySelectorAll(
|
||
|
|
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
|
||
|
|
).length;
|
||
|
|
|
||
|
|
expect(rippleAmount)
|
||
|
|
.withContext('Expected a disabled radio button to not show ripples')
|
||
|
|
.toBe(0);
|
||
|
|
|
||
|
|
testComponent.isFirstDisabled = false;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
dispatchFakeEvent(radioFormFieldElements[0], 'mousedown');
|
||
|
|
dispatchFakeEvent(radioFormFieldElements[0], 'mouseup');
|
||
|
|
|
||
|
|
rippleAmount = radioNativeElements[0].querySelectorAll(
|
||
|
|
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
|
||
|
|
).length;
|
||
|
|
|
||
|
|
expect(rippleAmount).withContext('Expected an enabled radio button to show ripples').toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not show ripples if matRippleDisabled input is set', () => {
|
||
|
|
testComponent.disableRipple = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
for (const radioFormField of radioFormFieldElements) {
|
||
|
|
dispatchFakeEvent(radioFormField, 'mousedown');
|
||
|
|
dispatchFakeEvent(radioFormField, 'mouseup');
|
||
|
|
|
||
|
|
const rippleAmount = radioNativeElements[0].querySelectorAll(
|
||
|
|
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
|
||
|
|
).length;
|
||
|
|
|
||
|
|
expect(rippleAmount).toBe(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
testComponent.disableRipple = false;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
for (const radioFormField of radioFormFieldElements) {
|
||
|
|
dispatchFakeEvent(radioFormField, 'mousedown');
|
||
|
|
dispatchFakeEvent(radioFormField, 'mouseup');
|
||
|
|
|
||
|
|
const rippleAmount = radioNativeElements[0].querySelectorAll(
|
||
|
|
'.mat-ripple-element:not(.mat-radio-persistent-ripple)',
|
||
|
|
).length;
|
||
|
|
|
||
|
|
expect(rippleAmount).toBe(1);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it(`should update the group's selected radio to null when unchecking that radio
|
||
|
|
programmatically`, () => {
|
||
|
|
const changeSpy = jasmine.createSpy('radio-group change listener');
|
||
|
|
groupInstance.change.subscribe(changeSpy);
|
||
|
|
radioInstances[0].checked = true;
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(changeSpy).not.toHaveBeenCalled();
|
||
|
|
expect(groupInstance.value).toBeTruthy();
|
||
|
|
|
||
|
|
radioInstances[0].checked = false;
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(changeSpy).not.toHaveBeenCalled();
|
||
|
|
expect(groupInstance.value).toBeFalsy();
|
||
|
|
expect(radioInstances.every(radio => !radio.checked)).toBe(true);
|
||
|
|
expect(groupInstance.selected).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not fire a change event from the group when a radio checked state changes', () => {
|
||
|
|
const changeSpy = jasmine.createSpy('radio-group change listener');
|
||
|
|
groupInstance.change.subscribe(changeSpy);
|
||
|
|
radioInstances[0].checked = true;
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(changeSpy).not.toHaveBeenCalled();
|
||
|
|
expect(groupInstance.value).toBeTruthy();
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
|
||
|
|
radioInstances[1].checked = true;
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.value).toBe('water');
|
||
|
|
expect(changeSpy).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it(`should update checked status if changed value to radio group's value`, () => {
|
||
|
|
const changeSpy = jasmine.createSpy('radio-group change listener');
|
||
|
|
groupInstance.change.subscribe(changeSpy);
|
||
|
|
groupInstance.value = 'apple';
|
||
|
|
|
||
|
|
expect(changeSpy).not.toHaveBeenCalled();
|
||
|
|
expect(groupInstance.value).toBe('apple');
|
||
|
|
expect(groupInstance.selected).withContext('expect group selected to be null').toBeFalsy();
|
||
|
|
expect(radioInstances[0].checked)
|
||
|
|
.withContext('should not select the first button')
|
||
|
|
.toBeFalsy();
|
||
|
|
expect(radioInstances[1].checked)
|
||
|
|
.withContext('should not select the second button')
|
||
|
|
.toBeFalsy();
|
||
|
|
expect(radioInstances[2].checked)
|
||
|
|
.withContext('should not select the third button')
|
||
|
|
.toBeFalsy();
|
||
|
|
|
||
|
|
radioInstances[0].value = 'apple';
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.selected)
|
||
|
|
.withContext('expect group selected to be first button')
|
||
|
|
.toBe(radioInstances[0]);
|
||
|
|
expect(radioInstances[0].checked)
|
||
|
|
.withContext('expect group select the first button')
|
||
|
|
.toBeTruthy();
|
||
|
|
expect(radioInstances[1].checked)
|
||
|
|
.withContext('should not select the second button')
|
||
|
|
.toBeFalsy();
|
||
|
|
expect(radioInstances[2].checked)
|
||
|
|
.withContext('should not select the third button')
|
||
|
|
.toBeFalsy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should apply class based on color attribute', () => {
|
||
|
|
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-accent')))
|
||
|
|
.withContext('Expected every radio element to use the accent color by default.')
|
||
|
|
.toBe(true);
|
||
|
|
|
||
|
|
testComponent.color = 'primary';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-primary')))
|
||
|
|
.withContext('Expected every radio element to use the primary color from the binding.')
|
||
|
|
.toBe(true);
|
||
|
|
|
||
|
|
testComponent.color = 'warn';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-warn')))
|
||
|
|
.withContext('Expected every radio element to use the primary color from the binding.')
|
||
|
|
.toBe(true);
|
||
|
|
|
||
|
|
testComponent.color = null;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-accent')))
|
||
|
|
.withContext('Expected every radio element to fallback to accent color if value is falsy.')
|
||
|
|
.toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should be able to inherit the color from the radio group', () => {
|
||
|
|
groupInstance.color = 'warn';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElements.every(radioEl => radioEl.classList.contains('mat-warn')))
|
||
|
|
.withContext('Expected every radio element to have the warn color.')
|
||
|
|
.toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should have the individual button color take precedence over the group color', () => {
|
||
|
|
radioInstances[1].color = 'primary';
|
||
|
|
groupInstance.color = 'warn';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElements[0].classList).toContain('mat-warn');
|
||
|
|
expect(radioNativeElements[1].classList).toContain('mat-primary');
|
||
|
|
expect(radioNativeElements[2].classList).toContain('mat-warn');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should have a focus indicator', () => {
|
||
|
|
const radioRippleNativeElements = radioNativeElements.map(
|
||
|
|
element => element.querySelector('.mat-radio-ripple')!,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(
|
||
|
|
radioRippleNativeElements.every(element =>
|
||
|
|
element.classList.contains('mat-focus-indicator'),
|
||
|
|
),
|
||
|
|
).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should set the input tabindex based on the selected radio button', () => {
|
||
|
|
const getTabIndexes = () => {
|
||
|
|
return radioInputElements.map(element => parseInt(element.getAttribute('tabindex') || ''));
|
||
|
|
};
|
||
|
|
|
||
|
|
expect(getTabIndexes()).toEqual([0, 0, 0]);
|
||
|
|
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(getTabIndexes()).toEqual([0, -1, -1]);
|
||
|
|
|
||
|
|
radioLabelElements[1].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(getTabIndexes()).toEqual([-1, 0, -1]);
|
||
|
|
|
||
|
|
radioLabelElements[2].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(getTabIndexes()).toEqual([-1, -1, 0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should set the input tabindex correctly with a pre-checked radio button', () => {
|
||
|
|
const precheckedFixture = TestBed.createComponent(RadiosInsidePreCheckedRadioGroup);
|
||
|
|
precheckedFixture.detectChanges();
|
||
|
|
|
||
|
|
const radios: NodeListOf<HTMLElement> =
|
||
|
|
precheckedFixture.nativeElement.querySelectorAll('mat-radio-button input');
|
||
|
|
|
||
|
|
expect(
|
||
|
|
Array.from(radios).map(radio => {
|
||
|
|
return radio.getAttribute('tabindex');
|
||
|
|
}),
|
||
|
|
).toEqual(['-1', '-1', '0']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should clear the selected radio button but preserve the value on destroy', () => {
|
||
|
|
radioLabelElements[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[0]);
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
|
||
|
|
fixture.componentInstance.isFirstShown = false;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.selected).toBe(null);
|
||
|
|
expect(groupInstance.value).toBe('fire');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('group with ngModel', () => {
|
||
|
|
let fixture: ComponentFixture<RadioGroupWithNgModel>;
|
||
|
|
let groupDebugElement: DebugElement;
|
||
|
|
let radioDebugElements: DebugElement[];
|
||
|
|
let innerRadios: DebugElement[];
|
||
|
|
let radioLabelElements: HTMLLabelElement[];
|
||
|
|
let groupInstance: MatRadioGroup;
|
||
|
|
let radioInstances: MatRadioButton[];
|
||
|
|
let testComponent: RadioGroupWithNgModel;
|
||
|
|
let groupNgModel: NgModel;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
fixture = TestBed.createComponent(RadioGroupWithNgModel);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
testComponent = fixture.debugElement.componentInstance;
|
||
|
|
|
||
|
|
groupDebugElement = fixture.debugElement.query(By.directive(MatRadioGroup))!;
|
||
|
|
groupInstance = groupDebugElement.injector.get<MatRadioGroup>(MatRadioGroup);
|
||
|
|
groupNgModel = groupDebugElement.injector.get<NgModel>(NgModel);
|
||
|
|
|
||
|
|
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
|
||
|
|
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
|
||
|
|
innerRadios = fixture.debugElement.queryAll(By.css('input[type="radio"]'));
|
||
|
|
|
||
|
|
radioLabelElements = radioDebugElements.map(
|
||
|
|
debugEl => debugEl.query(By.css('label'))!.nativeElement,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should set individual radio names based on the group name', () => {
|
||
|
|
expect(groupInstance.name).toBeTruthy();
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.name).toBe(groupInstance.name);
|
||
|
|
}
|
||
|
|
|
||
|
|
groupInstance.name = 'new name';
|
||
|
|
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.name).toBe(groupInstance.name);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should update the name of radio DOM elements if the name of the group changes', () => {
|
||
|
|
const nodes: HTMLInputElement[] = innerRadios.map(radio => radio.nativeElement);
|
||
|
|
|
||
|
|
expect(nodes.every(radio => radio.getAttribute('name') === groupInstance.name))
|
||
|
|
.withContext('Expected all radios to have the initial name.')
|
||
|
|
.toBe(true);
|
||
|
|
|
||
|
|
fixture.componentInstance.groupName = 'changed-name';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupInstance.name).toBe('changed-name');
|
||
|
|
expect(nodes.every(radio => radio.getAttribute('name') === groupInstance.name))
|
||
|
|
.withContext('Expected all radios to have the new name.')
|
||
|
|
.toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should check the corresponding radio button on group value change', () => {
|
||
|
|
expect(groupInstance.value).toBeFalsy();
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.checked).toBeFalsy();
|
||
|
|
}
|
||
|
|
|
||
|
|
groupInstance.value = 'vanilla';
|
||
|
|
for (const radio of radioInstances) {
|
||
|
|
expect(radio.checked).toBe(groupInstance.value === radio.value);
|
||
|
|
}
|
||
|
|
expect(groupInstance.selected!.value).toBe(groupInstance.value);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should have the correct control state initially and after interaction', () => {
|
||
|
|
// The control should start off valid, pristine, and untouched.
|
||
|
|
expect(groupNgModel.valid).toBe(true);
|
||
|
|
expect(groupNgModel.pristine).toBe(true);
|
||
|
|
expect(groupNgModel.touched).toBe(false);
|
||
|
|
|
||
|
|
// After changing the value programmatically, the control should stay pristine
|
||
|
|
// but remain untouched.
|
||
|
|
radioInstances[1].checked = true;
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupNgModel.valid).toBe(true);
|
||
|
|
expect(groupNgModel.pristine).toBe(true);
|
||
|
|
expect(groupNgModel.touched).toBe(false);
|
||
|
|
|
||
|
|
// After a user interaction occurs (such as a click), the control should become dirty and
|
||
|
|
// now also be touched.
|
||
|
|
radioLabelElements[2].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(groupNgModel.valid).toBe(true);
|
||
|
|
expect(groupNgModel.pristine).toBe(false);
|
||
|
|
expect(groupNgModel.touched).toBe(false);
|
||
|
|
|
||
|
|
// Blur the input element in order to verify that the ng-touched state has been set to true.
|
||
|
|
// The touched state should be only set to true after the form control has been blurred.
|
||
|
|
dispatchFakeEvent(innerRadios[2].nativeElement, 'blur');
|
||
|
|
expect(groupNgModel.valid).toBe(true);
|
||
|
|
expect(groupNgModel.pristine).toBe(false);
|
||
|
|
expect(groupNgModel.touched).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should write to the radio button based on ngModel', fakeAsync(() => {
|
||
|
|
testComponent.modelValue = 'chocolate';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
tick();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(innerRadios[1].nativeElement.checked).toBe(true);
|
||
|
|
expect(radioInstances[1].checked).toBe(true);
|
||
|
|
}));
|
||
|
|
|
||
|
|
it('should update the ngModel value when selecting a radio button', () => {
|
||
|
|
dispatchFakeEvent(innerRadios[1].nativeElement, 'change');
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(testComponent.modelValue).toBe('chocolate');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should update the model before firing change event', () => {
|
||
|
|
expect(testComponent.modelValue).toBeUndefined();
|
||
|
|
expect(testComponent.lastEvent).toBeUndefined();
|
||
|
|
|
||
|
|
dispatchFakeEvent(innerRadios[1].nativeElement, 'change');
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(testComponent.lastEvent.value).toBe('chocolate');
|
||
|
|
|
||
|
|
dispatchFakeEvent(innerRadios[0].nativeElement, 'change');
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(testComponent.lastEvent.value).toBe('vanilla');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('group with FormControl', () => {
|
||
|
|
it('should toggle the disabled state', () => {
|
||
|
|
const fixture = TestBed.createComponent(RadioGroupWithFormControl);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fixture.componentInstance.group.disabled).toBeFalsy();
|
||
|
|
|
||
|
|
fixture.componentInstance.formControl.disable();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fixture.componentInstance.group.disabled).toBeTruthy();
|
||
|
|
|
||
|
|
fixture.componentInstance.formControl.enable();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fixture.componentInstance.group.disabled).toBeFalsy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should have a selected button when one matches the initial value', () => {
|
||
|
|
const fixture = TestBed.createComponent(RadioGroupWithFormControl);
|
||
|
|
fixture.componentInstance.formControl.setValue('2');
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fixture.componentInstance.group.selected?.value).toBe('2');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('disableable', () => {
|
||
|
|
let fixture: ComponentFixture<DisableableRadioButton>;
|
||
|
|
let radioInstance: MatRadioButton;
|
||
|
|
let radioNativeElement: HTMLInputElement;
|
||
|
|
let radioHost: HTMLElement;
|
||
|
|
let testComponent: DisableableRadioButton;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
fixture = TestBed.createComponent(DisableableRadioButton);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
testComponent = fixture.debugElement.componentInstance;
|
||
|
|
const radioDebugElement = fixture.debugElement.query(By.directive(MatRadioButton))!;
|
||
|
|
radioHost = radioDebugElement.nativeElement;
|
||
|
|
radioInstance = radioDebugElement.injector.get<MatRadioButton>(MatRadioButton);
|
||
|
|
radioNativeElement = radioHost.querySelector('input')!;
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should toggle the disabled state', () => {
|
||
|
|
expect(radioInstance.disabled).toBeFalsy();
|
||
|
|
expect(radioNativeElement.disabled).toBeFalsy();
|
||
|
|
|
||
|
|
testComponent.disabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(radioInstance.disabled).toBeTruthy();
|
||
|
|
expect(radioNativeElement.disabled).toBeTruthy();
|
||
|
|
|
||
|
|
testComponent.disabled = false;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(radioInstance.disabled).toBeFalsy();
|
||
|
|
expect(radioNativeElement.disabled).toBeFalsy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should keep the button interactive if disabledInteractive is enabled', () => {
|
||
|
|
testComponent.disabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElement.disabled).toBe(true);
|
||
|
|
expect(radioNativeElement.hasAttribute('aria-disabled')).toBe(false);
|
||
|
|
expect(radioHost.classList).not.toContain('mat-mdc-radio-disabled-interactive');
|
||
|
|
|
||
|
|
testComponent.disabledInteractive = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioNativeElement.disabled).toBe(false);
|
||
|
|
expect(radioNativeElement.getAttribute('aria-disabled')).toBe('true');
|
||
|
|
expect(radioHost.classList).toContain('mat-mdc-radio-disabled-interactive');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('as standalone', () => {
|
||
|
|
let fixture: ComponentFixture<StandaloneRadioButtons>;
|
||
|
|
let radioDebugElements: DebugElement[];
|
||
|
|
let seasonRadioInstances: MatRadioButton[];
|
||
|
|
let weatherRadioInstances: MatRadioButton[];
|
||
|
|
let fruitRadioInstances: MatRadioButton[];
|
||
|
|
let fruitRadioNativeElements: HTMLElement[];
|
||
|
|
let fruitRadioNativeInputs: HTMLElement[];
|
||
|
|
let testComponent: StandaloneRadioButtons;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
fixture = TestBed.createComponent(StandaloneRadioButtons);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
testComponent = fixture.debugElement.componentInstance;
|
||
|
|
|
||
|
|
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
|
||
|
|
seasonRadioInstances = radioDebugElements
|
||
|
|
.filter(debugEl => debugEl.componentInstance.name == 'season')
|
||
|
|
.map(debugEl => debugEl.componentInstance);
|
||
|
|
weatherRadioInstances = radioDebugElements
|
||
|
|
.filter(debugEl => debugEl.componentInstance.name == 'weather')
|
||
|
|
.map(debugEl => debugEl.componentInstance);
|
||
|
|
fruitRadioInstances = radioDebugElements
|
||
|
|
.filter(debugEl => debugEl.componentInstance.name == 'fruit')
|
||
|
|
.map(debugEl => debugEl.componentInstance);
|
||
|
|
fruitRadioNativeElements = radioDebugElements
|
||
|
|
.filter(debugEl => debugEl.componentInstance.name == 'fruit')
|
||
|
|
.map(debugEl => debugEl.nativeElement);
|
||
|
|
|
||
|
|
fruitRadioNativeInputs = [];
|
||
|
|
for (const element of fruitRadioNativeElements) {
|
||
|
|
fruitRadioNativeInputs.push(<HTMLElement>element.querySelector('input'));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should uniquely select radios by a name', () => {
|
||
|
|
seasonRadioInstances[0].checked = true;
|
||
|
|
weatherRadioInstances[1].checked = true;
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(seasonRadioInstances[0].checked).toBe(true);
|
||
|
|
expect(seasonRadioInstances[1].checked).toBe(false);
|
||
|
|
expect(seasonRadioInstances[2].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[0].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[1].checked).toBe(true);
|
||
|
|
expect(weatherRadioInstances[2].checked).toBe(false);
|
||
|
|
|
||
|
|
seasonRadioInstances[1].checked = true;
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(seasonRadioInstances[0].checked).toBe(false);
|
||
|
|
expect(seasonRadioInstances[1].checked).toBe(true);
|
||
|
|
expect(seasonRadioInstances[2].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[0].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[1].checked).toBe(true);
|
||
|
|
expect(weatherRadioInstances[2].checked).toBe(false);
|
||
|
|
|
||
|
|
weatherRadioInstances[2].checked = true;
|
||
|
|
expect(seasonRadioInstances[0].checked).toBe(false);
|
||
|
|
expect(seasonRadioInstances[1].checked).toBe(true);
|
||
|
|
expect(seasonRadioInstances[2].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[0].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[1].checked).toBe(false);
|
||
|
|
expect(weatherRadioInstances[2].checked).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add required attribute to the underlying input element if defined', () => {
|
||
|
|
const radioInstance = seasonRadioInstances[0];
|
||
|
|
radioInstance.required = true;
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioInstance.required).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add value attribute to the underlying input element', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('value')).toBe('banana');
|
||
|
|
expect(fruitRadioNativeInputs[1].getAttribute('value')).toBe('raspberry');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add aria-label attribute to the underlying input element if defined', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Banana');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not add aria-label attribute if not defined', () => {
|
||
|
|
expect(fruitRadioNativeInputs[1].hasAttribute('aria-label')).toBeFalsy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should change aria-label attribute if property is changed at runtime', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Banana');
|
||
|
|
|
||
|
|
testComponent.ariaLabel = 'Pineapple';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-label')).toBe('Pineapple');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add aria-labelledby attribute to the underlying input element if defined', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('xyz');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not add aria-labelledby attribute if not defined', () => {
|
||
|
|
expect(fruitRadioNativeInputs[1].hasAttribute('aria-labelledby')).toBeFalsy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should change aria-labelledby attribute if property is changed at runtime', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('xyz');
|
||
|
|
|
||
|
|
testComponent.ariaLabelledby = 'uvw';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('uvw');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should add aria-describedby attribute to the underlying input element if defined', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-describedby')).toBe('abc');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not add aria-describedby attribute if not defined', () => {
|
||
|
|
expect(fruitRadioNativeInputs[1].hasAttribute('aria-describedby')).toBeFalsy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should change aria-describedby attribute if property is changed at runtime', () => {
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-describedby')).toBe('abc');
|
||
|
|
|
||
|
|
testComponent.ariaDescribedby = 'uvw';
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fruitRadioNativeInputs[0].getAttribute('aria-describedby')).toBe('uvw');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should focus on underlying input element when focus() is called', () => {
|
||
|
|
for (let i = 0; i < fruitRadioInstances.length; i++) {
|
||
|
|
expect(document.activeElement).not.toBe(fruitRadioNativeInputs[i]);
|
||
|
|
fruitRadioInstances[i].focus();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(document.activeElement).toBe(fruitRadioNativeInputs[i]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should focus on underlying input element when clicking on the touch target', () => {
|
||
|
|
const input = radioDebugElements[0].nativeElement.querySelector('input');
|
||
|
|
expect(document.activeElement).not.toBe(input);
|
||
|
|
|
||
|
|
radioDebugElements[0].nativeElement.querySelector('.mat-mdc-radio-touch-target').click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(document.activeElement).toBe(input);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not change focus origin if origin not specified', () => {
|
||
|
|
fruitRadioInstances[0].focus(undefined, 'mouse');
|
||
|
|
fruitRadioInstances[1].focus();
|
||
|
|
|
||
|
|
expect(fruitRadioNativeElements[1].classList).toContain('cdk-focused');
|
||
|
|
expect(fruitRadioNativeElements[1].classList).toContain('cdk-mouse-focused');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not add the "name" attribute if it is not passed in', () => {
|
||
|
|
const radio = fixture.debugElement.nativeElement.querySelector('#nameless input');
|
||
|
|
expect(radio.hasAttribute('name')).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should default the radio color to `accent`', () => {
|
||
|
|
expect(seasonRadioInstances.every(radio => radio.color === 'accent')).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('with tabindex', () => {
|
||
|
|
let fixture: ComponentFixture<FocusableRadioButton>;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
fixture = TestBed.createComponent(FocusableRadioButton);
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should forward focus to native input', () => {
|
||
|
|
let radioButtonEl = fixture.debugElement.query(
|
||
|
|
By.css('.mat-mdc-radio-button'),
|
||
|
|
)!.nativeElement;
|
||
|
|
let inputEl = fixture.debugElement.query(By.css('.mdc-radio__native-control'))!.nativeElement;
|
||
|
|
|
||
|
|
radioButtonEl.focus();
|
||
|
|
// Focus events don't always fire in tests, so we need to fake it.
|
||
|
|
dispatchFakeEvent(radioButtonEl, 'focus');
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(document.activeElement).toBe(inputEl);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should allow specifying an explicit tabindex for a single radio-button', () => {
|
||
|
|
const radioButtonInput = fixture.debugElement.query(By.css('.mat-mdc-radio-button input'))!
|
||
|
|
.nativeElement as HTMLInputElement;
|
||
|
|
|
||
|
|
expect(radioButtonInput.tabIndex)
|
||
|
|
.withContext('Expected the tabindex to be set to "0" by default.')
|
||
|
|
.toBe(0);
|
||
|
|
|
||
|
|
fixture.componentInstance.tabIndex = 4;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioButtonInput.tabIndex)
|
||
|
|
.withContext('Expected the tabindex to be set to "4".')
|
||
|
|
.toBe(4);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should remove the tabindex from the host element', () => {
|
||
|
|
const predefinedFixture = TestBed.createComponent(RadioButtonWithPredefinedTabindex);
|
||
|
|
predefinedFixture.detectChanges();
|
||
|
|
|
||
|
|
const radioButtonEl = predefinedFixture.debugElement.query(
|
||
|
|
By.css('.mat-mdc-radio-button'),
|
||
|
|
)!.nativeElement;
|
||
|
|
|
||
|
|
expect(radioButtonEl.hasAttribute('tabindex')).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should forward a pre-defined tabindex to the underlying input', () => {
|
||
|
|
const predefinedFixture = TestBed.createComponent(RadioButtonWithPredefinedTabindex);
|
||
|
|
predefinedFixture.detectChanges();
|
||
|
|
|
||
|
|
const radioButtonInput = predefinedFixture.debugElement.query(
|
||
|
|
By.css('.mat-mdc-radio-button input'),
|
||
|
|
)!.nativeElement as HTMLInputElement;
|
||
|
|
|
||
|
|
expect(radioButtonInput.getAttribute('tabindex')).toBe('5');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should remove the aria attributes from the host element', () => {
|
||
|
|
const predefinedFixture = TestBed.createComponent(RadioButtonWithPredefinedAriaAttributes);
|
||
|
|
predefinedFixture.detectChanges();
|
||
|
|
|
||
|
|
const radioButtonEl = predefinedFixture.debugElement.query(
|
||
|
|
By.css('.mat-mdc-radio-button'),
|
||
|
|
)!.nativeElement;
|
||
|
|
|
||
|
|
expect(radioButtonEl.hasAttribute('aria-label')).toBe(false);
|
||
|
|
expect(radioButtonEl.hasAttribute('aria-describedby')).toBe(false);
|
||
|
|
expect(radioButtonEl.hasAttribute('aria-labelledby')).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should remove the tabindex from the host element when disabled', () => {
|
||
|
|
const radioButton = fixture.debugElement.query(By.css('.mat-mdc-radio-button')).nativeElement;
|
||
|
|
|
||
|
|
fixture.componentInstance.disabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(radioButton.hasAttribute('tabindex')).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('group interspersed with other tags', () => {
|
||
|
|
let fixture: ComponentFixture<InterleavedRadioGroup>;
|
||
|
|
let groupDebugElement: DebugElement;
|
||
|
|
let groupInstance: MatRadioGroup;
|
||
|
|
let radioDebugElements: DebugElement[];
|
||
|
|
let radioInstances: MatRadioButton[];
|
||
|
|
|
||
|
|
beforeEach(waitForAsync(() => {
|
||
|
|
fixture = TestBed.createComponent(InterleavedRadioGroup);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
groupDebugElement = fixture.debugElement.query(By.directive(MatRadioGroup))!;
|
||
|
|
groupInstance = groupDebugElement.injector.get<MatRadioGroup>(MatRadioGroup);
|
||
|
|
radioDebugElements = fixture.debugElement.queryAll(By.directive(MatRadioButton));
|
||
|
|
radioInstances = radioDebugElements.map(debugEl => debugEl.componentInstance);
|
||
|
|
}));
|
||
|
|
|
||
|
|
it('should initialize selection of radios based on model value', () => {
|
||
|
|
expect(groupInstance.selected).toBe(radioInstances[2]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should preselect a radio button with a static value and an ngIf', () => {
|
||
|
|
const fixture = TestBed.createComponent(PreselectedRadioWithStaticValueAndNgIf);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(fixture.componentInstance.preselectedGroup.value).toBe('b');
|
||
|
|
expect(fixture.componentInstance.preselectedRadio.checked).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('MatRadioDefaultOverrides', () => {
|
||
|
|
describe('when MAT_RADIO_DEFAULT_OPTIONS overridden', () => {
|
||
|
|
beforeEach(waitForAsync(() => {
|
||
|
|
TestBed.configureTestingModule({
|
||
|
|
imports: [MatRadioModule, FormsModule, DefaultRadioButton, RadioButtonWithColorBinding],
|
||
|
|
providers: [
|
||
|
|
{
|
||
|
|
provide: MAT_RADIO_DEFAULT_OPTIONS,
|
||
|
|
useValue: {color: 'primary'},
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
}));
|
||
|
|
it('should override default color in Component', () => {
|
||
|
|
const fixture: ComponentFixture<DefaultRadioButton> =
|
||
|
|
TestBed.createComponent(DefaultRadioButton);
|
||
|
|
fixture.detectChanges();
|
||
|
|
const radioDebugElement: DebugElement = fixture.debugElement.query(
|
||
|
|
By.directive(MatRadioButton),
|
||
|
|
)!;
|
||
|
|
expect(radioDebugElement.nativeElement.classList).toContain('mat-primary');
|
||
|
|
});
|
||
|
|
it('should not override explicit input bindings', () => {
|
||
|
|
const fixture: ComponentFixture<RadioButtonWithColorBinding> = TestBed.createComponent(
|
||
|
|
RadioButtonWithColorBinding,
|
||
|
|
);
|
||
|
|
fixture.detectChanges();
|
||
|
|
const radioDebugElement: DebugElement = fixture.debugElement.query(
|
||
|
|
By.directive(MatRadioButton),
|
||
|
|
)!;
|
||
|
|
expect(radioDebugElement.nativeElement.classList).not.toContain('mat-primary');
|
||
|
|
expect(radioDebugElement.nativeElement.classList).toContain('mat-warn');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-group
|
||
|
|
[disabled]="isGroupDisabled"
|
||
|
|
[labelPosition]="labelPos"
|
||
|
|
[required]="isGroupRequired"
|
||
|
|
[value]="groupValue"
|
||
|
|
[disabledInteractive]="isGroupDisabledInteractive"
|
||
|
|
name="test-name">
|
||
|
|
@if (isFirstShown) {
|
||
|
|
<mat-radio-button value="fire" [disableRipple]="disableRipple" [disabled]="isFirstDisabled"
|
||
|
|
[color]="color">
|
||
|
|
Charmander
|
||
|
|
</mat-radio-button>
|
||
|
|
}
|
||
|
|
<mat-radio-button value="water" [disableRipple]="disableRipple" [color]="color">
|
||
|
|
Squirtle
|
||
|
|
</mat-radio-button>
|
||
|
|
<mat-radio-button value="leaf" [disableRipple]="disableRipple" [color]="color">
|
||
|
|
Bulbasaur
|
||
|
|
</mat-radio-button>
|
||
|
|
</mat-radio-group>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class RadiosInsideRadioGroup {
|
||
|
|
labelPos: 'before' | 'after';
|
||
|
|
isFirstDisabled = false;
|
||
|
|
isGroupDisabled = false;
|
||
|
|
isGroupRequired = false;
|
||
|
|
isGroupDisabledInteractive = false;
|
||
|
|
groupValue: string | null = null;
|
||
|
|
disableRipple = false;
|
||
|
|
color: string | null;
|
||
|
|
isFirstShown = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-group name="test-name">
|
||
|
|
<mat-radio-button value="fire">Charmander</mat-radio-button>
|
||
|
|
<mat-radio-button value="water">Squirtle</mat-radio-button>
|
||
|
|
<mat-radio-button value="leaf" checked>Bulbasaur</mat-radio-button>
|
||
|
|
</mat-radio-group>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class RadiosInsidePreCheckedRadioGroup {}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-button name="season" value="spring">Spring</mat-radio-button>
|
||
|
|
<mat-radio-button name="season" value="summer">Summer</mat-radio-button>
|
||
|
|
<mat-radio-button name="season" value="autum">Autumn</mat-radio-button>
|
||
|
|
|
||
|
|
<mat-radio-button name="weather" value="warm">Spring</mat-radio-button>
|
||
|
|
<mat-radio-button name="weather" value="hot">Summer</mat-radio-button>
|
||
|
|
<mat-radio-button name="weather" value="cool">Autumn</mat-radio-button>
|
||
|
|
|
||
|
|
<span id="xyz">Baby Banana</span>
|
||
|
|
<span id="abc">A smaller banana</span>
|
||
|
|
<mat-radio-button name="fruit"
|
||
|
|
value="banana"
|
||
|
|
[aria-label]="ariaLabel"
|
||
|
|
[aria-labelledby]="ariaLabelledby"
|
||
|
|
[aria-describedby]="ariaDescribedby">
|
||
|
|
</mat-radio-button>
|
||
|
|
<mat-radio-button name="fruit" value="raspberry">Raspberry</mat-radio-button>
|
||
|
|
<mat-radio-button id="nameless" value="no-name">No name</mat-radio-button>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class StandaloneRadioButtons {
|
||
|
|
ariaLabel: string = 'Banana';
|
||
|
|
ariaLabelledby: string = 'xyz';
|
||
|
|
ariaDescribedby: string = 'abc';
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-group [name]="groupName" [(ngModel)]="modelValue" (change)="lastEvent = $event">
|
||
|
|
@for (option of options; track option) {
|
||
|
|
<mat-radio-button [value]="option.value">{{option.label}}</mat-radio-button>
|
||
|
|
}
|
||
|
|
</mat-radio-group>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class RadioGroupWithNgModel {
|
||
|
|
modelValue: string;
|
||
|
|
groupName = 'radio-group';
|
||
|
|
options = [
|
||
|
|
{label: 'Vanilla', value: 'vanilla'},
|
||
|
|
{label: 'Chocolate', value: 'chocolate'},
|
||
|
|
{label: 'Strawberry', value: 'strawberry'},
|
||
|
|
];
|
||
|
|
lastEvent: MatRadioChange;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-button
|
||
|
|
[disabled]="disabled"
|
||
|
|
[disabledInteractive]="disabledInteractive">One</mat-radio-button>`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class DisableableRadioButton {
|
||
|
|
disabled = false;
|
||
|
|
disabledInteractive = false;
|
||
|
|
|
||
|
|
@ViewChild(MatRadioButton) matRadioButton: MatRadioButton;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-group [formControl]="formControl">
|
||
|
|
<mat-radio-button value="1">One</mat-radio-button>
|
||
|
|
<mat-radio-button value="2">Two</mat-radio-button>
|
||
|
|
</mat-radio-group>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class RadioGroupWithFormControl {
|
||
|
|
@ViewChild(MatRadioGroup) group: MatRadioGroup;
|
||
|
|
formControl = new FormControl('');
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `<mat-radio-button [disabled]="disabled" [tabIndex]="tabIndex"></mat-radio-button>`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class FocusableRadioButton {
|
||
|
|
tabIndex: number;
|
||
|
|
disabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
selector: 'transcluding-wrapper',
|
||
|
|
template: `
|
||
|
|
<div><ng-content></ng-content></div>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class TranscludingWrapper {}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-group name="group" [(ngModel)]="modelValue">
|
||
|
|
@for (option of options; track option) {
|
||
|
|
<transcluding-wrapper>
|
||
|
|
<mat-radio-button [value]="option.value">{{option.label}}</mat-radio-button>
|
||
|
|
</transcluding-wrapper>
|
||
|
|
}
|
||
|
|
</mat-radio-group>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule, TranscludingWrapper],
|
||
|
|
})
|
||
|
|
class InterleavedRadioGroup {
|
||
|
|
modelValue = 'strawberry';
|
||
|
|
options = [
|
||
|
|
{label: 'Vanilla', value: 'vanilla'},
|
||
|
|
{label: 'Chocolate', value: 'chocolate'},
|
||
|
|
{label: 'Strawberry', value: 'strawberry'},
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `<mat-radio-button tabindex="5"></mat-radio-button>`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class RadioButtonWithPredefinedTabindex {}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `<mat-radio-button></mat-radio-button>`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule],
|
||
|
|
})
|
||
|
|
class DefaultRadioButton {}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `<mat-radio-button color="warn"></mat-radio-button>`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule],
|
||
|
|
})
|
||
|
|
class RadioButtonWithColorBinding {}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-radio-button
|
||
|
|
aria-label="Radio button"
|
||
|
|
aria-describedby="something"
|
||
|
|
aria-labelledby="something-else"></mat-radio-button>`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class RadioButtonWithPredefinedAriaAttributes {}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
// Note that this is somewhat of a contrived template, but it is required to
|
||
|
|
// reproduce the issue. It was taken for a specific user report at #25831.
|
||
|
|
template: `
|
||
|
|
@if (true) {
|
||
|
|
<mat-radio-group [formControl]="controls.predecessor">
|
||
|
|
<mat-radio-button value="predecessor"></mat-radio-button>
|
||
|
|
</mat-radio-group>
|
||
|
|
}
|
||
|
|
<mat-radio-group [formControl]="controls.target" #preselectedGroup>
|
||
|
|
<mat-radio-button value="a"></mat-radio-button>
|
||
|
|
@if (true) {
|
||
|
|
<mat-radio-button value="b" #preselectedRadio></mat-radio-button>
|
||
|
|
}
|
||
|
|
</mat-radio-group>
|
||
|
|
`,
|
||
|
|
standalone: true,
|
||
|
|
imports: [MatRadioModule, FormsModule, ReactiveFormsModule],
|
||
|
|
})
|
||
|
|
class PreselectedRadioWithStaticValueAndNgIf {
|
||
|
|
@ViewChild('preselectedGroup', {read: MatRadioGroup}) preselectedGroup: MatRadioGroup;
|
||
|
|
@ViewChild('preselectedRadio', {read: MatRadioButton}) preselectedRadio: MatRadioButton;
|
||
|
|
|
||
|
|
controls = {
|
||
|
|
predecessor: new FormControl('predecessor'),
|
||
|
|
target: new FormControl('b'),
|
||
|
|
};
|
||
|
|
}
|