Minor Changes
-
1dfe461: feat(DST-1489): add non-form interactive list component
Adds
<ListView>and<ListView.Item>, a non-form list built on React Aria'sGridList/GridListItemfor rows the user operates in place — dismissing with a<Button>, opening a per-row<ActionMenu>, or following a link — without leaving the page. Unlike<SelectList>/<Select>/<ListBox>,<ListView>has no selection, no hidden input, and never becomes a submitted form value.A row is a named-area grid —
'label actions' 'description actions'— so every region names the cell it wants instead of counting columns and rows. Row content is authored with Marigold's slot-aware components (<TextValue>,<Description>,<Title>), and each of them claims its cell through slot context, so text still lands correctly when it's wrapped in a fragment, behindmemo(), or inside a consumer's own component. A bare string child is wrapped as the row's text value; to emphasise part of a line, nest<Text as="span">inside<TextValue>/<Description>. A row carries one<Description>— it takes inline markup, so several facts go on one line.Trailing controls claim their cell through Marigold's
ButtonContextand inherit the row'sghostcascade, the same mechanism<Panel.Header>and<SelectList.Option>use. A row with more than one control must wrap them in a<ButtonGroup>: each control reads the same context and would otherwise be placed in the same cell. A dev-only warning catches that at authoring time. The trailing cell takes Button-family controls only; trailing badges, pills or timestamps have no slot yet.<ListView.Item>forces any nested<Title>to render as a<span>instead of a real heading, so a list of rows never emits one document heading per row.The list is flat (divider lines only) and draws no surface of its own, the way
<Table>does — a framed list nested in a container that already draws a surface is a ring inside a ring. A standalone framed list is<Card><ListView /></Card>.This closes the gap between
<List>(presentational, no roles),<ListBox>(selection only — forbids focusable controls inside a row),<SelectList>/<Select>(form fields),<Table>(tabular), and<Menu>(commands that close on activation). The motivating consumer is the Popover notifications panel (DST-1485), and the docs cover a second must-support scenario: a resource list with a per-row action menu.v1 is intentionally minimal: no selection, no bulk-action bar, no async loading, no drag-and-drop, and no leading media. Leading icons and images need slot-aware icon/image components so placement doesn't depend on authoring order, which is tracked separately along with the rest of the follow-ups (selection ships together with a bulk-action bar, building on DST-1487).
Documentation
New
/components/collection/listviewdocs page: anatomy, appearance, a "which list?" decision guide, the must-support demos, accessibility notes (including the<Title>-as-span caveat), and cross-links to<List>,<ListBox>,<SelectList>,<Table>, and<Menu>. -
1dfe461: feat(DST-1489):
ListViewis flat, and aligns likeTableandAccordioninside a bled container.What changed:
- The list ships one variant.
defaultis the flat list (divider lines only) and there is no framed variant — the oldvariant="plain"is gone, since it is whatdefaultnow does. - Row padding is sourced from
--bleed-px(via--listview-item-px) on the base rather than per variant. That is the custom property a bledPanel.Content/Panel.CollapsibleContent/Drawer.Contentpublishes; it falls back to the standalone row padding (--spacing-stretch-regular-x) when unset.
Why:
A framed list was what a consumer got without asking, and it collides with anything that draws its own surface — a
Card, aPanel, a docs preview — as a ring inside a ring 8px apart. EveryListViewin the repo lives inside such a container, so the frame had no consumer of its own;Tableanswers the same shape by drawing no frame at all and leaving the surface to its container. A standalone framed list is<Card><ListView /></Card>.Binding the bleed to a variant had the same problem from the other side:
<Table>and<Accordion>adopt--bleed-pxunconditionally, so both align inside a bledPanelwith no opt-in, while<ListView>needed two knobs to do the same thing. - The list ships one variant.
-
ce2720e: feat(Table): expandable rows for grouped records
<Table>can now nest rows, so records that belong together (a settlement run
and its individual clearings, an order and its line items) share one table
instead of being faked with a button and a menu. SettreeColumnto theidof
the column that carries the hierarchy and nest<Table.Row>inside
<Table.Row>; use the new<Table.ExpandableRows>when the nested rows come from
data, and mark a rowexpandablewhen they are fetched only once it is opened.
Nested rows stay real table rows, so their values line up under the same headers
and each one carries its own actions.The tree column renders the expand control itself, reusing the caret and morph
animation from<Accordion>. The caret starts where the column's values start:
its hit target is wider than the caret and hangs back into the cell's edge
padding (--tree-caret-inset), so the ghost button's own whitespace is the
gutter rather than something added to it. Behind it every row reserves that
gutter, so a group row and a childless row at the same level start their value at
the same x whether or not there is a control to show. Levels below the root
indent by one gutter (--tree-indent), so a nested row's caret starts where its
parent's value starts, and the column's own header label takes the same gutter,
so it sits above the values it heads. Containment is carried by the children
rather than the parent: nested rows share a filled band across the full row, so
what belongs to a group is legible in the leading drag and selection columns
too, which don't indent. Expansion is collapsed by default and can be controlled
withexpandedKeys/defaultExpandedKeys/onExpandedChange.Drop indicators are level-aware: the insertion line starts at the x of the level
the row would land at, so a reorder inside an expanded group reads as landing in
that group, while a root-level drop still spans the row. Moving a row from one
level to another is not supported.The 32px control keeps that hit target without setting row height: a negative
block margin cancels the surplus over the line box, so a group row is exactly as
tall as a leaf row and thesizevariant's cell padding stays the only thing
that decides — 33 / 41 / 53px forcompact/default/spacious.New theme keys
treeIndentandexpandButton. The band isforeground/8,
which holds ~1.17:1 whether the rows sit onsurface, onbackgroundor inside
amutedtable, and yields to the hover wash so hovering still reads.admin
andmasterrows keep their access colour at every level — who may see a row
outranks where it sits in the tree.Inline editing composes with it:
Table.EditableCellrenders the tree column's
gutter too, so a row keeps its expand control even when that cell is the editable
one. When the edited value lives outside the row items, passdependenciesto
Table.ExpandableRowsas well as toTable.Body. Selection checkboxes, drag
handles and thecompact/default/spaciouspaddings all keep working
alongside the tree column, and the tree column still takes its edge padding from
aPanel's bleed contract.Selection works on grouped rows but does not cascade: each row selects on its
own, and a group row shows no partial state while only some of its children are
selected. Cascading selection is not part of this release.Potentially breaking for opted-in tables: setting
treeColumnchanges the
table's accessibility role fromgridtotreegrid, which is what makes screen
readers announce expanded state and nesting level. Tests asserting
getByRole('grid')on such a table need to becomegetByRole('treegrid').
Tables that don't settreeColumnare unaffected.Collapsed child rows are deliberately not rendered, so searching or deep-linking
to a nested row means expanding its ancestors first. Moving focus to a specific
row is not supported yet, which is tracked in DST-1713. Sorting doesn't reorder
levels for you either, so sort each level in your own data, and there is no
expand all control, so setexpandedKeysto every group's key when you need one.
Patch Changes
-
17f9158: fix(DST-1675): treat
collapsedas axis-less when resolving inset paddingresolveInsetAxesappended an axis suffix to every non-numeric inset token, includingcollapsed— the one token where an axis split is meaningless, since it means "no spacing" on both axes. So<Panel p="collapsed">resolved tovar(--spacing-collapsed-x)rather thanvar(--spacing-collapsed).That variable went undeclared until it was added to
@marigold/theme-rui, so the declaration was invalid at computed-value time andpaddingfell back to its initial value of0— the right answer for the wrong reason. The cost was that every theme implementing this API had to ship three variables that all mean0(--spacing-collapsed,-x,-y) to make one documented prop value work, with nothing to tell it so.collapsedis now recognised as axis-less and passed through unsuffixed, so a theme declares--spacing-collapsedonly. The same guard is applied to the deliberately-divergent copy of the axis logic inSelectList, which had the identical bug on itspprop. A newisAxislessTokenpredicate is exported from@marigold/systemso the concept has one home instead of being inlined at both call sites.isScaleis unchanged:collapsedstays non-numeric there, becausecreateWidthVar,createHeightVarandcreateSpacingVardepend on that classification.Theme:
--spacing-collapsed-xand--spacing-collapsed-yare removed from@marigold/theme-rui. They shipped intheme-rui6.0.0 stable but existed only to satisfy the suffix that is no longer generated — nothing else referenced them, and the documented token has always beencollapsed. -
148ef97: fix(DST-1740): give
CheckboxandSwitchlabels a containing block for their hidden inputReact Aria renders the real
<input>inside the control's<label>wrapped inVisuallyHidden, which isposition: absolutewith no offsets. The label wasposition: static, so the input anchored to whatever positioned ancestor the consumer happened to have. In a list inside a dialog that is the dialog itself, a dozen static elements up.An absolutely positioned box laid out against an ancestor outside a scroll container is not part of that container's scrolled content, so the input stopped travelling with the row it belongs to. Clicking a row far down the list focused an input the browser believed was over a thousand pixels away, and it scrolled the dialog to reveal it. In the reported case the dialog also set
overflow: hidden, so there was no scrollbar to get back and it ended up blank.Both labels are now
relative. React Aria's own docs name this as a precondition ofVisuallyHidden("it must have aposition: relativeorposition: absoluteancestor"), citing stray scrollbars — the focus jump is the sharper consequence, which is probably why it went unnoticed.Radioalready positioned its label and is unchanged.Switchlooked like it did too, but itsrelativesat on a wrapper around the track, a sibling of the input rather than an ancestor, so it never reached it. That wrapper is gone and the track is now a direct child of the label, which carries therelative.Checkbox.GrouprendersCheckboxchildren and needs no change of its own.Consumers who worked around this by setting
position: relativeon their own scroll containers can drop it, though leaving it in place is harmless. -
1fd3f85: fix(DST-1729): take
useLocalefromreact-aria-components, not@react-aria/i18n.Calendar's preset resolution read the locale through the@react-aria/i18nshell. RAC pins
itsreact-ariainternals exactly while the shells keep caret ranges, so a consumer's lockfile
can legitimately resolve tworeact-ariacopies — two module instances, two distinct
I18nContexts. The hook then reads a context that<I18nProvider>never wrote, and presets
silently fall back to the default locale. Nothing errors, and our own dedupe hides it locally,
so it only ever appears in a consumer's app.Same failure class as DSTSUP-261 and DST-1505. A check now fails CI if
packages/components
takesI18nProvider,useLocaleorisRTLfrom the shell again. -
4a18a38: fix(Tray): don't dismiss the tray on gestures that start in its content
A vertical swipe inside
<Tray.Content>was read as drag-to-dismiss, so
scrolling a list or swiping over a calendar closed the tray instead of
scrolling it, including the trays thatDatePickerandDateRangePicker
open on mobile. The gesture now only starts on the tray's chrome (drag
handle, title, actions), and motion no longer setstouch-action: pan-xon
the tray, so content can pan vertically on touch. As a result, text inside
the tray is selectable again — suppressing selection and touch panning is
now scoped to the chrome. No opt-in required; nothing to change in consumer
code. -
e41e633: style(DST-1711): give the undo toast's action button a visible weight.
addUndoToastrendered its Undo button asvariant="ghost", which has no fill and no border until it is hovered. It is now the defaultsecondarybutton, so the way back out of a deletion is visible at a glance.Review feedback on the Destructive Actions pattern found the button easy to overlook: the toast arrives far from the control that triggered it and leaves again after five seconds, which is the worst case for an affordance that only appears on hover. It is deliberately not a
destructivebutton — undo is the safe action, and red beside a completed deletion reads as a second delete. -
Updated dependencies [17f9158]
- @marigold/system@18.1.0