@scalewing/react 1.52.0 → 1.54.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.
- package/CHANGELOG.md +20 -0
- package/README.md +14 -1
- package/dist/components/Box.d.ts +26 -0
- package/dist/components/Box.js +6 -2
- package/dist/components/Checkbox.d.ts +18 -1
- package/dist/components/Checkbox.js +15 -5
- package/dist/components/Dialog.d.ts +14 -1
- package/dist/components/Dialog.js +5 -2
- package/dist/components/PasteRows.d.ts +55 -0
- package/dist/components/PasteRows.js +55 -0
- package/dist/components/Stepper.d.ts +2 -66
- package/dist/components/Stepper.js +18 -5
- package/dist/components/Table.d.ts +1 -0
- package/dist/components/Table.js +1 -0
- package/dist/components/box/external-link.d.ts +20 -0
- package/dist/components/box/external-link.js +37 -0
- package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
- package/dist/components/checkbox/CheckboxControl.js +4 -3
- package/dist/components/dialog/validate.d.ts +3 -1
- package/dist/components/dialog/validate.js +3 -1
- package/dist/components/paste-rows/parse-pasted-rows.d.ts +26 -0
- package/dist/components/paste-rows/parse-pasted-rows.js +123 -0
- package/dist/components/stepper/stepper-props.d.ts +78 -0
- package/dist/components/stepper/stepper-props.js +1 -0
- package/dist/components/stepper/use-stepper-commit.d.ts +22 -0
- package/dist/components/stepper/use-stepper-commit.js +66 -0
- package/dist/components/table/TableSelectCell.d.ts +26 -0
- package/dist/components/table/TableSelectCell.js +19 -0
- package/dist/css/css-box-link.d.ts +3 -0
- package/dist/css/css-box-link.js +41 -2
- package/dist/css/css-checkbox.js +37 -3
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-dialog-end.d.ts +13 -0
- package/dist/css/css-dialog-end.js +75 -0
- package/dist/css/css-select.js +10 -0
- package/dist/css/css-table-select.d.ts +3 -0
- package/dist/css/css-table-select.js +24 -0
- package/dist/css/css-table.js +4 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/styles.css +128 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.54.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4c2d7cd: `Box` `external` and `newTabLabel` (opt-in, `as="a"` only): a link that opens in a new tab, `target="_blank"` with `noopener noreferrer` added to `rel`, a decorative arrow (↗) after its words drawn with borders in the link's colour, and `newTabLabel` (your words, such as "(opens in a new tab)", required with `external`) read after its name. New class `sw-link-external-mark`. `Stepper` `onCommit` and `commitDelay` (opt-in): the finished count once per entry, on blur, on Enter and `commitDelay` ms (default 600) after the last − / + press or step key, never per typed digit and never for an unchanged count; `onChange` is unchanged. Requests: `docs/requests/futmas-external-link.md`, `docs/requests/futmas-stepper-commit.md`.
|
|
8
|
+
|
|
9
|
+
## 1.53.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 2d5e8ef: `Checkbox` `labelVisuallyHidden` (the label stays the name, not drawn; the box is its own target, 44 × 44 px on a coarse pointer) and `indeterminate` (the mixed state: a dash on the accent fill, the native `indeterminate` property and `aria-checked="mixed"`). `TableSelectCell` (`label`, `checked`, `indeterminate?`, `onCheckedChange`, `disabled?`, `as?: 'td' | 'th'`): a table's select column holding that Checkbox, as narrow as the box. New classes `sw-checkbox-label-hidden`, `sw-table-select`. Request: `docs/requests/futmas-table-row-selection.md`.
|
|
14
|
+
|
|
15
|
+
`Dialog` `placement` (`'center'`, the default, or `'end'`): from md up a full-height side panel at the inline end with Dialog's title row, close button, scrolling content and pinned footer; below md the centred dialog or the bottom sheet with `sheetBelow`. Exports `dialogPlacements`, `DialogPlacement`. New class `sw-dialog-end`. Request: `docs/requests/futmas-dialog-side-panel.md`.
|
|
16
|
+
|
|
17
|
+
`PasteRows`: a text area for rows pasted from a spreadsheet, a polite count of the columns found and one `Select size="xs"` per column choosing its meaning (`label`, `description?`, `value`, `onValueChange`, `columns`, `mapping`, `onMappingChange`, `countLabel`, `columnLabel?`, `delimiter?`, `rows?`, `placeholder?`, `error?`, `disabled?`, `required?`), with the pure `parsePastedRows(text, delimiter?)` (tab or comma cells, quoted cells, blank rows dropped). Exports `PasteRows`, `PasteRowsProps`, `PasteRowsColumn`, `parsePastedRows`, `PastedRows`, `PastedRowsDelimiter`, `pastedRowsDelimiters`. Request: `docs/requests/futmas-paste-rows.md`.
|
|
18
|
+
|
|
19
|
+
`Select`: on a coarse pointer the trigger and each option are at least 44 px high at every size, as every `Button` is since 1.51.0; a fine pointer keeps `xs` at 28 px, and an `xs` Select's open list is taller on touch. Request: `docs/requests/futmas-select-touch-target.md`.
|
|
20
|
+
|
|
21
|
+
No new tokens or dependencies.
|
|
22
|
+
|
|
3
23
|
## 1.52.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DateRangeButton,
|
|
26
26
|
Card,
|
|
27
27
|
Checkbox,
|
|
28
|
+
PasteRows,
|
|
28
29
|
RadioGroup,
|
|
29
30
|
SectionNav,
|
|
30
31
|
Skeleton,
|
|
@@ -105,7 +106,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
105
106
|
|
|
106
107
|
`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. A `ref` reaches its `header` element. An unknown `appearance` throws a `RangeError`.
|
|
107
108
|
|
|
108
|
-
`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): its name is one visually hidden line, the title, a comma and the word ("The site, Done"), and the drawn title is `aria-hidden`, because a browser lays a hidden box out as a block and would put a space before the comma ("The site , Done"). The step's region and its Edit button are still named by the drawn title. A current step, and a skipped step showing its reason, draw and read their words as they are. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. The comma after the title is Scalewing's, as before; the word is yours. 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. `titleDetail` (opt-in) is muted words after an open step's title, on its line, such as "Optional" on a step a person may skip, or "Wetlands done · Woodlands empty" on a step with a part per habitat: the label size at the body's weight in the muted colour, a space-2 after the title on its baseline, or on a line of their own under the title where they do not fit beside it (a long title on a phone), never past the panel (a long word breaks). They are drawn while the step is open (current or error, or every step of an open list, with fields or without) and stay as the step turns done in an open list; a folded step draws its summary or reason instead. They sit outside the heading, so the heading's name stays the title (and its status word) and a heading list reads the titles alone; an open list's region keeps the title as its name and is described by the words, read as a person enters it, and in a stepped list they follow the heading in reading order. Write your own separators inside the words; Scalewing draws only the gap after the title. The words share a skipped step's reason's muted colour, not its size: a reason is part of the title, these are a note beside it. Give a step its `titleDetail` from its first render: setting or clearing it later moves the heading into or out of the title line, which remounts it (and drops its focus if it has it). A blank `titleDetail` throws a `RangeError`. 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`.
|
|
109
|
+
`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`. `Box as="a" external` (opt-in) opens the address in a new tab: it sets `target="_blank"` (always), adds `noopener noreferrer` to your `rel` words (each once), draws a decorative arrow (↗, `sw-link-external-mark`, `aria-hidden`) right after the words with no space, so it never wraps alone, and reads `newTabLabel` after the link's name from a visually hidden span ("Check the marsh map (opens in a new tab)"). The arrow is drawn with borders in the link's colour (accent, the hover colour, a quiet link's), so forced colours keep it, and is mirrored in right-to-left text. `newTabLabel` is your words in your language; it is required with `external` (blank throws a `RangeError`) and refused without it (`TypeError`); `external` on a box that is not `as="a"` throws a `TypeError`. `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): its name is one visually hidden line, the title, a comma and the word ("The site, Done"), and the drawn title is `aria-hidden`, because a browser lays a hidden box out as a block and would put a space before the comma ("The site , Done"). The step's region and its Edit button are still named by the drawn title. A current step, and a skipped step showing its reason, draw and read their words as they are. The heading's text content now holds the title twice (drawn, then in the spoken line), so a test reading a folded heading's `textContent` or finding its title with a pattern (`getByText(/The site/)`) finds both: query headings by role and name (`getByRole('heading', { name: 'The site, Done' })`). Copying a folded heading, or finding its title in the page, meets the words twice too. The comma after the title is Scalewing's, as before; the word is yours. 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. `titleDetail` (opt-in) is muted words after an open step's title, on its line, such as "Optional" on a step a person may skip, or "Wetlands done · Woodlands empty" on a step with a part per habitat: the label size at the body's weight in the muted colour, a space-2 after the title on its baseline, or on a line of their own under the title where they do not fit beside it (a long title on a phone), never past the panel (a long word breaks). They are drawn while the step is open (current or error, or every step of an open list, with fields or without) and stay as the step turns done in an open list; a folded step draws its summary or reason instead. They sit outside the heading, so the heading's name stays the title (and its status word) and a heading list reads the titles alone; an open list's region keeps the title as its name and is described by the words, read as a person enters it, and in a stepped list they follow the heading in reading order. Write your own separators inside the words; Scalewing draws only the gap after the title. The words share a skipped step's reason's muted colour, not its size: a reason is part of the title, these are a note beside it. Give a step its `titleDetail` from its first render: setting or clearing it later moves the heading into or out of the title line, which remounts it (and drops its focus if it has it). A blank `titleDetail` throws a `RangeError`. 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`.
|
|
109
110
|
|
|
110
111
|
```tsx
|
|
111
112
|
<Steps aria-label="Heron survey">
|
|
@@ -256,6 +257,8 @@ A chosen photo can show itself: `value.preview` is the address of a small image
|
|
|
256
257
|
|
|
257
258
|
`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). `unit` (opt-in) is a short muted word after the count inside its frame, such as "min" or "players": the frame (`sw-stepper-count`) draws the control surface, focus ring and the invalid, changed and disabled states, a press on the word focuses the count, and the count keeps its few-digit width with its digits against the word, so the frame never follows it. The word (`sw-stepper-unit`) is hidden from assistive technology; the count is read with it (`aria-valuetext` "90 min", none while the box is empty), and a `valueText` you pass wins. The default joins the count and the word as given, so a word that changes with the count ("players") reads "1 players": pass `valueText` in your language's right form ("1 player", "11 players"). A blank `unit` throws a `RangeError`. Bounds that are not whole numbers, `min` above `max`, or a `step` that is not above zero throw a `RangeError`.
|
|
258
259
|
|
|
260
|
+
`Stepper` `onCommit` (opt-in) is the finished count once per entry, for a page that saves as you go: it fires on blur, on Enter (a form round the count still submits), and `commitDelay` ms (default 600) after the last − / + press or step key (ArrowUp, ArrowDown, Home, End), each press restarting the wait; blur or Enter while it waits commits at once. Typed digits never commit by themselves. It gives the count you hold at that moment, `null` or outside the bounds included as `onChange` reports them, so check it before saving. It does not fire when that count is the one last committed, or, before the first commit, the one held before the first edit; a count you set while nothing waits (a server's answer) becomes the new baseline. A commit still waiting when the Stepper unmounts is made then. `onChange` is unchanged. A `commitDelay` that is negative or not finite throws a `RangeError`.
|
|
261
|
+
|
|
259
262
|
```tsx
|
|
260
263
|
<Stepper
|
|
261
264
|
label="Herons"
|
|
@@ -271,6 +274,8 @@ A chosen photo can show itself: `value.preview` is the address of a small image
|
|
|
271
274
|
|
|
272
275
|
`Select` is a labeled listbox menu: supply `label`, `options` (`value` and `label`), a controlled `value` and `onChange`; `size="xs"` and `labelVisuallyHidden` are for toolbar chrome, and `action` is a last listbox command that never becomes the value. The closed trigger matches `Field`: its text is centred in the control, it ends in the `Accordion` chevron, and it is as wide as its longest option (as a native select is), so it keeps its width when the value changes; past the available width the label ellipsizes. The option labels that size it are drawn by CSS from `data-label`, so the trigger's text content and accessible value are only the current label. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so a `Select` beside a `Field` lines up at the label and the control.
|
|
273
276
|
|
|
277
|
+
On a coarse pointer every `Select` size's trigger and each option in its open list are at least 44 px high (the md control height), as every `Button` is; type and padding keep the size's, so an `xs` Select in a toolbar or a table row is still easy to tap. A fine pointer keeps `xs` at 28 px.
|
|
278
|
+
|
|
274
279
|
`Select` `aria-labelledby`, in place of `label`, names the select by words the page already draws, such as a label above a choice that is a `SegmentedControl` on a wide screen and a `Select` on a phone. The select then draws no label of its own, hidden or shown, so a screen reader meets the words once (as the page's text, then as the combobox's name) instead of the page's words followed by the select's hidden copy of them; the open list is named by the same words. `required` still sets `aria-required` but draws no mark, since the page owns the words. `label` and `aria-labelledby` are exclusive: a blank `label` with no `aria-labelledby`, a blank `aria-labelledby`, or `aria-labelledby` with `label`, `labelVisuallyHidden` or `requiredMark` throws a `RangeError`. The id must name an element in the page while the select is shown: a missing one leaves the select unnamed, which neither the types nor the select can check.
|
|
275
280
|
|
|
276
281
|
```tsx
|
|
@@ -475,6 +480,8 @@ A `Button` that toggles something passes `aria-pressed`. The pressed button gets
|
|
|
475
480
|
|
|
476
481
|
`Dialog` is a modal `<dialog>` controlled by `open`. Escape, a backdrop press, a platform close request, and a `<form method="dialog">` submit (or a submitter with `formmethod="dialog"`) are each prevented and call `onClose`; none of them closes the dialog itself. A consumer `onKeyDown`, `onCancel`, `onSubmit` or `onPointerDown` that prevents the event vetoes that request. Escape in a search field that holds text clears the field first, and a descendant's own `cancel` (a dismissed file picker) is not a close request. Set `open` to false to close it, or keep it true (for example while a form is saving) and it stays shown. `onClose` is not called when `open` turns false, and it is required: a dialog that must not be dismissed passes a callback that keeps `open` true. The `title` is an `h3` in the title style by default; `titleLevel={2}` renders it as an `h2` in the same style, for a dialog whose own section labels are `h3` headings, so they read as parts of the dialog and not as its peers. The default stays `3` so existing dialogs keep their outline; any value other than `2` renders an `h3`. `sheetBelow="md"` makes the dialog a bottom sheet below `md`: docked to the bottom edge at the full viewport width, top corners rounded and bottom corners square, a `space-8` strip of backdrop above it, and bottom padding that clears `env(safe-area-inset-bottom)` (with `viewport-fit=cover`); it slides up on open unless reduced motion is asked for, and has no grabber, since there is no drag. From `md` up it is the centered dialog at its `size`. `closeLabel` (such as "Close") adds an icon-only ghost close button at the end of the title row, named by the label, which calls `onClose` like Escape; the title still names the dialog. The button is the dialog's first control, so the browser focuses it on open; give a field `autoFocus` to start there instead. Without `closeLabel` the dialog has no close button and its markup is unchanged.
|
|
477
482
|
|
|
483
|
+
`Dialog` `placement="end"` (opt-in; `center` is the default) is a side panel from md up: the window's full height at the inline end (the right, the left in right-to-left), its `size`'s width (md 32rem, lg 56rem, xl 65rem, always leaving a space-8 strip of backdrop), square corners and one hairline on its inner edge, sliding in from its edge unless reduced motion is asked for. Its title row, `closeLabel` button, `description`, scrolling content and `footer` (pinned to the bottom edge even when the content is short) are Dialog's, as are its focus trap (the native modal), Escape and backdrop requests to `onClose`, and focus returning to the opener. Below md it is the centred dialog, or the bottom sheet with `sheetBelow="md"` (the usual pairing). `appearance="banded"` gives it the solid surface. An unknown placement throws a `RangeError`.
|
|
484
|
+
|
|
478
485
|
A sheet over a page takes a fuller frame. `icon` is a consumer glyph or tile before the title (hidden from assistive technology) and `description` a muted line under the title that also describes the dialog (unless you pass `aria-describedby`). `footer` holds the actions, pinned to the dialog's bottom edge under a hairline while the content scrolls, and `summary` (ActionBar's `{ label, value, detail?, tone? }`) puts a labelled figure at the footer's start in a polite live region. `size="xl"` (65rem) holds a six-column count grid with its labels and total. `role="alertdialog"` is for a warning that interrupts, such as an idle sign-out: give its safe action `autoFocus` (the browser otherwise focuses the first footer button), and point `aria-describedby` at the warning in its body when that is not the `description`. `appearance="banded"` (default `"glass"`) draws the dialog on the solid surface with the medium elevation instead of the frosted panel: its title row is a full-width band over a hairline (a lone title too), the footer the band it already is, and the close button a circle on the quiet muted fill in the text color, keeping its touch target; forced colors outline the dialog and keep the hairlines. `status` is one short line in the footer after the summary and before the actions, in the same polite live region: muted caption words, or a `Badge` and a short line in one `span` (a status alone makes a footer; blank words or `false` leave it out). `canvas="grouped"` (default `"default"`) fills the dialog with the grouped canvas grey that `ThemeProvider canvas="grouped"` paints, solid with no blur, so `Card`s inside stand apart; a banded title row and the footer keep the surface. `sheetGrabber` (with `sheetBelow`, which it needs) draws a grabber, a short rounded bar centered at the top of the bottom sheet, hidden from assistive technology; it stays at the top while the content scrolls, does not drag, and is not drawn from the breakpoint up. An unknown `size`, `role`, `appearance` or `canvas`, a blank `description`, an invalid `summary`, or `sheetGrabber` without `sheetBelow` throws a `RangeError`.
|
|
479
486
|
|
|
480
487
|
```tsx
|
|
@@ -557,6 +564,8 @@ A toast can also be a corner card that confirms a save: `placement="top-end"` pu
|
|
|
557
564
|
|
|
558
565
|
`Checkbox` is a controlled native checkbox for one independent form or confirmation choice. `checked` and `onCheckedChange` own its state. The consumer supplies `error` after validation to describe and style an invalid choice (associated by `aria-describedby` with `aria-invalid` and announced from a polite live region, as `Field`'s error is, never an alert); `required` retains native form semantics.
|
|
559
566
|
|
|
567
|
+
`Checkbox` `labelVisuallyHidden` (opt-in) keeps `label` as the checkbox's name but does not draw it, for a box that stands alone, such as a table's select column ("Select Mallard"); the box is then its own target, its drawn size (24 px) on a fine pointer and at least 44 × 44 px on a coarse one. `indeterminate` (opt-in) is the mixed state, such as "Select all" while only some rows are chosen: a dash on the accent fill (Highlight in forced colours), the native `indeterminate` property and `aria-checked="mixed"`, so Playwright's `toBeChecked({ indeterminate: true })` and `getByRole('checkbox', { checked: 'mixed' })` find it (Testing Library's `checked` query ignores the mixed state of a native checkbox; assert `aria-checked="mixed"` or the `indeterminate` property there). It wins over `checked` in the drawing and the state read out. A press reports `!checked` (with `checked` false: `true`, choose all); the box stays mixed until the props change.
|
|
568
|
+
|
|
560
569
|
`RadioGroup` is a controlled fieldset of native radio choices. Supply a unique nonempty `value` for each option, one selected `value` or an empty value for no selection, a `legend`, and `onChange`. Long labels wrap in a vertical group. The consumer provides localized option labels and validation `error`; `required` keeps native form semantics. An option's optional `icon` (a `ReactNode`, such as a Lucide glyph) sits between the radio and the label, in the text color and the option's gap; it is `aria-hidden`, so the radio's accessible name stays the `label` text, and a press on it chooses the option as the label does.
|
|
561
570
|
|
|
562
571
|
```tsx
|
|
@@ -754,6 +763,8 @@ The layout follows the window, with generated media queries only. From 75rem (12
|
|
|
754
763
|
|
|
755
764
|
`TableRow pressable` (opt-in) makes the whole row the pointer target of its one action, such as a search result that is picked: a plain primary press anywhere on it calls `click()` on the row's first link or button, so the action's own handler runs. Left alone: a modifier key or non-primary button, a press already handled, a press on another control, a press that ends a text selection (text stays selectable), and a disabled or missing action. Keep one control in the row: a control placed before the action (an icon button, say) would take the row's press. The row is not a control (no role, no tab stop), so keyboard and screen reader users use the action itself. It takes the pointer cursor and the quiet fill while hovered, pressed (a brief flash on touch) or while its action has keyboard focus (a `Highlight` outline in forced colours), and underlines its action on hover (`sw-table-row-press`). A consumer `onClick` runs once per press. Each click of a double click presses the action, as a native button's would, so keep it safe to repeat. `TableCell onPress` (a body cell or a row header in `TableBody`) makes the cell's children that action: a button in the cell's own text style at weight 600, underlined on hover and focus, with the accent focus ring (`sw-table-action`); a non-function value or one on a sortable header throws a `TypeError`.
|
|
756
765
|
|
|
766
|
+
`TableSelectCell` is a table's select column: a cell (`as="td"`, the default, in a body row, or `as="th"` in a `TableHeader` row, which gets `scope="col"`) holding one `Checkbox` with `labelVisuallyHidden`. Its props are `label` (the box's name, not drawn: "Select all", "Select Mallard"), `checked`, `indeterminate`, `onCheckedChange` and `disabled`. The column is as wide as the box (`sw-table-select`), and 44 px wide on a coarse pointer, where the box is the 44 × 44 px target; a fixed table sizes columns from its header row, so put one in the header too. Mark a chosen row with `TableRow selected`; a press on the box is left alone by `TableRow pressable`. The consumer keeps the chosen set and works out "Select all"'s `checked` and `indeterminate`. A blank label or an unknown `as` throws a `RangeError`.
|
|
767
|
+
|
|
757
768
|
```tsx
|
|
758
769
|
<TableRow pressable>
|
|
759
770
|
<TableCell onPress={() => pick('Red fox')}>Red fox</TableCell>
|
|
@@ -965,6 +976,8 @@ The consumer needs no PDF.js dependency of its own. With another bundler, build
|
|
|
965
976
|
|
|
966
977
|
`MultiSelect` is several choices from a short list behind one trigger, such as which kinds a list shows. The trigger reads the muted `label` and then `summary` (your words for what is chosen, such as "All entries" or "2 services") with a chevron; it opens a panel named by the label with `allLabel` and `clearLabel` commands over one checkbox per option, each with an optional `icon` (use a small `GlyphTile`, never a bare dot) and a hairline before an option with `separatorBefore`. `onChange` reports the checked values in the options' order. It is a disclosure of native checkboxes, not a listbox: the first checkbox takes focus as it opens, Escape closes it and returns focus to the trigger, focus or a press outside closes it, and disabling it while open closes it. Blank words, no or duplicate options, a value that is not an option, or an unknown `align` throw a `RangeError`.
|
|
967
978
|
|
|
979
|
+
`PasteRows` is rows pasted from a spreadsheet: a `Field` text area (`label`, `description`, `error`, `required`, `disabled`, `placeholder`, `rows` default 6, spell check off) for the controlled `value` (`onValueChange`), and, once something is pasted, a polite status line (`countLabel(n)`, such as "Found 2 columns") followed by one `Select` `size="xs"` per found column, its label visually hidden (`columnLabel(position)`, default "Column 1", a 1-based position), offering `columns` (`{ id, label }[]`, such as Team, Division, Don’t use). `mapping` holds one column id per found column, in order; a change calls `onMappingChange` with the whole new array (a missing entry, or an id not in `columns`, counts as the first choice). It parses rows and cells only, with the exported pure `parsePastedRows(text, delimiter?)` (`{ rows, columnCount, delimiter }`): rows on CR, LF or CRLF, cells on tabs or commas (`delimiter`: `auto`, tab when the text holds a tab, else comma; `tab`; `comma`; pass `tab` when one column may hold commas), quoted cells as spreadsheets write them (a cell that starts with a quote runs to the next closing quote, across tabs and line breaks), cells trimmed, blank rows dropped, short rows padded with `''`. Call it on the same `value` to derive the default mapping and the preview; what the columns mean, headers, checks and the preview are the consumer's. Query the text area by its label (role `textbox`), each column by `getByRole('combobox', { name: 'Column 1' })`, and the count by role `status`. While `disabled` the text area is disabled and the column Selects are left out (the count line stays). A blank label, no columns, a blank or repeated column id or label, or `rows` under 1 throws a `RangeError`.
|
|
980
|
+
|
|
968
981
|
```tsx
|
|
969
982
|
<MultiSelect
|
|
970
983
|
label="Show"
|
package/dist/components/Box.d.ts
CHANGED
|
@@ -24,6 +24,19 @@ export type BoxProps = SpacingProps & VisibilityProps & ColumnSpanProps & HTMLAt
|
|
|
24
24
|
* text lighter than 600. Only for `as="a"`; an unknown value throws.
|
|
25
25
|
*/
|
|
26
26
|
link?: BoxLink;
|
|
27
|
+
/**
|
|
28
|
+
* A `Box as="a"` that opens in a new tab: it sets `target="_blank"`,
|
|
29
|
+
* adds `noopener noreferrer` to `rel`, draws a decorative arrow (↗)
|
|
30
|
+
* after its words and reads `newTabLabel` after its name. Only for
|
|
31
|
+
* `as="a"`; it needs `newTabLabel`.
|
|
32
|
+
*/
|
|
33
|
+
external?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The hidden words read after an external link's name, in your
|
|
36
|
+
* language, such as "(opens in a new tab)". Required with `external`,
|
|
37
|
+
* refused without it.
|
|
38
|
+
*/
|
|
39
|
+
newTabLabel?: string;
|
|
27
40
|
};
|
|
28
41
|
export type BoxBorder = BoxBorderValue;
|
|
29
42
|
export type BoxLink = BoxLinkAppearance;
|
|
@@ -45,4 +58,17 @@ export declare const Box: import("react").ForwardRefExoticComponent<SpacingProps
|
|
|
45
58
|
* text lighter than 600. Only for `as="a"`; an unknown value throws.
|
|
46
59
|
*/
|
|
47
60
|
link?: BoxLink;
|
|
61
|
+
/**
|
|
62
|
+
* A `Box as="a"` that opens in a new tab: it sets `target="_blank"`,
|
|
63
|
+
* adds `noopener noreferrer` to `rel`, draws a decorative arrow (↗)
|
|
64
|
+
* after its words and reads `newTabLabel` after its name. Only for
|
|
65
|
+
* `as="a"`; it needs `newTabLabel`.
|
|
66
|
+
*/
|
|
67
|
+
external?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The hidden words read after an external link's name, in your
|
|
70
|
+
* language, such as "(opens in a new tab)". Required with `external`,
|
|
71
|
+
* refused without it.
|
|
72
|
+
*/
|
|
73
|
+
newTabLabel?: string;
|
|
48
74
|
} & import("react").RefAttributes<HTMLElement>>;
|
package/dist/components/Box.js
CHANGED
|
@@ -5,10 +5,14 @@ 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 { assertBoxExternal, externalLinkContent, externalLinkRel, } from './box/external-link.js';
|
|
8
9
|
import { tabStop } from '../tab-stop.js';
|
|
9
10
|
import { visibilityClassNames, } from '../visibility-classes.js';
|
|
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) {
|
|
11
|
+
export const Box = forwardRef(function Box({ as = 'div', background, border, children, className, columnSpan, external, hideBelow, hideFrom, link, newTabLabel, padding, paddingBottom, paddingLeft, paddingRight, paddingTop, paddingX, paddingY, radius, rel, style, tabIndex, target, ...rest }, ref) {
|
|
11
12
|
assertBoxLink(link, as);
|
|
13
|
+
assertBoxExternal({ as, external, newTabLabel });
|
|
14
|
+
// assertBoxExternal has made sure an external link has its words.
|
|
15
|
+
const opensNewTab = external === true && newTabLabel !== undefined;
|
|
12
16
|
const Component = as;
|
|
13
17
|
// A link with a destination is a tab stop in Safari too (see tabStop).
|
|
14
18
|
const linkTabStop = as === 'a' && rest.href !== undefined ? tabStop : undefined;
|
|
@@ -26,5 +30,5 @@ export const Box = forwardRef(function Box({ as = 'div', background, border, cla
|
|
|
26
30
|
: undefined,
|
|
27
31
|
borderRadius: radius ? `var(--sw-radius-${radius})` : undefined,
|
|
28
32
|
...style,
|
|
29
|
-
}, tabIndex: tabIndex ?? linkTabStop, ...rest }));
|
|
33
|
+
}, tabIndex: tabIndex ?? linkTabStop, ...rest, rel: opensNewTab ? externalLinkRel(rel) : rel, target: opensNewTab ? '_blank' : target, children: opensNewTab ? externalLinkContent(children, newTabLabel) : children }));
|
|
30
34
|
});
|
|
@@ -6,5 +6,22 @@ export type CheckboxProps = {
|
|
|
6
6
|
required?: boolean;
|
|
7
7
|
description?: string;
|
|
8
8
|
error?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Keeps `label` as the checkbox's name but does not draw it, so the box
|
|
11
|
+
* stands alone, as in a table's select column ("Select Mallard"). The
|
|
12
|
+
* box is then its own target: its size on a fine pointer, at least
|
|
13
|
+
* 44 × 44 px on a coarse one. Say what the box picks in words the page
|
|
14
|
+
* draws nearby (the row's name), not by position alone.
|
|
15
|
+
*/
|
|
16
|
+
labelVisuallyHidden?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The mixed state, such as "Select all" while only some rows are
|
|
19
|
+
* chosen: a dash in place of the check, the native `indeterminate`
|
|
20
|
+
* property and `aria-checked="mixed"`. It wins over `checked` in the
|
|
21
|
+
* drawing and the state read out. A press reports `!checked` (so with
|
|
22
|
+
* `checked` false, `true`: choose all) and the consumer decides what
|
|
23
|
+
* stays mixed.
|
|
24
|
+
*/
|
|
25
|
+
indeterminate?: boolean;
|
|
9
26
|
};
|
|
10
|
-
export declare function Checkbox({ label, checked, onCheckedChange, disabled, required, description, error, }: CheckboxProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Checkbox({ label, checked, onCheckedChange, disabled, required, description, error, labelVisuallyHidden, indeterminate, }: CheckboxProps): import("react").JSX.Element;
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId } from 'react';
|
|
3
|
+
import { useId, useLayoutEffect, useRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
4
5
|
import { CheckboxControl } from './checkbox/CheckboxControl.js';
|
|
5
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
|
-
export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, }) {
|
|
7
|
+
export function Checkbox({ label, checked, onCheckedChange, disabled = false, required = false, description, error, labelVisuallyHidden = false, indeterminate = false, }) {
|
|
7
8
|
const descriptionId = useId();
|
|
8
9
|
const errorId = useId();
|
|
10
|
+
const inputRef = useRef(null);
|
|
9
11
|
const describedBy = [
|
|
10
12
|
description ? descriptionId : null,
|
|
11
13
|
error ? errorId : null,
|
|
12
14
|
]
|
|
13
15
|
.filter((id) => id !== null)
|
|
14
16
|
.join(' ');
|
|
15
|
-
|
|
17
|
+
// A press clears the native property, so set it again after every render.
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
if (inputRef.current)
|
|
20
|
+
inputRef.current.indeterminate = indeterminate;
|
|
21
|
+
});
|
|
22
|
+
return (_jsxs("div", { className: "sw-checkbox", children: [_jsxs("label", { className: cx('sw-checkbox-label', labelVisuallyHidden && 'sw-checkbox-label-hidden'), children: [_jsx(CheckboxControl, { ref: inputRef, "aria-checked": indeterminate ? 'mixed' : undefined, "aria-describedby": describedBy || undefined, "aria-invalid": Boolean(error), checked: checked, disabled: disabled, onChange: (event) => {
|
|
23
|
+
const input = event.currentTarget;
|
|
24
|
+
// The press cleared the mixed state; keep it until the props say.
|
|
25
|
+
input.indeterminate = indeterminate;
|
|
16
26
|
if (!disabled)
|
|
17
|
-
onCheckedChange(
|
|
18
|
-
}, required: required }), _jsx("span", { className:
|
|
27
|
+
onCheckedChange(input.checked);
|
|
28
|
+
}, required: required }), _jsx("span", { className: labelVisuallyHidden ? 'sw-sr-only' : 'sw-checkbox-text', children: label })] }), description ? (_jsx("span", { className: "sw-checkbox-description", id: descriptionId, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-checkbox-error", id: errorId, message: error })] }));
|
|
19
29
|
}
|
|
@@ -2,10 +2,12 @@ import { type DialogHTMLAttributes, type ReactNode } from 'react';
|
|
|
2
2
|
import { type Breakpoint } from '../css/breakpoints.js';
|
|
3
3
|
import { type DialogAppearance, dialogAppearances } from '../css/css-dialog-banded.js';
|
|
4
4
|
import { type ThemeCanvas } from '../css/css-canvas-grouped.js';
|
|
5
|
+
import { type DialogPlacement, dialogPlacements } from '../css/css-dialog-end.js';
|
|
5
6
|
import { type ActionBarSummary } from './action-bar/ActionBarSummary.js';
|
|
6
7
|
import { type DialogRole, type DialogSize, dialogSizes } from './dialog/validate.js';
|
|
7
8
|
export { dialogAppearances, type DialogAppearance };
|
|
8
9
|
export { dialogSizes, type DialogRole, type DialogSize };
|
|
10
|
+
export { dialogPlacements, type DialogPlacement };
|
|
9
11
|
/**
|
|
10
12
|
* The heading level of the dialog's title. It changes only the element (and
|
|
11
13
|
* so the outline a screen reader reads), never the title's look.
|
|
@@ -98,6 +100,17 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
|
|
|
98
100
|
* footer stay on the surface. `default` keeps the appearance's fill.
|
|
99
101
|
*/
|
|
100
102
|
canvas?: ThemeCanvas;
|
|
103
|
+
/**
|
|
104
|
+
* `end` makes the dialog a side panel from md up: the window's full
|
|
105
|
+
* height at the inline end (the right, the left in right-to-left), its
|
|
106
|
+
* `size`'s width, square corners and a hairline on its inner edge, with
|
|
107
|
+
* the same title row, close button, scrolling content and footer pinned
|
|
108
|
+
* to the bottom edge. Below md it is the centred dialog, or the bottom
|
|
109
|
+
* sheet with `sheetBelow="md"`. It is the same modal: focus stays inside,
|
|
110
|
+
* Escape and a backdrop press ask `onClose`, and focus returns to the
|
|
111
|
+
* opener on close. `center` (the default) is unchanged.
|
|
112
|
+
*/
|
|
113
|
+
placement?: DialogPlacement;
|
|
101
114
|
children: ReactNode;
|
|
102
115
|
};
|
|
103
|
-
export declare function Dialog({ 'aria-describedby': describedBy, appearance, canvas, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, sheetGrabber, size, status, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
116
|
+
export declare function Dialog({ 'aria-describedby': describedBy, appearance, canvas, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, placement, sheetBelow, sheetGrabber, size, status, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
@@ -5,6 +5,7 @@ import { cx } from '../class-names.js';
|
|
|
5
5
|
import { dialogSheetClass } from '../css/css-dialog.js';
|
|
6
6
|
import { dialogAppearances, dialogBandedClass, } from '../css/css-dialog-banded.js';
|
|
7
7
|
import { dialogCanvasClass } from '../css/css-dialog-canvas.js';
|
|
8
|
+
import { dialogEndClass, dialogPlacements, } from '../css/css-dialog-end.js';
|
|
8
9
|
import { dialogGrabberClass, dialogHasGrabberClass, } from '../css/css-dialog-grabber.js';
|
|
9
10
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
10
11
|
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
@@ -14,11 +15,13 @@ import { assertDialogProps, dialogSizes, } from './dialog/validate.js';
|
|
|
14
15
|
import { Stack } from './Stack.js';
|
|
15
16
|
export { dialogAppearances };
|
|
16
17
|
export { dialogSizes };
|
|
17
|
-
export
|
|
18
|
+
export { dialogPlacements };
|
|
19
|
+
export function Dialog({ 'aria-describedby': describedBy, appearance = 'glass', canvas = 'default', children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, placement = 'center', sheetBelow, sheetGrabber = false, size = 'md', status, title, titleLevel = 3, ...rest }) {
|
|
18
20
|
assertDialogProps({
|
|
19
21
|
appearance,
|
|
20
22
|
canvas,
|
|
21
23
|
description,
|
|
24
|
+
placement,
|
|
22
25
|
role,
|
|
23
26
|
sheetBelow,
|
|
24
27
|
sheetGrabber,
|
|
@@ -41,5 +44,5 @@ export function Dialog({ 'aria-describedby': describedBy, appearance = 'glass',
|
|
|
41
44
|
onPointerDown,
|
|
42
45
|
onSubmit,
|
|
43
46
|
});
|
|
44
|
-
return (_jsxs("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), appearance === 'banded' && dialogBandedClass, canvas === 'grouped' && dialogCanvasClass, sheetGrabber && dialogHasGrabberClass, ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: [sheetGrabber ? (_jsx("span", { "aria-hidden": "true", className: dialogGrabberClass })) : null, _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { banded: appearance === 'banded', closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { status: hasStatus ? status : undefined, summary: summary, children: footer })) : null] })] }));
|
|
47
|
+
return (_jsxs("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), appearance === 'banded' && dialogBandedClass, canvas === 'grouped' && dialogCanvasClass, sheetGrabber && dialogHasGrabberClass, placement === 'end' && dialogEndClass, ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: [sheetGrabber ? (_jsx("span", { "aria-hidden": "true", className: dialogGrabberClass })) : null, _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { banded: appearance === 'banded', closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { status: hasStatus ? status : undefined, summary: summary, children: footer })) : null] })] }));
|
|
45
48
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type PastedRowsDelimiter } from './paste-rows/parse-pasted-rows.js';
|
|
2
|
+
export type PasteRowsColumn = {
|
|
3
|
+
/** What `mapping` holds for a found column given this meaning. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** The choice's words in each column's Select, such as "Team" or "Don’t use". */
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
export type PasteRowsProps = {
|
|
9
|
+
/** The text area's label, such as "Paste here". */
|
|
10
|
+
label: string;
|
|
11
|
+
/** A muted line under the label, read as the text area's description. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** The pasted text, as typed. */
|
|
14
|
+
value: string;
|
|
15
|
+
onValueChange: (value: string) => void;
|
|
16
|
+
/** The meanings each found column can be given, in menu order. */
|
|
17
|
+
columns: readonly PasteRowsColumn[];
|
|
18
|
+
/**
|
|
19
|
+
* One `columns` id per found column, in order. Derive it from the same
|
|
20
|
+
* `value` and `delimiter` with `parsePastedRows` (its `columnCount`), so a new column
|
|
21
|
+
* gets its default in the same render. A missing entry, or an id not
|
|
22
|
+
* in `columns`, counts as the first choice: it is drawn so and filled in
|
|
23
|
+
* on the next change.
|
|
24
|
+
*/
|
|
25
|
+
mapping: readonly string[];
|
|
26
|
+
/** Called with the whole new mapping when one column's Select changes. */
|
|
27
|
+
onMappingChange: (mapping: string[]) => void;
|
|
28
|
+
/** The line before the Selects, such as `(n) => \`Found ${n} columns\``. */
|
|
29
|
+
countLabel: (count: number) => string;
|
|
30
|
+
/**
|
|
31
|
+
* The name of the Select for the column at a 1-based position, not
|
|
32
|
+
* drawn: "Column 1" (the default, English).
|
|
33
|
+
*/
|
|
34
|
+
columnLabel?: (position: number) => string;
|
|
35
|
+
/** How cells are split (`parsePastedRows`); default `auto`. */
|
|
36
|
+
delimiter?: PastedRowsDelimiter;
|
|
37
|
+
/** The text area's visible rows; default 6. */
|
|
38
|
+
rows?: number;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Disables the text area and leaves the column Selects out (the count
|
|
43
|
+
* line stays), so nothing changes while the consumer saves.
|
|
44
|
+
*/
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A text area for rows pasted from a spreadsheet (cells split on tabs or
|
|
50
|
+
* commas) and, once something is pasted, a polite count line and one
|
|
51
|
+
* small Select per found column choosing what that column means. It parses
|
|
52
|
+
* only rows and cells (`parsePastedRows`); the consumer gives the
|
|
53
|
+
* meanings, keeps the mapping, checks the values and draws the preview.
|
|
54
|
+
*/
|
|
55
|
+
export declare function PasteRows({ columnLabel, columns, countLabel, delimiter, description, disabled, error, label, mapping, onMappingChange, onValueChange, placeholder, required, rows, value, }: PasteRowsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Field } from './Field.js';
|
|
4
|
+
import { Inline } from './Inline.js';
|
|
5
|
+
import { parsePastedRows, } from './paste-rows/parse-pasted-rows.js';
|
|
6
|
+
import { Select } from './Select.js';
|
|
7
|
+
import { Stack } from './Stack.js';
|
|
8
|
+
import { Text } from './Text.js';
|
|
9
|
+
const defaultColumnLabel = (position) => `Column ${position}`;
|
|
10
|
+
function assertPasteRowsProps({ columns, label, rows, }) {
|
|
11
|
+
if (typeof label !== 'string' || !label.trim())
|
|
12
|
+
throw new RangeError('PasteRows label must not be empty');
|
|
13
|
+
if (columns.length === 0)
|
|
14
|
+
throw new RangeError('PasteRows needs at least one column');
|
|
15
|
+
const ids = new Set();
|
|
16
|
+
const labels = new Set();
|
|
17
|
+
for (const column of columns) {
|
|
18
|
+
if (!column.id.trim() || !column.label.trim())
|
|
19
|
+
throw new RangeError('PasteRows column id and label must not be empty');
|
|
20
|
+
if (ids.has(column.id))
|
|
21
|
+
throw new RangeError(`Repeated PasteRows column id: ${column.id}`);
|
|
22
|
+
if (labels.has(column.label))
|
|
23
|
+
throw new RangeError(`Repeated PasteRows column label: ${column.label}`);
|
|
24
|
+
ids.add(column.id);
|
|
25
|
+
labels.add(column.label);
|
|
26
|
+
}
|
|
27
|
+
if (rows !== undefined && (!Number.isInteger(rows) || rows < 1))
|
|
28
|
+
throw new RangeError('PasteRows rows must be a whole number from 1');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A text area for rows pasted from a spreadsheet (cells split on tabs or
|
|
32
|
+
* commas) and, once something is pasted, a polite count line and one
|
|
33
|
+
* small Select per found column choosing what that column means. It parses
|
|
34
|
+
* only rows and cells (`parsePastedRows`); the consumer gives the
|
|
35
|
+
* meanings, keeps the mapping, checks the values and draws the preview.
|
|
36
|
+
*/
|
|
37
|
+
export function PasteRows({ columnLabel = defaultColumnLabel, columns, countLabel, delimiter = 'auto', description, disabled = false, error, label, mapping, onMappingChange, onValueChange, placeholder, required = false, rows = 6, value, }) {
|
|
38
|
+
assertPasteRowsProps({ columns, label, rows });
|
|
39
|
+
const { columnCount } = parsePastedRows(value, delimiter);
|
|
40
|
+
const options = columns.map((column) => ({
|
|
41
|
+
value: column.id,
|
|
42
|
+
label: column.label,
|
|
43
|
+
}));
|
|
44
|
+
// A missing entry, or an id not in `columns`, counts as the first choice.
|
|
45
|
+
const chosenId = (position) => {
|
|
46
|
+
const id = mapping[position];
|
|
47
|
+
return columns.some((column) => column.id === id) ? id : columns[0].id;
|
|
48
|
+
};
|
|
49
|
+
const choose = (index, id) => {
|
|
50
|
+
const next = Array.from({ length: columnCount }, (_, position) => chosenId(position));
|
|
51
|
+
next[index] = id;
|
|
52
|
+
onMappingChange(next);
|
|
53
|
+
};
|
|
54
|
+
return (_jsxs(Stack, { gap: 2, children: [_jsx(Field, { description: description, error: error, label: label, required: required, children: _jsx("textarea", { disabled: disabled, onChange: (event) => onValueChange(event.currentTarget.value), placeholder: placeholder, required: required, rows: rows, spellCheck: false, value: value }) }), _jsxs(Inline, { align: "center", gap: 2, wrap: true, children: [_jsx(Text, { color: "muted", role: "status", variant: "caption", children: columnCount > 0 ? countLabel(columnCount) : '' }), (disabled ? [] : Array.from({ length: columnCount })).map((_, index) => (_jsx(Select, { label: columnLabel(index + 1), labelVisuallyHidden: true, onChange: (id) => choose(index, id), options: options, size: "xs", value: chosenId(index) }, index)))] })] }));
|
|
55
|
+
}
|
|
@@ -1,69 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export type StepperWidth
|
|
3
|
-
export declare const stepperLimitAppearances: readonly ["faded", "plain"];
|
|
4
|
-
/**
|
|
5
|
-
* How − looks at `min` and + at `max`, where a press cannot move the count:
|
|
6
|
-
* `faded` (the default) fades the button as any disabled control; `plain`
|
|
7
|
-
* keeps its usual look. Either way the button is disabled.
|
|
8
|
-
*/
|
|
9
|
-
export type StepperLimitAppearance = (typeof stepperLimitAppearances)[number];
|
|
10
|
-
export type StepperProps = {
|
|
11
|
-
/** Names the count. Keep it visible unless headers around it say it (`labelVisuallyHidden`). */
|
|
12
|
-
label: string;
|
|
13
|
-
labelVisuallyHidden?: boolean;
|
|
14
|
-
/** The count, or `null` for an empty box (none). */
|
|
15
|
-
value: number | null;
|
|
16
|
-
/** A whole count, or `null` when the box is emptied. Typed counts are reported as typed, even outside the bounds. */
|
|
17
|
-
onChange: (value: number | null) => void;
|
|
18
|
-
/** The lowest count − reaches; defaults to 0. */
|
|
19
|
-
min?: number;
|
|
20
|
-
/** The highest count + reaches; no upper bound when left out. */
|
|
21
|
-
max?: number;
|
|
22
|
-
/** How much one press, or one arrow key, moves the count; defaults to 1. */
|
|
23
|
-
step?: number;
|
|
24
|
-
/** The − button's name, such as "One less". */
|
|
25
|
-
decrementLabel: string;
|
|
26
|
-
/** The + button's name, such as "One more". */
|
|
27
|
-
incrementLabel: string;
|
|
28
|
-
/**
|
|
29
|
-
* A message under the control, as `Field`'s: a polite live region linked by
|
|
30
|
-
* `aria-describedby`, with `aria-invalid` and the danger border. An empty
|
|
31
|
-
* string is no error.
|
|
32
|
-
*/
|
|
33
|
-
error?: string;
|
|
34
|
-
/** `Field`'s changed look (the accent border and tint) for a count someone changed, or a suggestion. `error` wins. */
|
|
35
|
-
changed?: boolean;
|
|
36
|
-
disabled?: boolean;
|
|
37
|
-
/** Readable and focusable, never changed: the buttons are hidden and keep their room. */
|
|
38
|
-
readOnly?: boolean;
|
|
39
|
-
/**
|
|
40
|
-
* Says the count in words for assistive technology (`aria-valuetext`),
|
|
41
|
-
* such as "12 straps". It wins over the count and `unit` read together.
|
|
42
|
-
*/
|
|
43
|
-
valueText?: string;
|
|
44
|
-
/**
|
|
45
|
-
* A short muted word after the count, inside its frame, such as "min".
|
|
46
|
-
* Without `valueText` the count is read with it ("90 min"), word for
|
|
47
|
-
* word: a unit that changes with the count ("players") reads "1 players",
|
|
48
|
-
* so pass `valueText` with the right form ("1 player"). Not blank.
|
|
49
|
-
*/
|
|
50
|
-
unit?: string;
|
|
51
|
-
/**
|
|
52
|
-
* Ids of elements that describe the count, such as the value it adds up
|
|
53
|
-
* to; read before an `error`'s message.
|
|
54
|
-
*/
|
|
55
|
-
'aria-describedby'?: string;
|
|
56
|
-
/** Shown in an empty box, such as "0". */
|
|
57
|
-
placeholder?: string;
|
|
58
|
-
name?: string;
|
|
59
|
-
width?: StepperWidth;
|
|
60
|
-
/**
|
|
61
|
-
* `plain` keeps − at `min` and + at `max` in their usual look instead of
|
|
62
|
-
* fading them (`faded`, the default). The button is still disabled, and a
|
|
63
|
-
* disabled or read-only Stepper is unchanged.
|
|
64
|
-
*/
|
|
65
|
-
limitAppearance?: StepperLimitAppearance;
|
|
66
|
-
};
|
|
1
|
+
import { type StepperProps } from './stepper/stepper-props.js';
|
|
2
|
+
export { stepperLimitAppearances, type StepperLimitAppearance, type StepperProps, type StepperWidth, } from './stepper/stepper-props.js';
|
|
67
3
|
/**
|
|
68
4
|
* A whole-number count between − and + buttons that can also be typed: one
|
|
69
5
|
* tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
|
|
@@ -6,7 +6,9 @@ import { assertStepperBounds, assertStepperUnit, readStepperEntry, stepperMoves,
|
|
|
6
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
7
7
|
import { FieldLabelText } from './FieldLabelText.js';
|
|
8
8
|
import { StepperCountFrame } from './stepper/StepperCountFrame.js';
|
|
9
|
-
|
|
9
|
+
import { stepperLimitAppearances, } from './stepper/stepper-props.js';
|
|
10
|
+
import { assertStepperCommitDelay, defaultStepperCommitDelay, useStepperCommit, } from './stepper/use-stepper-commit.js';
|
|
11
|
+
export { stepperLimitAppearances, } from './stepper/stepper-props.js';
|
|
10
12
|
/** Keeps focus where it was (the count, or nowhere) when a button is pressed. */
|
|
11
13
|
function keepFocus(event) {
|
|
12
14
|
event.preventDefault();
|
|
@@ -16,10 +18,11 @@ function keepFocus(event) {
|
|
|
16
18
|
* tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
|
|
17
19
|
* the buttons as pointer targets beside it. − stops at `min`, + at `max`.
|
|
18
20
|
*/
|
|
19
|
-
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, unit, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
|
|
21
|
+
export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, onCommit, commitDelay = defaultStepperCommitDelay, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, unit, 'aria-describedby': describedBy, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
|
|
20
22
|
const bounds = { min, max, step };
|
|
21
23
|
assertStepperBounds(bounds);
|
|
22
24
|
assertStepperUnit(unit);
|
|
25
|
+
assertStepperCommitDelay(commitDelay);
|
|
23
26
|
if (!stepperLimitAppearances.includes(limitAppearance))
|
|
24
27
|
throw new RangeError(`Unknown Stepper limitAppearance: ${String(limitAppearance)}`);
|
|
25
28
|
if (value !== null && !Number.isSafeInteger(value))
|
|
@@ -28,6 +31,7 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
28
31
|
const labelId = useId();
|
|
29
32
|
const errorId = useId();
|
|
30
33
|
const [draft, setDraft] = useState(null);
|
|
34
|
+
const commit = useStepperCommit(value, onCommit, commitDelay);
|
|
31
35
|
const allowsNegative = min < 0;
|
|
32
36
|
const moves = stepperMoves(value, bounds, disabled || readOnly);
|
|
33
37
|
// The typed text stays while it means the count the consumer holds, so
|
|
@@ -42,6 +46,7 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
42
46
|
if (next === null)
|
|
43
47
|
return;
|
|
44
48
|
setDraft(null);
|
|
49
|
+
commit.stepped();
|
|
45
50
|
onChange(next);
|
|
46
51
|
}
|
|
47
52
|
function onType(event) {
|
|
@@ -52,8 +57,10 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
52
57
|
return;
|
|
53
58
|
setDraft(next);
|
|
54
59
|
const count = entry.kind === 'count' ? entry.value : null;
|
|
55
|
-
if (count
|
|
56
|
-
|
|
60
|
+
if (count === value)
|
|
61
|
+
return;
|
|
62
|
+
commit.edited();
|
|
63
|
+
onChange(count);
|
|
57
64
|
}
|
|
58
65
|
/** ArrowUp and ArrowDown step; Home and End go to min and a finite max. */
|
|
59
66
|
function keyTarget(key) {
|
|
@@ -68,6 +75,9 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
68
75
|
return undefined;
|
|
69
76
|
}
|
|
70
77
|
function onKeyDown(event) {
|
|
78
|
+
// Enter finishes the entry; a form round the count still submits.
|
|
79
|
+
if (event.key === 'Enter')
|
|
80
|
+
commit.finish();
|
|
71
81
|
const target = keyTarget(event.key);
|
|
72
82
|
if (target === undefined)
|
|
73
83
|
return;
|
|
@@ -83,5 +93,8 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
83
93
|
.join(' ') || undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": stepperValueText(value, unit, valueText), autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
|
|
84
94
|
// iOS's numeric pad has no minus key, so a range below zero takes
|
|
85
95
|
// the text keyboard.
|
|
86
|
-
inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () =>
|
|
96
|
+
inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => {
|
|
97
|
+
setDraft(null);
|
|
98
|
+
commit.finish();
|
|
99
|
+
}, 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 })] }));
|
|
87
100
|
});
|
|
@@ -3,6 +3,7 @@ import { type TableLayout, type TableStickyHeader, type TableVerticalAlign } fro
|
|
|
3
3
|
import { type TableMaxHeight } from './table/table-max-height.js';
|
|
4
4
|
export { TableCell, type TableCellProps, type TableColumnWidth, type TableSort, } from './table/TableCell.js';
|
|
5
5
|
export { TableRow, type TableRowProps, type TableRowVariant, } from './table/TableRow.js';
|
|
6
|
+
export { TableSelectCell, type TableSelectCellProps, } from './table/TableSelectCell.js';
|
|
6
7
|
export type { TableCellTone } from '../css/css-table-ledger.js';
|
|
7
8
|
export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
|
|
8
9
|
export type { TableLayout, TableMaxHeight, TableStickyHeader, TableVerticalAlign, };
|