@threadlabs/looma 0.23.9 → 0.23.11
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/components/ui-combobox/ui-combobox.html +6 -0
- package/components/ui-combobox/ui-combobox.js +17 -1
- package/components/ui-disclosure/ui-disclosure.html +2 -4
- package/components/ui-tree/ui-tree.html +4 -0
- package/components/ui-tree/ui-tree.js +32 -6
- package/components/ui-tree-item/ui-tree-item.html +21 -8
- package/components/ui-tree-item/ui-tree-item.js +3 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-disclosure.css +0 -3
- package/styles/ui-tree-item.css +12 -4
- package/vanilla/UiCombobox.d.ts +2 -0
- package/vanilla/UiCombobox.js +1 -1
- package/vanilla/UiTree.d.ts +1 -0
- package/vanilla/UiTree.js +1 -1
- package/vanilla/UiTreeItem.js +25 -15
- package/vue/UiCombobox.d.ts +18 -0
- package/vue/UiCombobox.vue +39 -0
- package/vue/UiDisclosure.vue +1 -5
- package/vue/UiTree.d.ts +9 -0
- package/vue/UiTree.vue +31 -1
- package/vue/UiTreeItem.d.ts +3 -3
- package/vue/UiTreeItem.vue +55 -8
- package/vue/chunks/UiCombobox.js +46 -1
- package/vue/chunks/UiDisclosure.js +2 -2
- package/vue/chunks/UiTree.js +24 -1
- package/vue/chunks/UiTreeItem.js +39 -8
- package/vue/components.css +181 -178
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
>How the list narrows as the user types. label lists the options whose label contains the typed text, ignoring case. none lists every authored option, for
|
|
55
55
|
options already narrowed elsewhere, such as a server search, so a result that matched on data other than its label still shows. Leaving the field still
|
|
56
56
|
commits an option whose label matches the text, or the only one listed.</prop>
|
|
57
|
+
<prop name="autoHighlight" type="keyword" values="none, single" default="none"
|
|
58
|
+
>single highlights the only enabled authored suggestion while typing. Ambiguous results and an empty query stay unhighlighted. none preserves manual
|
|
59
|
+
highlighting with the arrow keys.</prop>
|
|
60
|
+
<prop name="selectOnTab" type="boolean" default="false"
|
|
61
|
+
>Commits a highlighted authored suggestion on forward Tab, then lets focus advance normally. In multiple mode a selected suggestion is retained without
|
|
62
|
+
duplication or removal. Shift+Tab does not commit, and Tab never chooses the create offer.</prop>
|
|
57
63
|
<prop name="allowFreeText" type="boolean" default="false">Allows values that do not match an authored option.</prop>
|
|
58
64
|
<prop name="allowCreate" type="boolean" default="false"
|
|
59
65
|
>Offers the current query as a new option, reported as create-item. When nothing else matches, the offer is highlighted, so Enter creates it; with nothing
|
|
@@ -174,6 +174,10 @@ export default function controller(host) {
|
|
|
174
174
|
// When nothing matches what was typed, the offer to create it is the only choice, so it is
|
|
175
175
|
// the one Enter makes, and it is highlighted as such.
|
|
176
176
|
if (query && !host.state.rows.some((row) => !row.disabled) && canCreate()) host.state.active = host.state.rows.length;
|
|
177
|
+
if (query && host.props.autoHighlight.value === "single") {
|
|
178
|
+
const enabled = host.state.rows.flatMap((row, index) => row.disabled ? [] : [index]);
|
|
179
|
+
if (enabled.length === 1) host.state.active = enabled[0];
|
|
180
|
+
}
|
|
177
181
|
// Options in their declared shape: a row's `selected` flag is the list's own view state.
|
|
178
182
|
host.dispatch("options-change", host.state.rows.map(asItem));
|
|
179
183
|
};
|
|
@@ -441,7 +445,19 @@ export default function controller(host) {
|
|
|
441
445
|
if (!host.state.raw && (input.selectionStart ?? 0) === 0 && event.key === "Backspace" && items().length) { event.preventDefault(); removeItemAt(items().length - 1, "keyboard"); return; }
|
|
442
446
|
}
|
|
443
447
|
if (event.key === "Escape" && host.state.expanded) { event.preventDefault(); event.stopPropagation(); close(); }
|
|
444
|
-
else if (event.key === "Tab") {
|
|
448
|
+
else if (event.key === "Tab") {
|
|
449
|
+
const highlighted = host.state.expanded ? (host.state.rows ?? [])[host.state.active] : undefined;
|
|
450
|
+
if (host.props.selectOnTab.value && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && highlighted && !highlighted.disabled) {
|
|
451
|
+
if (host.props.multiple.value) {
|
|
452
|
+
setMultiQuery("", "keyboard");
|
|
453
|
+
if (!selectedSet().has(highlighted.value)) addSelectedItem(highlighted, "keyboard");
|
|
454
|
+
} else {
|
|
455
|
+
commit(highlighted.value, highlighted.label, asItem(highlighted), "selection", "keyboard");
|
|
456
|
+
queueMicrotask(() => void validateCurrent());
|
|
457
|
+
}
|
|
458
|
+
} else if (!host.props.selectOnTab.value) resolveTyped("keyboard");
|
|
459
|
+
close();
|
|
460
|
+
}
|
|
445
461
|
else if (!event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && ["ArrowDown", "ArrowUp"].includes(event.key)) { event.preventDefault(); if (!host.state.expanded) open("disclosure"); move(event.key); }
|
|
446
462
|
else if (["Home", "End"].includes(event.key) && host.state.expanded && host.state.active >= 0) { event.preventDefault(); move(event.key); }
|
|
447
463
|
else if (event.key === "Enter" && host.state.expanded) {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
<prop name="density" type="keyword" values="comfortable, compact" default="comfortable"
|
|
4
4
|
>Compact trades padding and type size for rows that fit more on screen.</prop>
|
|
5
5
|
<prop name="variant" type="keyword" values="plain, navigation" default="plain"
|
|
6
|
-
>Navigation shares icon columns, spacing, type, hover, and corners with Nav Item
|
|
6
|
+
>Navigation shares icon columns, spacing, type, hover, and corners with Nav Item, while keeping expanded sections neutral: opening content is not route
|
|
7
|
+
selection. Plain retains the standard disclosure treatment.</prop>
|
|
7
8
|
<prop name="requiredOpen" type="boolean" default="false"
|
|
8
9
|
>In a named exclusive group, keeps at least one member open. Clicking the last open trigger does not close it; opening another member transfers the open
|
|
9
10
|
state.</prop>
|
|
@@ -227,9 +228,6 @@
|
|
|
227
228
|
background: var(--ui-nav-hover-surface);
|
|
228
229
|
color: var(--ui-text-primary);
|
|
229
230
|
}
|
|
230
|
-
:host-state([variant="navigation"][internalOpen]) .trigger {
|
|
231
|
-
color: var(--ui-selection-text);
|
|
232
|
-
}
|
|
233
231
|
:host-state([variant="navigation"]) :is(.leading, .indicator) {
|
|
234
232
|
inline-size: var(--ui-nav-icon-size);
|
|
235
233
|
font-size: var(--ui-nav-icon-size);
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
<prop name="marquee" type="boolean" default="false"
|
|
18
18
|
>Scrolls a name too long for its row while that row is hovered or keyboard focused, rests at the end, then scrolls it again. Off by default: a tree of names
|
|
19
19
|
that fit should not move. An item may set its own marquee to opt in or out on its own.</prop>
|
|
20
|
+
<prop name="moveActivation" type="keyword" values="click, keyboard-touch" default="click"
|
|
21
|
+
>Which handle activations start guided moving. click preserves two-click moving with any pointer. keyboard-touch leaves ordinary mouse and pen clicks inert
|
|
22
|
+
while retaining keyboard activation and touch taps. Actual pointer dragging works in either mode. Guided moves show Cancel on the source row; Escape or
|
|
23
|
+
reactivating that source handle also cancels without requesting a reorder.</prop>
|
|
20
24
|
<event
|
|
21
25
|
name="reorder"
|
|
22
26
|
type="object"
|
|
@@ -32,6 +32,7 @@ export default function controller(host) {
|
|
|
32
32
|
let typed = "";
|
|
33
33
|
let lastTyped = 0;
|
|
34
34
|
let moveMode = false;
|
|
35
|
+
let handlePointerType = null;
|
|
35
36
|
const announcer = document.createElement("span");
|
|
36
37
|
announcer.setAttribute("role", "status");
|
|
37
38
|
announcer.style.cssText = "position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap";
|
|
@@ -103,6 +104,8 @@ export default function controller(host) {
|
|
|
103
104
|
moveMode = false;
|
|
104
105
|
element.removeAttribute("data-move-mode");
|
|
105
106
|
source?.removeAttribute("data-dragging");
|
|
107
|
+
source?.removeAttribute("data-move-source");
|
|
108
|
+
source?.dispatchEvent(new CustomEvent("ui-tree-move-state", { detail: { moving: false } }));
|
|
106
109
|
source = null;
|
|
107
110
|
clearTarget();
|
|
108
111
|
rejection = null;
|
|
@@ -165,8 +168,16 @@ export default function controller(host) {
|
|
|
165
168
|
moveMode = true;
|
|
166
169
|
source = item;
|
|
167
170
|
source.setAttribute("data-dragging", "true");
|
|
171
|
+
source.setAttribute("data-move-source", "");
|
|
172
|
+
source.dispatchEvent(new CustomEvent("ui-tree-move-state", { detail: { moving: true } }));
|
|
168
173
|
element.setAttribute("data-move-mode", "");
|
|
169
|
-
announce(`Moving ${nameOf(item)}. Choose a destination by click or the arrow keys. Press Enter to place or Escape to cancel.`);
|
|
174
|
+
announce(`Moving ${nameOf(item)}. Choose a destination by click or the arrow keys. Press Enter to place, or use Cancel or Escape to cancel.`);
|
|
175
|
+
};
|
|
176
|
+
const cancelMove = () => {
|
|
177
|
+
const handle = source && rowFor(source)?.querySelector(".drag-handle");
|
|
178
|
+
finish();
|
|
179
|
+
handle?.focus();
|
|
180
|
+
announce("Move cancelled.");
|
|
170
181
|
};
|
|
171
182
|
const commitMove = (how) => {
|
|
172
183
|
if (!source) return;
|
|
@@ -182,12 +193,21 @@ export default function controller(host) {
|
|
|
182
193
|
} else announce("Choose another destination row.");
|
|
183
194
|
};
|
|
184
195
|
const onMoveClick = (event) => {
|
|
196
|
+
if (moveMode && event.composedPath().some((node) => node instanceof HTMLElement && node.hasAttribute("data-tree-cancel-move"))) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
event.stopPropagation();
|
|
199
|
+
cancelMove();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
185
202
|
const handle = event.composedPath().find((node) => node instanceof HTMLElement && node.classList.contains("drag-handle"));
|
|
186
203
|
if (handle) {
|
|
187
204
|
event.preventDefault();
|
|
188
205
|
event.stopPropagation();
|
|
189
206
|
const item = itemFromEvent(event);
|
|
190
|
-
if (item)
|
|
207
|
+
if (moveMode && item === source) cancelMove();
|
|
208
|
+
else if (item && (host.props.moveActivation.value !== "keyboard-touch" || event.detail === 0
|
|
209
|
+
|| (event.pointerType ?? handlePointerType) === "touch")) beginMove(item);
|
|
210
|
+
handlePointerType = null;
|
|
191
211
|
return;
|
|
192
212
|
}
|
|
193
213
|
if (!moveMode) {
|
|
@@ -214,6 +234,7 @@ export default function controller(host) {
|
|
|
214
234
|
else if (rejection) commitMove("pointer");
|
|
215
235
|
else announce("Choose another destination row.");
|
|
216
236
|
};
|
|
237
|
+
const onHandlePointerDown = (event) => { handlePointerType = event.pointerType; };
|
|
217
238
|
const onMovePointer = (event) => {
|
|
218
239
|
if (!moveMode) return;
|
|
219
240
|
const item = itemFromEvent(event);
|
|
@@ -223,13 +244,15 @@ export default function controller(host) {
|
|
|
223
244
|
if (!moveMode) return false;
|
|
224
245
|
if (event.key === "Escape") {
|
|
225
246
|
event.preventDefault();
|
|
226
|
-
|
|
227
|
-
announce("Move cancelled.");
|
|
247
|
+
cancelMove();
|
|
228
248
|
return true;
|
|
229
249
|
}
|
|
230
250
|
if (event.key === "Enter" || event.key === " ") {
|
|
231
251
|
event.preventDefault();
|
|
232
|
-
|
|
252
|
+
const cancelling = event.composedPath().some((node) => node instanceof HTMLElement
|
|
253
|
+
&& (node.hasAttribute("data-tree-cancel-move") || (node.classList.contains("drag-handle") && itemFromEvent(event) === source)));
|
|
254
|
+
if (cancelling) cancelMove();
|
|
255
|
+
else commitMove("keyboard");
|
|
233
256
|
return true;
|
|
234
257
|
}
|
|
235
258
|
if (event.key === "ArrowRight" && target) {
|
|
@@ -254,6 +277,7 @@ export default function controller(host) {
|
|
|
254
277
|
const candidate = items[index];
|
|
255
278
|
const side = step < 0 ? "before" : "after";
|
|
256
279
|
if (candidate !== source && chooseTarget(candidate, side)) {
|
|
280
|
+
focusItem(candidate);
|
|
257
281
|
announce(`Move ${nameOf(source)} ${side} ${nameOf(candidate)}. Press Enter to place.`);
|
|
258
282
|
break;
|
|
259
283
|
}
|
|
@@ -364,6 +388,7 @@ export default function controller(host) {
|
|
|
364
388
|
host.dispatch("select", { ids: items.map(itemId).filter((candidate) => candidate && selected.has(candidate)), trigger: how });
|
|
365
389
|
};
|
|
366
390
|
const syncStructure = () => {
|
|
391
|
+
if (source && !element.contains(source)) finish();
|
|
367
392
|
for (const item of allItems()) item.dispatchEvent(new CustomEvent("ui-tree-structure-sync"));
|
|
368
393
|
syncTabStop();
|
|
369
394
|
syncSelection();
|
|
@@ -416,7 +441,7 @@ export default function controller(host) {
|
|
|
416
441
|
if (match) { event.preventDefault(); focusItem(match); }
|
|
417
442
|
}
|
|
418
443
|
};
|
|
419
|
-
const listeners = { dragstart: onDragStart, drag: onDrag, dragenter: onDragOver, dragover: onDragOver, dragleave: onDragLeave, drop: onDrop, dragend: onDragEnd, keydown: onKeydown, focusin: onFocusin, pointermove: onMovePointer, "ui-tree-expansion-change": onExpansion };
|
|
444
|
+
const listeners = { dragstart: onDragStart, drag: onDrag, dragenter: onDragOver, dragover: onDragOver, dragleave: onDragLeave, drop: onDrop, dragend: onDragEnd, keydown: onKeydown, focusin: onFocusin, pointerdown: onHandlePointerDown, pointermove: onMovePointer, "ui-tree-expansion-change": onExpansion };
|
|
420
445
|
for (const [name, listener] of Object.entries(listeners)) element.addEventListener(name, listener);
|
|
421
446
|
element.addEventListener("click", onMoveClick, true);
|
|
422
447
|
const observer = new MutationObserver((records) => {
|
|
@@ -430,6 +455,7 @@ export default function controller(host) {
|
|
|
430
455
|
});
|
|
431
456
|
syncStructure();
|
|
432
457
|
return () => {
|
|
458
|
+
finish();
|
|
433
459
|
stop();
|
|
434
460
|
observer.disconnect();
|
|
435
461
|
cancelHover();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<link rel="component" href="../ui-button/ui-button.html">
|
|
1
2
|
<template component="ui-tree-item" controller="./ui-tree-item.js">
|
|
2
3
|
<defs>
|
|
3
4
|
<prop name="itemId" type="string" default=""
|
|
@@ -17,8 +18,8 @@
|
|
|
17
18
|
>Keeps an empty item expandable while its children are supplied later. Expanding emits expand; the item is aria-busy until child Tree Items are appended.
|
|
18
19
|
Clear lazy if loading finishes with no children.</prop>
|
|
19
20
|
<prop name="sortable" type="boolean" default="false"
|
|
20
|
-
>Gives the item a move handle. Drag it, or activate it and choose a target by a second click or the keyboard.
|
|
21
|
-
item to you. Needs itemId; disabled items show no handle.</prop>
|
|
21
|
+
>Gives the item a move handle. Drag it, or activate it according to the tree's moveActivation and choose a target by a second click or the keyboard. A
|
|
22
|
+
guided move exposes Cancel on this row. The tree reports reorder and leaves moving the item to you. Needs itemId; disabled items show no handle.</prop>
|
|
22
23
|
<prop name="dragType" type="string" default="item"
|
|
23
24
|
>The item's kind, for drag rules: it can be dropped before or after only items of the same type, and inside a branch only if that branch's accepts allows
|
|
24
25
|
it. Reported as sourceType and targetType.</prop>
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
<state type="integer" name="structuralLevel" value="1"></state>
|
|
45
46
|
<state type="boolean" name="tabStop" value="false"></state>
|
|
46
47
|
<state type="boolean" name="isContainer" value="false"></state>
|
|
48
|
+
<state type="boolean" name="moving" value="false"></state>
|
|
47
49
|
<event name="expand" bubbles="false" type="object">
|
|
48
50
|
<prop name="id" required type="string"></prop>
|
|
49
51
|
<prop name="expanded" required type="boolean"></prop>
|
|
@@ -70,7 +72,7 @@
|
|
|
70
72
|
$ref="dragHandle"
|
|
71
73
|
type="button"
|
|
72
74
|
draggable="true"
|
|
73
|
-
from:aria-label="format('Drag %s to reorder', label)"
|
|
75
|
+
from:aria-label="{ true: format('Cancel moving %s', label), false: format('Drag %s to reorder', label) }[format('%s', moving)]"
|
|
74
76
|
>
|
|
75
77
|
<svg
|
|
76
78
|
aria-hidden="true"
|
|
@@ -103,7 +105,10 @@
|
|
|
103
105
|
></label>
|
|
104
106
|
<span class="leading" $ref="leading"><slot name="leading"></slot></span>
|
|
105
107
|
<span class="label" $ref="label"><span class="label-text" $ref="labelText"><slot name="label"><template $value="label"></template></slot></span></span>
|
|
106
|
-
<span class="actions" $ref="actions"
|
|
108
|
+
<span class="actions" $ref="actions">
|
|
109
|
+
<ui-button $if="moving" data-tree-cancel-move variant="ghost" tone="neutral" size="sm" aria-label="Cancel move">Cancel</ui-button>
|
|
110
|
+
<span $else><slot name="actions"></slot></span>
|
|
111
|
+
</span>
|
|
107
112
|
<span class="drop-indicator row-drop-indicator" aria-hidden="true"></span>
|
|
108
113
|
</div>
|
|
109
114
|
<div class="children" $ref="children" role="group" from:hidden="not (isContainer and internalExpanded)"><slot></slot></div>
|
|
@@ -187,7 +192,12 @@
|
|
|
187
192
|
background: var(--ui-disabled-surface);
|
|
188
193
|
}
|
|
189
194
|
|
|
190
|
-
:host[data-dragging] .row {
|
|
195
|
+
:host[data-dragging]:not([data-move-source]) .row {
|
|
196
|
+
opacity: 0.42;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
:host[data-move-source] > .row > .label,
|
|
200
|
+
:host[data-move-source] > .row > .leading {
|
|
191
201
|
opacity: 0.42;
|
|
192
202
|
}
|
|
193
203
|
|
|
@@ -360,7 +370,8 @@
|
|
|
360
370
|
/* The controls sit over the label's end, so the text fades out where they begin. The fade is
|
|
361
371
|
the truncation mark while it shows: an ellipsis under it would read as a second one. */
|
|
362
372
|
.row:hover > .label,
|
|
363
|
-
.row:focus-within > .label
|
|
373
|
+
.row:focus-within > .label,
|
|
374
|
+
:host[data-move-source] > .row > .label {
|
|
364
375
|
text-overflow: clip;
|
|
365
376
|
mask-image: linear-gradient(
|
|
366
377
|
to right,
|
|
@@ -370,7 +381,8 @@
|
|
|
370
381
|
}
|
|
371
382
|
|
|
372
383
|
.row:hover > .label:dir(rtl),
|
|
373
|
-
.row:focus-within > .label:dir(rtl)
|
|
384
|
+
.row:focus-within > .label:dir(rtl),
|
|
385
|
+
:host[data-move-source] > .row > .label:dir(rtl) {
|
|
374
386
|
mask-image: linear-gradient(
|
|
375
387
|
to left,
|
|
376
388
|
#000 calc(100% - var(--_tree-actions-width, 1.75rem) - var(--ui-space-1)),
|
|
@@ -451,7 +463,8 @@
|
|
|
451
463
|
}
|
|
452
464
|
|
|
453
465
|
.row:hover > .actions,
|
|
454
|
-
.row:focus-within > .actions
|
|
466
|
+
.row:focus-within > .actions,
|
|
467
|
+
:host[data-move-source] > .row > .actions {
|
|
455
468
|
opacity: 1;
|
|
456
469
|
pointer-events: auto;
|
|
457
470
|
}
|
|
@@ -90,7 +90,9 @@ export default function controller(host) {
|
|
|
90
90
|
};
|
|
91
91
|
const onStructure = () => { updateLevel(); apply(); };
|
|
92
92
|
const onAutoExpand = () => setExpanded(true, "pointer");
|
|
93
|
+
const onMoveState = (event) => { host.state.moving = Boolean(event.detail?.moving); };
|
|
93
94
|
|
|
95
|
+
element.addEventListener("ui-tree-move-state", onMoveState);
|
|
94
96
|
element.addEventListener("ui-tree-auto-expand", onAutoExpand);
|
|
95
97
|
element.addEventListener("ui-tree-structure-sync", onStructure);
|
|
96
98
|
element.addEventListener("ui-tree-roving-tab-stop", onRoving);
|
|
@@ -219,6 +221,7 @@ export default function controller(host) {
|
|
|
219
221
|
element.removeEventListener("focusout", onFocusOut);
|
|
220
222
|
labelText?.removeEventListener("animationend", onMarqueeEnd);
|
|
221
223
|
element.removeEventListener("ui-tree-auto-expand", onAutoExpand);
|
|
224
|
+
element.removeEventListener("ui-tree-move-state", onMoveState);
|
|
222
225
|
element.removeEventListener("ui-tree-structure-sync", onStructure);
|
|
223
226
|
element.removeEventListener("ui-tree-roving-tab-stop", onRoving);
|
|
224
227
|
element.removeEventListener("ui-tree-request-expanded", onExpansionRequest);
|