@threadlabs/looma 0.26.26 → 0.27.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/components/shared/editor.js +4 -5
- package/components/shared/overlay.js +231 -55
- package/components/ui-affordance-scope/ui-affordance-scope.js +4 -8
- package/components/ui-card/ui-card.html +3 -0
- package/components/ui-dialog/ui-dialog.html +7 -5
- package/components/ui-dialog/ui-dialog.js +67 -7
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +6 -0
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +5 -9
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +6 -0
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +5 -9
- package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +21 -4
- package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.js +50 -22
- package/components/ui-editor-table-overlay/ui-editor-table-overlay.js +4 -6
- package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +3 -11
- package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.js +33 -44
- package/components/ui-listbox/ui-listbox.html +22 -17
- package/components/ui-menu/ui-menu.js +5 -3
- package/components/ui-popover/ui-popover.js +3 -2
- package/components/ui-scroll-area/ui-scroll-area.html +16 -0
- package/components/ui-search-shell/ui-search-shell.html +1 -1
- package/components/ui-search-shell/ui-search-shell.js +3 -5
- package/components/ui-section/ui-section.html +3 -0
- package/components/ui-toast/ui-toast.html +2 -1
- package/components/ui-toast/ui-toast.js +18 -2
- package/components/ui-toast-region/ui-toast-region.js +11 -3
- package/dist/index.js +1 -1
- package/editor/chunks/extensions.js +2 -0
- package/editor/extensions/slash-command.d.ts +2 -0
- package/editor/mention-contract.d.ts +2 -0
- package/package.json +1 -1
- package/styles/ui-card.css +3 -0
- package/styles/ui-dialog.css +4 -2
- package/styles/ui-editor-mention-menu.css +7 -1
- package/styles/ui-editor-slash-menu.css +7 -1
- package/styles/ui-editor-table-context-menu.css +29 -5
- package/styles/ui-editor-table-toolbar.css +3 -11
- package/styles/ui-listbox.css +6 -0
- package/styles/ui-scroll-area.css +13 -0
- package/styles/ui-search-shell.css +1 -1
- package/styles/ui-section.css +3 -0
- package/vanilla/UiDialog.js +10 -6
- package/vanilla/UiListbox.js +10 -9
- package/vanilla/UiScrollArea.d.ts +1 -0
- package/vanilla/UiScrollArea.js +4 -4
- package/vue/UiCard.vue +3 -0
- package/vue/UiDialog.vue +12 -6
- package/vue/UiEditorMentionMenu.vue +6 -0
- package/vue/UiEditorSlashMenu.vue +6 -0
- package/vue/UiEditorTableContextMenu.vue +21 -4
- package/vue/UiEditorTableToolbar.vue +3 -11
- package/vue/UiListbox.vue +20 -15
- package/vue/UiScrollArea.d.ts +9 -0
- package/vue/UiScrollArea.vue +39 -1
- package/vue/UiSearchShell.vue +1 -1
- package/vue/UiSection.vue +3 -0
- package/vue/UiToast.vue +1 -1
- package/vue/chunks/UiCard.js +1 -1
- package/vue/chunks/UiDialog.js +25 -8
- package/vue/chunks/UiEditorMentionMenu.js +1 -1
- package/vue/chunks/UiEditorSlashMenu.js +1 -1
- package/vue/chunks/UiEditorTableContextMenu.js +1 -1
- package/vue/chunks/UiEditorTableToolbar.js +1 -1
- package/vue/chunks/UiListbox.js +12 -12
- package/vue/chunks/UiScrollArea.js +33 -2
- package/vue/chunks/UiSearchShell.js +1 -1
- package/vue/chunks/UiSection.js +1 -1
- package/vue/chunks/UiToast.js +2 -2
- package/vue/components.css +258 -213
- package/vue/editor/index.js +225 -40
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createViewportSurface } from "../shared/overlay.js";
|
|
1
2
|
import { normalizeAnchor, positionMenu } from "../shared/editor.js";
|
|
2
3
|
|
|
3
4
|
// Places the menu at the slash and tracks the highlighted item; the template renders the items.
|
|
@@ -7,6 +8,7 @@ function connect(host) {
|
|
|
7
8
|
const place = () => {
|
|
8
9
|
if (host.state.visible) positionMenu(element, normalizeAnchor(host.props.anchorRect.value), 280);
|
|
9
10
|
};
|
|
11
|
+
const surface = createViewportSurface(element, { position: place });
|
|
10
12
|
const options = () => Array.from(element.querySelectorAll('[role="option"]'));
|
|
11
13
|
const syncOptions = () => {
|
|
12
14
|
const query = String(host.props.query.value || "").trim().toLocaleLowerCase();
|
|
@@ -31,7 +33,8 @@ function connect(host) {
|
|
|
31
33
|
const visible = Boolean(host.props.open.value && normalizeAnchor(host.props.anchorRect.value)
|
|
32
34
|
&& (rows.length > 0 || host.refs.empty.children.length));
|
|
33
35
|
if (host.state.visible !== visible) host.state.visible = visible;
|
|
34
|
-
|
|
36
|
+
if (visible) surface.show();
|
|
37
|
+
else surface.hide();
|
|
35
38
|
};
|
|
36
39
|
const stop = host.effect(() => {
|
|
37
40
|
const items = Array.isArray(host.props.items.value) ? host.props.items.value : [];
|
|
@@ -60,20 +63,13 @@ function connect(host) {
|
|
|
60
63
|
element.addEventListener("mousedown", onMousedown);
|
|
61
64
|
element.addEventListener("click", onClick);
|
|
62
65
|
element.addEventListener("mouseover", onMouseover);
|
|
63
|
-
window.addEventListener("resize", place);
|
|
64
|
-
window.addEventListener("scroll", place, true);
|
|
65
|
-
window.visualViewport?.addEventListener("resize", place);
|
|
66
|
-
window.visualViewport?.addEventListener("scroll", place);
|
|
67
66
|
return () => {
|
|
68
67
|
stop();
|
|
68
|
+
surface.destroy();
|
|
69
69
|
observer.disconnect();
|
|
70
70
|
element.removeEventListener("mousedown", onMousedown);
|
|
71
71
|
element.removeEventListener("click", onClick);
|
|
72
72
|
element.removeEventListener("mouseover", onMouseover);
|
|
73
|
-
window.removeEventListener("resize", place);
|
|
74
|
-
window.removeEventListener("scroll", place, true);
|
|
75
|
-
window.visualViewport?.removeEventListener("resize", place);
|
|
76
|
-
window.visualViewport?.removeEventListener("scroll", place);
|
|
77
73
|
};
|
|
78
74
|
}
|
|
79
75
|
|
|
@@ -80,10 +80,16 @@
|
|
|
80
80
|
</template>
|
|
81
81
|
</div>
|
|
82
82
|
<style>
|
|
83
|
+
:host[popover] {
|
|
84
|
+
position: fixed;
|
|
85
|
+
inset: auto;
|
|
86
|
+
margin: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
:host {
|
|
84
90
|
/* The max-width is a promise about the menu's footprint, so it counts padding and border. */
|
|
85
91
|
box-sizing: border-box;
|
|
86
|
-
min-width: 14rem;
|
|
92
|
+
min-width: min(14rem, calc(100vw - 24px));
|
|
87
93
|
max-width: min(17rem, calc(100vw - 24px));
|
|
88
94
|
max-height: min(28rem, calc(100vh - 24px));
|
|
89
95
|
padding: var(--ui-space-2);
|
|
@@ -138,16 +144,22 @@
|
|
|
138
144
|
color: var(--ui-danger);
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
/* Swatches wrap like words: the row asks for the menu's full width, and a narrower menu fits fewer. */
|
|
141
148
|
.swatches {
|
|
142
|
-
display:
|
|
143
|
-
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-wrap: wrap;
|
|
144
151
|
gap: 3px;
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
.
|
|
154
|
+
/* Three to a row at the menu's full width; 5rem keeps the longest label, "Default", whole. */
|
|
155
|
+
:host .swatch-button {
|
|
148
156
|
display: inline-flex;
|
|
149
157
|
align-items: center;
|
|
150
158
|
gap: var(--ui-space-2);
|
|
159
|
+
flex: 1 0 5rem;
|
|
160
|
+
min-width: 5rem;
|
|
161
|
+
max-width: calc((100% - 6px) / 3);
|
|
162
|
+
padding-inline-end: var(--ui-space-1);
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
.swatch {
|
|
@@ -194,6 +206,11 @@
|
|
|
194
206
|
margin: var(--ui-space-1) 0;
|
|
195
207
|
}
|
|
196
208
|
|
|
209
|
+
/* Separators divide sections, so none sits above the first. */
|
|
210
|
+
.sep:first-child {
|
|
211
|
+
display: none;
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
:host[hidden] {
|
|
198
215
|
display: none;
|
|
199
216
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createViewportSurface, writtenPlace } from "../shared/overlay.js";
|
|
1
2
|
import { backgrounds, normalizeCellBackground, viewport } from "../shared/editor.js";
|
|
2
3
|
|
|
3
4
|
const sections = [
|
|
@@ -25,27 +26,55 @@ const sections = [
|
|
|
25
26
|
]],
|
|
26
27
|
];
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
+
// Shows the menu at the place it is written, keeping one in a floating box inside the viewport, and lists
|
|
30
|
+
// the actions the selection permits.
|
|
29
31
|
function connect(host) {
|
|
30
32
|
const element = host.element;
|
|
31
|
-
let
|
|
33
|
+
let place = null;
|
|
32
34
|
const nudge = () => {
|
|
33
|
-
frame = undefined;
|
|
34
35
|
element.style.translate = "";
|
|
36
|
+
if (!place) return;
|
|
35
37
|
const rect = element.getBoundingClientRect();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const at = place.rect();
|
|
39
|
+
let x = at.left - rect.left;
|
|
40
|
+
let y = at.top - rect.top;
|
|
41
|
+
// Only a menu in a fixed or absolutely positioned box, as LoomaEditor places it, answers to the
|
|
42
|
+
// viewport; one in page flow travels with its place.
|
|
43
|
+
if (place.floating) {
|
|
44
|
+
const view = viewport();
|
|
45
|
+
const inset = 12;
|
|
46
|
+
const left = rect.left + x;
|
|
47
|
+
const top = rect.top + y;
|
|
48
|
+
if (left < view.left + inset) x += view.left + inset - left;
|
|
49
|
+
else if (left + rect.width > view.right - inset) x += view.right - inset - left - rect.width;
|
|
50
|
+
if (top < view.top + inset) y += view.top + inset - top;
|
|
51
|
+
else if (top + rect.height > view.bottom - inset) y += view.bottom - inset - top - rect.height;
|
|
52
|
+
}
|
|
44
53
|
if (x || y) element.style.translate = `${x}px ${y}px`;
|
|
45
54
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
const surface = createViewportSurface(element, { position: nudge });
|
|
56
|
+
// Scroll and resize events do not report layout that moves the place; sample it while shown.
|
|
57
|
+
let frame = null;
|
|
58
|
+
let last = "";
|
|
59
|
+
const track = () => {
|
|
60
|
+
frame = null;
|
|
61
|
+
if (!place || !element.matches(":popover-open")) return;
|
|
62
|
+
const at = place.rect();
|
|
63
|
+
if (`${at.left},${at.top}` !== last) {
|
|
64
|
+
last = `${at.left},${at.top}`;
|
|
65
|
+
surface.refresh();
|
|
66
|
+
}
|
|
67
|
+
frame = requestAnimationFrame(track);
|
|
68
|
+
};
|
|
69
|
+
const untrack = () => {
|
|
70
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
71
|
+
frame = null;
|
|
72
|
+
last = "";
|
|
73
|
+
};
|
|
74
|
+
const show = () => {
|
|
75
|
+
if (!element.matches(":popover-open")) place = writtenPlace(element, ["translate"]);
|
|
76
|
+
surface.show();
|
|
77
|
+
frame ??= requestAnimationFrame(track);
|
|
49
78
|
};
|
|
50
79
|
const stop = host.effect(() => {
|
|
51
80
|
const scope = String(host.props.scope.value || "cell");
|
|
@@ -62,23 +91,22 @@ function connect(host) {
|
|
|
62
91
|
.map(([action, label, icon, tone]) => ({ action, label, icon, danger: tone === "danger", checkable: action.startsWith("toggle-header-"), checked: action === "toggle-header-row" ? Boolean(host.props.headerRow.value) : action === "toggle-header-column" ? Boolean(host.props.headerColumn.value) : false })),
|
|
63
92
|
}))
|
|
64
93
|
.filter((section) => section.items.length);
|
|
65
|
-
if (host.props.open.value)
|
|
94
|
+
if (host.props.open.value) queueMicrotask(() => { if (host.props.open.value) show(); });
|
|
95
|
+
else {
|
|
96
|
+
untrack();
|
|
97
|
+
surface.hide();
|
|
98
|
+
}
|
|
66
99
|
});
|
|
67
100
|
const onClick = (event) => {
|
|
68
101
|
const action = event.target.closest?.("[data-action]")?.dataset.action;
|
|
69
102
|
if (action) host.dispatch("action", { action });
|
|
70
103
|
};
|
|
71
104
|
element.addEventListener("click", onClick);
|
|
72
|
-
window.addEventListener("resize", schedule);
|
|
73
|
-
window.visualViewport?.addEventListener("resize", schedule);
|
|
74
|
-
window.visualViewport?.addEventListener("scroll", schedule);
|
|
75
105
|
return () => {
|
|
76
106
|
stop();
|
|
107
|
+
untrack();
|
|
108
|
+
surface.destroy();
|
|
77
109
|
element.removeEventListener("click", onClick);
|
|
78
|
-
window.removeEventListener("resize", schedule);
|
|
79
|
-
window.visualViewport?.removeEventListener("resize", schedule);
|
|
80
|
-
window.visualViewport?.removeEventListener("scroll", schedule);
|
|
81
|
-
if (frame) cancelAnimationFrame(frame);
|
|
82
110
|
};
|
|
83
111
|
}
|
|
84
112
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { observeOverlayViewport } from "../shared/overlay.js";
|
|
2
|
+
|
|
1
3
|
function fallbackBoundaries(segments, length) {
|
|
2
4
|
return Array.from({ length: segments + 1 }, (_, index) => (index * length) / segments);
|
|
3
5
|
}
|
|
@@ -36,15 +38,11 @@ function createProximity(scope, selector = ".handle[data-ui-affordance]", radius
|
|
|
36
38
|
point = event.pointerType === "touch" ? null : { x: event.clientX, y: event.clientY }; schedule();
|
|
37
39
|
};
|
|
38
40
|
pointerTarget.addEventListener("pointermove", onPointermove, { passive: true });
|
|
39
|
-
|
|
40
|
-
owner.addEventListener("scroll", refresh, { passive: true, capture: true });
|
|
41
|
-
owner.visualViewport?.addEventListener("resize", refresh, { passive: true });
|
|
42
|
-
owner.visualViewport?.addEventListener("scroll", refresh, { passive: true });
|
|
41
|
+
const stopViewport = observeOverlayViewport(scope.ownerDocument, refresh);
|
|
43
42
|
measure();
|
|
44
43
|
return { refresh, destroy() {
|
|
45
44
|
pointerTarget.removeEventListener("pointermove", onPointermove);
|
|
46
|
-
|
|
47
|
-
owner.visualViewport?.removeEventListener("resize", refresh); owner.visualViewport?.removeEventListener("scroll", refresh);
|
|
45
|
+
stopViewport();
|
|
48
46
|
if (frame !== null) owner.cancelAnimationFrame(frame); clear();
|
|
49
47
|
} };
|
|
50
48
|
}
|
|
@@ -140,14 +140,6 @@
|
|
|
140
140
|
background: var(--ui-accent-soft, var(--ui-accent-subtle));
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
.looma-editor__mobile-toolbar-shell :host .menu {
|
|
144
|
-
top: auto;
|
|
145
|
-
right: 0;
|
|
146
|
-
bottom: calc(100% + var(--ui-space-2));
|
|
147
|
-
max-height: min(50dvh, 22rem);
|
|
148
|
-
overflow-y: auto;
|
|
149
|
-
overscroll-behavior: contain;
|
|
150
|
-
}
|
|
151
143
|
:host {
|
|
152
144
|
display: inline-flex;
|
|
153
145
|
align-items: center;
|
|
@@ -238,9 +230,9 @@
|
|
|
238
230
|
|
|
239
231
|
.menu {
|
|
240
232
|
box-sizing: border-box;
|
|
241
|
-
position:
|
|
242
|
-
|
|
243
|
-
|
|
233
|
+
position: fixed;
|
|
234
|
+
inset: auto;
|
|
235
|
+
margin: 0;
|
|
244
236
|
min-width: 12rem;
|
|
245
237
|
max-width: min(17rem, calc(100vw - 24px));
|
|
246
238
|
padding: var(--ui-space-1);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { closeOverlay, createViewportSurface, openOverlay, positionAnchoredSurface } from "../shared/overlay.js";
|
|
1
2
|
import { backgrounds, normalizeCellBackground } from "../shared/editor.js";
|
|
2
|
-
import { createAnchoredSurface } from "../shared/overlay.js";
|
|
3
3
|
|
|
4
4
|
const overflowSections = [
|
|
5
5
|
["Structure", [["add-row-before", "Add row above", "panel-top"], ["add-column-before", "Add column left", "panel-left"], ["toggle-header-row", "Header row", "rows"], ["toggle-header-column", "Header column", "columns"], ["move-row-up", "Move row up", "rows"], ["move-row-down", "Move row down", "rows"], ["move-column-left", "Move column left", "columns"], ["move-column-right", "Move column right", "columns"]]],
|
|
@@ -10,19 +10,13 @@ const overflowSections = [
|
|
|
10
10
|
// Lists the actions the table selection permits; the overflow menu opens and closes here.
|
|
11
11
|
function connect(host) {
|
|
12
12
|
const element = host.element;
|
|
13
|
+
let alive = true;
|
|
14
|
+
let menu = null;
|
|
13
15
|
let surface = null;
|
|
14
|
-
|
|
15
|
-
let positionFrame = null;
|
|
16
|
+
const document = element.ownerDocument;
|
|
16
17
|
const positionMenu = () => {
|
|
17
|
-
if (!host.state.overflowOpen) return;
|
|
18
18
|
const button = element.querySelector(".more");
|
|
19
|
-
const menu = element.querySelector(".menu");
|
|
20
19
|
if (!button || !menu) return;
|
|
21
|
-
if (surfaceMenu !== menu) {
|
|
22
|
-
surface?.destroy();
|
|
23
|
-
surfaceMenu = menu;
|
|
24
|
-
surface = createAnchoredSurface(menu, { anchor: button, placement: "bottom-end", gap: 8, viewportGap: 12 });
|
|
25
|
-
}
|
|
26
20
|
const rect = button.getBoundingClientRect();
|
|
27
21
|
const viewport = window.visualViewport;
|
|
28
22
|
const top = viewport?.offsetTop ?? 0;
|
|
@@ -30,12 +24,34 @@ function connect(host) {
|
|
|
30
24
|
const above = rect.top - top - 20;
|
|
31
25
|
const below = bottom - rect.bottom - 20;
|
|
32
26
|
menu.style.maxHeight = `${Math.max(80, Math.floor(Math.max(above, below)))}px`;
|
|
33
|
-
|
|
27
|
+
positionAnchoredSurface(menu, rect, { placement: "bottom-end", gap: 8, viewportGap: 12 });
|
|
28
|
+
};
|
|
29
|
+
const close = (reason) => {
|
|
30
|
+
host.state.overflowOpen = false;
|
|
31
|
+
if (reason === "escape") element.querySelector(".more")?.focus();
|
|
34
32
|
};
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const releasePresentation = () => {
|
|
34
|
+
surface?.destroy();
|
|
35
|
+
surface = null;
|
|
36
|
+
menu = null;
|
|
37
|
+
closeOverlay(document, element);
|
|
38
|
+
};
|
|
39
|
+
const syncPresentation = () => {
|
|
40
|
+
if (!alive || !host.state.overflowOpen) return;
|
|
41
|
+
const next = element.querySelector(".menu");
|
|
42
|
+
if (!next) return;
|
|
43
|
+
if (next !== menu) {
|
|
44
|
+
releasePresentation();
|
|
45
|
+
menu = next;
|
|
46
|
+
surface = createViewportSurface(menu, { position: positionMenu });
|
|
47
|
+
}
|
|
48
|
+
surface.show();
|
|
49
|
+
openOverlay({ id: element, modal: false, element: menu, relatedElements: [element], dismissible: true, requestClose: close });
|
|
38
50
|
};
|
|
51
|
+
const stopPresentation = host.effect(() => {
|
|
52
|
+
if (host.state.overflowOpen) queueMicrotask(syncPresentation);
|
|
53
|
+
else releasePresentation();
|
|
54
|
+
});
|
|
39
55
|
const stop = host.effect(() => {
|
|
40
56
|
const enabled = new Set(Array.isArray(host.props.actions.value) ? host.props.actions.value : []);
|
|
41
57
|
const alignment = host.props.cellAlignment.value === "center" || host.props.cellAlignment.value === "right" ? host.props.cellAlignment.value : "left";
|
|
@@ -58,14 +74,6 @@ function connect(host) {
|
|
|
58
74
|
host.state.hasOverflow = host.state.swatches.length > 0 || host.state.sections.length > 0;
|
|
59
75
|
if (!host.props.open.value || !host.state.hasOverflow) host.state.overflowOpen = false;
|
|
60
76
|
});
|
|
61
|
-
const stopMenu = host.effect(() => {
|
|
62
|
-
if (host.state.overflowOpen) schedulePosition();
|
|
63
|
-
else {
|
|
64
|
-
surface?.destroy();
|
|
65
|
-
surface = null;
|
|
66
|
-
surfaceMenu = null;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
77
|
const onClick = (event) => {
|
|
70
78
|
const action = event.target.closest?.("[data-action]")?.dataset.action;
|
|
71
79
|
if (!action) return;
|
|
@@ -76,32 +84,13 @@ function connect(host) {
|
|
|
76
84
|
host.state.overflowOpen = false;
|
|
77
85
|
host.dispatch("action", { action });
|
|
78
86
|
};
|
|
79
|
-
const onOutside = (event) => {
|
|
80
|
-
if (host.state.overflowOpen && !event.composedPath().includes(element)) host.state.overflowOpen = false;
|
|
81
|
-
};
|
|
82
|
-
const onKeydown = (event) => {
|
|
83
|
-
if (event.key === "Escape" && host.state.overflowOpen) {
|
|
84
|
-
host.state.overflowOpen = false;
|
|
85
|
-
event.preventDefault();
|
|
86
|
-
event.stopPropagation();
|
|
87
|
-
element.querySelector(".more")?.focus();
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
87
|
element.addEventListener("click", onClick);
|
|
91
|
-
document.addEventListener("pointerdown", onOutside, true);
|
|
92
|
-
document.addEventListener("keydown", onKeydown, true);
|
|
93
|
-
window.addEventListener("resize", positionMenu);
|
|
94
|
-
window.visualViewport?.addEventListener("resize", positionMenu);
|
|
95
88
|
return () => {
|
|
89
|
+
alive = false;
|
|
96
90
|
stop();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (positionFrame !== null) cancelAnimationFrame(positionFrame);
|
|
91
|
+
stopPresentation();
|
|
92
|
+
releasePresentation();
|
|
100
93
|
element.removeEventListener("click", onClick);
|
|
101
|
-
document.removeEventListener("pointerdown", onOutside, true);
|
|
102
|
-
document.removeEventListener("keydown", onKeydown, true);
|
|
103
|
-
window.removeEventListener("resize", positionMenu);
|
|
104
|
-
window.visualViewport?.removeEventListener("resize", positionMenu);
|
|
105
94
|
};
|
|
106
95
|
}
|
|
107
96
|
|
|
@@ -32,23 +32,25 @@
|
|
|
32
32
|
from:aria-invalid="$invalid or $validationError"
|
|
33
33
|
style:--_rows="$rows"
|
|
34
34
|
>
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
<div class="choices">
|
|
36
|
+
<ui-scroll-area $ref="choices">
|
|
37
|
+
<div
|
|
38
|
+
$each="choice of $choices"
|
|
39
|
+
$key="$choice.index"
|
|
40
|
+
class="option"
|
|
41
|
+
class:active="$choice.index = $active"
|
|
42
|
+
role="option"
|
|
43
|
+
data-ui-choice-option
|
|
44
|
+
from:data-multiple="$multiple"
|
|
45
|
+
from:id="concat($uid, '-option-', $choice.index)"
|
|
46
|
+
from:data-index="$choice.index"
|
|
47
|
+
from:aria-selected="$choice.selected"
|
|
48
|
+
from:aria-disabled="$choice.disabled"
|
|
49
|
+
>
|
|
50
|
+
<span>{$choice.label}</span>
|
|
51
|
+
</div>
|
|
52
|
+
</ui-scroll-area>
|
|
53
|
+
</div>
|
|
52
54
|
<select
|
|
53
55
|
class="fallback"
|
|
54
56
|
$ref="options"
|
|
@@ -115,7 +117,10 @@
|
|
|
115
117
|
outline: none;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/* Its own box around the nested Scroll Area, which it shrinks into so the choices scroll. */
|
|
118
121
|
.choices {
|
|
122
|
+
display: flex;
|
|
123
|
+
flex-direction: column;
|
|
119
124
|
max-block-size: calc(max(2, var(--_rows, 5)) * var(--ui-control-size-md));
|
|
120
125
|
}
|
|
121
126
|
|
|
@@ -53,7 +53,8 @@ function connect(host) {
|
|
|
53
53
|
const setup = () => {
|
|
54
54
|
const nextFor = host.props.inline.value ? "" : String(host.props.for.value ?? "");
|
|
55
55
|
const nextPlacement = String(host.props.placement.value ?? "bottom-start");
|
|
56
|
-
if (nextFor === lastFor && nextPlacement === lastPlacement && surface
|
|
56
|
+
if (nextFor === lastFor && nextPlacement === lastPlacement && surface
|
|
57
|
+
&& (!nextFor || anchor === document.getElementById(nextFor))) return;
|
|
57
58
|
lastFor = nextFor;
|
|
58
59
|
lastPlacement = nextPlacement;
|
|
59
60
|
surface?.destroy();
|
|
@@ -61,7 +62,7 @@ function connect(host) {
|
|
|
61
62
|
anchor = ids.get(nextFor);
|
|
62
63
|
if (anchor) anchorAria = { "aria-haspopup": anchor.getAttribute("aria-haspopup"), "aria-expanded": anchor.getAttribute("aria-expanded") };
|
|
63
64
|
anchor?.addEventListener("click", onAnchorClick);
|
|
64
|
-
surface =
|
|
65
|
+
surface = host.props.inline.value ? null : createAnchoredSurface(element, { anchor, placement: nextPlacement });
|
|
65
66
|
};
|
|
66
67
|
const apply = () => {
|
|
67
68
|
const inline = Boolean(host.props.inline.value);
|
|
@@ -77,7 +78,8 @@ function connect(host) {
|
|
|
77
78
|
anchor.setAttribute("aria-expanded", String(open));
|
|
78
79
|
}
|
|
79
80
|
if (inline) {
|
|
80
|
-
|
|
81
|
+
element.removeAttribute("popover");
|
|
82
|
+
element.hidden = false;
|
|
81
83
|
closeOverlay(document, overlayId);
|
|
82
84
|
} else if (open) {
|
|
83
85
|
surface?.show();
|
|
@@ -49,7 +49,8 @@ function connect(host) {
|
|
|
49
49
|
const setup = () => {
|
|
50
50
|
const nextFor = String(host.props.for.value ?? "");
|
|
51
51
|
const nextPlacement = String(host.props.placement.value ?? "bottom-start");
|
|
52
|
-
if (surface && nextFor === lastFor && nextPlacement === lastPlacement
|
|
52
|
+
if (surface && nextFor === lastFor && nextPlacement === lastPlacement
|
|
53
|
+
&& (!nextFor || anchor === document.getElementById(nextFor))) return;
|
|
53
54
|
lastFor = nextFor;
|
|
54
55
|
lastPlacement = nextPlacement;
|
|
55
56
|
surface?.destroy();
|
|
@@ -57,7 +58,7 @@ function connect(host) {
|
|
|
57
58
|
anchor = ids.get(nextFor);
|
|
58
59
|
if (anchor) anchorExpanded = anchor.getAttribute("aria-expanded");
|
|
59
60
|
anchor?.addEventListener("click", onAnchorClick);
|
|
60
|
-
surface =
|
|
61
|
+
surface = createAnchoredSurface(element, { anchor, placement: nextPlacement, gap: offset });
|
|
61
62
|
};
|
|
62
63
|
const apply = () => {
|
|
63
64
|
const externalOpen = Boolean(host.props.open.value);
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
<prop name="orientation" type="keyword" values="vertical, horizontal" default="vertical"
|
|
8
8
|
>The direction the content scrolls. Vertical scrolls up and down and clips sideways overflow; horizontal scrolls sideways and clips vertical
|
|
9
9
|
overflow.</prop>
|
|
10
|
+
<prop name="padding" type="keyword" values="none, xs, sm, md, lg" default="none"
|
|
11
|
+
>The space inside the scrolling region, on Card's scale: xs is 0.5rem, sm 0.75rem, md 1.5rem, and lg 2rem. It scrolls with the content, so the scrollbar and
|
|
12
|
+
fades stay at the edges.</prop>
|
|
10
13
|
<prop name="trim" type="boolean" default="false"
|
|
11
14
|
>Removes the outer block margins of the first and last projected items when the surrounding panel owns its edge padding. Margins between items remain
|
|
12
15
|
intact.</prop>
|
|
@@ -73,6 +76,19 @@
|
|
|
73
76
|
max-block-size: 30rem;
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
:host([padding="xs"]) {
|
|
80
|
+
padding: var(--ui-space-2);
|
|
81
|
+
}
|
|
82
|
+
:host([padding="sm"]) {
|
|
83
|
+
padding: var(--ui-space-3);
|
|
84
|
+
}
|
|
85
|
+
:host([padding="md"]) {
|
|
86
|
+
padding: var(--ui-space-5);
|
|
87
|
+
}
|
|
88
|
+
:host([padding="lg"]) {
|
|
89
|
+
padding: var(--ui-space-6);
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
/* A component that sets its display still honours the hidden attribute on its root. */
|
|
77
93
|
:host[hidden] {
|
|
78
94
|
display: none;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeOverlay, openOverlay } from "../shared/overlay.js";
|
|
1
|
+
import { closeDialog, closeOverlay, openOverlay, showDialog } from "../shared/overlay.js";
|
|
2
2
|
import { trackTrigger } from "../shared/trigger.js";
|
|
3
3
|
|
|
4
4
|
/** Owns native dialog state while leaving query and result state to the application. */
|
|
@@ -18,9 +18,8 @@ function connect(host) {
|
|
|
18
18
|
host.dispatch("close", { open: false, reason, trigger: how });
|
|
19
19
|
};
|
|
20
20
|
const closeNative = () => {
|
|
21
|
-
if (!dialog.open) return;
|
|
22
21
|
suppressNativeClose = true;
|
|
23
|
-
dialog
|
|
22
|
+
closeDialog(dialog);
|
|
24
23
|
suppressNativeClose = false;
|
|
25
24
|
};
|
|
26
25
|
const stop = host.effect(() => {
|
|
@@ -32,8 +31,7 @@ function connect(host) {
|
|
|
32
31
|
if (dialog.open && modal !== activeModal) closeNative();
|
|
33
32
|
activeModal = modal;
|
|
34
33
|
if (host.state.internalOpen && !dialog.open) {
|
|
35
|
-
|
|
36
|
-
else dialog.show();
|
|
34
|
+
showDialog(dialog, modal);
|
|
37
35
|
}
|
|
38
36
|
if (host.state.internalOpen) {
|
|
39
37
|
openOverlay({ id: overlayId, modal, element: dialog, modalElement: dialog, dismissible: false, requestClose: () => {} });
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
--_ui-selection-context-text: var(--ui-text-primary);
|
|
56
56
|
border-color: color-mix(in srgb, var(--ui-danger) 45%, var(--ui-border-default));
|
|
57
57
|
border-inline-start: var(--ui-accent-line-width) solid var(--ui-danger);
|
|
58
|
+
/* An accent edge keeps the corners on its side straight. */
|
|
59
|
+
border-start-start-radius: 0;
|
|
60
|
+
border-end-start-radius: 0;
|
|
58
61
|
background: var(--ui-danger-soft);
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
<prop name="tone" type="keyword" values="neutral, info, success, warning, danger" default="neutral"
|
|
6
6
|
>Semantic accent for this message. Danger uses an assertive alert role; the other tones use polite status semantics.</prop>
|
|
7
7
|
<prop name="duration" type="number" default="0"
|
|
8
|
-
>Milliseconds before
|
|
8
|
+
>Milliseconds the toast is shown before it dismisses itself; 0 keeps it until an action dismisses it. The timer starts when the toast is shown, starts again
|
|
9
|
+
each time it is shown, and pauses on hover or focus.</prop>
|
|
9
10
|
<event name="dismiss" type="object">
|
|
10
11
|
<prop name="id" required type="string"></prop>
|
|
11
12
|
<prop name="reason" required type="keyword" values="action, timeout"></prop>
|
|
@@ -4,7 +4,7 @@ function inputFor(event) {
|
|
|
4
4
|
return event.detail === 0 ? "keyboard" : "pointer";
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** An authored toast hides itself when dismissed, like a dialog closing; clearing hidden shows it again. */
|
|
8
8
|
function connect(host) {
|
|
9
9
|
const element = host.element;
|
|
10
10
|
const { action } = host.refs;
|
|
@@ -19,11 +19,13 @@ function connect(host) {
|
|
|
19
19
|
if (dismissed) return;
|
|
20
20
|
dismissed = true;
|
|
21
21
|
clearTimeout(timer);
|
|
22
|
+
element.hidden = true;
|
|
22
23
|
host.dispatch("dismiss", { id: element.id, reason, trigger });
|
|
23
24
|
};
|
|
24
25
|
const start = () => {
|
|
25
26
|
clearTimeout(timer);
|
|
26
|
-
|
|
27
|
+
// The duration counts only while the toast is shown; a hidden toast has nothing to time out.
|
|
28
|
+
if (paused || dismissed || element.hidden || remaining <= 0) return;
|
|
27
29
|
started = Date.now();
|
|
28
30
|
timer = setTimeout(() => dismiss("timeout", "programmatic"), remaining);
|
|
29
31
|
};
|
|
@@ -50,6 +52,19 @@ function connect(host) {
|
|
|
50
52
|
remaining = duration;
|
|
51
53
|
start();
|
|
52
54
|
});
|
|
55
|
+
// Hidden by the app, the timer stops; shown (again), it is a fresh request with the full duration.
|
|
56
|
+
const shown = new MutationObserver(() => {
|
|
57
|
+
if (element.hidden) {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
timer = null;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
dismissed = false;
|
|
63
|
+
paused = false;
|
|
64
|
+
remaining = Math.max(0, Number(host.props.duration.value) || 0);
|
|
65
|
+
start();
|
|
66
|
+
});
|
|
67
|
+
shown.observe(element, { attributes: true, attributeFilter: ["hidden"] });
|
|
53
68
|
element.addEventListener("click", onClick);
|
|
54
69
|
element.addEventListener("pointerenter", pause);
|
|
55
70
|
element.addEventListener("pointerleave", resume);
|
|
@@ -58,6 +73,7 @@ function connect(host) {
|
|
|
58
73
|
start();
|
|
59
74
|
return () => {
|
|
60
75
|
stop();
|
|
76
|
+
shown.disconnect();
|
|
61
77
|
clearTimeout(timer);
|
|
62
78
|
element.removeEventListener("click", onClick);
|
|
63
79
|
element.removeEventListener("pointerenter", pause);
|
|
@@ -5,13 +5,13 @@ let toastIds = 0;
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Shows the region while it has authored or generated messages. Generated messages dismiss by
|
|
8
|
-
* action or timeout; authored ui-toast children
|
|
8
|
+
* action or timeout; authored ui-toast children hide themselves, and a hidden one does not count.
|
|
9
9
|
*/
|
|
10
10
|
function connect(host) {
|
|
11
11
|
const element = host.element;
|
|
12
12
|
const [trigger, stopTracking] = trackTrigger(host);
|
|
13
13
|
const toasts = () => host.state.toasts ?? [];
|
|
14
|
-
const authored = () => Array.from(element.children).some((child) => !child.classList.contains("toast"));
|
|
14
|
+
const authored = () => Array.from(element.children).some((child) => !child.classList.contains("toast") && !child.hidden);
|
|
15
15
|
let announcedVisible = false;
|
|
16
16
|
const sync = () => {
|
|
17
17
|
const visible = authored() || toasts().length > 0;
|
|
@@ -86,8 +86,16 @@ function connect(host) {
|
|
|
86
86
|
if (event.target === element && typeof event.detail?.message === "string") add(event.detail.message, event.detail);
|
|
87
87
|
};
|
|
88
88
|
const stop = host.effect(sync);
|
|
89
|
-
|
|
89
|
+
// Children come and go, and an authored toast hides itself: watch each child's hidden, not the whole subtree.
|
|
90
|
+
const watchChildren = () => {
|
|
91
|
+
for (const child of element.children) observer.observe(child, { attributeFilter: ["hidden"] });
|
|
92
|
+
};
|
|
93
|
+
const observer = new MutationObserver(() => {
|
|
94
|
+
watchChildren();
|
|
95
|
+
sync();
|
|
96
|
+
});
|
|
90
97
|
observer.observe(element, { childList: true });
|
|
98
|
+
watchChildren();
|
|
91
99
|
element.addEventListener("click", onClick);
|
|
92
100
|
element.addEventListener("command", onCommand);
|
|
93
101
|
element.addEventListener("show-toast", onShowToast);
|