@cssints/headless 0.7.0 → 0.9.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/README.md +58 -1
- package/dist/index.d.mts +141 -0
- package/dist/index.mjs +177 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ A primitive that has a seam honours an optional `prefs` entry (a `Partial<Prefs>
|
|
|
65
65
|
- **menu / menubar** — `typeaheadDelayMs` (the typeahead buffer timeout).
|
|
66
66
|
- **tooltip** — `tooltipShowDelayMs` / `tooltipHideDelayMs`, exposed as `showDelayMs()` / `hideDelayMs()` for the host to wire its `setTimeout` from (the spec keeps timers as events).
|
|
67
67
|
|
|
68
|
-
Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, dialog-modal, disclosure, feed, grid, link, listbox, meter, player, radio, resizable-panels, slider, slider-multithumb, spinbutton, switch, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
|
|
68
|
+
Everything else **ignores** prefs: accordion, alert, alertdialog, breadcrumb, button, carousel, checkbox, checkbox-group, combobox, confirm, dialog-modal, disclosure, feed, field, grid, link, listbox, meter, player, radio, resizable-panels, slider, slider-multithumb, spinbutton, switch, tabs, timeline, toolbar, treegrid, treeview, windowsplitter — they expose no timing / motion / live-region / camera seam. `listbox` / `treeview` / `timeline` typeahead timeouts are host-driven via their reset methods, so they take no delay here either.
|
|
69
69
|
|
|
70
70
|
`motionDurationMs` is **not consumed**: canvas tweens are tick-driven, not millisecond-driven, and the look layer owns durations. It stays in `Prefs` only so a host's preference record fits the type.
|
|
71
71
|
|
|
@@ -123,6 +123,63 @@ Controlled is `sizes` + `onSizesChange` (the request), like every other primitiv
|
|
|
123
123
|
|
|
124
124
|
Left to the host: pane bounds in pixels (percent only), nested groups, and F6 (with three panes a Tab is enough). Not modelled: react-resizable-panels' collapse-on-drag threshold (a pane collapses on Enter or a host write, never by dragging through MIN) and restoring a pane that was mounted already collapsed (it returns to its own `min`).
|
|
125
125
|
|
|
126
|
+
## Form field
|
|
127
|
+
|
|
128
|
+
`createField` is the ARIA contract of a form field: a label, a control, a hint and an error. **There is no APG pattern for it**, so there is no Quint spec and no ported scenarios: the contract is WAI-ARIA (`aria-describedby`, `aria-invalid`, `aria-required`) and HTML (`label[for]`, `required`), and where those are silent the majority of Base UI Field, react-aria TextField and ariakit FormControl. `test/field.browser.test.tsx` is its oracle (plain assertions on the attribute objects; no DOM is needed, the file name is only what `vite.config.ts` includes).
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const field = createField({
|
|
132
|
+
required: true,
|
|
133
|
+
hint: "At least 8 characters",
|
|
134
|
+
get invalid() {
|
|
135
|
+
return touched() && password().length < 8;
|
|
136
|
+
},
|
|
137
|
+
get error() {
|
|
138
|
+
return "Too short";
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
<label {...field.labelProps}>Password</label>
|
|
143
|
+
<input type="password" {...field.controlProps} />
|
|
144
|
+
<p {...field.hintProps}>{field.hint()}</p>
|
|
145
|
+
<p {...field.errorProps}>{field.error()}</p>;
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **Ids** are `<id>-control`, `-label`, `-hint`, `-error` from `id` (or a unique one); `labelProps.for` is the control's id.
|
|
149
|
+
- **Render the hint and the error elements always.** They carry `hidden` while absent, and `aria-describedby` lists only the ones that are shown, hint first: a reference never points at a hidden or missing id.
|
|
150
|
+
- **The error does not replace the hint.** Both are listed, hint then error, as Base UI, react-aria and ariakit do: the hint is what tells the user how to fix the error.
|
|
151
|
+
- **`aria-invalid` only when invalid.** An error message makes the field invalid; `invalid: "message"` is both the flag and the message (an `error` wins over it); `invalid: true` without a message marks the control and shows no error; `invalid: false` wins over everything, so a host can keep the message ready and pass `touched() && failed()`.
|
|
152
|
+
- **`required`** sets both `required` and `aria-required` (a native control needs the first, a custom widget the second); use `novalidate` on the form if the browser's own bubbles are not wanted.
|
|
153
|
+
- The error has no `role=alert`: announcing it when it appears is the host's (`createAlert`).
|
|
154
|
+
|
|
155
|
+
## Confirm
|
|
156
|
+
|
|
157
|
+
`createConfirm` is a promise-based confirmation over `createDialogModal({ role: "alertdialog" })`. Like the field it is not checked against a spec (the dialog is; `test/confirm.browser.test.tsx` covers the promise around it, in Chromium).
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
const confirm = createConfirm();
|
|
161
|
+
|
|
162
|
+
const remove = async () => {
|
|
163
|
+
if (await confirm.confirm({ title: "Delete post?", description: "This cannot be undone.", confirmLabel: "Delete", destructive: true })) {
|
|
164
|
+
await deletePost();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
<button {...confirm.triggerProps} onClick={remove}>Delete</button>
|
|
169
|
+
<div {...confirm.backdropProps} />
|
|
170
|
+
<div {...confirm.dialogProps}>
|
|
171
|
+
<h2 {...confirm.titleProps}>{confirm.request()?.title}</h2>
|
|
172
|
+
<p {...confirm.descriptionProps}>{confirm.request()?.description}</p>
|
|
173
|
+
<button {...confirm.cancelProps}>{confirm.request()?.cancelLabel}</button>
|
|
174
|
+
<button {...confirm.confirmProps}>{confirm.request()?.confirmLabel}</button>
|
|
175
|
+
</div>;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `confirm(request)` resolves `true` on the confirm button, `false` on cancel, Escape or a click on the backdrop. `onOpenChange` fires when it opens and when it settles.
|
|
179
|
+
- **One request at a time.** A second `confirm()` while one is pending **rejects** with `ConfirmPendingError` and leaves the first alone; it is not queued (a double click on "Delete" would ask twice, and a queued question can outlive the screen that asked it). A pending request is answered `false` when the owner is disposed.
|
|
180
|
+
- **Focus.** The cancel button takes focus on open (the least destructive action); on settle focus returns to the element that had it when `confirm()` was called, before the promise resolves, so the caller may move it on. A focus trap and a scroll lock are the host's, as for dialog-modal.
|
|
181
|
+
- `aria-labelledby` is the title and `aria-describedby` **always** the description (an alertdialog needs one), so render `descriptionProps` always: it is `hidden` when the request has no description. `triggerProps` are dialog-modal's trigger attributes without the handlers; the click that calls `confirm()` is the host's. `request()` keeps the last request after it settles, so a closing animation does not flash empty.
|
|
182
|
+
|
|
126
183
|
## Commands
|
|
127
184
|
|
|
128
185
|
```sh
|
package/dist/index.d.mts
CHANGED
|
@@ -1294,6 +1294,80 @@ export interface DialogModal {
|
|
|
1294
1294
|
* are requests the host may ignore.
|
|
1295
1295
|
*/
|
|
1296
1296
|
export declare const createDialogModal: (options?: DialogModalOptions) => DialogModal;
|
|
1297
|
+
export interface ConfirmOptions {
|
|
1298
|
+
readonly id?: string;
|
|
1299
|
+
/** Called with `true` when a request opens the dialog and `false` when it settles. */
|
|
1300
|
+
readonly onOpenChange?: (open: boolean) => void;
|
|
1301
|
+
}
|
|
1302
|
+
export interface ConfirmRequest {
|
|
1303
|
+
readonly title: string;
|
|
1304
|
+
readonly description?: string;
|
|
1305
|
+
/** Default `"Confirm"`. */
|
|
1306
|
+
readonly confirmLabel?: string;
|
|
1307
|
+
/** Default `"Cancel"`. */
|
|
1308
|
+
readonly cancelLabel?: string;
|
|
1309
|
+
/** The confirm action destroys something: the host styles it so. */
|
|
1310
|
+
readonly destructive?: boolean;
|
|
1311
|
+
}
|
|
1312
|
+
/** The request being shown (or shown last), with the defaults applied. */
|
|
1313
|
+
export interface ConfirmView {
|
|
1314
|
+
readonly title: string;
|
|
1315
|
+
readonly description: string | undefined;
|
|
1316
|
+
readonly confirmLabel: string;
|
|
1317
|
+
readonly cancelLabel: string;
|
|
1318
|
+
readonly destructive: boolean;
|
|
1319
|
+
}
|
|
1320
|
+
/** The attributes of dialog-modal's trigger; the click that calls `confirm()` is the host's. */
|
|
1321
|
+
export type ConfirmTriggerProps = Omit<DialogTriggerProps, "onClick" | "onKeyDown">;
|
|
1322
|
+
export interface ConfirmTextProps {
|
|
1323
|
+
readonly id: string;
|
|
1324
|
+
readonly hidden?: boolean;
|
|
1325
|
+
}
|
|
1326
|
+
export interface ConfirmButtonProps {
|
|
1327
|
+
readonly id: string;
|
|
1328
|
+
readonly type: "button";
|
|
1329
|
+
readonly onClick: () => void;
|
|
1330
|
+
}
|
|
1331
|
+
export interface Confirm {
|
|
1332
|
+
/**
|
|
1333
|
+
* Opens the dialog and resolves with the answer: `true` on confirm, `false`
|
|
1334
|
+
* on cancel, Escape or a backdrop click. A request made while one is pending
|
|
1335
|
+
* rejects with `ConfirmPendingError` and leaves the pending one alone.
|
|
1336
|
+
*/
|
|
1337
|
+
readonly confirm: (request: ConfirmRequest) => Promise<boolean>;
|
|
1338
|
+
readonly open: Accessor<boolean>;
|
|
1339
|
+
readonly request: Accessor<ConfirmView | undefined>;
|
|
1340
|
+
readonly triggerProps: ConfirmTriggerProps;
|
|
1341
|
+
readonly dialogProps: DialogContentProps;
|
|
1342
|
+
readonly backdropProps: DialogBackdropProps;
|
|
1343
|
+
readonly titleProps: ConfirmTextProps;
|
|
1344
|
+
/** Render this element always: `aria-describedby` points at it. Hidden when there is no description. */
|
|
1345
|
+
readonly descriptionProps: ConfirmTextProps;
|
|
1346
|
+
readonly cancelProps: ConfirmButtonProps;
|
|
1347
|
+
readonly confirmProps: ConfirmButtonProps;
|
|
1348
|
+
}
|
|
1349
|
+
export declare class ConfirmPendingError extends Error {
|
|
1350
|
+
readonly name = "ConfirmPendingError";
|
|
1351
|
+
constructor();
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* A promise-based confirmation over `createDialogModal({ role: "alertdialog" })`
|
|
1355
|
+
* (APG Alert Dialog: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/).
|
|
1356
|
+
* Not checked against a Quint spec: the dialog is, the promise around it is
|
|
1357
|
+
* covered by `test/confirm.browser.test.tsx`.
|
|
1358
|
+
*
|
|
1359
|
+
* Focus: the cancel button, the least destructive action, takes focus when the
|
|
1360
|
+
* dialog opens; when the request settles, focus goes back to the element that
|
|
1361
|
+
* had it when `confirm()` was called, before the promise resolves, so the
|
|
1362
|
+
* caller's continuation may move it again. A focus trap and a scroll lock are
|
|
1363
|
+
* the host's (as for dialog-modal).
|
|
1364
|
+
*
|
|
1365
|
+
* One request at a time: a second `confirm()` while one is pending is rejected,
|
|
1366
|
+
* not queued. Queued, a double click on "Delete" would ask twice and a stale
|
|
1367
|
+
* question could outlive the screen that asked it. A pending request is
|
|
1368
|
+
* answered `false` when the owner is disposed.
|
|
1369
|
+
*/
|
|
1370
|
+
export declare const createConfirm: (options?: ConfirmOptions) => Confirm;
|
|
1297
1371
|
/**
|
|
1298
1372
|
* Options are read lazily (getters); pass a component's `props`.
|
|
1299
1373
|
*/
|
|
@@ -1381,6 +1455,73 @@ export interface Feed {
|
|
|
1381
1455
|
* request; `loadStart` / `loadFail` are adapter lifecycle events.
|
|
1382
1456
|
*/
|
|
1383
1457
|
export declare const createFeed: (options?: FeedOptions) => Feed;
|
|
1458
|
+
export interface FieldOptions {
|
|
1459
|
+
/** Base of every id the field makes (`<id>-control`, `-label`, `-hint`, `-error`). */
|
|
1460
|
+
readonly id?: string;
|
|
1461
|
+
readonly required?: boolean;
|
|
1462
|
+
/**
|
|
1463
|
+
* `true` marks the control invalid; a string does too and is the error message
|
|
1464
|
+
* (unless `error` is set). `false` forces valid: an `error` is then not shown,
|
|
1465
|
+
* so a host can pass `touched() && failed()` and keep the message ready.
|
|
1466
|
+
* Left out, the field is invalid exactly when it has an error message.
|
|
1467
|
+
*/
|
|
1468
|
+
readonly invalid?: boolean | string | undefined;
|
|
1469
|
+
/** Help text; absent (or empty) means the hint element is hidden and not referenced. */
|
|
1470
|
+
readonly hint?: string | undefined;
|
|
1471
|
+
/** The error message; absent (or empty) means no error element. */
|
|
1472
|
+
readonly error?: string | undefined;
|
|
1473
|
+
}
|
|
1474
|
+
/** Spread onto the form control (`input`, `textarea`, `select`, or a custom widget). */
|
|
1475
|
+
export interface FieldControlProps {
|
|
1476
|
+
readonly id: string;
|
|
1477
|
+
/** The ids of the hint and the error that are shown, hint first; absent when neither is. */
|
|
1478
|
+
readonly "aria-describedby": string | undefined;
|
|
1479
|
+
readonly "aria-invalid": "true" | undefined;
|
|
1480
|
+
/** For a control that is not native (a native one has `required`). */
|
|
1481
|
+
readonly "aria-required": "true" | undefined;
|
|
1482
|
+
readonly required: true | undefined;
|
|
1483
|
+
}
|
|
1484
|
+
export interface FieldLabelProps {
|
|
1485
|
+
readonly id: string;
|
|
1486
|
+
readonly for: string;
|
|
1487
|
+
}
|
|
1488
|
+
/** Spread onto the hint and the error element; render both, always. */
|
|
1489
|
+
export interface FieldMessageProps {
|
|
1490
|
+
readonly id: string;
|
|
1491
|
+
readonly hidden: boolean;
|
|
1492
|
+
}
|
|
1493
|
+
export interface Field {
|
|
1494
|
+
readonly invalid: () => boolean;
|
|
1495
|
+
/** The hint, or `undefined` when there is none. */
|
|
1496
|
+
readonly hint: () => string | undefined;
|
|
1497
|
+
/** The error shown: `undefined` unless the field is invalid and has a message. */
|
|
1498
|
+
readonly error: () => string | undefined;
|
|
1499
|
+
/** The ids `aria-describedby` lists now: hint, then error; never an id that is hidden. */
|
|
1500
|
+
readonly describedIds: () => readonly string[];
|
|
1501
|
+
readonly controlProps: FieldControlProps;
|
|
1502
|
+
readonly labelProps: FieldLabelProps;
|
|
1503
|
+
readonly hintProps: FieldMessageProps;
|
|
1504
|
+
readonly errorProps: FieldMessageProps;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* The ARIA contract of a form field: a label, a control, a hint and an error.
|
|
1508
|
+
* There is no APG pattern for it (APG has no "form field"; the nearest is the
|
|
1509
|
+
* label and description rules of the accessible name computation), so the
|
|
1510
|
+
* contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
|
|
1511
|
+
* `aria-required`) and HTML (`label[for]`, `required`), and from the
|
|
1512
|
+
* majority of Base UI Field, react-aria TextField and ariakit FormControl,
|
|
1513
|
+
* which list the description and the error message together. It is not checked
|
|
1514
|
+
* against a Quint spec; `test/field.browser.test.tsx` is its oracle.
|
|
1515
|
+
*
|
|
1516
|
+
* Decisions: the error does not replace the hint, both are listed, hint first
|
|
1517
|
+
* (the hint is what tells the user how to fix the error); an error implies
|
|
1518
|
+
* invalid and `invalid: false` overrides it; a hidden message is never
|
|
1519
|
+
* referenced, so `aria-describedby` has no dangling id. The error has no
|
|
1520
|
+
* `role=alert`: announcing it is the host's (compose `createAlert`).
|
|
1521
|
+
*
|
|
1522
|
+
* Options are read when an attribute is read: pass getters, never destructure.
|
|
1523
|
+
*/
|
|
1524
|
+
export declare const createField: (options?: FieldOptions) => Field;
|
|
1384
1525
|
/**
|
|
1385
1526
|
* APG grid primitive on Solid 2: roving focus, 2-D navigation, edit mode, a
|
|
1386
1527
|
* cell/row/column selection rectangle and sortable column headers. State plus
|
package/dist/index.mjs
CHANGED
|
@@ -2584,6 +2584,112 @@ const createDialogModal = (options = {}) => {
|
|
|
2584
2584
|
}
|
|
2585
2585
|
};
|
|
2586
2586
|
};
|
|
2587
|
+
// src/confirm.ts
|
|
2588
|
+
var ConfirmPendingError = class extends Error {
|
|
2589
|
+
name = "ConfirmPendingError";
|
|
2590
|
+
constructor() {
|
|
2591
|
+
super("a confirmation is already pending");
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
/**
|
|
2595
|
+
* A promise-based confirmation over `createDialogModal({ role: "alertdialog" })`
|
|
2596
|
+
* (APG Alert Dialog: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/).
|
|
2597
|
+
* Not checked against a Quint spec: the dialog is, the promise around it is
|
|
2598
|
+
* covered by `test/confirm.browser.test.tsx`.
|
|
2599
|
+
*
|
|
2600
|
+
* Focus: the cancel button, the least destructive action, takes focus when the
|
|
2601
|
+
* dialog opens; when the request settles, focus goes back to the element that
|
|
2602
|
+
* had it when `confirm()` was called, before the promise resolves, so the
|
|
2603
|
+
* caller's continuation may move it again. A focus trap and a scroll lock are
|
|
2604
|
+
* the host's (as for dialog-modal).
|
|
2605
|
+
*
|
|
2606
|
+
* One request at a time: a second `confirm()` while one is pending is rejected,
|
|
2607
|
+
* not queued. Queued, a double click on "Delete" would ask twice and a stale
|
|
2608
|
+
* question could outlive the screen that asked it. A pending request is
|
|
2609
|
+
* answered `false` when the owner is disposed.
|
|
2610
|
+
*/
|
|
2611
|
+
const createConfirm = (options = {}) => {
|
|
2612
|
+
const base = options.id ?? createUniqueId();
|
|
2613
|
+
const titleId = `${base}-title`;
|
|
2614
|
+
const descriptionId = `${base}-description`;
|
|
2615
|
+
const cancelId = `${base}-cancel`;
|
|
2616
|
+
const [request, setRequest] = createSignal(void 0);
|
|
2617
|
+
let pending;
|
|
2618
|
+
const settle = (answer) => {
|
|
2619
|
+
const current = pending;
|
|
2620
|
+
if (current === void 0) return;
|
|
2621
|
+
pending = void 0;
|
|
2622
|
+
dialog.close();
|
|
2623
|
+
current.invoker?.focus();
|
|
2624
|
+
current.resolve(answer);
|
|
2625
|
+
};
|
|
2626
|
+
const dialog = createDialogModal({
|
|
2627
|
+
id: base,
|
|
2628
|
+
role: "alertdialog",
|
|
2629
|
+
labelledby: titleId,
|
|
2630
|
+
describedby: descriptionId,
|
|
2631
|
+
onOpenChange: (open) => {
|
|
2632
|
+
if (!open) settle(false);
|
|
2633
|
+
options.onOpenChange?.(open);
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
createEffect(() => dialog.open(), (isOpen) => {
|
|
2637
|
+
if (isOpen) document.getElementById(cancelId)?.focus();
|
|
2638
|
+
});
|
|
2639
|
+
onCleanup(() => {
|
|
2640
|
+
pending?.resolve(false);
|
|
2641
|
+
pending = void 0;
|
|
2642
|
+
});
|
|
2643
|
+
const confirm = (next) => {
|
|
2644
|
+
if (pending !== void 0) return Promise.reject(new ConfirmPendingError());
|
|
2645
|
+
return new Promise((resolve) => {
|
|
2646
|
+
pending = {
|
|
2647
|
+
resolve,
|
|
2648
|
+
invoker: document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
2649
|
+
};
|
|
2650
|
+
setRequest({
|
|
2651
|
+
title: next.title,
|
|
2652
|
+
description: next.description,
|
|
2653
|
+
confirmLabel: next.confirmLabel ?? "Confirm",
|
|
2654
|
+
cancelLabel: next.cancelLabel ?? "Cancel",
|
|
2655
|
+
destructive: next.destructive === true
|
|
2656
|
+
});
|
|
2657
|
+
dialog.openDialog();
|
|
2658
|
+
});
|
|
2659
|
+
};
|
|
2660
|
+
return {
|
|
2661
|
+
confirm,
|
|
2662
|
+
open: dialog.open,
|
|
2663
|
+
request,
|
|
2664
|
+
triggerProps: {
|
|
2665
|
+
id: dialog.triggerProps.id,
|
|
2666
|
+
"aria-haspopup": "dialog",
|
|
2667
|
+
get "aria-expanded"() {
|
|
2668
|
+
return dialog.triggerProps["aria-expanded"];
|
|
2669
|
+
},
|
|
2670
|
+
"aria-controls": dialog.triggerProps["aria-controls"]
|
|
2671
|
+
},
|
|
2672
|
+
dialogProps: dialog.dialogProps,
|
|
2673
|
+
backdropProps: dialog.backdropProps,
|
|
2674
|
+
titleProps: { id: titleId },
|
|
2675
|
+
descriptionProps: {
|
|
2676
|
+
id: descriptionId,
|
|
2677
|
+
get hidden() {
|
|
2678
|
+
return request()?.description === void 0;
|
|
2679
|
+
}
|
|
2680
|
+
},
|
|
2681
|
+
cancelProps: {
|
|
2682
|
+
id: cancelId,
|
|
2683
|
+
type: "button",
|
|
2684
|
+
onClick: () => settle(false)
|
|
2685
|
+
},
|
|
2686
|
+
confirmProps: {
|
|
2687
|
+
id: `${base}-confirm`,
|
|
2688
|
+
type: "button",
|
|
2689
|
+
onClick: () => settle(true)
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
};
|
|
2587
2693
|
// src/feed.ts
|
|
2588
2694
|
/**
|
|
2589
2695
|
* APG Feed (https://www.w3.org/WAI/ARIA/apg/patterns/feed/): state plus prop
|
|
@@ -2719,6 +2825,76 @@ const createFeed = (options = {}) => {
|
|
|
2719
2825
|
}
|
|
2720
2826
|
};
|
|
2721
2827
|
};
|
|
2828
|
+
// src/field.ts
|
|
2829
|
+
const text = (value) => value === void 0 || value === "" ? void 0 : value;
|
|
2830
|
+
/**
|
|
2831
|
+
* The ARIA contract of a form field: a label, a control, a hint and an error.
|
|
2832
|
+
* There is no APG pattern for it (APG has no "form field"; the nearest is the
|
|
2833
|
+
* label and description rules of the accessible name computation), so the
|
|
2834
|
+
* contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
|
|
2835
|
+
* `aria-required`) and HTML (`label[for]`, `required`), and from the
|
|
2836
|
+
* majority of Base UI Field, react-aria TextField and ariakit FormControl,
|
|
2837
|
+
* which list the description and the error message together. It is not checked
|
|
2838
|
+
* against a Quint spec; `test/field.browser.test.tsx` is its oracle.
|
|
2839
|
+
*
|
|
2840
|
+
* Decisions: the error does not replace the hint, both are listed, hint first
|
|
2841
|
+
* (the hint is what tells the user how to fix the error); an error implies
|
|
2842
|
+
* invalid and `invalid: false` overrides it; a hidden message is never
|
|
2843
|
+
* referenced, so `aria-describedby` has no dangling id. The error has no
|
|
2844
|
+
* `role=alert`: announcing it is the host's (compose `createAlert`).
|
|
2845
|
+
*
|
|
2846
|
+
* Options are read when an attribute is read: pass getters, never destructure.
|
|
2847
|
+
*/
|
|
2848
|
+
const createField = (options = {}) => {
|
|
2849
|
+
const base = options.id ?? createUniqueId();
|
|
2850
|
+
const controlId = `${base}-control`;
|
|
2851
|
+
const labelId = `${base}-label`;
|
|
2852
|
+
const hintId = `${base}-hint`;
|
|
2853
|
+
const errorId = `${base}-error`;
|
|
2854
|
+
const hint = () => text(options.hint);
|
|
2855
|
+
const message = () => text(options.error) ?? (typeof options.invalid === "string" ? text(options.invalid) : void 0);
|
|
2856
|
+
const invalid = () => options.invalid === false ? false : options.invalid === true || message() !== void 0;
|
|
2857
|
+
const error = () => invalid() ? message() : void 0;
|
|
2858
|
+
const describedIds = () => [...hint() === void 0 ? [] : [hintId], ...error() === void 0 ? [] : [errorId]];
|
|
2859
|
+
return {
|
|
2860
|
+
invalid,
|
|
2861
|
+
hint,
|
|
2862
|
+
error,
|
|
2863
|
+
describedIds,
|
|
2864
|
+
controlProps: {
|
|
2865
|
+
id: controlId,
|
|
2866
|
+
get "aria-describedby"() {
|
|
2867
|
+
const ids = describedIds();
|
|
2868
|
+
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
2869
|
+
},
|
|
2870
|
+
get "aria-invalid"() {
|
|
2871
|
+
return invalid() ? "true" : void 0;
|
|
2872
|
+
},
|
|
2873
|
+
get "aria-required"() {
|
|
2874
|
+
return options.required === true ? "true" : void 0;
|
|
2875
|
+
},
|
|
2876
|
+
get required() {
|
|
2877
|
+
return options.required === true ? true : void 0;
|
|
2878
|
+
}
|
|
2879
|
+
},
|
|
2880
|
+
labelProps: {
|
|
2881
|
+
id: labelId,
|
|
2882
|
+
for: controlId
|
|
2883
|
+
},
|
|
2884
|
+
hintProps: {
|
|
2885
|
+
id: hintId,
|
|
2886
|
+
get hidden() {
|
|
2887
|
+
return hint() === void 0;
|
|
2888
|
+
}
|
|
2889
|
+
},
|
|
2890
|
+
errorProps: {
|
|
2891
|
+
id: errorId,
|
|
2892
|
+
get hidden() {
|
|
2893
|
+
return error() === void 0;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
};
|
|
2722
2898
|
// src/grid.ts
|
|
2723
2899
|
const GRID_KEYS = /* @__PURE__ */ new Set([
|
|
2724
2900
|
"ArrowRight",
|
|
@@ -7239,4 +7415,4 @@ const createRearrangeableListbox = (options) => {
|
|
|
7239
7415
|
})
|
|
7240
7416
|
};
|
|
7241
7417
|
};
|
|
7242
|
-
export { DEFAULT_PREFS, DRAG_PX, GRID, NO_POS, SNAP_PX, TONE, TREEVIEW_NODES, TWEEN_TICKS, WORLD_BOUND, YES_POS, ZMAX, ZMIN, ZONE, cellPart, createAccordion, createAlert, createAlertDialog, createBreadcrumb, createButton, createCanvas, createCarousel, createCheckbox, createCheckboxGroup, createCombobox, createDialogModal, createDisclosure, createFeed, createGrid, createLink, createListbox, createMenu, createMenubar, createMeter, createPlayer, createRadioGroup, createRearrangeableListbox, createResizablePanels, createSlider, createSliderMultiThumb, createSpinbutton, createSwitch, createTabs, createTimeline, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
|
|
7418
|
+
export { ConfirmPendingError, DEFAULT_PREFS, DRAG_PX, GRID, NO_POS, SNAP_PX, TONE, TREEVIEW_NODES, TWEEN_TICKS, WORLD_BOUND, YES_POS, ZMAX, ZMIN, ZONE, cellPart, createAccordion, createAlert, createAlertDialog, createBreadcrumb, createButton, createCanvas, createCarousel, createCheckbox, createCheckboxGroup, createCombobox, createConfirm, createDialogModal, createDisclosure, createFeed, createField, createGrid, createLink, createListbox, createMenu, createMenubar, createMeter, createPlayer, createRadioGroup, createRearrangeableListbox, createResizablePanels, createSlider, createSliderMultiThumb, createSpinbutton, createSwitch, createTabs, createTimeline, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cssints/headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Headless APG widgets on Solid 2: state plus attribute objects, no elements, no styling, each conformance-checked against its Quint spec",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|