diff --git a/src/cdk/a11y/BUILD.bazel b/src/cdk/a11y/BUILD.bazel index d770bf2ebbe5..849cc4d2d2f4 100644 --- a/src/cdk/a11y/BUILD.bazel +++ b/src/cdk/a11y/BUILD.bazel @@ -20,6 +20,7 @@ ng_module( "//src:dev_mode_types", "//src/cdk/coercion", "//src/cdk/keycodes", + "//src/cdk/layout", "//src/cdk/observers", "//src/cdk/platform", "@npm//@angular/core", diff --git a/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.spec.ts b/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.spec.ts index 65af07ff7b5d..6cdd028145d6 100644 --- a/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.spec.ts +++ b/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.spec.ts @@ -6,27 +6,34 @@ import { WHITE_ON_BLACK_CSS_CLASS, } from './high-contrast-mode-detector'; import {Platform} from '@angular/cdk/platform'; -import {inject} from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; +import {Provider} from '@angular/core'; +import {A11yModule} from '../a11y-module'; +import {DOCUMENT} from '@angular/common'; describe('HighContrastModeDetector', () => { - let fakePlatform: Platform; + function getDetector(document: unknown, platform?: Platform) { + const providers: Provider[] = [{provide: DOCUMENT, useValue: document}]; - beforeEach(inject([Platform], (p: Platform) => { - fakePlatform = p; - })); + if (platform) { + providers.push({provide: Platform, useValue: platform}); + } + + TestBed.configureTestingModule({imports: [A11yModule], providers}); + return TestBed.inject(HighContrastModeDetector); + } it('should detect NONE for non-browser platforms', () => { - fakePlatform.isBrowser = false; - const detector = new HighContrastModeDetector(fakePlatform, {}); + const detector = getDetector(getFakeDocument(''), {isBrowser: false} as Platform); + expect(detector.getHighContrastMode()) .withContext('Expected high-contrast mode `NONE` on non-browser platforms') .toBe(HighContrastMode.NONE); }); it('should not apply any css classes for non-browser platforms', () => { - fakePlatform.isBrowser = false; const fakeDocument = getFakeDocument(''); - const detector = new HighContrastModeDetector(fakePlatform, fakeDocument); + const detector = getDetector(fakeDocument, {isBrowser: false} as Platform); detector._applyBodyHighContrastModeCssClasses(); expect(fakeDocument.body.className) .withContext('Expected body not to have any CSS classes in non-browser platforms') @@ -34,24 +41,21 @@ describe('HighContrastModeDetector', () => { }); it('should detect WHITE_ON_BLACK when backgrounds are coerced to black', () => { - const detector = new HighContrastModeDetector(fakePlatform, getFakeDocument('rgb(0,0,0)')); + const detector = getDetector(getFakeDocument('rgb(0,0,0)')); expect(detector.getHighContrastMode()) .withContext('Expected high-contrast mode `WHITE_ON_BLACK`') .toBe(HighContrastMode.WHITE_ON_BLACK); }); it('should detect BLACK_ON_WHITE when backgrounds are coerced to white ', () => { - const detector = new HighContrastModeDetector( - fakePlatform, - getFakeDocument('rgb(255,255,255)'), - ); + const detector = getDetector(getFakeDocument('rgb(255,255,255)')); expect(detector.getHighContrastMode()) .withContext('Expected high-contrast mode `BLACK_ON_WHITE`') .toBe(HighContrastMode.BLACK_ON_WHITE); }); it('should detect NONE when backgrounds are not coerced ', () => { - const detector = new HighContrastModeDetector(fakePlatform, getFakeDocument('rgb(1,2,3)')); + const detector = getDetector(getFakeDocument('rgb(1,2,3)')); expect(detector.getHighContrastMode()) .withContext('Expected high-contrast mode `NONE`') .toBe(HighContrastMode.NONE); @@ -59,7 +63,7 @@ describe('HighContrastModeDetector', () => { it('should apply css classes for BLACK_ON_WHITE high-contrast mode', () => { const fakeDocument = getFakeDocument('rgb(255,255,255)'); - const detector = new HighContrastModeDetector(fakePlatform, fakeDocument); + const detector = getDetector(fakeDocument); detector._applyBodyHighContrastModeCssClasses(); expect(fakeDocument.body.classList).toContain(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS); expect(fakeDocument.body.classList).toContain(BLACK_ON_WHITE_CSS_CLASS); @@ -67,7 +71,7 @@ describe('HighContrastModeDetector', () => { it('should apply css classes for WHITE_ON_BLACK high-contrast mode', () => { const fakeDocument = getFakeDocument('rgb(0,0,0)'); - const detector = new HighContrastModeDetector(fakePlatform, fakeDocument); + const detector = getDetector(fakeDocument); detector._applyBodyHighContrastModeCssClasses(); expect(fakeDocument.body.classList).toContain(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS); expect(fakeDocument.body.classList).toContain(WHITE_ON_BLACK_CSS_CLASS); @@ -75,7 +79,7 @@ describe('HighContrastModeDetector', () => { it('should not apply any css classes when backgrounds are not coerced', () => { const fakeDocument = getFakeDocument(''); - const detector = new HighContrastModeDetector(fakePlatform, fakeDocument); + const detector = getDetector(fakeDocument); detector._applyBodyHighContrastModeCssClasses(); expect(fakeDocument.body.className) .withContext('Expected body not to have any CSS classes in non-browser platforms') @@ -88,6 +92,7 @@ function getFakeDocument(fakeComputedBackgroundColor: string) { return { body: document.createElement('body'), createElement: (tag: string) => document.createElement(tag), + querySelectorAll: (selector: string) => document.querySelectorAll(selector), defaultView: { getComputedStyle: () => ({backgroundColor: fakeComputedBackgroundColor}), }, diff --git a/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.ts b/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.ts index a92a39a2e990..2c789b38a247 100644 --- a/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.ts +++ b/src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {inject, Inject, Injectable, OnDestroy} from '@angular/core'; +import {BreakpointObserver} from '@angular/cdk/layout'; import {Platform} from '@angular/cdk/platform'; import {DOCUMENT} from '@angular/common'; -import {Inject, Injectable} from '@angular/core'; +import {Subscription} from 'rxjs'; /** Set of possible high-contrast mode backgrounds. */ export const enum HighContrastMode { @@ -38,16 +40,26 @@ export const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active'; * browser extension. */ @Injectable({providedIn: 'root'}) -export class HighContrastModeDetector { +export class HighContrastModeDetector implements OnDestroy { /** * Figuring out the high contrast mode and adding the body classes can cause * some expensive layouts. This flag is used to ensure that we only do it once. */ private _hasCheckedHighContrastMode: boolean; private _document: Document; + private _breakpointSubscription: Subscription; constructor(private _platform: Platform, @Inject(DOCUMENT) document: any) { this._document = document; + + this._breakpointSubscription = inject(BreakpointObserver) + .observe('(forced-colors: active)') + .subscribe(() => { + if (this._hasCheckedHighContrastMode) { + this._hasCheckedHighContrastMode = false; + this._applyBodyHighContrastModeCssClasses(); + } + }); } /** Gets the current high-contrast-mode for the page. */ @@ -88,6 +100,10 @@ export class HighContrastModeDetector { return HighContrastMode.NONE; } + ngOnDestroy(): void { + this._breakpointSubscription.unsubscribe(); + } + /** Applies CSS classes indicating high-contrast mode to document body (browser-only). */ _applyBodyHighContrastModeCssClasses(): void { if (!this._hasCheckedHighContrastMode && this._platform.isBrowser && this._document.body) { diff --git a/tools/public_api_guard/cdk/a11y.md b/tools/public_api_guard/cdk/a11y.md index e6e003306a38..3530215ef501 100644 --- a/tools/public_api_guard/cdk/a11y.md +++ b/tools/public_api_guard/cdk/a11y.md @@ -264,11 +264,13 @@ export const enum HighContrastMode { } // @public -export class HighContrastModeDetector { +export class HighContrastModeDetector implements OnDestroy { constructor(_platform: Platform, document: any); _applyBodyHighContrastModeCssClasses(): void; getHighContrastMode(): HighContrastMode; // (undocumented) + ngOnDestroy(): void; + // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; // (undocumented) static ɵprov: i0.ɵɵInjectableDeclaration;