@threadlabs/looma 0.15.5 → 0.15.7
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-tree/ui-tree.html +2 -2
- package/components/ui-tree-item/ui-tree-item.html +11 -10
- package/components/ui-tree-item/ui-tree-item.js +54 -9
- package/dist/index.js +1 -1
- package/editor/chunks/extensions.js +8 -2
- package/editor/extensions/preset.d.ts +7 -0
- package/package.json +1 -1
- package/styles/ui-tree-item.css +12 -12
- package/vue/UiTreeItem.vue +8 -8
- package/vue/chunks/UiTreeItem.js +1 -1
- package/vue/components.css +88 -88
- package/vue/editor/LoomaEditor.d.ts +17 -0
- package/vue/editor/index.js +12 -2
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
>The deepest level a drag may produce, counting the top level as 1. A drop that would place the dragged item, or any item beneath it, deeper than this is
|
|
14
14
|
refused and reported as reorder-rejected. 0 means no limit.</prop>
|
|
15
15
|
<prop name="marquee" type="boolean" default="false"
|
|
16
|
-
>Scrolls a name too long for its row while that row is hovered or focused, then
|
|
17
|
-
item may set its own marquee to opt in or out on its own.</prop>
|
|
16
|
+
>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
|
|
17
|
+
that fit should not move. An item may set its own marquee to opt in or out on its own.</prop>
|
|
18
18
|
<event
|
|
19
19
|
name="reorder"
|
|
20
20
|
type="object({ sourceId: string, targetId: string, position: before | inside | after, sourceType: string, targetType: string, sourceScope: string, targetScope: string, trigger: keyboard | pointer | programmatic })"
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
>Marks the item selected: it sets aria-selected and highlights the row. This is controlled state; respond to the parent Tree's select request by updating
|
|
36
36
|
it.</prop>
|
|
37
37
|
<prop name="marquee" type="boolean" default="false"
|
|
38
|
-
>Scrolls a name too long for its row while the row
|
|
39
|
-
product decides whether reading the whole name matters more than a
|
|
38
|
+
>Scrolls a name too long for its row while a pointer hovers the row or keyboard focus is in it, rests at the end, then scrolls it again from the start. A
|
|
39
|
+
touch does not start it. Off by default: a tree of short names should not move, and a product decides whether reading the whole name matters more than a
|
|
40
|
+
still list.</prop>
|
|
40
41
|
<prop name="disabled" type="boolean" default="false"
|
|
41
42
|
>Makes the item unavailable: faded, skipped by keyboard navigation, unable to expand or be dragged, and closed to drops inside it.</prop>
|
|
42
43
|
<state name="internalExpanded" :value="false"></state>
|
|
@@ -364,27 +365,27 @@
|
|
|
364
365
|
);
|
|
365
366
|
}
|
|
366
367
|
|
|
367
|
-
/* A hovered row reads its whole name: the label slides far enough to show the end,
|
|
368
|
-
there, and
|
|
368
|
+
/* A hovered row reads its whole name: the label slides far enough to show the end, rests
|
|
369
|
+
there, and starts again. Only when it actually overflows — the controller measures by how
|
|
369
370
|
much — after the controls have taken their place over its end — and hands over the distance
|
|
370
371
|
and a duration from it, so every name moves at the same speed. */
|
|
371
372
|
.row[data-ui-marquee] > .label > .label-text {
|
|
372
|
-
/* Once
|
|
373
|
-
|
|
373
|
+
/* Once per pass: the controller restarts it after a rest at the end, so the name holds still
|
|
374
|
+
long enough to read rather than sliding back and forth. */
|
|
374
375
|
animation: ui-tree-item-marquee var(--_marquee-duration) linear 0.5s 1 normal forwards;
|
|
375
376
|
will-change: transform;
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
/* This comes after the hover fade, which it replaces at the same specificity. While the name
|
|
379
380
|
slides, the label reaches back under the icon (the controller measures how far) with its text
|
|
380
|
-
where it was, and fades out across
|
|
381
|
-
under the icon rather than stopping at a hard edge beside it. */
|
|
381
|
+
where it was, and fades out across the gap before the icon, fully gone where the icon begins
|
|
382
|
+
to cover it: the name slips under the icon rather than stopping at a hard edge beside it. */
|
|
382
383
|
.row[data-ui-marquee] > .label {
|
|
383
384
|
margin-inline-start: calc(-1 * var(--_marquee-lead, 0px));
|
|
384
385
|
padding-inline-start: calc(var(--_marquee-lead, 0px) + var(--_ui-tree-item-label-padding-inline, var(--ui-space-2)));
|
|
385
386
|
mask-image: linear-gradient(
|
|
386
387
|
to right,
|
|
387
|
-
transparent
|
|
388
|
+
transparent var(--_marquee-icon-width, 0px),
|
|
388
389
|
#000 calc(var(--_marquee-lead, 0px) + var(--_ui-tree-item-label-padding-inline, var(--ui-space-2))),
|
|
389
390
|
#000 calc(100% - var(--_tree-actions-width, 1.75rem) - var(--ui-space-3)),
|
|
390
391
|
transparent calc(100% - var(--_tree-actions-width, 1.75rem))
|
|
@@ -394,7 +395,7 @@
|
|
|
394
395
|
.row[data-ui-marquee] > .label:dir(rtl) {
|
|
395
396
|
mask-image: linear-gradient(
|
|
396
397
|
to left,
|
|
397
|
-
transparent
|
|
398
|
+
transparent var(--_marquee-icon-width, 0px),
|
|
398
399
|
#000 calc(var(--_marquee-lead, 0px) + var(--_ui-tree-item-label-padding-inline, var(--ui-space-2))),
|
|
399
400
|
#000 calc(100% - var(--_tree-actions-width, 1.75rem) - var(--ui-space-3)),
|
|
400
401
|
transparent calc(100% - var(--_tree-actions-width, 1.75rem))
|
|
@@ -105,6 +105,10 @@ export default function controller(host) {
|
|
|
105
105
|
* overlay the label's end rather than taking width from it.
|
|
106
106
|
*/
|
|
107
107
|
const MARQUEE_SPEED = 36; // CSS pixels per second, for every name: a clamped duration made short slides crawl.
|
|
108
|
+
const MARQUEE_REST = 1800; // Milliseconds the name holds at its end before the next pass.
|
|
109
|
+
let marqueeRestart;
|
|
110
|
+
let pointerHover = false;
|
|
111
|
+
let keyboardFocus = false;
|
|
108
112
|
// The tree sets this for its items; an item's own prop overrides it either way.
|
|
109
113
|
const marqueeWanted = () => host.state.marquee
|
|
110
114
|
|| getComputedStyle(element).getPropertyValue("--_ui-default-tree-item-marquee").trim() === "1";
|
|
@@ -122,21 +126,60 @@ export default function controller(host) {
|
|
|
122
126
|
if (distance <= 0) return;
|
|
123
127
|
const lead = icon?.width ? (rightToLeft ? icon.right - cell.right : cell.left - icon.left) : 0;
|
|
124
128
|
row.style.setProperty("--_marquee-lead", `${Math.max(0, lead)}px`);
|
|
129
|
+
row.style.setProperty("--_marquee-icon-width", `${icon?.width ?? 0}px`);
|
|
125
130
|
row.style.setProperty("--_marquee-distance", `${rightToLeft ? distance : -distance}px`);
|
|
126
131
|
row.style.setProperty("--_marquee-duration", `${(distance / MARQUEE_SPEED).toFixed(2)}s`);
|
|
127
132
|
row.dataset.uiMarquee = "";
|
|
128
133
|
};
|
|
129
134
|
const stopMarquee = () => {
|
|
135
|
+
clearTimeout(marqueeRestart);
|
|
130
136
|
delete row.dataset.uiMarquee;
|
|
131
137
|
row.style.removeProperty("--_marquee-lead");
|
|
138
|
+
row.style.removeProperty("--_marquee-icon-width");
|
|
132
139
|
row.style.removeProperty("--_marquee-distance");
|
|
133
140
|
row.style.removeProperty("--_marquee-duration");
|
|
134
141
|
};
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
// A hovering pointer and keyboard focus both count: a keyboard walk through a tree reads the same
|
|
143
|
+
// names. A touch does not, so a swipe down the list, or the focus a tap leaves, never starts it.
|
|
144
|
+
const onPointerEnter = (event) => {
|
|
145
|
+
if (event.pointerType === "touch") return;
|
|
146
|
+
pointerHover = true;
|
|
147
|
+
startMarquee();
|
|
148
|
+
};
|
|
149
|
+
const onPointerLeave = (event) => {
|
|
150
|
+
if (event.pointerType === "touch") return;
|
|
151
|
+
pointerHover = false;
|
|
152
|
+
if (!keyboardFocus) stopMarquee();
|
|
153
|
+
};
|
|
154
|
+
// Keyboard focus lands on the item itself as the tree roves, or on a control in its row; a nested
|
|
155
|
+
// item's focus is that item's own.
|
|
156
|
+
const ownFocus = (event) => event.target === element || row.contains(event.target);
|
|
157
|
+
const onFocusIn = (event) => {
|
|
158
|
+
if (!ownFocus(event) || !event.target.matches?.(":focus-visible")) return;
|
|
159
|
+
keyboardFocus = true;
|
|
160
|
+
startMarquee();
|
|
161
|
+
};
|
|
162
|
+
const onFocusOut = (event) => {
|
|
163
|
+
if (!ownFocus(event)) return;
|
|
164
|
+
keyboardFocus = false;
|
|
165
|
+
if (!pointerHover) stopMarquee();
|
|
166
|
+
};
|
|
167
|
+
// After a rest at the end, the name goes again from the start for as long as it is being read.
|
|
168
|
+
const onMarqueeEnd = (event) => {
|
|
169
|
+
if (event.target !== labelText || !("uiMarquee" in row.dataset)) return;
|
|
170
|
+
clearTimeout(marqueeRestart);
|
|
171
|
+
marqueeRestart = setTimeout(() => {
|
|
172
|
+
if (!pointerHover && !keyboardFocus) return;
|
|
173
|
+
delete row.dataset.uiMarquee;
|
|
174
|
+
void labelText.offsetWidth; // Flushes the finished animation, so the next one starts over.
|
|
175
|
+
row.dataset.uiMarquee = "";
|
|
176
|
+
}, MARQUEE_REST);
|
|
177
|
+
};
|
|
178
|
+
row.addEventListener("pointerenter", onPointerEnter);
|
|
179
|
+
row.addEventListener("pointerleave", onPointerLeave);
|
|
180
|
+
element.addEventListener("focusin", onFocusIn);
|
|
181
|
+
element.addEventListener("focusout", onFocusOut);
|
|
182
|
+
labelText?.addEventListener("animationend", onMarqueeEnd);
|
|
140
183
|
|
|
141
184
|
// The label's fade has to end where the controls begin, and only the controls know their width.
|
|
142
185
|
const actionsSize = new ResizeObserver(([entry]) => {
|
|
@@ -146,13 +189,15 @@ export default function controller(host) {
|
|
|
146
189
|
if (actions) actionsSize.observe(actions);
|
|
147
190
|
|
|
148
191
|
return () => {
|
|
192
|
+
clearTimeout(marqueeRestart);
|
|
149
193
|
stop?.();
|
|
150
194
|
observer.disconnect();
|
|
151
195
|
actionsSize.disconnect();
|
|
152
|
-
row.removeEventListener("pointerenter",
|
|
153
|
-
row.removeEventListener("pointerleave",
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
row.removeEventListener("pointerenter", onPointerEnter);
|
|
197
|
+
row.removeEventListener("pointerleave", onPointerLeave);
|
|
198
|
+
element.removeEventListener("focusin", onFocusIn);
|
|
199
|
+
element.removeEventListener("focusout", onFocusOut);
|
|
200
|
+
labelText?.removeEventListener("animationend", onMarqueeEnd);
|
|
156
201
|
element.removeEventListener("ui-tree-auto-expand", onAutoExpand);
|
|
157
202
|
element.removeEventListener("ui-tree-structure-sync", onStructure);
|
|
158
203
|
element.removeEventListener("ui-tree-roving-tab-stop", onRoving);
|