@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "The Jx UI kit: interface elements authored as Jx documents, with their theme and icons",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,11 +58,12 @@
|
|
|
58
58
|
"upgrade": "bun update --latest && bun install"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@jxsuite/runtime": "^
|
|
62
|
-
"@jxsuite/schema": "^
|
|
61
|
+
"@jxsuite/runtime": "^5.0.0",
|
|
62
|
+
"@jxsuite/schema": "^4.0.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@happy-dom/global-registrator": "^
|
|
65
|
+
"@happy-dom/global-registrator": "^21.0.3",
|
|
66
|
+
"@jxsuite/test-support": "^0.0.0",
|
|
66
67
|
"@phosphor-icons/core": "^2.1.1"
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
* @docs extending/ui-kit
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
41
|
+
|
|
42
|
+
import { ARROW_SIGN, snapToStep } from "../color.ts";
|
|
41
43
|
|
|
42
44
|
/** The tag this sidecar belongs to. */
|
|
43
45
|
const HOST = "jx-color-area";
|
|
@@ -54,27 +56,18 @@ export interface ColorAreaState {
|
|
|
54
56
|
step: number;
|
|
55
57
|
/** Whether the element refuses to move. */
|
|
56
58
|
disabled: boolean;
|
|
57
|
-
[key: string]: unknown;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/** Which state key each axis writes. `x` is saturation and `y` is brightness. */
|
|
61
62
|
const AXIS_KEY = { x: "saturation", y: "brightness" } as const;
|
|
62
63
|
|
|
63
64
|
/** The axis a key belongs to when nothing cancels it. */
|
|
64
|
-
const KEY_AXIS:
|
|
65
|
-
ArrowDown
|
|
66
|
-
ArrowLeft
|
|
67
|
-
ArrowRight
|
|
68
|
-
ArrowUp
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/** Which way each arrow moves its axis. Up and Right increase; the square's y is measured upward. */
|
|
72
|
-
const KEY_SIGN: Readonly<Record<string, number>> = {
|
|
73
|
-
ArrowDown: -1,
|
|
74
|
-
ArrowLeft: -1,
|
|
75
|
-
ArrowRight: 1,
|
|
76
|
-
ArrowUp: 1,
|
|
77
|
-
};
|
|
65
|
+
const KEY_AXIS: ReadonlyMap<string, "x" | "y"> = new Map([
|
|
66
|
+
["ArrowDown", "y"],
|
|
67
|
+
["ArrowLeft", "x"],
|
|
68
|
+
["ArrowRight", "x"],
|
|
69
|
+
["ArrowUp", "y"],
|
|
70
|
+
]);
|
|
78
71
|
|
|
79
72
|
/** The hosts with a pointer down on them. Off to the side, because a drag is not element state. */
|
|
80
73
|
const dragging = new WeakSet<HTMLElement>();
|
|
@@ -151,6 +144,8 @@ export function onAreaAxisInput(state: ColorAreaState, event: Event): void {
|
|
|
151
144
|
return;
|
|
152
145
|
}
|
|
153
146
|
event.stopPropagation();
|
|
147
|
+
/* SAFETY: axisOf answers only for a node whose part is `x` or `y`, and jx-color-area.json renders
|
|
148
|
+
exactly those two parts, each as a native `<input type="range">`. */
|
|
154
149
|
writeAxis(state, axis, Number((event.target as HTMLInputElement).value));
|
|
155
150
|
announce(host, "input");
|
|
156
151
|
}
|
|
@@ -168,6 +163,8 @@ export function onAreaAxisChange(state: ColorAreaState, event: Event): void {
|
|
|
168
163
|
return;
|
|
169
164
|
}
|
|
170
165
|
event.stopPropagation();
|
|
166
|
+
/* SAFETY: axisOf answers only for a node whose part is `x` or `y`, and jx-color-area.json renders
|
|
167
|
+
exactly those two parts, each as a native `<input type="range">`. */
|
|
171
168
|
writeAxis(state, axis, Number((event.target as HTMLInputElement).value));
|
|
172
169
|
announce(host, "change");
|
|
173
170
|
}
|
|
@@ -188,7 +185,7 @@ export function onAreaKeydown(state: ColorAreaState, event: KeyboardEvent): void
|
|
|
188
185
|
if (!host || !focused || state.disabled === true) {
|
|
189
186
|
return;
|
|
190
187
|
}
|
|
191
|
-
const axis = KEY_AXIS
|
|
188
|
+
const axis = KEY_AXIS.get(event.key);
|
|
192
189
|
if (!axis) {
|
|
193
190
|
return;
|
|
194
191
|
}
|
|
@@ -197,7 +194,7 @@ export function onAreaKeydown(state: ColorAreaState, event: KeyboardEvent): void
|
|
|
197
194
|
}
|
|
198
195
|
event.preventDefault();
|
|
199
196
|
event.stopPropagation();
|
|
200
|
-
const delta = stepOf(state) * (event.shiftKey ? 10 : 1) * (
|
|
197
|
+
const delta = stepOf(state) * (event.shiftKey ? 10 : 1) * (ARROW_SIGN.get(event.key) ?? 0);
|
|
201
198
|
if (writeAxis(state, axis, Number(state[AXIS_KEY[axis]]) + delta)) {
|
|
202
199
|
/* Both names: a keystroke on a slider is a move AND a commit, which is what the platform's own
|
|
203
200
|
arrow does on the axis it owns. A key that changed nothing says nothing. */
|
|
@@ -244,7 +241,7 @@ export function onAreaPointerDown(state: ColorAreaState, event: PointerEvent): v
|
|
|
244
241
|
event.preventDefault();
|
|
245
242
|
dragging.add(host);
|
|
246
243
|
const track = trackOf(host);
|
|
247
|
-
if (track &&
|
|
244
|
+
if (track && isFunction(track.setPointerCapture)) {
|
|
248
245
|
/* Capture on the TRACK, so a drag that leaves the square keeps arriving here rather than
|
|
249
246
|
stopping wherever the pointer crossed the edge. */
|
|
250
247
|
try {
|
|
@@ -294,8 +291,9 @@ export function onAreaPointerUp(_state: ColorAreaState, event: PointerEvent): vo
|
|
|
294
291
|
const track = trackOf(host);
|
|
295
292
|
if (
|
|
296
293
|
track &&
|
|
297
|
-
|
|
298
|
-
track.hasPointerCapture
|
|
294
|
+
isFunction(track.releasePointerCapture) &&
|
|
295
|
+
isFunction(track.hasPointerCapture) &&
|
|
296
|
+
track.hasPointerCapture(event.pointerId)
|
|
299
297
|
) {
|
|
300
298
|
track.releasePointerCapture(event.pointerId);
|
|
301
299
|
}
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
* @docs extending/ui-kit
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
37
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
38
|
+
|
|
36
39
|
import {
|
|
37
40
|
formatHex,
|
|
38
41
|
formatOklch,
|
|
@@ -44,19 +47,24 @@ import {
|
|
|
44
47
|
} from "../color.ts";
|
|
45
48
|
import type { Rgb } from "../color.ts";
|
|
46
49
|
import { close, openAt } from "./popover.ts";
|
|
50
|
+
import type { PlacedPopover } from "./popover.ts";
|
|
47
51
|
|
|
48
52
|
/** The tag this sidecar belongs to. */
|
|
49
53
|
const HOST = "jx-color-field";
|
|
50
54
|
|
|
51
55
|
/** The reactive scope a `jx-color-field` document hands its handlers. */
|
|
52
56
|
export interface ColorFieldState {
|
|
53
|
-
/**
|
|
54
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The colour, spelled as `format` says. Null when a host clears the reflected `value` attribute,
|
|
59
|
+
* which is a write this module reads but never makes.
|
|
60
|
+
*/
|
|
61
|
+
value: string | null;
|
|
55
62
|
/**
|
|
56
63
|
* The colour behind `value` when `value` is a reference this page cannot resolve; empty when the
|
|
57
|
-
* value stands for itself. Drawn by the chip and opened on by the picker, never written
|
|
64
|
+
* value stands for itself. Drawn by the chip and opened on by the picker, never written; null
|
|
65
|
+
* when a host clears the reflected attribute.
|
|
58
66
|
*/
|
|
59
|
-
resolved: string;
|
|
67
|
+
resolved: string | null;
|
|
60
68
|
/** `hex` or `oklch`: which spelling {@link compose} writes. */
|
|
61
69
|
format: string;
|
|
62
70
|
/** Whether the alpha channel is offered at all. */
|
|
@@ -83,14 +91,26 @@ export interface ColorFieldState {
|
|
|
83
91
|
uid: string;
|
|
84
92
|
/** Whether the element refuses to move. */
|
|
85
93
|
disabled: boolean;
|
|
86
|
-
[key: string]: unknown;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The system's screen-colour picker, where the engine has one. Not in the TypeScript DOM library
|
|
98
|
+
* yet, so its result is typed as loosely as an engine's own implementation could hand it back.
|
|
99
|
+
*/
|
|
90
100
|
interface EyeDropperApi {
|
|
91
|
-
open: () => Promise<{ sRGBHex
|
|
101
|
+
open: () => Promise<{ sRGBHex?: string } | undefined>;
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A node the document has named a channel, with the properties the composed elements install: the
|
|
106
|
+
* area's two axes, and the `value` a slider or a slotted palette carries.
|
|
107
|
+
*/
|
|
108
|
+
type ChannelElement = HTMLElement & {
|
|
109
|
+
saturation?: number;
|
|
110
|
+
brightness?: number;
|
|
111
|
+
value?: number | string;
|
|
112
|
+
};
|
|
113
|
+
|
|
94
114
|
/** How many stems have been minted, so no two instances share one. */
|
|
95
115
|
let minted = 0;
|
|
96
116
|
|
|
@@ -120,9 +140,9 @@ function ownerOf(event: Event): HTMLElement | null {
|
|
|
120
140
|
return target instanceof Element ? target.closest<HTMLElement>(HOST) : null;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
/** The picker panel. */
|
|
124
|
-
function panelOf(host: HTMLElement):
|
|
125
|
-
return host.querySelector<
|
|
143
|
+
/** The picker panel: the `jx-popover` jx-color-field.json renders as its `picker` part. */
|
|
144
|
+
function panelOf(host: HTMLElement): PlacedPopover | null {
|
|
145
|
+
return host.querySelector<PlacedPopover>('[part="picker"]');
|
|
126
146
|
}
|
|
127
147
|
|
|
128
148
|
/** The colour the channels currently describe. */
|
|
@@ -242,11 +262,20 @@ const CHANNEL = "[data-jx-channel]";
|
|
|
242
262
|
* channel by name is what makes the answer the same for a control the element owns, a control one
|
|
243
263
|
* of its children owns, and a palette a consumer slotted in.
|
|
244
264
|
*/
|
|
245
|
-
function sourceOf(event: Event):
|
|
265
|
+
function sourceOf(event: Event): ChannelElement | null {
|
|
246
266
|
const { target } = event;
|
|
247
|
-
return target instanceof Element
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
return target instanceof Element ? target.closest<ChannelElement>(CHANNEL) : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The `value` an event's target carries, as text; empty when it carries none.
|
|
272
|
+
*
|
|
273
|
+
* The target is a native `<input>` for the text and system channels, but for a palette it is
|
|
274
|
+
* whatever a consumer slotted in, which may expose no `value` at all.
|
|
275
|
+
*/
|
|
276
|
+
function targetValue(event: Event): string {
|
|
277
|
+
const { target } = event;
|
|
278
|
+
return target !== null && "value" in target ? String(target.value ?? "") : "";
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
/**
|
|
@@ -272,7 +301,7 @@ function route(state: ColorFieldState, event: Event, name: "input" | "change"):
|
|
|
272
301
|
}
|
|
273
302
|
event.stopPropagation();
|
|
274
303
|
if (channel === "text") {
|
|
275
|
-
const typed =
|
|
304
|
+
const typed = targetValue(event);
|
|
276
305
|
state.text = typed;
|
|
277
306
|
const parsed = parseColor(typed);
|
|
278
307
|
if (name === "input") {
|
|
@@ -295,18 +324,18 @@ function route(state: ColorFieldState, event: Event, name: "input" | "change"):
|
|
|
295
324
|
whatever announced them, and both are opaque by construction — a colour well cannot express
|
|
296
325
|
an alpha and a palette entry need not — so the alpha the reader already chose is carried
|
|
297
326
|
across rather than reset to full by a control that has nothing to say about it. */
|
|
298
|
-
const parsed = parseColor(
|
|
327
|
+
const parsed = parseColor(targetValue(event));
|
|
299
328
|
if (!parsed) {
|
|
300
329
|
return;
|
|
301
330
|
}
|
|
302
331
|
applyRgb(state, { ...parsed, a: state.alpha === true ? Number(state.opacity) / 100 : 1 });
|
|
303
332
|
} else if (channel === "area") {
|
|
304
|
-
state.saturation = Number(source
|
|
305
|
-
state.brightness = Number(source
|
|
333
|
+
state.saturation = Number(source.saturation);
|
|
334
|
+
state.brightness = Number(source.brightness);
|
|
306
335
|
} else if (channel === "hue") {
|
|
307
|
-
state.hue = Number(source
|
|
336
|
+
state.hue = Number(source.value);
|
|
308
337
|
} else if (channel === "opacity") {
|
|
309
|
-
state.opacity = Number(source
|
|
338
|
+
state.opacity = Number(source.value);
|
|
310
339
|
} else {
|
|
311
340
|
return;
|
|
312
341
|
}
|
|
@@ -373,8 +402,8 @@ export function onOpenPicker(state: ColorFieldState, event: Event): void {
|
|
|
373
402
|
return;
|
|
374
403
|
}
|
|
375
404
|
const box = host.getBoundingClientRect();
|
|
376
|
-
|
|
377
|
-
|
|
405
|
+
panel.x = Math.round(box.left);
|
|
406
|
+
panel.y = Math.round(box.bottom);
|
|
378
407
|
openAt(panel, event.currentTarget instanceof Element ? event.currentTarget : host);
|
|
379
408
|
}
|
|
380
409
|
|
|
@@ -392,10 +421,16 @@ export function onOpenPicker(state: ColorFieldState, event: Event): void {
|
|
|
392
421
|
* @param offered The element's own `eyedropper` prop.
|
|
393
422
|
* @returns Whether to draw the button.
|
|
394
423
|
*/
|
|
395
|
-
export function eyeDropperAvailable(offered:
|
|
396
|
-
return (
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
export function eyeDropperAvailable(offered: JsonValue | undefined): boolean {
|
|
425
|
+
return offered === true && isFunction(eyeDropperOf());
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** The engine's `EyeDropper` constructor, or undefined where it ships none. */
|
|
429
|
+
function eyeDropperOf(): (new () => EyeDropperApi) | undefined {
|
|
430
|
+
/* SAFETY: `EyeDropper` is the WICG screen-picker constructor, absent from lib.dom; where an engine
|
|
431
|
+
ships it, `new EyeDropper().open()` resolves the picked colour, and where it does not the
|
|
432
|
+
property is undefined, which every caller guards. */
|
|
433
|
+
return (globalThis as { EyeDropper?: new () => EyeDropperApi }).EyeDropper;
|
|
399
434
|
}
|
|
400
435
|
|
|
401
436
|
/**
|
|
@@ -410,7 +445,7 @@ export function eyeDropperAvailable(offered: unknown): boolean {
|
|
|
410
445
|
*/
|
|
411
446
|
export function onEyeDropper(state: ColorFieldState, event: Event): Promise<void> {
|
|
412
447
|
const host = ownerOf(event);
|
|
413
|
-
const Api = (
|
|
448
|
+
const Api = eyeDropperOf();
|
|
414
449
|
if (!host || !Api || state.disabled === true) {
|
|
415
450
|
return Promise.resolve();
|
|
416
451
|
}
|
|
@@ -459,7 +494,7 @@ export function onFieldMount(state: ColorFieldState, event: Event): void {
|
|
|
459
494
|
const panel = panelOf(host);
|
|
460
495
|
if (panel) {
|
|
461
496
|
panel.addEventListener("toggle", (toggle: Event) => {
|
|
462
|
-
state.expanded =
|
|
497
|
+
state.expanded = "newState" in toggle && toggle.newState === "open";
|
|
463
498
|
});
|
|
464
499
|
}
|
|
465
500
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* @docs extending/ui-kit
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { snapToStep } from "../color.ts";
|
|
27
|
+
import { ARROW_SIGN, snapToStep } from "../color.ts";
|
|
28
28
|
|
|
29
29
|
/** The tag this sidecar belongs to. */
|
|
30
30
|
const HOST = "jx-color-slider";
|
|
@@ -41,16 +41,14 @@ export interface ColorSliderState {
|
|
|
41
41
|
step: number;
|
|
42
42
|
/** Whether the element refuses to move. */
|
|
43
43
|
disabled: boolean;
|
|
44
|
-
[key: string]: unknown;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
46
|
+
/** The track's resolved range and grid, every member a finite number. */
|
|
47
|
+
interface SliderBounds {
|
|
48
|
+
min: number;
|
|
49
|
+
max: number;
|
|
50
|
+
step: number;
|
|
51
|
+
}
|
|
54
52
|
|
|
55
53
|
/** The `jx-color-slider` an event is being handled on. */
|
|
56
54
|
function hostOf(event: Event): HTMLElement | null {
|
|
@@ -79,7 +77,7 @@ function announce(host: HTMLElement, ...names: string[]): void {
|
|
|
79
77
|
* is what the inner range's own `max` attribute is bound to, so the platform's clamp and this one
|
|
80
78
|
* cannot drift apart.
|
|
81
79
|
*/
|
|
82
|
-
function boundsOf(state: ColorSliderState):
|
|
80
|
+
function boundsOf(state: ColorSliderState): SliderBounds {
|
|
83
81
|
const min = Number.isFinite(Number(state.min)) ? Number(state.min) : 0;
|
|
84
82
|
const top = Number(state.maxValue);
|
|
85
83
|
const step = Number(state.step);
|
|
@@ -104,6 +102,8 @@ export function onSliderInput(state: ColorSliderState, event: Event): void {
|
|
|
104
102
|
return;
|
|
105
103
|
}
|
|
106
104
|
event.stopPropagation();
|
|
105
|
+
/* SAFETY: the element's only form control is its `input[part="input"]` range, so an `input` event
|
|
106
|
+
reaching the host from anything other than the host itself was fired by that range. */
|
|
107
107
|
state.value = Number((event.target as HTMLInputElement).value);
|
|
108
108
|
announce(host, "input");
|
|
109
109
|
}
|
|
@@ -120,6 +120,8 @@ export function onSliderChange(state: ColorSliderState, event: Event): void {
|
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
event.stopPropagation();
|
|
123
|
+
/* SAFETY: the element's only form control is its `input[part="input"]` range, so a `change` event
|
|
124
|
+
reaching the host from anything other than the host itself was fired by that range. */
|
|
123
125
|
state.value = Number((event.target as HTMLInputElement).value);
|
|
124
126
|
announce(host, "change");
|
|
125
127
|
}
|
|
@@ -132,7 +134,7 @@ export function onSliderChange(state: ColorSliderState, event: Event): void {
|
|
|
132
134
|
*/
|
|
133
135
|
export function onSliderKeydown(state: ColorSliderState, event: KeyboardEvent): void {
|
|
134
136
|
const host = hostOf(event);
|
|
135
|
-
const sign =
|
|
137
|
+
const sign = ARROW_SIGN.get(event.key);
|
|
136
138
|
if (!host || !sign || !event.shiftKey || state.disabled === true) {
|
|
137
139
|
return;
|
|
138
140
|
}
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
import { close, openAt } from "./popover.ts";
|
|
52
|
+
import type { PlacedPopover } from "./popover.ts";
|
|
52
53
|
|
|
53
54
|
/** A row as the element draws it. */
|
|
54
55
|
export interface ComboboxRow {
|
|
@@ -84,7 +85,6 @@ export interface ComboboxState {
|
|
|
84
85
|
disabled?: boolean;
|
|
85
86
|
readonly?: boolean;
|
|
86
87
|
uid?: string;
|
|
87
|
-
[key: string]: unknown;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const HOST = "jx-combobox";
|
|
@@ -103,9 +103,9 @@ export function inputOf(host: HTMLElement): HTMLInputElement | null {
|
|
|
103
103
|
return host.querySelector<HTMLInputElement>('input[part="input"]');
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/** The element's own panel. */
|
|
107
|
-
function panelOf(host: HTMLElement):
|
|
108
|
-
return host.querySelector<
|
|
106
|
+
/** The element's own panel: the `jx-popover` jx-combobox.json renders as its `popup` part. */
|
|
107
|
+
function panelOf(host: HTMLElement): PlacedPopover | null {
|
|
108
|
+
return host.querySelector<PlacedPopover>('[part="popup"]');
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/** The rows the element was handed, whatever it was handed. */
|
|
@@ -146,9 +146,8 @@ export function openList(host: HTMLElement): void {
|
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
const box = host.getBoundingClientRect();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
placed.y = Math.round(box.bottom);
|
|
149
|
+
panel.x = Math.round(box.left);
|
|
150
|
+
panel.y = Math.round(box.bottom);
|
|
152
151
|
openAt(panel, host);
|
|
153
152
|
}
|
|
154
153
|
|
|
@@ -295,7 +294,7 @@ export function onComboboxInput(state: ComboboxState, event: Event): void {
|
|
|
295
294
|
* @param event The platform's ToggleEvent, from the panel.
|
|
296
295
|
*/
|
|
297
296
|
export function onComboboxToggle(state: ComboboxState, event: Event): void {
|
|
298
|
-
const opening =
|
|
297
|
+
const opening = "newState" in event && event.newState === "open";
|
|
299
298
|
state.open = opening;
|
|
300
299
|
if (!opening) {
|
|
301
300
|
state.activeIndex = -1;
|
|
@@ -332,7 +331,7 @@ export function onComboboxButton(state: ComboboxState, event: Event): void {
|
|
|
332
331
|
*/
|
|
333
332
|
export function onComboboxPick(state: ComboboxState, event: Event): void {
|
|
334
333
|
const host = comboboxOf(event);
|
|
335
|
-
const picked = String(
|
|
334
|
+
const picked = "detail" in event ? String(event.detail ?? "") : "";
|
|
336
335
|
const row = rowFor(rowsOf(state), picked);
|
|
337
336
|
if (!host || !row) {
|
|
338
337
|
return;
|
package/src/behaviors/dialog.ts
CHANGED
|
@@ -79,7 +79,7 @@ export function close(host: HTMLElement, returnValue?: string): void {
|
|
|
79
79
|
|
|
80
80
|
/** The platform's `toggle` on the inner dialog: the one source of truth for `open`. */
|
|
81
81
|
export function onToggle(state: DialogState, event: Event): void {
|
|
82
|
-
state.open =
|
|
82
|
+
state.open = "newState" in event && event.newState === "open";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -106,7 +106,7 @@ export function onNativeClose(state: DialogState, event: Event): void {
|
|
|
106
106
|
*/
|
|
107
107
|
export function onCommand(_state: DialogState, event: Event): void {
|
|
108
108
|
const host = hostOf(event);
|
|
109
|
-
const command =
|
|
109
|
+
const command = "command" in event ? event.command : undefined;
|
|
110
110
|
if (!host) {
|
|
111
111
|
return;
|
|
112
112
|
}
|
package/src/behaviors/listbox.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* @docs extending/ui-kit
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
35
|
+
|
|
34
36
|
/** A row element with the property accessors its own document installs. */
|
|
35
37
|
type OptionElement = HTMLElement & { selected?: boolean; disabled?: boolean; value?: string };
|
|
36
38
|
|
|
@@ -38,7 +40,6 @@ type OptionElement = HTMLElement & { selected?: boolean; disabled?: boolean; val
|
|
|
38
40
|
export interface ListboxState {
|
|
39
41
|
/** The id of the row the caret is on; the empty string when none is. */
|
|
40
42
|
active?: string;
|
|
41
|
-
[key: string]: unknown;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const ROW = "jx-option";
|
|
@@ -81,7 +82,7 @@ export function syncListbox(host: HTMLElement, active: string): OptionElement |
|
|
|
81
82
|
row.selected = wanted;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
|
-
if (landed && moved &&
|
|
85
|
+
if (landed && moved && isFunction(landed.scrollIntoView)) {
|
|
85
86
|
landed.scrollIntoView({ block: "nearest" });
|
|
86
87
|
}
|
|
87
88
|
return landed;
|
|
@@ -125,7 +126,7 @@ export function mountListbox(state: ListboxState, host: HTMLElement): void {
|
|
|
125
126
|
});
|
|
126
127
|
};
|
|
127
128
|
const observer = new MutationObserver(() => {
|
|
128
|
-
if (
|
|
129
|
+
if (isFunction(globalThis.requestAnimationFrame)) {
|
|
129
130
|
scheduleSync();
|
|
130
131
|
} else {
|
|
131
132
|
syncListbox(host, String(state.active ?? ""));
|
package/src/behaviors/menu.ts
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* @docs extending/ui-kit
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
28
|
+
|
|
27
29
|
import { anchorOf, clampIntoViewport, openAt, supportsAnchorPositioning } from "./popover.ts";
|
|
28
30
|
|
|
29
31
|
export { clampIntoViewport } from "./popover.ts";
|
|
@@ -43,7 +45,6 @@ export interface MenuState {
|
|
|
43
45
|
area?: string;
|
|
44
46
|
/** Whether the platform is placing this panel against the element it was shown from. */
|
|
45
47
|
anchored?: boolean;
|
|
46
|
-
[key: string]: unknown;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/** Where a submenu sits when its document says nothing: beside its row, extending downward. */
|
|
@@ -57,9 +58,7 @@ type MenuElement = HTMLElement & { open?: boolean; x?: number; y?: number; floor
|
|
|
57
58
|
/** The menu an event's `currentTarget` is. */
|
|
58
59
|
function menuOf(event: Event): MenuElement | null {
|
|
59
60
|
const target = event.currentTarget;
|
|
60
|
-
return target instanceof HTMLElement && target.localName === MENU
|
|
61
|
-
? (target as MenuElement)
|
|
62
|
-
: null;
|
|
61
|
+
return target instanceof HTMLElement && target.localName === MENU ? target : null;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
/**
|
|
@@ -99,11 +98,11 @@ export function submenuOf(row: HTMLElement): MenuElement | null {
|
|
|
99
98
|
|
|
100
99
|
/** The row a submenu is slotted into, if it is a submenu. */
|
|
101
100
|
export function parentRowOf(menu: HTMLElement): RowElement | null {
|
|
102
|
-
return
|
|
101
|
+
return menu.parentElement?.closest<RowElement>(ROW) ?? null;
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
/** The outermost menu of a stack. */
|
|
106
|
-
export function rootMenuOf(menu: HTMLElement):
|
|
105
|
+
export function rootMenuOf(menu: HTMLElement): MenuElement {
|
|
107
106
|
let root = menu;
|
|
108
107
|
for (;;) {
|
|
109
108
|
const row = parentRowOf(root);
|
|
@@ -140,7 +139,7 @@ export function showMenu(menu: MenuElement, source?: Element | null): void {
|
|
|
140
139
|
|
|
141
140
|
/** Hide a menu panel through the platform, if it is showing. */
|
|
142
141
|
export function hideMenu(menu: MenuElement): void {
|
|
143
|
-
if (isShowing(menu) &&
|
|
142
|
+
if (isShowing(menu) && isFunction(menu.hidePopover)) {
|
|
144
143
|
menu.hidePopover();
|
|
145
144
|
}
|
|
146
145
|
}
|
|
@@ -201,7 +200,7 @@ function closeSubmenus(menu: HTMLElement, except: RowElement | null = null): voi
|
|
|
201
200
|
|
|
202
201
|
/** A row's label text: its label part, else all of its text. */
|
|
203
202
|
function rowLabel(row: HTMLElement): string {
|
|
204
|
-
return row.querySelector('[part="label"]')?.textContent ?? row.textContent
|
|
203
|
+
return row.querySelector('[part="label"]')?.textContent ?? row.textContent;
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
/** The next row after `active` whose label starts with `char`, wrapping; -1 when none does. */
|
|
@@ -285,7 +284,7 @@ export function onMenuKeydown(_state: MenuState, event: KeyboardEvent): void {
|
|
|
285
284
|
break;
|
|
286
285
|
}
|
|
287
286
|
case "Tab": {
|
|
288
|
-
hideMenu(rootMenuOf(menu)
|
|
287
|
+
hideMenu(rootMenuOf(menu));
|
|
289
288
|
break;
|
|
290
289
|
}
|
|
291
290
|
default: {
|
|
@@ -321,10 +320,10 @@ export function onMenuBeforeToggle(state: MenuState, event: Event): void {
|
|
|
321
320
|
if (!menu) {
|
|
322
321
|
return;
|
|
323
322
|
}
|
|
324
|
-
const opening =
|
|
323
|
+
const opening = "newState" in event && event.newState === "open";
|
|
325
324
|
const area = String(state.placement ?? "") || (parentRowOf(menu) ? SUBMENU_PLACEMENT : "");
|
|
326
325
|
state.area = area;
|
|
327
|
-
const
|
|
326
|
+
const source = "source" in event ? event.source : null;
|
|
328
327
|
const from = source instanceof Element ? source : anchorOf(menu);
|
|
329
328
|
state.anchored = opening && area !== "" && from !== null && !hasFloor(menu);
|
|
330
329
|
}
|
|
@@ -342,7 +341,7 @@ export function onMenuToggle(state: MenuState, event: Event): void {
|
|
|
342
341
|
if (!menu) {
|
|
343
342
|
return;
|
|
344
343
|
}
|
|
345
|
-
const opening =
|
|
344
|
+
const opening = "newState" in event && event.newState === "open";
|
|
346
345
|
state.open = opening;
|
|
347
346
|
const parent = parentRowOf(menu);
|
|
348
347
|
if (!opening) {
|
|
@@ -365,12 +364,12 @@ export function onMenuToggle(state: MenuState, event: Event): void {
|
|
|
365
364
|
|
|
366
365
|
/** Whether the stack's root declares a floor at all. */
|
|
367
366
|
function hasFloor(menu: HTMLElement): boolean {
|
|
368
|
-
return Number(
|
|
367
|
+
return Number(rootMenuOf(menu).floor ?? 0) > 0;
|
|
369
368
|
}
|
|
370
369
|
|
|
371
370
|
/** The lowest edge a stack may reach: the root menu's `floor`, else the viewport's bottom. */
|
|
372
371
|
function floorOf(menu: HTMLElement): number {
|
|
373
|
-
const floor = Number(
|
|
372
|
+
const floor = Number(rootMenuOf(menu).floor ?? 0);
|
|
374
373
|
return floor > 0 ? floor : window.innerHeight;
|
|
375
374
|
}
|
|
376
375
|
|
|
@@ -385,7 +384,7 @@ export function ensureCaret(menu: HTMLElement): void {
|
|
|
385
384
|
if (menu.contains(document.activeElement)) {
|
|
386
385
|
return;
|
|
387
386
|
}
|
|
388
|
-
let deepest = menu
|
|
387
|
+
let deepest: MenuElement = menu;
|
|
389
388
|
for (;;) {
|
|
390
389
|
const open = rowsOf(deepest, { includeDisabled: true })
|
|
391
390
|
.map((row) => submenuOf(row))
|
|
@@ -41,7 +41,6 @@ export interface NumberFieldState {
|
|
|
41
41
|
value: string;
|
|
42
42
|
/** Whether the control is holding something it cannot parse as a number. */
|
|
43
43
|
badInput: boolean;
|
|
44
|
-
[key: string]: unknown;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
const HOST = "jx-number-field";
|
|
@@ -65,7 +64,7 @@ function controlOf(host: HTMLElement): HTMLInputElement | null {
|
|
|
65
64
|
*/
|
|
66
65
|
function mirror(state: NumberFieldState, control: HTMLInputElement): void {
|
|
67
66
|
state.value = control.value;
|
|
68
|
-
state.badInput = control.validity
|
|
67
|
+
state.badInput = control.validity.badInput;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
/**
|
|
@@ -115,16 +114,15 @@ export function stepBy(state: NumberFieldState, event: Event, n: number): void {
|
|
|
115
114
|
* @param state The element's reactive scope.
|
|
116
115
|
* @param event The keyboard event.
|
|
117
116
|
*/
|
|
118
|
-
export function onNumberKeydown(state: NumberFieldState, event:
|
|
119
|
-
|
|
120
|
-
if (!key.shiftKey) {
|
|
117
|
+
export function onNumberKeydown(state: NumberFieldState, event: KeyboardEvent): void {
|
|
118
|
+
if (!event.shiftKey) {
|
|
121
119
|
return;
|
|
122
120
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
121
|
+
if (event.key === "ArrowUp") {
|
|
122
|
+
event.preventDefault();
|
|
125
123
|
stepBy(state, event, 10);
|
|
126
|
-
} else if (
|
|
127
|
-
|
|
124
|
+
} else if (event.key === "ArrowDown") {
|
|
125
|
+
event.preventDefault();
|
|
128
126
|
stepBy(state, event, -10);
|
|
129
127
|
}
|
|
130
128
|
}
|