Skip to content

fix(DST-1717): expose dependencies on the components that own their collection - #5740

Open
sebald wants to merge 3 commits into
mainfrom
fix/DST-1717-expose-dependencies
Open

fix(DST-1717): expose dependencies on the components that own their collection#5740
sebald wants to merge 3 commits into
mainfrom
fix/DST-1717-expose-dependencies

Conversation

@sebald

@sebald sebald commented Aug 14, 2026

Copy link
Copy Markdown
Member

Description

React Aria collections cache each rendered item against the item object. A render function that reads anything else — a label from state, a lookup by id — keeps rendering the value it first saw. The escape hatch is dependencies, listed like a hook's dependency array, but it only exists on the collection components themselves.

Select, ComboBox, TagGroup, TagField and Autocomplete own their collection internally, so there was no way to reach it: the option or tag went stale and stayed stale, with no error and no warning.

All five now accept dependencies and forward it to every collection they render — the popover copy and the tray/dropdown copy are separate collections and each needs it.

<Select items={people} dependencies={[shift]}>
  {person => (
    <Select.Option id={person.id}>
      {person.name}{shift}
    </Select.Option>
  )}
</Select>

Closes DST-1717

Split out of #5727

This rode along on the Table expandable-rows branch (DST-1702), where Table.Body gets dependencies from RAC for free — which is what made the gap in these five obvious. It is unrelated to that feature, so it ships on its own: the collection fix doesn't wait on a Chromatic run for the Table, and both diffs stay reviewable. Raised in review at #discussion_r3776037196.

One extra fix the tests forced out

Autocomplete shipped dependencies that you could not actually use. Its children inherited RAC's ChildrenOrFunction<ComboBoxRenderProps> — render props, not an item function — so items plus a render function only type-checked through a cast, and that pattern is the entire reason dependencies exists.

AutocompleteProps now takes an optional item type and declares the render function on children, the same shape TagFieldProps already uses:

export interface AutocompleteProps<T extends object = object>
  extends Omit<RAC.ComboBoxProps<T>, RemovedProps> {
  children?: ReactNode | ((item: T) => ReactNode);
}

AutocompleteBase becomes a generic function so T is inferred from items, mirroring TagFieldBase. Type-level only — no runtime change, and existing uses (static <Autocomplete.Option> children) are untouched. It deliberately does not copy ComboBox's (item: any), which is only any because that interface doesn't omit children before redeclaring it.

Screenshots / Preview

No visual change — dependencies alters when an item re-renders, not how it looks. All WithDependencies stories are snapshot-disabled.

Test Instructions

Each component gets a WithDependencies story plus a .test() that proves the stale render goes away: render options from items plus a function that reads a shift value from state, flip the state, assert the option text followed.

The tray path is now covered too. The first round of tests all drove the popover, which left the second forward per component — the half that only fails on touch — unproven. Select, ComboBox, TagField and Autocomplete each add a WithDependenciesMobile story (viewport: smallScreen) whose test opens the tray and asserts the same thing.

Verified load-bearing by mutation rather than trusted green. Dropping dependencies from Select's tray ListBox:

× Select > With Dependencies Mobile > Re-renders the tray options when a listed dependency changes
✓ Select > With Dependencies        > Re-renders the options when a listed dependency changes

Only the mobile test fails — which is precisely the blind spot this PR was flagged for.

Two things the tray tests needed that the popover ones did not, both worth knowing if you write more:

  • the trigger is matched by role, because the tray title repeats the field label and getByLabelText then finds two nodes;
  • closing the tray has to be awaited — the underlay swallows clicks until it unmounts, so a click on the control behind it silently does nothing. This one masqueraded as a stale render.
  1. pnpm sb → each component's With Dependencies / With Dependencies Mobile story → press Switch shift and re-open the list; the option label should follow.
  2. Automated: pnpm typecheck:only; pnpm test:sb --run Select ComboBox TagGroup TagField Autocomplete (125 passing).

Worth a reviewer's eye

TagGroup is the one component that gets no second test, deliberately. Its collapseAt branch does render a second TagList, but canCollapse is collapseAt !== undefined && typeof children !== 'function' — so a render function can never reach that branch, and with static children there is no per-item cache to go stale. Its dependencies forward is therefore inert today; it is kept, with a comment, so the two branches stay in step if collapse ever learns to handle items. Happy to drop it instead if you'd rather not carry unreachable wiring.

