@cssints/headless 0.12.0 → 0.13.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 +35 -0
- package/dist/index.d.mts +165 -0
- package/dist/index.mjs +197 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,6 +154,41 @@ const field = createField({
|
|
|
154
154
|
- **One refusal that belongs to two controls.** `describedby` is an id, or a list of ids, of an element the host owns, appended to `aria-describedby` after the field's own hint and error ids (`password-hint password-error form-error`) and dropped when empty. Give a form-level error ("The passwords do not match") one element with its own id and `role`, and pass that id to both fields: each control then names the same message, and it is announced once, not once per field. The field cannot see whether that element is shown, so pass the id only while it is (a getter: `get describedby() { return mismatch() ? "form-error" : undefined; }`); `describedIds()` includes it.
|
|
155
155
|
- **Getters that return `undefined` typecheck.** `id`, `required`, `invalid`, `hint`, `error` and `describedby` are all `T | undefined`, so a getter over an optional prop (`get required() { return props.required; }`) compiles under `exactOptionalPropertyTypes`.
|
|
156
156
|
|
|
157
|
+
## Text input and textarea
|
|
158
|
+
|
|
159
|
+
`createTextInput` and `createTextArea` are `createField` plus the props a host forgets. **There is no APG pattern for text editing** — an `<input>` already is the pattern — so there is no Quint spec: `test/textbox.browser.test.tsx` asserts the attribute objects and the element behaviour.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
const email = createTextInput({ type: "email", autoComplete: "email", inputMode: "email", required: true });
|
|
163
|
+
<label {...email.labelProps}>Email</label>
|
|
164
|
+
<input {...email.controlProps} />
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- Everything the field documents holds: ids, hint then error in `aria-describedby`, `aria-invalid` only when invalid, `role="alert"` on a shown error.
|
|
168
|
+
- The extra props: `type`, `autoComplete` (the HTML token list — the browser's autofill is the accessible path), `spellCheck`, `autoCapitalize`, `inputMode`, `maxLength`, `readOnly`, `disabled`, `required`, `name`, `dir`, `placeholder`, `onValueChange` (one callback for `input`).
|
|
169
|
+
- **`maxLength` is the browser's.** The primitive emits the attribute and reports the value it is given; it does not truncate in JS, because that breaks paste and IME.
|
|
170
|
+
- **`disabled` vs `readOnly`.** Disabled is out of the tab order and not submitted; readOnly is focusable, selectable and submitted. A field the user must read before signing is readOnly, not disabled.
|
|
171
|
+
- `createTextArea` adds `rows`, `wrap` and `resize` (`resize` is a style, since CSS has no attribute for it; `none` is what a layout that owns its own height wants).
|
|
172
|
+
|
|
173
|
+
## Pagination
|
|
174
|
+
|
|
175
|
+
`createPagination` is a navigation landmark over a page list. **No APG pattern exists**; the requirements are TetraLogical's (accessible-components-library v1.0.0), and `test/pagination.browser.test.tsx` is the oracle.
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const pages = createPagination({ page: 3, pageCount: 12, hrefFor: (page) => `/issues?page=${page}` });
|
|
179
|
+
<nav {...pages.navProps}>
|
|
180
|
+
<ul {...pages.listProps}>{/* items() with gaps, linkProps(page) */}</ul>
|
|
181
|
+
</nav>
|
|
182
|
+
<p {...pages.statusProps}>{pages.status()}</p>;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **One current page**, marked `aria-current="page"` on the item and its link; the current page is not a link target.
|
|
186
|
+
- **The ends stay in place** and carry `aria-disabled` at the boundary instead of disappearing: a control that vanishes moves every other control under the pointer.
|
|
187
|
+
- **One gap per elided run**, `siblings` (default 1) either side of the current page and `boundaries` (default 1) at each end.
|
|
188
|
+
- **The count is announced once**, in a `role="status"` line ("Page 3 of 12"), not in every link. Pass `labels.status` to translate it.
|
|
189
|
+
- **`hrefFor` is what makes it work without JS**: with it every page is a real link; without it they are buttons and the host owns navigation.
|
|
190
|
+
- **Target size is the skin's job**: 24x24 CSS px at AA, 44x44 at AAA.
|
|
191
|
+
|
|
157
192
|
## Confirm
|
|
158
193
|
|
|
159
194
|
`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).
|
package/dist/index.d.mts
CHANGED
|
@@ -1557,6 +1557,91 @@ export interface Field {
|
|
|
1557
1557
|
* Options are read when an attribute is read: pass getters, never destructure.
|
|
1558
1558
|
*/
|
|
1559
1559
|
export declare const createField: (options?: FieldOptions) => Field;
|
|
1560
|
+
export interface PaginationLabels {
|
|
1561
|
+
/** The navigation landmark's name; defaults to "Pagination". */
|
|
1562
|
+
readonly nav?: string | undefined;
|
|
1563
|
+
readonly previous?: string | undefined;
|
|
1564
|
+
readonly next?: string | undefined;
|
|
1565
|
+
/** The prefix of a page link's name; defaults to "Page". */
|
|
1566
|
+
readonly page?: string | undefined;
|
|
1567
|
+
/** The gap between page runs; defaults to "More pages". */
|
|
1568
|
+
readonly gap?: string | undefined;
|
|
1569
|
+
/** The live count; defaults to `Page <page> of <count>`. */
|
|
1570
|
+
readonly status?: ((page: number, count: number) => string) | undefined;
|
|
1571
|
+
}
|
|
1572
|
+
export interface PaginationOptions {
|
|
1573
|
+
/** The current page, 1-based. */
|
|
1574
|
+
readonly page: number;
|
|
1575
|
+
readonly pageCount: number;
|
|
1576
|
+
/** Pages shown on each side of the current one; defaults to 1. */
|
|
1577
|
+
readonly siblings?: number;
|
|
1578
|
+
/** Pages always shown at each end; defaults to 1. */
|
|
1579
|
+
readonly boundaries?: number;
|
|
1580
|
+
/** Where a page link points, when the host renders anchors; without it, buttons. */
|
|
1581
|
+
readonly hrefFor?: ((page: number) => string) | undefined;
|
|
1582
|
+
readonly disabled?: boolean;
|
|
1583
|
+
readonly labels?: PaginationLabels | undefined;
|
|
1584
|
+
readonly onPageChange?: ((page: number) => void) | undefined;
|
|
1585
|
+
}
|
|
1586
|
+
/** One entry of the page list: a page, or the run of pages the list elides. */
|
|
1587
|
+
export type PaginationItem = {
|
|
1588
|
+
readonly kind: "page";
|
|
1589
|
+
readonly page: number;
|
|
1590
|
+
readonly current: boolean;
|
|
1591
|
+
} | {
|
|
1592
|
+
readonly kind: "gap";
|
|
1593
|
+
readonly key: string;
|
|
1594
|
+
};
|
|
1595
|
+
export interface PaginationNavProps {
|
|
1596
|
+
readonly "aria-label": string;
|
|
1597
|
+
}
|
|
1598
|
+
export interface PaginationListProps {
|
|
1599
|
+
readonly role: "list";
|
|
1600
|
+
}
|
|
1601
|
+
export interface PaginationItemProps {
|
|
1602
|
+
readonly role: "listitem";
|
|
1603
|
+
readonly "aria-current": "page" | undefined;
|
|
1604
|
+
}
|
|
1605
|
+
export interface PaginationLinkProps {
|
|
1606
|
+
readonly href: string | undefined;
|
|
1607
|
+
readonly "aria-current": "page" | undefined;
|
|
1608
|
+
readonly "aria-disabled": "true" | undefined;
|
|
1609
|
+
readonly "aria-label": string;
|
|
1610
|
+
readonly rel: "prev" | "next" | undefined;
|
|
1611
|
+
readonly onClick: (event: {
|
|
1612
|
+
preventDefault: () => void;
|
|
1613
|
+
}) => void;
|
|
1614
|
+
}
|
|
1615
|
+
export interface PaginationStatusProps {
|
|
1616
|
+
readonly role: "status";
|
|
1617
|
+
readonly "aria-live": "polite";
|
|
1618
|
+
readonly "aria-atomic": "true";
|
|
1619
|
+
}
|
|
1620
|
+
export interface Pagination {
|
|
1621
|
+
/** The page list with the elided runs, recomputed when an option changes. */
|
|
1622
|
+
readonly items: () => readonly PaginationItem[];
|
|
1623
|
+
readonly current: () => number;
|
|
1624
|
+
readonly navProps: PaginationNavProps;
|
|
1625
|
+
readonly listProps: PaginationListProps;
|
|
1626
|
+
readonly itemProps: (page: number) => PaginationItemProps;
|
|
1627
|
+
readonly linkProps: (page: number) => PaginationLinkProps;
|
|
1628
|
+
readonly previousProps: PaginationLinkProps;
|
|
1629
|
+
readonly nextProps: PaginationLinkProps;
|
|
1630
|
+
/** The count, announced once here rather than in every link. */
|
|
1631
|
+
readonly status: () => string;
|
|
1632
|
+
readonly statusProps: PaginationStatusProps;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Pagination: a navigation landmark over a page list, the current page marked
|
|
1636
|
+
* `aria-current=page` and never a link. No APG pattern exists for it; the
|
|
1637
|
+
* requirements are TetraLogical's (accessible-components-library, v1.0.0):
|
|
1638
|
+
* exactly one current page, ends kept in place and disabled rather than
|
|
1639
|
+
* removed, the count announced once, and a host-supplied `href` for every link
|
|
1640
|
+
* so the control works without JS.
|
|
1641
|
+
*
|
|
1642
|
+
* Target size is the skin's job: 24x24 CSS px at AA, 44x44 at AAA.
|
|
1643
|
+
*/
|
|
1644
|
+
export declare const createPagination: (options: PaginationOptions) => Pagination;
|
|
1560
1645
|
/**
|
|
1561
1646
|
* APG grid primitive on Solid 2: roving focus, 2-D navigation, edit mode, a
|
|
1562
1647
|
* cell/row/column selection rectangle and sortable column headers. State plus
|
|
@@ -2581,6 +2666,86 @@ export interface Switch {
|
|
|
2581
2666
|
* pass `value` + `onValueChange` to own the state.
|
|
2582
2667
|
*/
|
|
2583
2668
|
export declare const createSwitch: (options?: SwitchOptions) => Switch;
|
|
2669
|
+
/** The HTML `autocomplete` tokens worth naming; the attribute also takes an arbitrary token. */
|
|
2670
|
+
export type AutoCompleteToken = "off" | "on" | "name" | "given-name" | "family-name" | "username" | "email" | "tel" | "url" | "organization" | "street-address" | "postal-code" | "current-password" | "new-password" | "one-time-code" | "nickname";
|
|
2671
|
+
export interface TextInputOptions extends FieldOptions {
|
|
2672
|
+
readonly type?: "text" | "email" | "tel" | "url" | "search" | "password";
|
|
2673
|
+
/** The HTML token list: the browser's own autofill is the accessible path. */
|
|
2674
|
+
readonly autoComplete?: AutoCompleteToken | undefined;
|
|
2675
|
+
readonly spellCheck?: boolean | undefined;
|
|
2676
|
+
readonly autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined;
|
|
2677
|
+
readonly inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
|
|
2678
|
+
readonly maxLength?: number | undefined;
|
|
2679
|
+
readonly readOnly?: boolean | undefined;
|
|
2680
|
+
readonly disabled?: boolean | undefined;
|
|
2681
|
+
readonly name?: string | undefined;
|
|
2682
|
+
readonly dir?: "ltr" | "rtl" | "auto" | undefined;
|
|
2683
|
+
readonly placeholder?: string | undefined;
|
|
2684
|
+
/** Change and input both land here; the host decides what a keystroke means. */
|
|
2685
|
+
readonly onValueChange?: ((value: string) => void) | undefined;
|
|
2686
|
+
}
|
|
2687
|
+
export interface TextAreaOptions extends TextInputOptions {
|
|
2688
|
+
readonly rows?: number | undefined;
|
|
2689
|
+
readonly wrap?: "soft" | "hard" | "off" | undefined;
|
|
2690
|
+
readonly resize?: "none" | "vertical" | "horizontal" | "both" | undefined;
|
|
2691
|
+
}
|
|
2692
|
+
export interface TextControlProps {
|
|
2693
|
+
readonly id: string;
|
|
2694
|
+
readonly "aria-describedby": string | undefined;
|
|
2695
|
+
readonly "aria-invalid": "true" | undefined;
|
|
2696
|
+
readonly "aria-required": "true" | undefined;
|
|
2697
|
+
readonly required: true | undefined;
|
|
2698
|
+
readonly type: TextInputOptions["type"];
|
|
2699
|
+
readonly autoComplete: AutoCompleteToken | undefined;
|
|
2700
|
+
readonly spellcheck: "true" | "false" | undefined;
|
|
2701
|
+
readonly autocapitalize: TextInputOptions["autoCapitalize"];
|
|
2702
|
+
readonly inputmode: TextInputOptions["inputMode"];
|
|
2703
|
+
readonly maxlength: number | undefined;
|
|
2704
|
+
readonly readOnly: true | undefined;
|
|
2705
|
+
readonly disabled: true | undefined;
|
|
2706
|
+
readonly name: string | undefined;
|
|
2707
|
+
readonly dir: "ltr" | "rtl" | "auto" | undefined;
|
|
2708
|
+
readonly placeholder: string | undefined;
|
|
2709
|
+
readonly onInput: (event: {
|
|
2710
|
+
currentTarget: {
|
|
2711
|
+
value: string;
|
|
2712
|
+
};
|
|
2713
|
+
}) => void;
|
|
2714
|
+
}
|
|
2715
|
+
export interface TextAreaControlProps extends TextControlProps {
|
|
2716
|
+
readonly rows: number | undefined;
|
|
2717
|
+
readonly wrap: TextAreaOptions["wrap"];
|
|
2718
|
+
readonly style: {
|
|
2719
|
+
readonly resize: "none" | "vertical" | "horizontal" | "both";
|
|
2720
|
+
} | undefined;
|
|
2721
|
+
}
|
|
2722
|
+
/** The field and the control, one object: every prop the element needs. */
|
|
2723
|
+
export interface TextInput extends Field {
|
|
2724
|
+
readonly controlProps: TextControlProps;
|
|
2725
|
+
}
|
|
2726
|
+
export interface TextArea extends Field {
|
|
2727
|
+
readonly controlProps: TextAreaControlProps;
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* A text input with the field contract attached: label, hint, error (announced),
|
|
2731
|
+
* `aria-describedby` and `aria-invalid` come from `createField`; this adds the
|
|
2732
|
+
* props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
|
|
2733
|
+
* disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
|
|
2734
|
+
* no primitive re-implements text editing, which is why there is no key
|
|
2735
|
+
* handling here).
|
|
2736
|
+
*
|
|
2737
|
+
* Both `required` and `aria-required` are emitted: the native attribute is what
|
|
2738
|
+
* the browser enforces, the ARIA one is what a custom widget reading the same
|
|
2739
|
+
* props needs. A host that validates itself passes `noValidate` on the form
|
|
2740
|
+
* rather than dropping the attribute.
|
|
2741
|
+
*/
|
|
2742
|
+
export declare const createTextInput: (options?: TextInputOptions) => TextInput;
|
|
2743
|
+
/**
|
|
2744
|
+
* The same contract for a textarea, plus rows, wrap and resize. `resize` is a
|
|
2745
|
+
* style because CSS has no attribute for it; `none` is the value a layout that
|
|
2746
|
+
* owns its own height wants.
|
|
2747
|
+
*/
|
|
2748
|
+
export declare const createTextArea: (options?: TextAreaOptions) => TextArea;
|
|
2584
2749
|
export type TabsOrientation = "horizontal" | "vertical";
|
|
2585
2750
|
export type TabsActivation = "automatic" | "manual";
|
|
2586
2751
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1048,7 +1048,7 @@ const RATE_PERCENTS = [
|
|
|
1048
1048
|
const SURFACE_SEEK_SECONDS = 5;
|
|
1049
1049
|
const SEEK_STEP_SECONDS = 1;
|
|
1050
1050
|
const SEEK_PAGE_SECONDS = 15;
|
|
1051
|
-
const clamp$
|
|
1051
|
+
const clamp$3 = (value, max) => value < 0 ? 0 : value > max ? max : value;
|
|
1052
1052
|
const mss = (seconds) => `${Math.floor(seconds / 60)}:${String(Math.floor(seconds) % 60).padStart(2, "0")}`;
|
|
1053
1053
|
/**
|
|
1054
1054
|
* APG media player primitive on Solid 2: playback state + attribute objects.
|
|
@@ -1085,7 +1085,7 @@ const createPlayer = (options) => {
|
|
|
1085
1085
|
const isActive = () => stage() === "playing" || stage() === "buffering";
|
|
1086
1086
|
const canPlay = () => stage() === "ready" || stage() === "paused" || stage() === "ended" || stage() === "buffering";
|
|
1087
1087
|
const moveTo = (seconds) => {
|
|
1088
|
-
setPosition(clamp$
|
|
1088
|
+
setPosition(clamp$3(seconds, duration()));
|
|
1089
1089
|
};
|
|
1090
1090
|
const play = () => {
|
|
1091
1091
|
if (!canPlay()) return;
|
|
@@ -1106,14 +1106,14 @@ const createPlayer = (options) => {
|
|
|
1106
1106
|
};
|
|
1107
1107
|
const commit = (seconds) => {
|
|
1108
1108
|
if (!known()) return;
|
|
1109
|
-
const target = clamp$
|
|
1109
|
+
const target = clamp$3(seconds, duration());
|
|
1110
1110
|
if (stage() === "ended") setStage("paused");
|
|
1111
1111
|
moveTo(target);
|
|
1112
1112
|
options.onSeek?.(target);
|
|
1113
1113
|
options.adapter.seek(target);
|
|
1114
1114
|
};
|
|
1115
1115
|
const setVolume = (value) => {
|
|
1116
|
-
requestVolume(clamp$
|
|
1116
|
+
requestVolume(clamp$3(value, 100));
|
|
1117
1117
|
};
|
|
1118
1118
|
const toggleMute = () => {
|
|
1119
1119
|
requestMuted(!muted());
|
|
@@ -2971,6 +2971,114 @@ const createField = (options = {}) => {
|
|
|
2971
2971
|
}
|
|
2972
2972
|
};
|
|
2973
2973
|
};
|
|
2974
|
+
// src/pagination.ts
|
|
2975
|
+
const clamp$2 = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
2976
|
+
/**
|
|
2977
|
+
* Pagination: a navigation landmark over a page list, the current page marked
|
|
2978
|
+
* `aria-current=page` and never a link. No APG pattern exists for it; the
|
|
2979
|
+
* requirements are TetraLogical's (accessible-components-library, v1.0.0):
|
|
2980
|
+
* exactly one current page, ends kept in place and disabled rather than
|
|
2981
|
+
* removed, the count announced once, and a host-supplied `href` for every link
|
|
2982
|
+
* so the control works without JS.
|
|
2983
|
+
*
|
|
2984
|
+
* Target size is the skin's job: 24x24 CSS px at AA, 44x44 at AAA.
|
|
2985
|
+
*/
|
|
2986
|
+
const createPagination = (options) => {
|
|
2987
|
+
const current = () => clamp$2(options.page, 1, Math.max(options.pageCount, 1));
|
|
2988
|
+
const label = (name, fallback) => options.labels?.[name] ?? fallback;
|
|
2989
|
+
const navigate = (page) => {
|
|
2990
|
+
if (options.disabled === true || page === current()) return;
|
|
2991
|
+
if (page < 1 || page > options.pageCount) return;
|
|
2992
|
+
options.onPageChange?.(page);
|
|
2993
|
+
};
|
|
2994
|
+
const pages = () => {
|
|
2995
|
+
const count = options.pageCount;
|
|
2996
|
+
const page = current();
|
|
2997
|
+
const siblings = Math.max(options.siblings ?? 1, 0);
|
|
2998
|
+
const boundaries = Math.max(options.boundaries ?? 1, 0);
|
|
2999
|
+
const first = Math.min(boundaries, count);
|
|
3000
|
+
const last = Math.max(count - boundaries + 1, 1);
|
|
3001
|
+
const from = Math.max(page - siblings, first + 1);
|
|
3002
|
+
const to = Math.min(page + siblings, last - 1);
|
|
3003
|
+
const items = [];
|
|
3004
|
+
for (let index = 1; index <= count; index++) {
|
|
3005
|
+
if (index <= first || index >= last || index >= from && index <= to) {
|
|
3006
|
+
items.push({
|
|
3007
|
+
kind: "page",
|
|
3008
|
+
page: index,
|
|
3009
|
+
current: index === page
|
|
3010
|
+
});
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
if (items.at(-1)?.kind !== "gap") items.push({
|
|
3014
|
+
kind: "gap",
|
|
3015
|
+
key: `gap-${index}`
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
return items;
|
|
3019
|
+
};
|
|
3020
|
+
const linkProps = (page) => ({
|
|
3021
|
+
get href() {
|
|
3022
|
+
return options.hrefFor?.(page);
|
|
3023
|
+
},
|
|
3024
|
+
get "aria-current"() {
|
|
3025
|
+
return page === current() ? "page" : void 0;
|
|
3026
|
+
},
|
|
3027
|
+
get "aria-disabled"() {
|
|
3028
|
+
const outside = page < 1 || page > options.pageCount;
|
|
3029
|
+
return options.disabled === true || outside ? "true" : void 0;
|
|
3030
|
+
},
|
|
3031
|
+
get "aria-label"() {
|
|
3032
|
+
return `${label("page", "Page")} ${page}`;
|
|
3033
|
+
},
|
|
3034
|
+
get rel() {},
|
|
3035
|
+
onClick: (event) => {
|
|
3036
|
+
event.preventDefault();
|
|
3037
|
+
navigate(page);
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
const endProps = (page, name, rel) => ({
|
|
3041
|
+
get href() {
|
|
3042
|
+
return options.hrefFor?.(page);
|
|
3043
|
+
},
|
|
3044
|
+
get "aria-current"() {},
|
|
3045
|
+
get "aria-disabled"() {
|
|
3046
|
+
const outside = page < 1 || page > options.pageCount;
|
|
3047
|
+
return options.disabled === true || outside ? "true" : void 0;
|
|
3048
|
+
},
|
|
3049
|
+
"aria-label": name,
|
|
3050
|
+
get rel() {
|
|
3051
|
+
return rel;
|
|
3052
|
+
},
|
|
3053
|
+
onClick: (event) => {
|
|
3054
|
+
event.preventDefault();
|
|
3055
|
+
navigate(page);
|
|
3056
|
+
}
|
|
3057
|
+
});
|
|
3058
|
+
return {
|
|
3059
|
+
items: pages,
|
|
3060
|
+
current,
|
|
3061
|
+
navProps: { get "aria-label"() {
|
|
3062
|
+
return label("nav", "Pagination");
|
|
3063
|
+
} },
|
|
3064
|
+
listProps: { role: "list" },
|
|
3065
|
+
itemProps: (page) => ({
|
|
3066
|
+
role: "listitem",
|
|
3067
|
+
get "aria-current"() {
|
|
3068
|
+
return page === current() ? "page" : void 0;
|
|
3069
|
+
}
|
|
3070
|
+
}),
|
|
3071
|
+
linkProps,
|
|
3072
|
+
previousProps: endProps(current() - 1, label("previous", "Previous page"), "prev"),
|
|
3073
|
+
nextProps: endProps(current() + 1, label("next", "Next page"), "next"),
|
|
3074
|
+
status: () => options.labels?.status?.(current(), options.pageCount) ?? `Page ${current()} of ${options.pageCount}`,
|
|
3075
|
+
statusProps: {
|
|
3076
|
+
role: "status",
|
|
3077
|
+
"aria-live": "polite",
|
|
3078
|
+
"aria-atomic": "true"
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
};
|
|
2974
3082
|
// src/grid.ts
|
|
2975
3083
|
const GRID_KEYS = /* @__PURE__ */ new Set([
|
|
2976
3084
|
"ArrowRight",
|
|
@@ -5525,6 +5633,90 @@ const createSwitch = (options = {}) => {
|
|
|
5525
5633
|
}
|
|
5526
5634
|
};
|
|
5527
5635
|
};
|
|
5636
|
+
// src/textbox.ts
|
|
5637
|
+
const control = (field, options) => ({
|
|
5638
|
+
...field.controlProps,
|
|
5639
|
+
get type() {
|
|
5640
|
+
return options.type ?? "text";
|
|
5641
|
+
},
|
|
5642
|
+
get autoComplete() {
|
|
5643
|
+
return options.autoComplete;
|
|
5644
|
+
},
|
|
5645
|
+
get spellcheck() {
|
|
5646
|
+
if (options.spellCheck === void 0) return;
|
|
5647
|
+
return options.spellCheck ? "true" : "false";
|
|
5648
|
+
},
|
|
5649
|
+
get autocapitalize() {
|
|
5650
|
+
return options.autoCapitalize;
|
|
5651
|
+
},
|
|
5652
|
+
get inputmode() {
|
|
5653
|
+
return options.inputMode;
|
|
5654
|
+
},
|
|
5655
|
+
get maxlength() {
|
|
5656
|
+
return options.maxLength;
|
|
5657
|
+
},
|
|
5658
|
+
get readOnly() {
|
|
5659
|
+
return options.readOnly === true ? true : void 0;
|
|
5660
|
+
},
|
|
5661
|
+
get disabled() {
|
|
5662
|
+
return options.disabled === true ? true : void 0;
|
|
5663
|
+
},
|
|
5664
|
+
get name() {
|
|
5665
|
+
return options.name;
|
|
5666
|
+
},
|
|
5667
|
+
get dir() {
|
|
5668
|
+
return options.dir;
|
|
5669
|
+
},
|
|
5670
|
+
get placeholder() {
|
|
5671
|
+
return options.placeholder;
|
|
5672
|
+
},
|
|
5673
|
+
onInput: (event) => {
|
|
5674
|
+
options.onValueChange?.(event.currentTarget.value);
|
|
5675
|
+
}
|
|
5676
|
+
});
|
|
5677
|
+
/**
|
|
5678
|
+
* A text input with the field contract attached: label, hint, error (announced),
|
|
5679
|
+
* `aria-describedby` and `aria-invalid` come from `createField`; this adds the
|
|
5680
|
+
* props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
|
|
5681
|
+
* disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
|
|
5682
|
+
* no primitive re-implements text editing, which is why there is no key
|
|
5683
|
+
* handling here).
|
|
5684
|
+
*
|
|
5685
|
+
* Both `required` and `aria-required` are emitted: the native attribute is what
|
|
5686
|
+
* the browser enforces, the ARIA one is what a custom widget reading the same
|
|
5687
|
+
* props needs. A host that validates itself passes `noValidate` on the form
|
|
5688
|
+
* rather than dropping the attribute.
|
|
5689
|
+
*/
|
|
5690
|
+
const createTextInput = (options = {}) => {
|
|
5691
|
+
const field = createField(options);
|
|
5692
|
+
return {
|
|
5693
|
+
...field,
|
|
5694
|
+
controlProps: control(field, options)
|
|
5695
|
+
};
|
|
5696
|
+
};
|
|
5697
|
+
/**
|
|
5698
|
+
* The same contract for a textarea, plus rows, wrap and resize. `resize` is a
|
|
5699
|
+
* style because CSS has no attribute for it; `none` is the value a layout that
|
|
5700
|
+
* owns its own height wants.
|
|
5701
|
+
*/
|
|
5702
|
+
const createTextArea = (options = {}) => {
|
|
5703
|
+
const field = createField(options);
|
|
5704
|
+
return {
|
|
5705
|
+
...field,
|
|
5706
|
+
controlProps: {
|
|
5707
|
+
...control(field, options),
|
|
5708
|
+
get rows() {
|
|
5709
|
+
return options.rows;
|
|
5710
|
+
},
|
|
5711
|
+
get wrap() {
|
|
5712
|
+
return options.wrap;
|
|
5713
|
+
},
|
|
5714
|
+
get style() {
|
|
5715
|
+
return options.resize === void 0 ? void 0 : { resize: options.resize };
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
};
|
|
5719
|
+
};
|
|
5528
5720
|
// src/tabs.ts
|
|
5529
5721
|
/**
|
|
5530
5722
|
* APG Tabs (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): state plus prop
|
|
@@ -7491,4 +7683,4 @@ const createRearrangeableListbox = (options) => {
|
|
|
7491
7683
|
})
|
|
7492
7684
|
};
|
|
7493
7685
|
};
|
|
7494
|
-
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 };
|
|
7686
|
+
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, createPagination, createPlayer, createRadioGroup, createRearrangeableListbox, createResizablePanels, createSlider, createSliderMultiThumb, createSpinbutton, createSwitch, createTabs, createTextArea, createTextInput, 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.13.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": {
|