@hina-ui/vue 1.7.13 → 1.7.14
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/index.js +1295 -283
- package/dist/index.js.map +1 -1
- package/dist/shared/lib/thumbhash.ts +12 -0
- package/dist/shared/lib/tour-engine.ts +721 -0
- package/dist/shared/lib/tour.ts +202 -24
- package/dist/shared/locale/en-US.ts +1 -0
- package/dist/shared/locale/ja-JP.ts +1 -0
- package/dist/shared/locale/types.ts +1 -0
- package/dist/shared/locale/zh-CN.ts +1 -0
- package/dist/shared/primitives/dialog/index.ts +2 -2
- package/dist/shared/primitives/dismissable-layer/index.ts +48 -1
- package/dist/shared/primitives/focus-scope/index.ts +5 -3
- package/dist/shared/styles/semantic.css +1 -0
- package/dist/shared/styles/tour.css +107 -2
- package/dist/shared/variants/image.ts +28 -0
- package/dist/shared/variants/tour.ts +22 -4
- package/dist/types/shared/src/lib/thumbhash.d.ts +2 -0
- package/dist/types/shared/src/lib/thumbhash.d.ts.map +1 -0
- package/dist/types/shared/src/lib/tour-engine.d.ts +47 -0
- package/dist/types/shared/src/lib/tour-engine.d.ts.map +1 -0
- package/dist/types/shared/src/lib/tour.d.ts +65 -9
- package/dist/types/shared/src/lib/tour.d.ts.map +1 -1
- package/dist/types/shared/src/locale/en-US.d.ts.map +1 -1
- package/dist/types/shared/src/locale/ja-JP.d.ts.map +1 -1
- package/dist/types/shared/src/locale/types.d.ts +1 -0
- package/dist/types/shared/src/locale/types.d.ts.map +1 -1
- package/dist/types/shared/src/locale/zh-CN.d.ts.map +1 -1
- package/dist/types/shared/src/primitives/dialog/index.d.ts +1 -1
- package/dist/types/shared/src/primitives/dialog/index.d.ts.map +1 -1
- package/dist/types/shared/src/primitives/dismissable-layer/index.d.ts +1 -0
- package/dist/types/shared/src/primitives/dismissable-layer/index.d.ts.map +1 -1
- package/dist/types/shared/src/primitives/focus-scope/index.d.ts.map +1 -1
- package/dist/types/shared/src/variants/image.d.ts +35 -0
- package/dist/types/shared/src/variants/image.d.ts.map +1 -1
- package/dist/types/shared/src/variants/tour.d.ts +9 -4
- package/dist/types/shared/src/variants/tour.d.ts.map +1 -1
- package/dist/types/vue/src/components/button/Button.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/dialog/Dialog.vue.d.ts +3 -0
- package/dist/types/vue/src/components/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/drawer/Drawer.vue.d.ts +3 -0
- package/dist/types/vue/src/components/drawer/Drawer.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/image/Image.vue.d.ts +1 -0
- package/dist/types/vue/src/components/image/Image.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/image/composables/useImage.d.ts +2 -0
- package/dist/types/vue/src/components/image/composables/useImage.d.ts.map +1 -1
- package/dist/types/vue/src/components/overlay-branch/OverlayBranch.vue.d.ts +17 -0
- package/dist/types/vue/src/components/overlay-branch/OverlayBranch.vue.d.ts.map +1 -0
- package/dist/types/vue/src/components/sheet/Sheet.vue.d.ts +3 -0
- package/dist/types/vue/src/components/sheet/Sheet.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/tour/Tour.vue.d.ts +10 -1
- package/dist/types/vue/src/components/tour/Tour.vue.d.ts.map +1 -1
- package/dist/types/vue/src/components/tour/types.d.ts +9 -3
- package/dist/types/vue/src/components/tour/types.d.ts.map +1 -1
- package/dist/types/vue/src/index.d.ts +2 -1
- package/dist/types/vue/src/index.d.ts.map +1 -1
- package/dist/types/vue/src/primitives/dismissable-layer/DismissableLayerBranch.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/components/button/Button.vue +1 -1
- package/src/components/dialog/Dialog.vue +14 -1
- package/src/components/drawer/Drawer.vue +14 -1
- package/src/components/image/Image.vue +58 -24
- package/src/components/image/composables/useImage.ts +46 -21
- package/src/components/overlay-branch/OverlayBranch.vue +14 -0
- package/src/components/sheet/Sheet.vue +14 -1
- package/src/components/tour/Tour.vue +302 -141
- package/src/components/tour/types.ts +33 -2
- package/src/index.ts +11 -0
- package/src/primitives/dismissable-layer/DismissableLayerBranch.ts +9 -18
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
nextTick,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
onMounted,
|
|
7
|
+
ref,
|
|
8
|
+
shallowRef,
|
|
9
|
+
useId,
|
|
10
|
+
watch,
|
|
11
|
+
watchEffect,
|
|
12
|
+
} from 'vue'
|
|
13
|
+
import { RotateCcw } from '@lucide/vue'
|
|
3
14
|
import {
|
|
4
15
|
beaconPoint,
|
|
5
16
|
clampTourStep,
|
|
6
17
|
hideOutsideTour,
|
|
7
|
-
|
|
8
|
-
revealTourTarget,
|
|
18
|
+
listenTourKeys,
|
|
9
19
|
runTourChange,
|
|
10
20
|
spotlightMask,
|
|
11
|
-
spotlightRect,
|
|
12
21
|
spotlightShape,
|
|
13
22
|
TOUR_ARROW_HEIGHT,
|
|
14
23
|
TOUR_ARROW_PADDING,
|
|
15
24
|
TOUR_ARROW_PATH,
|
|
16
25
|
TOUR_ARROW_WIDTH,
|
|
17
|
-
TOUR_PADDING,
|
|
18
|
-
TOUR_RADIUS,
|
|
19
26
|
TOUR_SIDE_OFFSET,
|
|
20
27
|
tourBeaconOnArrival,
|
|
21
28
|
tourKeyAction,
|
|
29
|
+
tourOwnsInput,
|
|
30
|
+
tourCardGap,
|
|
31
|
+
tourPageBlocked,
|
|
22
32
|
tourReference,
|
|
23
33
|
tourTargetBlocked,
|
|
24
34
|
trapTourFocus,
|
|
25
|
-
|
|
35
|
+
type TourCloseReason,
|
|
36
|
+
type TourPointerKind,
|
|
26
37
|
type TourRect,
|
|
27
38
|
type TourSpotlight,
|
|
28
39
|
} from '../../../../shared/src/lib/tour'
|
|
40
|
+
import { createTourEngine } from '../../../../shared/src/lib/tour-engine'
|
|
41
|
+
import { addBranch } from '../../../../shared/src/primitives/dismissable-layer'
|
|
29
42
|
import { DismissableLayer } from '../../primitives/dismissable-layer'
|
|
30
43
|
import { PopperArrow, PopperContent, PopperRoot } from '../../primitives/popper'
|
|
31
44
|
import { Presence } from '../../primitives/presence'
|
|
@@ -37,6 +50,7 @@
|
|
|
37
50
|
import Button from '../button/Button.vue'
|
|
38
51
|
import Card from '../card/Card.vue'
|
|
39
52
|
import CloseButton from '../close-button/CloseButton.vue'
|
|
53
|
+
import IconButton from '../icon-button/IconButton.vue'
|
|
40
54
|
import Text from '../text/Text.vue'
|
|
41
55
|
import {
|
|
42
56
|
tourBeacon,
|
|
@@ -47,14 +61,30 @@
|
|
|
47
61
|
tourCentered,
|
|
48
62
|
tourFooter,
|
|
49
63
|
tourHeader,
|
|
64
|
+
tourKey,
|
|
65
|
+
tourKeyCap,
|
|
50
66
|
tourMask,
|
|
67
|
+
tourPointer,
|
|
68
|
+
tourPointerArrow,
|
|
69
|
+
tourPointerTouch,
|
|
51
70
|
} from './tour.variants'
|
|
52
71
|
import type { TourExpose, TourProps, TourSlotProps, TourStep } from './types'
|
|
53
72
|
|
|
54
73
|
defineOptions({ name: 'HnTour' })
|
|
55
74
|
|
|
56
|
-
const props = withDefaults(defineProps<TourProps>(), {
|
|
57
|
-
|
|
75
|
+
const props = withDefaults(defineProps<TourProps>(), {
|
|
76
|
+
mask: true,
|
|
77
|
+
blockTarget: false,
|
|
78
|
+
blockPage: false,
|
|
79
|
+
pointer: 'auto',
|
|
80
|
+
})
|
|
81
|
+
const emit = defineEmits<{
|
|
82
|
+
finish: []
|
|
83
|
+
skip: []
|
|
84
|
+
close: [reason: TourCloseReason]
|
|
85
|
+
stepEnter: [step: number]
|
|
86
|
+
stepLeave: [step: number]
|
|
87
|
+
}>()
|
|
58
88
|
defineSlots<{
|
|
59
89
|
content?(props: TourSlotProps): unknown
|
|
60
90
|
footer?(props: TourSlotProps): unknown
|
|
@@ -74,150 +104,216 @@
|
|
|
74
104
|
const isLast = computed(() => current.value >= total.value)
|
|
75
105
|
const busy = ref(false)
|
|
76
106
|
const beaconPending = ref(false)
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
107
|
+
const entered = ref<number | null>(null)
|
|
108
|
+
const coarse = ref(false)
|
|
109
|
+
|
|
110
|
+
const pointerKind = computed<TourPointerKind>(() =>
|
|
111
|
+
props.pointer === 'auto' ? (coarse.value ? 'touch' : 'mouse') : props.pointer,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const card = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
115
|
+
const layer = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
116
|
+
const beaconButton = shallowRef<HTMLButtonElement | null>(null)
|
|
117
|
+
const maskElement = shallowRef<HTMLElement | null>(null)
|
|
118
|
+
const blockerElement = shallowRef<HTMLElement | null>(null)
|
|
119
|
+
const pointerElement = shallowRef<HTMLElement | null>(null)
|
|
120
|
+
const keyElement = shallowRef<HTMLElement | null>(null)
|
|
121
|
+
|
|
122
|
+
const engine = createTourEngine({
|
|
123
|
+
pointer: () => pointerElement.value,
|
|
124
|
+
keyBadge: () => keyElement.value,
|
|
125
|
+
pointerKind: () => pointerKind.value,
|
|
126
|
+
cardGap: () => tourCardGap(card.value?.$el),
|
|
127
|
+
})
|
|
128
|
+
const tour = shallowRef(engine.getState())
|
|
129
|
+
const unsubscribe = engine.subscribe(() => (tour.value = engine.getState()))
|
|
82
130
|
|
|
83
|
-
const padding = computed(() => currentStep.value.padding ?? TOUR_PADDING)
|
|
84
131
|
const active = computed(() => open.value && total.value > 0)
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
132
|
+
const engaged = computed(() => active.value && entered.value !== null)
|
|
133
|
+
const live = computed(() => engaged.value && entered.value === current.value)
|
|
134
|
+
const status = computed(() => (engaged.value ? tour.value.status : 'idle'))
|
|
135
|
+
const beaconVisible = computed(
|
|
136
|
+
() => live.value && status.value === 'ready' && !!tour.value.hole && beaconPending.value,
|
|
137
|
+
)
|
|
138
|
+
const cardVisible = computed(
|
|
139
|
+
() =>
|
|
140
|
+
live.value &&
|
|
141
|
+
(status.value === 'ready' || status.value === 'missing') &&
|
|
142
|
+
tour.value.settled &&
|
|
143
|
+
!beaconVisible.value,
|
|
144
|
+
)
|
|
145
|
+
let anchoredWhenShown = false
|
|
146
|
+
const anchored = computed(() => {
|
|
147
|
+
if (!cardVisible.value) return anchoredWhenShown
|
|
148
|
+
anchoredWhenShown = status.value === 'ready' && !!tour.value.hole
|
|
149
|
+
return anchoredWhenShown
|
|
150
|
+
})
|
|
151
|
+
let stepWhenShown = current.value
|
|
152
|
+
const shown = computed(() => {
|
|
153
|
+
if (cardVisible.value) stepWhenShown = current.value
|
|
154
|
+
return clampTourStep(stepWhenShown, total.value)
|
|
155
|
+
})
|
|
156
|
+
const shownStep = computed<TourStep>(() => props.steps[shown.value - 1] ?? {})
|
|
157
|
+
const maskVisible = computed(
|
|
158
|
+
() =>
|
|
159
|
+
!!props.mask &&
|
|
160
|
+
status.value !== 'idle' &&
|
|
161
|
+
!beaconVisible.value &&
|
|
162
|
+
!(beaconPending.value && status.value === 'waiting'),
|
|
163
|
+
)
|
|
164
|
+
const modal = computed(() => !!props.mask)
|
|
165
|
+
const scripted = computed(() => !!currentStep.value.play)
|
|
90
166
|
const blocked = computed(() => tourTargetBlocked(currentStep.value, props.blockTarget))
|
|
91
|
-
const
|
|
167
|
+
const pageBlocked = computed(
|
|
168
|
+
() => engaged.value && tourPageBlocked(currentStep.value, props.blockPage),
|
|
169
|
+
)
|
|
170
|
+
const blocking = computed(() => cardVisible.value && !!tour.value.hole && blocked.value)
|
|
171
|
+
const targetElements = computed(() => tour.value.elements)
|
|
92
172
|
|
|
93
173
|
const spotlight = computed(() =>
|
|
94
|
-
|
|
95
|
-
? spotlightShape(
|
|
174
|
+
tour.value.hole
|
|
175
|
+
? spotlightShape(tour.value.viewport, tour.value.hole, tour.value.radius)
|
|
96
176
|
: null,
|
|
97
177
|
)
|
|
98
178
|
const lastSpotlight = shallowRef<TourSpotlight | null>(null)
|
|
99
179
|
const spotlightState = computed(() => spotlightMask(spotlight.value, lastSpotlight.value))
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
:
|
|
109
|
-
|
|
180
|
+
const blockerStyle = computed(() => {
|
|
181
|
+
if (pageBlocked.value) return { left: '0px', top: '0px', width: '100%', height: '100%' }
|
|
182
|
+
const hole = tour.value.hole
|
|
183
|
+
if (!blocking.value || !hole) return undefined
|
|
184
|
+
return {
|
|
185
|
+
left: `${hole.x}px`,
|
|
186
|
+
top: `${hole.y}px`,
|
|
187
|
+
width: `${hole.width}px`,
|
|
188
|
+
height: `${hole.height}px`,
|
|
189
|
+
}
|
|
190
|
+
})
|
|
110
191
|
const beaconStyle = computed(() => {
|
|
111
|
-
if (!
|
|
112
|
-
const point = beaconPoint(
|
|
192
|
+
if (!tour.value.targetRect) return undefined
|
|
193
|
+
const point = beaconPoint(tour.value.targetRect, direction.value)
|
|
113
194
|
return { left: `${point.x}px`, top: `${point.y}px` }
|
|
114
195
|
})
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
196
|
+
let holeWhenShown: TourRect | null = null
|
|
197
|
+
const reference = tourReference(() => {
|
|
198
|
+
if (cardVisible.value) holeWhenShown = engine.getState().hole
|
|
199
|
+
return holeWhenShown
|
|
200
|
+
})
|
|
118
201
|
|
|
119
|
-
const card = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
120
|
-
const layer = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
121
|
-
const beaconButton = shallowRef<HTMLButtonElement | null>(null)
|
|
122
202
|
let returnFocus: HTMLElement | null = null
|
|
123
|
-
|
|
124
|
-
const mounted = ref(false)
|
|
203
|
+
let closeReason: TourCloseReason | null = null
|
|
125
204
|
let navigated = false
|
|
205
|
+
let refocusBeacon = false
|
|
206
|
+
const mounted = ref(false)
|
|
207
|
+
|
|
208
|
+
function enterStep() {
|
|
209
|
+
if (!mounted.value) return
|
|
210
|
+
if (!active.value) {
|
|
211
|
+
if (entered.value !== null) exitTour()
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (entered.value === current.value) return
|
|
215
|
+
const previous = entered.value
|
|
216
|
+
refocusBeacon = !!card.value?.$el.contains(document.activeElement)
|
|
217
|
+
if (previous !== null) {
|
|
218
|
+
engine.leave()
|
|
219
|
+
emit('stepLeave', previous)
|
|
220
|
+
} else {
|
|
221
|
+
returnFocus = document.activeElement as HTMLElement | null
|
|
222
|
+
closeReason = null
|
|
223
|
+
}
|
|
224
|
+
beaconPending.value = tourBeaconOnArrival(
|
|
225
|
+
currentStep.value,
|
|
226
|
+
!!props.beacon,
|
|
227
|
+
previous !== null && navigated,
|
|
228
|
+
)
|
|
229
|
+
navigated = false
|
|
230
|
+
const number = current.value
|
|
231
|
+
entered.value = number
|
|
232
|
+
emit('stepEnter', number)
|
|
233
|
+
void engine.enter(currentStep.value, number).then(result => {
|
|
234
|
+
if (result === 'missing' && entered.value === number)
|
|
235
|
+
devWarn('Tour', `the target of step ${number} was not found; the card is centered`)
|
|
236
|
+
})
|
|
237
|
+
if (!beaconPending.value) engine.play()
|
|
238
|
+
}
|
|
126
239
|
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (element && currentStep.value.scroll !== false) revealTourTarget(element)
|
|
240
|
+
function exitTour() {
|
|
241
|
+
const previous = entered.value
|
|
242
|
+
entered.value = null
|
|
243
|
+
engine.close()
|
|
244
|
+
lastSpotlight.value = null
|
|
245
|
+
if (previous !== null) emit('stepLeave', previous)
|
|
246
|
+
emit('close', closeReason ?? 'close')
|
|
247
|
+
closeReason = null
|
|
248
|
+
void nextTick(restoreFocus)
|
|
137
249
|
}
|
|
138
250
|
|
|
139
|
-
watch([active, current
|
|
251
|
+
watch([active, current], enterStep, { flush: 'post' })
|
|
140
252
|
|
|
141
253
|
onMounted(() => {
|
|
142
254
|
mounted.value = true
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!active.value) return
|
|
146
|
-
returnFocus = document.activeElement as HTMLElement | null
|
|
147
|
-
void nextTick(() => {
|
|
148
|
-
if (cardVisible.value) card.value?.$el.focus({ preventScroll: true })
|
|
149
|
-
})
|
|
255
|
+
coarse.value = !!window.matchMedia?.('(pointer: coarse)').matches
|
|
256
|
+
enterStep()
|
|
150
257
|
})
|
|
151
258
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
currentStep.value,
|
|
156
|
-
!!props.beacon,
|
|
157
|
-
!!isActive && !!wasActive && navigated,
|
|
158
|
-
)
|
|
159
|
-
navigated = false
|
|
160
|
-
if (!isActive) {
|
|
161
|
-
resolvedFor.value = null
|
|
162
|
-
lastSpotlight.value = null
|
|
163
|
-
}
|
|
164
|
-
if (isActive && !wasActive) returnFocus = document.activeElement as HTMLElement | null
|
|
165
|
-
if (!isActive && wasActive) void nextTick(restoreFocus)
|
|
166
|
-
if (isActive && wasActive && focusInCard)
|
|
167
|
-
void nextTick(() => {
|
|
168
|
-
if (beaconVisible.value) beaconButton.value?.focus()
|
|
169
|
-
})
|
|
259
|
+
onBeforeUnmount(() => {
|
|
260
|
+
unsubscribe()
|
|
261
|
+
engine.close()
|
|
170
262
|
})
|
|
171
263
|
|
|
172
264
|
watch([spotlight, maskVisible], ([value, visible]) => {
|
|
173
265
|
if (value && visible) lastSpotlight.value = value
|
|
174
266
|
})
|
|
175
267
|
|
|
268
|
+
watch(targetElements, elements => {
|
|
269
|
+
root.value = elements[0] ?? document.documentElement
|
|
270
|
+
})
|
|
271
|
+
|
|
176
272
|
watch(cardVisible, visible => {
|
|
177
273
|
if (visible) void nextTick(() => card.value?.$el.focus({ preventScroll: true }))
|
|
178
274
|
})
|
|
179
275
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
viewport.value = { width: window.innerWidth, height: window.innerHeight }
|
|
185
|
-
if (!element) {
|
|
186
|
-
hole.value = null
|
|
187
|
-
targetRect.value = null
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
const rect = element.getBoundingClientRect()
|
|
191
|
-
targetRect.value = spotlightRect(rect, 0)
|
|
192
|
-
hole.value = spotlightRect(rect, padding.value)
|
|
193
|
-
}
|
|
194
|
-
if (!element) {
|
|
195
|
-
update()
|
|
196
|
-
window.addEventListener('resize', update)
|
|
197
|
-
onCleanup(() => window.removeEventListener('resize', update))
|
|
198
|
-
return
|
|
199
|
-
}
|
|
200
|
-
onCleanup(watchTourTarget(element, update))
|
|
276
|
+
watch(beaconVisible, visible => {
|
|
277
|
+
if (!visible || !refocusBeacon) return
|
|
278
|
+
refocusBeacon = false
|
|
279
|
+
void nextTick(() => beaconButton.value?.focus())
|
|
201
280
|
})
|
|
202
281
|
|
|
282
|
+
function useBranch(element: () => HTMLElement | null | undefined) {
|
|
283
|
+
watch(
|
|
284
|
+
element,
|
|
285
|
+
(node, _, onCleanup) => {
|
|
286
|
+
if (node) onCleanup(addBranch(node))
|
|
287
|
+
},
|
|
288
|
+
{ immediate: true, flush: 'post' },
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
useBranch(() => layer.value?.$el)
|
|
293
|
+
useBranch(() => maskElement.value)
|
|
294
|
+
useBranch(() => blockerElement.value)
|
|
295
|
+
useBranch(() => beaconButton.value)
|
|
296
|
+
|
|
297
|
+
const owns = () => tourOwnsInput(layer.value?.$el, scripted.value || pageBlocked.value)
|
|
298
|
+
|
|
203
299
|
watchEffect(
|
|
204
300
|
onCleanup => {
|
|
205
301
|
if (!cardVisible.value || !modal.value) return
|
|
206
302
|
const layerElement = layer.value?.$el as HTMLElement | undefined
|
|
207
303
|
if (!layerElement?.isConnected) return
|
|
208
|
-
const reachable = !blocked.value
|
|
209
|
-
const
|
|
210
|
-
onCleanup(hideOutsideTour([layerElement,
|
|
211
|
-
onCleanup(
|
|
212
|
-
trapTourFocus(
|
|
213
|
-
() => [card.value?.$el, reachable ? element : null],
|
|
214
|
-
() => layer.value?.$el,
|
|
215
|
-
),
|
|
216
|
-
)
|
|
304
|
+
const reachable = !blocked.value && !pageBlocked.value
|
|
305
|
+
const elements = targetElements.value
|
|
306
|
+
onCleanup(hideOutsideTour([layerElement, ...elements]))
|
|
307
|
+
onCleanup(trapTourFocus(() => [card.value?.$el, ...(reachable ? elements : [])], owns))
|
|
217
308
|
},
|
|
218
309
|
{ flush: 'post' },
|
|
219
310
|
)
|
|
220
311
|
|
|
312
|
+
watchEffect(onCleanup => {
|
|
313
|
+
if (!engaged.value || beaconVisible.value) return
|
|
314
|
+
onCleanup(listenTourKeys(onKeydown))
|
|
315
|
+
})
|
|
316
|
+
|
|
221
317
|
function restoreFocus() {
|
|
222
318
|
const focus = returnFocus
|
|
223
319
|
returnFocus = null
|
|
@@ -257,21 +353,29 @@
|
|
|
257
353
|
|
|
258
354
|
function finish() {
|
|
259
355
|
emit('finish')
|
|
356
|
+
closeReason = 'finish'
|
|
260
357
|
open.value = false
|
|
261
358
|
}
|
|
262
359
|
|
|
263
|
-
function skip() {
|
|
360
|
+
function skip(reason: 'skip' | 'escape' = 'skip') {
|
|
264
361
|
if (busy.value) return
|
|
265
362
|
emit('skip')
|
|
363
|
+
closeReason = reason
|
|
266
364
|
open.value = false
|
|
267
365
|
}
|
|
268
366
|
|
|
269
367
|
function close() {
|
|
368
|
+
closeReason = 'close'
|
|
270
369
|
open.value = false
|
|
271
370
|
}
|
|
272
371
|
|
|
273
|
-
function
|
|
274
|
-
if (
|
|
372
|
+
function replay() {
|
|
373
|
+
if (live.value && !beaconPending.value) engine.replay()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function openBeacon() {
|
|
377
|
+
beaconPending.value = false
|
|
378
|
+
engine.play()
|
|
275
379
|
}
|
|
276
380
|
|
|
277
381
|
function preventDismiss(event: Event) {
|
|
@@ -279,6 +383,13 @@
|
|
|
279
383
|
}
|
|
280
384
|
|
|
281
385
|
function onKeydown(event: KeyboardEvent) {
|
|
386
|
+
if (event.defaultPrevented || !owns()) return
|
|
387
|
+
if (event.key === 'Escape') {
|
|
388
|
+
event.preventDefault()
|
|
389
|
+
event.stopPropagation()
|
|
390
|
+
if (!props.locked) skip('escape')
|
|
391
|
+
return
|
|
392
|
+
}
|
|
282
393
|
const action = tourKeyAction(event, direction.value)
|
|
283
394
|
if (!action) return
|
|
284
395
|
event.preventDefault()
|
|
@@ -288,25 +399,30 @@
|
|
|
288
399
|
}
|
|
289
400
|
|
|
290
401
|
const slotProps = computed<TourSlotProps>(() => ({
|
|
291
|
-
step:
|
|
292
|
-
current:
|
|
402
|
+
step: shownStep.value,
|
|
403
|
+
current: shown.value,
|
|
293
404
|
total: total.value,
|
|
294
405
|
busy: busy.value,
|
|
406
|
+
playing: tour.value.playing,
|
|
295
407
|
next: () => void next(),
|
|
296
408
|
prev: () => void prev(),
|
|
297
|
-
skip,
|
|
409
|
+
skip: () => skip(),
|
|
410
|
+
replay,
|
|
298
411
|
}))
|
|
299
412
|
|
|
300
413
|
const popperProps = computed(() =>
|
|
301
|
-
|
|
414
|
+
anchored.value
|
|
302
415
|
? {
|
|
303
416
|
asChild: true,
|
|
304
|
-
reference
|
|
305
|
-
side:
|
|
306
|
-
align:
|
|
417
|
+
reference,
|
|
418
|
+
side: shownStep.value.side ?? 'bottom',
|
|
419
|
+
align: shownStep.value.align ?? 'center',
|
|
307
420
|
sideOffset: TOUR_SIDE_OFFSET,
|
|
308
421
|
collisionPadding: 16,
|
|
309
422
|
arrowPadding: TOUR_ARROW_PADDING,
|
|
423
|
+
prioritizePosition: true,
|
|
424
|
+
sticky: 'always' as const,
|
|
425
|
+
updatePositionStrategy: 'always' as const,
|
|
310
426
|
}
|
|
311
427
|
: {
|
|
312
428
|
class: cn(tourCentered(), 'hn-anim-pop'),
|
|
@@ -314,23 +430,57 @@
|
|
|
314
430
|
},
|
|
315
431
|
)
|
|
316
432
|
|
|
317
|
-
defineExpose<TourExpose>({ next, prev, goTo, close })
|
|
433
|
+
defineExpose<TourExpose>({ next, prev, goTo, close, replay })
|
|
318
434
|
</script>
|
|
319
435
|
|
|
320
436
|
<template>
|
|
321
437
|
<TeleportPrimitive v-if="total > 0">
|
|
322
438
|
<div
|
|
439
|
+
ref="blockerElement"
|
|
323
440
|
data-hn-tour-blocker
|
|
324
441
|
aria-hidden="true"
|
|
325
|
-
:hidden="!
|
|
442
|
+
:hidden="!blockerStyle"
|
|
326
443
|
:class="tourBlocker()"
|
|
327
|
-
:style="
|
|
444
|
+
:style="blockerStyle"
|
|
328
445
|
/>
|
|
446
|
+
<div
|
|
447
|
+
ref="pointerElement"
|
|
448
|
+
data-hn-tour-pointer
|
|
449
|
+
:data-kind="pointerKind"
|
|
450
|
+
data-state="hidden"
|
|
451
|
+
aria-hidden="true"
|
|
452
|
+
:class="tourPointer()"
|
|
453
|
+
>
|
|
454
|
+
<span data-hn-tour-pointer-ring />
|
|
455
|
+
<span data-hn-tour-pointer-ripples />
|
|
456
|
+
<svg
|
|
457
|
+
v-if="pointerKind === 'mouse'"
|
|
458
|
+
data-hn-tour-pointer-glyph
|
|
459
|
+
viewBox="0 0 24 24"
|
|
460
|
+
stroke-width="1.5"
|
|
461
|
+
stroke-linejoin="round"
|
|
462
|
+
:class="tourPointerArrow()"
|
|
463
|
+
>
|
|
464
|
+
<path d="M4 2.5v16.2l4.1-3.9 2.8 6.6 2.9-1.2-2.8-6.5h5.8z" />
|
|
465
|
+
</svg>
|
|
466
|
+
<span v-else data-hn-tour-pointer-glyph :class="tourPointerTouch()" />
|
|
467
|
+
</div>
|
|
468
|
+
<div
|
|
469
|
+
ref="keyElement"
|
|
470
|
+
data-hn-tour-key
|
|
471
|
+
data-state="hidden"
|
|
472
|
+
aria-hidden="true"
|
|
473
|
+
:class="tourKey()"
|
|
474
|
+
>
|
|
475
|
+
<kbd data-hn-tour-key-label :class="tourKeyCap()" />
|
|
476
|
+
</div>
|
|
329
477
|
<Presence :present="maskVisible">
|
|
330
478
|
<div
|
|
479
|
+
ref="maskElement"
|
|
331
480
|
data-hn-tour-mask
|
|
332
481
|
aria-hidden="true"
|
|
333
482
|
:data-state="maskVisible ? 'open' : 'closed'"
|
|
483
|
+
:data-mask="props.mask === 'dim' ? 'dim' : undefined"
|
|
334
484
|
:data-hn-tour-hole="spotlightState.hole"
|
|
335
485
|
:class="tourMask()"
|
|
336
486
|
:style="spotlightState.style"
|
|
@@ -344,7 +494,7 @@
|
|
|
344
494
|
:aria-label="t.tour.beacon(current, currentStep.title ?? '')"
|
|
345
495
|
:class="tourBeacon()"
|
|
346
496
|
:style="beaconStyle"
|
|
347
|
-
@click="
|
|
497
|
+
@click="openBeacon"
|
|
348
498
|
>
|
|
349
499
|
<span :class="tourBeaconPulse()" />
|
|
350
500
|
<span :class="tourBeaconDot()" />
|
|
@@ -354,33 +504,31 @@
|
|
|
354
504
|
<DismissableLayer
|
|
355
505
|
ref="layer"
|
|
356
506
|
as-child
|
|
357
|
-
@escape-key-down="
|
|
507
|
+
@escape-key-down="preventDismiss"
|
|
358
508
|
@pointer-down-outside="preventDismiss"
|
|
359
509
|
@focus-outside="preventDismiss"
|
|
360
|
-
@dismiss="skip"
|
|
361
510
|
>
|
|
362
|
-
<component :is="
|
|
511
|
+
<component :is="anchored ? PopperContent : 'div'" v-bind="popperProps">
|
|
363
512
|
<Card
|
|
364
513
|
ref="card"
|
|
365
514
|
data-hn-tour
|
|
366
|
-
:data-step="
|
|
515
|
+
:data-step="shown"
|
|
367
516
|
:data-state="cardVisible ? 'open' : 'closed'"
|
|
368
517
|
:padded="false"
|
|
369
518
|
role="dialog"
|
|
370
519
|
tabindex="-1"
|
|
371
520
|
:dir="direction"
|
|
372
521
|
:aria-modal="modal ? 'true' : undefined"
|
|
373
|
-
:aria-labelledby="
|
|
522
|
+
:aria-labelledby="shownStep.title ? titleId : undefined"
|
|
374
523
|
:aria-describedby="
|
|
375
|
-
|
|
524
|
+
shownStep.description || $slots.content ? descriptionId : undefined
|
|
376
525
|
"
|
|
377
526
|
:aria-busy="busy || undefined"
|
|
378
|
-
:class="cn(tourCard(),
|
|
379
|
-
@keydown="onKeydown"
|
|
527
|
+
:class="cn(tourCard(), anchored && 'hn-anim-pop', props.class)"
|
|
380
528
|
>
|
|
381
|
-
<div v-if="
|
|
382
|
-
<Text v-if="
|
|
383
|
-
{{
|
|
529
|
+
<div v-if="shownStep.title || !props.locked" :class="tourHeader()">
|
|
530
|
+
<Text v-if="shownStep.title" :id="titleId" weight="medium" class="min-w-0 flex-1">
|
|
531
|
+
{{ shownStep.title }}
|
|
384
532
|
</Text>
|
|
385
533
|
<CloseButton
|
|
386
534
|
v-if="!props.locked"
|
|
@@ -388,16 +536,16 @@
|
|
|
388
536
|
class="ms-auto -me-1.5 -mt-1.5"
|
|
389
537
|
:label="props.skipText ?? t.tour.skip"
|
|
390
538
|
:disabled="busy"
|
|
391
|
-
@click="skip"
|
|
539
|
+
@click="skip()"
|
|
392
540
|
/>
|
|
393
541
|
</div>
|
|
394
|
-
<div v-if="
|
|
542
|
+
<div v-if="shownStep.description || $slots.content" :id="descriptionId">
|
|
395
543
|
<slot name="content" v-bind="slotProps">
|
|
396
|
-
<Text tone="muted" size="sm">{{
|
|
544
|
+
<Text tone="muted" size="sm">{{ shownStep.description }}</Text>
|
|
397
545
|
</slot>
|
|
398
546
|
</div>
|
|
399
547
|
<PopperArrow
|
|
400
|
-
v-if="
|
|
548
|
+
v-if="anchored"
|
|
401
549
|
data-hn-tour-arrow
|
|
402
550
|
:width="TOUR_ARROW_WIDTH"
|
|
403
551
|
:height="TOUR_ARROW_HEIGHT"
|
|
@@ -414,11 +562,24 @@
|
|
|
414
562
|
<slot name="footer" v-bind="slotProps">
|
|
415
563
|
<div :class="tourFooter()">
|
|
416
564
|
<Text as="span" tone="muted" size="sm" class="tabular-nums">
|
|
417
|
-
{{ t.tour.progress(
|
|
565
|
+
{{ t.tour.progress(shown, total) }}
|
|
418
566
|
</Text>
|
|
567
|
+
<IconButton
|
|
568
|
+
v-if="!!shownStep.play"
|
|
569
|
+
data-hn-tour-replay
|
|
570
|
+
:label="props.replayText ?? t.tour.replay"
|
|
571
|
+
:tooltip="false"
|
|
572
|
+
size="sm"
|
|
573
|
+
variant="ghost"
|
|
574
|
+
tone="neutral"
|
|
575
|
+
pill
|
|
576
|
+
@click="replay"
|
|
577
|
+
>
|
|
578
|
+
<RotateCcw />
|
|
579
|
+
</IconButton>
|
|
419
580
|
<div class="ms-auto flex gap-2">
|
|
420
581
|
<Button
|
|
421
|
-
v-if="
|
|
582
|
+
v-if="shown > 1"
|
|
422
583
|
data-hn-tour-prev
|
|
423
584
|
size="sm"
|
|
424
585
|
variant="soft"
|
|
@@ -430,7 +591,7 @@
|
|
|
430
591
|
</Button>
|
|
431
592
|
<Button data-hn-tour-next size="sm" :loading="busy" @click="next">
|
|
432
593
|
{{
|
|
433
|
-
|
|
594
|
+
shown >= total
|
|
434
595
|
? (props.finishText ?? t.tour.finish)
|
|
435
596
|
: (props.nextText ?? t.tour.next)
|
|
436
597
|
}}
|