@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2f617c9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@marigold/components Minor
@marigold/docs Minor
@marigold/cli Major
@marigold/theme-rui Patch
@marigold/system Minor
@marigold/icons Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
marigold-docs Ready Ready Preview Aug 21, 2026 11:57am
marigold-storybook Ready Ready Preview Aug 21, 2026 11:57am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
marigold-production Ignored Ignored Aug 21, 2026 11:57am

Request Review

@github-actions github-actions Bot added the type:feature New feature or component label Aug 14, 2026
sebald added a commit that referenced this pull request Aug 14, 2026
Select, ComboBox, TagGroup, TagField and Autocomplete have nothing to do with
expandable rows; they now ship in #5740 (DST-1717).
@github-actions

Copy link
Copy Markdown
Contributor

Accessibility tests executed. Download the report here.

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Marigold Code Coverage

Status Category Percentage Covered / Total
🔵 Lines 98.78% 2923 / 2959
🔵 Statements 97.79% 3062 / 3131
🔵 Functions 97.92% 803 / 820
🔵 Branches 91.22% 1893 / 2075
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/components/src/Autocomplete/Autocomplete.tsx 84.21% 100% 83.33% 84.21% 83-85
packages/components/src/Autocomplete/MobileAutocomplete.tsx 100% 100% 100% 100%
packages/components/src/ComboBox/ComboBox.tsx 100% 100% 100% 100%
packages/components/src/ComboBox/MobileCombobox.tsx 100% 100% 100% 100%
packages/components/src/Select/Select.tsx 100% 100% 100% 100%
packages/components/src/TagField/TagField.tsx 97.56% 94.44% 100% 100% 238
packages/components/src/TagGroup/TagGroup.tsx 100% 84.31% 100% 100%
Generated in workflow #23597 for commit 2f617c9 by the Vitest Coverage Report Action

sebald added 2 commits August 14, 2026 11:23
… collection

Select, ComboBox, TagGroup, TagField and Autocomplete render their collection
internally, so a consumer had no way to invalidate React Aria's item cache: an
option whose label read outside state kept the value it first rendered with.

Each now forwards `dependencies` to every collection it renders, including the
tray and popover copies. Autocomplete additionally types the item render
function on `children` — it inherited RAC's render-props type, so the pattern
`dependencies` exists for only type-checked through a cast.

Split out of #5727 (DST-1702), where this rode along unrelated to the Table.
Every `WithDependencies` test drove the popover, so the second forward per
component — the one the ticket flags as the risky half, because it only fails
on touch — was unproven. Adds a small-screen story and test per component that
opens the tray and asserts the option text follows the outside state.

Verified by mutation: dropping `dependencies` from Select's tray `ListBox`
fails only the new mobile test, and leaves the desktop one green.

Two things the tray tests needed that the popover ones did not: the trigger is
matched by role because the tray title repeats the label, and the underlay
swallows clicks until it unmounts, so closing has to be awaited before pressing
anything behind it.

TagGroup gets a note instead of a test. Its `collapseAt` branch renders a
second `TagList`, but `canCollapse` requires non-function `children`, so a
render function can never reach it and there is no stale render to provoke.
The forward stays so the branches keep in step.

Also swaps a raw `<button>` in the ComboBox story for `Button`, matching the
other four.
@github-actions

Copy link
Copy Markdown
Contributor

Accessibility tests executed. Download the report here.

@aromko aromko 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.

PR Review: #5740 - fix(DST-1717): expose dependencies on the components that own their collection

Overview

Field Value
Author @sebald
Branch fix/DST-1717-expose-dependenciesmain
Files Changed 13
Additions +641
Deletions -15
CI All green (Typecheck, Lint, Format, Unit Tests 1-4, Storybook Tests 1-4, Size Limit, CodeQL)
Visual Regression Not started

Linked Ticket

DST-1717: 🐛 Expose dependencies on the components that own their collection

  • Status: In Progress
  • Scope match: complete. All five components accept dependencies, both render paths are forwarded per component (popover ListBox and tray / MobileAutocomplete / TagFieldDropdown), each gets a WithDependencies story plus a .test(), and the changeset is minor on @marigold/components.
  • The ticket calls out the second forward per component as the thing to check in review — that is exactly what the four WithDependenciesMobile tests cover, and the description documents a mutation check (dropping dependencies from Select's tray ListBox fails only the mobile test).

Code Review Checklist

TypeScript Standards

  • No new any types — dependencies resolves to RAC's ReadonlyArray<any> through ListBoxProps['dependencies'], which is the ticket's stated typing
  • Uses import typeimport { ListBox, type ListBoxProps } in all five call sites
  • Proper interface definitions — AutocompleteProps<T extends object = object> gains the item render function and mirrors TagFieldProps
  • ComboBoxProps keeps its pre-existing (item: any) render function (see Suggestions)

Component Patterns

  • disabled/loading/error naming untouched; no isDisabled leakage
  • No className or style exposed — RemovedProps still omits both everywhere, and Autocomplete additionally omits children
  • useClassNames untouched; no hardcoded styles added
  • dependencies is destructured out of props in all five, so it never lands on a DOM node via ...rest
  • AutocompleteBase becomes a generic function declaration but keeps ref in props and the Object.assign compound shape, matching TagFieldBase

React Best Practices

  • Functional components only
  • No conditional hooks; the isSmallScreen branch stays below the hook calls
  • No new re-render or memoisation concerns — this is a prop forward; dependencies is the deliberate cache-invalidation escape hatch, not an optimisation

Testing Standards

  • Vitest / storybook/test, no Jest APIs
  • userEvent, never fireEvent
  • Accessible queries throughout (getByRole, getByLabelText, findByRole) — no getByTestId
  • Every new story carries tags: ['component-test'], and .test() children inherit it
  • chromatic: { disableSnapshot: true } on all new stories, consistent with the "no visual change" claim
  • Module-scope userEvent used where the test context provides one (see Suggestions)

Accessibility

  • No ARIA, focus or keyboard behaviour changed — the diff is prop plumbing plus stories
  • Tray tests scope the trigger by role because the tray title repeats the field label, and await the underlay unmounting before clicking behind it; both are documented inline

Issues Found

Critical (Must Fix)

  • packages/components/src/Autocomplete/Autocomplete.stories.tsx:536 — 🔴 Visual Regression Tests — not run. This PR changes UI-affecting files (packages/components/src/**, five *.stories.tsx), and no Chromatic build exists for this branch: the workflow is triggered by issue_comment (so runs are recorded against main, not the head branch), and both runs for this PR on 2026-08-14 05:19 came back skipped, not successful.

    Worth weighing against the diff, though: the five component files change no class, style, cn() or useClassNames line — it is pure prop plumbing — and the repo's norm is VRT at merge (DST-1711/1715/1716 all merged with post-merge runs only). So this is a "run it if you want the existing snapshots to back the no-visual-change claim", not a hard blocker. Trigger with /run-chromatic on this PR.

Warnings (Should Fix)

None.

Suggestions (Nice to Have)

  • packages/components/src/ComboBox/ComboBox.tsx:93 — 💡 dependencies exists purely for the items + render-function pattern, but on ComboBox that render function is still children?: ReactNode | ((item: any) => ReactNode) (line 86), so consumers who reach for this new prop get an untyped item. This PR already worked out the fix for exactly this shape on Autocomplete: add 'children' to RemovedProps, make the interface ComboBoxProps<T extends object = object>, and turn ComboBoxBase into a generic function so T is inferred from items. ComboBoxProps also carries a row of pre-existing RAC.ComboBoxProps<any>[...] lookups the same generic would clean up. Clearly out of the stated scope — but the two components now differ in exactly the place the new prop is used, so it's worth either doing here or filing a follow-up.

  • packages/components/src/Select/Select.stories.tsx:1057 — 💡 The new tests take { args, canvas, step } from the test context but reach for the module-scope userEvent imported from storybook/test. CLAUDE.md is explicit: "Destructure canvas, userEvent, args, step from the test context — never within(canvasElement), never a module-scope userEvent." The PR is internally split on it: TagGroup.stories.tsx's WithDependencies.test correctly destructures userEvent from the context, while the new tests in Select, ComboBox, TagField and Autocomplete do not. Fair point that the module-scope form is the dominant existing style in those four files, so a full sweep would be a separate change — new tests are just the cheap place to stop adding to it.

  • packages/components/src/TagGroup/TagGroup.tsx:40 — 💡 Select, ComboBox, TagField and Autocomplete each got the same six-line JSDoc explaining when dependencies is needed, and the docs pages render those descriptions via <AutoTypeTable>. TagGroup picks the prop up through Pick<TagListProps<object>, … 'dependencies'>, so its props table will show React Aria's terser "Values that should invalidate the item cache when using dynamic collections" instead. Redeclaring it on TagGroupProps with the same block would keep the five docs pages consistent.

Notes on the reviewer questions in the description

  • TagGroup's unreachable collapse-branch forward — keeping it is the right call, and the reasoning holds: canCollapse = collapseAt !== undefined && typeof children !== 'function', so a render function can never reach that TagList. Worth a small correction to the framing though: only the collapse-branch forward is inert. The non-collapse TagList (TagGroup.tsx:317) is live, and WithDependencies proves it. The comment parked at the end of TagGroup.stories.tsx says this accurately; the PR body reads as if the whole forward were dead.
  • Coverage of the five is complete — the only other internal items={items} collections in packages/components/src are Calendar/YearListBox, Calendar/MonthListBox (fixed internal data, no outside state) and Breadcrumbs (maps children directly, no dynamic collection), so none of them has the same gap.
  • The Autocomplete children retype is not a practical break — the render-props form it replaces was never wired to the RAC ComboBox anyway; children goes to the ListBox / MobileAutocomplete. Type-only, and the changeset already spells it out.

Recommendation

Comment

The code is in good shape: correct fix, both collections covered per component, tests that were verified load-bearing by mutation rather than trusted green, and full CI passing. Nothing here blocks merge. Optionally run /run-chromatic to have the existing snapshots back the "no visual change" claim; the three suggestions above are take-or-leave.


Generated with Claude Code review-pr skill

Comment thread packages/components/src/Select/Select.stories.tsx Outdated
Comment thread packages/components/src/TagGroup/TagGroup.tsx Outdated
Take `userEvent` from the test context in the eight new tests instead of
the module-scope import, per CLAUDE.md. The import stays for `within` and
`waitFor`, which have no context equivalent; the surrounding pre-existing
tests are left alone.

Redeclare `dependencies` on `TagGroupProps` with the same block the other
four components use. Picked up through `Pick<TagListProps<object>, …>` it
rendered React Aria's terser one-liner in the props table, so the five
docs pages disagreed on what the prop does.
@github-actions

Copy link
Copy Markdown
Contributor

Accessibility tests executed. Download the report here.

@sarahgm
sarahgm requested a review from aromko August 26, 2026 13:28
sarahgm added a commit that referenced this pull request Sep 4, 2026
Running the skill showed three of seven queued rows with a failing
check, and all three fail on `Vercel Preview Comments`. That check is
red while preview comments are unresolved, so `any(conclusion ==
"FAILURE")` was labelling open design feedback as a broken build. Two
different things wanting two different responses, and the second one is
`/triage-feedback`'s job.

Step 7 now classifies a red check three ways: our CI failed (a
`CheckRun` with a non-empty `workflowName`), unresolved preview comments
(the `Vercel Preview Comments` `CheckRun`), or a deploy and integration
problem (a `StatusContext`, or a `CheckRun` with no `workflowName`).
Verified on #5776, where 22 checks carry a `workflowName`, three are
`StatusContext` Vercel deploys, and two are `CheckRun`s without one.

The obvious shortcut is wrong and is called out: empty `workflowName`
does not mean third-party. GitHub's own aggregate `CodeQL` `CheckRun`
has an empty one and sits next to the `Analyze (javascript)` runs that
carry `workflowName: "CodeQL"`, so the shortcut would file a real
CodeQL failure as somebody's integration problem.

The preview-comments check is worth keeping rather than just not
mislabelling: it is the only window `gh` has into Vercel toolbar
feedback, this skill does not talk to the Vercel MCP, and it is
independent of GitHub threads. #5740 has zero unresolved threads and a
red preview check, so a thread count alone renders it clean.

So section 1's `Open threads` column becomes `Open feedback` and carries
both kinds, preview comments qualify a PR for section 2 but sort below a
broken build and below review threads, and a real build failure gets the
line under the table instead of a column, staying out of the ranking.
sarahgm added a commit that referenced this pull request Sep 4, 2026
… both key sources

Section 1 was showing PRs other people are already reviewing. #5684 has
two reviewers, every thread resolved, and nothing there for a third
opinion, yet it ranked first. So the section now keeps exactly two kinds
of PR: ones nobody has reviewed (no reviews, or only dismissed ones),
and ones you have reviewed yourself.

An unresolved preview comment does not count as somebody reviewing. It
is feedback for the author, not a code review, so #5740 and #5748 stay
in the queue: nobody has read either one. Cut rows are named in a
`+N cut (others reviewing)` line rather than dropped, because a PR others
commented on and nobody approved can otherwise sit forever without ever
reaching you.

Rendering real PR titles then exposed a worse bug. Step 5 copied
`/create-pr`'s first-hit-wins cascade, branch before title, which
silently picks the wrong ticket when the two disagree. #5776's branch is
`dst-1745_fix-popover` and its title is `fix(DST-1754): keep popovers
inside the body's clip box`: transposed digits. DST-1745 is a Core-only
invoice-printing migration that is Done, DST-1754 is the popover bug the
PR actually fixes and is In Review in the active sprint. One typo
produced three wrong rows: the PR was demoted as "ticket is Done", it
sank below a key-less PR, and step 8 reported DST-1754 as having no PR
at all.

So step 5 now reads both sources every time and prefers the title when
they disagree, flagging the row. The title is prose, read by every
reviewer, corrected when wrong, and lands in the changelog, while a
branch name is typed once and never fixed. Checked across all 32 open
PRs in the org: #5776 is the only disagreement, so the flag almost never
fires.

That also retires the Done edge case's example. The rule stands, but
there is no live instance of an open PR on a Done ticket, and the one
that looked like it was a typo wearing a costume.
sarahgm added a commit that referenced this pull request Sep 4, 2026
… the example

Two rules could not be followed as written.

The red-check table sent a `CheckRun` with an empty `workflowName` to the
integration row, which files the bare `CodeQL` aggregate as somebody
else's problem. That is the exact mislabelling the paragraph underneath
it forbids, and the table is the part an implementer follows. On #5776
the two empty-name checks are `Vercel Preview Comments` and that `CodeQL`
aggregate, sitting alongside the `Analyze (javascript)` and
`Analyze (typescript)` runs that do carry `workflowName: "CodeQL"`. Rows
are now matched in order, first hit wins, with the name test before the
type test, and the CI row tests no `workflowName` at all. Applied to
#5776's 27 checks that gives 1 preview-comments, 3 deploy, 23 our-CI.
`StatusContext` carries the deploy row alone, because no live `CheckRun`
is genuinely third-party.

The `statusCheckRollup` fetch was scoped to the rows about to be printed,
which section 2 cannot know beforehand: a CI failure and unresolved
preview comments are two of its four qualifying conditions and exist
nowhere but that rollup. Section 1 needs it on every candidate too, since
#5740 and #5748 are in the queue only because of a red preview check. It
now runs once over the whole shortlist, before either filter.

`latestReviews` does not return an empty `commit.oid`. It returns the
full oid on all three PRs the turn table cites, so the `reviews(last:30)`
block that claim justified had no remaining consumer and is gone. The
turn verdicts are unchanged: #5779 author (`5f38f3e` both sides), #5776
yours (`1165e01` against `63ea2b5`), #5761 yours.

Three smaller corrections. The largest departure from the ticket, dropping
`--review-requested=@me`, was the only one with no rationale written down,
so it now carries the numbers: 2 of 15 open PRs on marigold have a review
request of any kind and none of them is mine, so the ticket's query
returns an empty queue that reads as a clear board. The archived-repo
filter asked for `name` and compared it to `owner/repo`, which matches
nothing and leaves every archived repo in the queue, so it asks for
`nameWithOwner`. And the ranking tiers are named rather than numbered,
which also fixes an off-by-one where the prose said "tier 5" while
describing tier 6.

Refreshing the example against a full run left the rows and their order
alone, and changed three things. Five section-1 titles and all four tail
titles were shortened past what step 9 describes, which only says to strip
the Conventional Commits prefix, so they are the real titles now. The
rank-instability figures were wrong and are measured: DST-1717 came back
3rd of 5 keys and 6th of 8. And 5761 carries a `reviews.totalCount` of 47
against 3 live threads, not the 30 a `last:30` window reports back.

Running the skill again turned up three more.

The tail was not in rank order. Its queries already carry
`ORDER BY Rank ASC`, but DST-1529 is tenth of the fourteen tickets in
review and DST-1759 fourteenth, so the example ending with 1529 had been
sorted by something else. Step 8 now says that removing rows from a ranked
list does not reorder the survivors, which is what goes wrong if you
collect the leftovers into a set and render the set.

Two shell traps join the `while read` rule, because all three are a
metacharacter doing something other than what it looks like. `set -- $spec`
inside a loop fails the same way `for r in $REPOS` does, so fields get read
with `while IFS=' ' read -r`. And `.name?//""` does not compile at all,
because jq parses `?//` as the destructuring-alternative operator. That one
is quiet inside a loop: the rows still print and only the cells fed by the
broken filter are wrong, which is how it produced a full digest with every
preview-comment cell inverted.

Checking that guard corrected a claim rather than adding one.
`workflowName` is present on every `CheckRun` and merely empty on some, so
the guard exists for `name`, which a `StatusContext` genuinely lacks. Both
key sets are written down now, which also explains why a failing deploy is
read off `state`: a `StatusContext` has no `conclusion`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature or component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants