@threadlabs/looma 0.37.3 → 0.37.5
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/active-item-reveal.js +74 -0
- package/components/shared/menu-navigation.js +3 -1
- package/components/ui-button/ui-button.html +13 -0
- package/components/ui-combobox/ui-combobox.js +3 -1
- package/components/ui-context-menu/ui-context-menu.js +4 -1
- package/components/ui-editor-link-form/ui-editor-link-form.js +3 -1
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +12 -5
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +12 -5
- package/components/ui-form-field/ui-form-field.js +7 -1
- package/components/ui-listbox/ui-listbox.js +3 -1
- package/components/ui-menu/ui-menu.js +5 -1
- package/components/ui-pagination/ui-pagination.html +74 -0
- package/components/ui-pagination/ui-pagination.js +19 -0
- package/components/ui-search-shell/ui-search-shell.js +7 -4
- package/dist/index.js +20 -19
- package/docs/catalog.json +323 -1
- package/docs/examples/ui-pagination/examples/01-numbered.behavior.js +11 -0
- package/package.json +1 -1
- package/styles/ui-button.css +12 -0
- package/styles/ui-pagination.css +1 -0
- package/vanilla/UiButton.d.ts +1 -0
- package/vanilla/UiButton.js +31 -31
- package/vanilla/UiPagination.d.ts +35 -0
- package/vanilla/UiPagination.js +202 -0
- package/vanilla/index.js +1 -0
- package/vue/UiButton.d.ts +6 -0
- package/vue/UiButton.vue +34 -0
- package/vue/UiPagination.d.ts +115 -0
- package/vue/UiPagination.js +2 -0
- package/vue/UiPagination.vue +598 -0
- package/vue/chunks/UiActionBar.js +1 -1
- package/vue/chunks/UiAffordanceScope.js +1 -1
- package/vue/chunks/UiAvatar.js +1 -1
- package/vue/chunks/UiAvatarGroup.js +1 -1
- package/vue/chunks/UiBadge.js +1 -1
- package/vue/chunks/UiBreadcrumbItem.js +1 -1
- package/vue/chunks/UiBreadcrumbs.js +1 -1
- package/vue/chunks/UiButton.js +30 -2
- package/vue/chunks/UiCallout.js +1 -1
- package/vue/chunks/UiCard.js +1 -1
- package/vue/chunks/UiCheckbox.js +1 -1
- package/vue/chunks/UiCluster.js +1 -1
- package/vue/chunks/UiColorPicker.js +1 -1
- package/vue/chunks/UiCombobox.js +1 -1
- package/vue/chunks/UiContainer.js +1 -1
- package/vue/chunks/UiContextMenu.js +1 -1
- package/vue/chunks/UiCover.js +1 -1
- package/vue/chunks/UiCurveEditor.js +1 -1
- package/vue/chunks/UiCycleButton.js +1 -1
- package/vue/chunks/UiDescriptionItem.js +1 -1
- package/vue/chunks/UiDescriptionList.js +1 -1
- package/vue/chunks/UiDialog.js +1 -1
- package/vue/chunks/UiDisclosure.js +1 -1
- package/vue/chunks/UiEditable.js +1 -1
- package/vue/chunks/UiEditorBlockActions.js +1 -1
- package/vue/chunks/UiEditorChipEditor.js +1 -1
- package/vue/chunks/UiEditorCodeLanguage.js +1 -1
- package/vue/chunks/UiEditorFootnoteEditor.js +1 -1
- package/vue/chunks/UiEditorImageToolbar.js +1 -1
- package/vue/chunks/UiEditorInsertTableGrid.js +1 -1
- package/vue/chunks/UiEditorLinkCard.js +1 -1
- package/vue/chunks/UiEditorLinkForm.js +1 -1
- package/vue/chunks/UiEditorMentionMenu.js +1 -1
- package/vue/chunks/UiEditorMentionMenuItem.js +1 -1
- package/vue/chunks/UiEditorSlashMenu.js +1 -1
- package/vue/chunks/UiEditorSlashMenuGroup.js +1 -1
- package/vue/chunks/UiEditorSlashMenuItem.js +1 -1
- package/vue/chunks/UiEditorSurface.js +1 -1
- package/vue/chunks/UiEditorTableContextMenu.js +1 -1
- package/vue/chunks/UiEditorTableOverlay.js +1 -1
- package/vue/chunks/UiEditorTableToolbar.js +1 -1
- package/vue/chunks/UiEditorToolbar.js +1 -1
- package/vue/chunks/UiFloatingActionButton.js +1 -1
- package/vue/chunks/UiFormField.js +1 -1
- package/vue/chunks/UiGrid.js +1 -1
- package/vue/chunks/UiIcon.js +1 -1
- package/vue/chunks/UiIconButton.js +1 -1
- package/vue/chunks/UiImage.js +1 -1
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiInputGroup.js +1 -1
- package/vue/chunks/UiLightEditor.js +1 -1
- package/vue/chunks/UiList.js +1 -1
- package/vue/chunks/UiListItem.js +1 -1
- package/vue/chunks/UiListbox.js +1 -1
- package/vue/chunks/UiMenu.js +1 -1
- package/vue/chunks/UiMenuGroup.js +1 -1
- package/vue/chunks/UiMenuItem.js +1 -1
- package/vue/chunks/UiMeter.js +1 -1
- package/vue/chunks/UiNavItem.js +1 -1
- package/vue/chunks/UiNumberInput.js +1 -1
- package/vue/chunks/UiPageHeader.js +1 -1
- package/vue/chunks/UiPagination.js +551 -0
- package/vue/chunks/UiPopover.js +1 -1
- package/vue/chunks/UiRadio.js +1 -1
- package/vue/chunks/UiRadioGroup.js +1 -1
- package/vue/chunks/UiReel.js +1 -1
- package/vue/chunks/UiScrollArea.js +1 -1
- package/vue/chunks/UiSearchResultRow.js +1 -1
- package/vue/chunks/UiSearchShell.js +1 -1
- package/vue/chunks/UiSection.js +1 -1
- package/vue/chunks/UiSelect.js +1 -1
- package/vue/chunks/UiSeparator.js +1 -1
- package/vue/chunks/UiSidebar.js +1 -1
- package/vue/chunks/UiSlider.js +1 -1
- package/vue/chunks/UiSpinner.js +1 -1
- package/vue/chunks/UiStack.js +1 -1
- package/vue/chunks/UiStatusMessage.js +1 -1
- package/vue/chunks/UiSwatch.js +1 -1
- package/vue/chunks/UiSwitch.js +1 -1
- package/vue/chunks/UiSwitcher.js +1 -1
- package/vue/chunks/UiTable.js +1 -1
- package/vue/chunks/UiTabs.js +1 -1
- package/vue/chunks/UiText.js +1 -1
- package/vue/chunks/UiTextarea.js +1 -1
- package/vue/chunks/UiToast.js +1 -1
- package/vue/chunks/UiToastRegion.js +1 -1
- package/vue/chunks/UiTooltip.js +1 -1
- package/vue/chunks/UiTopBar.js +1 -1
- package/vue/chunks/UiTree.js +1 -1
- package/vue/chunks/UiTreeItem.js +1 -1
- package/vue/chunks/_plugin-vue_export-helper.js +27 -2
- package/vue/components.css +22 -0
- package/vue/editor/index.js +145 -36
- package/vue/index.d.ts +1 -0
- package/vue/index.js +2 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Reveal within the component's own scrollport; browser focus/scrollIntoView can move the page.
|
|
2
|
+
const pending = new WeakMap();
|
|
3
|
+
|
|
4
|
+
export function cancelActiveItemReveal(root) {
|
|
5
|
+
const current = pending.get(root);
|
|
6
|
+
if (!current) return;
|
|
7
|
+
root.ownerDocument.defaultView.cancelAnimationFrame(current.frame);
|
|
8
|
+
pending.delete(root);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function scrollport(root, item, view) {
|
|
12
|
+
for (let parent = item.parentElement; parent && root.contains(parent); parent = parent.parentElement) {
|
|
13
|
+
if (parent.scrollHeight > parent.clientHeight && /^(auto|scroll|hidden)$/.test(view.getComputedStyle(parent).overflowY)) return parent;
|
|
14
|
+
if (parent === root) break;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function destination(port, item, view) {
|
|
20
|
+
const bounds = port.getBoundingClientRect();
|
|
21
|
+
const scale = port.offsetHeight ? bounds.height / port.offsetHeight : 1;
|
|
22
|
+
if (!scale) return port.scrollTop;
|
|
23
|
+
const style = view.getComputedStyle(port);
|
|
24
|
+
const fade = style.maskImage !== "none" ? parseFloat(style.getPropertyValue("--_ui-scroll-fade-size")) || 0 : 0;
|
|
25
|
+
const top = bounds.top + (port.clientTop + Math.max(parseFloat(style.scrollPaddingTop) || 0, fade)) * scale;
|
|
26
|
+
const bottom = bounds.top + (port.clientTop + port.clientHeight - Math.max(parseFloat(style.scrollPaddingBottom) || 0, fade)) * scale;
|
|
27
|
+
const row = item.getBoundingClientRect();
|
|
28
|
+
const delta = row.top < top ? row.top - top : row.bottom > bottom ? Math.min(row.bottom - bottom, row.top - top) : 0;
|
|
29
|
+
return Math.max(0, Math.min(port.scrollHeight - port.clientHeight, port.scrollTop + delta / scale));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve after rendering so HTML and framework adapters reveal the same active row. */
|
|
33
|
+
export function revealActiveItem(root, resolve) {
|
|
34
|
+
const view = root.ownerDocument.defaultView;
|
|
35
|
+
const item = typeof resolve === "function" ? resolve() : resolve;
|
|
36
|
+
if (item && pending.get(root)?.item === item) return;
|
|
37
|
+
cancelActiveItemReveal(root);
|
|
38
|
+
const current = { item, frame: 0 };
|
|
39
|
+
pending.set(root, current);
|
|
40
|
+
current.frame = view.requestAnimationFrame(() => {
|
|
41
|
+
current.item = typeof resolve === "function" ? resolve() : resolve;
|
|
42
|
+
if (!root.isConnected || !current.item?.getClientRects().length || !root.contains(current.item)) {
|
|
43
|
+
pending.delete(root);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const port = scrollport(root, current.item, view);
|
|
47
|
+
if (!port) { pending.delete(root); return; }
|
|
48
|
+
const from = port.scrollTop;
|
|
49
|
+
const to = destination(port, current.item, view);
|
|
50
|
+
const token = view.getComputedStyle(root).getPropertyValue("--ui-motion-fast").trim();
|
|
51
|
+
const duration = view.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0
|
|
52
|
+
: (parseFloat(token) || 0) * (token.endsWith("ms") ? 1 : 1000);
|
|
53
|
+
if (Math.abs(to - from) < 1 || duration <= 0) {
|
|
54
|
+
port.scrollTop = to;
|
|
55
|
+
pending.delete(root);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const started = view.performance.now();
|
|
59
|
+
const animate = (now) => {
|
|
60
|
+
if (!root.isConnected || !current.item.getClientRects().length) { pending.delete(root); return; }
|
|
61
|
+
const progress = Math.min(1, (now - started) / duration);
|
|
62
|
+
port.scrollTop = from + (to - from) * (1 - (1 - progress) ** 3);
|
|
63
|
+
if (progress < 1) current.frame = view.requestAnimationFrame(animate);
|
|
64
|
+
else pending.delete(root);
|
|
65
|
+
};
|
|
66
|
+
current.frame = view.requestAnimationFrame(animate);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function focusAndRevealItem(root, item) {
|
|
71
|
+
if (!item) return;
|
|
72
|
+
item.focus({ preventScroll: true });
|
|
73
|
+
revealActiveItem(root, item);
|
|
74
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { focusAndRevealItem } from "./active-item-reveal.js";
|
|
2
|
+
|
|
1
3
|
const itemSelector = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
|
|
2
4
|
const searches = new WeakMap();
|
|
3
5
|
|
|
@@ -44,6 +46,6 @@ export function navigateMenu(event, menu) {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
event.preventDefault();
|
|
47
|
-
if (next >= 0) items[next]
|
|
49
|
+
if (next >= 0) focusAndRevealItem(menu, items[next]);
|
|
48
50
|
return true;
|
|
49
51
|
}
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
>Height, padding, and type size. xs is 1.5rem with the smallest text, for a control inside a line of small text; sm is the small control height (2rem by
|
|
20
20
|
default) with tighter padding and smaller text, md is the standard control height (2.5rem), and lg is taller (3rem) with roomier padding. Visible sizes
|
|
21
21
|
stay the same on touch. Inline xs and link actions can gain an adjustable invisible hit area.</prop>
|
|
22
|
+
<prop name="square" type="boolean" default="false"
|
|
23
|
+
>Gives short labels and icons the same square minimum footprint at each size. Longer labels grow naturally. Applies to boxed actions; link and card variants
|
|
24
|
+
keep their normal layout.</prop>
|
|
22
25
|
<prop name="shape" type="keyword" values="rounded, pill" default="rounded"
|
|
23
26
|
>rounded uses the theme's action radius; pill rounds the ends fully, for a compact choice such as a status or a person.</prop>
|
|
24
27
|
<prop name="tone" type="keyword" values="accent, neutral, danger, success, warning, info" default="accent"
|
|
@@ -233,6 +236,7 @@
|
|
|
233
236
|
}
|
|
234
237
|
|
|
235
238
|
:host {
|
|
239
|
+
--_button-square-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
|
|
236
240
|
appearance: none;
|
|
237
241
|
min-block-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
|
|
238
242
|
border-style: solid;
|
|
@@ -297,6 +301,7 @@
|
|
|
297
301
|
}
|
|
298
302
|
|
|
299
303
|
:host([size="xs"]) {
|
|
304
|
+
--_button-square-size: 1.5rem;
|
|
300
305
|
min-block-size: 1.5rem;
|
|
301
306
|
padding: calc(var(--ui-space-0-5) * 1.5) calc(var(--ui-space-1) * 1.25) !important;
|
|
302
307
|
font-size: var(--ui-font-size-xs);
|
|
@@ -307,17 +312,25 @@
|
|
|
307
312
|
}
|
|
308
313
|
|
|
309
314
|
:host([size="sm"]) {
|
|
315
|
+
--_button-square-size: var(--ui-control-size-sm);
|
|
310
316
|
min-block-size: var(--ui-control-size-sm);
|
|
311
317
|
padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-action-inset-sm, var(--ui-space-2)) !important;
|
|
312
318
|
font-size: var(--ui-font-size-sm);
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
:host([size="lg"]) {
|
|
322
|
+
--_button-square-size: var(--ui-control-size-lg);
|
|
316
323
|
min-block-size: var(--ui-control-size-lg);
|
|
317
324
|
padding: var(--ui-control-inset-block-lg, var(--ui-space-3)) var(--ui-action-inset-lg, var(--ui-space-4)) !important;
|
|
318
325
|
font-size: var(--ui-font-size-md);
|
|
319
326
|
}
|
|
320
327
|
|
|
328
|
+
/* Short numeric and icon actions share a minimum box; large values remain readable. */
|
|
329
|
+
:host([square]):not([data-appearance="link"], [data-appearance="card"]) {
|
|
330
|
+
min-inline-size: var(--_button-square-size);
|
|
331
|
+
padding-inline: var(--ui-space-1) !important;
|
|
332
|
+
}
|
|
333
|
+
|
|
321
334
|
/* Card actions inherit Button's palette and behavior; only their shared layout changes.
|
|
322
335
|
The icon may contain any decorative composition. Empty icons remove their column. */
|
|
323
336
|
:host[data-appearance="card"] {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, revealActiveItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
import { afterFormReset } from "../shared/form-reset.js";
|
|
2
3
|
import { closeOverlay, createAnchoredSurface, openOverlay } from "../shared/overlay.js";
|
|
3
4
|
|
|
@@ -107,6 +108,7 @@ export function connect(host) {
|
|
|
107
108
|
lookup = undefined;
|
|
108
109
|
};
|
|
109
110
|
const close = () => {
|
|
111
|
+
cancelActiveItemReveal(popup);
|
|
110
112
|
host.state.expanded = false;
|
|
111
113
|
host.state.active = -1;
|
|
112
114
|
cancelLookup();
|
|
@@ -326,7 +328,7 @@ export function connect(host) {
|
|
|
326
328
|
const current = indices.indexOf(host.state.active);
|
|
327
329
|
const next = key === "Home" ? 0 : key === "End" ? indices.length - 1 : key === "ArrowDown" ? Math.min(current + 1, indices.length - 1) : current < 0 ? indices.length - 1 : Math.max(0, current - 1);
|
|
328
330
|
host.state.active = indices[next];
|
|
329
|
-
|
|
331
|
+
revealActiveItem(popup, () => document.getElementById(`${uid}-option-${host.state.active}`));
|
|
330
332
|
};
|
|
331
333
|
const validateCurrent = async () => {
|
|
332
334
|
validationRun?.abort();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, focusAndRevealItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
import { closeOverlay, createAnchoredSurface, createIdResolver, openOverlay, requestTopOverlayClose } from "../shared/overlay.js";
|
|
2
3
|
import { menuItemFrom, menuItems, navigateMenu } from "../shared/menu-navigation.js";
|
|
3
4
|
|
|
@@ -19,7 +20,7 @@ export function connect(host) {
|
|
|
19
20
|
host.state.internalOpen = lastOpenProp;
|
|
20
21
|
|
|
21
22
|
const items = () => menuItems(element);
|
|
22
|
-
const focusFirst = () => requestAnimationFrame(() => items()[0]
|
|
23
|
+
const focusFirst = () => requestAnimationFrame(() => focusAndRevealItem(element, items()[0]));
|
|
23
24
|
const targetEvents = { contextmenu: onContextMenu, keydown: onTargetKeydown, pointerdown: onPointerdown, pointerup: endPress, pointercancel: endPress };
|
|
24
25
|
const detach = () => {
|
|
25
26
|
for (const [type, listener] of Object.entries(targetEvents)) target?.removeEventListener(type, listener);
|
|
@@ -37,6 +38,7 @@ export function connect(host) {
|
|
|
37
38
|
// Light dismiss leaves focus where the user pointed instead of pulling it back to the target.
|
|
38
39
|
const close = (reason, input, returnFocus = reason !== "light-dismiss") => {
|
|
39
40
|
if (!host.state.internalOpen) return;
|
|
41
|
+
cancelActiveItemReveal(element);
|
|
40
42
|
host.state.internalOpen = false;
|
|
41
43
|
endPress();
|
|
42
44
|
host.dispatch("close", { open: false, reason, trigger: input });
|
|
@@ -147,6 +149,7 @@ export function connect(host) {
|
|
|
147
149
|
const stop = host.effect(apply);
|
|
148
150
|
apply();
|
|
149
151
|
return () => {
|
|
152
|
+
cancelActiveItemReveal(element);
|
|
150
153
|
stop();
|
|
151
154
|
ids.stop();
|
|
152
155
|
observer.disconnect();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, focusAndRevealItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
// Gives each form its own field ids, reports field changes and choices, saves on Enter in a field,
|
|
2
3
|
// closes on Escape, and moves between the search field and its results with the arrow keys.
|
|
3
4
|
export function connect(host) {
|
|
@@ -26,7 +27,7 @@ export function connect(host) {
|
|
|
26
27
|
const moveFocus = (event, offset) => {
|
|
27
28
|
const results = [...element.querySelectorAll("[data-link-result]")];
|
|
28
29
|
const next = results[results.indexOf(event.target) + offset];
|
|
29
|
-
if (next) next
|
|
30
|
+
if (next) focusAndRevealItem(element, next);
|
|
30
31
|
else if (offset < 0) element.querySelector("[data-link-search]")?.focus();
|
|
31
32
|
else return;
|
|
32
33
|
event.preventDefault();
|
|
@@ -57,6 +58,7 @@ export function connect(host) {
|
|
|
57
58
|
element.addEventListener("keydown", onKeyDown);
|
|
58
59
|
element.addEventListener("submit", onSubmit);
|
|
59
60
|
return () => {
|
|
61
|
+
cancelActiveItemReveal(element);
|
|
60
62
|
element.removeEventListener("input", onInput);
|
|
61
63
|
element.removeEventListener("change", onChange);
|
|
62
64
|
element.removeEventListener("click", onClick);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, revealActiveItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
import { createViewportSurface } from "../shared/overlay.js";
|
|
2
3
|
import { normalizeAnchor, positionMenu } from "../shared/editor.js";
|
|
3
4
|
|
|
@@ -25,8 +26,13 @@ export function connect(host) {
|
|
|
25
26
|
const visible = Boolean(host.props.open.value && normalizeAnchor(host.props.anchorRect.value)
|
|
26
27
|
&& (host.props.loading.value || rows.length > 0 || empty));
|
|
27
28
|
if (host.state.visible !== visible) host.state.visible = visible;
|
|
28
|
-
if (visible)
|
|
29
|
-
|
|
29
|
+
if (visible) {
|
|
30
|
+
surface.show();
|
|
31
|
+
revealActiveItem(element, rows[active]);
|
|
32
|
+
} else {
|
|
33
|
+
cancelActiveItemReveal(element);
|
|
34
|
+
surface.hide();
|
|
35
|
+
}
|
|
30
36
|
};
|
|
31
37
|
const stop = host.effect(() => {
|
|
32
38
|
if (!authoredLabel) element.setAttribute("aria-label", String(host.props.label.value || "Mentions"));
|
|
@@ -43,7 +49,7 @@ export function connect(host) {
|
|
|
43
49
|
const row = rowOf(event);
|
|
44
50
|
if (row) host.dispatch("select", { index: Number(row.dataset.index), value: row.dataset.value ?? "" });
|
|
45
51
|
};
|
|
46
|
-
const
|
|
52
|
+
const onMousemove = (event) => {
|
|
47
53
|
const row = rowOf(event);
|
|
48
54
|
if (!row) return;
|
|
49
55
|
const index = Number(row.dataset.index);
|
|
@@ -56,13 +62,14 @@ export function connect(host) {
|
|
|
56
62
|
observer.observe(element, { childList: true, subtree: true });
|
|
57
63
|
element.addEventListener("mousedown", onMousedown);
|
|
58
64
|
element.addEventListener("click", onClick);
|
|
59
|
-
element.addEventListener("
|
|
65
|
+
element.addEventListener("mousemove", onMousemove);
|
|
60
66
|
return () => {
|
|
67
|
+
cancelActiveItemReveal(element);
|
|
61
68
|
stop();
|
|
62
69
|
surface.destroy();
|
|
63
70
|
observer.disconnect();
|
|
64
71
|
element.removeEventListener("mousedown", onMousedown);
|
|
65
72
|
element.removeEventListener("click", onClick);
|
|
66
|
-
element.removeEventListener("
|
|
73
|
+
element.removeEventListener("mousemove", onMousemove);
|
|
67
74
|
};
|
|
68
75
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, revealActiveItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
import { createViewportSurface } from "../shared/overlay.js";
|
|
2
3
|
import { normalizeAnchor, positionMenu } from "../shared/editor.js";
|
|
3
4
|
|
|
@@ -33,8 +34,13 @@ export function connect(host) {
|
|
|
33
34
|
const visible = Boolean(host.props.open.value && normalizeAnchor(host.props.anchorRect.value)
|
|
34
35
|
&& (rows.length > 0 || host.refs.empty.children.length));
|
|
35
36
|
if (host.state.visible !== visible) host.state.visible = visible;
|
|
36
|
-
if (visible)
|
|
37
|
-
|
|
37
|
+
if (visible) {
|
|
38
|
+
surface.show();
|
|
39
|
+
revealActiveItem(element, rows[active]);
|
|
40
|
+
} else {
|
|
41
|
+
cancelActiveItemReveal(element);
|
|
42
|
+
surface.hide();
|
|
43
|
+
}
|
|
38
44
|
};
|
|
39
45
|
const stop = host.effect(() => {
|
|
40
46
|
const items = Array.isArray(host.props.items.value) ? host.props.items.value : [];
|
|
@@ -49,7 +55,7 @@ export function connect(host) {
|
|
|
49
55
|
const row = rowOf(event);
|
|
50
56
|
if (row && !row.hidden && row.getAttribute("aria-disabled") !== "true") host.dispatch("select", { index: Number(row.dataset.index), value: row.dataset.value ?? "" });
|
|
51
57
|
};
|
|
52
|
-
const
|
|
58
|
+
const onMousemove = (event) => {
|
|
53
59
|
const row = rowOf(event);
|
|
54
60
|
if (!row || row.hidden || row.getAttribute("aria-disabled") === "true") return;
|
|
55
61
|
const index = Number(row.dataset.index);
|
|
@@ -62,13 +68,14 @@ export function connect(host) {
|
|
|
62
68
|
observer.observe(element, { childList: true, subtree: true });
|
|
63
69
|
element.addEventListener("mousedown", onMousedown);
|
|
64
70
|
element.addEventListener("click", onClick);
|
|
65
|
-
element.addEventListener("
|
|
71
|
+
element.addEventListener("mousemove", onMousemove);
|
|
66
72
|
return () => {
|
|
73
|
+
cancelActiveItemReveal(element);
|
|
67
74
|
stop();
|
|
68
75
|
surface.destroy();
|
|
69
76
|
observer.disconnect();
|
|
70
77
|
element.removeEventListener("mousedown", onMousedown);
|
|
71
78
|
element.removeEventListener("click", onClick);
|
|
72
|
-
element.removeEventListener("
|
|
79
|
+
element.removeEventListener("mousemove", onMousemove);
|
|
73
80
|
};
|
|
74
81
|
}
|
|
@@ -74,7 +74,13 @@ export function connect(host) {
|
|
|
74
74
|
};
|
|
75
75
|
const observer = new MutationObserver(schedule);
|
|
76
76
|
observer.observe(element, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-describedby", "slot", "id", "role"] });
|
|
77
|
-
|
|
77
|
+
// Track props before deferred rewiring, even when no control is mounted yet.
|
|
78
|
+
const stop = host.effect(() => {
|
|
79
|
+
void host.props.disabled.value;
|
|
80
|
+
void host.props.required.value;
|
|
81
|
+
void host.props.invalid.value;
|
|
82
|
+
schedule();
|
|
83
|
+
});
|
|
78
84
|
wire();
|
|
79
85
|
return () => {
|
|
80
86
|
stop();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, revealActiveItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
let nextId = 0;
|
|
2
3
|
|
|
3
4
|
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
@@ -79,7 +80,7 @@ export function connect(host) {
|
|
|
79
80
|
if (index < 0 || options[index]?.disabled || !options[index]) return;
|
|
80
81
|
host.state.active = index;
|
|
81
82
|
render();
|
|
82
|
-
element.querySelector(`[data-index="${
|
|
83
|
+
revealActiveItem(element, () => element.querySelector(`[data-index="${host.state.active}"]`));
|
|
83
84
|
};
|
|
84
85
|
const choose = (index, trigger) => {
|
|
85
86
|
const option = options[index];
|
|
@@ -158,6 +159,7 @@ export function connect(host) {
|
|
|
158
159
|
form?.addEventListener("reset", onReset);
|
|
159
160
|
validity.addEventListener("invalid", onInvalid);
|
|
160
161
|
return () => {
|
|
162
|
+
cancelActiveItemReveal(element);
|
|
161
163
|
stop();
|
|
162
164
|
observer.disconnect();
|
|
163
165
|
authored.disabled = host.props.disabled.value;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, focusAndRevealItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
import { closeOverlay, createAnchoredSurface, createIdResolver, openOverlay, requestTopOverlayClose } from "../shared/overlay.js";
|
|
2
3
|
import { menuItemFrom, menuItems, navigateMenu } from "../shared/menu-navigation.js";
|
|
3
4
|
|
|
@@ -30,11 +31,12 @@ export function connect(host) {
|
|
|
30
31
|
else {
|
|
31
32
|
host.state.internalOpen = true;
|
|
32
33
|
host.dispatch("open", { open: true, reason: "action", trigger });
|
|
33
|
-
requestAnimationFrame(() => items()[0]
|
|
34
|
+
requestAnimationFrame(() => focusAndRevealItem(element, items()[0]));
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
37
|
const close = (reason, trigger) => {
|
|
37
38
|
if (!host.state.internalOpen) return;
|
|
39
|
+
cancelActiveItemReveal(element);
|
|
38
40
|
host.state.internalOpen = false;
|
|
39
41
|
host.dispatch("close", { open: false, reason, trigger });
|
|
40
42
|
if (reason === "escape") anchor?.focus();
|
|
@@ -102,6 +104,7 @@ export function connect(host) {
|
|
|
102
104
|
host.dispatch("select", { value, trigger });
|
|
103
105
|
if (host.props.inline.value) return;
|
|
104
106
|
host.dispatch("close", { open: false, reason: "action", trigger });
|
|
107
|
+
cancelActiveItemReveal(element);
|
|
105
108
|
host.state.internalOpen = false;
|
|
106
109
|
};
|
|
107
110
|
const onClick = (event) => select(menuItemFrom(event.target), triggerFor(event));
|
|
@@ -130,6 +133,7 @@ export function connect(host) {
|
|
|
130
133
|
const stop = host.effect(apply);
|
|
131
134
|
apply();
|
|
132
135
|
return () => {
|
|
136
|
+
cancelActiveItemReveal(element);
|
|
133
137
|
stop();
|
|
134
138
|
ids.stop();
|
|
135
139
|
element.removeEventListener("click", onClick);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<link rel="component" href="../ui-button/ui-button.html">
|
|
2
|
+
<link rel="component" href="../ui-icon/ui-icon.html">
|
|
3
|
+
<link rel="component" href="../ui-cluster/ui-cluster.html">
|
|
4
|
+
<template component="ui-pagination" module="./ui-pagination.js">
|
|
5
|
+
<defs>
|
|
6
|
+
<prop name="page" type="number" default="1"
|
|
7
|
+
>Current one-based page. The consumer updates it after a navigate request; activation never changes it internally.</prop>
|
|
8
|
+
<prop name="pages" type="list" default="[]">Visible numbered pages in ascending order. Omit for previous/next navigation without a known total. Gaps between
|
|
9
|
+
numbers show an ellipsis. Supply href to keep a numbered page usable as a native link before JavaScript.
|
|
10
|
+
<prop type="object"><prop name="value" type="number" required></prop><prop name="href" type="string"></prop></prop>
|
|
11
|
+
</prop>
|
|
12
|
+
<prop name="hasPrevious" type="boolean" default="false">Whether a preceding page is available. Previous is always unavailable on page one.</prop>
|
|
13
|
+
<prop name="hasNext" type="boolean" default="false">Whether another page is available. The component does not infer a total.</prop>
|
|
14
|
+
<prop name="previousHref" type="string" default="">Optional native destination for Previous. Empty renders a request button.</prop>
|
|
15
|
+
<prop name="nextHref" type="string" default="">Optional native destination for Next. Empty renders a request button.</prop>
|
|
16
|
+
<prop name="label" type="string" default="Results pages">Accessible name for the navigation landmark.</prop>
|
|
17
|
+
<prop name="previousLabel" type="string" default="Previous">Accessible label for the preceding page action.</prop>
|
|
18
|
+
<prop name="nextLabel" type="string" default="Next">Accessible label for the following page action.</prop>
|
|
19
|
+
<prop name="pageLabel" type="string" default="Page">Prefix for each numbered page's accessible name, followed by its localized number.</prop>
|
|
20
|
+
<prop name="size" type="keyword" values="sm, md, lg" default="sm"
|
|
21
|
+
>Uses Button's shared size scale. Narrow groups wrap without enlarging compact controls.</prop>
|
|
22
|
+
<prop name="disabled" type="boolean" default="false">Makes all page controls unavailable while keeping the current page visible.</prop>
|
|
23
|
+
<event name="navigate" type="object">
|
|
24
|
+
<prop name="page" type="number" required></prop><prop name="previousPage" type="number" required></prop><prop
|
|
25
|
+
name="trigger"
|
|
26
|
+
type="keyword"
|
|
27
|
+
values="keyboard, pointer, programmatic"
|
|
28
|
+
required
|
|
29
|
+
></prop>
|
|
30
|
+
</event>
|
|
31
|
+
</defs>
|
|
32
|
+
<nav from:aria-label="$label">
|
|
33
|
+
<ui-cluster gap="xs">
|
|
34
|
+
<ui-button
|
|
35
|
+
square
|
|
36
|
+
variant="ghost"
|
|
37
|
+
tone="neutral"
|
|
38
|
+
from:size="$size"
|
|
39
|
+
from:disabled="$disabled or not $hasPrevious or $page <= 1"
|
|
40
|
+
from:as="{ true: 'a', false: 'button' }[concat($previousHref != '')]"
|
|
41
|
+
from:href="$previousHref"
|
|
42
|
+
from:aria-label="$previousLabel"
|
|
43
|
+
from:data-pagination-page="$page - 1"
|
|
44
|
+
><ui-icon name="chevron-left"></ui-icon></ui-button>
|
|
45
|
+
<template $each="item, index of $pages" $key="$item.value">
|
|
46
|
+
<span $if="$index > 0 and $item.value > $pages[$index - 1].value + 1" aria-hidden="true">…</span>
|
|
47
|
+
<ui-button
|
|
48
|
+
square
|
|
49
|
+
from:variant="{ true: 'solid', false: 'ghost' }[concat($item.value = $page)]"
|
|
50
|
+
from:tone="{ true: 'accent', false: 'neutral' }[concat($item.value = $page)]"
|
|
51
|
+
from:size="$size"
|
|
52
|
+
from:disabled="$disabled"
|
|
53
|
+
from:as="{ true: 'a', false: 'button' }[concat(default($item.href, '') != '')]"
|
|
54
|
+
from:href="$item.href"
|
|
55
|
+
from:data-pagination-page="$item.value"
|
|
56
|
+
from:aria-label="concat($pageLabel, ' ', format($item.value, 'number'))"
|
|
57
|
+
from:aria-current="{ true: 'page', false: null }[concat($item.value = $page)]"
|
|
58
|
+
>{format($item.value, 'number')}</ui-button>
|
|
59
|
+
</template>
|
|
60
|
+
<ui-button
|
|
61
|
+
square
|
|
62
|
+
variant="ghost"
|
|
63
|
+
tone="neutral"
|
|
64
|
+
from:size="$size"
|
|
65
|
+
from:disabled="$disabled or not $hasNext"
|
|
66
|
+
from:as="{ true: 'a', false: 'button' }[concat($nextHref != '')]"
|
|
67
|
+
from:href="$nextHref"
|
|
68
|
+
from:aria-label="$nextLabel"
|
|
69
|
+
from:data-pagination-page="$page + 1"
|
|
70
|
+
><ui-icon name="chevron-right"></ui-icon></ui-button>
|
|
71
|
+
<slot></slot>
|
|
72
|
+
</ui-cluster>
|
|
73
|
+
</nav>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { trackTrigger } from "../shared/trigger.js";
|
|
2
|
+
|
|
3
|
+
/** Report page intent while leaving controlled state and native navigation with the consumer. */
|
|
4
|
+
export function connect(host) {
|
|
5
|
+
const [takeTrigger, stopTrigger] = trackTrigger(host);
|
|
6
|
+
const activate = (event) => {
|
|
7
|
+
const control = event.target.closest?.("[data-pagination-page]");
|
|
8
|
+
if (!control || !host.element.contains(control)) return;
|
|
9
|
+
const trigger = takeTrigger();
|
|
10
|
+
if (event.defaultPrevented || event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
11
|
+
if (host.props.disabled.value || control.matches(":disabled, [aria-disabled='true']")) return;
|
|
12
|
+
const page = Number(control.getAttribute("data-pagination-page"));
|
|
13
|
+
const previousPage = host.props.page.value;
|
|
14
|
+
if (!Number.isSafeInteger(page) || page < 1 || page === previousPage) return;
|
|
15
|
+
host.dispatch("navigate", { page, previousPage, trigger });
|
|
16
|
+
};
|
|
17
|
+
host.element.addEventListener("click", activate);
|
|
18
|
+
return () => { stopTrigger(); host.element.removeEventListener("click", activate); };
|
|
19
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelActiveItemReveal, focusAndRevealItem } from "../shared/active-item-reveal.js";
|
|
1
2
|
import { closeDialog, closeOverlay, openOverlay, showDialog } from "../shared/overlay.js";
|
|
2
3
|
import { trackTrigger } from "../shared/trigger.js";
|
|
3
4
|
|
|
@@ -19,6 +20,7 @@ export function connect(host) {
|
|
|
19
20
|
};
|
|
20
21
|
const closeNative = () => {
|
|
21
22
|
suppressNativeClose = true;
|
|
23
|
+
cancelActiveItemReveal(dialog);
|
|
22
24
|
closeDialog(dialog);
|
|
23
25
|
suppressNativeClose = false;
|
|
24
26
|
};
|
|
@@ -63,10 +65,10 @@ export function connect(host) {
|
|
|
63
65
|
const index = rows.indexOf(target.closest?.(".body button[aria-current]"));
|
|
64
66
|
if (!fromSearch && index < 0) return;
|
|
65
67
|
event.preventDefault();
|
|
66
|
-
if (event.key === "Home") rows[0]
|
|
67
|
-
else if (event.key === "End") rows.at(-1)
|
|
68
|
-
else if (event.key === "ArrowDown") rows[fromSearch ? 0 : (index + 1) % rows.length]
|
|
69
|
-
else rows[fromSearch ? rows.length - 1 : (index - 1 + rows.length) % rows.length]
|
|
68
|
+
if (event.key === "Home") focusAndRevealItem(dialog, rows[0]);
|
|
69
|
+
else if (event.key === "End") focusAndRevealItem(dialog, rows.at(-1));
|
|
70
|
+
else if (event.key === "ArrowDown") focusAndRevealItem(dialog, rows[fromSearch ? 0 : (index + 1) % rows.length]);
|
|
71
|
+
else focusAndRevealItem(dialog, rows[fromSearch ? rows.length - 1 : (index - 1 + rows.length) % rows.length]);
|
|
70
72
|
};
|
|
71
73
|
const onClick = (event) => {
|
|
72
74
|
if (host.props.dismissible.value && event.target === dialog) dispatchClose("light-dismiss", trigger());
|
|
@@ -83,6 +85,7 @@ export function connect(host) {
|
|
|
83
85
|
dialog.addEventListener("click", onClick);
|
|
84
86
|
dialog.addEventListener("close", onClose);
|
|
85
87
|
return () => {
|
|
88
|
+
cancelActiveItemReveal(dialog);
|
|
86
89
|
stop();
|
|
87
90
|
stopTracking();
|
|
88
91
|
dialog.removeEventListener("cancel", onCancel);
|