Repository navigation
fix: restore inline focus ring in forced-colors mode - #185
Conversation
`hds-focus-ring-inline` sets `outline: none` and then paints its dashed ring with four repeating-linear-gradient layers. Forced-colors mode does not paint background images, so all four layers drop out, and since the native outline was already removed nothing is rendered at all. Keyboard users on a high-contrast theme cannot see which link has focus. Restore an outline inside `@media (forced-colors: active)`, using the `$border-high-contrast` value USWDS already uses for the same purpose, so the browser repaints it in a system color. The block ring is unaffected: `hds-focus-ring` paints through a ::before mask with a background-color, and background colors are remapped rather than dropped, which is why buttons and inputs still show a ring today and links do not. Measured with Playwright forced-colors emulation against the built stylesheet, reading computed styles on a keyboard-focused link: forced-colors off, before and after: outline none, 4 gradient layers forced-colors on, before: outline none, 0 gradient layers forced-colors on, after: outline solid 2px, 0 gradient layers The gradient count dropping 4 -> 0 under emulation doubles as a positive control that the media query really matched, which the reporter flagged as easy to get wrong. Fixes nasa#176
|
Approve. Reviewed against a fresh build on this branch and confirmed the compiled Thanks @ppcvote for the rigor. The "gradient count 4 to 0 as a positive control" verification is exactly the right way to guard against the false-pass trap @stevenpelletier90 warned about. One note on scope: this fixes the inline ring (the "nothing at all" case). Manual testing surfaced that the block-level |
…ors mode hds-focus-ring (buttons, accordion, icon buttons, pagination, side nav) drew its ring with mask-image plus background-color. Forced-colors keeps the mask but repaints the background as a system color, leaving only a thin, too-faint 2,3 dash (1.4.11 Non-text Contrast). Draw a solid $border-high-contrast outline and hide the masked ::before instead, matching the inline-ring fix in nasa#185. hds-link-appearance lost its gradient underline in forced-colors; restore it as a real text-decoration underline. Extends nasa#185. Refs nasa#176.
|
Thanks @abbybowman. I am very glad I could help with this. |
Fixes #176. Thanks @stevenpelletier90 for the report, the six-palette results, and especially the warning about confirming the media query is actually matching before trusting what you see. That one shaped how I verified this.
Why nothing is painted
hds-focus-ring-inline(src/scss/_hds-mixins.scss) opens withoutline: noneand then draws its dashed ring with fourrepeating-linear-gradientlayers. Forced-colors mode does not paint background images, so all four layers drop out, and the native outline is already gone. Nothing remains.This also explains the asymmetry you observed between buttons and links.
hds-focus-ringpaints through a::beforewithmask-imageplus a background-color, and background colors are remapped to system colors rather than dropped, so that ring survives. Measured on a keyboard-focused element with forced colors on:a.usa-linkoutline: none— nothing paintedbutton.usa-button::beforemask + background-colorThe fix
One
@media (forced-colors: active)block inside the mixin, using the$border-high-contrastvalue USWDS already uses for this inusa-range,usa-date-pickerandcheckbox-and-radio-colors:Fixing the mixin rather than
_link.scsscovers everything that takes the inline ring. Confirmed in the compiled stylesheet that all five call sites now carry it:Verification
Playwright with
forcedColors: 'active', realTabkeypress so:focus-visibleapplies, reading computed styles off the builtdist/css/hds.min.css:nonenonesolid 2px, system colorTwo things worth calling out:
setContent, which silently blocked thefile://stylesheet, and every element came back looking like the browser default. That is the same trap from the other direction, and it would have "proved" there was no bug.npm run lint:scss,prettier --checkandremarkall pass. Changeset included as a patch.On tests
I did not add an automated test. The suite runs Storybook stories in headless chromium and leans on Chromatic for visual verification, and forced-colors is not something either currently emulates, so a new
FocusTeststory would snapshot the normal ring and tell you nothing about this bug.FocusLinkalready covers the normal-mode ring.Your
inset: -2pxnote is well taken, and it is why I asserted on computedoutline-styleand layer counts rather than geometry or a screenshot. If you would like forced-colors coverage in CI, the smallest version is a vitest browser test with a dedicated playwright context, separate from the storybook project so it does not affect other stories. Happy to add that in this PR or a follow-up, whichever you prefer.