Skip to content

fix(DateInput): the scrolling picker column's rows keep AA contrast; the depth fade uses theme text colours - #9853

Merged
cixzhang merged 2 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-contrast
Nov 4, 2026
Merged

cixzhang merged 2 commits into
meta:mainfrom
vjeux:uikit/c21-wheel-contrast

Conversation

@vjeux

@vjeux vjeux commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

User impact

Mouse users get the scrolling wheel column in the bottom-sheet presentation. Its rows faded by opacity toward the ends, so rows near the ends fell below WCAG AA at rest (neutral light: 3.50:1 four rows out; matcha light: 2.38:1). The release audit's still frame, with every row at 30% opacity, fails.

Problem and solution fit

  • The depth cue was an opacity fade on a scroll-driven view() timeline.
  • The audit holds every animation at its end, so every row, the selected one too, takes the 0.3 keyframe. axe cannot measure the tipped rows, so it reports one node.
  • The fade predates feat(Wheel): UIPickerView's motion under a finger, with flat rows #8159, whose story first rendered the bare column.

So the depth cue moves from opacity to theme text colours.

Expected behavior and authority

WCAG 2.2 SC 1.4.3 (AA): 4.5:1 in every shipped theme, light and dark.

Smallest restoration

  • Wheel.tsx: the row's colour fades from the primary text colour at the centre to halfway toward the secondary one at the ends. Secondary alone fails AA in chocolate light (3.06:1) and matcha light (4.41:1); the mix passes everywhere.
  • Tip, motion and snap areas are unchanged; the selected row keeps its accent. Disabled rows keep main's opacity fade (inactive, so exempt). Reduce Motion keeps both.
  • The story's readout takes the theme's colours (gothic light was 1.14:1).

Before and after

Desktop core-wheel--minutes, still frame (rows at their fade end); chocolate is main's lowest theme. Accent: --color-text-accent; mix: the halfway mix.

Band row: main → head Fade-end row: main → head
Neutral light #b0b0b0 on #f0f0f0, 1.90:1 → accent, 15.11:1 #b3b3b3 on #ffffff, 2.11:1 → mix, 18.10:1
Neutral dark #727272 on #3c3c3c, 2.30:1 → accent, 9.75:1 #676767 on #262626, 2.68:1 → mix, 9.52:1
Chocolate light #d8c4b0 on #f8f2ea, 1.52:1 → accent, 5.29:1 #c9c0b6 on #fffcf7, 1.75:1 → mix, 5.81:1
Chocolate dark #624e39 on #312b24, 1.78:1 → accent, 6.02:1 #5b544b on #1c1610, 2.39:1 → mix, 10.77:1

Evidence

  • Reproduction before the change: the new spec fails 7 of 7 at the test commit.
  • Result after the change: 7 of 7, each theme light and dark, at rest and still.
  • Representative unchanged path: the Wheel interaction and rest specs pass.
  • Regression test: WheelContrast.a11y.chromium.spec.ts, axe plus per-row measure of the scrolling column.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

At b42c08d on main 55aaeda:

  • Release audit, red then green. node .github/scripts/accessibility-audit.js --components Wheel --baseline .github/a11y-baseline.json --fail-on-new on a build with main's Wheel, story and themes: 1 new serious color-contrast node in core-wheel--minutes, and the gate fails. On this head, over Wheel, DateInput, TimeInput and DateTimeInput: 0 violations, and the gate passes.
  • WheelContrast.a11y.chromium.spec.ts (the scrolling column): 7 of 7 fail at the test commit, and 7 of 7 pass at the head. WheelRest.a11y.chromium.spec.ts: 11 of 11, including a disabled row's fade under Reduce Motion, which fails on the previous head.
  • The PR's accessibility contracts (26 files, CI=1): 130 passed. Chromium interaction specs for DateInput, TimeInput and DateTimeInput: 22 passed, 2 skipped.
  • Units for DateInput, TimeInput and DateTimeInput: 865 of 865, with the markup snapshot updated (class lists only).
  • Typecheck, lint on the changed files, check:changesets, check:knowledge, check:repo, check:motion-roles, check:token-docs, sync:exports:check and verify-exports pass. review:pre finds nothing (377 authored lines).

