@cssints/headless 0.19.0 → 0.20.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 +79 -2
- package/dist/index.d.mts +457 -203
- package/dist/index.mjs +877 -416
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2051,6 +2051,90 @@ const createCheckbox = (options = {}) => {
|
|
|
2051
2051
|
}
|
|
2052
2052
|
};
|
|
2053
2053
|
};
|
|
2054
|
+
// src/field.ts
|
|
2055
|
+
const text = (value) => value === void 0 || value === "" ? void 0 : value;
|
|
2056
|
+
const external = (value) => (value === void 0 ? [] : typeof value === "string" ? [value] : value).filter((id) => id !== "");
|
|
2057
|
+
/**
|
|
2058
|
+
* The ARIA contract of a form field: a label, a control, a hint and an error.
|
|
2059
|
+
* There is no APG pattern for it (APG has no "form field"; the nearest is the
|
|
2060
|
+
* label and description rules of the accessible name computation), so the
|
|
2061
|
+
* contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
|
|
2062
|
+
* `aria-required`) and HTML (`label[for]`, `required`), and from the
|
|
2063
|
+
* majority of Base UI Field, react-aria TextField and ariakit FormControl,
|
|
2064
|
+
* which list the description and the error message together. It is not checked
|
|
2065
|
+
* against a Quint spec; `test/field.browser.test.tsx` is its oracle.
|
|
2066
|
+
*
|
|
2067
|
+
* Decisions: the error does not replace the hint, both are listed, hint first
|
|
2068
|
+
* (the hint is what tells the user how to fix the error); an error implies
|
|
2069
|
+
* invalid and `invalid: false` overrides it; a hidden message is never
|
|
2070
|
+
* referenced, so `aria-describedby` has no dangling id. The error element is
|
|
2071
|
+
* `role="alert"` only while it is shown (a hidden element announces nothing, and
|
|
2072
|
+
* one that appears is announced); the hint is never an alert.
|
|
2073
|
+
*
|
|
2074
|
+
* `describedby` lists host-owned ids after the field's own: one refusal that
|
|
2075
|
+
* belongs to two controls is one form-level element (with its own `role`), named
|
|
2076
|
+
* by both. The field cannot know whether that element is shown: the host
|
|
2077
|
+
* passes the id only while it is.
|
|
2078
|
+
*
|
|
2079
|
+
* Options are read when an attribute is read: pass getters, never destructure.
|
|
2080
|
+
*/
|
|
2081
|
+
const createField = (options = {}) => {
|
|
2082
|
+
const base = options.id ?? createUniqueId();
|
|
2083
|
+
const controlId = `${base}-control`;
|
|
2084
|
+
const labelId = `${base}-label`;
|
|
2085
|
+
const hintId = `${base}-hint`;
|
|
2086
|
+
const errorId = `${base}-error`;
|
|
2087
|
+
const hint = () => text(options.hint);
|
|
2088
|
+
const message = () => text(options.error) ?? (typeof options.invalid === "string" ? text(options.invalid) : void 0);
|
|
2089
|
+
const invalid = () => options.invalid === false ? false : options.invalid === true || message() !== void 0;
|
|
2090
|
+
const error = () => invalid() ? message() : void 0;
|
|
2091
|
+
const describedIds = () => [
|
|
2092
|
+
...hint() === void 0 ? [] : [hintId],
|
|
2093
|
+
...error() === void 0 ? [] : [errorId],
|
|
2094
|
+
...external(options.describedby)
|
|
2095
|
+
];
|
|
2096
|
+
return {
|
|
2097
|
+
invalid,
|
|
2098
|
+
hint,
|
|
2099
|
+
error,
|
|
2100
|
+
describedIds,
|
|
2101
|
+
controlProps: {
|
|
2102
|
+
id: controlId,
|
|
2103
|
+
get "aria-describedby"() {
|
|
2104
|
+
const ids = describedIds();
|
|
2105
|
+
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
2106
|
+
},
|
|
2107
|
+
get "aria-invalid"() {
|
|
2108
|
+
return invalid() ? "true" : void 0;
|
|
2109
|
+
},
|
|
2110
|
+
get "aria-required"() {
|
|
2111
|
+
return options.required === true ? "true" : void 0;
|
|
2112
|
+
},
|
|
2113
|
+
get required() {
|
|
2114
|
+
return options.required === true ? true : void 0;
|
|
2115
|
+
}
|
|
2116
|
+
},
|
|
2117
|
+
labelProps: {
|
|
2118
|
+
id: labelId,
|
|
2119
|
+
for: controlId
|
|
2120
|
+
},
|
|
2121
|
+
hintProps: {
|
|
2122
|
+
id: hintId,
|
|
2123
|
+
get hidden() {
|
|
2124
|
+
return hint() === void 0;
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
errorProps: {
|
|
2128
|
+
id: errorId,
|
|
2129
|
+
get hidden() {
|
|
2130
|
+
return error() === void 0;
|
|
2131
|
+
},
|
|
2132
|
+
get role() {
|
|
2133
|
+
return error() === void 0 ? void 0 : "alert";
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
};
|
|
2054
2138
|
// src/checkbox-group.ts
|
|
2055
2139
|
const CHILDREN = [
|
|
2056
2140
|
"c0",
|
|
@@ -2089,7 +2173,17 @@ const nextFocus = (focus) => {
|
|
|
2089
2173
|
* pass `value` + `onValueChange` to own the checked subset. The checked set is
|
|
2090
2174
|
* packed internally so it rides the scalar `createControllable`.
|
|
2091
2175
|
*
|
|
2092
|
-
*
|
|
2176
|
+
* The name, the description and the error are `createField`'s contract on the
|
|
2177
|
+
* `role="group"` element (`aria-label` / `aria-labelledby`, `aria-describedby`,
|
|
2178
|
+
* `aria-invalid`, an error element that is `role="alert"` while it is shown), so
|
|
2179
|
+
* a group reads like the fields around it. The group is not a `<label for>`
|
|
2180
|
+
* target: a host names a visible label element's id with `labelledby`, or passes
|
|
2181
|
+
* `label` without one.
|
|
2182
|
+
*
|
|
2183
|
+
* Behaviour is checked against `specs/apg/checkbox-group.qnt` by the bridge. That
|
|
2184
|
+
* model leaves labelling out on purpose ("shift+Tab, accessible names,
|
|
2185
|
+
* fieldset/legend or role=group labelling" in its header), so the field contract
|
|
2186
|
+
* is checked by plain assertions in `test/checkbox-group.browser.test.tsx`.
|
|
2093
2187
|
*/
|
|
2094
2188
|
const createCheckboxGroup = (options) => {
|
|
2095
2189
|
const values = options.values;
|
|
@@ -2100,6 +2194,18 @@ const createCheckboxGroup = (options) => {
|
|
|
2100
2194
|
initial: packedOf(options.defaultValue ?? []),
|
|
2101
2195
|
onChange: () => (next) => options.onValueChange?.(labelsOf(next))
|
|
2102
2196
|
});
|
|
2197
|
+
const field = createField({
|
|
2198
|
+
id: options.id,
|
|
2199
|
+
get invalid() {
|
|
2200
|
+
return options.invalid;
|
|
2201
|
+
},
|
|
2202
|
+
get error() {
|
|
2203
|
+
return options.error;
|
|
2204
|
+
},
|
|
2205
|
+
get describedby() {
|
|
2206
|
+
return options.describedby;
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2103
2209
|
const [memory, setMemory] = createSignal(normalizeChildren([]));
|
|
2104
2210
|
const [remember, setRememberSignal] = createSignal(options.remember === true);
|
|
2105
2211
|
const [focused, setFocused] = createSignal("none");
|
|
@@ -2157,6 +2263,9 @@ const createCheckboxGroup = (options) => {
|
|
|
2157
2263
|
focused,
|
|
2158
2264
|
parentChecked,
|
|
2159
2265
|
checkedValues: () => selectedValues(children()),
|
|
2266
|
+
error: field.error,
|
|
2267
|
+
invalid: field.invalid,
|
|
2268
|
+
describedIds: field.describedIds,
|
|
2160
2269
|
click: (target) => {
|
|
2161
2270
|
activate(target);
|
|
2162
2271
|
},
|
|
@@ -2179,7 +2288,20 @@ const createCheckboxGroup = (options) => {
|
|
|
2179
2288
|
requestPacked(packed(normalizeChildren(next)));
|
|
2180
2289
|
},
|
|
2181
2290
|
groupProps: {
|
|
2291
|
+
id: field.controlProps.id,
|
|
2182
2292
|
role: "group",
|
|
2293
|
+
get "aria-label"() {
|
|
2294
|
+
return options.label;
|
|
2295
|
+
},
|
|
2296
|
+
get "aria-labelledby"() {
|
|
2297
|
+
return options.labelledby;
|
|
2298
|
+
},
|
|
2299
|
+
get "aria-describedby"() {
|
|
2300
|
+
return field.controlProps["aria-describedby"];
|
|
2301
|
+
},
|
|
2302
|
+
get "aria-invalid"() {
|
|
2303
|
+
return field.controlProps["aria-invalid"];
|
|
2304
|
+
},
|
|
2183
2305
|
onKeyDown: (event) => {
|
|
2184
2306
|
if (event.key === "Tab") {
|
|
2185
2307
|
event.preventDefault();
|
|
@@ -2190,6 +2312,7 @@ const createCheckboxGroup = (options) => {
|
|
|
2190
2312
|
}
|
|
2191
2313
|
}
|
|
2192
2314
|
},
|
|
2315
|
+
errorProps: field.errorProps,
|
|
2193
2316
|
parentProps: {
|
|
2194
2317
|
role: "checkbox",
|
|
2195
2318
|
get "aria-checked"() {
|
|
@@ -2506,81 +2629,481 @@ const createCombobox = (options) => {
|
|
|
2506
2629
|
}
|
|
2507
2630
|
}
|
|
2508
2631
|
};
|
|
2509
|
-
};
|
|
2510
|
-
// src/dialog-modal.ts
|
|
2511
|
-
/**
|
|
2512
|
-
* APG Dialog (Modal) on Solid 2. `modal` adds `aria-modal`. Focus moves into
|
|
2513
|
-
* the dialog on open and is the host's to restore on close (the fixture does,
|
|
2514
|
-
* for dismiss / Escape). The full stack/focus-trap machine lives in
|
|
2515
|
-
* `specs/apg/dialog-modal.qnt`; the generated model is the oracle in tests, not
|
|
2516
|
-
* loaded here.
|
|
2517
|
-
*
|
|
2518
|
-
* Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultOpen`);
|
|
2519
|
-
* pass `value` + `onOpenChange` to own the open state. `openDialog` / `close`
|
|
2520
|
-
* are requests the host may ignore.
|
|
2521
|
-
*/
|
|
2522
|
-
const createDialogModal = (options = {}) => {
|
|
2523
|
-
const base = options.id ?? createUniqueId();
|
|
2524
|
-
const modal = options.modal ?? true;
|
|
2525
|
-
const role = options.role ?? "dialog";
|
|
2526
|
-
const [open, requestOpen] = createControllable({
|
|
2527
|
-
value: () => options.value,
|
|
2528
|
-
initial: options.defaultOpen ?? false,
|
|
2529
|
-
onChange: () => options.onOpenChange
|
|
2632
|
+
};
|
|
2633
|
+
// src/dialog-modal.ts
|
|
2634
|
+
/**
|
|
2635
|
+
* APG Dialog (Modal) on Solid 2. `modal` adds `aria-modal`. Focus moves into
|
|
2636
|
+
* the dialog on open and is the host's to restore on close (the fixture does,
|
|
2637
|
+
* for dismiss / Escape). The full stack/focus-trap machine lives in
|
|
2638
|
+
* `specs/apg/dialog-modal.qnt`; the generated model is the oracle in tests, not
|
|
2639
|
+
* loaded here.
|
|
2640
|
+
*
|
|
2641
|
+
* Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultOpen`);
|
|
2642
|
+
* pass `value` + `onOpenChange` to own the open state. `openDialog` / `close`
|
|
2643
|
+
* are requests the host may ignore.
|
|
2644
|
+
*/
|
|
2645
|
+
const createDialogModal = (options = {}) => {
|
|
2646
|
+
const base = options.id ?? createUniqueId();
|
|
2647
|
+
const modal = options.modal ?? true;
|
|
2648
|
+
const role = options.role ?? "dialog";
|
|
2649
|
+
const [open, requestOpen] = createControllable({
|
|
2650
|
+
value: () => options.value,
|
|
2651
|
+
initial: options.defaultOpen ?? false,
|
|
2652
|
+
onChange: () => options.onOpenChange
|
|
2653
|
+
});
|
|
2654
|
+
const openDialog = () => requestOpen(true);
|
|
2655
|
+
const close = () => requestOpen(false);
|
|
2656
|
+
return {
|
|
2657
|
+
open,
|
|
2658
|
+
openDialog,
|
|
2659
|
+
close,
|
|
2660
|
+
triggerProps: {
|
|
2661
|
+
id: `${base}-trigger`,
|
|
2662
|
+
"aria-haspopup": "dialog",
|
|
2663
|
+
get "aria-expanded"() {
|
|
2664
|
+
return open() ? "true" : "false";
|
|
2665
|
+
},
|
|
2666
|
+
"aria-controls": `${base}-dialog`,
|
|
2667
|
+
onClick: () => openDialog(),
|
|
2668
|
+
onKeyDown: (event) => {
|
|
2669
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2670
|
+
event.preventDefault();
|
|
2671
|
+
openDialog();
|
|
2672
|
+
} else if (event.key === "Escape" && open()) close();
|
|
2673
|
+
}
|
|
2674
|
+
},
|
|
2675
|
+
dialogProps: {
|
|
2676
|
+
id: `${base}-dialog`,
|
|
2677
|
+
role,
|
|
2678
|
+
get "aria-modal"() {
|
|
2679
|
+
return modal && open() ? "true" : void 0;
|
|
2680
|
+
},
|
|
2681
|
+
"aria-labelledby": options.labelledby,
|
|
2682
|
+
"aria-describedby": options.describedby ?? (role === "alertdialog" ? "description" : void 0),
|
|
2683
|
+
tabindex: -1,
|
|
2684
|
+
get hidden() {
|
|
2685
|
+
return !open();
|
|
2686
|
+
},
|
|
2687
|
+
onKeyDown: (event) => {
|
|
2688
|
+
if (event.key === "Escape" && !options.keyboardDismissDisabled) {
|
|
2689
|
+
event.preventDefault();
|
|
2690
|
+
close();
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
},
|
|
2694
|
+
backdropProps: {
|
|
2695
|
+
id: `${base}-backdrop`,
|
|
2696
|
+
get hidden() {
|
|
2697
|
+
return !open();
|
|
2698
|
+
},
|
|
2699
|
+
onClick: () => {
|
|
2700
|
+
if (options.dismissable ?? true) close();
|
|
2701
|
+
}
|
|
2702
|
+
},
|
|
2703
|
+
dismissProps: {
|
|
2704
|
+
id: `${base}-dismiss`,
|
|
2705
|
+
type: "button",
|
|
2706
|
+
onClick: () => close()
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
};
|
|
2710
|
+
// src/hotkeys.ts
|
|
2711
|
+
const MODIFIERS = /* @__PURE__ */ new Set([
|
|
2712
|
+
"mod",
|
|
2713
|
+
"ctrl",
|
|
2714
|
+
"alt",
|
|
2715
|
+
"shift",
|
|
2716
|
+
"meta"
|
|
2717
|
+
]);
|
|
2718
|
+
const SHIFTED = /* @__PURE__ */ new Set("~!@#$%^&*()_+{}|:\"<>?");
|
|
2719
|
+
const detectsApple = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
|
|
2720
|
+
const parse = (keys, isApple) => {
|
|
2721
|
+
const parts = keys.toLowerCase().split("+");
|
|
2722
|
+
const key = parts.pop() ?? "";
|
|
2723
|
+
if (key === "" || MODIFIERS.has(key)) throw new TypeError(`hotkeys: "${keys}" ends with no key`);
|
|
2724
|
+
const chord = {
|
|
2725
|
+
ctrl: false,
|
|
2726
|
+
meta: false,
|
|
2727
|
+
alt: false,
|
|
2728
|
+
shift: SHIFTED.has(key),
|
|
2729
|
+
key
|
|
2730
|
+
};
|
|
2731
|
+
for (const part of parts) switch (part) {
|
|
2732
|
+
case "mod":
|
|
2733
|
+
if (isApple) chord.meta = true;
|
|
2734
|
+
else chord.ctrl = true;
|
|
2735
|
+
break;
|
|
2736
|
+
case "ctrl":
|
|
2737
|
+
chord.ctrl = true;
|
|
2738
|
+
break;
|
|
2739
|
+
case "meta":
|
|
2740
|
+
chord.meta = true;
|
|
2741
|
+
break;
|
|
2742
|
+
case "alt":
|
|
2743
|
+
chord.alt = true;
|
|
2744
|
+
break;
|
|
2745
|
+
case "shift":
|
|
2746
|
+
chord.shift = true;
|
|
2747
|
+
break;
|
|
2748
|
+
default: throw new TypeError(`hotkeys: "${part}" in "${keys}" is not a modifier`);
|
|
2749
|
+
}
|
|
2750
|
+
return chord;
|
|
2751
|
+
};
|
|
2752
|
+
const matches = (chord, event) => event.key.toLowerCase() === chord.key && event.ctrlKey === chord.ctrl && event.metaKey === chord.meta && event.altKey === chord.alt && event.shiftKey === chord.shift;
|
|
2753
|
+
const editable = (target) => target instanceof HTMLElement && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable);
|
|
2754
|
+
const inScope = (scope, target) => scope === void 0 || target instanceof Node && scope.contains(target);
|
|
2755
|
+
/**
|
|
2756
|
+
* One `keydown` listener in the capture phase, one list of chords. A chord
|
|
2757
|
+
* fires when its modifiers match exactly, its `when` holds, its scope contains
|
|
2758
|
+
* the event's target, and the focus is not in a text field (unless
|
|
2759
|
+
* `allowInEditable`). The most recently bound match is the only one that fires,
|
|
2760
|
+
* so a modal's `"/"` overrides the page's and unbinding it brings the page's
|
|
2761
|
+
* back; `preventDefault` is on by default for the bindings whose chord the
|
|
2762
|
+
* browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
|
|
2763
|
+
*/
|
|
2764
|
+
const createHotkeys = (options = {}) => {
|
|
2765
|
+
const isApple = options.isApple ?? detectsApple();
|
|
2766
|
+
const bindings = [];
|
|
2767
|
+
let listening = false;
|
|
2768
|
+
let disposed = false;
|
|
2769
|
+
const onKeyDown = (event) => {
|
|
2770
|
+
for (let i = bindings.length - 1; i >= 0; i--) {
|
|
2771
|
+
const entry = bindings[i];
|
|
2772
|
+
if (entry === void 0) continue;
|
|
2773
|
+
const { chord, binding } = entry;
|
|
2774
|
+
if (!matches(chord, event) || binding.when?.() === false) continue;
|
|
2775
|
+
if (!inScope(binding.scope, event.target)) continue;
|
|
2776
|
+
if (binding.allowInEditable !== true && editable(event.target)) continue;
|
|
2777
|
+
if (binding.preventDefault !== false) event.preventDefault();
|
|
2778
|
+
binding.onTrigger(event);
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
const stop = () => {
|
|
2783
|
+
if (!listening) return;
|
|
2784
|
+
listening = false;
|
|
2785
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
2786
|
+
};
|
|
2787
|
+
return {
|
|
2788
|
+
bind: (binding) => {
|
|
2789
|
+
if (disposed) return () => {};
|
|
2790
|
+
const entry = {
|
|
2791
|
+
chord: parse(binding.keys, isApple),
|
|
2792
|
+
binding
|
|
2793
|
+
};
|
|
2794
|
+
bindings.push(entry);
|
|
2795
|
+
if (!listening) {
|
|
2796
|
+
listening = true;
|
|
2797
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
2798
|
+
}
|
|
2799
|
+
return () => {
|
|
2800
|
+
const at = bindings.indexOf(entry);
|
|
2801
|
+
if (at === -1) return;
|
|
2802
|
+
bindings.splice(at, 1);
|
|
2803
|
+
if (bindings.length === 0) stop();
|
|
2804
|
+
};
|
|
2805
|
+
},
|
|
2806
|
+
dispose: () => {
|
|
2807
|
+
disposed = true;
|
|
2808
|
+
bindings.length = 0;
|
|
2809
|
+
stop();
|
|
2810
|
+
}
|
|
2811
|
+
};
|
|
2812
|
+
};
|
|
2813
|
+
// src/status.ts
|
|
2814
|
+
/**
|
|
2815
|
+
* APG Alert's polite sibling: one `role=status` live region whose text the host
|
|
2816
|
+
* replaces ("Link copied", "Saved"). It is not a queue: a second message shows
|
|
2817
|
+
* in place of the first, so a burst reads as the last state, not as N alerts.
|
|
2818
|
+
* The region is rendered once and stays mounted, which is what makes a change
|
|
2819
|
+
* of its text announced.
|
|
2820
|
+
*
|
|
2821
|
+
* A status is never for errors: an error that needs attention now is
|
|
2822
|
+
* `createAlert` (`role=alert`, assertive), and a message with a title, a
|
|
2823
|
+
* description and a timeout is `createToast`.
|
|
2824
|
+
*/
|
|
2825
|
+
const createStatus = (options = {}) => {
|
|
2826
|
+
const id = options.id ?? createUniqueId();
|
|
2827
|
+
const [message, setMessage] = createSignal();
|
|
2828
|
+
return {
|
|
2829
|
+
message,
|
|
2830
|
+
show: (text) => {
|
|
2831
|
+
setMessage(text);
|
|
2832
|
+
},
|
|
2833
|
+
clear: () => {
|
|
2834
|
+
setMessage(void 0);
|
|
2835
|
+
},
|
|
2836
|
+
statusProps: {
|
|
2837
|
+
id,
|
|
2838
|
+
role: "status",
|
|
2839
|
+
"aria-live": "polite",
|
|
2840
|
+
"aria-atomic": "true"
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2843
|
+
};
|
|
2844
|
+
// src/textbox.ts
|
|
2845
|
+
const control = (field, options) => ({
|
|
2846
|
+
...field.controlProps,
|
|
2847
|
+
get type() {
|
|
2848
|
+
return options.type ?? "text";
|
|
2849
|
+
},
|
|
2850
|
+
get autoComplete() {
|
|
2851
|
+
return options.autoComplete;
|
|
2852
|
+
},
|
|
2853
|
+
get spellcheck() {
|
|
2854
|
+
if (options.spellCheck === void 0) return;
|
|
2855
|
+
return options.spellCheck ? "true" : "false";
|
|
2856
|
+
},
|
|
2857
|
+
get autocapitalize() {
|
|
2858
|
+
return options.autoCapitalize;
|
|
2859
|
+
},
|
|
2860
|
+
get inputmode() {
|
|
2861
|
+
return options.inputMode;
|
|
2862
|
+
},
|
|
2863
|
+
get maxlength() {
|
|
2864
|
+
return options.maxLength;
|
|
2865
|
+
},
|
|
2866
|
+
get readOnly() {
|
|
2867
|
+
return options.readOnly === true ? true : void 0;
|
|
2868
|
+
},
|
|
2869
|
+
get disabled() {
|
|
2870
|
+
return options.disabled === true ? true : void 0;
|
|
2871
|
+
},
|
|
2872
|
+
get name() {
|
|
2873
|
+
return options.name;
|
|
2874
|
+
},
|
|
2875
|
+
get dir() {
|
|
2876
|
+
return options.dir;
|
|
2877
|
+
},
|
|
2878
|
+
get placeholder() {
|
|
2879
|
+
return options.placeholder;
|
|
2880
|
+
},
|
|
2881
|
+
onInput: (event) => {
|
|
2882
|
+
options.onValueChange?.(event.currentTarget.value);
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
/**
|
|
2886
|
+
* A text input with the field contract attached: label, hint, error (announced),
|
|
2887
|
+
* `aria-describedby` and `aria-invalid` come from `createField`; this adds the
|
|
2888
|
+
* props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
|
|
2889
|
+
* disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
|
|
2890
|
+
* no primitive re-implements text editing, which is why there is no key
|
|
2891
|
+
* handling here).
|
|
2892
|
+
*
|
|
2893
|
+
* Both `required` and `aria-required` are emitted: the native attribute is what
|
|
2894
|
+
* the browser enforces, the ARIA one is what a custom widget reading the same
|
|
2895
|
+
* props needs. A host that validates itself passes `noValidate` on the form
|
|
2896
|
+
* rather than dropping the attribute.
|
|
2897
|
+
*/
|
|
2898
|
+
const createTextInput = (options = {}) => {
|
|
2899
|
+
const field = createField(options);
|
|
2900
|
+
return {
|
|
2901
|
+
...field,
|
|
2902
|
+
controlProps: control(field, options)
|
|
2903
|
+
};
|
|
2904
|
+
};
|
|
2905
|
+
/**
|
|
2906
|
+
* The same contract for a textarea, plus rows, wrap and resize. `resize` is a
|
|
2907
|
+
* style because CSS has no attribute for it; `none` is the value a layout that
|
|
2908
|
+
* owns its own height wants.
|
|
2909
|
+
*/
|
|
2910
|
+
const createTextArea = (options = {}) => {
|
|
2911
|
+
const field = createField(options);
|
|
2912
|
+
return {
|
|
2913
|
+
...field,
|
|
2914
|
+
controlProps: {
|
|
2915
|
+
...control(field, options),
|
|
2916
|
+
get rows() {
|
|
2917
|
+
return options.rows;
|
|
2918
|
+
},
|
|
2919
|
+
get wrap() {
|
|
2920
|
+
return options.wrap;
|
|
2921
|
+
},
|
|
2922
|
+
get style() {
|
|
2923
|
+
return options.resize === void 0 ? void 0 : { resize: options.resize };
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
};
|
|
2928
|
+
// src/command-palette.ts
|
|
2929
|
+
const countLabel = (count) => count === 0 ? "No commands" : count === 1 ? "1 command" : `${count} commands`;
|
|
2930
|
+
/**
|
|
2931
|
+
* A command palette: `createDialogModal` (a modal dialog, announced as one),
|
|
2932
|
+
* `createTextInput` (a `searchbox`, not a combobox — choosing runs a command,
|
|
2933
|
+
* it does not complete the field) and one filtered list. Focus stays in the
|
|
2934
|
+
* field; `aria-activedescendant` names the highlighted option, and ↓ on the
|
|
2935
|
+
* last / ↑ on the first return the highlight to the field. Home/End are menu
|
|
2936
|
+
* keys only while the field is empty, so the caret keeps them while typing.
|
|
2937
|
+
* Filtering is a plain case-insensitive substring match on the label and the
|
|
2938
|
+
* keywords, no fuzzy scoring: a palette is short and stable, and ranking
|
|
2939
|
+
* (recency, scopes) is a policy the host owns.
|
|
2940
|
+
*
|
|
2941
|
+
* The host's: rendering and grouping (the `group` field, headings, the CSS),
|
|
2942
|
+
* async commands (a rejected `run` is the host's to report), and any close
|
|
2943
|
+
* other than Escape / a command. `runActive` closes before it runs.
|
|
2944
|
+
*/
|
|
2945
|
+
const createCommandPalette = (options) => {
|
|
2946
|
+
const corpus = options.commands;
|
|
2947
|
+
const base = createUniqueId();
|
|
2948
|
+
const titleId = `${base}-title`;
|
|
2949
|
+
const listId = `${base}-list`;
|
|
2950
|
+
const optionId = (id) => `${base}-option-${id}`;
|
|
2951
|
+
const labels = {
|
|
2952
|
+
title: options.labels?.title ?? "Command palette",
|
|
2953
|
+
placeholder: options.labels?.placeholder ?? "Type a command",
|
|
2954
|
+
empty: options.labels?.empty ?? "No commands found",
|
|
2955
|
+
results: options.labels?.results ?? countLabel
|
|
2956
|
+
};
|
|
2957
|
+
const filter = (text) => {
|
|
2958
|
+
const needle = text.toLowerCase();
|
|
2959
|
+
return corpus.filter((command) => command.label.toLowerCase().includes(needle) || (command.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(needle))).map(({ id, label, group }) => ({
|
|
2960
|
+
id,
|
|
2961
|
+
label,
|
|
2962
|
+
group
|
|
2963
|
+
}));
|
|
2964
|
+
};
|
|
2965
|
+
const [query, setQueryRaw] = createSignal("");
|
|
2966
|
+
const [active, setActiveRaw] = createSignal(0);
|
|
2967
|
+
const results = () => filter(query());
|
|
2968
|
+
const dialog = createDialogModal({
|
|
2969
|
+
id: base,
|
|
2970
|
+
role: "dialog",
|
|
2971
|
+
labelledby: titleId,
|
|
2972
|
+
value: options.open,
|
|
2973
|
+
onOpenChange: (open) => options.onOpenChange?.(open)
|
|
2974
|
+
});
|
|
2975
|
+
const status = createStatus();
|
|
2976
|
+
const setQuery = (text) => {
|
|
2977
|
+
setQueryRaw(text);
|
|
2978
|
+
setActiveRaw(filter(text).length > 0 ? 0 : -1);
|
|
2979
|
+
};
|
|
2980
|
+
const run = (id) => {
|
|
2981
|
+
const chosen = corpus.find((command) => command.id === id);
|
|
2982
|
+
if (chosen === void 0) return;
|
|
2983
|
+
dialog.close();
|
|
2984
|
+
return chosen.run();
|
|
2985
|
+
};
|
|
2986
|
+
const runActive = () => {
|
|
2987
|
+
const current = results()[active()];
|
|
2988
|
+
return current === void 0 ? void 0 : run(current.id);
|
|
2989
|
+
};
|
|
2990
|
+
const onKeyDown = (event) => {
|
|
2991
|
+
if (event.isComposing) return;
|
|
2992
|
+
const last = results().length - 1;
|
|
2993
|
+
switch (event.key) {
|
|
2994
|
+
case "ArrowDown":
|
|
2995
|
+
event.preventDefault();
|
|
2996
|
+
setActiveRaw(active() < last ? active() + 1 : -1);
|
|
2997
|
+
return;
|
|
2998
|
+
case "ArrowUp":
|
|
2999
|
+
event.preventDefault();
|
|
3000
|
+
setActiveRaw(active() > 0 ? active() - 1 : last);
|
|
3001
|
+
return;
|
|
3002
|
+
case "Home":
|
|
3003
|
+
case "End":
|
|
3004
|
+
if (query() !== "") return;
|
|
3005
|
+
event.preventDefault();
|
|
3006
|
+
setActiveRaw(event.key === "Home" ? last >= 0 ? 0 : -1 : last);
|
|
3007
|
+
return;
|
|
3008
|
+
case "Enter":
|
|
3009
|
+
event.preventDefault();
|
|
3010
|
+
runActive();
|
|
3011
|
+
return;
|
|
3012
|
+
default: return;
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
let returnFocus = null;
|
|
3016
|
+
createEffect(() => dialog.open(), (isOpen) => {
|
|
3017
|
+
if (isOpen) {
|
|
3018
|
+
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3019
|
+
document.getElementById(`${base}-control`)?.focus();
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
setQueryRaw("");
|
|
3023
|
+
setActiveRaw(0);
|
|
3024
|
+
const back = returnFocus;
|
|
3025
|
+
returnFocus = null;
|
|
3026
|
+
if (back !== null && back.isConnected) back.focus();
|
|
3027
|
+
});
|
|
3028
|
+
createEffect(() => [dialog.open(), results().length], ([isOpen, count]) => {
|
|
3029
|
+
if (isOpen) status.show(labels.results(count));
|
|
3030
|
+
else status.clear();
|
|
3031
|
+
});
|
|
3032
|
+
const input = createTextInput({
|
|
3033
|
+
id: base,
|
|
3034
|
+
type: "text",
|
|
3035
|
+
autoComplete: "off",
|
|
3036
|
+
spellCheck: false,
|
|
3037
|
+
placeholder: labels.placeholder,
|
|
3038
|
+
onValueChange: setQuery
|
|
2530
3039
|
});
|
|
2531
|
-
const openDialog = () => requestOpen(true);
|
|
2532
|
-
const close = () => requestOpen(false);
|
|
2533
3040
|
return {
|
|
2534
|
-
open,
|
|
2535
|
-
openDialog,
|
|
2536
|
-
close,
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
} else if (event.key === "Escape" && open()) close();
|
|
2550
|
-
}
|
|
2551
|
-
},
|
|
2552
|
-
dialogProps: {
|
|
2553
|
-
id: `${base}-dialog`,
|
|
2554
|
-
role,
|
|
2555
|
-
get "aria-modal"() {
|
|
2556
|
-
return modal && open() ? "true" : void 0;
|
|
3041
|
+
open: dialog.open,
|
|
3042
|
+
openPalette: dialog.openDialog,
|
|
3043
|
+
closePalette: dialog.close,
|
|
3044
|
+
query,
|
|
3045
|
+
setQuery,
|
|
3046
|
+
active,
|
|
3047
|
+
setActive: (index) => setActiveRaw(Math.max(-1, Math.min(index, results().length - 1))),
|
|
3048
|
+
results,
|
|
3049
|
+
labels,
|
|
3050
|
+
status: status.message,
|
|
3051
|
+
inputProps: {
|
|
3052
|
+
...input.controlProps,
|
|
3053
|
+
role: "searchbox",
|
|
3054
|
+
get value() {
|
|
3055
|
+
return query();
|
|
2557
3056
|
},
|
|
2558
|
-
"aria-
|
|
2559
|
-
"aria-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
return !open();
|
|
3057
|
+
"aria-controls": listId,
|
|
3058
|
+
get "aria-activedescendant"() {
|
|
3059
|
+
const current = results()[active()];
|
|
3060
|
+
return current === void 0 ? void 0 : optionId(current.id);
|
|
2563
3061
|
},
|
|
2564
|
-
onKeyDown
|
|
2565
|
-
if (event.key === "Escape" && !options.keyboardDismissDisabled) {
|
|
2566
|
-
event.preventDefault();
|
|
2567
|
-
close();
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
3062
|
+
onKeyDown
|
|
2570
3063
|
},
|
|
2571
|
-
|
|
2572
|
-
id:
|
|
2573
|
-
|
|
2574
|
-
|
|
3064
|
+
listProps: {
|
|
3065
|
+
id: listId,
|
|
3066
|
+
role: "listbox",
|
|
3067
|
+
"aria-labelledby": titleId,
|
|
3068
|
+
onMouseDown: (event) => event.preventDefault()
|
|
3069
|
+
},
|
|
3070
|
+
optionProps: (id) => ({
|
|
3071
|
+
id: optionId(id),
|
|
3072
|
+
role: "option",
|
|
3073
|
+
get "aria-selected"() {
|
|
3074
|
+
return results()[active()]?.id === id ? "true" : "false";
|
|
2575
3075
|
},
|
|
3076
|
+
tabindex: -1,
|
|
2576
3077
|
onClick: () => {
|
|
2577
|
-
if (
|
|
3078
|
+
if (results().some((result) => result.id === id)) run(id);
|
|
3079
|
+
},
|
|
3080
|
+
onMouseEnter: () => {
|
|
3081
|
+
const index = results().findIndex((result) => result.id === id);
|
|
3082
|
+
if (index >= 0) setActiveRaw(index);
|
|
2578
3083
|
}
|
|
2579
|
-
},
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
3084
|
+
}),
|
|
3085
|
+
emptyProps: { get hidden() {
|
|
3086
|
+
return results().length > 0;
|
|
3087
|
+
} },
|
|
3088
|
+
statusProps: status.statusProps,
|
|
3089
|
+
dialogProps: dialog.dialogProps,
|
|
3090
|
+
backdropProps: dialog.backdropProps,
|
|
3091
|
+
titleProps: { id: titleId },
|
|
3092
|
+
runActive,
|
|
3093
|
+
attach: () => {
|
|
3094
|
+
const hotkeys = createHotkeys();
|
|
3095
|
+
const unbind = hotkeys.bind({
|
|
3096
|
+
keys: options.hotkey ?? "mod+k",
|
|
3097
|
+
allowInEditable: true,
|
|
3098
|
+
onTrigger: () => {
|
|
3099
|
+
if (dialog.open()) dialog.close();
|
|
3100
|
+
else dialog.openDialog();
|
|
3101
|
+
}
|
|
3102
|
+
});
|
|
3103
|
+
return () => {
|
|
3104
|
+
unbind();
|
|
3105
|
+
hotkeys.dispose();
|
|
3106
|
+
};
|
|
2584
3107
|
}
|
|
2585
3108
|
};
|
|
2586
3109
|
};
|
|
@@ -3233,90 +3756,6 @@ const createFeed = (options = {}) => {
|
|
|
3233
3756
|
}
|
|
3234
3757
|
};
|
|
3235
3758
|
};
|
|
3236
|
-
// src/field.ts
|
|
3237
|
-
const text = (value) => value === void 0 || value === "" ? void 0 : value;
|
|
3238
|
-
const external = (value) => (value === void 0 ? [] : typeof value === "string" ? [value] : value).filter((id) => id !== "");
|
|
3239
|
-
/**
|
|
3240
|
-
* The ARIA contract of a form field: a label, a control, a hint and an error.
|
|
3241
|
-
* There is no APG pattern for it (APG has no "form field"; the nearest is the
|
|
3242
|
-
* label and description rules of the accessible name computation), so the
|
|
3243
|
-
* contract comes from WAI-ARIA 1.2 (`aria-describedby`, `aria-invalid`,
|
|
3244
|
-
* `aria-required`) and HTML (`label[for]`, `required`), and from the
|
|
3245
|
-
* majority of Base UI Field, react-aria TextField and ariakit FormControl,
|
|
3246
|
-
* which list the description and the error message together. It is not checked
|
|
3247
|
-
* against a Quint spec; `test/field.browser.test.tsx` is its oracle.
|
|
3248
|
-
*
|
|
3249
|
-
* Decisions: the error does not replace the hint, both are listed, hint first
|
|
3250
|
-
* (the hint is what tells the user how to fix the error); an error implies
|
|
3251
|
-
* invalid and `invalid: false` overrides it; a hidden message is never
|
|
3252
|
-
* referenced, so `aria-describedby` has no dangling id. The error element is
|
|
3253
|
-
* `role="alert"` only while it is shown (a hidden element announces nothing, and
|
|
3254
|
-
* one that appears is announced); the hint is never an alert.
|
|
3255
|
-
*
|
|
3256
|
-
* `describedby` lists host-owned ids after the field's own: one refusal that
|
|
3257
|
-
* belongs to two controls is one form-level element (with its own `role`), named
|
|
3258
|
-
* by both. The field cannot know whether that element is shown: the host
|
|
3259
|
-
* passes the id only while it is.
|
|
3260
|
-
*
|
|
3261
|
-
* Options are read when an attribute is read: pass getters, never destructure.
|
|
3262
|
-
*/
|
|
3263
|
-
const createField = (options = {}) => {
|
|
3264
|
-
const base = options.id ?? createUniqueId();
|
|
3265
|
-
const controlId = `${base}-control`;
|
|
3266
|
-
const labelId = `${base}-label`;
|
|
3267
|
-
const hintId = `${base}-hint`;
|
|
3268
|
-
const errorId = `${base}-error`;
|
|
3269
|
-
const hint = () => text(options.hint);
|
|
3270
|
-
const message = () => text(options.error) ?? (typeof options.invalid === "string" ? text(options.invalid) : void 0);
|
|
3271
|
-
const invalid = () => options.invalid === false ? false : options.invalid === true || message() !== void 0;
|
|
3272
|
-
const error = () => invalid() ? message() : void 0;
|
|
3273
|
-
const describedIds = () => [
|
|
3274
|
-
...hint() === void 0 ? [] : [hintId],
|
|
3275
|
-
...error() === void 0 ? [] : [errorId],
|
|
3276
|
-
...external(options.describedby)
|
|
3277
|
-
];
|
|
3278
|
-
return {
|
|
3279
|
-
invalid,
|
|
3280
|
-
hint,
|
|
3281
|
-
error,
|
|
3282
|
-
describedIds,
|
|
3283
|
-
controlProps: {
|
|
3284
|
-
id: controlId,
|
|
3285
|
-
get "aria-describedby"() {
|
|
3286
|
-
const ids = describedIds();
|
|
3287
|
-
return ids.length === 0 ? void 0 : ids.join(" ");
|
|
3288
|
-
},
|
|
3289
|
-
get "aria-invalid"() {
|
|
3290
|
-
return invalid() ? "true" : void 0;
|
|
3291
|
-
},
|
|
3292
|
-
get "aria-required"() {
|
|
3293
|
-
return options.required === true ? "true" : void 0;
|
|
3294
|
-
},
|
|
3295
|
-
get required() {
|
|
3296
|
-
return options.required === true ? true : void 0;
|
|
3297
|
-
}
|
|
3298
|
-
},
|
|
3299
|
-
labelProps: {
|
|
3300
|
-
id: labelId,
|
|
3301
|
-
for: controlId
|
|
3302
|
-
},
|
|
3303
|
-
hintProps: {
|
|
3304
|
-
id: hintId,
|
|
3305
|
-
get hidden() {
|
|
3306
|
-
return hint() === void 0;
|
|
3307
|
-
}
|
|
3308
|
-
},
|
|
3309
|
-
errorProps: {
|
|
3310
|
-
id: errorId,
|
|
3311
|
-
get hidden() {
|
|
3312
|
-
return error() === void 0;
|
|
3313
|
-
},
|
|
3314
|
-
get role() {
|
|
3315
|
-
return error() === void 0 ? void 0 : "alert";
|
|
3316
|
-
}
|
|
3317
|
-
}
|
|
3318
|
-
};
|
|
3319
|
-
};
|
|
3320
3759
|
// src/pagination.ts
|
|
3321
3760
|
const clamp$2 = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
3322
3761
|
/**
|
|
@@ -4289,107 +4728,219 @@ const createGrid = (options = {}) => {
|
|
|
4289
4728
|
if (!flag) requestSort(-1);
|
|
4290
4729
|
}
|
|
4291
4730
|
};
|
|
4292
|
-
};
|
|
4293
|
-
// src/
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
const
|
|
4302
|
-
|
|
4303
|
-
const
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
* One `keydown` listener in the capture phase, one list of chords. A chord
|
|
4340
|
-
* fires when its modifiers match exactly, its `when` holds, its scope contains
|
|
4341
|
-
* the event's target, and the focus is not in a text field (unless
|
|
4342
|
-
* `allowInEditable`). The most recently bound match is the only one that fires,
|
|
4343
|
-
* so a modal's `"/"` overrides the page's and unbinding it brings the page's
|
|
4344
|
-
* back; `preventDefault` is on by default for the bindings whose chord the
|
|
4345
|
-
* browser would otherwise act on (Cmd/Ctrl+S, Cmd/Ctrl+K).
|
|
4731
|
+
};
|
|
4732
|
+
// src/knob.ts
|
|
4733
|
+
/** The pointer must travel this far before the drag picks a mode. */
|
|
4734
|
+
const LOCK_PX = 6;
|
|
4735
|
+
/** A slide is only locked while the other axis stays this small a share of the movement. */
|
|
4736
|
+
const SLIDE_VARIANCE = .35;
|
|
4737
|
+
/** The radius (px) at which a full turn sweeps the whole range; farther out is finer. */
|
|
4738
|
+
const REFERENCE_RADIUS = 100;
|
|
4739
|
+
/** The radius floor of the spin sensitivity, so a grab at the centre is not wildly coarse. */
|
|
4740
|
+
const MIN_SPIN_RADIUS = 25;
|
|
4741
|
+
/** Pixels of slide that move the value by the whole range. */
|
|
4742
|
+
const SLIDE_PX_PER_RANGE = 200;
|
|
4743
|
+
const angleOf = (x, y) => Math.atan2(y, x) * 180 / Math.PI;
|
|
4744
|
+
const angleDelta = (from, to) => {
|
|
4745
|
+
const diff = (to - from) % 360;
|
|
4746
|
+
return diff > 180 ? diff - 360 : diff < -180 ? diff + 360 : diff;
|
|
4747
|
+
};
|
|
4748
|
+
/**
|
|
4749
|
+
* A virtual knob after GarageBand's (https://jherrm.github.io/knobs/): one
|
|
4750
|
+
* control turned three ways — spun around its centre, or slid vertically or
|
|
4751
|
+
* horizontally, one gesture locked per drag. Plain Solid signals, no elements,
|
|
4752
|
+
* no styling. **No ARIA role exists for a dial**, so the knob is a `role=slider`
|
|
4753
|
+
* with `aria-valuenow` / `aria-valuetext`; the value the host renders in
|
|
4754
|
+
* `valueProps` is the live announcement (`role=status`), and a numeric field
|
|
4755
|
+
* must sit next to the dial, because a dial is never the only control.
|
|
4756
|
+
*
|
|
4757
|
+
* Request/commit (map ticket `rt1p`): uncontrolled by default (`defaultValue`);
|
|
4758
|
+
* pass `value` + `onValueChange` to own the value. A key, a wheel, a drag and
|
|
4759
|
+
* `setValue`/`stepBy` are requests the host may ignore.
|
|
4760
|
+
*
|
|
4761
|
+
* Gestures (`gesture`, default "both"), decided once on the first meaningful
|
|
4762
|
+
* movement so a drag never switches modes:
|
|
4763
|
+
* - a movement that stays on one axis (the other axis under `SLIDE_VARIANCE` of
|
|
4764
|
+
* it) locks a slide — the reference's "slide in a straight line";
|
|
4765
|
+
* - any other movement is a turn around the centre: the angle the pointer
|
|
4766
|
+
* covers is the value, and the farther from the centre the finer it is;
|
|
4767
|
+
* - `gesture: "spin"` / `"slide"` disable the other.
|
|
4768
|
+
*
|
|
4769
|
+
* A vertical slide turns the knob the way the grabbed side would turn a real
|
|
4770
|
+
* one: down on the right half is clockwise (up in value), down on the left half
|
|
4771
|
+
* counter-clockwise. A horizontal slide uses the same left/right side (the
|
|
4772
|
+
* reference picks top/bottom for this one; one rule for both is simpler to
|
|
4773
|
+
* learn). `wrap` makes the dial continuous instead of clamped.
|
|
4774
|
+
*
|
|
4775
|
+
* ponytail: no momentum/inertia and no `disabled` — the reference has a
|
|
4776
|
+
* deceleration tail, and no host here has asked for one; add inertia in the
|
|
4777
|
+
* host's rAF loop if a look needs it.
|
|
4346
4778
|
*/
|
|
4347
|
-
const
|
|
4348
|
-
const
|
|
4349
|
-
const
|
|
4350
|
-
|
|
4351
|
-
|
|
4779
|
+
const createKnob = (options = {}) => {
|
|
4780
|
+
const min = options.min ?? 0;
|
|
4781
|
+
const max = options.max ?? 100;
|
|
4782
|
+
const step = options.step ?? 1;
|
|
4783
|
+
const span = max - min;
|
|
4784
|
+
const base = options.id ?? createUniqueId();
|
|
4785
|
+
const wrap = () => options.wrap === true;
|
|
4786
|
+
const bound = (next) => {
|
|
4787
|
+
if (span <= 0) return min;
|
|
4788
|
+
if (wrap()) return min + ((next - min) % span + span) % span;
|
|
4789
|
+
return Math.max(min, Math.min(max, next));
|
|
4790
|
+
};
|
|
4791
|
+
const quantize = (next) => {
|
|
4792
|
+
const stepped = step > 0 ? min + Math.round((next - min) / step) * step : next;
|
|
4793
|
+
return bound(Number(stepped.toPrecision(12)));
|
|
4794
|
+
};
|
|
4795
|
+
const [value, request] = createControllable({
|
|
4796
|
+
value: () => options.value,
|
|
4797
|
+
initial: quantize(options.defaultValue ?? min + span / 2),
|
|
4798
|
+
onChange: () => options.onValueChange
|
|
4799
|
+
});
|
|
4800
|
+
const commit = (next) => request(quantize(next));
|
|
4801
|
+
const percentOf = (at) => {
|
|
4802
|
+
if (span <= 0) return 0;
|
|
4803
|
+
const fraction = (at - min) / span;
|
|
4804
|
+
return wrap() ? fraction - Math.floor(fraction) : Math.max(0, Math.min(1, fraction));
|
|
4805
|
+
};
|
|
4806
|
+
const stepBy = (delta) => commit(value() + delta);
|
|
4352
4807
|
const onKeyDown = (event) => {
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4808
|
+
switch (event.key) {
|
|
4809
|
+
case "ArrowUp":
|
|
4810
|
+
case "ArrowRight":
|
|
4811
|
+
stepBy(step);
|
|
4812
|
+
break;
|
|
4813
|
+
case "ArrowDown":
|
|
4814
|
+
case "ArrowLeft":
|
|
4815
|
+
stepBy(-step);
|
|
4816
|
+
break;
|
|
4817
|
+
case "PageUp":
|
|
4818
|
+
stepBy(step * 10);
|
|
4819
|
+
break;
|
|
4820
|
+
case "PageDown":
|
|
4821
|
+
stepBy(-step * 10);
|
|
4822
|
+
break;
|
|
4823
|
+
case "Home":
|
|
4824
|
+
commit(min);
|
|
4825
|
+
break;
|
|
4826
|
+
case "End":
|
|
4827
|
+
commit(max);
|
|
4828
|
+
break;
|
|
4829
|
+
default: return;
|
|
4363
4830
|
}
|
|
4831
|
+
event.preventDefault();
|
|
4364
4832
|
};
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4833
|
+
let drag;
|
|
4834
|
+
const modeFor = (current, event) => {
|
|
4835
|
+
if (options.gesture === "spin") return "spin";
|
|
4836
|
+
const dx = event.clientX - current.fromX;
|
|
4837
|
+
const dy = event.clientY - current.fromY;
|
|
4838
|
+
const vertical = Math.abs(dy);
|
|
4839
|
+
const horizontal = Math.abs(dx);
|
|
4840
|
+
const straight = Math.min(vertical, horizontal) <= Math.max(vertical, horizontal) * SLIDE_VARIANCE;
|
|
4841
|
+
if (options.gesture !== "slide" && !straight) return "spin";
|
|
4842
|
+
return vertical >= horizontal ? "vertical" : "horizontal";
|
|
4369
4843
|
};
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4844
|
+
const pointerDown = (event) => {
|
|
4845
|
+
if (event.pointerType === "mouse" && event.button !== 0) return;
|
|
4846
|
+
const element = event.currentTarget;
|
|
4847
|
+
if (!(element instanceof Element)) return;
|
|
4848
|
+
try {
|
|
4849
|
+
element.setPointerCapture(event.pointerId);
|
|
4850
|
+
} catch {}
|
|
4851
|
+
const rect = element.getBoundingClientRect();
|
|
4852
|
+
const centreX = rect.left + rect.width / 2;
|
|
4853
|
+
const centreY = rect.top + rect.height / 2;
|
|
4854
|
+
const startAngle = angleOf(event.clientX - centreX, event.clientY - centreY);
|
|
4855
|
+
drag = {
|
|
4856
|
+
pointerId: event.pointerId,
|
|
4857
|
+
centreX,
|
|
4858
|
+
centreY,
|
|
4859
|
+
side: event.clientX >= centreX ? 1 : -1,
|
|
4860
|
+
fromX: event.clientX,
|
|
4861
|
+
fromY: event.clientY,
|
|
4862
|
+
startAngle,
|
|
4863
|
+
raw: value(),
|
|
4864
|
+
mode: void 0,
|
|
4865
|
+
lastAngle: startAngle,
|
|
4866
|
+
lastX: event.clientX,
|
|
4867
|
+
lastY: event.clientY
|
|
4868
|
+
};
|
|
4869
|
+
};
|
|
4870
|
+
const pointerMove = (event) => {
|
|
4871
|
+
const current = drag;
|
|
4872
|
+
if (current === void 0 || current.pointerId !== event.pointerId) return;
|
|
4873
|
+
if (current.mode === void 0) {
|
|
4874
|
+
if (Math.hypot(event.clientX - current.fromX, event.clientY - current.fromY) < LOCK_PX) return;
|
|
4875
|
+
current.mode = modeFor(current, event);
|
|
4876
|
+
if (current.mode === "spin") current.lastAngle = current.startAngle;
|
|
4877
|
+
else {
|
|
4878
|
+
current.lastX = current.fromX;
|
|
4879
|
+
current.lastY = current.fromY;
|
|
4381
4880
|
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4881
|
+
}
|
|
4882
|
+
if (current.mode === "spin") {
|
|
4883
|
+
const angle = angleOf(event.clientX - current.centreX, event.clientY - current.centreY);
|
|
4884
|
+
const delta = angleDelta(current.lastAngle, angle);
|
|
4885
|
+
current.lastAngle = angle;
|
|
4886
|
+
const radius = Math.hypot(event.clientX - current.centreX, event.clientY - current.centreY);
|
|
4887
|
+
const perDegree = span / 360 * (REFERENCE_RADIUS / Math.max(radius, MIN_SPIN_RADIUS));
|
|
4888
|
+
current.raw = bound(current.raw + delta * perDegree);
|
|
4889
|
+
} else {
|
|
4890
|
+
const pixels = current.mode === "vertical" ? event.clientY - current.lastY : event.clientX - current.lastX;
|
|
4891
|
+
current.lastX = event.clientX;
|
|
4892
|
+
current.lastY = event.clientY;
|
|
4893
|
+
current.raw = bound(current.raw + pixels * (span / SLIDE_PX_PER_RANGE) * current.side);
|
|
4894
|
+
}
|
|
4895
|
+
commit(current.raw);
|
|
4896
|
+
};
|
|
4897
|
+
const pointerUp = (event) => {
|
|
4898
|
+
const current = drag;
|
|
4899
|
+
if (current === void 0 || current.pointerId !== event.pointerId) return;
|
|
4900
|
+
drag = void 0;
|
|
4901
|
+
const target = event.currentTarget;
|
|
4902
|
+
if (target instanceof Element) try {
|
|
4903
|
+
target.releasePointerCapture(event.pointerId);
|
|
4904
|
+
} catch {}
|
|
4905
|
+
};
|
|
4906
|
+
const onWheel = (event) => {
|
|
4907
|
+
if (event.deltaY === 0) return;
|
|
4908
|
+
event.preventDefault();
|
|
4909
|
+
stepBy(event.deltaY < 0 ? step : -step);
|
|
4910
|
+
};
|
|
4911
|
+
return {
|
|
4912
|
+
value,
|
|
4913
|
+
setValue: commit,
|
|
4914
|
+
percentOf,
|
|
4915
|
+
stepBy,
|
|
4916
|
+
knobProps: {
|
|
4917
|
+
role: "slider",
|
|
4918
|
+
get tabindex() {
|
|
4919
|
+
return 0;
|
|
4920
|
+
},
|
|
4921
|
+
"aria-valuemin": String(min),
|
|
4922
|
+
"aria-valuemax": String(max),
|
|
4923
|
+
get "aria-valuenow"() {
|
|
4924
|
+
return String(value());
|
|
4925
|
+
},
|
|
4926
|
+
get "aria-valuetext"() {
|
|
4927
|
+
const format = options.format;
|
|
4928
|
+
return format === void 0 ? String(value()) : format(value());
|
|
4929
|
+
},
|
|
4930
|
+
get "aria-label"() {
|
|
4931
|
+
return options.labels?.knob ?? "Knob";
|
|
4932
|
+
},
|
|
4933
|
+
onKeyDown,
|
|
4934
|
+
onPointerDown: pointerDown,
|
|
4935
|
+
onPointerMove: pointerMove,
|
|
4936
|
+
onPointerUp: pointerUp,
|
|
4937
|
+
onPointerCancel: pointerUp,
|
|
4938
|
+
onWheel
|
|
4388
4939
|
},
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4940
|
+
valueProps: {
|
|
4941
|
+
id: `${base}-value`,
|
|
4942
|
+
role: "status",
|
|
4943
|
+
"aria-live": "polite"
|
|
4393
4944
|
}
|
|
4394
4945
|
};
|
|
4395
4946
|
};
|
|
@@ -4820,9 +5371,11 @@ const createMenu = (options) => {
|
|
|
4820
5371
|
const disabled = new Set(options.disabled ?? []);
|
|
4821
5372
|
for (const [index, item] of items.entries()) if (item.disabled) disabled.add(index);
|
|
4822
5373
|
const held = (options.triggerMode ?? "outside") === "held";
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
5374
|
+
const [isOpen, requestOpen] = createControllable({
|
|
5375
|
+
value: () => options.open,
|
|
5376
|
+
initial: options.defaultOpen === true,
|
|
5377
|
+
onChange: () => options.onOpenChange
|
|
5378
|
+
});
|
|
4826
5379
|
let checked = new Set(options.defaultChecked ?? []);
|
|
4827
5380
|
const variantChoice = () => {
|
|
4828
5381
|
if (options.variant === "mixed") return 1;
|
|
@@ -4858,57 +5411,80 @@ const createMenu = (options) => {
|
|
|
4858
5411
|
version();
|
|
4859
5412
|
return read();
|
|
4860
5413
|
};
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
} else if (held) level = 1;
|
|
5414
|
+
let closed = held ? 1 : 0;
|
|
5415
|
+
let index = 0;
|
|
5416
|
+
let seenOpen = isOpen();
|
|
4865
5417
|
const isDisabled = (i) => disabled.has(i);
|
|
4866
|
-
const
|
|
4867
|
-
|
|
4868
|
-
|
|
5418
|
+
const sync = () => {
|
|
5419
|
+
const now = isOpen();
|
|
5420
|
+
if (now === seenOpen) return;
|
|
5421
|
+
seenOpen = now;
|
|
5422
|
+
index = now ? 0 : -1;
|
|
5423
|
+
closed = now ? closed : 1;
|
|
5424
|
+
};
|
|
5425
|
+
const level = () => {
|
|
5426
|
+
sync();
|
|
5427
|
+
return isOpen() ? 2 : closed;
|
|
5428
|
+
};
|
|
5429
|
+
const focusedIndex = () => {
|
|
5430
|
+
sync();
|
|
5431
|
+
return index;
|
|
5432
|
+
};
|
|
5433
|
+
const focusItem = (next) => {
|
|
5434
|
+
index = next;
|
|
4869
5435
|
bump();
|
|
4870
5436
|
};
|
|
5437
|
+
const move = (want, nextIndex, nextClosed) => {
|
|
5438
|
+
const apply = () => {
|
|
5439
|
+
seenOpen = isOpen();
|
|
5440
|
+
index = want ? nextIndex : -1;
|
|
5441
|
+
closed = nextClosed;
|
|
5442
|
+
bump();
|
|
5443
|
+
};
|
|
5444
|
+
if (isOpen() === want) {
|
|
5445
|
+
apply();
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
requestOpen(want);
|
|
5449
|
+
if (options.open === void 0) {
|
|
5450
|
+
apply();
|
|
5451
|
+
return;
|
|
5452
|
+
}
|
|
5453
|
+
queueMicrotask(() => {
|
|
5454
|
+
if (isOpen() === want) apply();
|
|
5455
|
+
});
|
|
5456
|
+
};
|
|
4871
5457
|
const openOn = (target) => {
|
|
4872
|
-
|
|
4873
|
-
index = target;
|
|
4874
|
-
open = true;
|
|
4875
|
-
bump();
|
|
4876
|
-
options.onOpenChange?.(true);
|
|
5458
|
+
move(true, target, 1);
|
|
4877
5459
|
};
|
|
4878
5460
|
const closeToTrigger = () => {
|
|
4879
|
-
|
|
4880
|
-
index = -1;
|
|
4881
|
-
open = false;
|
|
4882
|
-
bump();
|
|
4883
|
-
options.onOpenChange?.(false);
|
|
5461
|
+
move(false, -1, 1);
|
|
4884
5462
|
};
|
|
4885
5463
|
const focusTrigger = () => {
|
|
4886
|
-
if (level === 0)
|
|
5464
|
+
if (level() === 0) {
|
|
5465
|
+
closed = 1;
|
|
5466
|
+
bump();
|
|
5467
|
+
}
|
|
4887
5468
|
};
|
|
4888
5469
|
const leave = () => {
|
|
4889
|
-
|
|
4890
|
-
level = held ? 1 : 0;
|
|
4891
|
-
index = -1;
|
|
4892
|
-
open = false;
|
|
4893
|
-
bump();
|
|
4894
|
-
if (wasOpen) options.onOpenChange?.(false);
|
|
5470
|
+
move(false, -1, held ? 1 : 0);
|
|
4895
5471
|
};
|
|
4896
5472
|
const clickTrigger = () => {
|
|
4897
|
-
if (
|
|
5473
|
+
if (isOpen()) closeToTrigger();
|
|
4898
5474
|
else openOn(0);
|
|
4899
5475
|
};
|
|
4900
5476
|
const openMenu = () => {
|
|
4901
|
-
if (!
|
|
5477
|
+
if (!isOpen()) openOn(0);
|
|
4902
5478
|
};
|
|
4903
5479
|
const closeMenu = () => {
|
|
4904
|
-
if (
|
|
5480
|
+
if (isOpen()) closeToTrigger();
|
|
4905
5481
|
};
|
|
4906
5482
|
const activate = (close) => {
|
|
4907
|
-
if (level === 1) {
|
|
5483
|
+
if (level() === 1) {
|
|
4908
5484
|
openOn(0);
|
|
4909
5485
|
return;
|
|
4910
5486
|
}
|
|
4911
|
-
if (level !== 2 || index < 0 || isDisabled(index)) return;
|
|
5487
|
+
if (level() !== 2 || index < 0 || isDisabled(index)) return;
|
|
4912
5488
|
const item = items[index];
|
|
4913
5489
|
if (item === void 0) return;
|
|
4914
5490
|
if (item.kind === "checkbox") {
|
|
@@ -4949,23 +5525,23 @@ const createMenu = (options) => {
|
|
|
4949
5525
|
activate(false);
|
|
4950
5526
|
break;
|
|
4951
5527
|
case "ArrowDown":
|
|
4952
|
-
if (level === 1) openOn(0);
|
|
4953
|
-
else if (level === 2)
|
|
5528
|
+
if (level() === 1) openOn(0);
|
|
5529
|
+
else if (level() === 2) focusItem(wrapNext(focusedIndex()));
|
|
4954
5530
|
break;
|
|
4955
5531
|
case "ArrowUp":
|
|
4956
|
-
if (level === 1) openOn(count - 1);
|
|
4957
|
-
else if (level === 2)
|
|
5532
|
+
if (level() === 1) openOn(count - 1);
|
|
5533
|
+
else if (level() === 2) focusItem(wrapPrev(focusedIndex()));
|
|
4958
5534
|
break;
|
|
4959
5535
|
case "Home":
|
|
4960
|
-
if (level === 2)
|
|
5536
|
+
if (level() === 2) focusItem(0);
|
|
4961
5537
|
break;
|
|
4962
5538
|
case "End":
|
|
4963
|
-
if (level === 2)
|
|
5539
|
+
if (level() === 2) focusItem(count - 1);
|
|
4964
5540
|
break;
|
|
4965
5541
|
case "Escape":
|
|
4966
|
-
if (level === 2) closeToTrigger();
|
|
5542
|
+
if (level() === 2) closeToTrigger();
|
|
4967
5543
|
break;
|
|
4968
|
-
case "Tab": if (level !== 0) leave();
|
|
5544
|
+
case "Tab": if (level() !== 0) leave();
|
|
4969
5545
|
}
|
|
4970
5546
|
};
|
|
4971
5547
|
const search = (from, text) => {
|
|
@@ -4976,36 +5552,36 @@ const createMenu = (options) => {
|
|
|
4976
5552
|
}
|
|
4977
5553
|
};
|
|
4978
5554
|
const type = (text) => {
|
|
4979
|
-
if (!
|
|
5555
|
+
if (!isOpen() || text.length === 0) return;
|
|
4980
5556
|
buffer += text;
|
|
4981
5557
|
if (bufferTimer !== void 0) clearTimeout(bufferTimer);
|
|
4982
5558
|
bufferTimer = setTimeout(() => {
|
|
4983
5559
|
buffer = "";
|
|
4984
5560
|
}, prefs.typeaheadDelayMs);
|
|
4985
|
-
const
|
|
4986
|
-
|
|
5561
|
+
const from = focusedIndex() >= 0 ? focusedIndex() : 0;
|
|
5562
|
+
const match = search(from, buffer);
|
|
5563
|
+
if (match !== void 0) focusItem(match);
|
|
4987
5564
|
};
|
|
4988
5565
|
const codeOf = (i) => i % 2 === 0 ? 1 : 2;
|
|
4989
5566
|
const typeaheadCode = (code) => {
|
|
4990
|
-
if (!
|
|
4991
|
-
const from =
|
|
5567
|
+
if (!isOpen()) return;
|
|
5568
|
+
const from = focusedIndex() >= 0 ? focusedIndex() : 0;
|
|
4992
5569
|
for (let offset = 1; offset < count; offset += 1) {
|
|
4993
5570
|
const candidate = (from + offset) % count;
|
|
4994
5571
|
if (codeOf(candidate) === code) {
|
|
4995
|
-
|
|
5572
|
+
focusItem(candidate);
|
|
4996
5573
|
return;
|
|
4997
5574
|
}
|
|
4998
5575
|
}
|
|
4999
5576
|
};
|
|
5000
5577
|
const setFocusItem = (i) => {
|
|
5001
5578
|
if (i < 0 || i >= count) return;
|
|
5002
|
-
|
|
5003
|
-
setFocus(2, i);
|
|
5579
|
+
move(true, i, 1);
|
|
5004
5580
|
};
|
|
5005
5581
|
return {
|
|
5006
|
-
open: track(
|
|
5007
|
-
focusLevel: track(
|
|
5008
|
-
focusIndex: track(
|
|
5582
|
+
open: track(isOpen),
|
|
5583
|
+
focusLevel: track(level),
|
|
5584
|
+
focusIndex: track(focusedIndex),
|
|
5009
5585
|
items,
|
|
5010
5586
|
checked: track(() => [...currentChecked()].sort((a, b) => a - b)),
|
|
5011
5587
|
choice: track(() => currentChoice()),
|
|
@@ -5024,7 +5600,7 @@ const createMenu = (options) => {
|
|
|
5024
5600
|
"aria-haspopup": "menu",
|
|
5025
5601
|
get "aria-expanded"() {
|
|
5026
5602
|
version();
|
|
5027
|
-
return
|
|
5603
|
+
return isOpen() ? "true" : "false";
|
|
5028
5604
|
},
|
|
5029
5605
|
"aria-controls": menuId,
|
|
5030
5606
|
get disabled() {
|
|
@@ -5046,7 +5622,7 @@ const createMenu = (options) => {
|
|
|
5046
5622
|
role: "menu",
|
|
5047
5623
|
get hidden() {
|
|
5048
5624
|
version();
|
|
5049
|
-
return !
|
|
5625
|
+
return !isOpen();
|
|
5050
5626
|
},
|
|
5051
5627
|
onKeyDown: (event) => {
|
|
5052
5628
|
if (event.key === "Tab" || event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") event.preventDefault();
|
|
@@ -5072,7 +5648,7 @@ const createMenu = (options) => {
|
|
|
5072
5648
|
},
|
|
5073
5649
|
get tabindex() {
|
|
5074
5650
|
version();
|
|
5075
|
-
return level === 2 && index === i ? 0 : -1;
|
|
5651
|
+
return level() === 2 && index === i ? 0 : -1;
|
|
5076
5652
|
},
|
|
5077
5653
|
onClick: () => {
|
|
5078
5654
|
if (isDisabled(i)) return;
|
|
@@ -6530,37 +7106,6 @@ const createSpinbutton = (options = {}) => {
|
|
|
6530
7106
|
}
|
|
6531
7107
|
};
|
|
6532
7108
|
};
|
|
6533
|
-
// src/status.ts
|
|
6534
|
-
/**
|
|
6535
|
-
* APG Alert's polite sibling: one `role=status` live region whose text the host
|
|
6536
|
-
* replaces ("Link copied", "Saved"). It is not a queue: a second message shows
|
|
6537
|
-
* in place of the first, so a burst reads as the last state, not as N alerts.
|
|
6538
|
-
* The region is rendered once and stays mounted, which is what makes a change
|
|
6539
|
-
* of its text announced.
|
|
6540
|
-
*
|
|
6541
|
-
* A status is never for errors: an error that needs attention now is
|
|
6542
|
-
* `createAlert` (`role=alert`, assertive), and a message with a title, a
|
|
6543
|
-
* description and a timeout is `createToast`.
|
|
6544
|
-
*/
|
|
6545
|
-
const createStatus = (options = {}) => {
|
|
6546
|
-
const id = options.id ?? createUniqueId();
|
|
6547
|
-
const [message, setMessage] = createSignal();
|
|
6548
|
-
return {
|
|
6549
|
-
message,
|
|
6550
|
-
show: (text) => {
|
|
6551
|
-
setMessage(text);
|
|
6552
|
-
},
|
|
6553
|
-
clear: () => {
|
|
6554
|
-
setMessage(void 0);
|
|
6555
|
-
},
|
|
6556
|
-
statusProps: {
|
|
6557
|
-
id,
|
|
6558
|
-
role: "status",
|
|
6559
|
-
"aria-live": "polite",
|
|
6560
|
-
"aria-atomic": "true"
|
|
6561
|
-
}
|
|
6562
|
-
};
|
|
6563
|
-
};
|
|
6564
7109
|
// src/switch.ts
|
|
6565
7110
|
/**
|
|
6566
7111
|
* APG Switch (https://www.w3.org/WAI/ARIA/apg/patterns/switch/): a binary on/off
|
|
@@ -6815,90 +7360,6 @@ const createTable = (options) => {
|
|
|
6815
7360
|
toggleRow
|
|
6816
7361
|
};
|
|
6817
7362
|
};
|
|
6818
|
-
// src/textbox.ts
|
|
6819
|
-
const control = (field, options) => ({
|
|
6820
|
-
...field.controlProps,
|
|
6821
|
-
get type() {
|
|
6822
|
-
return options.type ?? "text";
|
|
6823
|
-
},
|
|
6824
|
-
get autoComplete() {
|
|
6825
|
-
return options.autoComplete;
|
|
6826
|
-
},
|
|
6827
|
-
get spellcheck() {
|
|
6828
|
-
if (options.spellCheck === void 0) return;
|
|
6829
|
-
return options.spellCheck ? "true" : "false";
|
|
6830
|
-
},
|
|
6831
|
-
get autocapitalize() {
|
|
6832
|
-
return options.autoCapitalize;
|
|
6833
|
-
},
|
|
6834
|
-
get inputmode() {
|
|
6835
|
-
return options.inputMode;
|
|
6836
|
-
},
|
|
6837
|
-
get maxlength() {
|
|
6838
|
-
return options.maxLength;
|
|
6839
|
-
},
|
|
6840
|
-
get readOnly() {
|
|
6841
|
-
return options.readOnly === true ? true : void 0;
|
|
6842
|
-
},
|
|
6843
|
-
get disabled() {
|
|
6844
|
-
return options.disabled === true ? true : void 0;
|
|
6845
|
-
},
|
|
6846
|
-
get name() {
|
|
6847
|
-
return options.name;
|
|
6848
|
-
},
|
|
6849
|
-
get dir() {
|
|
6850
|
-
return options.dir;
|
|
6851
|
-
},
|
|
6852
|
-
get placeholder() {
|
|
6853
|
-
return options.placeholder;
|
|
6854
|
-
},
|
|
6855
|
-
onInput: (event) => {
|
|
6856
|
-
options.onValueChange?.(event.currentTarget.value);
|
|
6857
|
-
}
|
|
6858
|
-
});
|
|
6859
|
-
/**
|
|
6860
|
-
* A text input with the field contract attached: label, hint, error (announced),
|
|
6861
|
-
* `aria-describedby` and `aria-invalid` come from `createField`; this adds the
|
|
6862
|
-
* props a host forgets — autofill tokens, spellcheck, inputmode, maxlength, the
|
|
6863
|
-
* disabled/readOnly pair. Platform: native (`input` is its own APG-equivalent;
|
|
6864
|
-
* no primitive re-implements text editing, which is why there is no key
|
|
6865
|
-
* handling here).
|
|
6866
|
-
*
|
|
6867
|
-
* Both `required` and `aria-required` are emitted: the native attribute is what
|
|
6868
|
-
* the browser enforces, the ARIA one is what a custom widget reading the same
|
|
6869
|
-
* props needs. A host that validates itself passes `noValidate` on the form
|
|
6870
|
-
* rather than dropping the attribute.
|
|
6871
|
-
*/
|
|
6872
|
-
const createTextInput = (options = {}) => {
|
|
6873
|
-
const field = createField(options);
|
|
6874
|
-
return {
|
|
6875
|
-
...field,
|
|
6876
|
-
controlProps: control(field, options)
|
|
6877
|
-
};
|
|
6878
|
-
};
|
|
6879
|
-
/**
|
|
6880
|
-
* The same contract for a textarea, plus rows, wrap and resize. `resize` is a
|
|
6881
|
-
* style because CSS has no attribute for it; `none` is the value a layout that
|
|
6882
|
-
* owns its own height wants.
|
|
6883
|
-
*/
|
|
6884
|
-
const createTextArea = (options = {}) => {
|
|
6885
|
-
const field = createField(options);
|
|
6886
|
-
return {
|
|
6887
|
-
...field,
|
|
6888
|
-
controlProps: {
|
|
6889
|
-
...control(field, options),
|
|
6890
|
-
get rows() {
|
|
6891
|
-
return options.rows;
|
|
6892
|
-
},
|
|
6893
|
-
get wrap() {
|
|
6894
|
-
return options.wrap;
|
|
6895
|
-
},
|
|
6896
|
-
get style() {
|
|
6897
|
-
return options.resize === void 0 ? void 0 : { resize: options.resize };
|
|
6898
|
-
}
|
|
6899
|
-
}
|
|
6900
|
-
};
|
|
6901
|
-
};
|
|
6902
7363
|
// src/tabs.ts
|
|
6903
7364
|
/**
|
|
6904
7365
|
* APG Tabs (https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): state plus prop
|
|
@@ -9274,4 +9735,4 @@ const createRearrangeableListbox = (options) => {
|
|
|
9274
9735
|
})
|
|
9275
9736
|
};
|
|
9276
9737
|
};
|
|
9277
|
-
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, createDrawer, createDropZone, createFeed, createField, createGrid, createHotkeys, createLink, createListbox, createMenu, createMenubar, createMeter, createPagination, createPanZoom, createPlayer, createPopover, createRadioGroup, createRearrangeableListbox, createResizablePanels, createScrollspy, createSlider, createSliderMultiThumb, createSpinbutton, createStatus, createSwitch, createTable, createTabs, createTextArea, createTextInput, createTimeline, createToast, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
|
|
9738
|
+
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, createCommandPalette, createConfirm, createDialogModal, createDisclosure, createDrawer, createDropZone, createFeed, createField, createGrid, createHotkeys, createKnob, createLink, createListbox, createMenu, createMenubar, createMeter, createPagination, createPanZoom, createPlayer, createPopover, createRadioGroup, createRearrangeableListbox, createResizablePanels, createScrollspy, createSlider, createSliderMultiThumb, createSpinbutton, createStatus, createSwitch, createTable, createTabs, createTextArea, createTextInput, createTimeline, createToast, createToolbar, createTooltipGroup, createTreegrid, createTreeview, createWindowSplitter, gridCandidates, headerPart, packSelection, packSort, rowPart, treegridCellPart, treegridFocusTargetPart, treegridRowPart, withPrefs };
|