@scalewing/react 1.35.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +7 -3
  3. package/dist/components/ActionMenu.js +2 -1
  4. package/dist/components/Box.js +5 -2
  5. package/dist/components/Button.js +3 -2
  6. package/dist/components/DateField.js +2 -1
  7. package/dist/components/DenominationGrid.d.ts +1 -1
  8. package/dist/components/DenominationGrid.js +2 -0
  9. package/dist/components/FilterChips.js +3 -1
  10. package/dist/components/MultiSelect.js +2 -1
  11. package/dist/components/RadioGroup.js +3 -1
  12. package/dist/components/SectionNav.js +2 -1
  13. package/dist/components/SegmentedControl.js +5 -1
  14. package/dist/components/Select.js +2 -1
  15. package/dist/components/Stepper.d.ts +5 -0
  16. package/dist/components/Stepper.js +4 -2
  17. package/dist/components/Steps.d.ts +11 -2
  18. package/dist/components/Steps.js +5 -3
  19. package/dist/components/Switch.js +2 -1
  20. package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
  21. package/dist/components/checkbox/CheckboxControl.js +3 -2
  22. package/dist/components/date-field/CalendarHeader.js +2 -1
  23. package/dist/components/denomination-grid/DenominationStrip.js +3 -1
  24. package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
  25. package/dist/components/denomination-grid/EntryCell.js +2 -2
  26. package/dist/components/denomination-grid/StripRow.d.ts +8 -2
  27. package/dist/components/denomination-grid/StripRow.js +14 -5
  28. package/dist/components/denomination-grid/StripTotal.d.ts +11 -0
  29. package/dist/components/denomination-grid/StripTotal.js +12 -0
  30. package/dist/components/denomination-grid/StripValueRow.d.ts +25 -0
  31. package/dist/components/denomination-grid/StripValueRow.js +27 -0
  32. package/dist/components/denomination-grid/types.d.ts +19 -0
  33. package/dist/components/denomination-grid/validate-value-row.d.ts +6 -0
  34. package/dist/components/denomination-grid/validate-value-row.js +30 -0
  35. package/dist/components/field/SearchFrame.js +2 -1
  36. package/dist/components/radio-group/RadioGroupOptionLabel.d.ts +3 -1
  37. package/dist/components/radio-group/RadioGroupOptionLabel.js +3 -2
  38. package/dist/components/steps/Step.d.ts +4 -2
  39. package/dist/components/steps/Step.js +15 -7
  40. package/dist/components/steps/steps-context.d.ts +5 -3
  41. package/dist/components/table/TableCell.js +2 -1
  42. package/dist/css/css-button.js +17 -5
  43. package/dist/css/css-denomination-grid.js +4 -0
  44. package/dist/css/css-denomination-value-row.d.ts +3 -0
  45. package/dist/css/css-denomination-value-row.js +54 -0
  46. package/dist/css/css-steps.d.ts +3 -1
  47. package/dist/css/css-steps.js +18 -3
  48. package/dist/index.d.ts +1 -1
  49. package/dist/styles.css +58 -5
  50. package/dist/tab-stop.d.ts +22 -0
  51. package/dist/tab-stop.js +23 -0
  52. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.37.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a9e2447: `DenominationGrid` row `valueRow` (opt-in, strip layout): a line right under a row that gives each column's value, such as the money its count adds up to. It is its own table row on the subtle fill, named in the label column (`label`), each consumer-formatted value centred under its count in tabular figures (`null` or `''` blank), with the row's `total` moved onto it; each typed count is described by the value under it, which is no tab stop. `Stepper` gains `aria-describedby` (ids read before its error). The grid still does no arithmetic or currency formatting. New classes `sw-denomination-value-row` and `sw-denomination-value`; no new tokens, no new dependency. Request: `docs/requests/teisoro-denomination-value-row.md`.
8
+
9
+ ## 1.36.0
10
+
11
+ ### Minor Changes
12
+
13
+ - bc5d37c: A `Button` with `aria-disabled="true"` now looks disabled (`docs/requests/teisoro-button-aria-disabled.md`, Teisoro F-011 task 2210): the faded look, or with `disabledAppearance="neutral"` the neutral grey, and `GrayText` in forced colors, while it stays focusable and its `onPress` still runs, for a submit that, pressed while off, takes the person to what is missing. No new prop, token, class or dependency.
14
+ - 3d431f7: `Steps` `open` shows every step's fields at once (`docs/requests/teisoro-steps-open.md`, Teisoro F-011 task 2210): each step is a region named by its title, its marker still following its status (a number to fill, a check when done, "!" in error, a grey ring when greyed), every title at the open size, no step `aria-current` and no focus moved when a status changes. Off by default; a list without `open` is unchanged. New generated classes `sw-steps-open` and `sw-steps-region`. No new tokens, no new dependency.
15
+
16
+ ### Patch Changes
17
+
18
+ - 743a7a0: Every press control and link now renders an explicit `tabindex="0"` (Teisoro F-011 task 2205), so Safari users reach it with Tab: Safari's default leaves a `<button>`, a link, a checkbox and a radio out of the Tab order unless it carries a `tabindex`, so Tab from a field went past them to the address bar. Covered: `Button` (with `CalendarButton` and `InfoTip`), `Box as="a"` with an `href`, `SectionNav` links, the `Select`, `MultiSelect` and `ActionMenu` triggers, `SegmentedControl` items, `DateField`'s calendar button and month steps, a search `Field`'s clear button, `Table` sort and `TableCell onPress` buttons, `Checkbox` (and `MultiSelect`'s rows), `Switch`, and the checked (else first enabled) radio of a `RadioGroup` or `FilterChips`. A consumer's `tabIndex` wins; a `Stepper`'s − and + and roving items keep their `-1`. No new prop, class or token, no new dependency.
19
+
3
20
  ## 1.35.1
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -57,7 +57,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
57
57
 
58
58
  `AppHeader` is the page's top chrome, sticky by default (`sticky={false}` leaves it in flow). Its default `appearance="glass"` is a rounded frosted card. `appearance="bar"` (`AppHeaderAppearance`, `appHeaderAppearances`) is a full-width band of the solid surface with the border hairline under it: no blur, no rounding, no side or top border, the same under Reduce Transparency, and the hairline kept in forced colors. Set it against the page's edges (no side gutter round it) and put the gutter inside it. An unknown `appearance` throws a `RangeError`.
59
59
 
