@leavepulse/ui 0.94.1 → 0.95.1
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/dist/assets/{LpTable-DxnthVz4.css → LpTable-BJjNEvgJ.css} +57 -30
- package/dist/chunks/{LpAddressInput-BCqR0zmv.js → LpAddressInput-C5VIgRp0.js} +1 -1
- package/dist/chunks/{LpAppShell-DlpQierx.js → LpAppShell-C4b3ySa_.js} +3 -3
- package/dist/chunks/{LpCodeBlock-BgWcg2mk.js → LpCodeBlock-DTU40x8P.js} +1 -1
- package/dist/chunks/{LpCodeDisplay-DBi1YPLp.js → LpCodeDisplay-F2hjLb0X.js} +2 -2
- package/dist/chunks/{LpConfirmDialog-y6k0ryWR.js → LpConfirmDialog-DrHcIxRn.js} +1 -1
- package/dist/chunks/{LpConfirmHost-D-X0tDym.js → LpConfirmHost-D0Cd8FA_.js} +1 -1
- package/dist/chunks/{LpCopyButton-BGzw95Xc.js → LpCopyButton-CiqUS4SX.js} +1 -1
- package/dist/chunks/{LpDataList-B56Gu6Yt.js → LpDataList-Cit9fOSp.js} +2 -2
- package/dist/chunks/{LpDrawer-BkWjFdV0.js → LpDrawer-BHlqi62K.js} +1 -1
- package/dist/chunks/{LpFileTree-BN_zybBN.js → LpFileTree-gVjF_b6r.js} +1 -1
- package/dist/chunks/{LpInspector-oTfkOdYd.js → LpInspector-DSL1lINx.js} +2 -2
- package/dist/chunks/{LpLightbox-DPgz7IsT.js → LpLightbox--EwvQGe-.js} +1 -1
- package/dist/chunks/{LpLogViewer-BuIzr_6h.js → LpLogViewer-DSq9e8mT.js} +1 -1
- package/dist/chunks/{LpMediaGallery-CTejKF2-.js → LpMediaGallery-DlYJf3DS.js} +1 -1
- package/dist/chunks/{LpModal-D1E7rQzw.js → LpModal-COMHrBKL.js} +1 -1
- package/dist/chunks/{LpModalNav-BFjiHWbr.js → LpModalNav-Cj02cfhL.js} +2 -2
- package/dist/chunks/{LpNotificationBell-KRgdRZs-.js → LpNotificationBell-DNGijtmW.js} +1 -1
- package/dist/chunks/{LpNotificationFeed-CMGYU8c-.js → LpNotificationFeed-BoovbtfL.js} +3 -3
- package/dist/chunks/{LpPageScrollbar-C9sTeU-2.js → LpPageScrollbar-BniFeL2U.js} +5 -1
- package/dist/chunks/{LpPaletteTheme-CLwO-SDf.js → LpPaletteTheme-_FwkJO2o.js} +1 -1
- package/dist/chunks/{LpPasswordInput-LPVI7D3v.js → LpPasswordInput-BBDz2zyU.js} +1 -1
- package/dist/chunks/{LpPie-CsiBXe-e.js → LpPie-CtC4XdhC.js} +1 -1
- package/dist/chunks/{LpPipeline-DVcoSs--.js → LpPipeline-aCALaUY8.js} +1 -1
- package/dist/chunks/{LpRelativeTime-CN5bWD73.js → LpRelativeTime-9MHlbq6F.js} +1 -1
- package/dist/chunks/{LpScrollArea-CqvwfZs2.js → LpScrollArea-B_1KZOSS.js} +11 -1
- package/dist/chunks/{LpSecretField-BwrZ4TfT.js → LpSecretField-C9vwMFQ6.js} +4 -4
- package/dist/chunks/{LpSidebar-BKhVDsMt.js → LpSidebar-DPri9uyz.js} +3 -3
- package/dist/chunks/{LpSidebarNav-DbCap8z1.js → LpSidebarNav-CV1rU6La.js} +1 -1
- package/dist/chunks/{LpThemeStudio-BPPctXgd.js → LpThemeStudio-yiEdFJQL.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-B3c44xFH.js → LpThemeSwitcher-DKkCFaFk.js} +1 -1
- package/dist/chunks/{LpToaster-Cdx1tnHB.js → LpToaster-C7hHkGOA.js} +1 -1
- package/dist/chunks/LpTooltip-Bt1ZFViJ.js +333 -0
- package/dist/chunks/{LpTruncate-C_C8eiIE.js → LpTruncate-DEHWL1OA.js} +2 -2
- package/dist/chunks/{LpUptimeBar-Dt6SY_-m.js → LpUptimeBar-CXgzU5ym.js} +1 -1
- package/dist/chunks/{LpWorkspaceSwitcher-DCIiOQt_.js → LpWorkspaceSwitcher-CrUVSI7x.js} +1 -1
- package/dist/chunks/{RevealToggle-BsYxY6Oz.js → RevealToggle-D9Vs88kM.js} +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpCopyButton.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDock.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpInspector.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNotificationFeed.vue.js +1 -1
- package/dist/components/LpPageScrollbar.vue.js +1 -1
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpRelativeTime.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTable.vue.js +38 -12
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.d.ts +3 -3
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTruncate.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/tableFeatures.d.ts +1 -0
- package/dist/components/tableFeatures.js +25 -0
- package/dist/composables/useShift.js +28 -11
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -34
- package/package.json +1 -1
- package/src/components/LpPageScrollbar.vue +14 -1
- package/src/components/LpScrollArea.vue +18 -1
- package/src/components/LpTable.vue +76 -4
- package/src/components/LpTooltip.vue +153 -14
- package/src/components/tableFeatures.ts +36 -0
- package/src/composables/useShift.ts +45 -15
- package/src/index.ts +2 -0
- package/dist/chunks/LpTooltip-BKde8Xjv.js +0 -230
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
orderPinned,
|
|
40
40
|
pinOffsets,
|
|
41
41
|
presetHidden,
|
|
42
|
+
rowChanges,
|
|
42
43
|
rowWindow,
|
|
43
44
|
sortByKeys,
|
|
44
45
|
type ColumnPin,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
import { devWarn } from "../composables/devWarn"
|
|
49
50
|
import { startPointerDrag } from "../composables/pointerDrag"
|
|
50
51
|
import { isHTMLElement, nodeWindow } from "../composables/useView"
|
|
52
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
51
53
|
import LpButton from "./LpButton.vue"
|
|
52
54
|
import { downloadFile, fileStem, toCsv, type CsvOptions } from "./exportData"
|
|
53
55
|
import {
|
|
@@ -1708,6 +1710,47 @@ const roving = useRovingFocus(rovingItems, {
|
|
|
1708
1710
|
},
|
|
1709
1711
|
})
|
|
1710
1712
|
|
|
1713
|
+
/*
|
|
1714
|
+
* A change that brings in, takes out or reorders many rows at once — a filter
|
|
1715
|
+
* lifted from ten rows back to sixty, a page of new rows, a sort of a long
|
|
1716
|
+
* list — is not animated row by row. Each row eased on its own reads as noise
|
|
1717
|
+
* at that size, and following them is costly: TransitionGroup measures every
|
|
1718
|
+
* row before and after (a forced layout of all the new rows inside the click),
|
|
1719
|
+
* then sets an enter or move transition on each. The rows land at once and the
|
|
1720
|
+
* body eases in as one instead, a single opacity animation on the compositor.
|
|
1721
|
+
* A few rows leaving, arriving or moving (a filter that hides some, a row
|
|
1722
|
+
* deleted) keep their own motion, which is what lets the eye follow them.
|
|
1723
|
+
*
|
|
1724
|
+
* Counted by rowChanges: the rows that arrive, leave or change their order;
|
|
1725
|
+
* the ones that only slide aside to close a gap do not count.
|
|
1726
|
+
*/
|
|
1727
|
+
const ROW_MOTION_LIMIT = 12
|
|
1728
|
+
const rowsEl = shallowRef<HTMLElement | null>(null)
|
|
1729
|
+
function setRowsEl(el: unknown) {
|
|
1730
|
+
const node = (el as { $el?: HTMLElement } | null)?.$el ?? null
|
|
1731
|
+
rowsEl.value = node
|
|
1732
|
+
roving.setContainer(node)
|
|
1733
|
+
}
|
|
1734
|
+
const motionIds = computed(() => (headerless.value ? allEntries.value : viewEntries.value).map((entry) => entry.id))
|
|
1735
|
+
/* The rows on screen, written after each render: what the next change is measured against. */
|
|
1736
|
+
let shownIds: readonly string[] = []
|
|
1737
|
+
const bigSwap = computed(() => rowChanges(shownIds, motionIds.value) > ROW_MOTION_LIMIT)
|
|
1738
|
+
let settleFlip = false
|
|
1739
|
+
watch(
|
|
1740
|
+
motionIds,
|
|
1741
|
+
(ids) => {
|
|
1742
|
+
const eased = bigSwap.value && shownIds.length > 0 && ids.length > 0
|
|
1743
|
+
shownIds = ids
|
|
1744
|
+
const body = rowsEl.value
|
|
1745
|
+
if (!eased || !body || props.virtualRowHeight || prefersReducedMotion(nodeWindow(body))) return
|
|
1746
|
+
// Two names for one animation: switching between them restarts it without
|
|
1747
|
+
// the reflow that taking a class off and putting it back would need.
|
|
1748
|
+
settleFlip = !settleFlip
|
|
1749
|
+
body.dataset.lpSettle = settleFlip ? "a" : "b"
|
|
1750
|
+
},
|
|
1751
|
+
{ flush: "post", immediate: true },
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1711
1754
|
function onRowKeydown(event: KeyboardEvent) {
|
|
1712
1755
|
// Space ticks rather than activates when the table is selectable — the
|
|
1713
1756
|
// standard split for a multi-select list.
|
|
@@ -2088,10 +2131,11 @@ usePaletteCommands(() => {
|
|
|
2088
2131
|
<TransitionGroup
|
|
2089
2132
|
tag="tbody"
|
|
2090
2133
|
name="lp-row"
|
|
2091
|
-
:css="animating && !virtualRowHeight"
|
|
2134
|
+
:css="animating && !virtualRowHeight && !bigSwap"
|
|
2135
|
+
:move-class="bigSwap ? 'lp-row-still' : undefined"
|
|
2092
2136
|
:class="busy && displayRows.length ? 'opacity-60 transition-opacity' : 'transition-opacity'"
|
|
2093
2137
|
:aria-busy="busy || undefined"
|
|
2094
|
-
:ref="
|
|
2138
|
+
:ref="setRowsEl"
|
|
2095
2139
|
>
|
|
2096
2140
|
<!-- The first page on its way: rows in the shape of the columns rather
|
|
2097
2141
|
than a spinner in an empty box, so the table already has its height
|
|
@@ -2252,7 +2296,8 @@ usePaletteCommands(() => {
|
|
|
2252
2296
|
:key="activeLayout"
|
|
2253
2297
|
tag="div"
|
|
2254
2298
|
name="lp-row"
|
|
2255
|
-
:css="animating"
|
|
2299
|
+
:css="animating && !bigSwap"
|
|
2300
|
+
:move-class="bigSwap ? 'lp-row-still' : undefined"
|
|
2256
2301
|
v-bind="scrollAttrs"
|
|
2257
2302
|
class="min-h-0 transition-opacity"
|
|
2258
2303
|
:class="[
|
|
@@ -2263,7 +2308,7 @@ usePaletteCommands(() => {
|
|
|
2263
2308
|
]"
|
|
2264
2309
|
:style="activeLayout === 'cards' ? { gridTemplateColumns: `repeat(auto-fill, minmax(min(${cardWidth}, 100%), 1fr))` } : undefined"
|
|
2265
2310
|
:aria-busy="busy || undefined"
|
|
2266
|
-
:ref="
|
|
2311
|
+
:ref="setRowsEl"
|
|
2267
2312
|
>
|
|
2268
2313
|
<template v-if="displayRows.length === 0 && busy">
|
|
2269
2314
|
<div
|
|
@@ -2501,6 +2546,30 @@ usePaletteCommands(() => {
|
|
|
2501
2546
|
transition: transform var(--duration-medium) var(--ease-emphasized);
|
|
2502
2547
|
}
|
|
2503
2548
|
|
|
2549
|
+
/*
|
|
2550
|
+
* A change too big to animate row by row (see ROW_MOTION_LIMIT): the rows
|
|
2551
|
+
* land at once and the body comes up from half-faded as one. `lp-row-still`
|
|
2552
|
+
* is the move class then, and has no transition on purpose: TransitionGroup
|
|
2553
|
+
* checks the move class for one before it measures the rows, and skips the
|
|
2554
|
+
* measuring when there is none.
|
|
2555
|
+
*/
|
|
2556
|
+
[data-lp-settle="a"] {
|
|
2557
|
+
animation: lp-rows-settle-a var(--duration-fast) var(--ease-emphasized);
|
|
2558
|
+
}
|
|
2559
|
+
[data-lp-settle="b"] {
|
|
2560
|
+
animation: lp-rows-settle-b var(--duration-fast) var(--ease-emphasized);
|
|
2561
|
+
}
|
|
2562
|
+
@keyframes lp-rows-settle-a {
|
|
2563
|
+
from {
|
|
2564
|
+
opacity: 0.4;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
@keyframes lp-rows-settle-b {
|
|
2568
|
+
from {
|
|
2569
|
+
opacity: 0.4;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2504
2573
|
.lp-row-enter-from,
|
|
2505
2574
|
.lp-row-leave-to {
|
|
2506
2575
|
opacity: 0;
|
|
@@ -2624,5 +2693,8 @@ tr:hover .lp-tick-cue,
|
|
|
2624
2693
|
.lp-tick {
|
|
2625
2694
|
transition: none;
|
|
2626
2695
|
}
|
|
2696
|
+
[data-lp-settle] {
|
|
2697
|
+
animation: none;
|
|
2698
|
+
}
|
|
2627
2699
|
}
|
|
2628
2700
|
</style>
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
* alone could not guarantee. reka opens the tip on keyboard focus as well as
|
|
24
24
|
* hover and closes it on Escape, satisfying WCAG 1.4.13.
|
|
25
25
|
*
|
|
26
|
+
* Until it is first wanted only the trigger is rendered: the reka tooltip (its
|
|
27
|
+
* provider, root, popper and portal) is stood up on the first pointer over the
|
|
28
|
+
* trigger, the first focus on it or a touch hold, and stays from then on. A
|
|
29
|
+
* table row carries two or three tips, nearly all of them never hovered, and
|
|
30
|
+
* fifty rows of full tooltips were most of the cost of showing the rows. The
|
|
31
|
+
* trigger's element is the same one before and after, so a ref to it, its
|
|
32
|
+
* focus and its hover survive the switch; the label is on it from the start,
|
|
33
|
+
* and the description arrives with the tip, as it always did.
|
|
34
|
+
*
|
|
26
35
|
* Do NOT wrap the `#trigger` of LpDropdownMenu, LpPopover or LpContextMenu:
|
|
27
36
|
* those bind their handlers to the slot's direct child, so a wrapper swallows
|
|
28
37
|
* them and the control stops opening. Put `aria-label` on the trigger instead.
|
|
@@ -30,13 +39,28 @@
|
|
|
30
39
|
import { clamp } from "../composables/clamp"
|
|
31
40
|
import {
|
|
32
41
|
injectTooltipProviderContext,
|
|
42
|
+
injectTooltipRootContext,
|
|
43
|
+
Slot,
|
|
33
44
|
TooltipContent,
|
|
34
45
|
TooltipPortal,
|
|
35
46
|
TooltipProvider,
|
|
36
47
|
TooltipRoot,
|
|
37
48
|
TooltipTrigger,
|
|
38
49
|
} from "reka-ui"
|
|
39
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
computed,
|
|
52
|
+
defineComponent,
|
|
53
|
+
mergeProps,
|
|
54
|
+
onBeforeUnmount,
|
|
55
|
+
onMounted,
|
|
56
|
+
onUpdated,
|
|
57
|
+
ref,
|
|
58
|
+
shallowRef,
|
|
59
|
+
useAttrs,
|
|
60
|
+
watch,
|
|
61
|
+
type ComponentPublicInstance,
|
|
62
|
+
type FunctionalComponent,
|
|
63
|
+
} from "vue"
|
|
40
64
|
import {
|
|
41
65
|
springSettled,
|
|
42
66
|
springStep,
|
|
@@ -263,6 +287,7 @@ const hold = useTouchHold({
|
|
|
263
287
|
enabled: () => props.touch === "hold" && !props.followCursor && !props.disabled,
|
|
264
288
|
delay: () => HOLD_MS,
|
|
265
289
|
onHold: () => {
|
|
290
|
+
armed.value = true
|
|
266
291
|
heldOpen.value = true
|
|
267
292
|
wanted.value = true
|
|
268
293
|
open.value = true
|
|
@@ -354,11 +379,132 @@ const guardOutside = useOutsideGuard()
|
|
|
354
379
|
defineOptions({ inheritAttrs: false })
|
|
355
380
|
// The caller's own listeners on the trigger run beside the hold's, not instead of them.
|
|
356
381
|
const attrs = useAttrs()
|
|
357
|
-
|
|
382
|
+
|
|
383
|
+
/*
|
|
384
|
+
* The tip is mounted on demand (see the header). The trigger is drawn by this
|
|
385
|
+
* component itself rather than by reka's TooltipTrigger, so it is the same
|
|
386
|
+
* element before and after: TooltipTrigger needs the root around it, and
|
|
387
|
+
* putting the root in later would have rebuilt the trigger under the pointer
|
|
388
|
+
* or the focus. What TooltipTrigger does with its events is done here instead,
|
|
389
|
+
* through the root's own context once it exists; reka's TooltipTrigger is
|
|
390
|
+
* still mounted with the tip, as the anchor it is placed against.
|
|
391
|
+
*/
|
|
392
|
+
const armed = ref(false)
|
|
393
|
+
type RootContext = ReturnType<typeof injectTooltipRootContext>
|
|
394
|
+
type ProviderContext = ReturnType<typeof injectTooltipProviderContext>
|
|
395
|
+
const root = shallowRef<RootContext | null>(null)
|
|
396
|
+
let provider: ProviderContext | null = null
|
|
397
|
+
|
|
398
|
+
const triggerRef = ref<ComponentPublicInstance | null>(null)
|
|
399
|
+
const triggerEl = shallowRef<HTMLElement | undefined>()
|
|
400
|
+
function readTrigger() {
|
|
401
|
+
const el = triggerRef.value?.$el as Node | null | undefined
|
|
402
|
+
const next = el && el.nodeType === 1 ? (el as HTMLElement) : undefined
|
|
403
|
+
if (next === triggerEl.value) return
|
|
404
|
+
triggerEl.value = next
|
|
405
|
+
root.value?.onTriggerChange(next)
|
|
406
|
+
}
|
|
407
|
+
onMounted(readTrigger)
|
|
408
|
+
onUpdated(readTrigger)
|
|
409
|
+
|
|
410
|
+
/* What happened before the tip existed, replayed once it does. */
|
|
411
|
+
let hovering = false
|
|
412
|
+
let focusedBy: HTMLElement | null = null
|
|
413
|
+
let pointerDown = false
|
|
414
|
+
let pointerMoveOpened = false
|
|
415
|
+
|
|
416
|
+
const live = () => (root.value && !root.value.disabled.value ? root.value : null)
|
|
417
|
+
|
|
418
|
+
function enter() {
|
|
419
|
+
const tip = live()
|
|
420
|
+
if (!tip || pointerMoveOpened || provider?.isPointerInTransitRef.value) return
|
|
421
|
+
tip.onTriggerEnter()
|
|
422
|
+
pointerMoveOpened = true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function focusOpen(target: HTMLElement) {
|
|
426
|
+
const tip = live()
|
|
427
|
+
if (!tip || pointerDown) return
|
|
428
|
+
if (tip.ignoreNonKeyboardFocus.value && !target.matches?.(":focus-visible")) return
|
|
429
|
+
tip.onOpen()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const triggerEvents = {
|
|
433
|
+
onPointerenter(event: PointerEvent) {
|
|
434
|
+
if (event.pointerType === "touch") return
|
|
435
|
+
hovering = true
|
|
436
|
+
armed.value = true
|
|
437
|
+
},
|
|
438
|
+
onPointermove(event: PointerEvent) {
|
|
439
|
+
onPointerMove(event)
|
|
440
|
+
if (event.pointerType === "touch") return
|
|
441
|
+
hovering = true
|
|
442
|
+
armed.value = true
|
|
443
|
+
enter()
|
|
444
|
+
},
|
|
445
|
+
onPointerleave() {
|
|
446
|
+
hovering = false
|
|
447
|
+
const tip = live()
|
|
448
|
+
if (!tip) return
|
|
449
|
+
tip.onTriggerLeave()
|
|
450
|
+
pointerMoveOpened = false
|
|
451
|
+
},
|
|
452
|
+
onPointerdown(event: PointerEvent) {
|
|
453
|
+
const tip = live()
|
|
454
|
+
if (tip?.open.value && !tip.disableClosingTrigger.value) tip.onClose()
|
|
455
|
+
pointerDown = true
|
|
456
|
+
const doc = (event.currentTarget as Node | null)?.ownerDocument ?? document
|
|
457
|
+
doc.addEventListener("pointerup", () => setTimeout(() => (pointerDown = false), 1), { once: true })
|
|
458
|
+
},
|
|
459
|
+
onFocus(event: FocusEvent) {
|
|
460
|
+
focusedBy = event.currentTarget as HTMLElement
|
|
461
|
+
armed.value = true
|
|
462
|
+
focusOpen(focusedBy)
|
|
463
|
+
},
|
|
464
|
+
onBlur() {
|
|
465
|
+
focusedBy = null
|
|
466
|
+
live()?.onClose()
|
|
467
|
+
},
|
|
468
|
+
onClick() {
|
|
469
|
+
const tip = live()
|
|
470
|
+
if (tip && !tip.disableClosingTrigger.value) tip.onClose()
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* Hands the root's context out and replays what the tip was armed by. */
|
|
475
|
+
const RootBridge = defineComponent({
|
|
476
|
+
setup() {
|
|
477
|
+
root.value = injectTooltipRootContext()
|
|
478
|
+
provider = injectTooltipProviderContext()
|
|
479
|
+
onMounted(() => {
|
|
480
|
+
// After reka's TooltipTrigger, which reports the element it renders (none).
|
|
481
|
+
root.value?.onTriggerChange(triggerEl.value)
|
|
482
|
+
if (hovering) enter()
|
|
483
|
+
if (focusedBy && focusedBy.ownerDocument.activeElement === focusedBy) focusOpen(focusedBy)
|
|
484
|
+
})
|
|
485
|
+
onBeforeUnmount(() => {
|
|
486
|
+
root.value = null
|
|
487
|
+
provider = null
|
|
488
|
+
})
|
|
489
|
+
return () => null
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
const triggerProps = computed(() =>
|
|
494
|
+
mergeProps(attrs, hold.handlers, triggerEvents, {
|
|
495
|
+
"aria-label": props.label ? props.content : undefined,
|
|
496
|
+
"aria-describedby": root.value?.open.value ? root.value.contentId : undefined,
|
|
497
|
+
"data-state": root.value?.stateAttribute.value ?? "closed",
|
|
498
|
+
"data-grace-area-trigger": "",
|
|
499
|
+
class: props.touch === "hold" && !props.followCursor ? "[-webkit-touch-callout:none]" : undefined,
|
|
500
|
+
}),
|
|
501
|
+
)
|
|
358
502
|
</script>
|
|
359
503
|
|
|
360
504
|
<template>
|
|
361
|
-
|
|
505
|
+
<!-- The caller's own element, with the trigger's props merged onto it. -->
|
|
506
|
+
<Slot ref="triggerRef" v-bind="triggerProps"><slot /></Slot>
|
|
507
|
+
<component v-if="armed" :is="shared ? PassThrough : TooltipProvider" v-bind="shared ? {} : { delayDuration: delay ?? 200 }">
|
|
362
508
|
<!--
|
|
363
509
|
`disable-closing-trigger` in follow mode: reka dismisses a tip when its
|
|
364
510
|
trigger is clicked, which is right for a button (the tip explained the
|
|
@@ -373,17 +519,10 @@ const triggerAttrs = computed(() => mergeProps(attrs, hold.handlers))
|
|
|
373
519
|
:disable-closing-trigger="followCursor"
|
|
374
520
|
@update:open="onOpenChange"
|
|
375
521
|
>
|
|
376
|
-
<!--
|
|
377
|
-
|
|
378
|
-
<TooltipTrigger
|
|
379
|
-
|
|
380
|
-
v-bind="triggerAttrs"
|
|
381
|
-
:aria-label="label ? content : undefined"
|
|
382
|
-
:class="touch === 'hold' && !followCursor ? '[-webkit-touch-callout:none]' : undefined"
|
|
383
|
-
@pointermove="onPointerMove"
|
|
384
|
-
>
|
|
385
|
-
<slot />
|
|
386
|
-
</TooltipTrigger>
|
|
522
|
+
<!-- Renders nothing: the anchor the tip is placed against, and the id
|
|
523
|
+
the trigger's aria-describedby points at. -->
|
|
524
|
+
<TooltipTrigger as-child :reference="triggerEl" />
|
|
525
|
+
<RootBridge />
|
|
387
526
|
<TooltipPortal :to="portalTo">
|
|
388
527
|
<TooltipContent
|
|
389
528
|
@interact-outside="guardOutside"
|
|
@@ -157,3 +157,39 @@ export function presetHidden(
|
|
|
157
157
|
* - `list`: one compact line per row with no header, for narrow places.
|
|
158
158
|
*/
|
|
159
159
|
export type TableLayout = "table" | "cards" | "list"
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* How many rows a change of the shown list moves on screen: the ones that
|
|
163
|
+
* arrive, the ones that leave, and the ones that change their order among the
|
|
164
|
+
* rows that stay. Rows that only slide up or down to close a gap or make room
|
|
165
|
+
* are not counted; they move with the others and say nothing new.
|
|
166
|
+
*
|
|
167
|
+
* The order changes are the survivors outside the longest run that kept its
|
|
168
|
+
* order (a longest increasing subsequence of their old positions), which is the
|
|
169
|
+
* fewest rows that had to be picked up and put elsewhere.
|
|
170
|
+
*/
|
|
171
|
+
export function rowChanges(before: readonly string[], after: readonly string[]): number {
|
|
172
|
+
const old = new Map<string, number>()
|
|
173
|
+
before.forEach((id, i) => old.set(id, i))
|
|
174
|
+
let entered = 0
|
|
175
|
+
const kept: number[] = []
|
|
176
|
+
for (const id of after) {
|
|
177
|
+
const at = old.get(id)
|
|
178
|
+
if (at === undefined) entered++
|
|
179
|
+
else kept.push(at)
|
|
180
|
+
}
|
|
181
|
+
const left = before.length - kept.length
|
|
182
|
+
// Patience sorting: tails[k] is the smallest end of an increasing run of k + 1.
|
|
183
|
+
const tails: number[] = []
|
|
184
|
+
for (const at of kept) {
|
|
185
|
+
let lo = 0
|
|
186
|
+
let hi = tails.length
|
|
187
|
+
while (lo < hi) {
|
|
188
|
+
const mid = (lo + hi) >> 1
|
|
189
|
+
if (tails[mid]! < at) lo = mid + 1
|
|
190
|
+
else hi = mid
|
|
191
|
+
}
|
|
192
|
+
tails[lo] = at
|
|
193
|
+
}
|
|
194
|
+
return entered + left + (kept.length - tails.length)
|
|
195
|
+
}
|
|
@@ -63,6 +63,33 @@ export interface UseShift {
|
|
|
63
63
|
retune: () => void
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
interface FirstRead {
|
|
67
|
+
read: () => void
|
|
68
|
+
write: () => void
|
|
69
|
+
}
|
|
70
|
+
const firstReads = new Map<Window, Set<FirstRead>>()
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* Runs in the next frame, every read of every box mounted meanwhile before any
|
|
74
|
+
* write: one layout for all of them, however many there are.
|
|
75
|
+
*/
|
|
76
|
+
function firstRead(win: Window, pass: FirstRead): () => void {
|
|
77
|
+
let queue = firstReads.get(win)
|
|
78
|
+
if (!queue) {
|
|
79
|
+
const batch = new Set<FirstRead>()
|
|
80
|
+
queue = batch
|
|
81
|
+
firstReads.set(win, batch)
|
|
82
|
+
win.requestAnimationFrame(() => {
|
|
83
|
+
firstReads.delete(win)
|
|
84
|
+
for (const p of batch) p.read()
|
|
85
|
+
for (const p of batch) p.write()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
queue.add(pass)
|
|
89
|
+
const batch = queue
|
|
90
|
+
return () => batch.delete(pass)
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
/** Kept in step with the `duration-fast` token the transition utilities use. */
|
|
67
94
|
const TWEEN_MS = 160
|
|
68
95
|
|
|
@@ -83,7 +110,7 @@ export function useShift(
|
|
|
83
110
|
|
|
84
111
|
let ro: ResizeObserver | undefined
|
|
85
112
|
let mo: MutationObserver | undefined
|
|
86
|
-
let
|
|
113
|
+
let cancelFirstRead: (() => void) | undefined
|
|
87
114
|
let settleTimer: ReturnType<typeof setTimeout> | undefined
|
|
88
115
|
/** Where the box last settled, so a retune has a size to ease FROM. */
|
|
89
116
|
let lastHeight = 0
|
|
@@ -259,7 +286,8 @@ export function useShift(
|
|
|
259
286
|
mo?.disconnect()
|
|
260
287
|
ro = undefined
|
|
261
288
|
mo = undefined
|
|
262
|
-
|
|
289
|
+
cancelFirstRead?.()
|
|
290
|
+
cancelFirstRead = undefined
|
|
263
291
|
clearTimeout(settleTimer)
|
|
264
292
|
tweening.value = false
|
|
265
293
|
resizing.value = false
|
|
@@ -277,21 +305,23 @@ export function useShift(
|
|
|
277
305
|
teardown()
|
|
278
306
|
if (!enabled || !node) return
|
|
279
307
|
|
|
280
|
-
lastHeight = node.offsetHeight
|
|
281
|
-
lastWidth = node.offsetWidth
|
|
282
|
-
laidOut = node.getClientRects().length > 0
|
|
283
308
|
scrollHost = node
|
|
284
309
|
node.addEventListener("scroll", rememberScroll, { capture: true, passive: true })
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
// The size a first `retune` eases from, read in the next frame together
|
|
311
|
+
// with every other box mounted in this render, and the transition armed
|
|
312
|
+
// then: a box must not animate in from its pre-content size. Read here,
|
|
313
|
+
// at mount, each box forced a layout of its own: fifty badges arriving
|
|
314
|
+
// in a table's rows laid the page out fifty times inside one click.
|
|
315
|
+
// Until the read, a retune finds no size to ease from and lets the box
|
|
316
|
+
// take its own, which is what a box that has just appeared should do.
|
|
317
|
+
cancelFirstRead = firstRead(nodeWindow(node), {
|
|
318
|
+
read: () => {
|
|
319
|
+
if (inFlight) return
|
|
320
|
+
lastHeight = node.offsetHeight
|
|
321
|
+
lastWidth = node.offsetWidth
|
|
322
|
+
laidOut = node.getClientRects().length > 0
|
|
323
|
+
},
|
|
324
|
+
write: () => (tweening.value = true),
|
|
295
325
|
})
|
|
296
326
|
|
|
297
327
|
// Two triggers, since either can move the size alone: the resize observer
|
package/src/index.ts
CHANGED
|
@@ -272,6 +272,8 @@ export type { SortState, TableColumn, TableListMode, TablePage, TableQuery } fro
|
|
|
272
272
|
export type { TableLabels } from "./components/LpTable.vue"
|
|
273
273
|
export type { TableCellEdit, TableExportOptions, TableSelectionToken } from "./components/LpTable.vue"
|
|
274
274
|
export type { ColumnPin, ColumnPreset, TableLayout } from "./components/tableFeatures"
|
|
275
|
+
export { rowWindow } from "./components/tableFeatures"
|
|
276
|
+
export type { RowWindow } from "./components/tableFeatures"
|
|
275
277
|
export { compareValues } from "./components/tableSort"
|
|
276
278
|
export { filterRows, showOnly, shownValues } from "./components/tableFilter"
|
|
277
279
|
export type { TableFilters } from "./components/tableFilter"
|