Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions
Bot
requested review from
cvkxx,
ernestt,
kentonquatman and
rubyycheung
August 11, 2026 22:09
Contributor
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR Modified ComponentsIndicator (@astryxdesign/core) · View in Storybook
Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
cixzhang
enabled auto-merge (squash)
August 11, 2026 22:54
The busy idiom a host writes is `children={isBusy && <Spinner/>}`, which
passes `false` when it is not busy. `false` is not null and is not caught
by `??`, so every indicator took its children path, rendered nothing in
it, and dropped the checkmark, the checkbox tick and the radio dot on
every selected row.
Filed as #4893 against CheckIndicator, where #4890 moved the branch. It
was wider: measured before writing anything, ALL THREE indicators lost
their mark on `false` and on `''`. CheckIndicator additionally leaked an
empty span. #4890 widened its case from one state to every state -- the
unchecked early return had been masking it.
All three now guard with `isRenderable`, the util the repo already ships
for this. `0` still counts as content: it renders the character "0".
Also, both from the #4890 audit and contained to the same lines:
CheckIndicator's children slot now reserves the glyph's 1rem box and
carries its color, so swapping a Spinner in neither shifts the row nor
loses the disabled shade.
Adds apps/storybook/stories/Indicator.stories.tsx -- the indicators had
no story file (#4894), which is why the children path had no rendered
coverage and this shipped unseen.
Not fixed here: `ref` is dropped on the glyph path. Icon in string mode
routes through IconFromRegistry, which takes no ref, so that is a change
to Icon's ref behavior for every caller -- its own PR.
CI caught what my local run could not: 'satisfies Meta' + StoryObj<typeof meta> makes `args` required on every story, and `state` is required on an indicator, so five render-only stories failed the storybook typecheck. 122 of the 123 story files use `const meta: Meta<typeof X>` with `StoryObj<typeof X>`; mine was the one that did not. Now it matches. Verified the way CI does, after a full `pnpm build`: storybook typecheck clean, storybook build completes, core typecheck (incl. tests) clean, cli typecheck:strict clean, docsite generate + 364 tests green.
…try types The docsite keeps its own copy of the authoring doc types, and `next build` type-checks the emitted componentRegistry.ts against it. Two fields had drifted out of that copy, so any full docsite build failed: deprecatedFor added to ComponentThemingTarget by #4712 (Indicator's renamed theme targets) replaces on ComponentThemingDerivedVar, used by TextArea Pre-existing on main, not introduced here -- verified by building the docsite on a clean origin/main worktree, which fails with the same `'deprecatedFor' does not exist in type 'ThemingTarget'`, and passes (315/315 static pages) with only this one-file fix applied. It surfaced on this PR because the change touches core .doc.mjs inputs, so Vercel's ignored-build-step ran a docsite build for the first time since #4712. Adds SYNC breadcrumbs on both authoring interfaces, since `check:sync` is the mechanism the repo already uses to catch exactly this drift. Diffed all six shared shapes; these two were the only gaps.
This was referenced Aug 11, 2026
cixzhang
added a commit
that referenced
this pull request
Aug 12, 2026
An indicator is aria-hidden by contract: the owning control supplies the
role and the accessible name, so an indicator a consumer un-hides gets
that control announced twice. All three forwarded a caller's aria-hidden,
role and aria-label straight through, so `<CheckIndicator
aria-hidden="false" />` rendered exactly that. Rubric P3 puts owned aria-*
after {...rest}, at BLOCK severity.
Pre-existing and missed by all three prior audit passes, including the two
I ran on this component today.
Two mechanisms, because one does not cover both cases:
- Spread order on the siblings: {...rest} moves ahead of their own
aria-hidden, which is P3's letter.
- Stripping the three props on all three: ordering cannot help
CheckIndicator's glyph path, because it renders Icon and Icon
deliberately lets a caller override its a11y defaults as a documented
escape hatch. Forwarding would hand that hatch to an element that must
never take it. And role/aria-label are not emitted by the siblings at
all, so ordering cannot stop those either -- my own test caught that
after the first attempt.
Ordinary props still forward: data-*, id, handlers and styling are pinned
by a negative control in each case, so "strip a11y" cannot quietly become
"strip everything".
Also fixes the doc example, under the rubric rule this prompted (X21,
added in wiki 90431f1): Indicator.doc.mjs was the only worked example of
writing a replacement indicator, and it still taught `{children ?? mark}`
-- the exact shape #4913 removed from all three shipped components because
it deletes the state mark on a falsy child (#4893). The example now uses
isRenderable, and the bestPractice that said "render children when
present" now says what "present" means and why `??` is the trap.
Verified: 7 of the new tests go red against origin/main; 386 pass across
the subject and every consumer; core tsc, typecheck:docs and strict lint
clean; docsite generate + 364 tests green.
cixzhang
added a commit
that referenced
this pull request
Aug 12, 2026
…ff (#4918) (#4921) * fix(indicator): the decorative contract is not a caller's to switch off An indicator is aria-hidden by contract: the owning control supplies the role and the accessible name, so an indicator a consumer un-hides gets that control announced twice. All three forwarded a caller's aria-hidden, role and aria-label straight through, so `<CheckIndicator aria-hidden="false" />` rendered exactly that. Rubric P3 puts owned aria-* after {...rest}, at BLOCK severity. Pre-existing and missed by all three prior audit passes, including the two I ran on this component today. Two mechanisms, because one does not cover both cases: - Spread order on the siblings: {...rest} moves ahead of their own aria-hidden, which is P3's letter. - Stripping the three props on all three: ordering cannot help CheckIndicator's glyph path, because it renders Icon and Icon deliberately lets a caller override its a11y defaults as a documented escape hatch. Forwarding would hand that hatch to an element that must never take it. And role/aria-label are not emitted by the siblings at all, so ordering cannot stop those either -- my own test caught that after the first attempt. Ordinary props still forward: data-*, id, handlers and styling are pinned by a negative control in each case, so "strip a11y" cannot quietly become "strip everything". Also fixes the doc example, under the rubric rule this prompted (X21, added in wiki 90431f1): Indicator.doc.mjs was the only worked example of writing a replacement indicator, and it still taught `{children ?? mark}` -- the exact shape #4913 removed from all three shipped components because it deletes the state mark on a falsy child (#4893). The example now uses isRenderable, and the bestPractice that said "render children when present" now says what "present" means and why `??` is the trap. Verified: 7 of the new tests go red against origin/main; 386 pass across the subject and every consumer; core tsc, typecheck:docs and strict lint clean; docsite generate + 364 tests green. * refactor(indicator): enforce the decorative contract in the type, not by stripping Replaces the runtime strip from the previous commit. Dropping a caller's props into `_ariaHidden` and friends made a prop vanish with no signal; a type says so at the call site. The type only gets half of it, and the half matters: role identifier -> excess-property check applies -> compile error aria-hidden hyphenated -> TypeScript EXEMPTS it -> compiles, always That is a language rule, not a gap in our types -- proven with a probe on a bare interface with no Astryx involved. So `IndicatorProps` omits all four (intent, and `role` actually enforced), and each component emits its own `aria-hidden` AFTER `{...rest}`, which is what keeps a caller from un-hiding it. That is rubric P3's literal prescription, and it is attribute order rather than a disappearing prop. CheckIndicator's glyph path re-asserts `aria-hidden` on the Icon call for the same reason: Icon puts its a11y defaults BEFORE `{...props}` as a deliberate escape hatch, which is right for an icon and wrong for an element that is decorative by contract. Nothing is stripped now. A forwarded `aria-label` reaches the DOM and is inert inside an aria-hidden subtree -- forwarding aria props is fine, which was the point. The test that documents the TS exemption carries NO @ts-expect-error on purpose: it compiles today, and it starts failing the day TypeScript changes its mind, which is when the ordering can go. Verified: 3 runtime tests + the `role` type test go red against origin/main; 384 pass across the subject and every consumer; core tsc, typecheck:docs and strict lint clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4893 — a regression I introduced in #4890.
The bug
The busy idiom a host actually writes is:
When
isBusyisfalsethat passesfalse.falseis notnull, and it is not caught by??— so every indicator took its children path, rendered nothing there, and deleted the state mark on every selected row.It was wider than the issue says
#4893 was filed against
CheckIndicator, because that is where #4890 moved the branch. I probed all three before writing anything, and all three were affected — the siblings'{children ?? mark}has the same hole, and it predates #4890:childrenfalse''null/undefined<b/>So
CheckIndicatoradditionally leaked an empty<span>; the mark loss is the whole family. #4890 widened the CheckIndicator case from one state to every state — before it, the unchecked early return happened to mask it.The fix
isRenderable(children)in all three — the util the repo already ships for exactly this (utils/isRenderable.ts), and what@astryx/no-nullish-jsx-guardsteers to. That rule could not catch this: it sees JSX guards, not an early-returnifor a??fallback.0deliberately still counts as content — it renders the character0, so it is a real child. There is a test pinning that.Two contained follow-ons in the same branch, both from the #4890 audit:
CheckIndicator's children slot now reserves the glyph's box (1rem, Icon'ssm, which is what both indicator sizes resolve to). The doc says children "keeps the indicator's place"; it did not — the row shifted when a Spinner swapped in.--color-icon-disabledwhen disabled, else accent), so an inherit-shade Spinner reads correctly. The sibling indicators already setcoloron their chrome for this reason. That closes the "size/isDisableddropped on the children path" FIX.Deliberately not fixed here:
refis dropped onCheckIndicator's glyph path. It looked like a one-liner and is not —Iconin string mode routes throughIconFromRegistry, which accepts no ref, so fixing it means changingIcon's ref behavior for every caller. That is its own PR, not a rider on a regression fix. It stays open in the ledger.New:
apps/storybook/stories/Indicator.stories.tsxThis is scope I added on purpose. The indicators had no story file — rubric X9, open as #4894 — and the audit found that is why this bug shipped unseen: the
childrenpath had no rendered coverage anywhere, so nothing could have shown it. A regression fix with no way to see the regression would repeat that.Five stories:
AllStates,Sizes,Disabled,BusyChildren(the case above, both columns),BusyToggle(flipisBusylive). This also un-blocksrtl:audit, which currently reports 0 stories / 0 checks for Indicator.Test plan
The new tests fail against main. I reverted the three source files in this same tree and re-ran:
All 29 pass with the fix. Each indicator also has a negative control — a real child still replaces the mark — so "always draw the mark" cannot pass this suite.
Visual, measured rather than asserted. Two Storybooks:
mainat47f8bb4560with this same story file copied in, and this branch. Real Chromium,deviceScaleFactor: 4, storycore-indicator--busy-children, neutral light.DOM count of visible marks in the not-busy column: before 0 of 4, after 4 of 4.
Pixel count of mark-ink inside each indicator's chrome:
(For the filled chromes the mark is light-on-accent, so total non-white ink goes down slightly while the mark appears — counting light pixels enclosed by painted ones is the honest measure. Crops in
/tmp/shots-4893/crops/.)Suites: vitest 374 passed across Indicator + CheckboxInput + RadioList + Selector + DropdownMenu + CheckboxList;
tsccore clean;CI=true eslint --no-cache0 errors 0 warnings;check:changesets55 valid.Note on the audit trail
The ledger row for Indicator is 79.1 / C with three open BLOCKs (#4893, #4894, #4895). This PR closes #4893 and, via the story file, #4894. Per the rubric's closing protocol I will re-audit and update the ledger once this merges — the score has to move.
Two CI fixes on top, both pre-existing rather than caused here
build-storybook— storybook typecheck. My first push usedsatisfies Meta+StoryObj<typeof meta>, which makesargsrequired on every story;stateis required on an indicator, so the five render-only stories failed. The other 122 story files useconst meta: Meta<typeof X>withStoryObj<typeof X>; mine now matches. My fault, caught by CI, fixed ina913c40406.Vercel— the docsite build. This one is not from this PR.next buildtype-checks the emittedcomponentRegistry.tsagainst the docsite's own copy of the authoring doc types, and two fields had drifted out of that copy:deprecatedFor(added toComponentThemingTargetby #4712) andreplaces(onComponentThemingDerivedVar, used by TextArea).Verified with a control rather than asserted: a clean
origin/mainworktree fails the docsite build with the same'deprecatedFor' does not exist in type 'ThemingTarget', and passes 315/315 static pages with only the one-file type fix applied. It surfaced here because this PR touches core.doc.mjsinputs, so Vercel's ignored-build-step ran a docsite build for the first time since #4712.I diffed all six shapes the docsite duplicates; those two were the only gaps. Both authoring interfaces now carry a
SYNC:breadcrumb, which is the mechanism the repo already uses to catch this.One more thing seen while chasing it:
/communityprerenders a GitHub API call, and a 403 rate-limit from a build machine takes the whole build down (Error occurred prerendering page "/community"). I hit it locally, and the same build passed once the limit reset. ThefetchContributorscatch returns[], so something downstream of an empty list is the actual crash. Not this PR's to fix, but it is a real build-flake and worth its own issue.Rebased after #4911 and #4915 landed. Both overlapped this PR while it sat in CI:
@examplefences — the same X14 fix ([audit] Indicator: every @example JSDoc fence in the Indicator sources is language-tagged ```tsx, which the repo documents as silently breaking Storybook autodocs rendering — 7 occurrences across 5 files, introduced by #4712 and missed by both prior audits #4895) I was holding back so it would not race this PR's armed merge. Dropped mine; its version is in the base now.generate-data.mjstype sync. Dropped my duplicate and took its file verbatim. What I kept is the pair ofSYNC:breadcrumbs on the authoring interfaces — fix(docsite): sync ThemingTarget/DerivedVar with the authoring types #4915 does not have those, and they are what stops the drift coming back a third time.What remains here is only the falsy-children fix, its tests, and the story file. Re-verified post-rebase: 280 tests green, core
tscclean, storybooktscclean.