diff --git a/src/material/checkbox/checkbox.html b/src/material/checkbox/checkbox.html index 9cac64e875b7..759f09f64bed 100644 --- a/src/material/checkbox/checkbox.html +++ b/src/material/checkbox/checkbox.html @@ -10,7 +10,10 @@ [attr.aria-labelledby]="ariaLabelledby" [attr.aria-describedby]="ariaDescribedby" [attr.aria-checked]="indeterminate ? 'mixed' : null" + [attr.aria-controls]="ariaControls" [attr.aria-disabled]="disabled && disabledInteractive ? true : null" + [attr.aria-expanded]="ariaExpanded" + [attr.aria-owns]="ariaOwns" [attr.name]="name" [attr.value]="value" [checked]="checked" diff --git a/src/material/checkbox/checkbox.md b/src/material/checkbox/checkbox.md index 0260fa3cf6dd..0cfb43e1bd12 100644 --- a/src/material/checkbox/checkbox.md +++ b/src/material/checkbox/checkbox.md @@ -74,3 +74,9 @@ binding these properties, as demonstrated below. ``` + +Additionally, `MatCheckbox` now supports the following accessibility properties: + +- **`aria-expanded`**: Indicates whether the checkbox controls the visibility of another element. This should be a boolean value (`true` or `false`). +- **`aria-controls`**: Specifies the ID of the element that the checkbox controls. +- **`aria-owns`**: Specifies the ID of the element that the checkbox visually owns. diff --git a/src/material/checkbox/checkbox.spec.ts b/src/material/checkbox/checkbox.spec.ts index 9c029d2bc8e2..35fb4441d91e 100644 --- a/src/material/checkbox/checkbox.spec.ts +++ b/src/material/checkbox/checkbox.spec.ts @@ -807,6 +807,94 @@ describe('MatCheckbox', () => { }); }); + describe('with provided aria-expanded', () => { + let checkboxDebugElement: DebugElement; + let checkboxNativeElement: HTMLElement; + let inputElement: HTMLInputElement; + + it('should use the provided postive aria-expanded', () => { + fixture = createComponent(CheckboxWithPositiveAriaExpanded); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-expanded')).toBe('true'); + }); + + it('should use the provided negative aria-expanded', () => { + fixture = createComponent(CheckboxWithNegativeAriaExpanded); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-expanded')).toBe('false'); + }); + + it('should not assign aria-expanded if none is provided', () => { + fixture = createComponent(SingleCheckbox); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-expanded')).toBe(null); + }); + }); + + describe('with provided aria-controls', () => { + let checkboxDebugElement: DebugElement; + let checkboxNativeElement: HTMLElement; + let inputElement: HTMLInputElement; + + it('should use the provided aria-controls', () => { + fixture = createComponent(CheckboxWithAriaControls); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-controls')).toBe('some-id'); + }); + + it('should not assign aria-controls if none is provided', () => { + fixture = createComponent(SingleCheckbox); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-controls')).toBe(null); + }); + }); + + describe('with provided aria-owns', () => { + let checkboxDebugElement: DebugElement; + let checkboxNativeElement: HTMLElement; + let inputElement: HTMLInputElement; + + it('should use the provided aria-owns', () => { + fixture = createComponent(CheckboxWithAriaOwns); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-owns')).toBe('some-id'); + }); + + it('should not assign aria-owns if none is provided', () => { + fixture = createComponent(SingleCheckbox); + checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox))!; + checkboxNativeElement = checkboxDebugElement.nativeElement; + inputElement = checkboxNativeElement.querySelector('input'); + + fixture.detectChanges(); + expect(inputElement.getAttribute('aria-owns')).toBe(null); + }); + }); + describe('with provided tabIndex', () => { let checkboxDebugElement: DebugElement; let checkboxNativeElement: HTMLElement; @@ -1237,6 +1325,38 @@ class CheckboxWithAriaLabelledby {} }) class CheckboxWithAriaDescribedby {} +/** Simple test component with an aria-expanded set with true. */ +@Component({ + template: ``, + standalone: true, + imports: [MatCheckbox], +}) +class CheckboxWithPositiveAriaExpanded {} + +/** Simple test component with an aria-expanded set with false. */ +@Component({ + template: ``, + standalone: true, + imports: [MatCheckbox], +}) +class CheckboxWithNegativeAriaExpanded {} + +/** Simple test component with an aria-controls set. */ +@Component({ + template: ``, + standalone: true, + imports: [MatCheckbox], +}) +class CheckboxWithAriaControls {} + +/** Simple test component with an aria-owns set. */ +@Component({ + template: ``, + standalone: true, + imports: [MatCheckbox], +}) +class CheckboxWithAriaOwns {} + /** Simple test component with name attribute */ @Component({ template: ``, diff --git a/src/material/checkbox/checkbox.ts b/src/material/checkbox/checkbox.ts index 19a09c881c29..b4627499dae5 100644 --- a/src/material/checkbox/checkbox.ts +++ b/src/material/checkbox/checkbox.ts @@ -160,6 +160,19 @@ export class MatCheckbox /** The 'aria-describedby' attribute is read after the element's label and field type. */ @Input('aria-describedby') ariaDescribedby: string; + /** + * Users can specify the `aria-expanded` attribute which will be forwarded to the input element + */ + @Input({alias: 'aria-expanded', transform: booleanAttribute}) ariaExpanded: boolean; + + /** + * Users can specify the `aria-controls` attribute which will be forwarded to the input element + */ + @Input('aria-controls') ariaControls: string; + + /** Users can specify the `aria-owns` attribute which will be forwarded to the input element */ + @Input('aria-owns') ariaOwns: string; + private _uniqueId: string; /** A unique id for the checkbox input. If none is supplied, it will be auto-generated. */ diff --git a/tools/public_api_guard/material/checkbox.md b/tools/public_api_guard/material/checkbox.md index bc59c890df69..e1b0acd3cf25 100644 --- a/tools/public_api_guard/material/checkbox.md +++ b/tools/public_api_guard/material/checkbox.md @@ -48,9 +48,12 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess }; // (undocumented) _animationMode?: string | undefined; + ariaControls: string; ariaDescribedby: string; + ariaExpanded: boolean; ariaLabel: string; ariaLabelledby: string | null; + ariaOwns: string; readonly change: EventEmitter; get checked(): boolean; set checked(value: boolean); @@ -78,6 +81,8 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess labelPosition: 'before' | 'after'; name: string | null; // (undocumented) + static ngAcceptInputType_ariaExpanded: unknown; + // (undocumented) static ngAcceptInputType_checked: unknown; // (undocumented) static ngAcceptInputType_disabled: unknown; @@ -123,7 +128,7 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess // (undocumented) writeValue(value: any): void; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }