@jxsuite/ui 0.0.0 → 0.2.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 (105) hide show
  1. package/components/jx-accordion-item.json +281 -0
  2. package/components/jx-accordion.json +33 -0
  3. package/components/jx-action-button.json +268 -34
  4. package/components/jx-action-group.json +131 -0
  5. package/components/jx-button.json +141 -26
  6. package/components/jx-checkbox.json +222 -0
  7. package/components/jx-color-area.json +263 -0
  8. package/components/jx-color-field.json +565 -0
  9. package/components/jx-color-slider.json +216 -0
  10. package/components/jx-combobox.json +551 -0
  11. package/components/jx-dialog.json +416 -0
  12. package/components/jx-divider.json +52 -0
  13. package/components/jx-dot.json +38 -0
  14. package/components/jx-field.json +162 -0
  15. package/components/jx-kbd.json +24 -0
  16. package/components/jx-listbox.json +76 -0
  17. package/components/jx-menu-item.json +6 -13
  18. package/components/jx-menu.json +45 -6
  19. package/components/jx-number-field.json +441 -0
  20. package/components/jx-option.json +350 -0
  21. package/components/jx-popover.json +227 -0
  22. package/components/jx-select.json +637 -0
  23. package/components/jx-spinner.json +129 -0
  24. package/components/jx-split.json +221 -0
  25. package/components/jx-swatch-group.json +107 -0
  26. package/components/jx-swatch.json +262 -0
  27. package/components/jx-switch.json +331 -0
  28. package/components/jx-tab-panel.json +58 -0
  29. package/components/jx-tab.json +296 -0
  30. package/components/jx-tabs.json +124 -0
  31. package/components/jx-textfield.json +645 -0
  32. package/components/jx-toast-host.json +121 -0
  33. package/components/jx-toast.json +255 -0
  34. package/components/jx-toolbar.json +81 -0
  35. package/components/jx-tooltip.json +207 -0
  36. package/components/jx-tree-item.json +325 -0
  37. package/components/jx-tree.json +202 -0
  38. package/icons/list.json +4 -0
  39. package/icons/manifest.json +12 -0
  40. package/package.json +27 -5
  41. package/project.json +164 -153
  42. package/src/behaviors/action-group.ts +255 -0
  43. package/src/behaviors/color-area.ts +303 -0
  44. package/src/behaviors/color-field.ts +465 -0
  45. package/src/behaviors/color-slider.ts +154 -0
  46. package/src/behaviors/combobox.ts +474 -0
  47. package/src/behaviors/dialog.ts +137 -0
  48. package/src/behaviors/listbox.ts +118 -0
  49. package/src/behaviors/menu.ts +80 -48
  50. package/src/behaviors/number-field.ts +161 -0
  51. package/src/behaviors/popover.ts +399 -0
  52. package/src/behaviors/select.ts +154 -0
  53. package/src/behaviors/split.ts +478 -0
  54. package/src/behaviors/swatch-group.ts +297 -0
  55. package/src/behaviors/tabs.ts +319 -0
  56. package/src/behaviors/textfield.ts +164 -0
  57. package/src/behaviors/toast-host.ts +241 -0
  58. package/src/behaviors/toast.ts +261 -0
  59. package/src/behaviors/toolbar.ts +413 -0
  60. package/src/behaviors/tooltip.ts +719 -0
  61. package/src/behaviors/tree.ts +830 -0
  62. package/src/color.ts +433 -0
  63. package/src/documents.ts +91 -1
  64. package/src/index.ts +41 -1
  65. package/src/loaders.ts +47 -0
  66. package/src/testing/anchor-support.ts +28 -0
  67. package/src/testing/popover-shim.ts +39 -6
  68. package/src/theme.ts +30 -6
  69. package/stylebook/jx-accordion-item.json +195 -0
  70. package/stylebook/jx-accordion.json +220 -0
  71. package/stylebook/jx-action-button.json +191 -23
  72. package/stylebook/jx-action-group.json +335 -0
  73. package/stylebook/jx-button.json +129 -26
  74. package/stylebook/jx-checkbox.json +245 -0
  75. package/stylebook/jx-color-area.json +272 -0
  76. package/stylebook/jx-color-field.json +261 -0
  77. package/stylebook/jx-color-slider.json +220 -0
  78. package/stylebook/jx-combobox.json +332 -0
  79. package/stylebook/jx-dialog.json +186 -0
  80. package/stylebook/jx-divider.json +125 -0
  81. package/stylebook/jx-dot.json +92 -0
  82. package/stylebook/jx-field.json +313 -0
  83. package/stylebook/jx-icon.json +4 -0
  84. package/stylebook/jx-kbd.json +90 -0
  85. package/stylebook/jx-listbox.json +337 -0
  86. package/stylebook/jx-menu.json +0 -1
  87. package/stylebook/jx-number-field.json +203 -0
  88. package/stylebook/jx-option.json +285 -0
  89. package/stylebook/jx-popover.json +123 -0
  90. package/stylebook/jx-select.json +407 -0
  91. package/stylebook/jx-spinner.json +111 -0
  92. package/stylebook/jx-split.json +170 -0
  93. package/stylebook/jx-swatch-group.json +175 -0
  94. package/stylebook/jx-swatch.json +249 -0
  95. package/stylebook/jx-switch.json +240 -0
  96. package/stylebook/jx-tab-panel.json +147 -0
  97. package/stylebook/jx-tab.json +201 -0
  98. package/stylebook/jx-tabs.json +278 -0
  99. package/stylebook/jx-textfield.json +309 -0
  100. package/stylebook/jx-toast-host.json +157 -0
  101. package/stylebook/jx-toast.json +266 -0
  102. package/stylebook/jx-toolbar.json +435 -0
  103. package/stylebook/jx-tooltip.json +162 -0
  104. package/stylebook/jx-tree-item.json +258 -0
  105. package/stylebook/jx-tree.json +429 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * `jx-toolbar`'s behaviour sidecar: the roving caret over a row of MIXED controls, and the one
