The ink scanner reads the WRONG ELEMENT three ways — a descendant variant, a conditional `disabled`, and `placeholder:` text — so its verdict can be about an element that is not the one being painted
Found while running MOTIR-2475 — the faint sweep the scanner from MOTIR-2459 enables. Three sites in the swept tree turned out to be ruled on an element other than the one the ink actually paints, in both directions. The sweep fixed those three instances by restructuring them; the SCANNER still cannot see any of the three shapes, so the guard will not catch the next one.
Repo: motir-core. One PR.
The three shapes, each with the fixture that exposed it
tests/theme/inkContrastScan.ts attributes a class literal to the JSX element whose className attribute lexically contains it (owningElement), then judges THAT element. Three constructs break the assumption that the two are the same element.
| # | shape | what the scanner decided | what was true | fixture (as it stood on 0d5279da) |
|---|---|---|---|---|
| 1 | a descendant variant — [&_svg]:text-(--el-text-faint), [&_.seg-trail]:… | judged the ANCESTOR that carries the class | the ink lands only on a descendant | PublicTabNav.tsx:75 — a false POSITIVE: ruled violation because the <Link> renders a label, while the ink only ever painted an aria-hidden glyph inside it |
| 2 | a conditionally disabled control — disabled={disabled || opt.disabled} | disabled — the 1.4.3 exemption, unconditionally | the control is disabled only sometimes; the enabled render is unexempt | Segmented.tsx:87 — a false NEGATIVE: an inactive segment's trailing COUNT painted at ~2.4:1 and was never reported |
| 3 | placeholder: ink on a self-closing labelled control | decorative — "a labelled control whose content is glyphs only" | rendersText is false for a self-closing element, but a placeholder IS text | OnboardingEntrance.tsx:141 — a false NEGATIVE: the entrance textarea's placeholder at 2.61:1 |
Shape 2 and 3 are the dangerous ones: they make a defect invisible, and the guard's silence then reads as coverage. Shape 1 only costs a spurious failure — but it pushes the next author toward restructuring code to satisfy a parser rather than a reader.
This is about the SCANNER, not the sweep. All three instances are already fixed on main by MOTIR-2475; this card is the class.
Acceptance criteria
scanSourcejudges a descendant-variant class against the descendant it selects, not the element carrying it: a[&_…]:text-(--el-text-faint)whose selector cannot be resolved to an element in the file is reported asunattributablerather than silently attributed to the ancestor — proven by a fixture holding both arms (anaria-hiddenglyph target and a text target).- A class in a conditionally disabled control (a
disabled/aria-disabledattribute whose initializer is an EXPRESSION rather than a literaltrue) no longer takes the 1.4.3 exemption — the exemption holds only for an unconditionally disabled element, or for the disabled branch of a ternary, whichinDisabledBranchalready covers. A fixture carriesdisabled={a || b}and expectsviolation. - A
placeholder:-prefixed class is judged as TEXT on every element, self-closing or not — theaccessibleName && !rendersTextdecorative arm must not swallow it. A fixture carries<textarea aria-label placeholder:text-(--el-text-faint) />and expectsviolation. - Every new rule is proven in
tests/theme/inkContrastScan.test.tson a fixture before the repo-wide run, in the mould the scanner's own header sets out ("a lint whose negative case is never exercised is a lint nobody knows is running"). tests/theme/inkContrastLint.test.tsstill passes over the whole scanned tree with the widened rules, and any site the widening newly reports is FIXED in this PR, not exempted — the no-allowlist rule from MOTIR-2475 stands.- The PR body states the verdict census before and after the widening, so the delta is legible.
pnpm lint,pnpm typecheck,pnpm prettierandnext buildpass.
Context refs
tests/theme/inkContrastScan.ts—owningElement,isDecorative,isDisabledElement,rendersText: the four functions all three shapes pass through.tests/theme/inkContrastScan.test.ts— the fixture suite to extend.tests/theme/inkContrastLint.test.ts— the repo-wide guard (MOTIR-2475).- MOTIR-2477 — the muted sweep, which runs through the SAME three filters and inherits all three blind spots.