@leavepulse/ui 0.56.0 → 0.58.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/dist/chunks/{ContextMenuItems-BtKU7074.js → ContextMenuItems-BY5sTVQF.js} +1 -1
- package/dist/chunks/{LayoutCanvas-BkaGXTjx.js → LayoutCanvas-BcGLJVBq.js} +1 -1
- package/dist/chunks/{LayoutNode-B3_9E1Xa.js → LayoutNode-BkBn6dvM.js} +1 -1
- package/dist/chunks/{LpActionBar-C3hkVsFz.js → LpActionBar-DCN-_pZP.js} +2 -2
- package/dist/chunks/{LpAddressInput-NUHmplCd.js → LpAddressInput-Dio6Sqed.js} +7 -7
- package/dist/chunks/{LpAlert-B6ilrM7T.js → LpAlert-CXMn1GYE.js} +1 -1
- package/dist/chunks/{LpAppShell-BEGjSqFl.js → LpAppShell-Be6T96td.js} +3 -3
- package/dist/chunks/{LpAutocomplete-C8IgSMsi.js → LpAutocomplete-hJ_6CHlh.js} +1 -1
- package/dist/chunks/{LpAvatar-LTvy0OkE.js → LpAvatar-BxNs23Xu.js} +2 -2
- package/dist/chunks/{LpBreadcrumbs-D4TsoFju.js → LpBreadcrumbs-BLzOEgAF.js} +1 -1
- package/dist/chunks/{LpButton-BdfZFNqi.js → LpButton-vhlwmpBy.js} +1 -1
- package/dist/chunks/{LpCalendar-DrA2McTU.js → LpCalendar-BLoE8Qm7.js} +1 -1
- package/dist/chunks/{LpCard-DbjO5yaF.js → LpCard-DhR7u0d8.js} +1 -1
- package/dist/chunks/{LpCheckbox-DvVYeOD-.js → LpCheckbox-B_C8sOoT.js} +1 -1
- package/dist/chunks/{LpCodeBlock-ca7ANFyW.js → LpCodeBlock-CXsqJwDy.js} +1 -1
- package/dist/chunks/{LpColorPicker-D1sQqtXS.js → LpColorPicker-DQ88niXq.js} +2 -2
- package/dist/chunks/{LpConfirmDialog-CrbUZgJE.js → LpConfirmDialog-DxlSdjmk.js} +3 -3
- package/dist/chunks/{LpConfirmHost-BgMIV8r9.js → LpConfirmHost-BcrmTNu_.js} +1 -1
- package/dist/chunks/{LpContextMenu-EF_90u3g.js → LpContextMenu-B1NGfpp1.js} +2 -2
- package/dist/chunks/{LpDatePicker-BrdFtSTy.js → LpDatePicker-B-VMHMgy.js} +3 -3
- package/dist/chunks/{LpDateRangePicker-Drd7EbIz.js → LpDateRangePicker-B68KPKPl.js} +6 -15
- package/dist/chunks/{LpDisclosure-Cc23-JOT.js → LpDisclosure-Ct-VBHxt.js} +1 -1
- package/dist/chunks/{LpDrawer-CBFVD624.js → LpDrawer-DSknigxH.js} +5 -7
- package/dist/chunks/{LpDropdownMenu-DrpJJBp0.js → LpDropdownMenu-C7oHVlKY.js} +2 -2
- package/dist/chunks/{LpEmptyState-BJbZXC8D.js → LpEmptyState-CBwwkkdA.js} +1 -1
- package/dist/chunks/{LpFieldNotice-wtieLUTj.js → LpFieldNotice-9hH4ITZa.js} +1 -1
- package/dist/chunks/{LpFileTree-3E4w4Rjr.js → LpFileTree-CMA0_rsB.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-B4uB8Ks_.js → LpFileTreeNode-B4T258hF.js} +3 -3
- package/dist/chunks/{LpFormField-CtVYj6tO.js → LpFormField-BpQ6WJPe.js} +1 -1
- package/dist/chunks/LpHintLayer-B5-6FyEf.js +154 -0
- package/dist/chunks/LpHints-BqoZXHZk.js +109 -0
- package/dist/chunks/{LpIcon-DYtaid0K.js → LpIcon-B5guGKrW.js} +1 -1
- package/dist/chunks/{LpInput-D-YGQbEv.js → LpInput-WvyP02ID.js} +2 -2
- package/dist/chunks/{LpLightbox-EVXzAq62.js → LpLightbox-Dq-e36uT.js} +1 -1
- package/dist/chunks/{LpLogViewer-BfgtXVKm.js → LpLogViewer-DwiYE32z.js} +2 -2
- package/dist/chunks/{LpMapPicker-Cc84LaM3.js → LpMapPicker-BH5csaBt.js} +1 -1
- package/dist/chunks/{LpMediaGallery-BnlkN_1f.js → LpMediaGallery-B1kDypJ-.js} +2 -2
- package/dist/chunks/{LpModal-JNa3B_Lr.js → LpModal-BxYssnXg.js} +1 -1
- package/dist/chunks/{LpModalNav-DXBilXr0.js → LpModalNav-Bn2ws64b.js} +4 -4
- package/dist/chunks/{LpNotificationBell-DEL-RoHS.js → LpNotificationBell-DJfs4JjQ.js} +4 -4
- package/dist/chunks/{LpNumberField-B77O-z28.js → LpNumberField-CJPjmoDz.js} +1 -1
- package/dist/chunks/{LpPagination-BcDlo3r_.js → LpPagination-DehAaJLY.js} +1 -1
- package/dist/chunks/{LpPasswordInput-eKzQhbQP.js → LpPasswordInput-Bg5uTZbx.js} +2 -2
- package/dist/chunks/{LpPhoneInput-BCbrrhgE.js → LpPhoneInput-DStwcbnQ.js} +2 -2
- package/dist/chunks/{LpPie-B7ti5qCH.js → LpPie-DeTkbbOK.js} +3 -3
- package/dist/chunks/{LpRangeCalendar-DZuaRV0W.js → LpRangeCalendar-D5sDID9W.js} +1 -1
- package/dist/chunks/{LpSection-tKEH0jYf.js → LpSection-B19VPYmr.js} +1 -1
- package/dist/chunks/{LpSegmented-C09EEam2.js → LpSegmented-D3Eam8sa.js} +1 -1
- package/dist/chunks/{LpSelect-DNIL6MB9.js → LpSelect-DynjWK1A.js} +1 -1
- package/dist/chunks/{LpSidebar-C2oolgwy.js → LpSidebar-CshW0fqC.js} +3 -3
- package/dist/chunks/{LpSidebarNav-C9dHu-2b.js → LpSidebarNav-C9b3i9G2.js} +2 -2
- package/dist/chunks/{LpStat-B-7udBRP.js → LpStat-BS2TEXYK.js} +1 -1
- package/dist/chunks/{LpStepper-C7XiVKZj.js → LpStepper-BSglWF45.js} +1 -1
- package/dist/chunks/{LpTableFilter-CrW7_Rj4.js → LpTableFilter-CwHXVxJt.js} +2 -2
- package/dist/chunks/{LpTableOfContents-BUcAfGPQ.js → LpTableOfContents-DYuqkByy.js} +2 -8
- package/dist/chunks/{LpTabs-BMSBB78h.js → LpTabs-BBGRHY38.js} +1 -1
- package/dist/chunks/{LpTagsInput-D1SuqbhB.js → LpTagsInput-CHbQoAAA.js} +3 -3
- package/dist/chunks/{LpTextarea-uuScWo6-.js → LpTextarea-Bz2IWhTH.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-Fz85BCRS.js → LpThemeSwitcher-B_qhSl7q.js} +2 -2
- package/dist/chunks/{LpTimeField-Cqe7fXnh.js → LpTimeField-DvGXgdES.js} +1 -1
- package/dist/chunks/{LpToaster-DTtWuKvq.js → LpToaster-Bo9Q4QBP.js} +1 -1
- package/dist/chunks/{LpTooltip-CXVKcOXT.js → LpTooltip-suFCM34F.js} +2 -2
- package/dist/chunks/LpTour-D7m9ZxNk.js +334 -0
- package/dist/chunks/{LpUptimeBar-ugcv25xj.js → LpUptimeBar-B4ltCfno.js} +1 -1
- package/dist/chunks/{MenuTitle-J3ZDYjpV.js → MenuTitle-DQP6hc2N.js} +1 -1
- package/dist/chunks/TourCard-DCHOjr6e.js +100 -0
- package/dist/chunks/icons-QqBscG5I.js +43 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +2 -2
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +2 -2
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.d.ts +33 -0
- package/dist/components/LpHintLayer.vue.js +4 -0
- package/dist/components/LpHints.vue.d.ts +20 -0
- package/dist/components/LpHints.vue.js +4 -0
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +2 -2
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.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/LpNumberField.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteHeader.vue.js +2 -2
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.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/LpStat.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpTable.vue.js +6 -6
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTableOfContents.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTour.vue.d.ts +98 -0
- package/dist/components/LpTour.vue.js +4 -0
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/TourCard.vue.d.ts +28 -0
- package/dist/components/TourCard.vue.js +4 -0
- package/dist/components/tour.d.ts +21 -0
- package/dist/components/tour.js +38 -0
- package/dist/composables/scroll.d.ts +17 -0
- package/dist/composables/scroll.js +29 -0
- package/dist/composables/useGamepadNav.js +2 -7
- package/dist/composables/useHintSystem.d.ts +195 -0
- package/dist/composables/useHintSystem.js +334 -0
- package/dist/composables/useHints.d.ts +18 -0
- package/dist/composables/useHints.js +120 -0
- package/dist/composables/useMediaQuery.d.ts +2 -0
- package/dist/composables/useMediaQuery.js +18 -0
- package/dist/composables/usePaintSelect.js +1 -7
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +89 -64
- package/package.json +1 -1
- package/src/component-names.ts +3 -0
- package/src/components/LpDateRangePicker.vue +3 -12
- package/src/components/LpDrawer.vue +4 -7
- package/src/components/LpHintLayer.vue +148 -0
- package/src/components/LpHints.vue +116 -0
- package/src/components/LpTableOfContents.vue +2 -14
- package/src/components/LpTooltip.vue +2 -4
- package/src/components/LpTour.vue +358 -0
- package/src/components/TourCard.vue +41 -0
- package/src/components/tour.ts +57 -0
- package/src/composables/scroll.ts +54 -0
- package/src/composables/useGamepadNav.ts +2 -7
- package/src/composables/useHintSystem.ts +512 -0
- package/src/composables/useHints.ts +151 -0
- package/src/composables/useMediaQuery.ts +20 -0
- package/src/composables/usePaintSelect.ts +1 -8
- package/src/index.ts +27 -0
- package/src/kit-icons.ts +1 -1
- package/src/tokens/tokens.css +1 -0
- package/dist/chunks/icons-CkA0njzm.js +0 -43
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { Motion } from "motion-v"
|
|
13
13
|
import { computed, onBeforeUnmount, onMounted, ref, useId, watch } from "vue"
|
|
14
14
|
import { useLabels } from "../composables/useLpConfig"
|
|
15
|
+
import { scrollParent } from "../composables/scroll"
|
|
15
16
|
import { usePillTransition } from "../composables/usePillTransition"
|
|
16
17
|
|
|
17
18
|
export interface TocLink {
|
|
@@ -113,24 +114,11 @@ function unfreeze() {
|
|
|
113
114
|
safetyTimer = null
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
/*
|
|
117
|
-
* The element that actually scrolls to the heading. Inside an app shell that is
|
|
118
|
-
* a scroll region, not the window, and watching window.scrollY there saw no
|
|
119
|
-
* movement and unfroze on the first frame.
|
|
120
|
-
*/
|
|
121
|
-
function scrollerOf(el: HTMLElement): HTMLElement | null {
|
|
122
|
-
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
123
|
-
const { overflowY } = getComputedStyle(node)
|
|
124
|
-
if (/(auto|scroll|overlay)/.test(overflowY) && node.scrollHeight > node.clientHeight) return node
|
|
125
|
-
}
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
128
|
-
|
|
129
117
|
function suppressSpyUntilScrollEnds(target: HTMLElement) {
|
|
130
118
|
spySuppressed = true
|
|
131
119
|
if (rafId != null) cancelAnimationFrame(rafId)
|
|
132
120
|
if (safetyTimer != null) clearTimeout(safetyTimer)
|
|
133
|
-
const scroller =
|
|
121
|
+
const scroller = scrollParent(target)
|
|
134
122
|
const position = () => (scroller ? scroller.scrollTop : window.scrollY)
|
|
135
123
|
let last = position()
|
|
136
124
|
const tick = () => {
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
type SpringState,
|
|
43
43
|
} from "../composables/usePillTransition"
|
|
44
44
|
import { PANEL_SURFACE, TOOLTIP_ANIM } from "./dropdown"
|
|
45
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
45
46
|
|
|
46
47
|
const props = withDefaults(
|
|
47
48
|
defineProps<{
|
|
@@ -116,9 +117,6 @@ const shared = injectTooltipProviderContext(null)
|
|
|
116
117
|
*/
|
|
117
118
|
const PassThrough: FunctionalComponent = (_, { slots }) => slots.default?.()
|
|
118
119
|
|
|
119
|
-
const reducedMotion = () =>
|
|
120
|
-
typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
|
|
121
|
-
|
|
122
120
|
const target = { x: 0, y: 0 }
|
|
123
121
|
const offset = ref({ x: 0, y: 0 })
|
|
124
122
|
|
|
@@ -174,7 +172,7 @@ const onPointerMove = (e: PointerEvent) => {
|
|
|
174
172
|
const dir = props.side === "bottom" ? -1 : 1
|
|
175
173
|
target.y = e.clientY - (rect.top + rect.height / 2) + half * dir
|
|
176
174
|
// Without motion the tip goes straight to the reading instead of trailing.
|
|
177
|
-
if (
|
|
175
|
+
if (prefersReducedMotion()) {
|
|
178
176
|
offset.value = { x: target.x, y: target.y }
|
|
179
177
|
return
|
|
180
178
|
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface TourContext {
|
|
3
|
+
/** Undo something when this step is left, in either direction (close the menu it opened). */
|
|
4
|
+
onLeave(cleanup: () => void): void
|
|
5
|
+
/** Undo something when the tour ends, for what several steps share (a dialog they all live in). */
|
|
6
|
+
onEnd(cleanup: () => void): void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TourStep {
|
|
10
|
+
/**
|
|
11
|
+
* What to point at: a `v-hint` id, a CSS selector, an element or a getter.
|
|
12
|
+
* Left out, the step is a centred card with nothing highlighted (an intro).
|
|
13
|
+
* A target that never appears is skipped, so a step for a control the user
|
|
14
|
+
* cannot see costs nothing.
|
|
15
|
+
*/
|
|
16
|
+
target?: string | Element | (() => Element | null)
|
|
17
|
+
title: string
|
|
18
|
+
text?: string
|
|
19
|
+
/** A shortcut for the same action, shown as keycaps ("Ctrl+K"). */
|
|
20
|
+
keys?: string
|
|
21
|
+
side?: "top" | "right" | "bottom" | "left"
|
|
22
|
+
/**
|
|
23
|
+
* Ask the user to press the highlighted control themselves; the tour moves
|
|
24
|
+
* on when they do. Doing it beats reading about it.
|
|
25
|
+
*/
|
|
26
|
+
try?: boolean
|
|
27
|
+
/** With `try`, a "do it for me" button runs this instead. */
|
|
28
|
+
perform?: () => void | Promise<void>
|
|
29
|
+
/** Runs before the step shows: open the dialog or tab its target lives in. */
|
|
30
|
+
enter?: (context: TourContext) => void | Promise<void>
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
/*
|
|
36
|
+
* A guided walk over the real interface: the target is lit through a dim
|
|
37
|
+
* scrim, a card beside it says what it is, and the user steps through with the
|
|
38
|
+
* buttons or the arrow keys.
|
|
39
|
+
*
|
|
40
|
+
* The scrim is four panels around a hole rather than a mask, so the target
|
|
41
|
+
* stays clickable (a `try` step waits for exactly that click) and the hole can
|
|
42
|
+
* glide between targets on any engine, WebKitGTK included. The card is a reka
|
|
43
|
+
* popover anchored to the hole, so flipping and sliding to stay on screen is
|
|
44
|
+
* floating-ui's job; on a phone it becomes a sheet at the bottom instead.
|
|
45
|
+
*
|
|
46
|
+
* Whatever a step opens through `enter` it undoes through `onLeave` (gone when
|
|
47
|
+
* the step is) or `onEnd` (shared by the steps after it), and the tour runs
|
|
48
|
+
* those however it ends: finished, closed, Escape, or unmounted by a route
|
|
49
|
+
* change. A tour must leave the page as it found it.
|
|
50
|
+
*/
|
|
51
|
+
import { PopoverAnchor, PopoverContent, PopoverPortal, PopoverRoot } from "reka-ui"
|
|
52
|
+
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from "vue"
|
|
53
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
54
|
+
import { hints } from "../composables/useHints"
|
|
55
|
+
import { useHotkeys } from "../composables/useHotkeys"
|
|
56
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
57
|
+
import { useMediaQuery } from "../composables/useMediaQuery"
|
|
58
|
+
import { scrollToElement } from "../composables/scroll"
|
|
59
|
+
import { PANEL_SURFACE } from "./dropdown"
|
|
60
|
+
import TourCard from "./TourCard.vue"
|
|
61
|
+
import type { Box } from "../composables/spatialNav"
|
|
62
|
+
import { holeOf, panelsAround } from "./tour"
|
|
63
|
+
|
|
64
|
+
const props = withDefaults(
|
|
65
|
+
defineProps<{
|
|
66
|
+
steps: TourStep[]
|
|
67
|
+
/** Room around the target inside the lit hole, px. */
|
|
68
|
+
padding?: number
|
|
69
|
+
labels?: Partial<typeof DEFAULT_LABELS>
|
|
70
|
+
}>(),
|
|
71
|
+
{ padding: 6 },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const emit = defineEmits<{
|
|
75
|
+
/** The last step was passed. */
|
|
76
|
+
(e: "finish"): void
|
|
77
|
+
/** Closed early, on this step index. */
|
|
78
|
+
(e: "dismiss", index: number): void
|
|
79
|
+
}>()
|
|
80
|
+
|
|
81
|
+
const open = defineModel<boolean>("open", { default: false })
|
|
82
|
+
const index = defineModel<number>("step", { default: 0 })
|
|
83
|
+
|
|
84
|
+
const DEFAULT_LABELS = {
|
|
85
|
+
next: "Next",
|
|
86
|
+
back: "Back",
|
|
87
|
+
done: "Done",
|
|
88
|
+
close: "Close tour",
|
|
89
|
+
progress: "{n} of {total}",
|
|
90
|
+
perform: "Do it for me",
|
|
91
|
+
tryIt: "Try it: press the highlighted control.",
|
|
92
|
+
}
|
|
93
|
+
const labels = useLabels("tour", DEFAULT_LABELS, () => props.labels)
|
|
94
|
+
|
|
95
|
+
const WAIT_MS = 800
|
|
96
|
+
const TRY_SETTLE_MS = 120
|
|
97
|
+
|
|
98
|
+
const wide = useMediaQuery("(min-width: 640px)")
|
|
99
|
+
const step = computed<TourStep | undefined>(() => props.steps[index.value])
|
|
100
|
+
const target = shallowRef<Element | null>(null)
|
|
101
|
+
const box = ref<Box | null>(null)
|
|
102
|
+
const viewport = ref({ width: 0, height: 0 })
|
|
103
|
+
const ready = ref(false)
|
|
104
|
+
const cleanups: (() => void)[] = []
|
|
105
|
+
const leaving: (() => void)[] = []
|
|
106
|
+
let frame = 0
|
|
107
|
+
let token = 0
|
|
108
|
+
let direction = 1
|
|
109
|
+
|
|
110
|
+
const hole = computed(() => (box.value ? holeOf(box.value, props.padding, viewport.value) : null))
|
|
111
|
+
const panels = computed(() =>
|
|
112
|
+
hole.value
|
|
113
|
+
? panelsAround(hole.value, viewport.value)
|
|
114
|
+
: [{ x: 0, y: 0, width: viewport.value.width, height: viewport.value.height }],
|
|
115
|
+
)
|
|
116
|
+
const reference = {
|
|
117
|
+
getBoundingClientRect: () => {
|
|
118
|
+
const h = hole.value
|
|
119
|
+
return h ? new DOMRect(h.x, h.y, h.width, h.height) : new DOMRect(viewport.value.width / 2, viewport.value.height / 2, 0, 0)
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
const last = computed(() => index.value >= props.steps.length - 1)
|
|
123
|
+
const progress = computed(() => labels.value.progress.replace("{n}", String(index.value + 1)).replace("{total}", String(props.steps.length)))
|
|
124
|
+
const smooth = () => !prefersReducedMotion()
|
|
125
|
+
|
|
126
|
+
function resolve(raw: TourStep["target"]): Element | null {
|
|
127
|
+
if (!raw) return null
|
|
128
|
+
try {
|
|
129
|
+
if (typeof raw === "function") return raw()
|
|
130
|
+
if (typeof raw !== "string") return raw.isConnected ? raw : null
|
|
131
|
+
return hints.element(raw) ?? document.querySelector(raw)
|
|
132
|
+
} catch {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function waitFor(raw: TourStep["target"], mine: number): Promise<Element | null> {
|
|
138
|
+
const started = performance.now()
|
|
139
|
+
return new Promise((done) => {
|
|
140
|
+
const tick = () => {
|
|
141
|
+
if (mine !== token) return done(null)
|
|
142
|
+
const el = resolve(raw)
|
|
143
|
+
if (el && el.getClientRects().length) return done(el)
|
|
144
|
+
if (performance.now() - started > WAIT_MS) return done(null)
|
|
145
|
+
requestAnimationFrame(tick)
|
|
146
|
+
}
|
|
147
|
+
tick()
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function measure() {
|
|
152
|
+
viewport.value = { width: window.innerWidth, height: window.innerHeight }
|
|
153
|
+
const el = target.value
|
|
154
|
+
if (!el || !el.isConnected) {
|
|
155
|
+
box.value = null
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const r = el.getBoundingClientRect()
|
|
159
|
+
const prev = box.value
|
|
160
|
+
if (!prev || prev.x !== r.x || prev.y !== r.y || prev.width !== r.width || prev.height !== r.height) {
|
|
161
|
+
box.value = { x: r.x, y: r.y, width: r.width, height: r.height }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function track() {
|
|
166
|
+
measure()
|
|
167
|
+
frame = requestAnimationFrame(track)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function show(): Promise<void> {
|
|
171
|
+
const mine = ++token
|
|
172
|
+
ready.value = false
|
|
173
|
+
leave()
|
|
174
|
+
const current = step.value
|
|
175
|
+
if (!current) return
|
|
176
|
+
try {
|
|
177
|
+
await current.enter?.({ onLeave: (fn) => leaving.push(fn), onEnd: (fn) => cleanups.push(fn) })
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.warn("[LpTour] step skipped, its enter() failed:", error)
|
|
180
|
+
if (mine === token) skip()
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
if (mine !== token) return
|
|
184
|
+
if (!current.target) {
|
|
185
|
+
target.value = null
|
|
186
|
+
measure()
|
|
187
|
+
ready.value = true
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const el = current.enter ? await waitFor(current.target, mine) : resolve(current.target)
|
|
191
|
+
if (mine !== token) return
|
|
192
|
+
if (!el || !el.getClientRects().length) return skip()
|
|
193
|
+
target.value = el
|
|
194
|
+
scrollToElement(el, { block: wide.value ? "nearest" : "center", behavior: smooth() ? "smooth" : "auto", margin: 48 })
|
|
195
|
+
await nextTick()
|
|
196
|
+
measure()
|
|
197
|
+
ready.value = true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function skip(): Promise<void> | void {
|
|
201
|
+
const nextIndex = index.value + direction
|
|
202
|
+
if (nextIndex < 0 || nextIndex >= props.steps.length) return direction > 0 ? finish() : show()
|
|
203
|
+
index.value = nextIndex
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function next(): void {
|
|
207
|
+
direction = 1
|
|
208
|
+
if (last.value) return finish()
|
|
209
|
+
index.value += 1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function back() {
|
|
213
|
+
if (index.value === 0) return
|
|
214
|
+
direction = -1
|
|
215
|
+
index.value -= 1
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function forward() {
|
|
219
|
+
if (!step.value?.try) next()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function perform() {
|
|
223
|
+
await step.value?.perform?.()
|
|
224
|
+
setTimeout(next, TRY_SETTLE_MS)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function runAll(list: (() => void)[]) {
|
|
228
|
+
for (const cleanup of list.splice(0).reverse()) {
|
|
229
|
+
try {
|
|
230
|
+
cleanup()
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.warn("[LpTour] cleanup failed:", error)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function leave() {
|
|
238
|
+
runAll(leaving)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function end() {
|
|
242
|
+
token++
|
|
243
|
+
leave()
|
|
244
|
+
cancelAnimationFrame(frame)
|
|
245
|
+
frame = 0
|
|
246
|
+
target.value = null
|
|
247
|
+
box.value = null
|
|
248
|
+
runAll(cleanups)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function finish(): void {
|
|
252
|
+
open.value = false
|
|
253
|
+
emit("finish")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function dismiss() {
|
|
257
|
+
const at = index.value
|
|
258
|
+
open.value = false
|
|
259
|
+
emit("dismiss", at)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function onTargetClick(event: Event) {
|
|
263
|
+
if (!step.value?.try || !target.value) return
|
|
264
|
+
if (event.target instanceof Node && target.value.contains(event.target)) setTimeout(next, TRY_SETTLE_MS)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
watch(
|
|
268
|
+
open,
|
|
269
|
+
(value) => {
|
|
270
|
+
if (value) {
|
|
271
|
+
direction = 1
|
|
272
|
+
document.addEventListener("click", onTargetClick, true)
|
|
273
|
+
frame ||= requestAnimationFrame(track)
|
|
274
|
+
void show()
|
|
275
|
+
} else {
|
|
276
|
+
document.removeEventListener("click", onTargetClick, true)
|
|
277
|
+
end()
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{ immediate: true },
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
watch(index, () => {
|
|
284
|
+
if (open.value) void show()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
onBeforeUnmount(() => {
|
|
288
|
+
document.removeEventListener("click", onTargetClick, true)
|
|
289
|
+
if (open.value) end()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
useHotkeys(
|
|
293
|
+
() => [
|
|
294
|
+
{ key: "Escape", handler: dismiss, overLayers: true, allowInInput: true },
|
|
295
|
+
{ key: "ArrowRight", handler: forward, overLayers: true },
|
|
296
|
+
{ key: "Enter", handler: forward, overLayers: true },
|
|
297
|
+
{ key: "ArrowLeft", handler: back, overLayers: true },
|
|
298
|
+
],
|
|
299
|
+
{ enabled: open, priority: 100 },
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
const CARD = `${PANEL_SURFACE} rounded-card w-80 max-w-[calc(100vw-24px)] p-4 text-sm text-ink outline-none`
|
|
303
|
+
</script>
|
|
304
|
+
|
|
305
|
+
<template>
|
|
306
|
+
<Teleport to="body">
|
|
307
|
+
<div v-if="open" class="lp-tour pointer-events-none fixed inset-0 z-(--z-tour)" @pointerdown.stop>
|
|
308
|
+
<div
|
|
309
|
+
v-for="(panel, i) in panels"
|
|
310
|
+
:key="i"
|
|
311
|
+
class="lp-scrim pointer-events-auto absolute left-0 top-0 transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
|
|
312
|
+
:style="{ transform: `translate(${panel.x}px, ${panel.y}px)`, width: `${panel.width}px`, height: `${panel.height}px` }"
|
|
313
|
+
/>
|
|
314
|
+
<div
|
|
315
|
+
v-if="hole"
|
|
316
|
+
class="absolute left-0 top-0 rounded-control ring-2 ring-brand transition-[transform,width,height] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
|
|
317
|
+
:class="step?.try ? 'pointer-events-none animate-pulse' : 'pointer-events-auto'"
|
|
318
|
+
:style="{ transform: `translate(${hole.x}px, ${hole.y}px)`, width: `${hole.width}px`, height: `${hole.height}px` }"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<PopoverRoot v-if="open && wide" :open="ready">
|
|
323
|
+
<PopoverAnchor :reference="reference" />
|
|
324
|
+
<PopoverPortal>
|
|
325
|
+
<PopoverContent
|
|
326
|
+
:side="step?.side ?? 'bottom'"
|
|
327
|
+
:side-offset="12"
|
|
328
|
+
:collision-padding="12"
|
|
329
|
+
update-position-strategy="always"
|
|
330
|
+
:trap-focus="false"
|
|
331
|
+
:class="[CARD, 'z-[calc(var(--z-tour)+1)] data-[state=open]:animate-[popover-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none']"
|
|
332
|
+
role="dialog"
|
|
333
|
+
:aria-label="step?.title"
|
|
334
|
+
@pointerdown.stop
|
|
335
|
+
@interact-outside.prevent
|
|
336
|
+
@escape-key-down.prevent
|
|
337
|
+
@focus-outside.prevent
|
|
338
|
+
>
|
|
339
|
+
<slot name="card" :step="step" :index="index" :next="next" :back="back" :dismiss="dismiss">
|
|
340
|
+
<TourCard :step="step" :index="index" :last="last" :progress="progress" :labels="labels" @next="next" @back="back" @perform="perform" @dismiss="dismiss" />
|
|
341
|
+
</slot>
|
|
342
|
+
</PopoverContent>
|
|
343
|
+
</PopoverPortal>
|
|
344
|
+
</PopoverRoot>
|
|
345
|
+
|
|
346
|
+
<div
|
|
347
|
+
v-if="open && !wide && ready"
|
|
348
|
+
:class="[CARD, 'pointer-events-auto fixed inset-x-3 bottom-3 z-[calc(var(--z-tour)+1)] w-auto animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none']"
|
|
349
|
+
role="dialog"
|
|
350
|
+
:aria-label="step?.title"
|
|
351
|
+
@pointerdown.stop
|
|
352
|
+
>
|
|
353
|
+
<slot name="card" :step="step" :index="index" :next="next" :back="back" :dismiss="dismiss">
|
|
354
|
+
<TourCard :step="step" :index="index" :last="last" :progress="progress" :labels="labels" @next="next" @back="back" @perform="perform" @dismiss="dismiss" />
|
|
355
|
+
</slot>
|
|
356
|
+
</div>
|
|
357
|
+
</Teleport>
|
|
358
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TourStep } from "./LpTour.vue"
|
|
3
|
+
import LpButton from "./LpButton.vue"
|
|
4
|
+
import LpIcon from "./LpIcon.vue"
|
|
5
|
+
import LpKeys from "./LpKeys.vue"
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
step: TourStep | undefined
|
|
9
|
+
index: number
|
|
10
|
+
last: boolean
|
|
11
|
+
progress: string
|
|
12
|
+
labels: { next: string; back: string; done: string; close: string; perform: string; tryIt: string }
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
defineEmits<{
|
|
16
|
+
(e: "next"): void
|
|
17
|
+
(e: "back"): void
|
|
18
|
+
(e: "perform"): void
|
|
19
|
+
(e: "dismiss"): void
|
|
20
|
+
}>()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div class="flex items-start justify-between gap-3">
|
|
25
|
+
<p class="font-medium" aria-live="polite">{{ step?.title }}</p>
|
|
26
|
+
<button type="button" class="text-muted hover:text-ink" :aria-label="labels.close" @click="$emit('dismiss')">
|
|
27
|
+
<LpIcon name="lucide:x" class="size-4" />
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
<p v-if="step?.text" class="mt-1.5 text-muted">{{ step.text }}</p>
|
|
31
|
+
<LpKeys v-if="step?.keys" :combo="step.keys" size="sm" class="mt-2" />
|
|
32
|
+
<p v-if="step?.try" class="mt-2 text-xs text-brand">{{ labels.tryIt }}</p>
|
|
33
|
+
<div class="mt-4 flex items-center justify-between gap-2">
|
|
34
|
+
<span class="text-xs tabular-nums text-muted">{{ progress }}</span>
|
|
35
|
+
<div class="flex gap-2">
|
|
36
|
+
<LpButton v-if="index > 0" size="sm" variant="ghost" @click="$emit('back')">{{ labels.back }}</LpButton>
|
|
37
|
+
<LpButton v-if="step?.try && step.perform" size="sm" variant="outline" @click="$emit('perform')">{{ labels.perform }}</LpButton>
|
|
38
|
+
<LpButton size="sm" :variant="step?.try ? 'ghost' : undefined" @click="$emit('next')">{{ last ? labels.done : labels.next }}</LpButton>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Geometry for LpTour, kept out of the component so it can be tested without a
|
|
3
|
+
* DOM: where the spotlight hole goes and what the dim panels around it cover.
|
|
4
|
+
* Placing the step card is reka's popper (floating-ui), anchored to the hole.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Box } from "../composables/spatialNav"
|
|
8
|
+
|
|
9
|
+
type Size = { width: number; height: number }
|
|
10
|
+
|
|
11
|
+
/** The hole around a target: padded, then clipped to the viewport so a tall
|
|
12
|
+
* target never pushes the dim panels off-screen. */
|
|
13
|
+
export function holeOf(target: Box, padding: number, viewport: Size): Box {
|
|
14
|
+
const x = Math.max(0, target.x - padding)
|
|
15
|
+
const y = Math.max(0, target.y - padding)
|
|
16
|
+
const right = Math.min(viewport.width, target.x + target.width + padding)
|
|
17
|
+
const bottom = Math.min(viewport.height, target.y + target.height + padding)
|
|
18
|
+
return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The four dim panels around a hole: top, bottom, left, right. */
|
|
22
|
+
export function panelsAround(hole: Box, viewport: Size): Box[] {
|
|
23
|
+
const bottom = hole.y + hole.height
|
|
24
|
+
return [
|
|
25
|
+
{ x: 0, y: 0, width: viewport.width, height: hole.y },
|
|
26
|
+
{ x: 0, y: bottom, width: viewport.width, height: Math.max(0, viewport.height - bottom) },
|
|
27
|
+
{ x: 0, y: hole.y, width: hole.x, height: hole.height },
|
|
28
|
+
{ x: hole.x + hole.width, y: hole.y, width: Math.max(0, viewport.width - hole.x - hole.width), height: hole.height },
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Open the context menu of an element as a right-click would, at its centre.
|
|
34
|
+
* Lets a tour step show a row's menu without the user hunting for it; the menu
|
|
35
|
+
* itself is whatever the app already wires to that element.
|
|
36
|
+
*
|
|
37
|
+
* Returns the way to close it again: a press outside the menu, which every
|
|
38
|
+
* dismissable layer honours. Not Escape — that key also closes whatever is on
|
|
39
|
+
* top, a tour included, and would end the walk instead of the menu.
|
|
40
|
+
*/
|
|
41
|
+
export function openContextMenu(el: Element): () => void {
|
|
42
|
+
const box = el.getBoundingClientRect()
|
|
43
|
+
el.dispatchEvent(
|
|
44
|
+
new MouseEvent("contextmenu", {
|
|
45
|
+
bubbles: true,
|
|
46
|
+
cancelable: true,
|
|
47
|
+
button: 2,
|
|
48
|
+
clientX: box.left + box.width / 2,
|
|
49
|
+
clientY: box.top + box.height / 2,
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
return () => {
|
|
53
|
+
const outside = { bubbles: true, cancelable: true, clientX: 0, clientY: 0 }
|
|
54
|
+
document.body.dispatchEvent(new PointerEvent("pointerdown", outside))
|
|
55
|
+
document.body.dispatchEvent(new PointerEvent("pointerup", outside))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The element that actually scrolls, and scrolling it to show a child. Inside an
|
|
3
|
+
* app shell that is a scroll region, not the window, and `scrollIntoView` moves
|
|
4
|
+
* every scrollable ancestor at once, including `overflow: hidden` wrappers that
|
|
5
|
+
* then sit shifted with no way back for the user.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SCROLLS = /(auto|scroll|overlay)/
|
|
9
|
+
|
|
10
|
+
export interface ScrollParentOptions {
|
|
11
|
+
/** Which overflow counts: vertical (default), horizontal, or either. */
|
|
12
|
+
axis?: "y" | "x" | "both"
|
|
13
|
+
/** Start the search at the element itself rather than its parent. */
|
|
14
|
+
self?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The nearest ancestor that scrolls along `axis` and has something to scroll, or null. */
|
|
18
|
+
export function scrollParent(el: Element | null | undefined, { axis = "y", self = false }: ScrollParentOptions = {}): HTMLElement | null {
|
|
19
|
+
for (let node = self ? (el as HTMLElement | null | undefined) : el?.parentElement; node; node = node.parentElement) {
|
|
20
|
+
if (node === document.body || node === document.documentElement) return null
|
|
21
|
+
const style = getComputedStyle(node)
|
|
22
|
+
const y = axis !== "x" && SCROLLS.test(style.overflowY) && node.scrollHeight > node.clientHeight
|
|
23
|
+
const x = axis !== "y" && SCROLLS.test(style.overflowX) && node.scrollWidth > node.clientWidth
|
|
24
|
+
if (y || x) return node
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ScrollToElementOptions {
|
|
30
|
+
/** `nearest` scrolls only when the element is out of view. */
|
|
31
|
+
block?: "nearest" | "center" | "start"
|
|
32
|
+
behavior?: ScrollBehavior
|
|
33
|
+
/** Room kept between the element and the scroller's edge. */
|
|
34
|
+
margin?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Scroll the nearest scroller, and only it, so that `el` is in view. */
|
|
38
|
+
export function scrollToElement(
|
|
39
|
+
el: Element | null | undefined,
|
|
40
|
+
{ block = "nearest", behavior = "auto", margin = 0 }: ScrollToElementOptions = {},
|
|
41
|
+
): void {
|
|
42
|
+
if (!el) return
|
|
43
|
+
const box = scrollParent(el) ?? (document.scrollingElement as HTMLElement | null)
|
|
44
|
+
if (!box) return
|
|
45
|
+
const outer = box === document.scrollingElement ? new DOMRect(0, 0, window.innerWidth, window.innerHeight) : box.getBoundingClientRect()
|
|
46
|
+
const inner = el.getBoundingClientRect()
|
|
47
|
+
const top = inner.top - outer.top + box.scrollTop
|
|
48
|
+
let target = box.scrollTop
|
|
49
|
+
if (block === "start") target = top - margin
|
|
50
|
+
else if (block === "center") target = top - (box.clientHeight - inner.height) / 2
|
|
51
|
+
else if (inner.top < outer.top + margin) target = top - margin
|
|
52
|
+
else if (inner.bottom > outer.bottom - margin) target = top - box.clientHeight + inner.height + margin
|
|
53
|
+
if (target !== box.scrollTop) box.scrollTo({ top: target, behavior })
|
|
54
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { onBeforeUnmount, onMounted, ref, type MaybeRefOrGetter, toValue } from "vue"
|
|
2
2
|
import { focusableIn, nextInDirection, type Direction } from "./spatialNav"
|
|
3
|
+
import { scrollParent } from "./scroll"
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
* A game controller driving the interface: the D-pad and the left stick move
|
|
@@ -124,13 +125,7 @@ export function useGamepadNav(options: UseGamepadNavOptions = {}) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function scrollBy(dx: number, dy: number) {
|
|
127
|
-
|
|
128
|
-
while (element && element !== document.body) {
|
|
129
|
-
const style = getComputedStyle(element)
|
|
130
|
-
if (/(auto|scroll)/.test(style.overflowY + style.overflowX) && element.scrollHeight > element.clientHeight) break
|
|
131
|
-
element = element.parentElement
|
|
132
|
-
}
|
|
133
|
-
;(element && element !== document.body ? element : document.scrollingElement)?.scrollBy(dx, dy)
|
|
128
|
+
;(scrollParent(document.activeElement, { axis: "both", self: true }) ?? document.scrollingElement)?.scrollBy(dx, dy)
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
function poll(now: number) {
|