-
Notifications
You must be signed in to change notification settings - Fork 6.7k
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
feat(cdk/a11y): add high-contrast mode detection #17378
feat(cdk/a11y): add high-contrast mode detection #17378
Conversation
284d733
to
31024f2
Compare
src/cdk/a11y/_a11y.scss
Outdated
@@ -28,4 +28,10 @@ | |||
@media (-ms-high-contrast: $target) { |
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.
can we detect this in JS and apply the appropriate class instead so that we don't need to duplicate all of the styles?
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 potentially don't need the @media
query at all if the class works for IE11 and Edge, but I don't have a Windows machine available to confirm at the moment (our test workstation is busted). I'll chat with Kristiyan about it
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.
Sounds good. If what you have currently doesn't work we can probably add some extra logic to detect on IE/Edge which would likely wind up smaller than duplicating all the CSS
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.
I wasn't super concerned because the high-contrast styles tend to be pretty small and relatively infrequent, but agreed that it's better to avoid if possible.
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.
I tested this out against IE, Edge, Firefox and Chrome on Windows. These are the results:
- IE and Edge work after fixing the issue with
classList
that I commented on below and I tweaked the color for detecting black-on-white mode. - Firefox also seems to work since it follows a similar implementation to the Microsoft browsers.
- It doesn't detect high contrast mode in Chrome that is added through the extension. It looks like Chrome returns the original colors from
getComputedStyle
. We could work around it by checking the attribute that the extension sets on thebody
, but that might be a little fragile.
I like the approach since it allows us to at least cover Firefox users which we couldn't before, however I'm not sure whether we can apply the same styles to all browsers and expect it to work consistently. E.g. the places where we add an outline
are working correctly, but some of the more advanced cases, like using a border-image
to remove the border from form fields, don't work on Firefox.
// 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'); |
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.
// Get the computed style ofor 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 comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW you could also fake it in tests using spyOn(window, 'getComputedStyle').and.returnValue
.
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.
I know, I'm just not a big fan of using Jasmine spies in place of more full-blown fakes.
_applyBodyHighContrastModeCssClasses(): void { | ||
if (this._platfrom.isBrowser) { | ||
const body = this._document.body; | ||
body.classList.remove( |
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.
The signature of classlist.remove
with multiple classes isn't supported on IE. You need to do 3 separate remove
calls.
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.
Done
|
||
|
||
/** Set of possible high-contrast mode backgrounds. */ | ||
export enum HighContrastMode { |
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 it be a const enum
so that TS inlines the values during compilation?
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.
Done, meant to do that
src/cdk/a11y/_a11y.scss
Outdated
@@ -28,4 +28,10 @@ | |||
@media (-ms-high-contrast: $target) { | |||
@content; | |||
} | |||
|
|||
@at-root { | |||
.cdk-high-contrast-#{$target} #{&} { |
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.
While we're at it, should we have some compile-time validation that the target is valid?
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.
Done
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.
This produces an unintended effect for cases where #{&}
includes multiple comma-delimited selectors.
For example, consider the following Sass:
.element-a, .element-b {
@include cdk-high-contrast() {
background: blue;
}
}
This will actually produce the following:
.cdk-high-contrast .element-a, .element-b {
background: blue;
}
https://www.sassmeister.com/gist/30c179f33ac9dc40dab1ef20cd374dfc
In short, Sass is not prepending .cdk-high-contrast
to each selector and instead just putting it once before the whole rule.
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.
Looks like this fix may be viable, where we loop over each item in the active selector:
@at-root {
@each $i in & {
.cdk-high-contrast #{$i} {
@content
}
}
}
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.
Thanks for looking into the failures. Using @each
here will duplicate the content for each part of the compound selector. Instead I did this:
@at-root {
$selector-context: #{&};
.cdk-high-contrast-#{$target} {
#{$selector-context} {
@content
}
}
}
For the "body not present yet" issue, I'm going to just add the check for now and we can figure out if further action is necessary as a follow-up. I suspect this is really just an issue with tests and not with real applications.
src/cdk/a11y/public-api.ts
Outdated
export { | ||
HighContrastModeDetector, | ||
HighContrastMode, | ||
}from './high-contrast-mode/high-contrast-mode-detector'; |
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.
Needs a space before from
.
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.
Done
@@ -53,7 +54,7 @@ export interface GranularSanityChecks { | |||
* This module should be imported to each top-level component module (e.g., MatTabsModule). | |||
*/ | |||
@NgModule({ | |||
imports: [BidiModule], | |||
imports: [A11yModule, BidiModule], |
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.
I'm not sure whether we should be adding the A11yModule
to all of the Material modules since it's one of our largest modules and we only need the high contrast detection. It might be better to just inject the HighContrastModeDetector
since it's a tree-shakeable provider and add the call to set the body class. We can do the same in the A11yModule
to cover users that import the CDK, but not Material.
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.
Done out of additional caution, but adding A11yModule
shouldn't increase any size if we don't actually consume any of the directives or injectables.
|
||
const mode = this.getHighContrastMode(); | ||
if (mode === HighContrastMode.BLACK_ON_WHITE) { | ||
body.classList.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_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.
IE doesn't support add
with multiple arguments either.
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.
Done
|
||
switch (computedColor) { | ||
case 'rgb(0,0,0)': return HighContrastMode.WHITE_ON_BLACK; | ||
case 'rgb(1,1,1)': return HighContrastMode.BLACK_ON_WHITE; |
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.
The white-on-black detection here works in IE, but not the black-on-white. The return value here should be rgb(255,255,255)
.
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.
Yep, this should always have been 255; pure brain misfire. Done.
Once in a while we hit an issue because we use some of the `classList` methods in a way that isn't supported in all browsers (e.g. passing multiple parameters to `add` in angular#17378). These changes add a simple lint rule to catch these cases earlier.
Once in a while we hit an issue because we use some of the `classList` methods in a way that isn't supported in all browsers (e.g. passing multiple parameters to `add` in angular#17378). These changes add a simple lint rule to catch these cases earlier.
Once in a while we hit an issue because we use some of the `classList` methods in a way that isn't supported in all browsers (e.g. passing multiple parameters to `add` in angular#17378). These changes add a simple lint rule to catch these cases earlier.
Once in a while we hit an issue because we use some of the `classList` methods in a way that isn't supported in all browsers (e.g. passing multiple parameters to `add` in angular#17378). These changes add a simple lint rule to catch these cases earlier.
Once in a while we hit an issue because we use some of the `classList` methods in a way that isn't supported in all browsers (e.g. passing multiple parameters to `add` in #17378). These changes add a simple lint rule to catch these cases earlier.
31024f2
to
6e3873b
Compare
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.
@crisbeto addressed comments, could you take another look? In particular, I want to see if removing the ms-
media queries still works for IE
// 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'); |
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.
src/cdk/a11y/_a11y.scss
Outdated
@@ -28,4 +28,10 @@ | |||
@media (-ms-high-contrast: $target) { | |||
@content; | |||
} | |||
|
|||
@at-root { | |||
.cdk-high-contrast-#{$target} #{&} { |
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.
Done
|
||
|
||
/** Set of possible high-contrast mode backgrounds. */ | ||
export enum HighContrastMode { |
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.
Done, meant to do that
// Get the computed style ofor 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 comment
The 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.
|
||
switch (computedColor) { | ||
case 'rgb(0,0,0)': return HighContrastMode.WHITE_ON_BLACK; | ||
case 'rgb(1,1,1)': return HighContrastMode.BLACK_ON_WHITE; |
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.
Yep, this should always have been 255; pure brain misfire. Done.
_applyBodyHighContrastModeCssClasses(): void { | ||
if (this._platfrom.isBrowser) { | ||
const body = this._document.body; | ||
body.classList.remove( |
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.
Done
|
||
const mode = this.getHighContrastMode(); | ||
if (mode === HighContrastMode.BLACK_ON_WHITE) { | ||
body.classList.add(HIGH_CONTRAST_MODE_ACTIVE_CSS_CLASS, BLACK_ON_WHITE_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.
Done
src/cdk/a11y/public-api.ts
Outdated
export { | ||
HighContrastModeDetector, | ||
HighContrastMode, | ||
}from './high-contrast-mode/high-contrast-mode-detector'; |
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.
Done
@@ -53,7 +54,7 @@ export interface GranularSanityChecks { | |||
* This module should be imported to each top-level component module (e.g., MatTabsModule). | |||
*/ | |||
@NgModule({ | |||
imports: [BidiModule], | |||
imports: [A11yModule, BidiModule], |
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.
Done out of additional caution, but adding A11yModule
shouldn't increase any size if we don't actually consume any of the directives or injectables.
6e3873b
to
c4b930c
Compare
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.
LGTM. I re-tested it and it seems to work without the -ms-
media queries. Some of the workarounds that I did for IE/Edge don't seem to be working for Firefox though (e.g. the one in the form field I mentioned above).
c212d57
to
5f30ca0
Compare
Comments addressed. For cases like form-field, we can potentially add handing in specific components that checks both the platform and the high-contrast mode to apply alternative styles. |
5f30ca0
to
7627011
Compare
7627011
to
3a1a20e
Compare
/** Applies CSS classes indicating high-contrast mode to document body (browser-only). */ | ||
_applyBodyHighContrastModeCssClasses(): void { | ||
if (this._platfrom.isBrowser) { | ||
const bodyClasses = this._document.body.classList; |
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.
It appears that this function may be called too early, when body
does not yet exist (e.g. if script is in head). It looks like we need some delay to make sure this is applied.
See _checkThemeIsPresent
in common-module.ts
for a simliar comment about checking for this._document.body
Confirmed that two changes need to occur, and then all internal tests pass. In @at-root {
@each $i in & {
.cdk-high-contrast-#{$target} #{$i} {
@content
}
}
} In |
2ede62b
to
4f2af2c
Compare
* Add a new `HighContrastModeDetector` service that's can detect whether the browser is in high-contrast mode (and which variety). * Augments `A11yModule` to use this serve to add classes to the document body indicating the high-contrast mode * Have `MatCommonModule` apply high-contrast css classes * Replace `ms-` media queries in `cdk-high-contrast` Sass mixin with the new CSS classes
4f2af2c
to
697c3ea
Compare
@andrewseguin updated the code so that it should work universally now. I didn't use the Sass loop since that would end up duplicating the content; what I did instead should still fix the problem. I tried to run a presubmit (since it's 4am in California now), but I can't get my ssh session to pick up my security key |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
HighContrastModeDetector
service that's can detect whetherthe browser is in high-contrast mode (and which variety).
A11yModule
to use this serve to add classes to the documentbody indicating the high-contrast mode
A11yModule
toMatCommonModule
so that this runs for all ofthe material components
cdk-high-contrast
Sass mixin to include the new CSSclasses