@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 4, 2026 3:16am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 4, 2026
@astracat-bot

astracat-bot Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 4, 2026, 3:40 AM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · b42c08d

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
DateTimeInput 168.6 kB (+157 B) · 17.2 kB CSS (+102 B) — — · — · —
DateInput · Storybook 148.0 kB (+97 B) · 15.5 kB CSS (+102 B) 5,042 (+32) ✓ · ✓ · ✓
PowerSearch 236.2 kB (+95 B) · 21.8 kB CSS (+94 B) — — · — · —
1 more component
Component Size LoC Visual · A11y · RTL
TimeInput 132.7 kB (+86 B) · 14.5 kB CSS (+96 B) — — · — · —

Package size: @astryxdesign/core 6.30 MB packed (+670 B); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: core 1 row; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the colour fade fixes the mouse wheel column's contrast in every shipped theme, but the new contrast spec also reports the touch column as passing when it is not.

  • WheelContrast.a11y.chromium.spec.ts runs core-wheel--touch-minutes through a measure that reads only opacity, so it passes touch rows that PickerWheel's mask-image fade draws at about 2.3–4.1:1. Measure only the scrolling column there and narrow the file header, or fold the mask into the measure.

Two small follow-ups: the comment at PickerWheel.tsx:79-81 still describes the scrolling column's old 0.3 opacity fade, and the new fade comment in Wheel.tsx could keep only its reason.

[Automated review]

await storybook?.close();
});