3
+ * question `jx-action-group` never has to ask — does the control the caret is standing on want this
4
+ * key for itself?
5
+ *
6
+ * **Why this is not `jx-action-group`.** That element roves `jx-action-button` and nothing else:
7
+ * its `itemsOf` is a query for one tag, its `compact` seam is a rule about that tag's borders, and
8
+ * its `selects="single"` clicks the child an arrow lands on. Both surfaces this element was built
9
+ * for — Studio's grid toolbar and the Project Styles chrome bar — hold a `jx-textfield` beside
10
+ * their buttons, and a text field is precisely the child that breaks every one of those three: it
11
+ * is not an action button, it has no border to join, and an arrow key inside it is a caret move
12
+ * rather than a selection. So the two elements are not one element under two names.
13
+ * `jx-action-group` is a ROW OF BUTTONS that may be joined into a segmented control or made a
14
+ * radiogroup; `jx-toolbar` is a CONTAINER OF ARBITRARY CONTROLS with one tab stop.
15
+ *
16
+ * What it does beyond reading state is the allowed set (specs/ui.md §2): move focus, write a
17
+ * child's own declared `tabindex` prop (or, for a plain native control, its own `tabIndex`), and
18
+ * read a text control's caret offset, which is a measurement. It measures no box, listens on no
19
+ * document, and calls no platform overlay API. It owns no overflow menu, and §5.5 says why.
20
+ *
21
+ * @docs extending/ui-kit
22
+ */
23
+
24
+ /** The tag of the toolbar. */
25
+ const TOOLBAR = "jx-toolbar";
26
+
27
+ /**
28
+ * Anything that could take DOM focus. Deliberately not keyed on `tabindex`, because the roving
29
+ * caret WRITES `tabindex` — a query that read it would stop finding the items it had just parked.
30
+ */
31
+ const FOCUSABLE =
32
+ "button, input, select, textarea, a[href], [contenteditable=''], [contenteditable='true']";
33
+
34
+ /**
35
+ * Containers that run a roving caret of their own.
36
+ *
37
+ * A control inside one of these belongs to IT, not to the toolbar. Two roving carets over one set
38
+ * of buttons is two writers of the same `tabindex`, and they do not agree: the group parks its
39
+ * caret on the child it thinks is current and the toolbar parks the row's on another. So a nested
40
+ * composite is left alone entirely — it keeps its own single tab stop, and the toolbar's arrows do
41
+ * not reach into it. See §5.5 for why that limit is stated rather than engineered around.
42
+ */
43
+ const COMPOSITE = "jx-action-group, jx-tabs, jx-tree, jx-swatch-group, jx-listbox, jx-menu";
44
+
45
+ /**
46
+ * Controls whose own arrow keys are theirs: they step a value or drive a popup, rather than moving
47
+ * a caret through text.
48
+ *
49
+ * These are not toolbar items at all, and the reason is not symmetry with {@link COMPOSITE} but
50
+ * data loss: a reader arrowing along the row past a `<select>` or a spin button would rewrite the
51
+ * value on the way past, once per press, and past a combobox would open and walk its list. An extra
52
+ * tab stop is an inconvenience; silently changing what somebody chose is a defect. A text field is
53
+ * the opposite case and IS an item — walking off the end of its text costs nothing — which is what
54
+ * the boundary rule in {@link cedesKey} is for.
55
+ */
56
+ const OWN_ARROWS =
57
+ "select, input[type='range'], input[type='number'], [role='spinbutton'], [role='slider'], [role='combobox']";
58
+
59
+ /** `<input>` types that have a text caret a reader moves with the arrow keys. */
60
+ const TEXT_TYPES = new Set(["", "text", "search", "url", "tel", "password", "email"]);
61
+
62
+ /** The reactive scope a `jx-toolbar` document hands its handlers. */
63
+ export interface ToolbarState {
64
+ orientation?: string;
65
+ label?: string;
66
+ [key: string]: unknown;
67
+ }
68
+
69
+ /** A toolbar item: a control, or the kit element that wraps one. */
70
+ type ItemElement = HTMLElement & { tabindex?: string; disabled?: boolean };
71
+
72
+ /** The toolbar an event's `currentTarget` is. */
73
+ function barOf(event: Event): HTMLElement | null {
74
+ const target = event.currentTarget;
75
+ return target instanceof HTMLElement && target.localName === TOOLBAR ? target : null;
76
+ }
77
+
78
+ /** Whether `bar` is the nearest toolbar around `node` — a key inside a nested one is its own. */
79
+ function ownsNode(bar: HTMLElement, node: EventTarget | null): boolean {
80
+ return node instanceof Element && node.closest(TOOLBAR) === bar;
81
+ }
82
+
83
+ /**
84
+ * The element a focusable node stands for: the OUTERMOST custom element between it and the toolbar,
85
+ * or the node itself when there is none.
86
+ *
87
+ * This is what makes one item out of the several focusable nodes a kit control owns — a
88
+ * `jx-textfield` is its `<input>` AND its clear `<button>`, and a toolbar that counted both would
89
+ * park a caret on a button that only exists while the field has a value. It is also what sees
90
+ * through the `display: contents` wrappers a `$switch` branch leaves around a single control, which
91
+ * Studio's grid toolbar has four of.
92
+ *
93
+ * @param bar The toolbar.
94
+ * @param node A focusable node inside it.
95
+ * @returns The item that node belongs to.
96
+ */
97
+ function itemFor(bar: HTMLElement, node: HTMLElement): ItemElement {
98
+ let item: ItemElement = node;
99
+ for (let el = node.parentElement; el && el !== bar; el = el.parentElement) {
100
+ if (el.localName.includes("-")) {
101
+ item = el as ItemElement;
102
+ }
103
+ }
104
+ return item;
105
+ }
106
+
107
+ /**
108
+ * The node inside an item that actually takes focus.
109
+ *
110
+ * `[part="control"]` and `[part="input"]` are the kit's own two names for "this element's own
111
+ * control" (`jx-button`, `jx-action-button`, `jx-swatch` and `jx-color-field` use the first,
112
+ * `jx-textfield` and `jx-number-field` the second), so they are asked for first — but only if the
113
+ * node wearing the name is FOCUSABLE. A composed control names the one node it wants roved: the
114
+ * colour field is a swatch, a text field and two doors, and the swatch wears the name because it is
115
+ * the opener, so the query finds it ahead of the text field's `[part="input"]`. On `jx-checkbox`
116
+ * and `jx-switch` `[part="control"]` is the `<label>` that wraps the input, so taking the named
117
+ * node on trust focused a label, which focuses nothing, and left the real control where it was. A
118
+ * plain native control is its own answer, and anything else falls back to the first focusable node
119
+ * the item holds.
120
+ *
121
+ * @param item A toolbar item.
122
+ * @returns The node to focus and to ask about a key.
123
+ */
124
+ export function controlOf(item: ItemElement): HTMLElement {
125
+ if (item.matches(FOCUSABLE)) {
126
+ return item;
127
+ }
128
+ const named = [...item.querySelectorAll<HTMLElement>('[part="control"], [part="input"]')].find(
129
+ (node) => node.matches(FOCUSABLE),
130
+ );
131
+ return named ?? item.querySelector<HTMLElement>(FOCUSABLE) ?? item;
132
+ }
133
+
134
+ /**
135
+ * The toolbar's own items, in document order.
136
+ *
137
+ * @param bar The `jx-toolbar` element.
138
+ * @returns The controls it roves; a nested toolbar's, a nested composite's and a control that owns
139
+ * its own arrow keys are not among them.
140
+ */
141
+ export function itemsOf(bar: HTMLElement): ItemElement[] {
142
+ const items: ItemElement[] = [];
143
+ for (const node of bar.querySelectorAll<HTMLElement>(FOCUSABLE)) {
144
+ if (node.closest(TOOLBAR) !== bar) {
145
+ continue;
146
+ }
147
+ const composite = node.closest<HTMLElement>(COMPOSITE);
148
+ if (composite && bar.contains(composite)) {
149
+ continue;
150
+ }
151
+ const item = itemFor(bar, node);
152
+ if (!items.includes(item) && !controlOf(item).matches(OWN_ARROWS)) {
153
+ items.push(item);
154
+ }
155
+ }
156
+ return items;
157
+ }
158
+
159
+ /**
160
+ * Whether an item cannot take the caret.
161
+ *
162
+ * Read from the PROPERTY once the child has upgraded and from the attribute before it has, for the
163
+ * reason `behaviors/action-group.ts` gives: the caret is placed from a lifecycle event and a
164
+ * slotted child's own `connectedCallback` is async, so the toolbar can be asked to rove either side
165
+ * of that moment. `hidden` counts too — a control a branch has hidden rather than removed is one
166
+ * the reader cannot see, and a caret parked on it is a tab stop that goes nowhere.
167
+ */
168
+ function isOut(item: ItemElement): boolean {
169
+ if (item.hidden) {
170
+ return true;
171
+ }
172
+ return item.disabled === undefined ? item.hasAttribute("disabled") : item.disabled === true;
173
+ }
174
+
175
+ /** The roving `tabindex` an item is currently carrying, as the string it was written as. */
176
+ function caretOf(item: ItemElement): string {
177
+ return "tabindex" in item ? String(item.tabindex ?? "") : (item.getAttribute("tabindex") ?? "");
178
+ }
179
+
180
+ /**
181
+ * Write the roving `tabindex` onto one item.
182
+ *
183
+ * A kit control declares a `tabindex` PROP for exactly this — it forwards the value to the native
184
+ * control inside itself, so the toolbar never reaches into another element's internals. A plain
185
+ * native control in the row is its own control, so its own `tabIndex` is that same write.
186
+ */
187
+ function setCaret(item: ItemElement, value: string): void {
188
+ if ("tabindex" in item) {
189
+ item.tabindex = value;
190
+ } else if (item.matches(FOCUSABLE)) {
191
+ item.tabIndex = Number(value);
192
+ }
193
+ }
194
+
195
+ /** Write `0` on `caret` and `-1` on every other item, the ones that cannot act included. */
196
+ function writeRoving(items: ItemElement[], caret: ItemElement | null): void {
197
+ for (const item of items) {
198
+ setCaret(item, item === caret ? "0" : "-1");
199
+ }
200
+ }
201
+
202
+ /** The item that should hold the caret: the one that already does and still can, else the first. */
203
+ function caretFor(items: ItemElement[]): ItemElement | null {
204
+ const live = items.filter((item) => !isOut(item));
205
+ return live.find((item) => caretOf(item) === "0") ?? live[0] ?? null;
206
+ }
207
+
208
+ /**
209
+ * Move the caret to the reachable item at `index`, wrapping at both ends, and focus its control.
210
+ *
211
+ * @param bar The toolbar whose caret is moving.
212
+ * @param index An index into the REACHABLE items; negative counts from the end.
213
+ * @returns The item that took the caret, or null when the toolbar has none that can.
214
+ */
215
+ export function focusItem(bar: HTMLElement, index: number): ItemElement | null {
216
+ const items = itemsOf(bar);
217
+ const live = items.filter((item) => !isOut(item));
218
+ if (live.length === 0) {
219
+ return null;
220
+ }
221
+ const at = ((index % live.length) + live.length) % live.length;
222
+ const caret = live[at]!;
223
+ writeRoving(items, caret);
224
+ controlOf(caret).focus();
225
+ return caret;
226
+ }
227
+
228
+ /**
229
+ * Whether a text control's caret has nowhere left to go in the direction `forward` names.
230
+ *
231
+ * A selection that cannot be read counts as the edge, and that direction is chosen deliberately: a
232
+ * toolbar that ceded a key it could not reason about would leave the reader inside a control with
233
+ * no arrow key out of it, which is SC 2.1.2. Chrome refuses `selectionStart` on `type="email"`, and
234
+ * happy-dom answers null for the types it does not model, so this is a case that really occurs.
235
+ */
236
+ function atEdge(node: HTMLInputElement | HTMLTextAreaElement, forward: boolean): boolean {
237
+ let start: number | null = null;
238
+ let end: number | null = null;
239
+ try {
240
+ start = node.selectionStart;
241
+ end = node.selectionEnd;
242
+ } catch {
243
+ return true;
244
+ }
245
+ if (start === null || end === null) {
246
+ return true;
247
+ }
248
+ if (start !== end) {
249
+ // A live selection is a gesture in progress; the arrow that collapses it is the control's.
250
+ return false;
251
+ }
252
+ return forward ? start >= String(node.value ?? "").length : start <= 0;
253
+ }
254
+
255
+ /** Whether `node` is a control with a text caret the arrow keys move. */
256
+ function isTextEntry(node: HTMLElement): node is HTMLInputElement | HTMLTextAreaElement {
257
+ if (node.localName === "textarea") {
258
+ return true;
259
+ }
260
+ return (
261
+ node.localName === "input" && TEXT_TYPES.has((node.getAttribute("type") ?? "").toLowerCase())
262
+ );
263
+ }
264
+
265
+ /**
266
+ * Whether the focused control answers this key itself, so the toolbar must not.
267
+ *
268
+ * The whole of the mixed-row problem is here. A toolbar owns the arrow pair of its own axis, and
269
+ * inside a text field that same pair moves the caret — the APG's own note says a toolbar holding
270
+ * such a control needs an answer and does not give one. This is the answer: **the field keeps the
271
+ * key while it has text left to walk in that direction, and the toolbar takes it at the edge.** So
272
+ * the reader arrows in, edits, and arrows out with the same key they came in with, and no gesture
273
+ * is lost either way. Home and End are the field's unconditionally, because inside text they mean
274
+ * the ends of the line and a toolbar that stole them would break editing to save a keystroke.
275
+ *
276
+ * @param node The event's target.
277
+ * @param key The key pressed.
278
+ * @param forward The toolbar's own forward key on its own axis.
279
+ * @returns Whether to stand aside.
280
+ */
281
+ export function cedesKey(node: EventTarget | null, key: string, forward: string): boolean {
282
+ if (!(node instanceof HTMLElement) || !isTextEntry(node)) {
283
+ return false;
284
+ }
285
+ if (key === "Home" || key === "End") {
286
+ return true;
287
+ }
288
+ return !atEdge(node, key === forward);
289
+ }
290
+
291
+ /**
292
+ * Give the toolbar exactly one tab stop.
293
+ *
294
+ * Without this every control in the row is its own, because a native `<button>` and a native
295
+ * `<input>` are both focusable with no `tabindex` at all — so the collapse to one is a WRITE that
296
+ * has to happen before the reader's first Tab, not something their first arrow key can repair.
297
+ *
298
+ * Idempotent, which is what lets it be called from a lifecycle event that may arrive more than once
299
+ * and from the observer below on every change to the row.
300
+ *
301
+ * @param bar The `jx-toolbar` element.
302
+ */
303
+ export function syncRoving(bar: HTMLElement): void {
304
+ const items = itemsOf(bar);
305
+ writeRoving(items, caretFor(items));
306
+ }
307
+
308
+ /** Whether the row already says what {@link syncRoving} would write. */
309
+ function isSettled(bar: HTMLElement): boolean {
310
+ const items = itemsOf(bar);
311
+ const caret = caretFor(items);
312
+ return items.every((item) => caretOf(item) === (item === caret ? "0" : "-1"));
313
+ }
314
+
315
+ /** Toolbars already being watched, so a second `jx-ready` does not observe one twice. */
316
+ const watched = new WeakSet<HTMLElement>();
317
+
318
+ /**
319
+ * Keep the one-tab-stop invariant as the row's controls come and go.
320
+ *
321
+ * A Studio toolbar is not a fixed row: the grid's Add Row and Delete Rows appear with the source's
322
+ * capabilities, its pager appears with a second page, and every button in it enables and disables
323
+ * with the selection. A control appended afterwards arrives with no `tabindex` at all, which is a
324
+ * SECOND tab stop in an element whose whole purpose is to have one; removing or disabling the one
325
+ * holding the caret leaves ZERO, so a row of operable controls cannot be reached by Tab.
326
+ *
327
+ * `hidden` is watched beside `disabled` because {@link isOut} reads it, and the `$switch` branches a
328
+ * surface writes are not the only way a control leaves the row.
329
+ *
330
+ * @param bar The `jx-toolbar` element.
331
+ */
332
+ function watchRoving(bar: HTMLElement): void {
333
+ if (watched.has(bar)) {
334
+ return;
335
+ }
336
+ watched.add(bar);
337
+ /* `isSettled` first, so the observer writes only when the invariant is actually broken: a
338
+ re-write is a mutation of its own, and a callback that always writes is a loop. */
339
+ const observer = new MutationObserver(() => {
340
+ if (!isSettled(bar)) {
341
+ syncRoving(bar);
342
+ }
343
+ });
344
+ observer.observe(bar, {
345
+ childList: true,
346
+ subtree: true,
347
+ attributes: true,
348
+ attributeFilter: ["disabled", "hidden"],
349
+ });
350
+ }
351
+
352
+ /**
353
+ * The toolbar keyboard contract: the arrow pair of its own axis moves the caret between controls
354
+ * with wrap, Home and End go to the ends, and a control that answers the key itself keeps it.
355
+ *
356
+ * Enter, Space, the focus ring and `:disabled` are the platform's, on each control.
357
+ *
358
+ * @param {ToolbarState} state
359
+ * @param {KeyboardEvent} event
360
+ */
361
+ export function onToolbarKeydown(state: ToolbarState, event: KeyboardEvent): void {
362
+ const bar = barOf(event);
363
+ if (!bar || !ownsNode(bar, event.target)) {
364
+ return;
365
+ }
366
+ const vertical = state.orientation === "vertical";
367
+ const forward = vertical ? "ArrowDown" : "ArrowRight";
368
+ const back = vertical ? "ArrowUp" : "ArrowLeft";
369
+ if (event.key !== forward && event.key !== back && event.key !== "Home" && event.key !== "End") {
370
+ return;
371
+ }
372
+ if (cedesKey(event.target, event.key, forward)) {
373
+ return;
374
+ }
375
+ const live = itemsOf(bar).filter((item) => !isOut(item));
376
+ if (live.length === 0) {
377
+ return;
378
+ }
379
+ const active = live.findIndex(
380
+ (item) =>
381
+ item === event.target || (event.target instanceof Node && item.contains(event.target)),
382
+ );
383
+ let index: number;
384
+ if (event.key === forward) {
385
+ index = active + 1;
386
+ } else if (event.key === back) {
387
+ index = active === -1 ? -1 : active - 1;
388
+ } else if (event.key === "Home") {
389
+ index = 0;
390
+ } else {
391
+ index = -1;
392
+ }
393
+ focusItem(bar, index);
394
+ event.preventDefault();
395
+ event.stopPropagation();
396
+ }
397
+
398
+ /**
399
+ * Collapse the toolbar to one tab stop once it has rendered, and keep it collapsed to one.
400
+ *
401
+ * `onMount` is handed the reactive scope alone, so the element announces itself with `jx-ready` —
402
+ * the kit's existing readiness event — and this reads the host off that event.
403
+ *
404
+ * @param {ToolbarState} _state
405
+ * @param {Event} event
406
+ */
407
+ export function onToolbarReady(_state: ToolbarState, event: Event): void {
408
+ const bar = barOf(event);
409
+ if (bar) {
410
+ syncRoving(bar);
411
+ watchRoving(bar);
412
+ }
413
+ }