@react-x11/components 0.12.0 → 0.14.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.
Files changed (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -0,0 +1,700 @@
1
+ // The form controls' React half: the widgets mounted at the rectangles
2
+ // layout reserved for them, what a press on a `<button>`, a `<label>` or an
3
+ // image button does, and a form's submission — checked, encoded, and handed
4
+ // to `onSubmit`.
5
+ //
6
+ // Every widget here is a **core** one rather than something drawn: a
7
+ // `<select>` in a document drops the same menu as a `<Select>` in the window
8
+ // around it, a `<textinput>` gets the same caret, the same IME and the same
9
+ // edit menu, and all of them join the window's focus order. What the
10
+ // document draws itself is a `<button>`, whose content is the page's, and an
11
+ // image button, which is a picture; a press on either reaches the element,
12
+ // which is why this also watches the document's own presses.
13
+ //
14
+ // What the markup cannot say — what was typed, what a reset puts back — is
15
+ // `FormState`'s (form.ts), one per `<Html>`, and what a submission carries
16
+ // and where it goes is form.ts's too. This is the half that has a React
17
+ // tree.
18
+ import React from 'react';
19
+ import { Button, Checkbox, Radio, RadioGroup, Select } from 'react-x11';
20
+ import { cancelLater, later } from '../internal/timers.js';
21
+ import { hx } from './hx.js';
22
+ import { attr, isElement, tagOf } from './dom.js';
23
+ import { between, buttonLabel, optionsOf, selectedOption } from './controls.js';
24
+ import { FormState, buttonType, firstInvalid, formOwner, formSubmission, implicitSubmission, inputType, isDisabled, labeledControl, optionElements, optionValue, radioGroup, } from './form.js';
25
+ const h = React.createElement;
26
+ /** How far a press may travel and still be a click — `useLinkClicks`'s
27
+ * rule, so a link and a button in one document agree about it. */
28
+ const CLICK_SLOP = 4;
29
+ /** How long a validation message stays, as a browser's bubble does. */
30
+ const MESSAGE_MS = 5000;
31
+ export function useForms(options) {
32
+ const { view, baseUrl, onControlChange, onSubmit, touch } = options;
33
+ // What the controls hold that the markup does not, for the widgets'
34
+ // next mount, a submission and a reset. A reset bumps `resets`, which is
35
+ // in every widget's key: an uncontrolled field shows its markup's value
36
+ // again only by mounting again.
37
+ const forms = React.useMemo(() => new FormState(), []);
38
+ const live = (el) => forms.typed(el);
39
+ const [resets, setResets] = React.useState(0);
40
+ const [invalid, setInvalid] = React.useState(null);
41
+ // Each element's widget is keyed by the element, not by where it is: a
42
+ // field that layout moves — a stylesheet arriving, an image above it
43
+ // loading, a resize — is the same widget, and keeps its focus, its caret
44
+ // and its undo. Keyed by position, each move mounted a new one, and a
45
+ // page whose stylesheet landed while someone was typing lost the field
46
+ // from under them.
47
+ const widgets = React.useMemo(() => new WidgetBoxes(), []);
48
+ React.useEffect(() => {
49
+ if (!invalid)
50
+ return;
51
+ const timer = later(() => setInvalid(null), MESSAGE_MS);
52
+ return () => cancelLater(timer);
53
+ }, [invalid]);
54
+ const submit = (form, submitter, point) => {
55
+ if (!onSubmit)
56
+ return;
57
+ // a form that would send what its own constraints refuse says why, at
58
+ // the first control that is wrong, and sends nothing
59
+ const wrong = firstInvalid(form, submitter, live);
60
+ if (wrong) {
61
+ setInvalid(wrong);
62
+ widgets.focus(wrong.element);
63
+ return;
64
+ }
65
+ setInvalid(null);
66
+ const node = view.current;
67
+ const submission = formSubmission(form, submitter, {
68
+ base: node?.documentBase ?? baseUrl ?? null,
69
+ documentUrl: baseUrl ?? null,
70
+ live,
71
+ point,
72
+ });
73
+ if (submission)
74
+ onSubmit(submission);
75
+ };
76
+ /** A button's activation behaviour (HTML 4.10.6): a submit button
77
+ * submits its form, a reset button resets it, and any other does
78
+ * nothing. Reported first, as a pressed widget is. */
79
+ const press = (button, point) => {
80
+ onControlChange?.(button, attr(button, 'value') ?? '');
81
+ const kind = buttonType(button);
82
+ const form = kind && kind !== 'button' ? formOwner(button) : null;
83
+ if (!form)
84
+ return;
85
+ if (kind === 'submit')
86
+ submit(form, button, point);
87
+ else if (forms.reset(form)) {
88
+ setInvalid(null);
89
+ setResets((n) => n + 1);
90
+ touch();
91
+ }
92
+ };
93
+ /** A control's value changed: tell the application, and let a message
94
+ * about it go. */
95
+ const changed = (el, value, restyle) => {
96
+ onControlChange?.(el, value);
97
+ // a field's change lets its own message go, and a radio's its group's
98
+ if (invalid &&
99
+ (invalid.element === el ||
100
+ (tagOf(el) === 'input' &&
101
+ inputType(el) === 'radio' &&
102
+ radioGroup(el).includes(invalid.element)))) {
103
+ setInvalid(null);
104
+ }
105
+ if (restyle)
106
+ touch();
107
+ };
108
+ const setChecked = (el, checked) => {
109
+ forms.remember(el);
110
+ if (checked)
111
+ el.attribs.checked = '';
112
+ else
113
+ delete el.attribs.checked;
114
+ changed(el, checked, true);
115
+ };
116
+ // Core's radio is a group member and HTML's is a free-standing input that
117
+ // happens to share a `name`. Each one is therefore its own one-member
118
+ // `RadioGroup`, and the exclusivity that makes it a group is done where
119
+ // HTML keeps it: in the DOM, across the radios of its name in its form.
120
+ const checkRadio = (el) => {
121
+ for (const other of radioGroup(el)) {
122
+ if (attr(other, 'checked') === undefined)
123
+ continue;
124
+ forms.remember(other);
125
+ delete other.attribs.checked;
126
+ }
127
+ forms.remember(el);
128
+ el.attribs.checked = '';
129
+ changed(el, attr(el, 'value') ?? 'on', true);
130
+ };
131
+ /** A press on a `<label>` is one on its control (HTML 4.10.4): a box is
132
+ * toggled, a radio checked, a button pressed, and a field focused. */
133
+ const activateLabel = (control) => {
134
+ if (isDisabled(control))
135
+ return;
136
+ const tag = tagOf(control);
137
+ const type = tag === 'input' ? inputType(control) : '';
138
+ if (type === 'checkbox') {
139
+ setChecked(control, attr(control, 'checked') === undefined);
140
+ }
141
+ else if (type === 'radio') {
142
+ if (attr(control, 'checked') === undefined)
143
+ checkRadio(control);
144
+ }
145
+ else if (tag === 'button' || buttonType(control) !== null) {
146
+ press(control, type === 'image' ? { x: 0, y: 0 } : undefined);
147
+ }
148
+ else {
149
+ widgets.focus(control);
150
+ }
151
+ };
152
+ // The document's own presses. A press and a release on the same thing,
153
+ // close together: a click, not a drag.
154
+ const pressed = React.useRef(null);
155
+ const onMouseDown = (ev) => {
156
+ pressed.current = null;
157
+ if (ev.button !== 1)
158
+ return;
159
+ const at = pressableAt(ev);
160
+ if (at)
161
+ pressed.current = { at, x: ev.x, y: ev.y };
162
+ };
163
+ const onMouseUp = (ev) => {
164
+ const start = pressed.current;
165
+ pressed.current = null;
166
+ if (!start || ev.button !== 1)
167
+ return;
168
+ if (Math.abs(ev.x - start.x) > CLICK_SLOP ||
169
+ Math.abs(ev.y - start.y) > CLICK_SLOP) {
170
+ return;
171
+ }
172
+ const at = pressableAt(ev);
173
+ if (!at || at.element !== start.at.element)
174
+ return;
175
+ if (at.kind === 'label') {
176
+ // a label is text, and a drag that selected some of it was reading
177
+ // it; a button is pressed however many times it is clicked
178
+ if (!(ev.currentTarget?.textSelection?.isCollapsed ?? true))
179
+ return;
180
+ activateLabel(at.control);
181
+ }
182
+ else if (at.kind === 'image')
183
+ press(at.element, imagePoint(at.element, ev));
184
+ else
185
+ press(at.element);
186
+ };
187
+ /** Where in an image button a press landed, in its own CSS pixels. */
188
+ const imagePoint = (el, ev) => {
189
+ const node = view.current;
190
+ const rect = node?.elementRect(el);
191
+ const origin = node?.getClientRects()[0];
192
+ if (!rect || !origin)
193
+ return { x: 0, y: 0 };
194
+ return {
195
+ x: Math.max(0, ev.x - origin.x - rect.x),
196
+ y: Math.max(0, ev.y - origin.y - rect.y),
197
+ };
198
+ };
199
+ // `autofocus`: the first control that asks for the focus gets it once a
200
+ // document is up — where nothing else in the window holds it. A page
201
+ // never takes the keyboard from the application around it.
202
+ const autofocused = React.useRef(null);
203
+ const autofocus = (rects) => {
204
+ const document = view.current?.document ?? null;
205
+ if (!document || autofocused.current === document)
206
+ return;
207
+ const wants = rects.find((r) => attr(r.element, 'autofocus') !== undefined);
208
+ if (!wants)
209
+ return;
210
+ autofocused.current = document;
211
+ let top = view.current;
212
+ while (top?.parent)
213
+ top = top.parent;
214
+ if (top?.focusWithin)
215
+ return;
216
+ widgets.focus(wants.element);
217
+ };
218
+ const ctx = {
219
+ forms,
220
+ generation: resets,
221
+ widgets,
222
+ changed,
223
+ setChecked,
224
+ checkRadio,
225
+ press,
226
+ submitFrom: (field) => {
227
+ const plan = implicitSubmission(field);
228
+ if (plan)
229
+ submit(plan.form, plan.submitter);
230
+ },
231
+ };
232
+ const rendered = React.useRef([]);
233
+ React.useEffect(() => autofocus(rendered.current));
234
+ return {
235
+ onMouseDown,
236
+ onMouseUp,
237
+ render: (rects, look) => {
238
+ rendered.current = rects;
239
+ const out = rects.map((rect) => renderControl(rect, look, ctx));
240
+ if (invalid) {
241
+ const bubble = renderMessage(invalid, rects, view.current, look);
242
+ if (bubble)
243
+ out.push(bubble);
244
+ }
245
+ return out;
246
+ },
247
+ };
248
+ }
249
+ /**
250
+ * The thing a press lands on: the nearest `<button>`, image button or
251
+ * `<label>` around the element under it. A link inside one is the link's,
252
+ * and a disabled button is pressed by nobody; a label with nothing to label
253
+ * is only text.
254
+ */
255
+ function pressableAt(ev) {
256
+ const target = ev.target;
257
+ let node = typeof target?.elementAtPoint === 'function'
258
+ ? target.elementAtPoint(ev.x, ev.y)
259
+ : null;
260
+ for (; node; node = node.parent?.type === 'tag' ? node.parent : null) {
261
+ const tag = tagOf(node);
262
+ if (tag === 'a' && attr(node, 'href') !== undefined)
263
+ return null;
264
+ if (tag === 'button') {
265
+ return isDisabled(node) ? null : { kind: 'button', element: node };
266
+ }
267
+ if (tag === 'input' && inputType(node) === 'image') {
268
+ return isDisabled(node) ? null : { kind: 'image', element: node };
269
+ }
270
+ if (tag === 'label') {
271
+ const control = labeledControl(node);
272
+ return control ? { kind: 'label', element: node, control } : null;
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+ /**
278
+ * The box each element's widget is mounted in, by element: the key the
279
+ * widget is mounted under, and where to find it to give it the focus.
280
+ */
281
+ class WidgetBoxes {
282
+ _ids = new WeakMap();
283
+ _next = 0;
284
+ _boxes = new Map();
285
+ _refs = new WeakMap();
286
+ idOf(el) {
287
+ let id = this._ids.get(el);
288
+ if (id === undefined)
289
+ this._ids.set(el, (id = ++this._next));
290
+ return id;
291
+ }
292
+ /** A ref for the box of `el`'s widget, the same one every render. */
293
+ refOf(el) {
294
+ let ref = this._refs.get(el);
295
+ if (!ref) {
296
+ ref = (node) => {
297
+ if (node)
298
+ this._boxes.set(el, node);
299
+ else if (this._boxes.get(el))
300
+ this._boxes.delete(el);
301
+ };
302
+ this._refs.set(el, ref);
303
+ }
304
+ return ref;
305
+ }
306
+ /** Focus `el`'s widget: the first node in its box that takes the focus. */
307
+ focus(el) {
308
+ const box = this._boxes.get(el);
309
+ if (!box)
310
+ return false;
311
+ const stack = [box];
312
+ while (stack.length) {
313
+ const node = stack.shift();
314
+ if (takesFocus(node)) {
315
+ node.focus();
316
+ return true;
317
+ }
318
+ stack.unshift(...node.children);
319
+ }
320
+ return false;
321
+ }
322
+ }
323
+ /**
324
+ * Whether a node takes the focus: core's rule, which it keeps to itself
325
+ * (`isFocusable` in its a11y.js) — `focusable` or a `tabIndex` where the
326
+ * props say, and else the element's own default (a `<textinput>`'s) or a
327
+ * selectable surface, and never when disabled. `focus()` itself focuses
328
+ * any node it is asked to, the box a widget sits in included.
329
+ */
330
+ function takesFocus(node) {
331
+ const { props, focusableByDefault } = node;
332
+ if (props.disabled)
333
+ return false;
334
+ if (typeof props.focusable === 'boolean')
335
+ return props.focusable;
336
+ if (props.tabIndex != null)
337
+ return true;
338
+ return (focusableByDefault ?? false) || props.selectable === true;
339
+ }
340
+ /** One form control, as a real widget at the rectangle layout reserved for
341
+ * it. */
342
+ function renderControl(rect, look, ctx) {
343
+ const { forms } = ctx;
344
+ const el = rect.element;
345
+ const key = `${rect.kind}:${ctx.widgets.idOf(el)}#${ctx.generation}`;
346
+ const disabled = isDisabled(el);
347
+ const readOnly = attr(el, 'readonly') !== undefined;
348
+ // a field whose box the document draws takes its content box
349
+ const at = rect.bare ?? rect;
350
+ const left = Math.round(at.x);
351
+ const top = Math.round(at.y);
352
+ const width = Math.round(at.width);
353
+ const height = Math.round(at.height);
354
+ // What the widget shows through: its own rectangle, or where the
355
+ // document cuts the element, what the cut leaves of it — out to where a
356
+ // focus ring reaches, which a clip around the element cuts as it cuts
357
+ // the element, and one that leaves the box whole does not.
358
+ const port = rect.clip
359
+ ? between(rect.clip, {
360
+ x: left - RING_REACH,
361
+ y: top - RING_REACH,
362
+ width: width + 2 * RING_REACH,
363
+ height: height + 2 * RING_REACH,
364
+ })
365
+ : { x: left, y: top, width, height };
366
+ const frame = {
367
+ position: 'absolute',
368
+ left: left - port.x,
369
+ top: top - port.y,
370
+ width,
371
+ height,
372
+ // The face and the size the box was measured in, the element's: the
373
+ // palette's from the UA sheet, or the page's where it set its own. A
374
+ // field inherits them, and so does a `<Button>`'s or a `<Select>`'s
375
+ // caption — named here because the text cascade takes the palette's
376
+ // from the window, and a provider inside it that names a face or a size
377
+ // reaches `useTheme` and not the cascade.
378
+ fontFamily: rect.fontFamily,
379
+ fontSize: rect.fontSize,
380
+ // core's `opacity` is CSS's: the widget faded as a group, and at 0 not
381
+ // drawn and still hit
382
+ ...(rect.opacity !== undefined && { opacity: rect.opacity }),
383
+ };
384
+ const field = rect.bare ? bareField(rect.bare) : fieldChrome(look);
385
+ const order = tabOrder(el);
386
+ // A button or a select whose font the page set is the drawn control: a
387
+ // native bezel sets its title at AppKit's size, whatever it is handed,
388
+ // in a bezel only as tall as that title, where the box was measured for
389
+ // the page's.
390
+ const ownFont = !paletteFont(rect, look);
391
+ // A text edit does NOT restyle: the value lives in the widget and in
392
+ // `forms`, and neither changes any box — while a restyle would re-run
393
+ // the cascade and relayout the whole document *per keystroke*. This also
394
+ // matches HTML's own semantics: typing updates the value, not the
395
+ // attribute selectors match against. The checkables do restyle, because
396
+ // `:checked` is a selector documents really use.
397
+ const typed = (value) => {
398
+ forms.setTyped(el, value);
399
+ ctx.changed(el, value, false);
400
+ };
401
+ let widget;
402
+ switch (rect.kind) {
403
+ case 'checkbox':
404
+ widget = h(Checkbox, {
405
+ checked: attr(el, 'checked') !== undefined,
406
+ disabled,
407
+ ...order,
408
+ // the element's whole box takes the press, as it does in a browser:
409
+ // a page that sizes one over its label, invisible, means the label
410
+ style: { width: '100%', height: '100%' },
411
+ onChange: (ev) => ctx.setChecked(el, ev.value),
412
+ });
413
+ break;
414
+ case 'radio': {
415
+ const value = attr(el, 'value') ?? 'on';
416
+ // No `tabindex` reaches a radio: core's `<Radio>` takes no props for
417
+ // the node it draws, as the other widgets do, and the group's box is
418
+ // not the one that takes the focus.
419
+ widget = h(RadioGroup, {
420
+ value: attr(el, 'checked') !== undefined ? value : undefined,
421
+ onChange: () => ctx.checkRadio(el),
422
+ }, h(Radio, { key: 'r', value, disabled }));
423
+ break;
424
+ }
425
+ case 'button':
426
+ widget = h(Button, {
427
+ label: buttonLabel(el),
428
+ disabled,
429
+ ...order,
430
+ ...(ownFont && { native: false }),
431
+ style: { width: '100%', height: '100%' },
432
+ onPress: () => ctx.press(el),
433
+ });
434
+ break;
435
+ case 'select': {
436
+ const options = optionsOf(el);
437
+ widget = h(Select, {
438
+ options: options.map((o) => ({ value: o.value, label: o.label })),
439
+ value: selectedOption(el) ?? undefined,
440
+ // a `<select>` with nothing selected has no options, and shows
441
+ // none: core's "Select…" is an application's prompt, not a page's
442
+ placeholder: '',
443
+ disabled,
444
+ ...order,
445
+ style: rect.bare
446
+ ? [BARE_TRIGGER, { width: '100%', height: '100%' }]
447
+ : { width: '100%', height: '100%' },
448
+ // the slots choose the drawn trigger on every backend, and put the
449
+ // caption and the arrow in the page's ink
450
+ ...(rect.bare && {
451
+ labelStyle: {
452
+ color: rect.bare.color,
453
+ fontFamily: rect.fontFamily,
454
+ fontSize: rect.fontSize,
455
+ },
456
+ chevronStyle: rect.bare.chevron
457
+ ? { color: rect.bare.color }
458
+ : { display: 'none' },
459
+ }),
460
+ // and so does a caption in the page's font, whose size the chevron
461
+ // is read back from: it is as tall as the capitals beside it
462
+ ...(!rect.bare &&
463
+ ownFont && {
464
+ labelStyle: {
465
+ fontFamily: rect.fontFamily,
466
+ fontSize: rect.fontSize,
467
+ },
468
+ }),
469
+ onChange: (ev) => {
470
+ const next = String(ev.value ?? '');
471
+ forms.remember(el);
472
+ setSelectedOption(el, next);
473
+ ctx.changed(el, next, true);
474
+ },
475
+ });
476
+ break;
477
+ }
478
+ case 'textarea':
479
+ // Uncontrolled on purpose: the widget owns the live text the way a
480
+ // browser's does, and one mounted again — its element hidden and
481
+ // shown, a reset — starts from what `forms` kept of it.
482
+ widget = hx('textarea', {
483
+ defaultValue: forms.value(el),
484
+ maxLength: maxLength(el),
485
+ ...order,
486
+ style: [field, { width: '100%', height: '100%' }],
487
+ onChange: readOnly ? undefined : (ev) => typed(ev.value),
488
+ });
489
+ break;
490
+ case 'input': {
491
+ const type = inputType(el);
492
+ widget = hx('textinput', {
493
+ defaultValue: forms.value(el),
494
+ placeholder: attr(el, 'placeholder'),
495
+ maxLength: maxLength(el),
496
+ ...order,
497
+ // Core's word for a password field: nothing in it reaches a
498
+ // selection, PRIMARY included.
499
+ sensitive: type === 'password',
500
+ style: [field, { width: '100%', height: '100%' }],
501
+ onChange: readOnly
502
+ ? undefined
503
+ : (ev) => {
504
+ typed(ev.value);
505
+ // echoed where it always was, for a handler that reads it
506
+ // back off the element — after `typed`, which keeps what the
507
+ // attribute said before for a reset
508
+ el.attribs.value = ev.value;
509
+ },
510
+ // Enter submits the field's form, as it does in a browser
511
+ onSubmit: () => ctx.submitFrom(el),
512
+ });
513
+ break;
514
+ }
515
+ default:
516
+ return null;
517
+ }
518
+ // Two boxes, always: the one the widget shows through, and in it the
519
+ // element's. A widget the document comes to cut, or stops cutting, is
520
+ // the same widget in the same place in the tree, and keeps its focus
521
+ // and its caret.
522
+ return hx('box', {
523
+ key,
524
+ selectable: false,
525
+ style: {
526
+ position: 'absolute',
527
+ left: port.x,
528
+ top: port.y,
529
+ width: port.width,
530
+ height: port.height,
531
+ // cut to it, and nothing itself: a press beside the widget is the
532
+ // document's
533
+ ...(rect.clip && { overflow: 'hidden', pointerEvents: 'box-none' }),
534
+ },
535
+ }, hx('box', {
536
+ ref: ctx.widgets.refOf(el),
537
+ style: frame,
538
+ selectable: false,
539
+ // a control the page keeps from assistive technology is kept from
540
+ // it here: core leaves the node, and the widget in it, out of the
541
+ // accessibility tree
542
+ ...(ariaHidden(el) && { 'aria-hidden': true }),
543
+ }, widget));
544
+ }
545
+ /**
546
+ * What an element's `tabindex` says of its widget's place in the Tab order
547
+ * (HTML 6.6.3): a negative one is focusable — by a press, by its label, by
548
+ * `autofocus` — and not reached by Tab, which is core's `tabIndex={-1}`
549
+ * too. Radix lays a native `<select tabindex="-1" aria-hidden="true">`
550
+ * beside the picker it draws, cut to nothing: it was a stop the eye could
551
+ * not find, and Space on it opened an empty menu.
552
+ *
553
+ * Nothing else is handed over. Zero is where a control already is, and a
554
+ * positive one would put a page's control ahead of the application's own,
555
+ * whose window it is: the order between a document and what is around it
556
+ * is not the document's to set.
557
+ */
558
+ function tabOrder(el) {
559
+ // the rules for parsing integers (HTML 2.3.4.1): white space, a sign,
560
+ // digits, and whatever follows them ignored
561
+ const m = /^[ \t\n\f\r]*([+-]?\d+)/.exec(attr(el, 'tabindex') ?? '');
562
+ return m && Number(m[1]) < 0 ? { tabIndex: -1 } : undefined;
563
+ }
564
+ /** Whether an element is hidden from assistive technology: `aria-hidden`
565
+ * is true on it or on an element around it (WAI-ARIA 1.2, 6.6). */
566
+ function ariaHidden(el) {
567
+ for (let at = el; at;) {
568
+ if (attr(at, 'aria-hidden')?.trim().toLowerCase() === 'true')
569
+ return true;
570
+ const parent = at.parent;
571
+ at = isElement(parent) ? parent : null;
572
+ }
573
+ return false;
574
+ }
575
+ /** How far past its box a widget's focus ring is drawn, and then some. */
576
+ const RING_REACH = 8;
577
+ /** A field's `maxlength`, which the widget enforces as it is typed into. */
578
+ function maxLength(el) {
579
+ const raw = attr(el, 'maxlength')?.trim();
580
+ return raw && /^\d+$/.test(raw) ? Number(raw) : undefined;
581
+ }
582
+ /**
583
+ * Why a form did not submit, under the control that stopped it — a
584
+ * browser's validation bubble, in the palette's surface and border. Where
585
+ * the control has no widget (a checkbox the page hid and drew its own), it
586
+ * goes under the element's box; where it has neither, it is not shown, and
587
+ * the form still does not submit, as in a browser.
588
+ */
589
+ function renderMessage(invalid, rects, view, look) {
590
+ const rect = rects.find((r) => r.element === invalid.element) ??
591
+ view?.elementRect(invalid.element) ??
592
+ null;
593
+ if (!rect)
594
+ return null;
595
+ return hx('box', {
596
+ key: 'form-message',
597
+ selectable: false,
598
+ style: {
599
+ position: 'absolute',
600
+ left: Math.round(rect.x),
601
+ top: Math.round(rect.y + rect.height + 4),
602
+ maxWidth: 320,
603
+ paddingLeft: 10,
604
+ paddingRight: 10,
605
+ paddingTop: 6,
606
+ paddingBottom: 6,
607
+ backgroundColor: look.surface,
608
+ borderWidth: look.controlBorder,
609
+ borderColor: look.borderColor,
610
+ borderRadius: look.controlRadius,
611
+ zIndex: 1,
612
+ },
613
+ }, hx('text', {
614
+ style: {
615
+ color: look.color,
616
+ fontFamily: look.fontFamily,
617
+ fontSize: Math.round(look.fontSize * 0.9),
618
+ },
619
+ }, invalid.message));
620
+ }
621
+ /**
622
+ * The chrome a text field needs.
623
+ *
624
+ * `<textinput>` and `<textarea>` are core *elements* rather than components,
625
+ * so they draw no frame of their own — an application supplies one, which is
626
+ * why core's own `<Button>` and `<Select>` are components and these are not.
627
+ * The values are the palette's, so a field in a document and a `<Select>`
628
+ * beside it are the same height with the same corner and the same edge.
629
+ * Its text is in the face and at the size of the frame around it, which
630
+ * are the element's (`renderControl`).
631
+ */
632
+ function fieldChrome(look) {
633
+ return {
634
+ backgroundColor: look.surface,
635
+ borderWidth: look.controlBorder,
636
+ borderColor: look.borderColor,
637
+ borderRadius: look.controlRadius,
638
+ paddingLeft: 6,
639
+ paddingRight: 6,
640
+ color: look.color,
641
+ };
642
+ }
643
+ /**
644
+ * A text field whose box the author styled: the document draws the border
645
+ * and the background, so the widget draws neither, and its text is the
646
+ * element's colour, which the author chose to go on that background, rather
647
+ * than the theme's. Its face and size are the frame's, as every field's are.
648
+ */
649
+ function bareField(bare) {
650
+ return {
651
+ backgroundColor: 'transparent',
652
+ borderWidth: 0,
653
+ borderRadius: 0,
654
+ paddingLeft: 0,
655
+ paddingRight: 0,
656
+ color: bare.color,
657
+ };
658
+ }
659
+ /**
660
+ * Whether a control is set in the face and at the size the UA sheet gives
661
+ * a button or a select, the palette's: the page left its font alone. The
662
+ * size is compared loosely, since the rect's is a device size divided back
663
+ * by the scale.
664
+ */
665
+ function paletteFont(rect, look) {
666
+ return (rect.fontFamily === (look.controlFontFamily ?? look.fontFamily) &&
667
+ Math.abs(rect.fontSize - (look.controlFontSize ?? look.fontSize)) < 0.01);
668
+ }
669
+ /**
670
+ * The trigger of a `<select>` whose box the page styled: no frame, no fill
671
+ * and none of its own insets, and no wash under the pointer, since the box
672
+ * it would tint is the document's. Core's focus ring still marks it for the
673
+ * keyboard.
674
+ */
675
+ const BARE_TRIGGER = {
676
+ paddingTop: 0,
677
+ paddingBottom: 0,
678
+ paddingLeft: 0,
679
+ paddingRight: 0,
680
+ borderWidth: 0,
681
+ borderRadius: 0,
682
+ backgroundColor: 'transparent',
683
+ ':hover': { backgroundColor: 'transparent' },
684
+ ':active': { backgroundColor: 'transparent' },
685
+ };
686
+ /** Select the option of a drop-down `<select>` whose value is `value`, and
687
+ * no other. */
688
+ function setSelectedOption(el, value) {
689
+ let found = false;
690
+ for (const option of optionElements(el)) {
691
+ if (!found && optionValue(option) === value) {
692
+ option.attribs.selected = '';
693
+ found = true;
694
+ }
695
+ else {
696
+ delete option.attribs.selected;
697
+ }
698
+ }
699
+ }
700
+ //# sourceMappingURL=widgets.js.map