@jxsuite/ui 0.5.0 → 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/icons/list.json +1 -0
- package/icons/manifest.json +3 -0
- 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/stylebook/jx-icon.json +1 -0
|
@@ -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
|
}
|
package/src/behaviors/popover.ts
CHANGED
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
* @docs extending/ui-kit
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
49
|
+
|
|
48
50
|
/** The reactive scope a popover-shaped document hands its handlers. */
|
|
49
51
|
export interface PopoverState {
|
|
50
52
|
open?: boolean;
|
|
@@ -56,7 +58,6 @@ export interface PopoverState {
|
|
|
56
58
|
x?: number;
|
|
57
59
|
y?: number;
|
|
58
60
|
floor?: number;
|
|
59
|
-
[key: string]: unknown;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/**
|
|
@@ -66,28 +67,23 @@ export interface PopoverState {
|
|
|
66
67
|
* stub it either way.
|
|
67
68
|
*/
|
|
68
69
|
export function supportsAnchorPositioning(): boolean {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The popover API as this module calls it.
|
|
76
|
-
*
|
|
77
|
-
* Declared here rather than reached for through the DOM types because an engine may not have it at
|
|
78
|
-
* all — the kit publishes to site authors, and every call site guards on `typeof`.
|
|
79
|
-
*/
|
|
80
|
-
interface PopoverApi {
|
|
81
|
-
showPopover?: (options?: { source?: HTMLElement }) => void;
|
|
82
|
-
hidePopover?: () => void;
|
|
70
|
+
return (
|
|
71
|
+
typeof CSS !== "undefined" &&
|
|
72
|
+
isFunction(CSS.supports) &&
|
|
73
|
+
CSS.supports("position-area", "block-end")
|
|
74
|
+
);
|
|
83
75
|
}
|
|
84
76
|
|
|
85
|
-
/**
|
|
86
|
-
export type
|
|
77
|
+
/** A `jx-popover` element, with the `x` and `y` coordinates its document declares as props. */
|
|
78
|
+
export type PlacedPopover = HTMLElement & { x: number; y: number };
|
|
87
79
|
|
|
88
|
-
/**
|
|
89
|
-
function
|
|
90
|
-
|
|
80
|
+
/** Run `callback` in the next frame, or now on an engine with no frames to wait for. */
|
|
81
|
+
function inFrame(callback: () => void): void {
|
|
82
|
+
if (isFunction(globalThis.requestAnimationFrame)) {
|
|
83
|
+
requestAnimationFrame(callback);
|
|
84
|
+
} else {
|
|
85
|
+
callback();
|
|
86
|
+
}
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
/**
|
|
@@ -126,13 +122,12 @@ export function anchorOf(host: HTMLElement): Element | null {
|
|
|
126
122
|
* @param host The panel element.
|
|
127
123
|
* @param anchor The element the panel was opened from, if any.
|
|
128
124
|
*/
|
|
129
|
-
export function openAt(host:
|
|
130
|
-
|
|
131
|
-
if (typeof api.showPopover !== "function") {
|
|
125
|
+
export function openAt(host: HTMLElement, anchor?: Element | null): void {
|
|
126
|
+
if (!isFunction(host.showPopover)) {
|
|
132
127
|
return;
|
|
133
128
|
}
|
|
134
129
|
if (!anchor) {
|
|
135
|
-
|
|
130
|
+
host.showPopover();
|
|
136
131
|
return;
|
|
137
132
|
}
|
|
138
133
|
/* BEFORE the call, because `beforetoggle` fires inside it and {@link onBeforeToggle} reads the
|
|
@@ -141,7 +136,7 @@ export function openAt(host: PopoverElement, anchor?: Element | null): void {
|
|
|
141
136
|
open. */
|
|
142
137
|
anchors.set(host, anchor);
|
|
143
138
|
try {
|
|
144
|
-
show(
|
|
139
|
+
show(host, anchor);
|
|
145
140
|
} catch (error) {
|
|
146
141
|
anchors.delete(host);
|
|
147
142
|
throw error;
|
|
@@ -157,16 +152,16 @@ export function openAt(host: PopoverElement, anchor?: Element | null): void {
|
|
|
157
152
|
* that one bare throws the same error a second time, out of `openAt` and into whatever bound the
|
|
158
153
|
* handler. So it is re-raised from the FIRST call, unswallowed.
|
|
159
154
|
*/
|
|
160
|
-
function show(
|
|
155
|
+
function show(host: HTMLElement, anchor: Element): void {
|
|
161
156
|
try {
|
|
162
|
-
/* `ShowPopoverOptions.source` is typed `HTMLElement`, but any element can be a
|
|
157
|
+
/* SAFETY: `ShowPopoverOptions.source` is typed `HTMLElement`, but any element can be a
|
|
163
158
|
trigger and the platform only reads it as a focus origin. */
|
|
164
|
-
|
|
159
|
+
host.showPopover({ source: anchor as HTMLElement });
|
|
165
160
|
} catch (error) {
|
|
166
161
|
if (!(error instanceof TypeError)) {
|
|
167
162
|
throw error;
|
|
168
163
|
}
|
|
169
|
-
|
|
164
|
+
host.showPopover();
|
|
170
165
|
}
|
|
171
166
|
}
|
|
172
167
|
|
|
@@ -175,10 +170,9 @@ function show(api: PopoverApi, anchor: Element): void {
|
|
|
175
170
|
*
|
|
176
171
|
* @param host The panel element.
|
|
177
172
|
*/
|
|
178
|
-
export function close(host:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
api.hidePopover();
|
|
173
|
+
export function close(host: HTMLElement): void {
|
|
174
|
+
if (isFunction(host.hidePopover)) {
|
|
175
|
+
host.hidePopover();
|
|
182
176
|
}
|
|
183
177
|
}
|
|
184
178
|
|
|
@@ -210,7 +204,7 @@ function invokerOf(host: HTMLElement): Element | null {
|
|
|
210
204
|
* {@link openAt} recorded, then the invoker that names the panel.
|
|
211
205
|
*/
|
|
212
206
|
function toggleSourceOf(host: HTMLElement, event: Event): Element | null {
|
|
213
|
-
const
|
|
207
|
+
const source = "source" in event ? event.source : null;
|
|
214
208
|
if (source instanceof Element) {
|
|
215
209
|
return source;
|
|
216
210
|
}
|
|
@@ -250,16 +244,12 @@ export function measureAnchor(
|
|
|
250
244
|
*/
|
|
251
245
|
function clearWhenStill(state: PopoverState, host: HTMLElement): void {
|
|
252
246
|
const settle = (): void => {
|
|
253
|
-
if (
|
|
247
|
+
if (isFunction(host.getAnimations) && host.getAnimations().length > 0) {
|
|
254
248
|
return;
|
|
255
249
|
}
|
|
256
250
|
state.settling = false;
|
|
257
251
|
};
|
|
258
|
-
|
|
259
|
-
requestAnimationFrame(settle);
|
|
260
|
-
} else {
|
|
261
|
-
settle();
|
|
262
|
-
}
|
|
252
|
+
inFrame(settle);
|
|
263
253
|
}
|
|
264
254
|
|
|
265
255
|
/**
|
|
@@ -278,7 +268,7 @@ export function onBeforeToggle(state: PopoverState, event: Event): void {
|
|
|
278
268
|
if (!host) {
|
|
279
269
|
return;
|
|
280
270
|
}
|
|
281
|
-
const opening =
|
|
271
|
+
const opening = "newState" in event && event.newState === "open";
|
|
282
272
|
const floor = Number(state.floor ?? 0);
|
|
283
273
|
state.anchored = opening && floor <= 0 && toggleSourceOf(host, event) !== null;
|
|
284
274
|
}
|
|
@@ -297,7 +287,7 @@ export function onToggle(state: PopoverState, event: Event): void {
|
|
|
297
287
|
if (!host) {
|
|
298
288
|
return;
|
|
299
289
|
}
|
|
300
|
-
const opening =
|
|
290
|
+
const opening = "newState" in event && event.newState === "open";
|
|
301
291
|
state.open = opening;
|
|
302
292
|
state.settling = true;
|
|
303
293
|
if (opening) {
|
|
@@ -391,9 +381,5 @@ export function clampIntoViewport(
|
|
|
391
381
|
state.y = Math.max(margin, Math.round(floor - box.height - blockOffsetOf(panel)));
|
|
392
382
|
}
|
|
393
383
|
};
|
|
394
|
-
|
|
395
|
-
requestAnimationFrame(measure);
|
|
396
|
-
} else {
|
|
397
|
-
measure();
|
|
398
|
-
}
|
|
384
|
+
inFrame(measure);
|
|
399
385
|
}
|
package/src/behaviors/select.ts
CHANGED
|
@@ -38,12 +38,25 @@
|
|
|
38
38
|
* @docs extending/ui-kit
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* A row as the element draws it: the value the control reports back, and the label the reader sees.
|
|
43
|
+
* Only `value` and `label` are ever read here.
|
|
44
|
+
*/
|
|
45
|
+
export interface SelectRow {
|
|
43
46
|
value: string;
|
|
44
47
|
label: string;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** The reactive scope a `jx-select` document hands its sidecar. */
|
|
51
|
+
export interface SelectState {
|
|
52
|
+
/** The value the element holds; null when a host clears the reflected `value` attribute. */
|
|
53
|
+
value?: string | null;
|
|
54
|
+
/** The stand-in row for a value no listed row holds: empty, or exactly one row. */
|
|
55
|
+
unlisted?: SelectRow[];
|
|
56
|
+
/** This element's own id stem, minted at mount. */
|
|
57
|
+
uid?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
/** How many stems have been minted, so no two instances share one. */
|
|
48
61
|
let minted = 0;
|
|
49
62
|
|
|
@@ -89,17 +102,17 @@ function isListed(sel: HTMLSelectElement, want: string): boolean {
|
|
|
89
102
|
* @param state The element's reactive scope.
|
|
90
103
|
* @param sel The element's own `<select>`.
|
|
91
104
|
*/
|
|
92
|
-
export function syncSelect(state:
|
|
93
|
-
const want = String(state
|
|
94
|
-
const rows =
|
|
105
|
+
export function syncSelect(state: SelectState, sel: HTMLSelectElement): void {
|
|
106
|
+
const want = String(state.value ?? "");
|
|
107
|
+
const rows = state.unlisted ?? [];
|
|
95
108
|
/* The empty string is a legal value — the inherit row — rather than absence, so it is never
|
|
96
109
|
stood in for: a blank row labelled with a blank value would say nothing about what is held. */
|
|
97
110
|
if (want !== "" && !isListed(sel, want)) {
|
|
98
111
|
if (rows.length !== 1 || rows[0]?.value !== want) {
|
|
99
|
-
state
|
|
112
|
+
state.unlisted = [{ label: want, value: want }];
|
|
100
113
|
}
|
|
101
114
|
} else if (rows.length > 0) {
|
|
102
|
-
state
|
|
115
|
+
state.unlisted = [];
|
|
103
116
|
}
|
|
104
117
|
if ([...sel.options].some((option) => option.value === want)) {
|
|
105
118
|
if (sel.value !== want || sel.selectedIndex < 0) {
|
|
@@ -120,9 +133,9 @@ export function syncSelect(state: Record<string, unknown>, sel: HTMLSelectElemen
|
|
|
120
133
|
* @param state The element's reactive scope, whose `uid` and `unlisted` this writes.
|
|
121
134
|
* @param host The `jx-select` element.
|
|
122
135
|
*/
|
|
123
|
-
export function mountSelect(state:
|
|
136
|
+
export function mountSelect(state: SelectState, host: HTMLElement): void {
|
|
124
137
|
minted += 1;
|
|
125
|
-
state
|
|
138
|
+
state.uid = `jx-select-${minted}`;
|
|
126
139
|
const sel = controlOf(host);
|
|
127
140
|
if (!sel) {
|
|
128
141
|
return;
|
package/src/behaviors/split.ts
CHANGED
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
* @docs extending/ui-kit
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
+
import { isFunction, isNonNullObject } from "@jxsuite/schema/guards";
|
|
53
|
+
import type { JsonValue } from "@jxsuite/schema/types";
|
|
54
|
+
|
|
52
55
|
/** The tag this sidecar belongs to. */
|
|
53
56
|
const HOST = "jx-split";
|
|
54
57
|
|
|
@@ -79,7 +82,6 @@ export interface SplitState {
|
|
|
79
82
|
dragging: boolean;
|
|
80
83
|
/** The position the toggle returns to; negative until a collapse has recorded one. */
|
|
81
84
|
restore: number;
|
|
82
|
-
[key: string]: unknown;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
/**
|
|
@@ -101,14 +103,16 @@ const NO_MODIFIERS: Readonly<SplitModifiers> = Object.freeze({
|
|
|
101
103
|
shiftKey: false,
|
|
102
104
|
});
|
|
103
105
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The four modifier flags off a pointer or key event, each `true` only when it is exactly `true`,
|
|
108
|
+
* because a synthetic event a test or a driver builds may carry none of them.
|
|
109
|
+
*/
|
|
110
|
+
function modifiersOf(event: Partial<SplitModifiers>): SplitModifiers {
|
|
107
111
|
return {
|
|
108
|
-
altKey:
|
|
109
|
-
ctrlKey:
|
|
110
|
-
metaKey:
|
|
111
|
-
shiftKey:
|
|
112
|
+
altKey: event.altKey === true,
|
|
113
|
+
ctrlKey: event.ctrlKey === true,
|
|
114
|
+
metaKey: event.metaKey === true,
|
|
115
|
+
shiftKey: event.shiftKey === true,
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -123,20 +127,34 @@ function modifiersOf(event: object): SplitModifiers {
|
|
|
123
127
|
* @param event The `input` or `change`.
|
|
124
128
|
*/
|
|
125
129
|
export function splitModifiersOf(event: Event): SplitModifiers {
|
|
126
|
-
const
|
|
127
|
-
|
|
130
|
+
const detail = "detail" in event ? event.detail : undefined;
|
|
131
|
+
if (!isNonNullObject(detail)) {
|
|
132
|
+
return { ...NO_MODIFIERS };
|
|
133
|
+
}
|
|
134
|
+
/* A detail is whatever its dispatcher put there, so each flag is probed by name rather than
|
|
135
|
+
trusted to be a boolean: only an exact `true` counts, as it does off an event. */
|
|
136
|
+
return {
|
|
137
|
+
altKey: "altKey" in detail && detail.altKey === true,
|
|
138
|
+
ctrlKey: "ctrlKey" in detail && detail.ctrlKey === true,
|
|
139
|
+
metaKey: "metaKey" in detail && detail.metaKey === true,
|
|
140
|
+
shiftKey: "shiftKey" in detail && detail.shiftKey === true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The range the value may take, and the track length it was converted against. */
|
|
145
|
+
interface SplitBounds {
|
|
146
|
+
/** The track's length along the dragged axis, in pixels. */
|
|
147
|
+
length: number;
|
|
148
|
+
lo: number;
|
|
149
|
+
hi: number;
|
|
128
150
|
}
|
|
129
151
|
|
|
130
|
-
/** One live gesture: where it started, and the bounds it was measured against. */
|
|
131
|
-
interface Drag {
|
|
152
|
+
/** One live gesture: where it started, and the bounds it was measured against, once. */
|
|
153
|
+
interface Drag extends SplitBounds {
|
|
132
154
|
/** The pointer coordinate at `pointerdown`, along the dragged axis. */
|
|
133
155
|
origin: number;
|
|
134
156
|
/** The value at `pointerdown`. */
|
|
135
157
|
start: number;
|
|
136
|
-
/** The track's length along that axis, measured once. */
|
|
137
|
-
length: number;
|
|
138
|
-
lo: number;
|
|
139
|
-
hi: number;
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
/**
|
|
@@ -167,13 +185,13 @@ function announce(host: HTMLElement, modifiers: SplitModifiers, ...names: string
|
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
/** A number, or the fallback when the scope holds something that is not one. */
|
|
170
|
-
function finiteOr(value:
|
|
188
|
+
function finiteOr(value: JsonValue | undefined, fallback: number): number {
|
|
171
189
|
const n = Number(value);
|
|
172
190
|
return Number.isFinite(n) ? n : fallback;
|
|
173
191
|
}
|
|
174
192
|
|
|
175
193
|
/** A POSITIVE number, or the fallback. A step of zero is a key that does nothing. */
|
|
176
|
-
function positiveOr(value:
|
|
194
|
+
function positiveOr(value: JsonValue | undefined, fallback: number): number {
|
|
177
195
|
const n = Number(value);
|
|
178
196
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
179
197
|
}
|
|
@@ -262,10 +280,7 @@ export function trackOf(host: HTMLElement, vertical: boolean): number {
|
|
|
262
280
|
* @param state The element's reactive scope.
|
|
263
281
|
* @param host The element.
|
|
264
282
|
*/
|
|
265
|
-
function boundsOf(
|
|
266
|
-
state: SplitState,
|
|
267
|
-
host: HTMLElement,
|
|
268
|
-
): { lo: number; hi: number; length: number } {
|
|
283
|
+
function boundsOf(state: SplitState, host: HTMLElement): SplitBounds {
|
|
269
284
|
const length = trackOf(host, isVertical(state));
|
|
270
285
|
const min = finiteOr(state.min, 0);
|
|
271
286
|
const max = finiteOr(state.max, 1);
|
|
@@ -373,7 +388,7 @@ export function onSplitPointerDown(state: SplitState, event: PointerEvent): void
|
|
|
373
388
|
origin: coordOf(isVertical(state), event),
|
|
374
389
|
start: Number(state.value),
|
|
375
390
|
});
|
|
376
|
-
if (
|
|
391
|
+
if (isFunction(host.setPointerCapture)) {
|
|
377
392
|
/* Capture on the HOST, which is also the element the listener is on: a drag that runs off the
|
|
378
393
|
end of the splitter keeps arriving here instead of stopping at whatever it crossed. */
|
|
379
394
|
try {
|
|
@@ -436,8 +451,9 @@ export function onSplitPointerUp(state: SplitState, event: PointerEvent): void {
|
|
|
436
451
|
}
|
|
437
452
|
state.dragging = false;
|
|
438
453
|
if (
|
|
439
|
-
|
|
440
|
-
host.hasPointerCapture
|
|
454
|
+
isFunction(host.releasePointerCapture) &&
|
|
455
|
+
isFunction(host.hasPointerCapture) &&
|
|
456
|
+
host.hasPointerCapture(event.pointerId)
|
|
441
457
|
) {
|
|
442
458
|
host.releasePointerCapture(event.pointerId);
|
|
443
459
|
}
|
|
@@ -38,7 +38,6 @@ export interface SwatchGroupState {
|
|
|
38
38
|
value?: string;
|
|
39
39
|
/** The group's accessible name. */
|
|
40
40
|
label?: string;
|
|
41
|
-
[key: string]: unknown;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/** A swatch element with the property accessors its own document installs. */
|
|
@@ -67,9 +66,7 @@ const watched = new WeakMap<HTMLElement, MutationObserver>();
|
|
|
67
66
|
/** The group an event's `currentTarget` is. */
|
|
68
67
|
function hostOf(event: Event): GroupElement | null {
|
|
69
68
|
const target = event.currentTarget;
|
|
70
|
-
return target instanceof HTMLElement && target.localName === GROUP
|
|
71
|
-
? (target as GroupElement)
|
|
72
|
-
: null;
|
|
69
|
+
return target instanceof HTMLElement && target.localName === GROUP ? target : null;
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
/** Whether `group` is the nearest group around `node` — a key inside a nested group is its own. */
|
|
@@ -147,12 +144,12 @@ export function focusSwatch(group: HTMLElement, index: number): SwatchElement |
|
|
|
147
144
|
*
|
|
148
145
|
* @param group The `jx-swatch-group` element.
|
|
149
146
|
*/
|
|
150
|
-
export function syncSwatches(group:
|
|
147
|
+
export function syncSwatches(group: GroupElement): void {
|
|
151
148
|
const swatches = swatchesOf(group);
|
|
152
149
|
if (swatches.length === 0) {
|
|
153
150
|
return;
|
|
154
151
|
}
|
|
155
|
-
const value = String(
|
|
152
|
+
const value = String(group.value ?? "");
|
|
156
153
|
const enabled = swatches.filter((swatch) => !isDisabled(swatch));
|
|
157
154
|
const chosen = enabled.find((swatch) => valueOf(swatch) === value);
|
|
158
155
|
const caret = chosen ?? enabled[0] ?? null;
|
|
@@ -233,7 +230,7 @@ export function onSwatchGroupSelect(scope: SwatchGroupState, event: Event): void
|
|
|
233
230
|
return;
|
|
234
231
|
}
|
|
235
232
|
event.stopPropagation();
|
|
236
|
-
const value = String(
|
|
233
|
+
const value = "detail" in event ? String(event.detail ?? "") : "";
|
|
237
234
|
if (value === String(scope.value ?? "")) {
|
|
238
235
|
return;
|
|
239
236
|
}
|
package/src/behaviors/tabs.ts
CHANGED
|
@@ -33,7 +33,6 @@ export interface TabsState {
|
|
|
33
33
|
label?: string;
|
|
34
34
|
activation?: string;
|
|
35
35
|
orientation?: string;
|
|
36
|
-
[key: string]: unknown;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** A tab element with the property accessors its document installs. */
|
|
@@ -87,9 +86,7 @@ function watchTabs(host: HTMLElement): void {
|
|
|
87
86
|
/** The tablist an event's `currentTarget` is. */
|
|
88
87
|
function hostOf(event: Event): TabsElement | null {
|
|
89
88
|
const target = event.currentTarget;
|
|
90
|
-
return target instanceof HTMLElement && target.localName === TABS
|
|
91
|
-
? (target as TabsElement)
|
|
92
|
-
: null;
|
|
89
|
+
return target instanceof HTMLElement && target.localName === TABS ? target : null;
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
/** Whether `host` is the nearest tablist around `node` — a nested strip owns its own keys. */
|
|
@@ -146,12 +143,12 @@ export function focusTab(tabs: TabElement[], index: number): TabElement | null {
|
|
|
146
143
|
*
|
|
147
144
|
* @param host The `jx-tabs` element.
|
|
148
145
|
*/
|
|
149
|
-
export function syncTabs(host:
|
|
146
|
+
export function syncTabs(host: TabsElement): void {
|
|
150
147
|
const tabs = tabsOf(host);
|
|
151
148
|
if (tabs.length === 0) {
|
|
152
149
|
return;
|
|
153
150
|
}
|
|
154
|
-
const selected = String(
|
|
151
|
+
const selected = String(host.selected ?? "");
|
|
155
152
|
const found = tabs.findIndex((tab) => valueOf(tab) === selected);
|
|
156
153
|
const caret = found === -1 ? 0 : found;
|
|
157
154
|
for (const [at, tab] of tabs.entries()) {
|
|
@@ -228,7 +225,7 @@ export function onTabsSelect(scope: TabsState, event: Event): void {
|
|
|
228
225
|
return;
|
|
229
226
|
}
|
|
230
227
|
event.stopPropagation();
|
|
231
|
-
const value = String(
|
|
228
|
+
const value = "detail" in event ? String(event.detail ?? "") : "";
|
|
232
229
|
if (value === String(scope.selected ?? "")) {
|
|
233
230
|
return;
|
|
234
231
|
}
|
|
@@ -12,6 +12,23 @@
|
|
|
12
12
|
* @docs extending/ui-kit
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/** The reactive scope a `jx-textfield` document hands its sidecar. */
|
|
16
|
+
export interface TextfieldState {
|
|
17
|
+
/** The field's text. */
|
|
18
|
+
value?: string;
|
|
19
|
+
/** This element's own id stem, minted at mount. */
|
|
20
|
+
uid?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The reactive scope a `jx-field` row hands its sidecar. */
|
|
24
|
+
export interface FieldRowState {
|
|
25
|
+
/** This row's own id stem, minted at mount. */
|
|
26
|
+
uid?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A kit control a `jx-field` row names, through the properties its definition observes. */
|
|
30
|
+
type NameableControl = Element & { labelledby?: string; describedby?: string };
|
|
31
|
+
|
|
15
32
|
/** The element's own native control. */
|
|
16
33
|
function controlOf(host: HTMLElement): HTMLInputElement | HTMLTextAreaElement | null {
|
|
17
34
|
return host.querySelector<HTMLInputElement | HTMLTextAreaElement>('[part="input"]');
|
|
@@ -63,12 +80,12 @@ function hostOf(event: Event): HTMLElement | null {
|
|
|
63
80
|
* @param state The element's reactive scope, whose `value` this empties.
|
|
64
81
|
* @param event The click on the clear button.
|
|
65
82
|
*/
|
|
66
|
-
export function clearField(state:
|
|
83
|
+
export function clearField(state: TextfieldState, event: Event): void {
|
|
67
84
|
const host = hostOf(event);
|
|
68
85
|
if (!host) {
|
|
69
86
|
return;
|
|
70
87
|
}
|
|
71
|
-
state
|
|
88
|
+
state.value = "";
|
|
72
89
|
focusField(host);
|
|
73
90
|
host.dispatchEvent(new Event("input", { bubbles: true }));
|
|
74
91
|
host.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -85,19 +102,24 @@ let minted = 0;
|
|
|
85
102
|
*
|
|
86
103
|
* @param state The element's reactive scope, whose `uid` this writes.
|
|
87
104
|
*/
|
|
88
|
-
export function mintFieldId(state:
|
|
105
|
+
export function mintFieldId(state: TextfieldState): void {
|
|
89
106
|
minted += 1;
|
|
90
|
-
state
|
|
107
|
+
state.uid = `jx-tf-${minted}`;
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
let namedRows = 0;
|
|
94
111
|
|
|
95
112
|
/** Whether an element's DEFINITION says it can be named — that it observes `labelledby`. */
|
|
96
113
|
function observesNaming(el: Element): boolean {
|
|
97
|
-
const def = customElements.get(el.localName)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
114
|
+
const def = customElements.get(el.localName);
|
|
115
|
+
/* `observedAttributes` is a static member the platform reads off the constructor, which the DOM
|
|
116
|
+
types do not declare, so it is probed by name and shape. */
|
|
117
|
+
return (
|
|
118
|
+
def !== undefined &&
|
|
119
|
+
"observedAttributes" in def &&
|
|
120
|
+
Array.isArray(def.observedAttributes) &&
|
|
121
|
+
def.observedAttributes.includes("labelledby")
|
|
122
|
+
);
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
/**
|
|
@@ -141,24 +163,23 @@ function observesNaming(el: Element): boolean {
|
|
|
141
163
|
* @param state The row's reactive scope, whose `uid` this writes.
|
|
142
164
|
* @param host The `jx-field` element, whose slotted control this names.
|
|
143
165
|
*/
|
|
144
|
-
export function nameFieldControl(state:
|
|
166
|
+
export function nameFieldControl(state: FieldRowState, host: HTMLElement): void {
|
|
145
167
|
namedRows += 1;
|
|
146
168
|
const uid = `jx-field-${namedRows}`;
|
|
147
|
-
state
|
|
169
|
+
state.uid = uid;
|
|
148
170
|
|
|
149
171
|
/* The row's OWN parts, found among its children rather than by a descendant query. Both spellings
|
|
150
172
|
return the same node for a rendered row — the row's own parts precede everything slotted, so
|
|
151
173
|
nothing in the suite can tell them apart — but a `[part]` lookup that may descend is one
|
|
152
174
|
refactor away from reaching into a nested row's parts. */
|
|
153
175
|
const own = (name: string) => [...host.children].find((el) => el.getAttribute("part") === name);
|
|
154
|
-
const control = own("control")?.firstElementChild;
|
|
176
|
+
const control: NameableControl | null | undefined = own("control")?.firstElementChild;
|
|
155
177
|
if (!control || !observesNaming(control)) {
|
|
156
178
|
return;
|
|
157
179
|
}
|
|
158
|
-
|
|
159
|
-
target["labelledby"] ||= `${uid}-label`;
|
|
180
|
+
control.labelledby ||= `${uid}-label`;
|
|
160
181
|
const help = own("help");
|
|
161
182
|
if (help && (help.firstElementChild !== null || help.textContent !== "")) {
|
|
162
|
-
|
|
183
|
+
control.describedby ||= `${uid}-help`;
|
|
163
184
|
}
|
|
164
185
|
}
|
|
@@ -42,11 +42,13 @@ const FOCUSABLE =
|
|
|
42
42
|
|
|
43
43
|
/** The reactive scope a `jx-toast-host` document hands its handlers. */
|
|
44
44
|
export interface ToastHostState {
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The key that moves focus into the stack. Empty turns the gesture off, and so does null, which
|
|
47
|
+
* is what a host clearing the reflected `hotkey` attribute leaves.
|
|
48
|
+
*/
|
|
49
|
+
hotkey: string | null;
|
|
47
50
|
/** `polite`, `assertive` or `off`. */
|
|
48
51
|
live: string;
|
|
49
|
-
[key: string]: unknown;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** Where focus was when the reader entered the stack, so it can be given back. */
|
|
@@ -208,13 +210,14 @@ export function onStackClose(_state: ToastHostState, event: Event): void {
|
|
|
208
210
|
* @param host The element itself.
|
|
209
211
|
*/
|
|
210
212
|
export function onHostMount(state: ToastHostState, host: HTMLElement): void {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
/* The document, the shadow root, or — for a detached host — its topmost ancestor: every one of
|
|
214
|
+
them is a node a keydown from inside it reaches. */
|
|
215
|
+
const root = host.getRootNode();
|
|
216
|
+
const onKeydown = (press: Event): void => {
|
|
217
|
+
if (!(press instanceof KeyboardEvent) || press.altKey || press.ctrlKey || press.metaKey) {
|
|
215
218
|
return;
|
|
216
219
|
}
|
|
217
|
-
const wanted =
|
|
220
|
+
const wanted = state.hotkey ?? "";
|
|
218
221
|
if (wanted !== "" && press.key === wanted) {
|
|
219
222
|
if (focusStack(host)) {
|
|
220
223
|
press.preventDefault();
|