const STORIES = ['core-wheel--minutes', 'core-wheel--touch-minutes'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: core-wheel--touch-minutes renders PickerWheel, whose rows are faded by the mask-image on its fade layer (PickerWheel.tsx:84-90). rowsBelowAA multiplies only opacity, and axe ignores masks, so these cases pass rows drawn at about 2.3–4.1:1 (the row four out, in 13 of 14 theme × mode cases). Measure only the scrolling column here and narrow the header at lines 8-10, or fold the mask alpha into the measure.

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated
Comment on lines +81 to +84
* The depth cue in colour, on the same timeline. A centred row uses the
* primary text colour. A row at either end eases halfway to the secondary text
* colour. That mix meets WCAG AA in every shipped theme. The secondary colour
* alone does not, and an opacity fade falls below AA.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (comment style): the second and third sentences restate the keyframes. Suggested: "Colour keeps every row at WCAG AA, which an opacity fade does not. The ends stop halfway to the secondary text colour, because that colour alone falls below AA in two shipped themes."

…y shipped theme, at rest and still

A Chromium spec opens the Core/Wheel minutes story, the scrolling column, in each shipped theme, light and dark, at rest and in the still frame the release audit scans. It runs axe's color-contrast rule, and it measures each row in view itself, from the text colour at its drawn opacity over the backgrounds behind it, because axe reports those rows' backgrounds as unknown inside the scroller. Red: the column fades its rows to 30% opacity, which falls below 4.5:1, and the readout field is white under the theme's text colour in a dark theme.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: the colour fade restores AA contrast for the mouse wheel column in every shipped theme, but it also stops disabled rows from fading.

  • Wheel.tsx:585 leaves disabled rows at full --color-text-disabled at every distance, where main fades them to about 0.46–0.73 opacity as component:DateInput DEC-2 has every row fade, so give disabled rows their own scroll-timeline fade and keep a reduced-motion check on a bounded wheel's disabled row, as the WheelRest case did before it moved to the minutes story.

Small follow-ups: the new comments at Wheel.tsx:174-175 and :192-193 could keep only their reason, and the changeset's "a mouse wheel's rows" reads as the mouse's scroll wheel.

[Automated review]

{...optionProps(option, index)}
{...stylex.props(
styles.item,
index !== activeIndex && !option.isDisabled && styles.itemFade,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabled rows skip the fade here and get no other one, so in a bounded sheet they stay at full --color-text-disabled at every distance. On main they fade to about 0.46–0.73 opacity with the rest, and component:DateInput DEC-2 has the column fade its rows. Giving disabled rows their own scroll-timeline fade (for example the old opacity keyframes on their inner span, since WCAG 1.4.3 exempts inactive components) would keep the depth cue there.

await page.emulateMedia({reducedMotion: 'reduce'});
const {hours} = await openHourWheelWithMouse(page);
await page.goto(
`${storybook.origin}/iframe.html?id=core-wheel--minutes&viewMode=story`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous version of this case checked hour 8 in the bounded TimeInput sheet, a disabled row. On this head that row reports opacity 1 and a constant colour, so the old assertion would fail. Moving to the minutes story drops that coverage; a reduced-motion check on a bounded wheel's disabled row would keep it.

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated
Comment on lines +174 to +175
// themselves out once on the document timeline. Under reduced motion the
// rows do not tip; the colour fade on the row keeps the depth cue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (comment style): the first half restates the 'none' value. Suggested: "Under reduced motion, the colour fade on the row keeps the depth cue."

Comment thread packages/core/src/DateInput/Wheel.tsx Outdated
Comment on lines +192 to +193
* not move a snap area. The selected row and disabled rows keep their own
* colours, so only the other rows take it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (comment style): this restates the condition at line 585 without the reason. Suggested: "An animated colour outranks the selected and disabled colours. Those rows do not take this fade."

…the depth fade uses theme text colours

The scrolling column faded its rows to 30% opacity toward the ends of its view() timeline, so every row but the centred one fell below 4.5:1. In the still frame the release audit scans, every animation ends, so the selected row fell to 1.9:1 too. The fade now runs on the row's colour: the primary text colour at the centre, easing halfway to the secondary text colour at the ends. That mix meets AA in every shipped theme; the secondary colour alone does not in two. The tip stays on the inner element, and the selected and disabled rows keep their own colours. Disabled rows keep main's opacity fade on the same timeline, which WCAG 1.4.3 allows for inactive components. Under Reduce Motion the rows stay flat and keep the colour fade. The story's readout field takes the theme's colours, so a dark theme no longer draws its text on a white fill.
@vjeux
vjeux force-pushed the uikit/c21-wheel-contrast branch from 236f5ac to b42c08d Compare November 4, 2026 03:04
@vjeux vjeux changed the title fix(DateInput): a mouse wheel's rows keep AA contrast; the depth fade uses theme text colours fix(DateInput): the scrolling picker column's rows keep AA contrast; the depth fade uses theme text colours Nov 4, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge: the colour fade keeps every enabled row of the mouse wheel column at WCAG AA in all seven shipped themes, light and dark, at rest and in the audit's still frame, and disabled rows fade again as they do on main.

  • Worth a look before merging: in Neutral light the fade now runs from black to about #0c0c0c across the visible rows, so the tip alone carries the depth cue there; Chocolate light and Neutral dark keep a visible fade.

[Automated review]

@cixzhang cixzhang added the ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) label Nov 4, 2026
@cixzhang
cixzhang marked this pull request as draft November 4, 2026 03:48
@cixzhang
cixzhang marked this pull request as ready for review November 4, 2026 03:48
@cixzhang
cixzhang merged commit 42ccd7c into meta:main Nov 4, 2026
105 of 110 checks passed
@vjeux
vjeux deleted the uikit/c21-wheel-contrast branch November 4, 2026 04:47
cixzhang added a commit that referenced this pull request Nov 4, 2026
…ight test (#9888)

test(DateInput): the wheel contrast proof imports the hermetic Playwright test

#9853 added the spec with @playwright/test; #9811 made that import an
ESLint error. Each was green alone; together main Lint fails.

This branch was successfully deployed

1 active deployment
Preview — b42c08dd Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants