@polyxd/react 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/action-bar.d.ts +16 -0
- package/dist/components/action-bar.js +96 -0
- package/dist/components/action-bar.js.map +1 -0
- package/dist/components/chart.js +133 -2
- package/dist/components/chart.js.map +1 -1
- package/dist/components/content.d.ts +16 -0
- package/dist/components/content.js +540 -19
- package/dist/components/content.js.map +1 -1
- package/dist/components/flow.d.ts +0 -4
- package/dist/components/flow.js +124 -6
- package/dist/components/flow.js.map +1 -1
- package/dist/components/index.js +7 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/inputs.d.ts +8 -0
- package/dist/components/inputs.js +334 -11
- package/dist/components/inputs.js.map +1 -1
- package/dist/components/marks.d.ts +40 -0
- package/dist/components/marks.js +221 -0
- package/dist/components/marks.js.map +1 -0
- package/dist/components/navigation.d.ts +2 -1
- package/dist/components/navigation.js +48 -12
- package/dist/components/navigation.js.map +1 -1
- package/dist/components/overlays.d.ts +18 -0
- package/dist/components/overlays.js +128 -0
- package/dist/components/overlays.js.map +1 -0
- package/dist/components/pickers.d.ts +10 -0
- package/dist/components/pickers.js +402 -0
- package/dist/components/pickers.js.map +1 -0
- package/dist/components/table.d.ts +5 -0
- package/dist/components/table.js +19 -10
- package/dist/components/table.js.map +1 -1
- package/dist/components/tree.d.ts +11 -0
- package/dist/components/tree.js +268 -0
- package/dist/components/tree.js.map +1 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.js.map +1 -1
- package/dist/format.d.ts +3 -1
- package/dist/format.js +19 -0
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/skeleton.d.ts +16 -0
- package/dist/skeleton.js +30 -0
- package/dist/skeleton.js.map +1 -0
- package/dist/surface.js +4 -3
- package/dist/surface.js.map +1 -1
- package/package.json +2 -2
- package/src/styles.css +2226 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId, useState } from "react";
|
|
2
|
+
import { createElement, useContext, useEffect, useId, useRef, useState } from "react";
|
|
3
3
|
import { Checkbox, RadioGroup, Slider, Switch } from "radix-ui";
|
|
4
|
-
import { resolveFormat, useBindings, useSurface } from "../context.js";
|
|
4
|
+
import { HeadingContext, resolveFormat, useBindings, useSurface } from "../context.js";
|
|
5
5
|
import { asList, absolute, childPointer, get } from "../data.js";
|
|
6
6
|
import { currencySymbol, formatValue } from "../format.js";
|
|
7
7
|
import { Render, useA11y } from "../surface.js";
|
|
@@ -52,6 +52,23 @@ export function TextInput({ node }) {
|
|
|
52
52
|
e.currentTarget.setCustomValidity(b.text(v.message));
|
|
53
53
|
};
|
|
54
54
|
const clear = (e) => e.currentTarget.setCustomValidity("");
|
|
55
|
+
const label = b.text(node.label);
|
|
56
|
+
const field = { label, help, required: node.required, id, helpId };
|
|
57
|
+
if (kind === "suggestions" || kind === "mentions")
|
|
58
|
+
return _jsx(CompletingInput, { node: node, field: field, common: common, mentions: kind === "mentions" });
|
|
59
|
+
if (kind === "tags")
|
|
60
|
+
return _jsx(TagsInput, { node: node, field: field, common: common });
|
|
61
|
+
if (kind === "richtext")
|
|
62
|
+
return _jsx(RichTextInput, { node: node, field: field, common: common });
|
|
63
|
+
if (kind === "inline")
|
|
64
|
+
return _jsx(InlineInput, { node: node, field: field, common: common });
|
|
65
|
+
if (kind === "masked") {
|
|
66
|
+
const mask = typeof node.mask === "string" ? node.mask : "";
|
|
67
|
+
return (_jsx(Field, { ...field, children: _jsx("input", { ...common, className: "pxd-input pxd-input-masked", type: "text", inputMode: /[A*]/.test(mask) ? undefined : "numeric", placeholder: mask || undefined, value: stored, maxLength: mask.length || undefined, onChange: (e) => b.write(node.value, applyMask(mask, e.target.value, e.target.value.length < stored.length)), onInvalid: invalid, onInput: clear }) }));
|
|
68
|
+
}
|
|
69
|
+
if (kind === "code") {
|
|
70
|
+
return (_jsx(Field, { ...field, children: _jsx("textarea", { ...common, className: "pxd-input pxd-input-code", rows: 4, spellCheck: false, autoCorrect: "off", autoCapitalize: "off", value: shown, maxLength: v.maxLength, minLength: v.minLength, onChange: (e) => onChange(e.target.value), onInvalid: invalid, onInput: clear }) }));
|
|
71
|
+
}
|
|
55
72
|
if (kind === "search") {
|
|
56
73
|
// A search field: icon and placeholder inside a pill; the label remains its accessible name.
|
|
57
74
|
const placeholder = node.placeholder !== undefined ? b.text(node.placeholder) : undefined;
|
|
@@ -62,7 +79,212 @@ export function TextInput({ node }) {
|
|
|
62
79
|
return (_jsxs("div", { className: "pxd-hero-field", children: [_jsx("label", { className: "pxd-hero-label", htmlFor: id, children: b.text(node.label) }), _jsxs("div", { className: "pxd-hero-input", children: [kind === "currency" && (_jsx("span", { className: "pxd-hero-symbol", "aria-hidden": "true", children: currencySymbol(b.text(node.currency) || "USD", s.locale) })), _jsx("input", { ...common, className: "pxd-hero-value", type: "text", inputMode: kind === "currency" ? "decimal" : "numeric", pattern: "-?[0-9]*[.,]?[0-9]*", placeholder: "0", size: Math.max(1, shown.length || 1), value: shown, onChange: (e) => onChange(e.target.value), onBlur: () => setDraft(null), onInvalid: invalid, onInput: clear })] }), help && (_jsx("p", { className: "pxd-field-help pxd-hero-help", id: helpId, children: help }))] }));
|
|
63
80
|
}
|
|
64
81
|
const control = kind === "multiline" ? (_jsx("textarea", { ...common, rows: 3, value: shown, maxLength: v.maxLength, minLength: v.minLength, onChange: (e) => onChange(e.target.value), onInvalid: invalid, onInput: clear })) : (_jsxs("div", { className: kind === "currency" ? "pxd-input-adorned" : undefined, children: [kind === "currency" && (_jsx("span", { className: "pxd-input-prefix", "aria-hidden": "true", children: currencySymbol(b.text(node.currency) || "USD", s.locale) })), _jsx("input", { ...common, type: INPUT_TYPE[kind] ?? "text", inputMode: kind === "currency" ? "decimal" : kind === "number" ? "numeric" : undefined, pattern: numeric ? "-?[0-9]*[.,]?[0-9]*" : undefined, value: shown, maxLength: v.maxLength, minLength: v.minLength, onChange: (e) => onChange(e.target.value), onBlur: () => setDraft(null), onInvalid: invalid, onInput: clear })] }));
|
|
65
|
-
return (_jsx(Field, { label:
|
|
82
|
+
return (_jsx(Field, { label: label, help: help, required: node.required, id: id, helpId: helpId, children: control }));
|
|
83
|
+
}
|
|
84
|
+
/** Fills a mask as you type: # takes a digit, A a letter, * either; other characters are typed for you. */
|
|
85
|
+
export function applyMask(mask, text, deleting = false) {
|
|
86
|
+
const chars = text.replace(/[^A-Za-z0-9]/g, "").split("");
|
|
87
|
+
let out = "";
|
|
88
|
+
let ci = 0;
|
|
89
|
+
for (const m of mask) {
|
|
90
|
+
if (ci >= chars.length)
|
|
91
|
+
break;
|
|
92
|
+
if (m === "#" || m === "A" || m === "*") {
|
|
93
|
+
const ok = m === "#" ? /\d/ : m === "A" ? /[A-Za-z]/ : /./;
|
|
94
|
+
while (ci < chars.length && !ok.test(chars[ci]))
|
|
95
|
+
ci++;
|
|
96
|
+
if (ci >= chars.length)
|
|
97
|
+
break;
|
|
98
|
+
out += chars[ci++];
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
out += m;
|
|
102
|
+
}
|
|
103
|
+
// Deleting past a literal would put it straight back; drop trailing literals so Backspace moves on.
|
|
104
|
+
if (deleting)
|
|
105
|
+
while (out.length && !/[#A*]/.test(mask[out.length - 1]))
|
|
106
|
+
out = out.slice(0, -1);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** A listbox under a text field, driven by the arrow keys; the option under the cursor is announced through aria-activedescendant. */
|
|
110
|
+
function useCombobox(options, pick) {
|
|
111
|
+
const listId = useId();
|
|
112
|
+
const [open, setOpen] = useState(false);
|
|
113
|
+
const [active, setActive] = useState(-1);
|
|
114
|
+
const shown = open && options.length > 0;
|
|
115
|
+
const onKeyDown = (e) => {
|
|
116
|
+
if (!shown) {
|
|
117
|
+
if (e.key === "ArrowDown" && options.length)
|
|
118
|
+
(setOpen(true), setActive(0), e.preventDefault());
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (e.key === "ArrowDown")
|
|
122
|
+
(setActive((a) => (a + 1) % options.length), e.preventDefault());
|
|
123
|
+
else if (e.key === "ArrowUp")
|
|
124
|
+
(setActive((a) => (a - 1 + options.length) % options.length), e.preventDefault());
|
|
125
|
+
else if (e.key === "Enter" && active >= 0)
|
|
126
|
+
(pick(options[active]), setOpen(false), e.preventDefault());
|
|
127
|
+
else if (e.key === "Escape")
|
|
128
|
+
setOpen(false);
|
|
129
|
+
};
|
|
130
|
+
const inputProps = {
|
|
131
|
+
role: "combobox",
|
|
132
|
+
"aria-expanded": shown,
|
|
133
|
+
"aria-controls": listId,
|
|
134
|
+
"aria-autocomplete": "list",
|
|
135
|
+
"aria-activedescendant": shown && active >= 0 ? `${listId}-${active}` : undefined,
|
|
136
|
+
autoComplete: "off",
|
|
137
|
+
onKeyDown,
|
|
138
|
+
onBlur: () => setOpen(false),
|
|
139
|
+
};
|
|
140
|
+
const list = shown ? (_jsx("ul", { id: listId, role: "listbox", className: "pxd-combobox-list", children: options.map((o, i) => (_jsxs("li", { id: `${listId}-${i}`, role: "option", "aria-selected": i === active, className: `pxd-combobox-option${i === active ? " pxd-combobox-active" : ""}`, onMouseDown: (e) => (e.preventDefault(), pick(o), setOpen(false)), children: [o.label, o.description && _jsx("span", { className: "pxd-option-description", children: o.description })] }, String(o.value)))) })) : null;
|
|
141
|
+
return { list, inputProps, show: (on) => (setOpen(on), setActive(-1)) };
|
|
142
|
+
}
|
|
143
|
+
/** Free text with completions ('suggestions'), or an @ that offers people ('mentions'). */
|
|
144
|
+
function CompletingInput({ node, field, common, mentions }) {
|
|
145
|
+
const b = useBindings();
|
|
146
|
+
const all = useOptions(node);
|
|
147
|
+
const text = String(b.value(node.value) ?? "");
|
|
148
|
+
// What is being completed: the whole value, or the word after the last @.
|
|
149
|
+
const at = mentions ? /@([^\s@]*)$/.exec(text) : null;
|
|
150
|
+
const query = mentions ? (at ? at[1] : null) : text;
|
|
151
|
+
const options = query === null ? [] : all.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())).slice(0, 8);
|
|
152
|
+
const combo = useCombobox(options, (o) => b.write(node.value, mentions && at ? `${text.slice(0, at.index)}@${o.label} ` : o.label));
|
|
153
|
+
return (_jsx(Field, { ...field, children: _jsxs("div", { className: "pxd-combobox", children: [_jsx("input", { ...common, ...combo.inputProps, type: "text", value: text, onChange: (e) => {
|
|
154
|
+
b.write(node.value, e.target.value);
|
|
155
|
+
combo.show(mentions ? /@[^\s@]*$/.test(e.target.value) : e.target.value.length > 0);
|
|
156
|
+
}, onFocus: () => combo.show(!mentions && text.length > 0) }), combo.list] }) }));
|
|
157
|
+
}
|
|
158
|
+
/** Several short values: Enter or a comma makes a chip of what was typed; Backspace in an empty field removes the last one. */
|
|
159
|
+
function TagsInput({ node, field, common }) {
|
|
160
|
+
const b = useBindings();
|
|
161
|
+
const raw = b.value(node.value);
|
|
162
|
+
const tags = Array.isArray(raw) ? raw.map(String) : [];
|
|
163
|
+
const [draft, setDraft] = useState("");
|
|
164
|
+
const add = () => {
|
|
165
|
+
const t = draft.trim().replace(/,$/, "").trim();
|
|
166
|
+
if (t && !tags.includes(t))
|
|
167
|
+
b.write(node.value, [...tags, t]);
|
|
168
|
+
setDraft("");
|
|
169
|
+
};
|
|
170
|
+
const remove = (i) => b.write(node.value, tags.filter((_, j) => j !== i));
|
|
171
|
+
return (_jsx(Field, { ...field, children: _jsxs("div", { className: "pxd-tags", role: "group", "aria-label": field.label, children: [_jsx("ul", { className: "pxd-tags-list", children: tags.map((t, i) => (_jsxs("li", { className: "pxd-tag-chip", children: [t, _jsx("button", { type: "button", className: "pxd-tag-remove", "aria-label": `Remove ${t}`, onClick: () => remove(i), children: _jsx(Icon, { name: "close", size: 14 }) })] }, `${t}-${i}`))) }), _jsx("input", { ...common, className: "pxd-tags-input", type: "text", value: draft, onChange: (e) => (e.target.value.endsWith(",") ? (setDraft(e.target.value), add()) : setDraft(e.target.value)), onKeyDown: (e) => {
|
|
172
|
+
if (e.key === "Enter")
|
|
173
|
+
(e.preventDefault(), add());
|
|
174
|
+
else if (e.key === "Backspace" && draft === "" && tags.length)
|
|
175
|
+
remove(tags.length - 1);
|
|
176
|
+
}, onBlur: add })] }) }));
|
|
177
|
+
}
|
|
178
|
+
/** Inline emphasis and lists to and from the markdown-ish text a 'richtext' value holds. */
|
|
179
|
+
function toMarkup(n) {
|
|
180
|
+
if (n.nodeType === 3)
|
|
181
|
+
return n.textContent ?? "";
|
|
182
|
+
if (n.nodeType !== 1)
|
|
183
|
+
return "";
|
|
184
|
+
const el = n;
|
|
185
|
+
const inner = (x = el) => Array.from(x.childNodes).map(toMarkup).join("");
|
|
186
|
+
switch (el.tagName.toLowerCase()) {
|
|
187
|
+
case "b":
|
|
188
|
+
case "strong":
|
|
189
|
+
return `**${inner()}**`;
|
|
190
|
+
case "i":
|
|
191
|
+
case "em":
|
|
192
|
+
return `*${inner()}*`;
|
|
193
|
+
case "br":
|
|
194
|
+
return "\n";
|
|
195
|
+
case "ul":
|
|
196
|
+
return Array.from(el.children).map((li) => `- ${inner(li)}`).join("\n") + "\n";
|
|
197
|
+
case "ol":
|
|
198
|
+
return Array.from(el.children).map((li, i) => `${i + 1}. ${inner(li)}`).join("\n") + "\n";
|
|
199
|
+
case "div":
|
|
200
|
+
case "p":
|
|
201
|
+
case "li":
|
|
202
|
+
return inner() + "\n";
|
|
203
|
+
default:
|
|
204
|
+
return inner();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function inlineNodes(text) {
|
|
208
|
+
const out = [];
|
|
209
|
+
const re = /\*\*([^*]+)\*\*|\*([^*]+)\*/g;
|
|
210
|
+
let last = 0;
|
|
211
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
212
|
+
if (m.index > last)
|
|
213
|
+
out.push(document.createTextNode(text.slice(last, m.index)));
|
|
214
|
+
const el = document.createElement(m[1] ? "strong" : "em");
|
|
215
|
+
el.textContent = m[1] ?? m[2];
|
|
216
|
+
out.push(el);
|
|
217
|
+
last = m.index + m[0].length;
|
|
218
|
+
}
|
|
219
|
+
if (last < text.length)
|
|
220
|
+
out.push(document.createTextNode(text.slice(last)));
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
function fromMarkup(text) {
|
|
224
|
+
const out = [];
|
|
225
|
+
let list = null;
|
|
226
|
+
for (const line of text.split("\n")) {
|
|
227
|
+
const item = /^(-|\d+\.)\s+(.*)$/.exec(line);
|
|
228
|
+
if (item) {
|
|
229
|
+
const tag = item[1] === "-" ? "ul" : "ol";
|
|
230
|
+
if (!list || list.tagName.toLowerCase() !== tag)
|
|
231
|
+
(list = document.createElement(tag), out.push(list));
|
|
232
|
+
const li = document.createElement("li");
|
|
233
|
+
li.append(...inlineNodes(item[2]));
|
|
234
|
+
list.append(li);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
list = null;
|
|
238
|
+
const div = document.createElement("div");
|
|
239
|
+
if (line)
|
|
240
|
+
div.append(...inlineNodes(line));
|
|
241
|
+
else
|
|
242
|
+
div.append(document.createElement("br"));
|
|
243
|
+
out.push(div);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** A small editable area with Bold, Italic and List; the value is text with **bold**, *italic* and '- ' items. */
|
|
248
|
+
function RichTextInput({ node, field, common }) {
|
|
249
|
+
const b = useBindings();
|
|
250
|
+
const ref = useRef(null);
|
|
251
|
+
const stored = String(b.value(node.value) ?? "");
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
const el = ref.current;
|
|
254
|
+
if (!el || el.dataset.ready)
|
|
255
|
+
return;
|
|
256
|
+
el.dataset.ready = "1";
|
|
257
|
+
el.replaceChildren(...fromMarkup(stored));
|
|
258
|
+
}, [stored]);
|
|
259
|
+
const commit = () => ref.current && b.write(node.value, toMarkup(ref.current).replace(/\n+$/, ""));
|
|
260
|
+
const exec = (command) => (e) => {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
ref.current?.focus();
|
|
263
|
+
document.execCommand(command);
|
|
264
|
+
commit();
|
|
265
|
+
};
|
|
266
|
+
const { id, className: _c, required: _r, autoComplete: _a, ...rest } = common;
|
|
267
|
+
return (_jsx(Field, { ...field, children: _jsxs("div", { className: "pxd-richtext", children: [_jsxs("div", { className: "pxd-richtext-toolbar", role: "toolbar", "aria-label": "Formatting", children: [_jsx("button", { type: "button", className: "pxd-richtext-control", "aria-label": "Bold", onMouseDown: exec("bold"), children: _jsx("strong", { children: "B" }) }), _jsx("button", { type: "button", className: "pxd-richtext-control", "aria-label": "Italic", onMouseDown: exec("italic"), children: _jsx("em", { children: "I" }) }), _jsx("button", { type: "button", className: "pxd-richtext-control", "aria-label": "Bulleted list", onMouseDown: exec("insertUnorderedList"), children: _jsx(Icon, { name: "menu", size: 16 }) })] }), _jsx("div", { ...rest, id: id, ref: ref, className: "pxd-input pxd-richtext-area", contentEditable: true, role: "textbox", "aria-multiline": "true", "aria-label": field.label, onInput: commit, onBlur: commit })] }) }));
|
|
268
|
+
}
|
|
269
|
+
/** Reads as text until Edit is chosen; Enter saves, Escape restores what was there. */
|
|
270
|
+
function InlineInput({ node, field, common }) {
|
|
271
|
+
const b = useBindings();
|
|
272
|
+
const stored = String(b.value(node.value) ?? "");
|
|
273
|
+
const [draft, setDraft] = useState(null);
|
|
274
|
+
const save = () => {
|
|
275
|
+
if (draft !== null)
|
|
276
|
+
b.write(node.value, draft);
|
|
277
|
+
setDraft(null);
|
|
278
|
+
};
|
|
279
|
+
if (draft === null) {
|
|
280
|
+
return (_jsxs("div", { className: "pxd-inline-edit", children: [_jsx("span", { className: "pxd-field-label", children: field.label }), _jsxs("button", { type: "button", className: "pxd-inline-value", "aria-label": `Edit ${field.label}`, onClick: () => setDraft(stored), children: [_jsx("span", { className: stored ? undefined : "pxd-inline-empty", children: stored || "Not set" }), _jsx("span", { className: "pxd-inline-hint", "aria-hidden": "true", children: "Edit" })] })] }));
|
|
281
|
+
}
|
|
282
|
+
return (_jsx(Field, { ...field, children: _jsx("input", { ...common, type: "text", autoFocus: true, value: draft, onChange: (e) => setDraft(e.target.value), onBlur: save, onKeyDown: (e) => {
|
|
283
|
+
if (e.key === "Enter")
|
|
284
|
+
(e.preventDefault(), save());
|
|
285
|
+
else if (e.key === "Escape")
|
|
286
|
+
setDraft(null);
|
|
287
|
+
} }) }));
|
|
66
288
|
}
|
|
67
289
|
/** A Choice's options, from literal options or host data. */
|
|
68
290
|
export function optionsOf(node, data, scope, text) {
|
|
@@ -106,6 +328,8 @@ export function Choice({ node }) {
|
|
|
106
328
|
const multiple = node.mode === "multiple";
|
|
107
329
|
const value = b.value(node.value);
|
|
108
330
|
const key = (v) => JSON.stringify(v);
|
|
331
|
+
// Element ids from a value: JSON is exact but not id-safe ("p_tom" has quotes), so encode it.
|
|
332
|
+
const idOf = (v) => `${labelId}-${encodeURIComponent(key(v)).replace(/%/g, "_")}`;
|
|
109
333
|
const help = node.help !== undefined ? b.text(node.help) : undefined;
|
|
110
334
|
const withFaces = options.some((o) => o.avatar !== undefined);
|
|
111
335
|
const searchable = options.length > 10 || (withFaces && options.length > 6);
|
|
@@ -127,12 +351,12 @@ export function Choice({ node }) {
|
|
|
127
351
|
const recent = query ? [] : options.filter((o) => o.recent);
|
|
128
352
|
const rest = options.filter((o) => (query ? matches(o) : !o.recent));
|
|
129
353
|
return (_jsxs("fieldset", { className: "pxd-field pxd-choice pxd-people", "aria-describedby": help ? helpId : undefined, ...a11y, children: [_jsx("legend", { className: "pxd-field-label", id: labelId, children: label }), help && _jsx("p", { className: "pxd-field-help", id: helpId, children: help }), search, _jsxs("div", { className: "pxd-people-group", children: [recent.length > 0 && (_jsx("div", { className: "pxd-people-recent", children: recent.map((o) => (_jsxs(Checkbox.Root, { className: "pxd-person-tile", checked: isOn(o.value), onCheckedChange: () => toggle(o.value), "aria-label": o.label, children: [_jsxs("span", { className: "pxd-person-face", children: [_jsx(Avatar, { value: o.avatar, name: o.label, size: 56 }), _jsx(Checkbox.Indicator, { className: "pxd-person-tick", children: _jsx(Icon, { name: "check", size: 14 }) })] }), _jsx("span", { className: "pxd-person-name", "aria-hidden": "true", children: o.label.split(" ")[0] })] }, key(o.value)))) })), _jsx("div", { className: "pxd-people-list", children: rest.map((o) => {
|
|
130
|
-
const cid =
|
|
354
|
+
const cid = idOf(o.value);
|
|
131
355
|
return (_jsxs(Checkbox.Root, { id: cid, className: "pxd-person-row", checked: isOn(o.value), onCheckedChange: () => toggle(o.value), "aria-labelledby": `${cid}-name`, "aria-describedby": o.description ? `${cid}-desc` : undefined, children: [_jsx(Avatar, { value: o.avatar, name: o.label }), _jsxs("span", { className: "pxd-person-text", children: [_jsx("span", { id: `${cid}-name`, children: o.label }), o.description && (_jsx("span", { className: "pxd-option-description", id: `${cid}-desc`, children: o.description }))] }), _jsx(Checkbox.Indicator, { className: "pxd-person-check", children: _jsx(Icon, { name: "check" }) })] }, key(o.value)));
|
|
132
356
|
}) })] })] }));
|
|
133
357
|
}
|
|
134
358
|
return (_jsxs("fieldset", { className: "pxd-field pxd-choice", "aria-describedby": help ? helpId : undefined, ...a11y, children: [_jsx("legend", { className: "pxd-field-label", id: labelId, children: label }), help && _jsx("p", { className: "pxd-field-help", id: helpId, children: help }), search, _jsx("div", { className: `pxd-choice-list${searchable ? " pxd-choice-scroll" : ""}`, children: options.filter(matches).map((o) => {
|
|
135
|
-
const cid =
|
|
359
|
+
const cid = idOf(o.value);
|
|
136
360
|
return (_jsxs("div", { className: "pxd-choice-option", children: [o.avatar !== undefined && _jsx(Avatar, { value: o.avatar, name: o.label, size: 32 }), _jsx(Checkbox.Root, { id: cid, className: "pxd-checkbox", checked: isOn(o.value), onCheckedChange: () => toggle(o.value), "aria-describedby": o.description ? `${cid}-desc` : undefined, children: _jsx(Checkbox.Indicator, { className: "pxd-checkbox-indicator", children: "\u2713" }) }), _jsx(OptionText, { id: cid, label: o.label, description: o.description })] }, key(o.value)));
|
|
137
361
|
}) })] }));
|
|
138
362
|
}
|
|
@@ -147,12 +371,12 @@ export function Choice({ node }) {
|
|
|
147
371
|
const recent = query ? [] : options.filter((o) => o.recent);
|
|
148
372
|
const rest = options.filter((o) => (query ? matches(o) : !o.recent));
|
|
149
373
|
return (_jsxs("div", { className: "pxd-field pxd-choice pxd-people", ...a11y, children: [heading, help && _jsx("p", { className: "pxd-field-help", id: helpId, children: help }), search, _jsxs(RadioGroup.Root, { "aria-labelledby": labelId, "aria-describedby": help ? helpId : undefined, "aria-required": node.required || undefined, value: current, onValueChange: onValueChange, className: "pxd-people-group", children: [recent.length > 0 && (_jsx("div", { className: "pxd-people-recent", role: "presentation", children: recent.map((o) => (_jsxs(RadioGroup.Item, { value: key(o.value), className: "pxd-person-tile", "aria-label": o.label, children: [_jsx(Avatar, { value: o.avatar, name: o.label, size: 56 }), _jsx("span", { className: "pxd-person-name", "aria-hidden": "true", children: o.label.split(" ")[0] })] }, key(o.value)))) })), _jsx("div", { className: "pxd-people-list", role: "presentation", children: rest.map((o) => {
|
|
150
|
-
const cid =
|
|
374
|
+
const cid = idOf(o.value);
|
|
151
375
|
return (_jsxs(RadioGroup.Item, { id: cid, value: key(o.value), className: "pxd-person-row", "aria-labelledby": `${cid}-name`, "aria-describedby": o.description ? `${cid}-desc` : undefined, children: [_jsx(Avatar, { value: o.avatar, name: o.label }), _jsxs("span", { className: "pxd-person-text", children: [_jsx("span", { id: `${cid}-name`, children: o.label }), o.description && (_jsx("span", { className: "pxd-option-description", id: `${cid}-desc`, children: o.description }))] }), _jsx(RadioGroup.Indicator, { className: "pxd-person-check", children: _jsx(Icon, { name: "check" }) })] }, key(o.value)));
|
|
152
376
|
}) })] })] }));
|
|
153
377
|
}
|
|
154
378
|
return (_jsxs("div", { className: "pxd-field pxd-choice", ...a11y, children: [heading, help && _jsx("p", { className: "pxd-field-help", id: helpId, children: help }), search, _jsx(RadioGroup.Root, { className: `pxd-choice-list${searchable ? " pxd-choice-scroll" : ""}`, "aria-labelledby": labelId, "aria-describedby": help ? helpId : undefined, "aria-required": node.required || undefined, value: current, onValueChange: onValueChange, orientation: "vertical", children: options.filter(matches).map((o) => {
|
|
155
|
-
const cid =
|
|
379
|
+
const cid = idOf(o.value);
|
|
156
380
|
return (_jsxs("div", { className: "pxd-choice-option", children: [_jsx(RadioGroup.Item, { id: cid, value: key(o.value), className: "pxd-radio", "aria-describedby": o.description ? `${cid}-desc` : undefined, children: _jsx(RadioGroup.Indicator, { className: "pxd-radio-indicator" }) }), _jsx(OptionText, { id: cid, label: o.label, description: o.description })] }, key(o.value)));
|
|
157
381
|
}) })] }));
|
|
158
382
|
}
|
|
@@ -176,10 +400,11 @@ export function Toggle({ node }) {
|
|
|
176
400
|
const a11y = useA11y(node);
|
|
177
401
|
return (_jsxs("div", { className: "pxd-toggle", children: [node.action ? (_jsx(Switch.Root, { id: id, className: "pxd-switch", checked: checked, onCheckedChange: change, "aria-describedby": description ? descId : undefined, ...a11y, children: _jsx(Switch.Thumb, { className: "pxd-switch-thumb" }) })) : (_jsx(Checkbox.Root, { id: id, className: "pxd-checkbox", checked: checked, onCheckedChange: (c) => change(c === true), "aria-describedby": description ? descId : undefined, ...a11y, children: _jsx(Checkbox.Indicator, { className: "pxd-checkbox-indicator", children: "\u2713" }) })), _jsxs("div", { className: "pxd-toggle-text", children: [_jsx("label", { htmlFor: id, className: "pxd-toggle-label", children: b.text(node.label) }), description && (_jsx("p", { className: "pxd-field-help", id: descId, children: description }))] })] }));
|
|
178
402
|
}
|
|
179
|
-
const DATE_TYPE = { date: "date", time: "time", datetime: "datetime-local" };
|
|
403
|
+
const DATE_TYPE = { date: "date", time: "time", datetime: "datetime-local", month: "month" };
|
|
180
404
|
/** Native date/time inputs: typing is always possible, and pickers are platform-native. */
|
|
181
405
|
export function DateInput({ node }) {
|
|
182
406
|
const b = useBindings();
|
|
407
|
+
const s = useSurface();
|
|
183
408
|
const id = useId();
|
|
184
409
|
const helpId = useId();
|
|
185
410
|
const kind = node.kind ?? "date";
|
|
@@ -188,6 +413,16 @@ export function DateInput({ node }) {
|
|
|
188
413
|
const min = node.min !== undefined ? b.text(node.min) : undefined;
|
|
189
414
|
const max = node.max !== undefined ? b.text(node.max) : undefined;
|
|
190
415
|
const a11y = useA11y(node);
|
|
416
|
+
if (node.multiple && kind !== "dateRange") {
|
|
417
|
+
// Each chosen date is a chip; the field adds another.
|
|
418
|
+
const dates = Array.isArray(value) ? value.map(String) : [];
|
|
419
|
+
const spoken = (d) => formatValue(d, { type: kind === "month" ? "date" : kind === "time" ? "time" : kind === "datetime" ? "datetime" : "date" }, s.locale);
|
|
420
|
+
const add = (d) => d && !dates.includes(d) && b.write(node.value, [...dates, d].sort());
|
|
421
|
+
return (_jsxs("fieldset", { className: "pxd-field pxd-date-multiple", ...a11y, children: [_jsxs("legend", { className: "pxd-field-label", children: [b.text(node.label), node.required && _jsx(RequiredMark, {})] }), help && _jsx("p", { className: "pxd-field-help", id: helpId, children: help }), dates.length > 0 && (_jsx("ul", { className: "pxd-tags-list", "aria-label": `Chosen ${b.text(node.label)}`, children: dates.map((d) => (_jsxs("li", { className: "pxd-tag-chip", children: [kind === "year" || kind === "month" ? d : spoken(d), _jsx("button", { type: "button", className: "pxd-tag-remove", "aria-label": `Remove ${d}`, onClick: () => b.write(node.value, dates.filter((x) => x !== d)), children: _jsx(Icon, { name: "close", size: 14 }) })] }, d))) })), _jsxs("label", { className: "pxd-date-add", htmlFor: id, children: [_jsx("span", { className: "pxd-sr-only", children: "Add a date" }), kind === "year" ? (_jsx("input", { id: id, className: "pxd-input pxd-input-year", inputMode: "numeric", pattern: "[0-9]{4}", maxLength: 4, placeholder: "YYYY", "aria-describedby": help ? helpId : undefined, onKeyDown: (e) => e.key === "Enter" && (e.preventDefault(), add(e.currentTarget.value), (e.currentTarget.value = "")), onBlur: (e) => (add(e.currentTarget.value), (e.currentTarget.value = "")) })) : (_jsx("input", { id: id, className: "pxd-input pxd-input-date", type: DATE_TYPE[kind], min: min, max: max, "aria-describedby": help ? helpId : undefined, onChange: (e) => (add(e.target.value), (e.target.value = "")) }))] })] }));
|
|
422
|
+
}
|
|
423
|
+
if (kind === "year") {
|
|
424
|
+
return (_jsx(Field, { label: b.text(node.label), help: help, required: node.required, id: id, helpId: helpId, children: _jsx("input", { id: id, className: "pxd-input pxd-input-year", inputMode: "numeric", pattern: "[0-9]{4}", maxLength: 4, placeholder: "YYYY", value: value ?? "", min: min, max: max, required: node.required, "aria-describedby": help ? helpId : undefined, onChange: (e) => b.write(node.value, e.target.value.replace(/\D/g, "").slice(0, 4) || null), ...a11y }) }));
|
|
425
|
+
}
|
|
191
426
|
if (kind === "dateRange") {
|
|
192
427
|
const [start, end] = Array.isArray(value) ? value : [];
|
|
193
428
|
const setAt = (i, v) => {
|
|
@@ -197,7 +432,7 @@ export function DateInput({ node }) {
|
|
|
197
432
|
};
|
|
198
433
|
return (_jsxs("fieldset", { className: "pxd-field", ...a11y, children: [_jsx("legend", { className: "pxd-field-label", children: b.text(node.label) }), help && _jsx("p", { className: "pxd-field-help", children: help }), _jsxs("div", { className: "pxd-date-range", children: [_jsxs("label", { children: ["From", _jsx("input", { className: "pxd-input", type: "date", value: start ?? "", min: min, max: max, required: node.required, onChange: (e) => setAt(0, e.target.value) })] }), _jsxs("label", { children: ["To", _jsx("input", { className: "pxd-input", type: "date", value: end ?? "", min: start ?? min, max: max, required: node.required, onChange: (e) => setAt(1, e.target.value) })] })] })] }));
|
|
199
434
|
}
|
|
200
|
-
return (_jsx(Field, { label: b.text(node.label), help: help, required: node.required, id: id, helpId: helpId, children: _jsx("input", { id: id, className:
|
|
435
|
+
return (_jsx(Field, { label: b.text(node.label), help: help, required: node.required, id: id, helpId: helpId, children: _jsx("input", { id: id, className: `pxd-input pxd-input-date${kind === "month" ? " pxd-input-month" : ""}`, type: DATE_TYPE[kind], value: value ?? "", min: min, max: max, required: node.required, "aria-describedby": help ? helpId : undefined, onChange: (e) => b.write(node.value, e.target.value || null), ...a11y }) }));
|
|
201
436
|
}
|
|
202
437
|
export function RangeInput({ node }) {
|
|
203
438
|
const b = useBindings();
|
|
@@ -216,15 +451,103 @@ export function RangeInput({ node }) {
|
|
|
216
451
|
const value = Number(b.value(node.value) ?? node.min);
|
|
217
452
|
return (_jsxs("div", { className: "pxd-field pxd-range", ...a11y, children: [_jsxs("div", { className: "pxd-range-header", children: [_jsx("span", { className: "pxd-field-label", id: labelId, children: b.text(node.label) }), _jsx("output", { className: "pxd-range-value", "aria-live": "polite", children: fmt(value) })] }), _jsxs(Slider.Root, { className: "pxd-slider", min: node.min, max: node.max, step: node.step ?? 1, value: [value], onValueChange: ([v]) => b.write(node.value, v), children: [_jsx(Slider.Track, { className: "pxd-slider-track", children: _jsx(Slider.Range, { className: "pxd-slider-range" }) }), _jsx(Slider.Thumb, { className: "pxd-slider-thumb", "aria-labelledby": labelId, "aria-valuetext": fmt(value) })] }), _jsxs("div", { className: "pxd-range-bounds", "aria-hidden": "true", children: [_jsx("span", { children: fmt(node.min) }), _jsx("span", { children: fmt(node.max) })] })] }));
|
|
218
453
|
}
|
|
454
|
+
/** What a control is called, as the summary says it: its label, else its aria-label, else its field's. */
|
|
455
|
+
function controlName(control) {
|
|
456
|
+
const own = control.labels?.[0]?.textContent ?? control.getAttribute("aria-label") ?? control.closest(".pxd-field")?.querySelector(".pxd-field-label, legend")?.textContent ?? "";
|
|
457
|
+
return own.replace(/\*\s*$/, "").trim(); // the required mark is for the eye, not the summary
|
|
458
|
+
}
|
|
459
|
+
/** The controls that failed, in DOM order, one per radio group. Each gets an id the summary can link to. */
|
|
460
|
+
function collectProblems(form, base) {
|
|
461
|
+
const seen = new Set();
|
|
462
|
+
const out = [];
|
|
463
|
+
for (const el of Array.from(form.elements)) {
|
|
464
|
+
if (!el.willValidate || el.validity.valid)
|
|
465
|
+
continue;
|
|
466
|
+
if (el.type === "radio" && el.name) {
|
|
467
|
+
if (seen.has(el.name))
|
|
468
|
+
continue;
|
|
469
|
+
seen.add(el.name);
|
|
470
|
+
}
|
|
471
|
+
if (!el.id)
|
|
472
|
+
el.id = `${base}-${out.length}`;
|
|
473
|
+
out.push(el);
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Submits through checkValidity, so every control's own message is set (onInvalid) without the
|
|
479
|
+
* browser's bubbles. When something fails, a summary at the top of the fields says what, in plain
|
|
480
|
+
* words, and links to each control; each control is marked and, if its field says nothing yet, the
|
|
481
|
+
* message is said under it. The marks are undone on the next submit and redone from scratch.
|
|
482
|
+
*/
|
|
219
483
|
export function Form({ node }) {
|
|
220
484
|
const b = useBindings();
|
|
221
485
|
const s = useSurface();
|
|
486
|
+
const level = Math.min(useContext(HeadingContext), 6);
|
|
487
|
+
const summaryId = useId();
|
|
488
|
+
const summary = useRef(null);
|
|
489
|
+
const marked = useRef([]);
|
|
490
|
+
const [problems, setProblems] = useState([]);
|
|
222
491
|
// An asterisk has to say what it means, once, before the fields it marks.
|
|
223
492
|
const anyRequired = (node.children ?? []).some((id) => s.byId.get(id)?.required);
|
|
224
|
-
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (problems.length)
|
|
495
|
+
summary.current?.focus();
|
|
496
|
+
}, [problems]);
|
|
497
|
+
const unmark = () => {
|
|
498
|
+
for (const c of marked.current) {
|
|
499
|
+
c.removeAttribute("aria-invalid");
|
|
500
|
+
if (c.pxdDescribed)
|
|
501
|
+
c.setAttribute("aria-describedby", c.pxdDescribed);
|
|
502
|
+
else
|
|
503
|
+
c.removeAttribute("aria-describedby");
|
|
504
|
+
c.pxdNote?.remove();
|
|
505
|
+
c.pxdNote = undefined;
|
|
506
|
+
}
|
|
507
|
+
marked.current = [];
|
|
508
|
+
};
|
|
509
|
+
const mark = (controls) => {
|
|
510
|
+
for (const c of controls) {
|
|
511
|
+
c.setAttribute("aria-invalid", "true");
|
|
512
|
+
const field = c.closest(".pxd-field");
|
|
513
|
+
if (field && !field.querySelector(".pxd-field-error")) {
|
|
514
|
+
const note = field.ownerDocument.createElement("p");
|
|
515
|
+
note.className = "pxd-field-error";
|
|
516
|
+
note.id = `${c.id}-error`;
|
|
517
|
+
note.textContent = c.validationMessage;
|
|
518
|
+
field.append(note);
|
|
519
|
+
c.pxdNote = note;
|
|
520
|
+
c.pxdDescribed = c.getAttribute("aria-describedby");
|
|
521
|
+
c.setAttribute("aria-describedby", [c.pxdDescribed, note.id].filter(Boolean).join(" "));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
marked.current = controls;
|
|
525
|
+
};
|
|
526
|
+
const goTo = (id) => {
|
|
527
|
+
const el = summary.current?.ownerDocument.getElementById(id);
|
|
528
|
+
if (!el)
|
|
529
|
+
return;
|
|
530
|
+
el.scrollIntoView({ block: "center" });
|
|
531
|
+
el.focus();
|
|
532
|
+
// A Radix group's native input is hidden; the group's first real control takes the focus.
|
|
533
|
+
if (el.ownerDocument.activeElement !== el)
|
|
534
|
+
el.closest(".pxd-field")?.querySelector("[role='radio'], button, input:not([aria-hidden]), textarea, select")?.focus();
|
|
535
|
+
};
|
|
536
|
+
return (_jsxs("form", { className: `pxd-form${node.aside ? " pxd-form-with-aside" : ""}${node.layout === "horizontal" ? " pxd-form-horizontal" : ""}`, noValidate: true, onSubmit: (e) => {
|
|
225
537
|
e.preventDefault();
|
|
538
|
+
const form = e.currentTarget;
|
|
539
|
+
unmark();
|
|
540
|
+
if (!form.checkValidity()) {
|
|
541
|
+
const failed = collectProblems(form, summaryId);
|
|
542
|
+
mark(failed);
|
|
543
|
+
return setProblems(failed.map((c) => ({ id: c.id, label: controlName(c), message: c.validationMessage })));
|
|
544
|
+
}
|
|
545
|
+
setProblems([]);
|
|
226
546
|
s.dispatch(node.submit.action, b.scope, node.id);
|
|
227
|
-
}, ...useA11y(node), children: [node.aside && (_jsx("aside", { className: "pxd-form-aside", children: _jsx(Render, { id: node.aside }) })), _jsxs("div", { className: "pxd-stack pxd-form-fields", children: [
|
|
547
|
+
}, ...useA11y(node), children: [node.aside && (_jsx("aside", { className: "pxd-form-aside", children: _jsx(Render, { id: node.aside }) })), _jsxs("div", { className: "pxd-stack pxd-form-fields", children: [problems.length > 0 && (_jsxs("section", { ref: summary, className: "pxd-error-summary", role: "alert", tabIndex: -1, "aria-labelledby": summaryId, children: [createElement(`h${level}`, { id: summaryId, className: "pxd-error-summary-title" }, "Check these fields before continuing"), _jsx("ul", { className: "pxd-error-summary-list", children: problems.map((p) => (_jsx("li", { children: _jsx("a", { href: `#${p.id}`, className: "pxd-link", onClick: (e) => {
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
goTo(p.id);
|
|
550
|
+
}, children: p.label ? `${p.label}: ${p.message}` : p.message }) }, p.id))) })] })), anyRequired && (_jsxs("p", { className: "pxd-required-legend", children: [_jsx("span", { className: "pxd-required", "aria-hidden": "true", children: "*" }), " ", "Required"] })), _jsx(Children, { ids: node.children })] }), _jsxs("div", { className: "pxd-action-bar", children: [_jsx("button", { type: "submit", className: "pxd-button pxd-button-primary", children: b.text(node.submit.label) }), node.cancel && (_jsx("button", { type: "button", className: "pxd-button pxd-button-secondary", onClick: () => s.dispatch(node.cancel.action, b.scope, node.id), children: b.text(node.cancel.label) }))] })] }));
|
|
228
551
|
}
|
|
229
552
|
export { Render };
|
|
230
553
|
//# sourceMappingURL=inputs.js.map
|