-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(cdk/a11y): add high-contrast mode detection #17378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { | ||
BLACK_ON_WHITE_CSS_CLASS, | ||
HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, | ||
HighContrastMode, | ||
HighContrastModeDetector, WHITE_ON_BLACK_CSS_CLASS, | ||
} from './high-contrast-mode-detector'; | ||
import {Platform} from '@angular/cdk/platform'; | ||
|
||
|
||
describe('HighContrastModeDetector', () => { | ||
let fakePlatform: Platform; | ||
|
||
beforeEach(() => { | ||
fakePlatform = new Platform(); | ||
}); | ||
|
||
it('should detect NONE for non-browser platforms', () => { | ||
fakePlatform.isBrowser = false; | ||
const detector = new HighContrastModeDetector(fakePlatform, {}); | ||
expect(detector.getHighContrastMode()) | ||
.toBe(HighContrastMode.NONE, 'Expected high-contrast mode `NONE` on non-browser platforms'); | ||
}); | ||
|
||
it('should not apply any css classes for non-browser platforms', () => { | ||
fakePlatform.isBrowser = false; | ||
const fakeDocument = getFakeDocument(''); | ||
const detector = new HighContrastModeDetector(fakePlatform, fakeDocument); | ||
detector._applyBodyHighContrastModeCssClasses(); | ||
expect(fakeDocument.body.className) | ||
.toBe('', 'Expected body not to have any CSS classes in non-browser platforms'); | ||
}); | ||
|
||
it('should detect WHITE_ON_BLACK when backgrounds are coerced to black', () => { | ||
const detector = new HighContrastModeDetector(fakePlatform, getFakeDocument('rgb(0,0,0)')); | ||
expect(detector.getHighContrastMode()) | ||
.toBe(HighContrastMode.WHITE_ON_BLACK, 'Expected high-contrast mode `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)')); | ||
expect(detector.getHighContrastMode()) | ||
.toBe(HighContrastMode.BLACK_ON_WHITE, 'Expected high-contrast mode `BLACK_ON_WHITE`'); | ||
}); | ||
|
||
it('should detect NONE when backgrounds are not coerced ', () => { | ||
const detector = new HighContrastModeDetector(fakePlatform, getFakeDocument('rgb(1,2,3)')); | ||
expect(detector.getHighContrastMode()) | ||
.toBe(HighContrastMode.NONE, 'Expected high-contrast mode `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); | ||
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); | ||
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); | ||
detector._applyBodyHighContrastModeCssClasses(); | ||
expect(fakeDocument.body.className) | ||
.toBe('', 'Expected body not to have any CSS classes in non-browser platforms'); | ||
}); | ||
}); | ||
|
||
|
||
/** Gets a fake document that includes a fake `window.getComputedStyle` implementation. */ | ||
function getFakeDocument(fakeComputedBackgroundColor: string) { | ||
return { | ||
body: document.createElement('body'), | ||
createElement: (tag: string) => document.createElement(tag), | ||
defaultView: { | ||
getComputedStyle: () => ({backgroundColor: fakeComputedBackgroundColor}), | ||
}, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Platform} from '@angular/cdk/platform'; | ||
import {DOCUMENT} from '@angular/common'; | ||
import {Inject, Injectable} from '@angular/core'; | ||
|
||
|
||
/** Set of possible high-contrast mode backgrounds. */ | ||
export const enum HighContrastMode { | ||
NONE, | ||
BLACK_ON_WHITE, | ||
WHITE_ON_BLACK, | ||
} | ||
|
||
/** CSS class applied to the document body when in black-on-white high-contrast mode. */ | ||
export const BLACK_ON_WHITE_CSS_CLASS = 'cdk-high-contrast-black-on-white'; | ||
|
||
/** CSS class applied to the document body when in white-on-black high-contrast mode. */ | ||
export const WHITE_ON_BLACK_CSS_CLASS = 'cdk-high-contrast-white-on-black'; | ||
|
||
/** CSS class applied to the document body when in high-contrast mode. */ | ||
export const HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS = 'cdk-high-contrast-active'; | ||
|
||
/** | ||
* Service to determine whether the browser is currently in a high-constrast-mode environment. | ||
* | ||
* Microsoft Windows supports an accessibility feature called "High Contrast Mode". This mode | ||
* changes the appearance of all applications, including web applications, to dramatically increase | ||
* contrast. | ||
* | ||
* IE, Edge, and Firefox currently support this mode. Chrome does not support Windows High Contrast | ||
* Mode. This service does not detect high-contrast mode as added by the Chrome "High Contrast" | ||
* browser extension. | ||
*/ | ||
@Injectable({providedIn: 'root'}) | ||
export class HighContrastModeDetector { | ||
private _document: Document; | ||
|
||
constructor(private _platform: Platform, @Inject(DOCUMENT) document: any) { | ||
this._document = document; | ||
} | ||
|
||
/** Gets the current high-constrast-mode for the page. */ | ||
getHighContrastMode(): HighContrastMode { | ||
if (!this._platform.isBrowser) { | ||
return HighContrastMode.NONE; | ||
} | ||
|
||
// Create a test element with an arbitrary background-color that is neither black nor | ||
// white; high-contrast mode will coerce the color to either black or white. Also ensure that | ||
// appending the test element to the DOM does not affect layout by absolutely positioning it | ||
const testElement = this._document.createElement('div'); | ||
testElement.style.backgroundColor = 'rgb(1,2,3)'; | ||
testElement.style.position = 'absolute'; | ||
this._document.body.appendChild(testElement); | ||
|
||
// Get the computed style for the background color, collapsing spaces to normalize between | ||
// browsers. Once we get this color, we no longer need the test element. Access the `window` | ||
// via the document so we can fake it in tests. | ||
const documentWindow = this._document.defaultView!; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW you could also fake it in tests using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know, I'm just not a big fan of using Jasmine spies in place of more full-blown fakes. |
||
const computedColor = | ||
(documentWindow.getComputedStyle(testElement).backgroundColor || '').replace(/ /g, ''); | ||
this._document.body.removeChild(testElement); | ||
|
||
switch (computedColor) { | ||
case 'rgb(0,0,0)': return HighContrastMode.WHITE_ON_BLACK; | ||
case 'rgb(255,255,255)': return HighContrastMode.BLACK_ON_WHITE; | ||
} | ||
return HighContrastMode.NONE; | ||
} | ||
|
||
/** Applies CSS classes indicating high-contrast mode to document body (browser-only). */ | ||
_applyBodyHighContrastModeCssClasses(): void { | ||
if (this._platform.isBrowser && this._document.body) { | ||
const bodyClasses = this._document.body.classList; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It appears that this function may be called too early, when See |
||
// IE11 doesn't support `classList` operations with multiple arguments | ||
bodyClasses.remove(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS); | ||
bodyClasses.remove(BLACK_ON_WHITE_CSS_CLASS); | ||
bodyClasses.remove(WHITE_ON_BLACK_CSS_CLASS); | ||
|
||
const mode = this.getHighContrastMode(); | ||
if (mode === HighContrastMode.BLACK_ON_WHITE) { | ||
bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS); | ||
bodyClasses.add(BLACK_ON_WHITE_CSS_CLASS); | ||
} else if (mode === HighContrastMode.WHITE_ON_BLACK) { | ||
bodyClasses.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS); | ||
bodyClasses.add(WHITE_ON_BLACK_CSS_CLASS); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we cache this in a similar way to how we cache the viewport size? It's unlikely for this to change dynamically and people might end up calling it frequently, if they don't know that it can be expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We invalidate the viewport size cache on window resize, though. We don't have a similar way to know to invalidate for this, so I want to avoid caching for now.