cleanplate 0.3.19 → 0.3.20
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/dist/components/modal/Modal.d.ts +6 -0
- package/dist/components/modal/Modal.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.js +4 -4
- package/docs/FormControls.md +47 -6
- package/docs/Modal.md +48 -0
- package/package.json +1 -1
package/docs/FormControls.md
CHANGED
|
@@ -141,11 +141,11 @@ interface InputProps {
|
|
|
141
141
|
### Other control types
|
|
142
142
|
- **TextAreaProps**: label, value/defaultValue, onChange, isDisabled, isRequired, isFluid, className, error, dataTestId.
|
|
143
143
|
- **FileProps**: `name`, `label`, `variant` (`"button" | "card"`, default `"button"`), `multiple`, `accept`, `value: File[]` (controlled), `defaultValue: File[]` (uncontrolled initial visual list), `onChange(files: File[], e?)`, `buttonLabel` (default `"Browse file"`), `dropZoneText` (default `"Drag files to upload"`, card variant only), plus the common `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, `dataTestId`. The card variant supports drag-and-drop. **FileVariant** = `"button" | "card"`.
|
|
144
|
-
- **RadioProps**: `options` (non-empty `RadioOption[]`), `name`, `label` (group `<legend>`), optional `id`, `value`, `defaultValue`, `onChange(value, e)`, `orientation` (`"vertical" | "horizontal"`), `variant` (`"default" | "card"`), `isDisabled`, `isRequired`, `isFluid`, `className`, `error`,
|
|
145
|
-
- **RadioOption**: `{ label, value, isDisabled?, description?, icon?, dataTestId?, id? }`. `description` is rendered under the option label as muted secondary text and linked via `aria-describedby`. `icon` accepts any `ReactNode` (e.g. `<Icon />`, `<img />`, custom SVG) and renders to the left of the label/description.
|
|
144
|
+
- **RadioProps**: `options` (non-empty `RadioOption[]`), `name`, `label` (group `<legend>`), optional `id`, `value`, `defaultValue`, `onChange(value, e)`, `orientation` (`"vertical" | "horizontal"`), `variant` (`"default" | "card"`), `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, **`dataTestId`** (root on `<fieldset>`; suffixed ids on options container, rows, inputs, and labels — see **Checkbox and Radio — E2E / test selectors**).
|
|
145
|
+
- **RadioOption**: `{ label, value, isDisabled?, description?, icon?, dataTestId?, id? }`. `description` is rendered under the option label as muted secondary text and linked via `aria-describedby`. `icon` accepts any `ReactNode` (e.g. `<Icon />`, `<img />`, custom SVG) and renders to the left of the label/description. **`dataTestId`** on an option overrides the group-derived `-input-{value}` id for that option's native `<input>`.
|
|
146
146
|
- **ToggleProps**: checked, defaultChecked, onChange(checked: boolean), label, isDisabled, isRequired, isFluid, className, error, dataTestId.
|
|
147
|
-
- **CheckboxProps**: `options` (non-empty `CheckboxOption[]`), `name`, `label` (group `<legend>`), optional `id`, `value` (`CheckboxValue[]`), `defaultValue` (`CheckboxValue[]`), `onChange(values, e)`, `orientation` (`"vertical" | "horizontal"`), `variant` (`"default" | "card"`), `isDisabled`, `isRequired`, `isFluid`, `className`, `error`,
|
|
148
|
-
- **CheckboxOption**: `{ label, value, isDisabled?, description?, icon?, dataTestId?, id? }`. `description` is rendered under the option label as muted secondary text and linked via `aria-describedby`. `icon` accepts any `ReactNode` (e.g. `<Icon />`, `<img />`, custom SVG) and renders to the left of the label/description. `CheckboxValue = string | number`.
|
|
147
|
+
- **CheckboxProps**: `options` (non-empty `CheckboxOption[]`), `name`, `label` (group `<legend>`), optional `id`, `value` (`CheckboxValue[]`), `defaultValue` (`CheckboxValue[]`), `onChange(values, e)`, `orientation` (`"vertical" | "horizontal"`), `variant` (`"default" | "card"`), `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, **`dataTestId`** (root on `<fieldset>`; suffixed ids on options container, rows, inputs, and labels — see **Checkbox and Radio — E2E / test selectors**).
|
|
148
|
+
- **CheckboxOption**: `{ label, value, isDisabled?, description?, icon?, dataTestId?, id? }`. `description` is rendered under the option label as muted secondary text and linked via `aria-describedby`. `icon` accepts any `ReactNode` (e.g. `<Icon />`, `<img />`, custom SVG) and renders to the left of the label/description. **`dataTestId`** on an option overrides the group-derived `-input-{value}` id for that option's native `<input>`. `CheckboxValue = string | number`.
|
|
149
149
|
- **DateProps**: `value` / `defaultValue` (`Date | null`), `onChange(date: Date | null)`, `placeholder`, **`dateFormat`** (display string via `date-fns` + `locale`, default `MMM dd, yyyy`), **`name`** (renders a hidden `<input>` that submits **`yyyy-MM-dd`** for the committed calendar date), **`minDate`** / **`maxDate`** (inclusive navigation + selection bounds), **`disabledDates`** / **`disabledDaysOfWeek`** (greyed cells), **`locale`** (`date-fns` `Locale` — grid, subview copy, and field text), **`weekStartsOn`** (`0`–`6`, default `0` = Sunday), **`clearable`** (default `true`; shows clear control when a value exists), **`readOnly`** (no picker; value fixed), **`popoverPlacement`** (Floating UI placement for desktop; default `bottom-start`), **`onOpen`** / **`onClose`**, plus shared `label`, `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, `dataTestId`.
|
|
150
150
|
- **FormControlsStepperProps**: label, placeholder, value/defaultValue, onChange(e), min, max, step, layout (`"default" | "split-controls" | "trailing-stacked-chevrons"`), isDisabled, isRequired, isFluid, className, error, dataTestId.
|
|
151
151
|
|
|
@@ -258,6 +258,7 @@ const [interests, setInterests] = useState(["product"]);
|
|
|
258
258
|
<FormControls.Checkbox
|
|
259
259
|
label="Email me about"
|
|
260
260
|
name="interests"
|
|
261
|
+
dataTestId="interests"
|
|
261
262
|
value={interests}
|
|
262
263
|
onChange={(v) => setInterests(v)}
|
|
263
264
|
options={[
|
|
@@ -312,6 +313,7 @@ const [plan, setPlan] = useState("std");
|
|
|
312
313
|
<FormControls.Radio
|
|
313
314
|
label="Shipping"
|
|
314
315
|
name="ship"
|
|
316
|
+
dataTestId="shipping"
|
|
315
317
|
value={plan}
|
|
316
318
|
onChange={(v) => setPlan(String(v))}
|
|
317
319
|
isRequired
|
|
@@ -356,6 +358,45 @@ import { FormControls, Icon } from "cleanplate";
|
|
|
356
358
|
/>
|
|
357
359
|
```
|
|
358
360
|
|
|
361
|
+
### Checkbox and Radio — E2E / test selectors
|
|
362
|
+
|
|
363
|
+
Both **Checkbox** and **Radio** share the same suffix scheme. Pass **`dataTestId="interests"`** on the group to get stable Playwright / Testing Library hooks:
|
|
364
|
+
|
|
365
|
+
| Suffix | Element |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| *(root)* | `<fieldset>` wrapper |
|
|
368
|
+
| `-options` | Options container (`role="group"` / `role="radiogroup"`) |
|
|
369
|
+
| `-option-{value}` | Option row |
|
|
370
|
+
| `-input-{value}` | Native `<input>` (visually hidden) |
|
|
371
|
+
| `-label-{value}` | Clickable label (preferred for Playwright clicks) |
|
|
372
|
+
|
|
373
|
+
`{value}` is derived from each option's `value` prop: non-alphanumeric characters become `-` (e.g. `premium_plus` → `premium-plus`, `std` → `std`).
|
|
374
|
+
|
|
375
|
+
**Per-option override:** set **`dataTestId`** on a **`CheckboxOption`** / **`RadioOption`** to replace only that option's `-input-{value}` id. Row and label suffixes still use the group base + value key.
|
|
376
|
+
|
|
377
|
+
#### Playwright — Checkbox (multi-select)
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
await page.getByTestId("interests-label-newsletter").click();
|
|
381
|
+
await expect(page.getByTestId("interests-input-newsletter")).toBeChecked();
|
|
382
|
+
await page.getByTestId("interests-label-product").click();
|
|
383
|
+
await expect(page.getByTestId("interests-input-product")).toBeChecked();
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### Playwright — Radio (single-select)
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
await page.getByTestId("shipping-label-express").click();
|
|
390
|
+
await expect(page.getByTestId("shipping-input-express")).toBeChecked();
|
|
391
|
+
await expect(page.getByTestId("shipping-input-std")).not.toBeChecked();
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Direct input check (also works)
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
await page.getByTestId("shipping-input-std").check();
|
|
398
|
+
```
|
|
399
|
+
|
|
359
400
|
### File (button variant)
|
|
360
401
|
|
|
361
402
|
Compact trigger that looks like a primary button. Selected files render below the trigger as small cards with a type-specific thumbnail icon, name, size, and a remove button.
|
|
@@ -401,8 +442,8 @@ Pagination uses `FormControls.Select` for rows-per-page. Pills uses `FormControl
|
|
|
401
442
|
- **Input (`autoComplete` / `onBlur`):** `autoComplete` maps to the native attribute (`"email"`, `"current-password"`, `"off"`, …). `onBlur` runs after any internal numeric clamp so consumers see the final value.
|
|
402
443
|
- **Select:** Built on **Floating UI** — desktop uses a **portalled** panel with flip/shift to stay in the viewport; panel **width** matches the trigger, with optional **`panelMinWidth`** when options need more horizontal space; **`searchable={false}`** hides the panel search field (full static list, or async `onSearch("")` on open); **≤768px** uses a **bottom sheet** (`role="dialog"`, `aria-modal`, `aria-labelledby` to the field label when the label exists). **Option** shape supports `group`, `icon`, `avatar`, `meta`, `disabled`. **`mode`** (`'single' | 'multi'`) replaces **`isMulti`** (still supported, deprecated). Single mode: **`onChange(Option | null)`** — `null` when cleared. Multi mode: **`onChange(Option[])`** — use **`[]`** for clear. **`name` + hidden `<input>`:** native form submit posts the selected **`value`**(s); **multi** joins with **commas** — avoid comma characters inside `value` if you rely on `FormData`, or parse manually. **`options={null}` + `onSearch`:** async loading; show loading/empty/error states in the panel. **`groups`:** sticky headings for shared `Option.group`. **`maxSelect`:** multi only; **`triggerMaxItems`:** chip overflow **`+N`**. **`aria-controls`** on the combobox trigger and panel search point at the listbox **only while open**. **`aria-invalid`** reflects **`error`** on trigger, search field, and listbox. Validation message uses **`role="alert"`** (via shared field error pattern).
|
|
403
444
|
- **Date:** **`Date | null`** with **`onChange`**. Opens a **`role="dialog"`** calendar: **staging** applies on day tap; **Cancel** reverts to the last committed value; **OK** commits (and clears staging). **Desktop:** portalled Floating UI panel with flip/shift, fixed **max width ~400px** (capped by viewport). **≤768px:** bottom sheet fixed to the lower viewport + dimmed backdrop, `aria-modal`, body scroll lock while open (same breakpoint idea as Select). **Header:** month cluster + year cluster (44px arrow hits); tapping month/year opens **scrollable subviews** with **back (`arrow_back`)** and headings **“Select a month of {yyyy}”** / **“Select a year for {MMMM}”** (locale-aware via `locale`). **Trigger:** **`calendar_month`** trailing icon (not Select chevrons); optional **clear** when `clearable`. **`readOnly`** and **`isDisabled`** block interaction. Constraints: **`minDate`/`maxDate`** (inclusive), **`disabledDates`**, **`disabledDaysOfWeek`**. **`dateFormat`** + **`locale`** control the field string; grid labels follow **`locale`** and **`weekStartsOn`**. **`name`:** hidden input posts **`yyyy-MM-dd`** for the **committed** value only. **`onOpen`/`onClose`** fire when the panel opens/closes. **`popoverPlacement`** adjusts desktop anchor (default `bottom-start`). **`error`** / **`isRequired`** use the shared field error pattern (`aria-invalid`, message under the field).
|
|
404
|
-
- **Radio:** Group-first API — pass `options: RadioOption[]`. Renders `<fieldset>` + `<legend>` with a single `value` and `onChange(value, e)`. `isRequired` puts `*` on the legend and adds `required`/`aria-required` to the first enabled option (HTML5 only requires one input in the group to carry it). Custom ring/dot follows the native `:checked` state so uncontrolled groups stay visually correct. Pass `variant="card"` for tile-style options (ring in top-right, optional `icon` on the left, primary-brand border + tint when selected).
|
|
405
|
-
- **Checkbox:** Group-first API — pass `options: CheckboxOption[]`. Renders `<fieldset>` + `<legend>` with a `value: CheckboxValue[]` and `onChange(values, e)`. `isRequired` puts `*` on the legend and sets `aria-required` on the group; native HTML5 doesn't enforce "at least one" for checkbox groups, so add custom validation at the form layer. Custom box/tick follows the native `:checked` state. Pass `variant="card"` for tile-style options (box in top-right, optional `icon` on the left, primary-brand border + tint when checked). For a single checkbox, pass a one-element `options` array — `value=[]` is unchecked, `value=[opt.value]` is checked.
|
|
445
|
+
- **Radio:** Group-first API — pass `options: RadioOption[]`. Renders `<fieldset>` + `<legend>` with a single `value` and `onChange(value, e)`. `isRequired` puts `*` on the legend and adds `required`/`aria-required` to the first enabled option (HTML5 only requires one input in the group to carry it). Custom ring/dot follows the native `:checked` state so uncontrolled groups stay visually correct. Pass `variant="card"` for tile-style options (ring in top-right, optional `icon` on the left, primary-brand border + tint when selected). **`dataTestId`** on the group maps to the fieldset and emits suffixed ids (`-options`, `-option-{value}`, `-input-{value}`, `-label-{value}`); per-option `dataTestId` overrides the input suffix only.
|
|
446
|
+
- **Checkbox:** Group-first API — pass `options: CheckboxOption[]`. Renders `<fieldset>` + `<legend>` with a `value: CheckboxValue[]` and `onChange(values, e)`. `isRequired` puts `*` on the legend and sets `aria-required` on the group; native HTML5 doesn't enforce "at least one" for checkbox groups, so add custom validation at the form layer. Custom box/tick follows the native `:checked` state. Pass `variant="card"` for tile-style options (box in top-right, optional `icon` on the left, primary-brand border + tint when checked). For a single checkbox, pass a one-element `options` array — `value=[]` is unchecked, `value=[opt.value]` is checked. **`dataTestId`** uses the same suffix scheme as Radio.
|
|
406
447
|
- **File:** Native `<input type="file">` is visually hidden but stays in the a11y tree. Manages a `File[]` selection internally; `onChange(files, e)` fires for picker selections, drops, and removals (the underlying event is `undefined` for non-picker triggers). With `multiple`, subsequent picks/drops append; without, the new selection replaces the old. The card variant supports drag-and-drop and tints primary-brand on hover. Removing a file resets the native input so re-selecting the same file still emits a change. `defaultValue` seeds the visual list only — browsers don't allow programmatic pre-population of file inputs.
|
|
407
448
|
- **isFluid:** Full-width field wrapper.
|
|
408
449
|
|
package/docs/Modal.md
CHANGED
|
@@ -22,6 +22,7 @@ Purpose: A full-featured modal overlay for forms, long content, or custom dialog
|
|
|
22
22
|
| onPrimaryButtonClick | () => void | no | — | Called when the primary footer button is clicked. |
|
|
23
23
|
| secondaryButtonLabel | string | no | "" | Label for the secondary footer button; empty hides it. |
|
|
24
24
|
| onSecondaryButtonClick | () => void | no | — | Called when the secondary footer button is clicked. |
|
|
25
|
+
| dataTestId | string | no | — | Root `data-testid` on the dialog panel; suffixed ids on overlay, header, title, close, body, footer, and action buttons (see **E2E / test selectors**). |
|
|
25
26
|
|
|
26
27
|
## Types
|
|
27
28
|
|
|
@@ -59,9 +60,39 @@ interface ModalProps {
|
|
|
59
60
|
onPrimaryButtonClick?: () => void;
|
|
60
61
|
secondaryButtonLabel?: string;
|
|
61
62
|
onSecondaryButtonClick?: () => void;
|
|
63
|
+
dataTestId?: string;
|
|
62
64
|
}
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
## E2E / test selectors
|
|
68
|
+
|
|
69
|
+
Pass `dataTestId="save-modal"` to get stable Playwright / Testing Library hooks:
|
|
70
|
+
|
|
71
|
+
| Suffix | Element |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| *(root)* | Dialog panel (`role="dialog"`) |
|
|
74
|
+
| `-overlay` | Backdrop overlay |
|
|
75
|
+
| `-header` | Header row (title + close button) |
|
|
76
|
+
| `-title` | Title text |
|
|
77
|
+
| `-close` | Header close (X) button |
|
|
78
|
+
| `-body` | Main content area |
|
|
79
|
+
| `-footer` | Footer action row |
|
|
80
|
+
| `-primary` | Primary footer button |
|
|
81
|
+
| `-secondary` | Secondary footer button |
|
|
82
|
+
|
|
83
|
+
Header, title, close, footer, and button suffixes are omitted when those regions are not rendered (e.g. no `title` and `showCloseButton={false}` hides `-header`, `-title`, and `-close`).
|
|
84
|
+
|
|
85
|
+
### Playwright example
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await page.getByRole("button", { name: "Open settings" }).click();
|
|
89
|
+
await expect(page.getByTestId("settings-modal")).toBeVisible();
|
|
90
|
+
await expect(page.getByTestId("settings-modal-title")).toHaveText("Settings");
|
|
91
|
+
await page.getByTestId("settings-modal-body").getByLabel("Display name").fill("Ada");
|
|
92
|
+
await page.getByTestId("settings-modal-primary").click();
|
|
93
|
+
await expect(page.getByTestId("settings-modal")).toBeHidden();
|
|
94
|
+
```
|
|
95
|
+
|
|
65
96
|
## Usage Examples
|
|
66
97
|
|
|
67
98
|
### Basic
|
|
@@ -135,6 +166,23 @@ const App = () => {
|
|
|
135
166
|
</Modal>
|
|
136
167
|
```
|
|
137
168
|
|
|
169
|
+
### With test selectors
|
|
170
|
+
|
|
171
|
+
```jsx
|
|
172
|
+
<Modal
|
|
173
|
+
isOpen={isOpen}
|
|
174
|
+
onClose={() => setIsOpen(false)}
|
|
175
|
+
title="Delete item"
|
|
176
|
+
dataTestId="delete-item-modal"
|
|
177
|
+
primaryButtonLabel="Delete"
|
|
178
|
+
onPrimaryButtonClick={handleDelete}
|
|
179
|
+
secondaryButtonLabel="Cancel"
|
|
180
|
+
onSecondaryButtonClick={() => setIsOpen(false)}
|
|
181
|
+
>
|
|
182
|
+
<Typography variant="p">This action cannot be undone.</Typography>
|
|
183
|
+
</Modal>
|
|
184
|
+
```
|
|
185
|
+
|
|
138
186
|
## Behavior Notes
|
|
139
187
|
|
|
140
188
|
- **Rendering:** The modal mounts when `isOpen` is true and unmounts shortly after close so the exit transition can finish.
|