Skip to content

Commit

Permalink
fix(cdk/a11y): re-apply the high contrast mode class when the forced-…
Browse files Browse the repository at this point in the history
…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)
  • Loading branch information
crisbeto committed Jun 16, 2022
1 parent 2388fe7 commit 08c3867
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/cdk/a11y/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 23 additions & 18 deletions src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,80 @@ 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')
.toBe('');
});

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);
});

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);
});

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);
});

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')
Expand All @@ -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}),
},
Expand Down
20 changes: 18 additions & 2 deletions src/cdk/a11y/high-contrast-mode/high-contrast-mode-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion tools/public_api_guard/cdk/a11y.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<HighContrastModeDetector, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<HighContrastModeDetector>;
Expand Down

0 comments on commit 08c3867

Please sign in to comment.