60
- `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
60
+ `Box hideBelow="md"` hides a region under 48rem and `hideFrom="md"` hides it at 48rem and wider, so a product can swap a wide layout for a narrow one without its own stylesheet. They also take `lg` (64rem), for content that fits only from a laptop up, such as a row of labelled destinations that a tablet shows as glyphs (name each glyph-only control, for example with `Tooltip relationship="label"`); the layout props keep `md`. `breakpointQuery('below', 'lg')` returns the query a hide class uses (`not all and (min-width: 64rem)`), for an app whose script must follow the same width. An unknown breakpoint throws a `RangeError`. Hidden regions leave the accessibility tree; do not use this to protect data. `Box as="a"` is a layout link. Use `Button` for press actions. `Box as="a" link="quiet"` (opt-in; `link="accent"`, the default, is the canvas link) is a title that opens something, such as a table row's title: it keeps the surrounding text's colour at weight 600 (`sw-link-quiet`) and is underlined on hover and keyboard focus. Its weight is the non-colour cue WCAG 1.4.1 asks for (axe's `link-in-text-block` passes it on colour, the same colour and background as its text, or on its weight; the weight is what keeps it visibly a link), so use it in text lighter than 600. In text already at 600 the quiet look has no cue: an accent link there needs at least 3:1 contrast against the text. `link` on a box that is not `as="a"` throws a `TypeError`, an unknown value a `RangeError`. `Field` associates a native `<input>` or `<select>` with a label and token gap; native text controls inherit the generated document canvas. `Steps` is a one-page vertical stepper: an `ol` of `Step` panels in order, numbered by their place, a line joining each marker to the next (in the success color below a done step). A `Step` takes `status` (`'upcoming' | 'current' | 'done' | 'error' | 'skipped'`) and `title` (a heading at the list's `headingLevel`, 2 by default, or the step's own). The current step is open with its `children` and has `aria-current="step"`; an error step stays open with a "!" marker. A done step folds to one line: a check, the title, its `summary` (truncated) and, with `onEdit` and `editLabel`, a ghost small `Button` at the end named by its label and the title ("Edit The site"). A step ahead is greyed with its number in a ring; a skipped step is greyed with its `skippedReason` after the title (pass your own separator, such as "· not needed"). A folded step's heading ends in its status word for assistive technology (visually hidden; English by default, `labels` on `Steps` takes your words). When a step becomes current from a folded state (upcoming, done or skipped), focus moves to its heading, a programmatic focus target; if several do in one update, the first in the list takes it. A first render, a move between current and error, and a step that turns to error leave focus alone: after a refused submit, move focus to the first invalid field yourself, as with `Field`. An error step has no `aria-current`; its heading's status word says it needs attention. Scalewing holds no order rules: the consumer sets each step's status. `Steps open` (off by default) keeps every step open at once, for a form whose sections are all filled on one page: each step shows its `children` whatever its status, inside a `section` named by its title (a region, class `sw-steps-region`), every title at the open size; the marker still follows the status (a number to fill, a check when done, "!" in error, a grey ring and muted title when greyed, such as after a blocking step), no step is `aria-current`, and no step takes focus when its status changes. Pass the `Step`s as direct children (a Fragment around several would share one number). A `Step` outside `Steps`, a Fragment child, a blank title or reason, an unknown status or heading level, `onEdit` without `editLabel`, or a blank status word throws a `RangeError`.
61
61
 
62
62
  ```tsx
63
63
  <Steps aria-label="Heron survey">
@@ -89,6 +89,8 @@ Import the CSS once at the application entry. Do not copy it into your source tr
89
89
  </Field>
90
90
  ```
91
91
 
92
+ Every press control and link takes an explicit `tabindex="0"`: `Button` (and the controls built on it, `CalendarButton` and `InfoTip`), `Box as="a"` with an `href`, `SectionNav` links, the `Select`, `MultiSelect` and `ActionMenu` triggers, `SegmentedControl` items (every item, as outside Safari: its arrow keys change the value, not the focus, so it is not a roving group), `DateField`'s calendar button and month steps, a search `Field`'s clear button, a `Table`'s sort and `TableCell onPress` buttons, `Checkbox`, `Switch`, and the checked radio of a `RadioGroup` or `FilterChips` (the first enabled one while none is; the others keep no `tabindex`, so each browser's arrow-key group rule stands). Safari's default (its "Press Tab to highlight each item" setting off) leaves a button, a link, a checkbox and a radio out of the Tab order unless it carries one, so a Safari user would Tab past them; the explicit `0` gives them the order every other browser already does. A `tabIndex` you pass wins, and controls that are deliberately no tab stop keep their `-1`: a `Stepper`'s − and +, and a roving item (a `Tabs` tab, a menu item, a calendar day) other than the current one. A `<summary>` is already in Safari's order and takes none.
93
+
92
94
  An element a script focuses, such as a notice or a card announced after a save, takes the same accent focus ring as the canvas's links and native controls, 2 px past its edge (`--sw-focus-ring-width` and `--sw-focus-ring-offset`), instead of the browser's outline. Give it `tabIndex={-1}`, so it takes focus from a script but is no tab stop, and put the focus on the whole card rather than its text, so the ring goes round it clear of the words; a `Box` round the card with the card's `radius` makes the ring follow its corners. As with the browser's own outline, the ring shows only when focus is visible (`:focus-visible`): after a keyboard press, not after a mouse press. The rule has zero specificity, so a component's own ring always wins.
93
95
 
94
96
  ```tsx
@@ -111,7 +113,7 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
111
113
  </Field>
112
114
  ```
113
115
 
114
- `Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
116
+ `Stepper` is a whole-number count changed with − and + or typed. The count is a native text input with `role="spinbutton"` and `inputmode="numeric"`, named by `label` (visible above it, or `labelVisuallyHidden` where table headers say it) and the one tab stop: ArrowUp and ArrowDown step it by `step` (default 1), Home goes to `min` and End to a finite `max` (a range below zero takes `inputmode="text"`, since a phone's numeric pad has no minus key). The − and + buttons are pointer targets outside the tab order, named by `decrementLabel` and `incrementLabel` (your copy), and a press on one leaves focus where it was; the count and both buttons sit in a `group` named by the label. − is disabled at `min` (default 0) and + at `max` (no upper bound when left out). `value` is a count or `null` for an empty box, which the buttons treat as `min` and `placeholder` can fill. Typed text is refused unless it is digits (with a leading minus when `min` is below zero), so `onChange` only receives a whole number or `null`; a typed count is reported as typed even outside the bounds, so say why in `error` (a polite message, `aria-invalid` and the danger border, as `Field`'s), and the buttons then move it back inside them. `aria-describedby` takes ids of elements that describe the count, such as the value it adds up to, read before an `error`'s message. `changed` takes `Field`'s changed look; `readOnly` keeps the count focusable and hides the buttons in their own room; `disabled` stops all three. `limitAppearance="plain"` (default `"faded"`) keeps − at `min` and + at `max` in their usual look instead of faded, for a count sheet where a faded − reads as broken; they are still disabled, a disabled or read-only Stepper is unchanged, forced colors still grey them, and an unknown value throws a `RangeError`. The count is a few digits wide and keeps that width (`width="fixed"`), or fills its container (`width="full"`, for a table cell). Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
115
117
 
116
118
  ```tsx
117
119
  <Stepper
@@ -219,6 +221,8 @@ A `Button`'s own children are laid out in a row one token gap apart (spacing ste
219
221
 
220
222
  On the web, `variant="neutral"` is a quiet grey fill (the muted color at 16 %, the fill of an open menu row) with its words in the text color and no border: an action beside the primary that should not draw the eye, such as a sheet's Cancel. It is web only (`webButtonVariants`; the shared `buttonVariants` in `@scalewing/tokens` and React Native do not have it). `disabledAppearance="neutral"` (default `"faded"`) draws a disabled button on that grey with its words in the muted color at 48 % (about #AEAEAE on the grey) and full opacity, whatever its variant, instead of fading its own fill (a disabled accent reads as a paler accent); a `ghost` button, which has no fill, still fades. The button is still a disabled `button`; forced colors draw it as any disabled button. An unknown `disabledAppearance` throws a `RangeError`.
221
223
 
224
+ A `Button` with `aria-disabled="true"` (not `disabled`) looks disabled, faded or, with `disabledAppearance="neutral"`, on the neutral grey, with `GrayText` in forced colors, yet stays a focusable, pressable `button`: its `onPress` still runs, so a submit pressed while off can take the person to what is missing. Its look alone does not say what is missing: a press while off should show the message of the first thing to fix. Use `disabled` when a press should do nothing.
225
+
222
226
  A `Button` that toggles something passes `aria-pressed`. The pressed button gets a 2 px accent ring outside its fill, past a 2 px gap in `--sw-color-background`, so the ring meets only the page and keeps the accent's contrast on the canvas (4.5:1 or more in every palette, at least the 3:1 a state indicator needs) whatever its `variant` or fill. A focused pressed button moves its focus outline out past the ring. In forced colors, which drop shadows, the pressed button keeps the forced button colors, so its label and any glyph or badge inside it stay readable, and draws the same ring and its own border in the system highlight (`Highlight`); nothing opts out of forced colors. An unpressed button is drawn at full strength, so its label keeps its contrast. To make the choice stand out further, give the pressed button `primary` and the others `secondary`.
223
227
 
224
228
  `ButtonGroup` is the action row of a form or dialog (`role="group"`, named by `aria-label`): one line from `md` up, aligned by `justify`, and full-width stacked buttons below it. `attached` makes it one joined control instead, such as Previous, Today and Next: its direct `Button` children sit side by side at one height with no gap, their borders overlapping into single dividers (a borderless variant draws its divider in its own text color at low strength), with square inner corners and `sm`-radius outer ones, mirrored right to left. It keeps its row on a phone and sizes to its buttons, so `justify` does not apply. The group is its own stacking context: a pressed toggle's ring and the focused button's usual outer ring paint over the neighbours. Give every button the same variant and size, such as `secondary`; a wrapper around a button (a tooltip's span) is not joined.
@@ -560,7 +564,7 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
560
564
  </Grid>
561
565
  ```
562
566
 
563
- `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that shows under the label on a phone. Pass `totalLabel` (for example "Total") to name the total column: it renders a `th scope="col"` shown over the totals from `md` up and visually hidden below it, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). `labelWidth` (`xs` to `xl`, the `Table` column sizes) lines a strip's columns up with every other strip of the same columns in a container of the same width, such as the cards of a feed: the row labels take that width and the count columns share the rest of the region equally, so no content moves a column. `labelAppearance="plain"` (default `"tone"`) sets every row label in the text color at label weight with no tone stripe, in both layouts, for a grid whose rows need no color to tell apart; a row's `tone` still colors its total, and an unknown value throws. Three more grid-level options match a framed count sheet, each off by default: `frame="table"` (default `"none"`) draws a strip as a framed table, its scroll region a surface inside a border hairline at the `sm` radius, the heading row on the subtle fill, a rule between columns, the counts at label size centred under their heads and the cells padded a space-3 step across (the tiles layout has no frame and ignores it); `totalEmphasis="strong"` (default `"default"`) sets the row totals in bold label type, a neutral row's in the text color and a toned row's in its tone, under the label on a phone too; and `cellToneAppearance="pill"` (default `"text"`) sets a `cellTones` count of a strip's read-only row on a rounded tint of its tone in the tone's ink (the `Notice` fill and words, 4.5:1 on the tint except where a palette's own `danger` is already under 4.5:1, harvest and mocha dark), outlined in forced colors; entry counts and tiles keep their tone border. With `zeroLabel=""` a toned zero or empty count shows nothing in either appearance, so tone only counts that are there. An unknown value of any of them throws a `RangeError`. `zeroLabel` (default `"—"`) is what a zero or empty read-only count shows; an empty string leaves those counts blank, so only the counts that are there stand out (it suits the strip, where the column header names each blank cell, better than tiles, where a blank tile is only its column's name), and a `zeroLabel` that is not a string throws. On a phone the total cell stays in the table, visually hidden, so a screen reader still reads each total in its row; the copy under the label is `aria-hidden`. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start and the same edge shade as `Table` while counts are scrolled out of view (the start shade is cast from the pinned labels' edge, since they cover the region's own); the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. Each tiles row shows its label line (icon, label and `total`) when the grid has several rows, or when a lone row has an icon or a `total`; a lone plain row is named by the grid's `label` alone. Every row is a region named by its `label`, except a lone plain row whose `label` is the grid's own: it has no name of its own, so assistive technology hears the grid's group once rather than a region repeating it. `rowRole="group"` (default `"region"`) keeps every named row's name but makes it a `group` instead of a landmark region, for a page with many grids (one per order card, say) whose rows would otherwise fill the landmark list; the strip ignores it, since its rows are table rows, and an unknown `rowRole` throws a `RangeError` in either layout. A row's icon and its label words stay on one line in both layouts (`sw-denomination-label-line`): a long label wraps its words beside the icon, never under it, and on a phone the strip's total goes on its own line under them. A row's `cellTones` (one Badge tone or `null` per column) tones a single count in place: the count is set in the tone over its signed or zero look, and in the tiles layout its tile's border takes the tone too, so a short bill turns red without leaving its row; `neutral` or `null` leaves a count as it is, a row's `tone` still colors only its label, and a `cellTones` that is not one known tone or `null` per column throws. The tone never says why on its own; put that in words beside the grid. Every negative count, signed or not and in either layout, is written with the typographic minus ("−1"), never a hyphen. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
567
+ `DenominationGrid` shows integer counts per unit across a fixed set of columns. The `strip` layout is a captioned table: each row has a toned label with an icon slot, muted zero cells, optional signed deltas toned by sign, and an optional consumer-formatted `total` that shows under the label on a phone. Pass `totalLabel` (for example "Total") to name the total column: it renders a `th scope="col"` shown over the totals from `md` up and visually hidden below it, and without it the corner stays an empty cell as before (either way the column takes no width on a phone). `labelWidth` (`xs` to `xl`, the `Table` column sizes) lines a strip's columns up with every other strip of the same columns in a container of the same width, such as the cards of a feed: the row labels take that width and the count columns share the rest of the region equally, so no content moves a column. `labelAppearance="plain"` (default `"tone"`) sets every row label in the text color at label weight with no tone stripe, in both layouts, for a grid whose rows need no color to tell apart; a row's `tone` still colors its total, and an unknown value throws. Three more grid-level options match a framed count sheet, each off by default: `frame="table"` (default `"none"`) draws a strip as a framed table, its scroll region a surface inside a border hairline at the `sm` radius, the heading row on the subtle fill, a rule between columns, the counts at label size centred under their heads and the cells padded a space-3 step across (the tiles layout has no frame and ignores it); `totalEmphasis="strong"` (default `"default"`) sets the row totals in bold label type, a neutral row's in the text color and a toned row's in its tone, under the label on a phone too; and `cellToneAppearance="pill"` (default `"text"`) sets a `cellTones` count of a strip's read-only row on a rounded tint of its tone in the tone's ink (the `Notice` fill and words, 4.5:1 on the tint except where a palette's own `danger` is already under 4.5:1, harvest and mocha dark), outlined in forced colors; entry counts and tiles keep their tone border. A strip row's `valueRow` (`{ label, cells }`, opt-in) adds a line right under it that gives each column's value, such as the money its count adds up to: its own table row on the subtle fill, `label` (such as "Value") in the label column (named with the row's label, "Counted Value", for table navigation), each of `cells` (one consumer-formatted string or `null` per column; `null` or `''` leaves it blank) centred under its count at label size in tabular figures, and the row's `total` moved onto it (bold with `totalEmphasis="strong"`, under the line's name on a phone); the counts' own total cell stays empty. It keeps the row's tone and `disabled` (a disabled line fades its values). Each entry count with a value is described by it (`aria-describedby`, before an error), and the value is no tab stop. The grid still does no arithmetic. A `valueRow` in the tiles layout, a blank label, or cells that are not one string or `null` per column throw a `RangeError`. With `zeroLabel=""` a toned zero or empty count shows nothing in either appearance, so tone only counts that are there. An unknown value of any of them throws a `RangeError`. `zeroLabel` (default `"—"`) is what a zero or empty read-only count shows; an empty string leaves those counts blank, so only the counts that are there stand out (it suits the strip, where the column header names each blank cell, better than tiles, where a blank tile is only its column's name), and a `zeroLabel` that is not a string throws. On a phone the total cell stays in the table, visually hidden, so a screen reader still reads each total in its row; the copy under the label is `aria-hidden`. A strip wider than its container scrolls sideways inside its own keyboard-focusable region, named after `label`, with the row labels pinned at the start and the same edge shade as `Table` while counts are scrolled out of view (the start shade is cast from the pinned labels' edge, since they cover the region's own); the page never scrolls sideways. The `tiles` layout stacks the column label, the count, and an optional `subtotal` string per column. Each tiles row shows its label line (icon, label and `total`) when the grid has several rows, or when a lone row has an icon or a `total`; a lone plain row is named by the grid's `label` alone. Every row is a region named by its `label`, except a lone plain row whose `label` is the grid's own: it has no name of its own, so assistive technology hears the grid's group once rather than a region repeating it. `rowRole="group"` (default `"region"`) keeps every named row's name but makes it a `group` instead of a landmark region, for a page with many grids (one per order card, say) whose rows would otherwise fill the landmark list; the strip ignores it, since its rows are table rows, and an unknown `rowRole` throws a `RangeError` in either layout. A row's icon and its label words stay on one line in both layouts (`sw-denomination-label-line`): a long label wraps its words beside the icon, never under it, and on a phone the strip's total goes on its own line under them. A row's `cellTones` (one Badge tone or `null` per column) tones a single count in place: the count is set in the tone over its signed or zero look, and in the tiles layout its tile's border takes the tone too, so a short bill turns red without leaving its row; `neutral` or `null` leaves a count as it is, a row's `tone` still colors only its label, and a `cellTones` that is not one known tone or `null` per column throws. The tone never says why on its own; put that in words beside the grid. Every negative count, signed or not and in either layout, is written with the typographic minus ("−1"), never a hyphen. The primitive does no arithmetic and no currency formatting; tones reuse the Badge vocabulary. Invalid columns, rows, or cells throw.
564
568
 
565
569
  A strip row with `entry` makes its counts editable: each column holds a `Stepper` (`width="full"`, its label hidden and named by the row label and the column's label and caption as its header reads, such as "Received $20" or "Counted $1 × 100") whose count is the row's `cells` entry (`null` is an empty box). `entry.onChange(columnKey, value)` reports a change; `entry.decrementLabel` and `entry.incrementLabel` name the buttons; `entry.max` caps each column (`null` for none), `entry.errors` puts a message under a column's count (`null` for none), and `entry.placeholder` fills an empty box. `entry.limitAppearance` is the Steppers' `limitAppearance`: `"plain"` keeps − at 0 (and + at a cap) unfaded. The minimum is 0. The grid still does no arithmetic, so recompute the row's `total` in `onChange`. A strip with an entry row centres its heads and read-only counts over the steppers, and each count keeps spacing step 8 of room, so a narrow strip scrolls in its region instead of crushing it. A `cellTones` tone on an entry count draws its border in the tone (danger for a difference); an `accent` tone, for a suggestion or a change, is the `Stepper`'s `changed` look (border and tint); a message in `entry.errors` wins. A row `caption` is a muted line under its label ("from the customer"), and a column `caption` a muted note after its label ("× 100"). Captions show in both layouts. In the strip, `emphasis="quiet"` sets a row on the subtle fill, for a row to compare against, and `disabled` fades a row that does not apply, disables its entry and marks it `aria-disabled`. Entry rows, quiet rows and disabled rows need the `strip` layout. An entry row's counts are zero or more and it is not `signed`; its label is its own among the entry rows, as each column's label and caption are, so every count has its own name. Breaking any of these, a cap or message list that is not one per column, a cap that is not a whole count of zero or more, blank button names or captions, an unknown `emphasis`, or a `disabled` that is not a boolean throws a `RangeError`.
566
570
 
@@ -4,6 +4,7 @@ import { useId, useLayoutEffect, useRef, useState, } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { actionMenuTriggerAppearances, actionMenuTriggerTextClass, } from '../css/css-action-menu-trigger.js';
6
6
  import { ActionMenuList } from './action-menu/ActionMenuList.js';
7
+ import { tabStop } from '../tab-stop.js';
7
8
  export { actionMenuTriggerAppearances };
8
9
  function nextEnabled(items, current, direction) {
9
10
  if (items.length === 0)
@@ -93,5 +94,5 @@ export function ActionMenu({ label, items, header, trigger, disabled = false, al
93
94
  close(true);
94
95
  item.onSelect();
95
96
  }
96
- return (_jsxs("div", { className: "sw-action-menu", children: [_jsx("button", { "aria-controls": open ? menuId : undefined, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": label, className: cx('sw-action-menu-trigger', triggerAppearance === 'text' && actionMenuTriggerTextClass), disabled: !enabled, onClick: () => (open ? close(true) : openMenu()), onKeyDown: onTriggerKeyDown, ref: triggerRef, type: "button", children: triggerAppearance === 'text' ? (_jsx("span", { children: trigger ?? label })) : ((trigger ?? label)) }), open ? (_jsx(ActionMenuList, { align: align, focusIndex: focusIndex, header: header, id: menuId, items: items, label: label, onDismiss: () => close(), onFocusIndex: setFocusIndex, onKeyDown: onMenuKeyDown, onSelect: select, triggerRef: triggerRef })) : null] }));
97
+ return (_jsxs("div", { className: "sw-action-menu", children: [_jsx("button", { "aria-controls": open ? menuId : undefined, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": label, className: cx('sw-action-menu-trigger', triggerAppearance === 'text' && actionMenuTriggerTextClass), disabled: !enabled, onClick: () => (open ? close(true) : openMenu()), onKeyDown: onTriggerKeyDown, ref: triggerRef, tabIndex: tabStop, type: "button", children: triggerAppearance === 'text' ? (_jsx("span", { children: trigger ?? label })) : ((trigger ?? label)) }), open ? (_jsx(ActionMenuList, { align: align, focusIndex: focusIndex, header: header, id: menuId, items: items, label: label, onDismiss: () => close(), onFocusIndex: setFocusIndex, onKeyDown: onMenuKeyDown, onSelect: select, triggerRef: triggerRef })) : null] }));
97
98
  }
@@ -5,10 +5,13 @@ import { boxBorderClass } from '../css/css-box-border.js';
5
5
  import { assertBoxLink, boxLinkClass, } from '../css/css-box-link.js';
6
6
  import { columnSpanClassNames, } from '../column-span-classes.js';
7
7
  import { spacingClassNames } from '../spacing-classes.js';
8
+ import { tabStop } from '../tab-stop.js';
8
9
  import { visibilityClassNames, } from '../visibility-classes.js';
9
- export const Box = forwardRef(function Box({ as = 'div', background, border, className, columnSpan, hideBelow, hideFrom, link, padding, paddingBottom, paddingLeft, paddingRight, paddingTop, paddingX, paddingY, radius, style, ...rest }, ref) {
10
+ export const Box = forwardRef(function Box({ as = 'div', background, border, className, columnSpan, hideBelow, hideFrom, link, padding, paddingBottom, paddingLeft, paddingRight, paddingTop, paddingX, paddingY, radius, style, tabIndex, ...rest }, ref) {
10
11
  assertBoxLink(link, as);
11
12
  const Component = as;
13
+ // A link with a destination is a tab stop in Safari too (see tabStop).
14
+ const linkTabStop = as === 'a' && rest.href !== undefined ? tabStop : undefined;
12
15
  return (_jsx(Component, { ref: ref, className: cx(...spacingClassNames({
13
16
  padding,
14
17
  paddingBottom,
@@ -23,5 +26,5 @@ export const Box = forwardRef(function Box({ as = 'div', background, border, cla
23
26
  : undefined,
24
27
  borderRadius: radius ? `var(--sw-radius-${radius})` : undefined,
25
28
  ...style,
26
- }, ...rest }));
29
+ }, tabIndex: tabIndex ?? linkTabStop, ...rest }));
27
30
  });
@@ -2,8 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { buttonClassNames, buttonDisabledAppearances, } from '../css/css-button.js';
3
3
  import { forwardRef } from 'react';
4
4
  import { cx } from '../class-names.js';
5
- export const Button = forwardRef(function Button({ children, className, disabled = false, disabledAppearance = 'faded', onPress, size = 'md', type = 'button', variant = 'primary', ...rest }, ref) {
5
+ import { tabStop } from '../tab-stop.js';
6
+ export const Button = forwardRef(function Button({ children, className, disabled = false, disabledAppearance = 'faded', onPress, size = 'md', tabIndex, type = 'button', variant = 'primary', ...rest }, ref) {
6
7
  if (!buttonDisabledAppearances.includes(disabledAppearance))
7
8
  throw new RangeError(`Unknown Button disabledAppearance: ${String(disabledAppearance)}`);
8
- return (_jsx("button", { ref: ref, className: cx(...buttonClassNames({ size, variant, disabledAppearance }), className), disabled: disabled, type: type, ...rest, onClick: onPress, children: children }));
9
+ return (_jsx("button", { ref: ref, className: cx(...buttonClassNames({ size, variant, disabledAppearance }), className), disabled: disabled, tabIndex: tabIndex ?? tabStop, type: type, ...rest, onClick: onPress, children: children }));
9
10
  });
@@ -13,6 +13,7 @@ import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
13
13
  import { useCalendarPopup } from './date-field/use-calendar-popup.js';
14
14
  import { useDateEntry } from './date-field/use-date-entry.js';
15
15
  import { useLangLocale } from './date-field/use-lang-locale.js';
16
+ import { tabStop } from '../tab-stop.js';
16
17
  export function DateField({ label, value, onChange, min, max, today, disabled = false, required = false, requiredMark = 'visible', description, error, locale, weekStartsOn = 0, labels, }) {
17
18
  assertRequiredMark(requiredMark);
18
19
  assertDateBounds(value, min, max);
@@ -59,5 +60,5 @@ export function DateField({ label, value, onChange, min, max, today, disabled =
59
60
  day: words.dayPlaceholder,
60
61
  month: words.monthPlaceholder,
61
62
  year: words.yearPlaceholder,
62
- }), ref: inputRef, required: required, spellCheck: false, type: "text", value: entry.text }), _jsxs("button", { ...trigger, "aria-describedby": labelId, className: "sw-date-field-button", disabled: disabled, onClick: toggle, ref: buttonRef, type: "button", children: [_jsx("span", { className: "sw-sr-only", children: words.chooseDate }), _jsx(DateFieldGlyph, { name: "calendar" })] })] }), description ? (_jsx("span", { className: "sw-date-field-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-date-field-error", id: entryErrorId, message: entry.invalid ? words.invalidEntry : undefined }), _jsx(FieldErrorRegion, { className: "sw-date-field-error", id: errorId, message: error }), shown ? (_jsx(CalendarDialog, { ...dialog, buttonRef: buttonRef, anchorRef: controlRef, labelId: labelId, labels: words, locale: resolvedLocale, max: max, min: min, required: required, value: value, weekStartsOn: weekStartsOn })) : null] }));
63
+ }), ref: inputRef, required: required, spellCheck: false, type: "text", value: entry.text }), _jsxs("button", { ...trigger, "aria-describedby": labelId, className: "sw-date-field-button", disabled: disabled, onClick: toggle, ref: buttonRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-sr-only", children: words.chooseDate }), _jsx(DateFieldGlyph, { name: "calendar" })] })] }), description ? (_jsx("span", { className: "sw-date-field-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-date-field-error", id: entryErrorId, message: entry.invalid ? words.invalidEntry : undefined }), _jsx(FieldErrorRegion, { className: "sw-date-field-error", id: errorId, message: error }), shown ? (_jsx(CalendarDialog, { ...dialog, buttonRef: buttonRef, anchorRef: controlRef, labelId: labelId, labels: words, locale: resolvedLocale, max: max, min: min, required: required, value: value, weekStartsOn: weekStartsOn })) : null] }));
63
64
  }
@@ -2,7 +2,7 @@ import { denominationGridFrames, denominationTotalEmphases } from '../css/css-de
2
2
  import { denominationCellToneAppearances } from '../css/css-denomination-cell-tones.js';
3
3
  import { denominationLabelAppearances } from '../css/css-denomination-label.js';
4
4
  import { type DenominationGridProps } from './denomination-grid/types.js';
5
- export type { DenominationCellToneAppearance, DenominationGridColumn, DenominationGridEntry, DenominationGridFrame, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationLabelWidth, DenominationTotalEmphasis, } from './denomination-grid/types.js';
5
+ export type { DenominationCellToneAppearance, DenominationGridColumn, DenominationGridEntry, DenominationGridFrame, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationGridValueRow, DenominationLabelWidth, DenominationTotalEmphasis, } from './denomination-grid/types.js';
6
6
  export type { DenominationLabelAppearance } from '../css/css-denomination-label.js';
7
7
  export { denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationTotalEmphases, };
8
8
  export declare function DenominationGrid({ label, columns, rows, layout, subtotal, zeroLabel, totalLabel, rowRole, labelWidth, labelAppearance, frame, totalEmphasis, cellToneAppearance, }: DenominationGridProps): import("react").JSX.Element;
@@ -7,6 +7,7 @@ import { denominationLabelAppearances, denominationLabelsPlainClass, } from '../
7
7
  import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
8
8
  import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
9
9
  import { assertDenominationGrid } from './denomination-grid/validate.js';
10
+ import { assertDenominationValueRows } from './denomination-grid/validate-value-row.js';
10
11
  import { assertDenominationEntries, assertDenominationExtras, } from './denomination-grid/validate-entry.js';
11
12
  export { denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationTotalEmphases, };
12
13
  /** Fails closed on a value outside a closed set, naming the prop. */
@@ -18,6 +19,7 @@ export function DenominationGrid({ label, columns, rows, layout = 'strip', subto
18
19
  assertDenominationGrid(label, columns, rows);
19
20
  assertDenominationExtras(layout, columns, rows);
20
21
  assertDenominationEntries(layout, columns, rows);
22
+ assertDenominationValueRows(layout, columns, rows);
21
23
  if (typeof zeroLabel !== 'string')
22
24
  throw new RangeError('zeroLabel must be a string');
23
25
  if (totalLabel !== undefined && !totalLabel.trim())
@@ -4,6 +4,7 @@ import { useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { FieldErrorRegion } from './FieldErrorRegion.js';
6
6
  import { assertRequiredMark, showsRequiredMark, } from './FieldLabelText.js';
7
+ import { radioTabStopValue, tabStop } from '../tab-stop.js';
7
8
  function isCount(count) {
8
9
  return count === undefined || (Number.isInteger(count) && count >= 0);
9
10
  }
@@ -26,8 +27,9 @@ export function FilterChips({ label, value, onChange, options, required = false,
26
27
  throw new RangeError('value must match an option or be empty');
27
28
  if (options.some((option) => !isCount(option.count)))
28
29
  throw new RangeError('count must be a non-negative integer');
30
+ const entry = radioTabStopValue(options, value);
29
31
  return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": error ? true : undefined, className: "sw-filter-chips", children: [_jsxs("legend", { className: "sw-filter-chips-legend", children: [label, showsRequiredMark(required, requiredMark) ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: option.value === value, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
30
32
  if (event.currentTarget.checked && !option.disabled)
31
33
  onChange(option.value);
32
- }, required: required, type: "radio", value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-filter-chip-count", children: option.count })] }))] })] }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-filter-chips-error", id: errorId, message: error })] }));
34
+ }, required: required, tabIndex: option.value === entry ? tabStop : undefined, type: "radio", value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-filter-chip-count", children: option.count })] }))] })] }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-filter-chips-error", id: errorId, message: error })] }));
33
35
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId, useRef, useState } from 'react';
4
4
  import { MultiSelectPanel } from './multi-select/MultiSelectPanel.js';
5
5
  import { assertMultiSelect } from './multi-select/validate.js';
6
+ import { tabStop } from '../tab-stop.js';
6
7
  /**
7
8
  * Several choices from a short list behind one trigger: "Show All entries ▾"
8
9
  * opens a panel with All, Clear and a checkbox per option. A disclosure (a
@@ -58,5 +59,5 @@ export function MultiSelect({ align = 'start', allLabel, clearLabel, disabled =
58
59
  close(true);
59
60
  }
60
61
  }
61
- return (_jsxs("div", { className: "sw-multi-select", children: [_jsxs("button", { "aria-controls": shown ? panelId : undefined, "aria-expanded": shown, className: "sw-multi-select-trigger", disabled: disabled, onClick: () => (shown ? close(true) : setOpen(true)), onKeyDown: onTriggerKeyDown, ref: triggerRef, type: "button", children: [_jsx("span", { className: "sw-multi-select-label", children: label }), ' ', _jsx("span", { className: "sw-multi-select-summary", children: summary })] }), shown ? (_jsx(MultiSelectPanel, { align: align, allLabel: allLabel, checked: checked, clearLabel: clearLabel, id: panelId, label: label, onAll: () => report(new Set(options.map((option) => option.value))), onClear: () => report(new Set()), onClose: close, onToggle: onToggle, options: options, triggerRef: triggerRef })) : null] }));
62
+ return (_jsxs("div", { className: "sw-multi-select", children: [_jsxs("button", { "aria-controls": shown ? panelId : undefined, "aria-expanded": shown, className: "sw-multi-select-trigger", disabled: disabled, onClick: () => (shown ? close(true) : setOpen(true)), onKeyDown: onTriggerKeyDown, ref: triggerRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-multi-select-label", children: label }), ' ', _jsx("span", { className: "sw-multi-select-summary", children: summary })] }), shown ? (_jsx(MultiSelectPanel, { align: align, allLabel: allLabel, checked: checked, clearLabel: clearLabel, id: panelId, label: label, onAll: () => report(new Set(options.map((option) => option.value))), onClear: () => report(new Set()), onClose: close, onToggle: onToggle, options: options, triggerRef: triggerRef })) : null] }));
62
63
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
4
  import { FieldErrorRegion } from './FieldErrorRegion.js';
5
5
  import { RadioGroupOptionLabel, } from './radio-group/RadioGroupOptionLabel.js';
6
+ import { radioTabStopValue } from '../tab-stop.js';
6
7
  export function RadioGroup({ legend, value, onChange, options, required = false, disabled = false, description, error, }) {
7
8
  const name = useId();
8
9
  const descriptionId = useId();
@@ -20,8 +21,9 @@ export function RadioGroup({ legend, value, onChange, options, required = false,
20
21
  throw new RangeError('options must have unique values');
21
22
  if (value !== '' && !values.includes(value))
22
23
  throw new RangeError('value must match an option or be empty');
24
+ const entry = radioTabStopValue(options, value);
23
25
  return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), className: "sw-radio-group", disabled: disabled, children: [_jsx("legend", { className: "sw-radio-group-legend", children: legend }), description ? (_jsx("span", { className: "sw-radio-group-description", id: descriptionId, children: description })) : null, _jsx("span", { className: "sw-radio-group-options", children: options.map((option) => (_jsx(RadioGroupOptionLabel, { checked: value === option.value, name: name, onSelect: () => {
24
26
  if (!disabled && !option.disabled)
25
27
  onChange(option.value);
26
- }, option: option, required: required }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-radio-group-error", id: errorId, message: error })] }));
28
+ }, option: option, required: required, isTabStop: option.value === entry }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-radio-group-error", id: errorId, message: error })] }));
27
29
  }
@@ -3,6 +3,7 @@ import { cx } from '../class-names.js';
3
3
  import { breakpoints } from '../css/breakpoints.js';
4
4
  import { sectionNavVerticalClass } from '../css/css-section-nav.js';
5
5
  import { assertSectionNavAppearance, } from '../css/css-section-nav-filled.js';
6
+ import { tabStop } from '../tab-stop.js';
6
7
  export { sectionNavAppearances } from '../css/css-section-nav-filled.js';
7
8
  function plainPress(event) {
8
9
  return (event.button === 0 &&
@@ -43,7 +44,7 @@ export function SectionNav({ appearance = 'underline', items, onNavigate, vertic
43
44
  assertItems(items);
44
45
  assertBreakpoint(verticalFrom);
45
46
  assertSectionNavAppearance(appearance);
46
- return (_jsx("nav", { className: cx('sw-section-nav', verticalFrom && sectionNavVerticalClass(verticalFrom), appearance === 'filled' && 'sw-section-nav-filled'), ...label, children: _jsx("ul", { className: "sw-section-nav-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current": ariaCurrent(item.current), className: "sw-section-nav-link", href: item.href,
47
+ return (_jsx("nav", { className: cx('sw-section-nav', verticalFrom && sectionNavVerticalClass(verticalFrom), appearance === 'filled' && 'sw-section-nav-filled'), ...label, children: _jsx("ul", { className: "sw-section-nav-list", children: items.map((item) => (_jsx("li", { children: _jsxs("a", { "aria-current": ariaCurrent(item.current), className: "sw-section-nav-link", href: item.href, tabIndex: tabStop,
47
48
  // Only a given onNavigate attaches a handler, so the links
48
49
  // render from a server component without one.
49
50
  onClick: onNavigate
@@ -4,6 +4,7 @@ import { forwardRef, useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { segmentedRaisedClass } from '../css/css-segmented-raised.js';
6
6
  import { FieldErrorRegion } from './FieldErrorRegion.js';
7
+ import { tabStop } from '../tab-stop.js';
7
8
  export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
8
9
  const errorId = useId();
9
10
  const withIcons = items.some((item) => item.icon);
@@ -30,6 +31,9 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
30
31
  if (!selected) {
31
32
  onChange?.(item.id);
32
33
  }
33
- }, role: "radio", type: "button", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-segmented-item-icon", children: item.icon })) : null, item.label] }, item.id));
34
+ }, role: "radio",
35
+ // Every item stays a tab stop, as each already was outside
36
+ // Safari: the arrow keys change the value, not the focus.
37
+ tabIndex: tabStop, type: "button", children: [item.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-segmented-item-icon", children: item.icon })) : null, item.label] }, item.id));
34
38
  }) }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
35
39
  });
@@ -9,6 +9,7 @@ import { SelectListbox } from './select/SelectListbox.js';
9
9
  import { SelectValue } from './select/SelectValue.js';
10
10
  import { useSelectList } from './select/use-select-list.js';
11
11
  import { Stack } from './Stack.js';
12
+ import { tabStop } from '../tab-stop.js';
12
13
  export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, requiredMark = 'visible', error, width = 'content', }) {
13
14
  assertRequiredMark(requiredMark);
14
15
  if (placeholder !== undefined && !placeholder.trim())
@@ -35,5 +36,5 @@ export function Select({ action, label, labelVisuallyHidden = false, onChange, o
35
36
  return;
36
37
  }
37
38
  openList();
38
- }, onKeyDown: onTriggerKeyDown, role: "combobox", type: "button", children: _jsx(SelectValue, { options: options, placeholder: placeholder, text: showsPlaceholder ? placeholder : (selected?.label ?? ''), textIsPlaceholder: showsPlaceholder }) }), open ? (_jsx(SelectListbox, { actionLabel: action?.label, highlight: highlight, id: listId, labelId: labelId, onCommit: commit, onHighlight: setHighlight, optionIdPrefix: optionIdPrefix, options: options, value: value })) : null] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }) }));
39
+ }, onKeyDown: onTriggerKeyDown, role: "combobox", tabIndex: tabStop, type: "button", children: _jsx(SelectValue, { options: options, placeholder: placeholder, text: showsPlaceholder ? placeholder : (selected?.label ?? ''), textIsPlaceholder: showsPlaceholder }) }), open ? (_jsx(SelectListbox, { actionLabel: action?.label, highlight: highlight, id: listId, labelId: labelId, onCommit: commit, onHighlight: setHighlight, optionIdPrefix: optionIdPrefix, options: options, value: value })) : null] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }) }));
39
40
  }
@@ -38,6 +38,11 @@ export type StepperProps = {
38
38
  readOnly?: boolean;
39
39
  /** Says the count in words for assistive technology (`aria-valuetext`), such as "12 straps". */
40
40
  valueText?: string;
41
+ /**
42
+ * Ids of elements that describe the count, such as the value it adds up
43
+ * to; read before an `error`'s message.
44
+ */
45
+ 'aria-describedby'?: string;
41
46
  /** Shown in an empty box, such as "0". */
42
47
  placeholder?: string;
43
48
  name?: string;
@@ -15,7 +15,7 @@ function keepFocus(event) {
15
15
  * tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
16
16
  * the buttons as pointer targets beside it. − stops at `min`, + at `max`.
17
17
  */
18
- export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
18
+ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
19
19
  const bounds = { min, max, step };
20
20
  assertStepperBounds(bounds);
21
21
  if (!stepperLimitAppearances.includes(limitAppearance))
@@ -76,7 +76,9 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
76
76
  return (_jsxs("div", { className: cx('sw-stepper', width === 'full' && 'sw-stepper-full', invalid && 'sw-stepper-invalid', changed && !invalid && 'sw-stepper-changed', readOnly && 'sw-stepper-readonly', limitAppearance === 'plain' &&
77
77
  !disabled &&
78
78
  !readOnly &&
79
- 'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx("input", { ref: ref, "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": valueText, autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
79
+ 'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx("input", { ref: ref, "aria-describedby": [describedBy?.trim() || null, invalid ? errorId : null]
80
+ .filter((id) => id !== null)
81
+ .join(' ') || undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": valueText, autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
80
82
  // iOS's numeric pad has no minus key, so a range below zero takes
81
83
  // the text keyboard.
82
84
  inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => setDraft(null), onChange: onType, onKeyDown: onKeyDown, placeholder: placeholder, readOnly: readOnly, role: "spinbutton", type: "text", value: text }), _jsx("button", { "aria-controls": inputId, "aria-label": incrementLabel, className: "sw-stepper-button", disabled: moves.up === null, onClick: () => move(moves.up), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "+" }) })] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
@@ -9,10 +9,19 @@ export type StepsProps = Omit<HTMLAttributes<HTMLOListElement>, 'children'> & {
9
9
  headingLevel?: StepHeadingLevel;
10
10
  /** The status words assistive technology hears after a folded step's title. English by default. */
11
11
  labels?: Partial<StepsLabels>;
12
+ /**
13
+ * Every step open: each shows its `children` whatever its status, as a
14
+ * region named by its title, so a form's sections are all filled on one
15
+ * page. The markers still say where each stands. No step is
16
+ * `aria-current` and none takes focus when its status changes. A step's
17
+ * own `headingLevel` still overrides the list's.
18
+ */
19
+ open?: boolean;
12
20
  };
13
21
  /**
14
22
  * A one-page vertical stepper: a form's steps in order, the current one
15
23
  * open, the done ones folded to a line with their summary and Edit, the
16
- * ones ahead greyed, a line joining each marker to the next.
24
+ * ones ahead greyed, a line joining each marker to the next. With `open`,
25
+ * every step stays open and only its marker follows its status.
17
26
  */
18
- export declare function Steps({ children, className, headingLevel, labels, ...rest }: StepsProps): import("react").JSX.Element;
27
+ export declare function Steps({ children, className, headingLevel, labels, open, ...rest }: StepsProps): import("react").JSX.Element;
@@ -14,9 +14,10 @@ function assertSteps(labels, level) {
14
14
  /**
15
15
  * A one-page vertical stepper: a form's steps in order, the current one
16
16
  * open, the done ones folded to a line with their summary and Edit, the
17
- * ones ahead greyed, a line joining each marker to the next.
17
+ * ones ahead greyed, a line joining each marker to the next. With `open`,
18
+ * every step stays open and only its marker follows its status.
18
19
  */
19
- export function Steps({ children, className, headingLevel = 2, labels, ...rest }) {
20
+ export function Steps({ children, className, headingLevel = 2, labels, open = false, ...rest }) {
20
21
  const words = { ...defaultStepsLabels, ...labels };
21
22
  assertSteps(words, headingLevel);
22
23
  const steps = Children.toArray(children).filter(isValidElement);
@@ -25,10 +26,11 @@ export function Steps({ children, className, headingLevel = 2, labels, ...rest }
25
26
  throw new RangeError('Steps children must be Steps, not a Fragment');
26
27
  // The first step to open in a commit claims focus (see Step).
27
28
  const focusClaimRef = useRef(false);
28
- return (_jsx("ol", { className: cx('sw-steps', className), ...rest, children: steps.map((step, index) => (_jsx(StepPlaceContext.Provider, { value: {
29
+ return (_jsx("ol", { className: cx('sw-steps', open && 'sw-steps-open', className), ...rest, children: steps.map((step, index) => (_jsx(StepPlaceContext.Provider, { value: {
29
30
  number: index + 1,
30
31
  headingLevel,
31
32
  labels: words,
32
33
  focusClaimRef,
34
+ open,
33
35
  }, children: step }, step.key))) }));
34
36
  }
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useId } from 'react';
4
+ import { tabStop } from '../tab-stop.js';
4
5
  export function Switch({ label, checked, onCheckedChange, disabled = false, description, }) {
5
6
  const labelId = useId();
6
7
  const descriptionId = useId();
7
8
  return (_jsxs("label", { className: "sw-switch", children: [_jsxs("span", { className: "sw-switch-control", children: [_jsx("input", { "aria-checked": checked, "aria-describedby": description ? descriptionId : undefined, "aria-labelledby": labelId, checked: checked, className: "sw-switch-input", disabled: disabled, onChange: (event) => {
8
9
  if (!disabled)
9
10
  onCheckedChange(event.currentTarget.checked);
10
- }, role: "switch", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-switch-track", children: _jsx("span", { className: "sw-switch-thumb" }) })] }), _jsxs("span", { className: "sw-switch-copy", children: [_jsx("span", { className: "sw-switch-label", id: labelId, children: label }), description ? (_jsx("span", { className: "sw-switch-description", id: descriptionId, children: description })) : null] })] }));
11
+ }, role: "switch", tabIndex: tabStop, type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-switch-track", children: _jsx("span", { className: "sw-switch-thumb" }) })] }), _jsxs("span", { className: "sw-switch-copy", children: [_jsx("span", { className: "sw-switch-label", id: labelId, children: label }), description ? (_jsx("span", { className: "sw-switch-description", id: descriptionId, children: description })) : null] })] }));
11
12
  }
@@ -5,5 +5,5 @@ type CheckboxControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'classNa
5
5
  * generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
6
6
  * and MultiSelect's rows so the two cannot drift apart.
7
7
  */
8
- export declare function CheckboxControl(props: CheckboxControlProps): import("react").JSX.Element;
8
+ export declare function CheckboxControl({ tabIndex, ...props }: CheckboxControlProps): import("react").JSX.Element;
9
9
  export {};
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { tabStop } from '../../tab-stop.js';
2
3
  /**
3
4
  * The native checkbox and its drawn mark, the one markup Checkbox's
4
5
  * generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
5
6
  * and MultiSelect's rows so the two cannot drift apart.
6
7
  */
7
- export function CheckboxControl(props) {
8
- return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
8
+ export function CheckboxControl({ tabIndex, ...props }) {
9
+ return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
9
10
  }
@@ -4,13 +4,14 @@ import { monthInRange, monthsInRange, shiftMonth, } from '../../calendar-month.j
4
4
  import { calendarYearRange, yearsIn } from '../../calendar-years.js';
5
5
  import { Select } from '../Select.js';
6
6
  import { DateFieldGlyph } from './DateFieldGlyph.js';
7
+ import { tabStop } from '../../tab-stop.js';
7
8
  function MonthStep({ delta, label, max, min, month, onMonthChange, }) {
8
9
  const target = shiftMonth(month, delta);
9
10
  const unavailable = target.year < 1 || target.year > 9999 || !monthInRange(target, min, max);
10
11
  return (_jsx("button", { "aria-disabled": unavailable || undefined, "aria-label": label, className: "sw-date-field-step", onClick: () => {
11
12
  if (!unavailable)
12
13
  onMonthChange(target);
13
- }, type: "button", children: _jsx(DateFieldGlyph, { name: delta < 0 ? 'previous' : 'next' }) }));
14
+ }, tabIndex: tabStop, type: "button", children: _jsx(DateFieldGlyph, { name: delta < 0 ? 'previous' : 'next' }) }));
14
15
  }
15
16
  /**
16
17
  * Month stepping plus month and year selectors for jumping far, both cut to
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
2
3
  import { cx } from '../../class-names.js';
3
4
  import { denominationLabelWidthClass } from '../../css/css-denomination-aligned.js';
4
5
  import { denominationFramedClass, denominationScrollFramedClass, } from '../../css/css-denomination-appearance.js';
@@ -18,6 +19,7 @@ function TotalHead({ totalLabel }) {
18
19
  return (_jsx("th", { className: "sw-denomination-head sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-denomination-total-label", children: totalLabel }) }));
19
20
  }
20
21
  export function DenominationStrip({ className, framed, label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
22
+ const idPrefix = useId();
21
23
  const hasTotals = rows.some((row) => row.total !== undefined);
22
24
  // An aligned strip sizes its columns from the region's width; it needs
23
25
  // the column count and whether a total column takes a share.
@@ -27,5 +29,5 @@ export function DenominationStrip({ className, framed, label, columns, rows, zer
27
29
  '--sw-denomination-columns': columns.length,
28
30
  '--sw-denomination-total': hasTotals ? 1 : 0,
29
31
  };
30
- return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned', framed && denominationScrollFramedClass), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth), framed && denominationFramedClass, className), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
32
+ return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned', framed && denominationScrollFramedClass), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth), framed && denominationFramedClass, className), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row, rowIndex) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, idPrefix: idPrefix, row: row, rowIndex: rowIndex, zeroLabel: zeroLabel }, row.id))) })] }) }));
31
33
  }
@@ -4,6 +4,8 @@ type EntryCellProps = {
4
4
  entry: DenominationGridEntry;
5
5
  column: DenominationGridColumn;
6
6
  index: number;
7
+ /** The value cell under this count, which describes it, if any. */
8
+ describedBy?: string;
7
9
  };
8
10
  /**
9
11
  * One editable count of a strip row: a full-width `Stepper` named by the
@@ -11,5 +13,5 @@ type EntryCellProps = {
11
13
  * message from the entry, and the cell's tone on its border (an accent one
12
14
  * is the Stepper's changed look).
13
15
  */
14
- export declare function EntryCell({ row, entry, column, index }: EntryCellProps): import("react").JSX.Element;
16
+ export declare function EntryCell({ row, entry, column, index, describedBy, }: EntryCellProps): import("react").JSX.Element;
15
17
  export {};
@@ -9,8 +9,8 @@ import { columnName } from './ColumnHeadText.js';
9
9
  * message from the entry, and the cell's tone on its border (an accent one
10
10
  * is the Stepper's changed look).
11
11
  */
12
- export function EntryCell({ row, entry, column, index }) {
12
+ export function EntryCell({ row, entry, column, index, describedBy, }) {
13
13
  const max = entry.max?.[index] ?? undefined;
14
14
  const tone = row.cellTones?.[index];
15
- return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, limitAppearance: entry.limitAppearance, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
15
+ return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { "aria-describedby": describedBy, changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, limitAppearance: entry.limitAppearance, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
16
16
  }
@@ -4,10 +4,16 @@ type StripRowProps = {
4
4
  columns: readonly DenominationGridColumn[];
5
5
  zeroLabel: string;
6
6
  hasTotals: boolean;
7
+ /** A per-grid prefix that keeps the value cells' ids unique on the page. */
8
+ idPrefix: string;
9
+ /** The row's place in the grid, for its value cells' ids. */
10
+ rowIndex: number;
7
11
  };
8
12
  /**
9
13
  * One strip row: its pinned label (with the phone total under it), a count
10
- * per column, read-only or entered, and its total.
14
+ * per column, read-only or entered, and its total. With a `valueRow`, the
15
+ * value line follows it and takes the total; each entry count is described
16
+ * by the value under it.
11
17
  */
12
- export declare function StripRow({ row, columns, zeroLabel, hasTotals, }: StripRowProps): import("react").JSX.Element;
18
+ export declare function StripRow({ row, columns, zeroLabel, hasTotals, idPrefix, rowIndex, }: StripRowProps): import("react").JSX.Element;
13
19
  export {};