Skip to content

fix(check): keep the outline a word inherits from its caption in contrast - #7698

Open
wonjangcloud9 wants to merge 1 commit into
heygen-com:mainfrom
wonjangcloud9:fix/check-contrast-inherited-stroke
Open

wonjangcloud9 wants to merge 1 commit into
heygen-com:mainfrom
wonjangcloud9:fix/check-contrast-inherited-stroke

Conversation

@wonjangcloud9

Copy link
Copy Markdown
Contributor

Context: I've been using HyperFrames every day for the past two weeks to make Korean Shorts and long-form videos, and it's been great to work with. This PR is one of a batch of fixes for things I ran into along the way, so you'll see several PRs from me at once — sorry for the pile-up, and thanks for your patience. Each one is reproduced on current main, independent of the others, and can be reviewed, merged or closed on its own.

What

check's contrast audit now counts the outline that a highlighted word inherits from its caption. On main, a yellow keyword inside a black-outlined caption on a yellow background was reported as 1.18:1. A side effect of the same fix: a child text element that inherits its colour from its parent (<div>… <b>word</b></div>) is now measured. Before, it was skipped without a message.

Why

A vertical map Short had captions like this:

.cap    { color: #FFF; -webkit-text-stroke: 12px #000; paint-order: stroke fill; text-transform: uppercase; }
.cap em { font-style: normal; color: #FFD21F; }
<div class="cap">Its own <em>nukes?</em></div>

The frame reads fine, because the black outline separates the yellow word from the yellow country fill. check still reported:

contrast_aa_failure  "nukes?"  Contrast is 1.18:1; WCAG AA requires 3:1.  fg rgb(255,210,31)  bg rgb(235,194,40)

The audit already counts a solid -webkit-text-stroke (it uses whichever of fill or stroke gives the stronger edge), and an <em> that declares its own stroke passes. The bug is ordering. __contrastAuditPrepare hides each element's own paint (color, fill, -webkit-text-stroke-color → transparent !important) at the moment it visits that element. It visits .cap before the <em> inside it. Both properties are inherited, so when the walk reaches the <em>, its computed -webkit-text-stroke-color is the caption's transparent. The stroke is dropped (stroke[3] <= 0.01) and the word is judged by its fill alone. The same ordering makes a child that inherits color read as transparent, and the walk skips it (fg[3] <= 0.01).

Related work

  • fix(cli): account for text strokes in contrast checks #2349 (closed) first proposed stroke-aware contrast. Main has since added stroke handling, but only for a stroke the element reads after its parents are hidden.
  • No open issue or PR covers inherited strokes. I searched "text-stroke contrast", "outline contrast" and "contrast_aa_failure".
  • Not changed here: text-shadow outlines (a stack of 0 0 Npx #000 shadows) still count as no outline, and the shadow stays visible in the background sample. That is a separate design question.

How

packages/cli/src/commands/contrast-audit.browser.js: the walk now only reads (colour, stroke, size, backdrop) and records which elements to hide. The hide moves into hideOwnPaint, the same code unchanged, and runs once after the walk, in the same order as the candidates, pushing the same restore records. The screenshot still happens after prepare returns, so every glyph is hidden when it is taken, as before.

Test plan

Unit (packages/cli/src/commands/layout-audit.browser.test.ts, happy-dom): keeps the stroke a keyword inherits from its outlined caption. happy-dom drops -webkit-text-stroke-color, so the test models inheritance by watching the caption's style.setProperty. On main (affa03b6) with the fix stashed it fails: the keyword comes back with its fill and wcagAA: false. With the fix it passes (fg rgb(0,0,0), bg rgb(235,194,40), wcagAA: true).

vitest run src/commands/ src/utils/checkBrowser.test.ts src/utils/layoutAudit.test.ts passes 2613 tests. The Chrome contrast suites (layout-audit.chromium, contrast-occlusion.chromium, contrast-solid/clip/svg-gradient.browser, run with PUPPETEER_EXECUTABLE_PATH) pass 92 tests.

Real CLI, hyperframes check --no-cache --json.

Minimal repro (1080×1920, yellow band, three captions: outline on the caption, outline on the <em> itself, and a text-shadow halo):

main affa03b6   contrast checked 30 passed 10
  #kw-stroke "nukes?"          fg rgb(255,210,31) bg rgb(235,194,40) 1.18   ← outline inherited from .cap
  #shadow    "Its own nukes?"  1.8   #kw-shadow "nukes?" 2.02               ← text-shadow, unchanged
  #stroke-own "Its own nukes?" 1.71                                          ← no outline at all, real failure
this branch     contrast checked 30 passed 15
  (#kw-stroke gone; the three others unchanged)

The original project (South Africa map Short):

main affa03b6   checked 22 passed 19
  em "nukes?" 1.19:1 · b "6" 2.63:1 · em "nukes," 1.18:1      (all outlined, all readable on the frame)
this branch     checked 22 passed 22

Inherited colour (<div style="color:#c9c9c9">Pale grey line with a <b>bold word</b> inside</div> on yellow):

main         checked 5   → only #line reported (1.03:1); <b id="word"> never measured
this branch  checked 10  → #line and #word both reported (1.03:1)

Gates: oxlint and oxfmt are clean. fallow audit --base origin/main passes in the lefthook pre-commit. The comment ratchet passes, and the lefthook typecheck passes.

…rast

__contrastAuditPrepare hid each text element's colour and stroke as it walked
the tree. -webkit-text-stroke and color are inherited, so once an outlined
caption was hidden, a highlighted word inside it (<em> with its own fill and
the caption's stroke) read its stroke as transparent. The word was judged by
its fill alone: a yellow keyword with a 12px black outline on a yellow map
was reported as 1.18:1. A child that inherits its colour was skipped outright
(its colour read as transparent), so it was never measured.

Read every candidate first and hide the paint after the walk.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant