fix(DST-1717): expose dependencies on the components that own their collection - #5740
fix(DST-1717): expose dependencies on the components that own their collection#5740sebald wants to merge 3 commits into
dependencies on the components that own their collection#5740Conversation
🦋 Changeset detectedLatest commit: 2f617c9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Select, ComboBox, TagGroup, TagField and Autocomplete have nothing to do with expandable rows; they now ship in #5740 (DST-1717).
|
Accessibility tests executed. Download the report here. |
Coverage Report for Marigold Code Coverage
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
… 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.
3b10ccd to
70146bc
Compare
|
Accessibility tests executed. Download the report here. |
aromko
left a comment
There was a problem hiding this comment.
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-dependencies → main |
| 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 (popoverListBoxand tray /MobileAutocomplete/TagFieldDropdown), each gets aWithDependenciesstory plus a.test(), and the changeset isminoron@marigold/components. - The ticket calls out the second forward per component as the thing to check in review — that is exactly what the four
WithDependenciesMobiletests cover, and the description documents a mutation check (droppingdependenciesfrom Select's trayListBoxfails only the mobile test).
Code Review Checklist
TypeScript Standards
- No new
anytypes —dependenciesresolves to RAC'sReadonlyArray<any>throughListBoxProps['dependencies'], which is the ticket's stated typing - Uses
import type—import { ListBox, type ListBoxProps }in all five call sites - Proper interface definitions —
AutocompleteProps<T extends object = object>gains the item render function and mirrorsTagFieldProps -
ComboBoxPropskeeps its pre-existing(item: any)render function (see Suggestions)
Component Patterns
-
disabled/loading/errornaming untouched; noisDisabledleakage - No
classNameorstyleexposed —RemovedPropsstill omits both everywhere, andAutocompleteadditionally omitschildren -
useClassNamesuntouched; no hardcoded styles added -
dependenciesis destructured out of props in all five, so it never lands on a DOM node via...rest -
AutocompleteBasebecomes a generic function declaration but keepsrefin props and theObject.assigncompound shape, matchingTagFieldBase
React Best Practices
- Functional components only
- No conditional hooks; the
isSmallScreenbranch stays below the hook calls - No new re-render or memoisation concerns — this is a prop forward;
dependenciesis the deliberate cache-invalidation escape hatch, not an optimisation
Testing Standards
- Vitest /
storybook/test, no Jest APIs -
userEvent, neverfireEvent - Accessible queries throughout (
getByRole,getByLabelText,findByRole) — nogetByTestId - 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
userEventused 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 byissue_comment(so runs are recorded againstmain, not the head branch), and both runs for this PR on 2026-08-14 05:19 came backskipped, not successful.Worth weighing against the diff, though: the five component files change no
class,style,cn()oruseClassNamesline — 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-chromaticon this PR.
Warnings (Should Fix)
None.
Suggestions (Nice to Have)
-
packages/components/src/ComboBox/ComboBox.tsx:93— 💡dependenciesexists purely for theitems+ render-function pattern, but onComboBoxthat render function is stillchildren?: ReactNode | ((item: any) => ReactNode)(line 86), so consumers who reach for this new prop get an untypeditem. This PR already worked out the fix for exactly this shape onAutocomplete: add'children'toRemovedProps, make the interfaceComboBoxProps<T extends object = object>, and turnComboBoxBaseinto a generic function soTis inferred fromitems.ComboBoxPropsalso carries a row of pre-existingRAC.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-scopeuserEventimported fromstorybook/test. CLAUDE.md is explicit: "Destructurecanvas,userEvent,args,stepfrom the test context — neverwithin(canvasElement), never a module-scopeuserEvent." The PR is internally split on it:TagGroup.stories.tsx'sWithDependencies.testcorrectly destructuresuserEventfrom the context, while the new tests inSelect,ComboBox,TagFieldandAutocompletedo 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,TagFieldandAutocompleteeach got the same six-line JSDoc explaining whendependenciesis needed, and the docs pages render those descriptions via<AutoTypeTable>.TagGrouppicks the prop up throughPick<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 onTagGroupPropswith 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 thatTagList. Worth a small correction to the framing though: only the collapse-branch forward is inert. The non-collapseTagList(TagGroup.tsx:317) is live, andWithDependenciesproves it. The comment parked at the end ofTagGroup.stories.tsxsays 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 inpackages/components/srcareCalendar/YearListBox,Calendar/MonthListBox(fixed internal data, no outside state) andBreadcrumbs(maps children directly, no dynamic collection), so none of them has the same gap. - The
Autocompletechildrenretype is not a practical break — the render-props form it replaces was never wired to the RACComboBoxanyway;childrengoes to theListBox/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
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.
|
Accessibility tests executed. Download the report here. |
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.
… 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.
… 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`.
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,TagFieldandAutocompleteown 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
dependenciesand forward it to every collection they render — the popover copy and the tray/dropdown copy are separate collections and each needs it.Closes DST-1717
Split out of #5727
This rode along on the
Tableexpandable-rows branch (DST-1702), whereTable.Bodygetsdependenciesfrom 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
Autocompleteshippeddependenciesthat you could not actually use. Itschildreninherited RAC'sChildrenOrFunction<ComboBoxRenderProps>— render props, not an item function — soitemsplus a render function only type-checked through a cast, and that pattern is the entire reasondependenciesexists.AutocompletePropsnow takes an optional item type and declares the render function onchildren, the same shapeTagFieldPropsalready uses:AutocompleteBasebecomes a generic function soTis inferred fromitems, mirroringTagFieldBase. Type-level only — no runtime change, and existing uses (static<Autocomplete.Option>children) are untouched. It deliberately does not copyComboBox's(item: any), which is onlyanybecause that interface doesn't omitchildrenbefore redeclaring it.Screenshots / Preview
No visual change —
dependenciesalters when an item re-renders, not how it looks. AllWithDependenciesstories are snapshot-disabled.Test Instructions
Each component gets a
WithDependenciesstory plus a.test()that proves the stale render goes away: render options fromitemsplus a function that reads ashiftvalue 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,TagFieldandAutocompleteeach add aWithDependenciesMobilestory (viewport: smallScreen) whose test opens the tray and asserts the same thing.Verified load-bearing by mutation rather than trusted green. Dropping
dependenciesfrom Select's trayListBox: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:
getByLabelTextthen finds two nodes;pnpm sb→ each component's With Dependencies / With Dependencies Mobile story → press Switch shift and re-open the list; the option label should follow.pnpm typecheck:only;pnpm test:sb --run Select ComboBox TagGroup TagField Autocomplete(125 passing).Worth a reviewer's eye
TagGroupis the one component that gets no second test, deliberately. ItscollapseAtbranch does render a secondTagList, butcanCollapseiscollapseAt !== 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. Itsdependenciesforward is therefore inert today; it is kept, with a comment, so the two branches stay in step if collapse ever learns to handleitems. Happy to drop it instead if you'd rather not carry unreachable wiring.