Skip to content

Commit 08c3867

Browse files
committed
fix(cdk/a11y): re-apply the high contrast mode class when the forced-colors media query changes (#25088)
Adds some logic to reapply the high contrast mode class when the `forced-colors` media query changes so that the user doesn't have to refresh the page. Also reworks the testing setup for the `HighContrastMode` detector, because it was constructing the detector manually. (cherry picked from commit bc81e7d)
1 parent 2388fe7 commit 08c3867

File tree

4 files changed

+45
-21
lines changed

4 files changed

+45
-21
lines changed

src/cdk/a11y/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ng_module(
2020
"//src:dev_mode_types",
2121
"//src/cdk/coercion",
2222
"//src/cdk/keycodes",
23+
"//src/cdk/layout",
2324
"//src/cdk/observers",
2425
"//src/cdk/platform",
2526
"@npm//@angular/core",

src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.spec.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,80 @@ import {
66
WHITE_ON_BLACK_CSS_CLASS,
77
} from './high-contrast-mode-detector';
88
import {Platform} from '@angular/cdk/platform';
9-
import {inject} from '@angular/core/testing';
9+
import {TestBed} from '@angular/core/testing';
10+
import {Provider} from '@angular/core';
11+
import {A11yModule} from '../a11y-module';
12+
import {DOCUMENT} from '@angular/common';
1013

1114
describe('HighContrastModeDetector', () => {
12-
let fakePlatform: Platform;
15+
function getDetector(document: unknown, platform?: Platform) {
16+
const providers: Provider[] = [{provide: DOCUMENT, useValue: document}];
1317

14-
beforeEach(inject([Platform], (p: Platform) => {
15-
fakePlatform = p;
16-
}));
18+
if (platform) {
19+
providers.push({provide: Platform, useValue: platform});
20+
}
21+
22+
TestBed.configureTestingModule({imports: [A11yModule], providers});
23+
return TestBed.inject(HighContrastModeDetector);
24+
}
1725

1826
it('should detect NONE for non-browser platforms', () => {
19-
fakePlatform.isBrowser = false;
20-
const detector = new HighContrastModeDetector(fakePlatform, {});
27+
const detector = getDetector(getFakeDocument(''), {isBrowser: false} as Platform);
28+
2129
expect(detector.getHighContrastMode())
2230
.withContext('Expected high-contrast mode `NONE` on non-browser platforms')
2331
.toBe(HighContrastMode.NONE);
2432
});
2533

2634
it('should not apply any css classes for non-browser platforms', () => {
27-
fakePlatform.isBrowser = false;
2835
const fakeDocument = getFakeDocument('');
29-
const detector = new HighContrastModeDetector(fakePlatform, fakeDocument);
36+
const detector = getDetector(fakeDocument, {isBrowser: false} as Platform);
3037
detector._applyBodyHighContrastModeCssClasses();
3138
expect(fakeDocument.body.className)
3239
.withContext('Expected body not to have any CSS classes in non-browser platforms')
3340
.toBe('');
3441
});
3542

3643
it('should detect WHITE_ON_BLACK when backgrounds are coerced to black', () => {
37-
const detector = new HighContrastModeDetector(fakePlatform, getFakeDocument('rgb(0,0,0)'));
44+
const detector = getDetector(getFakeDocument('rgb(0,0,0)'));
3845
expect(detector.getHighContrastMode())
3946
.withContext('Expected high-contrast mode `WHITE_ON_BLACK`')
4047
.toBe(HighContrastMode.WHITE_ON_BLACK);
4148
});
4249

4350
it('should detect BLACK_ON_WHITE when backgrounds are coerced to white ', () => {
44-
const detector = new HighContrastModeDetector(
45-
fakePlatform,
46-
getFakeDocument('rgb(255,255,255)'),
47-
);
51+
const detector = getDetector(getFakeDocument('rgb(255,255,255)'));
4852
expect(detector.getHighContrastMode())
4953
.withContext('Expected high-contrast mode `BLACK_ON_WHITE`')
5054
.toBe(HighContrastMode.BLACK_ON_WHITE);
5155
});
5256

5357
it('should detect NONE when backgrounds are not coerced ', () => {
54-
const detector = new HighContrastModeDetector(fakePlatform, getFakeDocument('rgb(1,2,3)'));
58+
const detector = getDetector(getFakeDocument('rgb(1,2,3)'));
5559
expect(detector.getHighContrastMode())
5660
.withContext('Expected high-contrast mode `NONE`')
5761
.toBe(HighContrastMode.NONE);
5862
});
5963

6064
it('should apply css classes for BLACK_ON_WHITE high-contrast mode', () => {
6165
const fakeDocument = getFakeDocument('rgb(255,255,255)');
62-
const detector = new HighContrastModeDetector(fakePlatform, fakeDocument);
66+
const detector = getDetector(fakeDocument);
6367
detector._applyBodyHighContrastModeCssClasses();
6468
expect(fakeDocument.body.classList).toContain(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS);
6569
expect(fakeDocument.body.classList).toContain(BLACK_ON_WHITE_CSS_CLASS);
6670
});
6771

6872
it('should apply css classes for WHITE_ON_BLACK high-contrast mode', () => {
6973
const fakeDocument = getFakeDocument('rgb(0,0,0)');
70-
const detector = new HighContrastModeDetector(fakePlatform, fakeDocument);
74+
const detector = getDetector(fakeDocument);
7175
detector._applyBodyHighContrastModeCssClasses();
7276
expect(fakeDocument.body.classList).toContain(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS);
7377
expect(fakeDocument.body.classList).toContain(WHITE_ON_BLACK_CSS_CLASS);
7478
});
7579

7680
it('should not apply any css classes when backgrounds are not coerced', () => {
7781
const fakeDocument = getFakeDocument('');
78-
const detector = new HighContrastModeDetector(fakePlatform, fakeDocument);
82+
const detector = getDetector(fakeDocument);
7983
detector._applyBodyHighContrastModeCssClasses();
8084
expect(fakeDocument.body.className)
8185
.withContext('Expected body not to have any CSS classes in non-browser platforms')
@@ -88,6 +92,7 @@ function getFakeDocument(fakeComputedBackgroundColor: string) {
8892
return {
8993
body: document.createElement('body'),
9094
createElement: (tag: string) => document.createElement(tag),
95+
querySelectorAll: (selector: string) => document.querySelectorAll(selector),
9196
defaultView: {
9297
getComputedStyle: () => ({backgroundColor: fakeComputedBackgroundColor}),
9398
},

src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {inject, Inject, Injectable, OnDestroy} from '@angular/core';
10+
import {BreakpointObserver} from '@angular/cdk/layout';
911
import {Platform} from '@angular/cdk/platform';
1012
import {DOCUMENT} from '@angular/common';
11-
import {Inject, Injectable} from '@angular/core';
13+
import {Subscription} from 'rxjs';
1214

1315
/** Set of possible high-contrast mode backgrounds. */
1416
export const enum HighContrastMode {
@@ -38,16 +40,26 @@ export const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active';
3840
* browser extension.
3941
*/
4042
@Injectable({providedIn: 'root'})
41-
export class HighContrastModeDetector {
43+
export class HighContrastModeDetector implements OnDestroy {
4244
/**
4345
* Figuring out the high contrast mode and adding the body classes can cause
4446
* some expensive layouts. This flag is used to ensure that we only do it once.
4547
*/
4648
private _hasCheckedHighContrastMode: boolean;
4749
private _document: Document;
50+
private _breakpointSubscription: Subscription;
4851

4952
constructor(private _platform: Platform, @Inject(DOCUMENT) document: any) {
5053
this._document = document;
54+
55+
this._breakpointSubscription = inject(BreakpointObserver)
56+
.observe('(forced-colors: active)')
57+
.subscribe(() => {
58+
if (this._hasCheckedHighContrastMode) {
59+
this._hasCheckedHighContrastMode = false;
60+
this._applyBodyHighContrastModeCssClasses();
61+
}
62+
});
5163
}
5264

5365
/** Gets the current high-contrast-mode for the page. */
@@ -88,6 +100,10 @@ export class HighContrastModeDetector {
88100
return HighContrastMode.NONE;
89101
}
90102

103+
ngOnDestroy(): void {
104+
this._breakpointSubscription.unsubscribe();
105+
}
106+
91107
/** Applies CSS classes indicating high-contrast mode to document body (browser-only). */
92108
_applyBodyHighContrastModeCssClasses(): void {
93109
if (!this._hasCheckedHighContrastMode && this._platform.isBrowser && this._document.body) {

tools/public_api_guard/cdk/a11y.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,13 @@ export const enum HighContrastMode {
264264
}
265265

266266
// @public
267-
export class HighContrastModeDetector {
267+
export class HighContrastModeDetector implements OnDestroy {
268268
constructor(_platform: Platform, document: any);
269269
_applyBodyHighContrastModeCssClasses(): void;
270270
getHighContrastMode(): HighContrastMode;
271271
// (undocumented)
272+
ngOnDestroy(): void;
273+
// (undocumented)
272274
static ɵfac: i0.ɵɵFactoryDeclaration<HighContrastModeDetector, never>;
273275
// (undocumented)
274276
static ɵprov: i0.ɵɵInjectableDeclaration<HighContrastModeDetector>;

0 commit comments

Comments
 (0)