@jxsuite/ui 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/behaviors/action-group.ts +0 -1
- package/src/behaviors/color-area.ts +19 -21
- package/src/behaviors/color-field.ts +63 -28
- package/src/behaviors/color-slider.ts +13 -11
- package/src/behaviors/combobox.ts +8 -9
- package/src/behaviors/dialog.ts +2 -2
- package/src/behaviors/listbox.ts +4 -3
- package/src/behaviors/menu.ts +14 -15
- package/src/behaviors/number-field.ts +7 -9
- package/src/behaviors/popover.ts +33 -47
- package/src/behaviors/select.ts +22 -9
- package/src/behaviors/split.ts +41 -25
- package/src/behaviors/swatch-group.ts +4 -7
- package/src/behaviors/tabs.ts +4 -7
- package/src/behaviors/textfield.ts +35 -14
- package/src/behaviors/toast-host.ts +11 -8
- package/src/behaviors/toast.ts +22 -21
- package/src/behaviors/toolbar.ts +2 -3
- package/src/behaviors/tooltip.ts +52 -30
- package/src/behaviors/tree.ts +19 -9
- package/src/color.ts +17 -2
- package/src/documents.ts +68 -42
- package/src/icons.ts +10 -8
- package/src/index.ts +8 -3
- package/src/loaders.ts +9 -4
- package/src/testing/popover-shim.ts +18 -7
- package/src/theme.ts +4 -3
package/src/behaviors/toast.ts
CHANGED
|
@@ -45,9 +45,11 @@ export interface ToastState {
|
|
|
45
45
|
timeout: number;
|
|
46
46
|
/** `info`, `positive`, `negative` or `warning`. */
|
|
47
47
|
variant: string;
|
|
48
|
-
[key: string]: unknown;
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
/** A `jx-toast` element, with the `open` accessor its document installs. */
|
|
51
|
+
type ToastElement = HTMLElement & { open?: boolean };
|
|
52
|
+
|
|
51
53
|
/** One toast's clock: what is left of it, when this run started, and what is holding it. */
|
|
52
54
|
interface Clock {
|
|
53
55
|
timer: ReturnType<typeof setTimeout> | null;
|
|
@@ -60,25 +62,25 @@ interface Clock {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/** Which hold an event asks for. Three channels, because they overlap and must not cancel. */
|
|
63
|
-
const HOLD_REASON:
|
|
64
|
-
focusin
|
|
65
|
-
focusout
|
|
66
|
-
[TOAST_PAUSE
|
|
67
|
-
[TOAST_RESUME
|
|
68
|
-
pointerenter
|
|
69
|
-
pointerleave
|
|
70
|
-
pointerout
|
|
71
|
-
pointerover
|
|
72
|
-
|
|
65
|
+
const HOLD_REASON: ReadonlyMap<string, "focus" | "pointer" | "stack"> = new Map([
|
|
66
|
+
["focusin", "focus"],
|
|
67
|
+
["focusout", "focus"],
|
|
68
|
+
[TOAST_PAUSE, "stack"],
|
|
69
|
+
[TOAST_RESUME, "stack"],
|
|
70
|
+
["pointerenter", "pointer"],
|
|
71
|
+
["pointerleave", "pointer"],
|
|
72
|
+
["pointerout", "pointer"],
|
|
73
|
+
["pointerover", "pointer"],
|
|
74
|
+
]);
|
|
73
75
|
|
|
74
76
|
/** Each scope's clock. */
|
|
75
|
-
const clocks = new WeakMap<
|
|
77
|
+
const clocks = new WeakMap<ToastState, Clock>();
|
|
76
78
|
|
|
77
79
|
/** The element each scope belongs to, learned at mount. */
|
|
78
|
-
const hosts = new WeakMap<
|
|
80
|
+
const hosts = new WeakMap<ToastState, HTMLElement>();
|
|
79
81
|
|
|
80
82
|
/** This scope's clock, minted on first sight. */
|
|
81
|
-
function clockOf(state:
|
|
83
|
+
function clockOf(state: ToastState): Clock {
|
|
82
84
|
const found = clocks.get(state);
|
|
83
85
|
if (found) {
|
|
84
86
|
return found;
|
|
@@ -106,7 +108,7 @@ function ownerOf(event: Event): HTMLElement | null {
|
|
|
106
108
|
* @returns True when the destination is still inside the element the listener is bound to.
|
|
107
109
|
*/
|
|
108
110
|
export function staysInside(event: Event): boolean {
|
|
109
|
-
const to =
|
|
111
|
+
const to = "relatedTarget" in event ? event.relatedTarget : null;
|
|
110
112
|
const from = event.currentTarget;
|
|
111
113
|
return to instanceof Node && from instanceof Node && from.contains(to);
|
|
112
114
|
}
|
|
@@ -147,12 +149,11 @@ function schedule(state: ToastState): void {
|
|
|
147
149
|
* @param host The `jx-toast` element.
|
|
148
150
|
* @param reason Why it closed.
|
|
149
151
|
*/
|
|
150
|
-
export function closeToast(host:
|
|
151
|
-
|
|
152
|
-
if (el.open !== true) {
|
|
152
|
+
export function closeToast(host: ToastElement, reason: ToastCloseReason): void {
|
|
153
|
+
if (host.open !== true) {
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
+
host.open = false;
|
|
156
157
|
host.dispatchEvent(new CustomEvent("close", { bubbles: true, detail: { reason } }));
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -207,7 +208,7 @@ export function onToastUnmount(state: ToastState): void {
|
|
|
207
208
|
*/
|
|
208
209
|
export function onToastHold(state: ToastState, event: Event): void {
|
|
209
210
|
const clock = clockOf(state);
|
|
210
|
-
const reason = HOLD_REASON
|
|
211
|
+
const reason = HOLD_REASON.get(event.type) ?? event.type;
|
|
211
212
|
if (clock.timer !== null) {
|
|
212
213
|
clock.remaining = Math.max(1, clock.remaining - (Date.now() - clock.startedAt));
|
|
213
214
|
}
|
|
@@ -225,7 +226,7 @@ export function onToastRelease(state: ToastState, event: Event): void {
|
|
|
225
226
|
if (staysInside(event)) {
|
|
226
227
|
return;
|
|
227
228
|
}
|
|
228
|
-
clockOf(state).holds.delete(HOLD_REASON
|
|
229
|
+
clockOf(state).holds.delete(HOLD_REASON.get(event.type) ?? event.type);
|
|
229
230
|
schedule(state);
|
|
230
231
|
}
|
|
231
232
|
|
package/src/behaviors/toolbar.ts
CHANGED
|
@@ -63,7 +63,6 @@ const TEXT_TYPES = new Set(["", "text", "search", "url", "tel", "password", "ema
|
|
|
63
63
|
export interface ToolbarState {
|
|
64
64
|
orientation?: string;
|
|
65
65
|
label?: string;
|
|
66
|
-
[key: string]: unknown;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
/** A toolbar item: a control, or the kit element that wraps one. */
|
|
@@ -98,7 +97,7 @@ function itemFor(bar: HTMLElement, node: HTMLElement): ItemElement {
|
|
|
98
97
|
let item: ItemElement = node;
|
|
99
98
|
for (let el = node.parentElement; el && el !== bar; el = el.parentElement) {
|
|
100
99
|
if (el.localName.includes("-")) {
|
|
101
|
-
item = el
|
|
100
|
+
item = el;
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
return item;
|
|
@@ -249,7 +248,7 @@ function atEdge(node: HTMLInputElement | HTMLTextAreaElement, forward: boolean):
|
|
|
249
248
|
// A live selection is a gesture in progress; the arrow that collapses it is the control's.
|
|
250
249
|
return false;
|
|
251
250
|
}
|
|
252
|
-
return forward ? start >=
|
|
251
|
+
return forward ? start >= node.value.length : start <= 0;
|
|
253
252
|
}
|
|
254
253
|
|
|
255
254
|
/** Whether `node` is a control with a text caret the arrow keys move. */
|
package/src/behaviors/tooltip.ts
CHANGED
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
* authors to prefer, and on Firefox and Safari it is the shape that shows nothing at all without
|
|
19
19
|
* this module.
|
|
20
20
|
*
|
|
21
|
-
* "AN OLDER ENGINE" INCLUDES THE DESKTOP APP. Studio ships inside
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
21
|
+
* "AN OLDER ENGINE" INCLUDES THE DESKTOP APP. Studio ships inside Electron, whose bundled Chromium
|
|
22
|
+
* trails the stable channel and can sit releases short of the attribute. Until Electron's Chromium
|
|
23
|
+
* reaches it the ONLY end-user path to Studio runs the fallback for every hinted button in its
|
|
24
|
+
* chrome, several hundred of them, and this module is not a corner for other people's browsers but
|
|
25
|
+
* the behaviour a user sees. Three consequences are designed for rather than tolerated: focus opens
|
|
26
|
+
* the tip only when it is KEYBOARD focus, the way the platform's interest-on-focus does, so a click
|
|
27
|
+
* does not pin a tip open; Escape is ONE listener per root shared by every bound tip, not one per
|
|
28
|
+
* tip on the document, so a keystroke in Studio walks one handler rather than one per button; and a
|
|
29
|
+
* tip that is MOVED — a keyed `$map` reordering its rows, a button re-parented into a toolbar —
|
|
30
|
+
* keeps its binding, because the runtime fires `disconnectedCallback` for a move and never re-runs
|
|
31
|
+
* `onMount`, so an unmount that disposed at once would strip a reordered row of its tooltip for
|
|
32
|
+
* good.
|
|
33
33
|
*
|
|
34
34
|
* IT ADDS LISTENERS TO AN ELEMENT THE KIT DOES NOT OWN, and that is said out loud rather than left
|
|
35
35
|
* to be discovered in review. specs/ui.md §2 principle 5 forbids WRITING ATTRIBUTES or styles on
|
|
@@ -51,8 +51,9 @@
|
|
|
51
51
|
* @docs extending/ui-kit
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
55
|
+
|
|
54
56
|
import { close, openAt, supportsAnchorPositioning } from "./popover.ts";
|
|
55
|
-
import type { PopoverElement } from "./popover.ts";
|
|
56
57
|
|
|
57
58
|
/** The reactive scope a `jx-tooltip` document hands its handlers. */
|
|
58
59
|
export interface TooltipState {
|
|
@@ -66,9 +67,26 @@ export interface TooltipState {
|
|
|
66
67
|
placement?: string;
|
|
67
68
|
/** Whether the platform is placing the tip against the control it was shown from. */
|
|
68
69
|
anchored?: boolean;
|
|
69
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The id of the control the tip is wired to, from the reflected `for` attribute; null when a host
|
|
72
|
+
* clears it.
|
|
73
|
+
*/
|
|
74
|
+
for?: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The reactive scope a hinted kit control hands {@link mintHintId}. */
|
|
78
|
+
export interface HintState {
|
|
79
|
+
/** The control's own id stem, minted at mount. */
|
|
80
|
+
uid?: string;
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The root a tip's id lookups run against. An element's root node is the Document, the
|
|
85
|
+
* DocumentFragment or ShadowRoot holding it, or — while it is detached — its topmost ancestor
|
|
86
|
+
* element. Every one of them can be queried; only the first two can look an id up directly.
|
|
87
|
+
*/
|
|
88
|
+
type LookupRoot = ParentNode & { getElementById?: (elementId: string) => HTMLElement | null };
|
|
89
|
+
|
|
72
90
|
/**
|
|
73
91
|
* How long a tip stays up after the pointer leaves the control, in milliseconds.
|
|
74
92
|
*
|
|
@@ -113,7 +131,7 @@ function watchEscape(root: Node, dismiss: () => void): () => void {
|
|
|
113
131
|
if (!entry) {
|
|
114
132
|
const set = new Set<() => void>();
|
|
115
133
|
const listener = (event: Event): void => {
|
|
116
|
-
if ((event
|
|
134
|
+
if (!(event instanceof KeyboardEvent) || event.key !== "Escape") {
|
|
117
135
|
return;
|
|
118
136
|
}
|
|
119
137
|
for (const fn of set) {
|
|
@@ -197,7 +215,7 @@ const watchers = new WeakMap<object, () => void>();
|
|
|
197
215
|
*/
|
|
198
216
|
export function supportsInterestInvokers(): boolean {
|
|
199
217
|
return (
|
|
200
|
-
|
|
218
|
+
isFunction(globalThis.HTMLButtonElement) && "interestForElement" in HTMLButtonElement.prototype
|
|
201
219
|
);
|
|
202
220
|
}
|
|
203
221
|
|
|
@@ -216,12 +234,12 @@ function platformOwns(tip: HTMLElement, trigger: Element): boolean {
|
|
|
216
234
|
|
|
217
235
|
/** Show the tip from its control, so the platform knows what the hint belongs to. */
|
|
218
236
|
function showTip(tip: HTMLElement, source: Element | null): void {
|
|
219
|
-
openAt(tip
|
|
237
|
+
openAt(tip, source);
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
/** Hide the tip. `open` and `data-open` follow from the platform's own toggle. */
|
|
223
241
|
function hideTip(tip: HTMLElement): void {
|
|
224
|
-
close(tip
|
|
242
|
+
close(tip);
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
/**
|
|
@@ -291,7 +309,7 @@ export function bindTooltip(tip: HTMLElement, trigger: Element): () => void {
|
|
|
291
309
|
};
|
|
292
310
|
const showLater = (): void => {
|
|
293
311
|
cancel();
|
|
294
|
-
const delay =
|
|
312
|
+
const delay = "delay" in tip ? Number(tip.delay) || 0 : 0;
|
|
295
313
|
showTimer = setTimeout(show, delay);
|
|
296
314
|
};
|
|
297
315
|
const hideLater = (): void => {
|
|
@@ -389,8 +407,10 @@ export function bindTooltip(tip: HTMLElement, trigger: Element): () => void {
|
|
|
389
407
|
* miss.
|
|
390
408
|
*/
|
|
391
409
|
function triggerOf(state: TooltipState, tip: HTMLElement): HTMLElement | null {
|
|
392
|
-
|
|
393
|
-
|
|
410
|
+
/* SAFETY: `tip` is an Element, and the root node of an element is never a Text, Comment or Attr
|
|
411
|
+
node: it is a Document, a DocumentFragment or an Element, each of them a ParentNode. */
|
|
412
|
+
const root = tip.getRootNode() as LookupRoot;
|
|
413
|
+
const named = state.for ?? "";
|
|
394
414
|
const byFor = named === "" ? null : (root.getElementById?.(named) ?? null);
|
|
395
415
|
if (byFor) {
|
|
396
416
|
return byFor;
|
|
@@ -414,7 +434,9 @@ function anchorOf(state: TooltipState, tip: HTMLElement): HTMLElement | null {
|
|
|
414
434
|
if (wired) {
|
|
415
435
|
return wired;
|
|
416
436
|
}
|
|
417
|
-
|
|
437
|
+
/* SAFETY: `tip` is an Element, and the root node of an element is never a Text, Comment or Attr
|
|
438
|
+
node: it is a Document, a DocumentFragment or an Element, each of them a ParentNode. */
|
|
439
|
+
const root = tip.getRootNode() as LookupRoot;
|
|
418
440
|
const own = tip.id;
|
|
419
441
|
if (!/^[\w-]+$/.test(own)) {
|
|
420
442
|
return null;
|
|
@@ -464,7 +486,7 @@ export function placeTooltip(state: TooltipState, tip: HTMLElement): void {
|
|
|
464
486
|
state.flipped = true;
|
|
465
487
|
}
|
|
466
488
|
};
|
|
467
|
-
if (
|
|
489
|
+
if (isFunction(globalThis.requestAnimationFrame)) {
|
|
468
490
|
requestAnimationFrame(measure);
|
|
469
491
|
} else {
|
|
470
492
|
measure();
|
|
@@ -516,7 +538,7 @@ export function measureAnchoredFlip(state: TooltipState, tip: HTMLElement): () =
|
|
|
516
538
|
}
|
|
517
539
|
state.flipped = own.bottom <= box.top;
|
|
518
540
|
};
|
|
519
|
-
if (!anchor ||
|
|
541
|
+
if (!anchor || !isFunction(globalThis.requestAnimationFrame)) {
|
|
520
542
|
/* No control to be on a side of, or no frames to coalesce to: one synchronous reading, which
|
|
521
543
|
is nothing at all in the first case, and no watch. */
|
|
522
544
|
measure();
|
|
@@ -539,7 +561,7 @@ export function measureAnchoredFlip(state: TooltipState, tip: HTMLElement): () =
|
|
|
539
561
|
listener before it scrolls. Removal keys on `capture` alone, which is why it is spelled twice. */
|
|
540
562
|
window.addEventListener("scroll", schedule, { capture: true, passive: true });
|
|
541
563
|
window.addEventListener("resize", schedule);
|
|
542
|
-
const observer =
|
|
564
|
+
const observer = isFunction(globalThis.ResizeObserver) ? new ResizeObserver(schedule) : null;
|
|
543
565
|
if (observer) {
|
|
544
566
|
observer.observe(tip);
|
|
545
567
|
observer.observe(anchor);
|
|
@@ -576,8 +598,8 @@ export function onTooltipBeforeToggle(state: TooltipState, event: Event): void {
|
|
|
576
598
|
if (!(tip instanceof HTMLElement)) {
|
|
577
599
|
return;
|
|
578
600
|
}
|
|
579
|
-
const opening =
|
|
580
|
-
const
|
|
601
|
+
const opening = "newState" in event && event.newState === "open";
|
|
602
|
+
const source = "source" in event ? event.source : null;
|
|
581
603
|
const from = source instanceof Element ? source : anchorOf(state, tip);
|
|
582
604
|
state.anchored = opening && from !== null;
|
|
583
605
|
if (!opening) {
|
|
@@ -602,7 +624,7 @@ export function onTooltipToggle(state: TooltipState, event: Event): void {
|
|
|
602
624
|
if (!(tip instanceof HTMLElement)) {
|
|
603
625
|
return;
|
|
604
626
|
}
|
|
605
|
-
const opening =
|
|
627
|
+
const opening = "newState" in event && event.newState === "open";
|
|
606
628
|
state.open = opening;
|
|
607
629
|
if (!opening) {
|
|
608
630
|
// A closed tip has no side, and the watch that was following it has nothing left to follow.
|
|
@@ -713,7 +735,7 @@ let mintedHints = 0;
|
|
|
713
735
|
* @param state The control's reactive scope, whose `uid` this writes.
|
|
714
736
|
* @param host The `jx-action-button`, `jx-button` or `jx-switch` element.
|
|
715
737
|
*/
|
|
716
|
-
export function mintHintId(state:
|
|
738
|
+
export function mintHintId(state: HintState, host: HTMLElement): void {
|
|
717
739
|
mintedHints += 1;
|
|
718
|
-
state
|
|
740
|
+
state.uid = `${host.localName}-${mintedHints}`;
|
|
719
741
|
}
|
package/src/behaviors/tree.ts
CHANGED
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
* @docs extending/ui-kit
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
54
|
+
|
|
53
55
|
/** The tag of the tree. */
|
|
54
56
|
const TREE = "jx-tree";
|
|
55
57
|
/** The tag of one row. */
|
|
@@ -100,7 +102,6 @@ export interface TreeState {
|
|
|
100
102
|
anchor?: string;
|
|
101
103
|
padtop?: number;
|
|
102
104
|
padbottom?: number;
|
|
103
|
-
[key: string]: unknown;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/** A row element with the property accessors its own document installs. */
|
|
@@ -153,9 +154,7 @@ const handoffs = new WeakMap<HTMLElement, Handoff>();
|
|
|
153
154
|
/** The tree an event's `currentTarget` is. */
|
|
154
155
|
function hostOf(event: Event): TreeElement | null {
|
|
155
156
|
const target = event.currentTarget;
|
|
156
|
-
return target instanceof HTMLElement && target.localName === TREE
|
|
157
|
-
? (target as TreeElement)
|
|
158
|
-
: null;
|
|
157
|
+
return target instanceof HTMLElement && target.localName === TREE ? target : null;
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
/** Whether `tree` is the nearest tree around `node` — a key inside a nested tree is its own. */
|
|
@@ -209,7 +208,7 @@ function isDisabled(item: ItemElement): boolean {
|
|
|
209
208
|
*/
|
|
210
209
|
function labelOf(item: ItemElement): string {
|
|
211
210
|
const named = propOf(item, "label", "label");
|
|
212
|
-
return named === "" ?
|
|
211
|
+
return named === "" ? item.textContent : named;
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
/**
|
|
@@ -255,12 +254,12 @@ export function itemsOf(
|
|
|
255
254
|
*
|
|
256
255
|
* @param tree The `jx-tree` element.
|
|
257
256
|
*/
|
|
258
|
-
export function syncTree(tree:
|
|
257
|
+
export function syncTree(tree: TreeElement): void {
|
|
259
258
|
const items = itemsOf(tree, { includeDisabled: true });
|
|
260
259
|
if (items.length === 0) {
|
|
261
260
|
return;
|
|
262
261
|
}
|
|
263
|
-
const current = String(
|
|
262
|
+
const current = String(tree.current ?? "");
|
|
264
263
|
const enabled = items.filter((item) => !isDisabled(item));
|
|
265
264
|
const caret = enabled.find((item) => valueOf(item) === current) ?? enabled[0] ?? null;
|
|
266
265
|
for (const item of items) {
|
|
@@ -425,8 +424,19 @@ function activeIndex(items: ItemElement[], target: EventTarget | null): number {
|
|
|
425
424
|
);
|
|
426
425
|
}
|
|
427
426
|
|
|
427
|
+
/**
|
|
428
|
+
* What a tree event carries: a row's value for `change` and `activate`, else the event's own detail
|
|
429
|
+
* record.
|
|
430
|
+
*/
|
|
431
|
+
type TreeEventDetail =
|
|
432
|
+
| string
|
|
433
|
+
| TreeExpandDetail
|
|
434
|
+
| TreeMoveDetail
|
|
435
|
+
| TreeSelectDetail
|
|
436
|
+
| TreeTypeaheadDetail;
|
|
437
|
+
|
|
428
438
|
/** Say a public thing, from the tree, so `e.target` is the element a host wrote. */
|
|
429
|
-
function announce(host: HTMLElement, name: string, detail:
|
|
439
|
+
function announce(host: HTMLElement, name: string, detail: TreeEventDetail): void {
|
|
430
440
|
host.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
|
|
431
441
|
}
|
|
432
442
|
|
|
@@ -441,7 +451,7 @@ function moveTo(host: HTMLElement, scope: TreeState, item: ItemElement): void {
|
|
|
441
451
|
scope.current = valueOf(item);
|
|
442
452
|
syncTree(host);
|
|
443
453
|
item.focus();
|
|
444
|
-
if (
|
|
454
|
+
if (isFunction(item.scrollIntoView)) {
|
|
445
455
|
item.scrollIntoView({ block: "nearest" });
|
|
446
456
|
}
|
|
447
457
|
announce(host, "change", valueOf(item));
|
package/src/color.ts
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
* @docs extending/ui-kit
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
29
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
30
|
+
|
|
28
31
|
/** An sRGB colour. `r`, `g` and `b` are 0-255 and need not be integers; `a` is 0-1. */
|
|
29
32
|
export interface Rgb {
|
|
30
33
|
r: number;
|
|
@@ -65,6 +68,18 @@ export function clamp(value: number, min: number, max: number): number {
|
|
|
65
68
|
return value < min ? min : value > max ? max : value;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Which way each arrow key moves a colour element's value: Up and Right increase, Down and Left
|
|
73
|
+
* decrease. `jx-color-area` measures its vertical axis upward from the bottom edge, so ArrowUp
|
|
74
|
+
* raising brightness is the same rule as ArrowUp raising a slider's value.
|
|
75
|
+
*/
|
|
76
|
+
export const ARROW_SIGN: ReadonlyMap<string, -1 | 1> = new Map([
|
|
77
|
+
["ArrowDown", -1],
|
|
78
|
+
["ArrowLeft", -1],
|
|
79
|
+
["ArrowRight", 1],
|
|
80
|
+
["ArrowUp", 1],
|
|
81
|
+
]);
|
|
82
|
+
|
|
68
83
|
/**
|
|
69
84
|
* `value` snapped to the grid `step` describes, measured from `min` and held inside `[min, max]`.
|
|
70
85
|
*
|
|
@@ -427,7 +442,7 @@ export function preferredInk(rgb: Rgb): "black" | "white" {
|
|
|
427
442
|
* @param color The colour, as it was written.
|
|
428
443
|
* @returns `"black"`, `"white"`, or `""`.
|
|
429
444
|
*/
|
|
430
|
-
export function inkOn(color:
|
|
431
|
-
const parsed =
|
|
445
|
+
export function inkOn(color: JsonValue | undefined): string {
|
|
446
|
+
const parsed = isString(color) ? parseColor(color) : null;
|
|
432
447
|
return parsed ? preferredInk({ ...parsed, a: 1 }) : "";
|
|
433
448
|
}
|
package/src/documents.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* bytes the shell runs.
|
|
5
5
|
*/
|
|
6
6
|
import { overlayScopeFor } from "@jxsuite/schema/overlays";
|
|
7
|
-
import type {
|
|
7
|
+
import type { JsonPrimitive, JxDocument } from "@jxsuite/schema/types";
|
|
8
8
|
import jxAccordion from "../components/jx-accordion.json";
|
|
9
9
|
import jxAccordionItem from "../components/jx-accordion-item.json";
|
|
10
10
|
import jxActionButton from "../components/jx-action-button.json";
|
|
@@ -44,48 +44,74 @@ import jxTreeItem from "../components/jx-tree-item.json";
|
|
|
44
44
|
import jxToastHost from "../components/jx-toast-host.json";
|
|
45
45
|
import jxToolbar from "../components/jx-toolbar.json";
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A value as TypeScript types a JSON module import. Under the root config's
|
|
49
|
+
* `exactOptionalPropertyTypes` that is always a `JsonValue`, but `packages/desktop` compiles this
|
|
50
|
+
* file without the flag, and there an array of differently shaped objects is normalised by giving
|
|
51
|
+
* each object the others' keys as `?: undefined`, which `JsonValue` refuses.
|
|
52
|
+
*/
|
|
53
|
+
type ImportedJson =
|
|
54
|
+
| JsonPrimitive
|
|
55
|
+
| undefined
|
|
56
|
+
| readonly ImportedJson[]
|
|
57
|
+
| { readonly [key: string]: ImportedJson };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A bundled component file as the document the runtime defines it from.
|
|
61
|
+
*
|
|
62
|
+
* @param {Readonly<Record<string, ImportedJson>>} json - One of the `components/*.json` imports
|
|
63
|
+
* above
|
|
64
|
+
* @returns {JxDocument} The same object, typed as the document it is
|
|
65
|
+
*/
|
|
66
|
+
function kitDocument(json: Readonly<Record<string, ImportedJson>>): JxDocument {
|
|
67
|
+
/* SAFETY: every argument is a `components/*.json` file of this package, and `conformance.test.ts`
|
|
68
|
+
validates each entry of `documents` against the Jx document schema (ui.md §3.2), so the JSON
|
|
69
|
+
is a JxDocument; TypeScript only sees the literal shape the JSON import infers. */
|
|
70
|
+
return json as JxDocument;
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
/** Tag name → document, in registration order (a document's `$elements` come before it). */
|
|
48
|
-
export const documents
|
|
49
|
-
"jx-icon": jxIcon
|
|
50
|
-
"jx-spinner": jxSpinner
|
|
74
|
+
export const documents = {
|
|
75
|
+
"jx-icon": kitDocument(jxIcon),
|
|
76
|
+
"jx-spinner": kitDocument(jxSpinner),
|
|
51
77
|
// Before the buttons: each renders a `jx-tooltip` for its `hint`.
|
|
52
|
-
"jx-tooltip": jxTooltip
|
|
53
|
-
"jx-button": jxButton
|
|
54
|
-
"jx-action-button": jxActionButton
|
|
55
|
-
"jx-menu-item": jxMenuItem
|
|
56
|
-
"jx-menu": jxMenu
|
|
57
|
-
"jx-popover": jxPopover
|
|
58
|
-
"jx-textfield": jxTextfield
|
|
59
|
-
"jx-checkbox": jxCheckbox
|
|
60
|
-
"jx-switch": jxSwitch
|
|
61
|
-
"jx-number-field": jxNumberField
|
|
62
|
-
"jx-select": jxSelect
|
|
63
|
-
"jx-option": jxOption
|
|
64
|
-
"jx-listbox": jxListbox
|
|
65
|
-
"jx-combobox": jxCombobox
|
|
66
|
-
"jx-action-group": jxActionGroup
|
|
67
|
-
"jx-toolbar": jxToolbar
|
|
68
|
-
"jx-tab": jxTab
|
|
69
|
-
"jx-tab-panel": jxTabPanel
|
|
70
|
-
"jx-tabs": jxTabs
|
|
71
|
-
"jx-tree-item": jxTreeItem
|
|
72
|
-
"jx-tree": jxTree
|
|
73
|
-
"jx-accordion-item": jxAccordionItem
|
|
74
|
-
"jx-accordion": jxAccordion
|
|
75
|
-
"jx-divider": jxDivider
|
|
76
|
-
"jx-split": jxSplit
|
|
77
|
-
"jx-kbd": jxKbd
|
|
78
|
-
"jx-dot": jxDot
|
|
79
|
-
"jx-field": jxField
|
|
80
|
-
"jx-dialog": jxDialog
|
|
81
|
-
"jx-toast": jxToast
|
|
82
|
-
"jx-toast-host": jxToastHost
|
|
83
|
-
"jx-color-area": jxColorArea
|
|
84
|
-
"jx-color-slider": jxColorSlider
|
|
85
|
-
"jx-swatch": jxSwatch
|
|
86
|
-
"jx-swatch-group": jxSwatchGroup
|
|
87
|
-
"jx-color-field": jxColorField
|
|
88
|
-
}
|
|
78
|
+
"jx-tooltip": kitDocument(jxTooltip),
|
|
79
|
+
"jx-button": kitDocument(jxButton),
|
|
80
|
+
"jx-action-button": kitDocument(jxActionButton),
|
|
81
|
+
"jx-menu-item": kitDocument(jxMenuItem),
|
|
82
|
+
"jx-menu": kitDocument(jxMenu),
|
|
83
|
+
"jx-popover": kitDocument(jxPopover),
|
|
84
|
+
"jx-textfield": kitDocument(jxTextfield),
|
|
85
|
+
"jx-checkbox": kitDocument(jxCheckbox),
|
|
86
|
+
"jx-switch": kitDocument(jxSwitch),
|
|
87
|
+
"jx-number-field": kitDocument(jxNumberField),
|
|
88
|
+
"jx-select": kitDocument(jxSelect),
|
|
89
|
+
"jx-option": kitDocument(jxOption),
|
|
90
|
+
"jx-listbox": kitDocument(jxListbox),
|
|
91
|
+
"jx-combobox": kitDocument(jxCombobox),
|
|
92
|
+
"jx-action-group": kitDocument(jxActionGroup),
|
|
93
|
+
"jx-toolbar": kitDocument(jxToolbar),
|
|
94
|
+
"jx-tab": kitDocument(jxTab),
|
|
95
|
+
"jx-tab-panel": kitDocument(jxTabPanel),
|
|
96
|
+
"jx-tabs": kitDocument(jxTabs),
|
|
97
|
+
"jx-tree-item": kitDocument(jxTreeItem),
|
|
98
|
+
"jx-tree": kitDocument(jxTree),
|
|
99
|
+
"jx-accordion-item": kitDocument(jxAccordionItem),
|
|
100
|
+
"jx-accordion": kitDocument(jxAccordion),
|
|
101
|
+
"jx-divider": kitDocument(jxDivider),
|
|
102
|
+
"jx-split": kitDocument(jxSplit),
|
|
103
|
+
"jx-kbd": kitDocument(jxKbd),
|
|
104
|
+
"jx-dot": kitDocument(jxDot),
|
|
105
|
+
"jx-field": kitDocument(jxField),
|
|
106
|
+
"jx-dialog": kitDocument(jxDialog),
|
|
107
|
+
"jx-toast": kitDocument(jxToast),
|
|
108
|
+
"jx-toast-host": kitDocument(jxToastHost),
|
|
109
|
+
"jx-color-area": kitDocument(jxColorArea),
|
|
110
|
+
"jx-color-slider": kitDocument(jxColorSlider),
|
|
111
|
+
"jx-swatch": kitDocument(jxSwatch),
|
|
112
|
+
"jx-swatch-group": kitDocument(jxSwatchGroup),
|
|
113
|
+
"jx-color-field": kitDocument(jxColorField),
|
|
114
|
+
} as const satisfies Readonly<Record<string, JxDocument>>;
|
|
89
115
|
|
|
90
116
|
/** Every tag the kit defines. */
|
|
91
117
|
export const KIT_TAGS: readonly string[] = Object.keys(documents);
|
|
@@ -104,7 +130,7 @@ export const KIT_TAGS: readonly string[] = Object.keys(documents);
|
|
|
104
130
|
* asks the same question of a project's `*.class.json` definitions, and two derivations of "what
|
|
105
131
|
* makes a tag a popover" is two chances to disagree about one project.
|
|
106
132
|
*/
|
|
107
|
-
const KIT_SCOPE = overlayScopeFor(Object.values(documents)
|
|
133
|
+
const KIT_SCOPE = overlayScopeFor(Object.values(documents));
|
|
108
134
|
|
|
109
135
|
/** Tags whose definition declares `popover`. */
|
|
110
136
|
export const POPOVER_TAGS: ReadonlySet<string> = KIT_SCOPE.popoverTags ?? new Set();
|
package/src/icons.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @docs extending/ui-kit
|
|
7
7
|
*/
|
|
8
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
9
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
8
10
|
import manifest from "../icons/manifest.json";
|
|
9
11
|
import { ICON_VIEW_BOX, ICON_WEIGHTS } from "./icons-build.ts";
|
|
10
12
|
import type { IconManifest, IconWeight } from "./icons-build.ts";
|
|
@@ -12,7 +14,8 @@ import type { IconManifest, IconWeight } from "./icons-build.ts";
|
|
|
12
14
|
export { ICON_VIEW_BOX, ICON_WEIGHTS } from "./icons-build.ts";
|
|
13
15
|
export type { IconWeight } from "./icons-build.ts";
|
|
14
16
|
|
|
15
|
-
const
|
|
17
|
+
const iconManifest: IconManifest = manifest;
|
|
18
|
+
const { icons } = iconManifest;
|
|
16
19
|
|
|
17
20
|
/** Every icon name the kit ships, sorted. */
|
|
18
21
|
export const ICON_NAMES: readonly string[] = Object.keys(icons);
|
|
@@ -33,10 +36,11 @@ const warned = new Set<string>();
|
|
|
33
36
|
* every listed icon carries; a name it lacks warns once and draws nothing, so a typo shows as a gap
|
|
34
37
|
* rather than a crash or a wrong glyph.
|
|
35
38
|
*
|
|
36
|
-
* Called from the `jx-icon` document with the element's own `name` and `weight` state.
|
|
39
|
+
* Called from the `jx-icon` document with the element's own `name` and `weight` state. Both are
|
|
40
|
+
* declared strings, but a property write can store any value, so neither is trusted to be one.
|
|
37
41
|
*/
|
|
38
|
-
export function iconPath(name:
|
|
39
|
-
const key =
|
|
42
|
+
export function iconPath(name: JsonValue | undefined, weight: JsonValue | undefined): string {
|
|
43
|
+
const key = isString(name) ? name : "";
|
|
40
44
|
const entry = icons[key];
|
|
41
45
|
if (!entry) {
|
|
42
46
|
if (key && !warned.has(key)) {
|
|
@@ -45,11 +49,9 @@ export function iconPath(name: unknown, weight: unknown): string {
|
|
|
45
49
|
}
|
|
46
50
|
return "";
|
|
47
51
|
}
|
|
48
|
-
const w = (
|
|
49
|
-
? (weight as IconWeight)
|
|
50
|
-
: "regular";
|
|
52
|
+
const w = ICON_WEIGHTS.find((candidate) => candidate === String(weight)) ?? "regular";
|
|
51
53
|
return entry[w] ?? entry.regular ?? "";
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** The viewBox `jx-icon` draws on; the manifest's, so a rebuilt manifest cannot drift from it. */
|
|
55
|
-
export const iconViewBox: string =
|
|
57
|
+
export const iconViewBox: string = iconManifest.viewBox || ICON_VIEW_BOX;
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @docs extending/ui-kit
|
|
11
11
|
*/
|
|
12
12
|
import { defineElement, preloadDocument, preloadModule } from "@jxsuite/runtime";
|
|
13
|
+
import type { JxScope } from "@jxsuite/runtime/types";
|
|
13
14
|
import * as color from "./color.ts";
|
|
14
15
|
import * as colorArea from "./behaviors/color-area.ts";
|
|
15
16
|
import * as colorField from "./behaviors/color-field.ts";
|
|
@@ -50,8 +51,12 @@ export {
|
|
|
50
51
|
/** The base every kit document resolves its references against. */
|
|
51
52
|
export const KIT_BASE = "jx-ui:/components/";
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Behaviour modules, under the `$src` specifiers the documents use. The runtime reads each
|
|
56
|
+
* namespace's exports into scope, so every entry must be a `JxScope`: export name → value, narrowed
|
|
57
|
+
* where the runtime uses it.
|
|
58
|
+
*/
|
|
59
|
+
export const KIT_MODULES = {
|
|
55
60
|
"jx-ui:/behaviors/color-area.ts": colorArea,
|
|
56
61
|
"jx-ui:/behaviors/color-field.ts": colorField,
|
|
57
62
|
"jx-ui:/behaviors/color-slider.ts": colorSlider,
|
|
@@ -74,7 +79,7 @@ export const KIT_MODULES: Readonly<Record<string, Record<string, unknown>>> = {
|
|
|
74
79
|
"jx-ui:/behaviors/toast-host.ts": toastHost,
|
|
75
80
|
"jx-ui:/color.ts": color,
|
|
76
81
|
"jx-ui:/icons.ts": icons,
|
|
77
|
-
}
|
|
82
|
+
} as const satisfies Readonly<Record<string, JxScope>>;
|
|
78
83
|
|
|
79
84
|
export interface RegisterUiOptions {
|
|
80
85
|
/** Adopt the theme into the document as well. Default is true. */
|