@hina-ui/vue 1.4.0 → 1.5.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/index.js +330 -148
- package/dist/index.js.map +1 -1
- package/dist/types/components/dialog/Dialog.vue.d.ts +3 -1
- package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/types/components/drawer/Drawer.vue.d.ts +21 -14
- package/dist/types/components/drawer/Drawer.vue.d.ts.map +1 -1
- package/dist/types/components/drawer/drawer.variants.d.ts +10 -1
- package/dist/types/components/drawer/drawer.variants.d.ts.map +1 -1
- package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts +4 -1
- package/dist/types/components/dropdown-menu/DropdownMenu.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/HoverCard.vue.d.ts +4 -1
- package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +6 -4
- package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +9 -4
- package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -1
- package/dist/types/components/popover/Popover.vue.d.ts +4 -1
- package/dist/types/components/popover/Popover.vue.d.ts.map +1 -1
- package/dist/types/components/sheet/Sheet.vue.d.ts +21 -14
- package/dist/types/components/sheet/Sheet.vue.d.ts.map +1 -1
- package/dist/types/components/sheet/sheet.variants.d.ts +54 -3
- package/dist/types/components/sheet/sheet.variants.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/anchored-overlay.d.ts +12 -2
- package/dist/types/lib/anchored-overlay.d.ts.map +1 -1
- package/dist/types/lib/overlay-anchor.d.ts +16 -0
- package/dist/types/lib/overlay-anchor.d.ts.map +1 -0
- package/dist/types/lib/scroll-viewport.d.ts +9 -0
- package/dist/types/lib/scroll-viewport.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/dialog/Dialog.vue +5 -1
- package/src/components/drawer/Drawer.vue +69 -32
- package/src/components/drawer/drawer.variants.ts +6 -2
- package/src/components/dropdown-menu/DropdownMenu.vue +18 -5
- package/src/components/hover-card/HoverCard.vue +6 -1
- package/src/components/hover-card/HoverCardAnchor.vue +3 -1
- package/src/components/hover-card/composables/useHoverCardAnchor.ts +33 -42
- package/src/components/popover/Popover.vue +17 -7
- package/src/components/sheet/Sheet.vue +67 -24
- package/src/components/sheet/sheet.variants.ts +17 -4
- package/src/index.ts +1 -0
- package/src/lib/anchored-overlay.ts +28 -19
- package/src/lib/overlay-anchor.ts +65 -0
- package/src/lib/scroll-viewport.ts +8 -0
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
import ScrollArea from '../scroll-area/ScrollArea.vue'
|
|
15
15
|
import Heading from '../heading/Heading.vue'
|
|
16
16
|
import Text from '../text/Text.vue'
|
|
17
|
+
import { useOverlayPortal } from '../../lib/overlay-portal'
|
|
18
|
+
import { useScrollViewport } from '../../lib/scroll-viewport'
|
|
17
19
|
import { cn } from '../../lib/cn'
|
|
18
|
-
import { useUiLocale } from '../../locale'
|
|
19
20
|
import { drawerCard } from './drawer.variants'
|
|
20
21
|
|
|
21
22
|
defineOptions({ name: 'HnDrawer' })
|
|
@@ -26,18 +27,32 @@
|
|
|
26
27
|
description?: string
|
|
27
28
|
side?: 'start' | 'end'
|
|
28
29
|
size?: 'sm' | 'md' | 'lg'
|
|
30
|
+
header?: boolean
|
|
31
|
+
closable?: boolean
|
|
29
32
|
locked?: boolean
|
|
30
33
|
class?: string
|
|
31
34
|
}>(),
|
|
32
|
-
{ side: 'end', size: 'md', locked: false },
|
|
35
|
+
{ side: 'end', size: 'md', header: true, closable: true, locked: false },
|
|
33
36
|
)
|
|
34
37
|
|
|
38
|
+
const slots = defineSlots<{
|
|
39
|
+
default?(): unknown
|
|
40
|
+
icon?(): unknown
|
|
41
|
+
title?(): unknown
|
|
42
|
+
body?(props: { close: () => void }): unknown
|
|
43
|
+
content?(props: { close: () => void }): unknown
|
|
44
|
+
footer?(props: { close: () => void }): unknown
|
|
45
|
+
}>()
|
|
46
|
+
|
|
35
47
|
function guard(e: Event) {
|
|
36
48
|
if (props.locked) e.preventDefault()
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
const { scrollArea, viewport } = useScrollViewport()
|
|
52
|
+
defineExpose({ viewport })
|
|
53
|
+
|
|
39
54
|
const open = defineModel<boolean>('open')
|
|
40
|
-
const
|
|
55
|
+
const { content, present } = useOverlayPortal(open)
|
|
41
56
|
|
|
42
57
|
function close() {
|
|
43
58
|
open.value = false
|
|
@@ -49,45 +64,67 @@
|
|
|
49
64
|
<DialogTrigger v-if="$slots.default" as-child>
|
|
50
65
|
<slot />
|
|
51
66
|
</DialogTrigger>
|
|
52
|
-
<DialogPortal>
|
|
67
|
+
<DialogPortal v-if="present">
|
|
53
68
|
<DialogOverlay class="hn-scrim" />
|
|
54
69
|
<DialogContent as-child @escape-key-down="guard" @interact-outside="guard">
|
|
55
70
|
<Card
|
|
71
|
+
ref="content"
|
|
72
|
+
v-bind="props.description ? {} : { 'aria-describedby': undefined }"
|
|
56
73
|
:data-hn-side="props.side"
|
|
57
74
|
:padded="false"
|
|
58
75
|
:class="
|
|
59
|
-
cn(
|
|
60
|
-
'hn-anim-drawer pointer-events-auto fixed inset-y-0 z-(--hn-z-overlay) flex flex-col gap-4',
|
|
61
|
-
'max-w-[calc(100vw-3rem)] rounded-none py-(--hn-panel-p) shadow-lg outline-none',
|
|
62
|
-
drawerCard({ side: props.side, size: props.size }),
|
|
63
|
-
props.class,
|
|
64
|
-
)
|
|
76
|
+
cn(drawerCard({ side: props.side, size: props.size, padded: !slots.body }), props.class)
|
|
65
77
|
"
|
|
66
78
|
>
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
<template v-if="!props.header || slots.body">
|
|
80
|
+
<DialogTitle as-child>
|
|
81
|
+
<Heading :level="2" class="sr-only">{{ props.title }}</Heading>
|
|
82
|
+
</DialogTitle>
|
|
83
|
+
<DialogDescription v-if="props.description" class="sr-only">
|
|
84
|
+
{{ props.description }}
|
|
85
|
+
</DialogDescription>
|
|
86
|
+
</template>
|
|
87
|
+
<slot v-if="slots.body" name="body" :close="close" />
|
|
88
|
+
<template v-else>
|
|
89
|
+
<div
|
|
90
|
+
v-if="props.header"
|
|
91
|
+
class="flex shrink-0 items-start justify-between gap-4 px-(--hn-panel-p)"
|
|
92
|
+
>
|
|
93
|
+
<div class="flex min-w-0 flex-col gap-1.5">
|
|
94
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
95
|
+
<span
|
|
96
|
+
v-if="slots.icon"
|
|
97
|
+
class="text-muted flex shrink-0 [&_svg]:size-5"
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
>
|
|
100
|
+
<slot name="icon" />
|
|
101
|
+
</span>
|
|
102
|
+
<DialogTitle as-child>
|
|
103
|
+
<Heading :level="2" size="lg" class="min-w-0">
|
|
104
|
+
<slot name="title">{{ props.title }}</slot>
|
|
105
|
+
</Heading>
|
|
106
|
+
</DialogTitle>
|
|
107
|
+
</div>
|
|
108
|
+
<DialogDescription v-if="props.description" as-child>
|
|
109
|
+
<Text tone="muted">{{ props.description }}</Text>
|
|
110
|
+
</DialogDescription>
|
|
111
|
+
</div>
|
|
112
|
+
<DialogClose v-if="props.closable" as-child>
|
|
113
|
+
<CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
|
|
114
|
+
</DialogClose>
|
|
75
115
|
</div>
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<div
|
|
82
|
-
|
|
116
|
+
<ScrollArea v-if="$slots.content" ref="scrollArea" class="min-h-0 grow">
|
|
117
|
+
<div class="px-(--hn-panel-p) py-1">
|
|
118
|
+
<slot name="content" :close="close" />
|
|
119
|
+
</div>
|
|
120
|
+
</ScrollArea>
|
|
121
|
+
<div
|
|
122
|
+
v-if="$slots.footer"
|
|
123
|
+
class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
|
|
124
|
+
>
|
|
125
|
+
<slot name="footer" :close="close" />
|
|
83
126
|
</div>
|
|
84
|
-
</
|
|
85
|
-
<div
|
|
86
|
-
v-if="$slots.footer"
|
|
87
|
-
class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
|
|
88
|
-
>
|
|
89
|
-
<slot name="footer" :close="close" />
|
|
90
|
-
</div>
|
|
127
|
+
</template>
|
|
91
128
|
</Card>
|
|
92
129
|
</DialogContent>
|
|
93
130
|
</DialogPortal>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { tv, type VariantProps } from '../../lib/tv'
|
|
2
2
|
|
|
3
3
|
export const drawerCard = tv({
|
|
4
|
-
base:
|
|
4
|
+
base: [
|
|
5
|
+
'hn-anim-drawer pointer-events-auto fixed inset-y-0 z-(--hn-z-overlay) flex flex-col',
|
|
6
|
+
'max-w-[calc(100vw-3rem)] rounded-none shadow-lg outline-none',
|
|
7
|
+
],
|
|
5
8
|
variants: {
|
|
9
|
+
padded: { true: 'gap-4 py-(--hn-panel-p)' },
|
|
6
10
|
side: {
|
|
7
11
|
start: 'start-0',
|
|
8
12
|
end: 'end-0',
|
|
@@ -13,7 +17,7 @@ export const drawerCard = tv({
|
|
|
13
17
|
lg: 'w-120',
|
|
14
18
|
},
|
|
15
19
|
},
|
|
16
|
-
defaultVariants: { side: 'end', size: 'md' },
|
|
20
|
+
defaultVariants: { side: 'end', size: 'md', padded: true },
|
|
17
21
|
})
|
|
18
22
|
|
|
19
23
|
export type DrawerVariants = VariantProps<typeof drawerCard>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
|
|
2
3
|
import { computed } from 'vue'
|
|
3
4
|
import {
|
|
4
5
|
DropdownMenuRoot,
|
|
@@ -16,7 +17,8 @@
|
|
|
16
17
|
const props = withDefaults(
|
|
17
18
|
defineProps<{
|
|
18
19
|
label?: string
|
|
19
|
-
anchor?:
|
|
20
|
+
anchor?: OverlayAnchor | null
|
|
21
|
+
updatePositionStrategy?: OverlayPositionStrategy
|
|
20
22
|
modal?: boolean
|
|
21
23
|
dir?: 'ltr' | 'rtl'
|
|
22
24
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
@@ -24,13 +26,23 @@
|
|
|
24
26
|
sideOffset?: number
|
|
25
27
|
class?: string
|
|
26
28
|
}>(),
|
|
27
|
-
{
|
|
29
|
+
{
|
|
30
|
+
updatePositionStrategy: 'optimized',
|
|
31
|
+
side: 'bottom',
|
|
32
|
+
align: 'center',
|
|
33
|
+
sideOffset: 8,
|
|
34
|
+
modal: true,
|
|
35
|
+
},
|
|
28
36
|
)
|
|
29
37
|
|
|
30
38
|
const open = defineModel<boolean>('open')
|
|
31
39
|
const emit = defineEmits<DropdownMenuContentEmits>()
|
|
32
40
|
defineSlots<{ default?(): unknown; content?(): unknown }>()
|
|
33
|
-
const { trigger, reference, visible, events } = useAnchoredOverlay(
|
|
41
|
+
const { trigger, content, present, reference, visible, events } = useAnchoredOverlay(
|
|
42
|
+
props,
|
|
43
|
+
open,
|
|
44
|
+
emit,
|
|
45
|
+
)
|
|
34
46
|
|
|
35
47
|
const pressOrigin = computed(() => {
|
|
36
48
|
if (props.side === 'left') return 'right'
|
|
@@ -52,17 +64,18 @@
|
|
|
52
64
|
>
|
|
53
65
|
<slot />
|
|
54
66
|
</DropdownMenuTrigger>
|
|
55
|
-
<DropdownMenuPortal>
|
|
67
|
+
<DropdownMenuPortal v-if="present">
|
|
56
68
|
<DropdownMenuContent
|
|
57
|
-
v-if="reference"
|
|
58
69
|
v-bind="{ 'aria-label': props.label, ...$attrs, ...events }"
|
|
59
70
|
:reference="reference"
|
|
71
|
+
:update-position-strategy="props.updatePositionStrategy"
|
|
60
72
|
as-child
|
|
61
73
|
:side="props.side"
|
|
62
74
|
:align="props.align"
|
|
63
75
|
:side-offset="props.sideOffset"
|
|
64
76
|
>
|
|
65
77
|
<Card
|
|
78
|
+
ref="content"
|
|
66
79
|
:padded="false"
|
|
67
80
|
:class="
|
|
68
81
|
cn(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
|
|
2
3
|
import { HoverCardContent, HoverCardPortal, HoverCardRoot, HoverCardTrigger } from 'reka-ui'
|
|
3
4
|
import { cn } from '../../lib/cn'
|
|
4
5
|
import Card from '../card/Card.vue'
|
|
@@ -8,7 +9,8 @@
|
|
|
8
9
|
|
|
9
10
|
const props = withDefaults(
|
|
10
11
|
defineProps<{
|
|
11
|
-
anchor?:
|
|
12
|
+
anchor?: OverlayAnchor | null
|
|
13
|
+
updatePositionStrategy?: OverlayPositionStrategy
|
|
12
14
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
13
15
|
align?: 'start' | 'center' | 'end'
|
|
14
16
|
sideOffset?: number
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
positionerClass?: string
|
|
20
22
|
}>(),
|
|
21
23
|
{
|
|
24
|
+
updatePositionStrategy: 'optimized',
|
|
22
25
|
side: 'bottom',
|
|
23
26
|
align: 'center',
|
|
24
27
|
sideOffset: 8,
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
<HoverCardAnchor
|
|
42
45
|
v-slot="{ reference, contentRef, present }"
|
|
43
46
|
:anchor="props.anchor"
|
|
47
|
+
:update-position-strategy="props.updatePositionStrategy"
|
|
44
48
|
:external="!$slots.default"
|
|
45
49
|
:close-delay="props.closeDelay"
|
|
46
50
|
:positioner-class="props.positionerClass"
|
|
@@ -48,6 +52,7 @@
|
|
|
48
52
|
<HoverCardPortal v-if="present">
|
|
49
53
|
<HoverCardContent
|
|
50
54
|
:reference="reference"
|
|
55
|
+
:update-position-strategy="props.updatePositionStrategy"
|
|
51
56
|
as-child
|
|
52
57
|
:side="props.side"
|
|
53
58
|
:align="props.align"
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
|
|
2
3
|
import { useHoverCardAnchor } from './composables/useHoverCardAnchor'
|
|
3
4
|
|
|
4
5
|
defineOptions({ name: 'HnHoverCardAnchor' })
|
|
5
6
|
const props = defineProps<{
|
|
6
|
-
anchor?:
|
|
7
|
+
anchor?: OverlayAnchor | null
|
|
8
|
+
updatePositionStrategy: OverlayPositionStrategy
|
|
7
9
|
external: boolean
|
|
8
10
|
closeDelay: number
|
|
9
11
|
positionerClass?: string
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { computed, onScopeDispose,
|
|
1
|
+
import { computed, onScopeDispose, watch, type ComponentPublicInstance } from 'vue'
|
|
2
2
|
import { useEventListener, useRafFn } from '@vueuse/core'
|
|
3
3
|
import { injectHoverCardRootContext } from 'reka-ui'
|
|
4
|
+
import {
|
|
5
|
+
overlayAnchorElement,
|
|
6
|
+
useOverlayAnchor,
|
|
7
|
+
type OverlayAnchor,
|
|
8
|
+
type OverlayPositionStrategy,
|
|
9
|
+
} from '../../../lib/overlay-anchor'
|
|
4
10
|
import { useOverlayPortal } from '../../../lib/overlay-portal'
|
|
5
11
|
import { useOverlayPositionerClass } from '../../../lib/overlay-positioner'
|
|
6
12
|
import { inPointerCorridor } from '../../../lib/pointer-corridor'
|
|
7
13
|
|
|
8
14
|
interface HoverCardAnchorOptions {
|
|
9
|
-
anchor?:
|
|
15
|
+
anchor?: OverlayAnchor | null
|
|
16
|
+
updatePositionStrategy: OverlayPositionStrategy
|
|
10
17
|
external: boolean
|
|
11
18
|
closeDelay: number
|
|
12
19
|
positionerClass?: string
|
|
@@ -16,18 +23,18 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
|
16
23
|
const root = injectHoverCardRootContext()
|
|
17
24
|
const original = { onOpen: root.onOpen, onClose: root.onClose, onDismiss: root.onDismiss }
|
|
18
25
|
const anchor = computed(() => (props.external ? (props.anchor ?? undefined) : undefined))
|
|
26
|
+
const element = computed(() => overlayAnchorElement(anchor.value))
|
|
27
|
+
const virtual = computed(() => !!anchor.value && !element.value)
|
|
19
28
|
const { content, present } = useOverlayPortal(root.open)
|
|
20
29
|
useOverlayPositionerClass(content, () => props.positionerClass)
|
|
21
30
|
const panel = computed(() =>
|
|
22
31
|
props.external ? (content.value?.$el as HTMLElement | undefined) : undefined,
|
|
23
32
|
)
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const reference = computed(() =>
|
|
30
|
-
props.external ? (liveAnchor.value ?? retained.value) : undefined,
|
|
33
|
+
const reference = useOverlayAnchor(
|
|
34
|
+
() => (props.external ? anchor.value : root.triggerElement.value),
|
|
35
|
+
root.open,
|
|
36
|
+
present,
|
|
37
|
+
() => props.updatePositionStrategy,
|
|
31
38
|
)
|
|
32
39
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
33
40
|
let tracking = false
|
|
@@ -45,6 +52,7 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
|
45
52
|
|
|
46
53
|
function close() {
|
|
47
54
|
if (
|
|
55
|
+
virtual.value ||
|
|
48
56
|
timer !== undefined ||
|
|
49
57
|
!root.open.value ||
|
|
50
58
|
root.hasSelectionRef.value ||
|
|
@@ -63,8 +71,8 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
|
63
71
|
const pointer = event as PointerEvent
|
|
64
72
|
if (!root.open.value || !tracking || pointer.pointerType === 'touch') return
|
|
65
73
|
const target = pointer.target as Node | null
|
|
66
|
-
if (target && (
|
|
67
|
-
const a =
|
|
74
|
+
if (target && (element.value?.contains(target) || panel.value?.contains(target))) return keep()
|
|
75
|
+
const a = element.value?.getBoundingClientRect()
|
|
68
76
|
const b = panel.value?.getBoundingClientRect()
|
|
69
77
|
if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
|
|
70
78
|
else close()
|
|
@@ -72,33 +80,20 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
|
72
80
|
|
|
73
81
|
function leave(event: Event) {
|
|
74
82
|
const pointer = event as PointerEvent
|
|
75
|
-
if (pointer.pointerType === 'touch' || !root.open.value) return
|
|
83
|
+
if (virtual.value || pointer.pointerType === 'touch' || !root.open.value) return
|
|
76
84
|
tracking = true
|
|
77
|
-
const a =
|
|
85
|
+
const a = element.value?.getBoundingClientRect()
|
|
78
86
|
const b = panel.value?.getBoundingClientRect()
|
|
79
87
|
if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
|
|
80
88
|
else close()
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function measure() {
|
|
84
|
-
|
|
85
|
-
if (!element?.isConnected) {
|
|
86
|
-
liveAnchor.value = undefined
|
|
92
|
+
if (!anchor.value || (element.value && !element.value.isConnected)) {
|
|
87
93
|
if (root.open.value) dismiss()
|
|
88
94
|
return
|
|
89
95
|
}
|
|
90
|
-
|
|
91
|
-
const rect = element.getBoundingClientRect()
|
|
92
|
-
const previous = retained.value?.getBoundingClientRect()
|
|
93
|
-
if (
|
|
94
|
-
!previous ||
|
|
95
|
-
retained.value?.contextElement !== element ||
|
|
96
|
-
rect.x !== previous.x ||
|
|
97
|
-
rect.y !== previous.y ||
|
|
98
|
-
rect.width !== previous.width ||
|
|
99
|
-
rect.height !== previous.height
|
|
100
|
-
)
|
|
101
|
-
retained.value = { getBoundingClientRect: () => rect, contextElement: element }
|
|
96
|
+
reference.value?.getBoundingClientRect()
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
const { pause, resume } = useRafFn(measure, { immediate: false })
|
|
@@ -112,35 +107,32 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
|
112
107
|
cancel()
|
|
113
108
|
tracking = false
|
|
114
109
|
pause()
|
|
115
|
-
if (!props.external || !mounted)
|
|
116
|
-
liveAnchor.value = undefined
|
|
117
|
-
return
|
|
118
|
-
}
|
|
110
|
+
if (!props.external || !mounted) return
|
|
119
111
|
if (element !== previous?.[0]) {
|
|
120
112
|
root.hasSelectionRef.value = false
|
|
121
113
|
root.isPointerDownOnContentRef.value = false
|
|
122
114
|
}
|
|
123
115
|
measure()
|
|
124
|
-
resume()
|
|
116
|
+
if (!virtual.value) resume()
|
|
125
117
|
},
|
|
126
118
|
{ immediate: true, flush: 'post' },
|
|
127
119
|
)
|
|
128
120
|
|
|
129
|
-
useEventListener(
|
|
130
|
-
useEventListener(
|
|
131
|
-
useEventListener(
|
|
132
|
-
useEventListener(
|
|
121
|
+
useEventListener(element, 'pointerenter', keep)
|
|
122
|
+
useEventListener(element, 'pointerleave', leave)
|
|
123
|
+
useEventListener(element, 'focusin', keep)
|
|
124
|
+
useEventListener(element, 'focusout', event => {
|
|
133
125
|
const target = (event as FocusEvent).relatedTarget as Node | null
|
|
134
|
-
if (!target || (!
|
|
126
|
+
if (!target || (!element.value?.contains(target) && !panel.value?.contains(target))) close()
|
|
135
127
|
})
|
|
136
128
|
useEventListener(panel, 'pointerenter', keep)
|
|
137
129
|
useEventListener(panel, 'pointerleave', leave)
|
|
138
|
-
useEventListener(() =>
|
|
130
|
+
useEventListener(() => element.value?.ownerDocument, 'pointermove', track)
|
|
139
131
|
useEventListener(
|
|
140
|
-
() =>
|
|
132
|
+
() => element.value?.ownerDocument,
|
|
141
133
|
'scroll',
|
|
142
134
|
event => {
|
|
143
|
-
if (root.open.value && (event.target as Node | null)?.contains(
|
|
135
|
+
if (root.open.value && (event.target as Node | null)?.contains(element.value ?? null))
|
|
144
136
|
dismiss()
|
|
145
137
|
},
|
|
146
138
|
{ capture: true, passive: true },
|
|
@@ -153,7 +145,6 @@ export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
|
153
145
|
|
|
154
146
|
function contentRef(value: Element | ComponentPublicInstance | null) {
|
|
155
147
|
content.value = value as ComponentPublicInstance | null
|
|
156
|
-
if (!value && !root.open.value) retained.value = undefined
|
|
157
148
|
}
|
|
158
149
|
|
|
159
150
|
return { reference, contentRef, present }
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { OverlayAnchor, OverlayPositionStrategy } from '../../lib/overlay-anchor'
|
|
2
3
|
import { computed } from 'vue'
|
|
3
4
|
import { PopoverRoot, PopoverTrigger, PopoverPortal, type PopoverContentEmits } from 'reka-ui'
|
|
4
5
|
import PopoverContent from './PopoverContent.vue'
|
|
5
6
|
import { useAnchoredOverlay } from '../../lib/anchored-overlay'
|
|
6
7
|
import Card from '../card/Card.vue'
|
|
7
|
-
import { useOverlayPortal } from '../../lib/overlay-portal'
|
|
8
8
|
import { cn } from '../../lib/cn'
|
|
9
9
|
|
|
10
10
|
defineOptions({ name: 'HnPopover', inheritAttrs: false })
|
|
11
11
|
|
|
12
12
|
const props = withDefaults(
|
|
13
13
|
defineProps<{
|
|
14
|
-
anchor?:
|
|
14
|
+
anchor?: OverlayAnchor | null
|
|
15
|
+
updatePositionStrategy?: OverlayPositionStrategy
|
|
15
16
|
modal?: boolean
|
|
16
17
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
17
18
|
align?: 'start' | 'center' | 'end'
|
|
@@ -19,18 +20,26 @@
|
|
|
19
20
|
padded?: boolean
|
|
20
21
|
class?: string
|
|
21
22
|
}>(),
|
|
22
|
-
{
|
|
23
|
+
{
|
|
24
|
+
updatePositionStrategy: 'optimized',
|
|
25
|
+
side: 'bottom',
|
|
26
|
+
align: 'center',
|
|
27
|
+
sideOffset: 8,
|
|
28
|
+
padded: true,
|
|
29
|
+
modal: true,
|
|
30
|
+
},
|
|
23
31
|
)
|
|
24
32
|
|
|
25
33
|
const open = defineModel<boolean>('open')
|
|
26
34
|
const emit = defineEmits<PopoverContentEmits>()
|
|
27
35
|
defineSlots<{ default?(): unknown; content?(): unknown }>()
|
|
28
|
-
const { trigger, reference, visible, events } = useAnchoredOverlay(
|
|
29
|
-
|
|
36
|
+
const { trigger, content, present, reference, visible, events } = useAnchoredOverlay(
|
|
37
|
+
props,
|
|
38
|
+
open,
|
|
39
|
+
emit,
|
|
40
|
+
event => emit('openAutoFocus', event),
|
|
30
41
|
)
|
|
31
42
|
|
|
32
|
-
const { content, present } = useOverlayPortal(visible)
|
|
33
|
-
|
|
34
43
|
const pressOrigin = computed(() => {
|
|
35
44
|
if (props.side === 'left') return 'right'
|
|
36
45
|
if (props.side === 'right') return 'left'
|
|
@@ -54,6 +63,7 @@
|
|
|
54
63
|
<PopoverContent
|
|
55
64
|
v-bind="{ ...$attrs, ...events }"
|
|
56
65
|
:reference="reference"
|
|
66
|
+
:update-position-strategy="props.updatePositionStrategy"
|
|
57
67
|
as-child
|
|
58
68
|
:side="props.side"
|
|
59
69
|
:align="props.align"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from 'vue'
|
|
3
3
|
import {
|
|
4
4
|
DialogClose,
|
|
5
5
|
DialogContent,
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
DialogTitle,
|
|
11
11
|
DialogTrigger,
|
|
12
12
|
} from 'reka-ui'
|
|
13
|
+
import { useOverlayPortal } from '../../lib/overlay-portal'
|
|
14
|
+
import { useScrollViewport } from '../../lib/scroll-viewport'
|
|
13
15
|
import { cn } from '../../lib/cn'
|
|
14
|
-
import { useUiLocale } from '../../locale'
|
|
15
16
|
import Card from '../card/Card.vue'
|
|
16
17
|
import CloseButton from '../close-button/CloseButton.vue'
|
|
17
18
|
import Heading from '../heading/Heading.vue'
|
|
@@ -26,16 +27,29 @@
|
|
|
26
27
|
defineProps<{
|
|
27
28
|
title: string
|
|
28
29
|
description?: string
|
|
30
|
+
header?: boolean
|
|
31
|
+
closable?: boolean
|
|
29
32
|
handle?: boolean
|
|
30
33
|
locked?: boolean
|
|
31
34
|
class?: string
|
|
32
35
|
}>(),
|
|
33
|
-
{ handle: true, locked: false },
|
|
36
|
+
{ header: true, closable: true, handle: true, locked: false },
|
|
34
37
|
)
|
|
35
38
|
|
|
39
|
+
const slots = defineSlots<{
|
|
40
|
+
default?(): unknown
|
|
41
|
+
icon?(): unknown
|
|
42
|
+
title?(): unknown
|
|
43
|
+
body?(props: { close: () => void }): unknown
|
|
44
|
+
content?(props: { close: () => void }): unknown
|
|
45
|
+
footer?(props: { close: () => void }): unknown
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
const { scrollArea, viewport } = useScrollViewport()
|
|
49
|
+
defineExpose({ viewport })
|
|
50
|
+
|
|
36
51
|
const open = defineModel<boolean>('open')
|
|
37
|
-
const
|
|
38
|
-
const panel = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
52
|
+
const { content: panel, present } = useOverlayPortal(open)
|
|
39
53
|
|
|
40
54
|
const { dragging, offset, onPointerDown } = useDragToDismiss(() => panel.value?.$el ?? null, {
|
|
41
55
|
enabled: () => !props.locked,
|
|
@@ -63,54 +77,83 @@
|
|
|
63
77
|
<DialogTrigger v-if="$slots.default" as-child>
|
|
64
78
|
<slot />
|
|
65
79
|
</DialogTrigger>
|
|
66
|
-
<DialogPortal>
|
|
80
|
+
<DialogPortal v-if="present">
|
|
67
81
|
<DialogOverlay class="hn-scrim" />
|
|
68
82
|
<DialogContent as-child @escape-key-down="guard" @interact-outside="guard">
|
|
69
83
|
<Card
|
|
70
84
|
ref="panel"
|
|
85
|
+
v-bind="props.description ? {} : { 'aria-describedby': undefined }"
|
|
71
86
|
data-hn-sheet
|
|
72
87
|
:data-dragging="dragging ? '' : undefined"
|
|
73
88
|
:padded="false"
|
|
74
89
|
:style="style"
|
|
75
|
-
:class="
|
|
90
|
+
:class="
|
|
91
|
+
cn(sheetPanel({ grip: props.header || props.handle, padded: !slots.body }), props.class)
|
|
92
|
+
"
|
|
76
93
|
>
|
|
94
|
+
<template v-if="!props.header || slots.body">
|
|
95
|
+
<DialogTitle as-child>
|
|
96
|
+
<Heading :level="2" class="sr-only">{{ props.title }}</Heading>
|
|
97
|
+
</DialogTitle>
|
|
98
|
+
<DialogDescription v-if="props.description" class="sr-only">
|
|
99
|
+
{{ props.description }}
|
|
100
|
+
</DialogDescription>
|
|
101
|
+
</template>
|
|
77
102
|
<div
|
|
103
|
+
v-if="props.handle || (props.header && !slots.body)"
|
|
78
104
|
data-hn-sheet-grip
|
|
79
105
|
:data-dragging="dragging ? '' : undefined"
|
|
80
|
-
:class="sheetGrip()"
|
|
106
|
+
:class="sheetGrip({ standalone: !!slots.body })"
|
|
81
107
|
@pointerdown="onPointerDown"
|
|
82
108
|
>
|
|
83
109
|
<div
|
|
84
110
|
v-if="props.handle"
|
|
85
111
|
aria-hidden="true"
|
|
86
112
|
:data-disabled="props.locked ? '' : undefined"
|
|
87
|
-
:class="sheetHandle()"
|
|
113
|
+
:class="sheetHandle({ header: props.header && !slots.body })"
|
|
88
114
|
/>
|
|
89
|
-
<div
|
|
115
|
+
<div
|
|
116
|
+
v-if="props.header && !slots.body"
|
|
117
|
+
class="flex items-start justify-between gap-4 px-(--hn-panel-p)"
|
|
118
|
+
>
|
|
90
119
|
<div class="flex min-w-0 flex-col gap-1.5">
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
120
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
121
|
+
<span
|
|
122
|
+
v-if="slots.icon"
|
|
123
|
+
class="text-muted flex shrink-0 [&_svg]:size-5"
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
>
|
|
126
|
+
<slot name="icon" />
|
|
127
|
+
</span>
|
|
128
|
+
<DialogTitle as-child>
|
|
129
|
+
<Heading :level="2" size="lg" class="min-w-0">
|
|
130
|
+
<slot name="title">{{ props.title }}</slot>
|
|
131
|
+
</Heading>
|
|
132
|
+
</DialogTitle>
|
|
133
|
+
</div>
|
|
94
134
|
<DialogDescription v-if="props.description" as-child>
|
|
95
135
|
<Text tone="muted">{{ props.description }}</Text>
|
|
96
136
|
</DialogDescription>
|
|
97
137
|
</div>
|
|
98
|
-
<DialogClose v-if="!props.handle" as-child>
|
|
138
|
+
<DialogClose v-if="!props.handle && props.closable" as-child>
|
|
99
139
|
<CloseButton :disabled="props.locked" class="-mt-1.5 -me-1.5 shrink-0" />
|
|
100
140
|
</DialogClose>
|
|
101
141
|
</div>
|
|
102
142
|
</div>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
143
|
+
<slot v-if="slots.body" name="body" :close="close" />
|
|
144
|
+
<template v-else>
|
|
145
|
+
<ScrollArea v-if="$slots.content" ref="scrollArea" class="min-h-0 grow">
|
|
146
|
+
<div class="px-(--hn-panel-p) py-1">
|
|
147
|
+
<slot name="content" :close="close" />
|
|
148
|
+
</div>
|
|
149
|
+
</ScrollArea>
|
|
150
|
+
<div
|
|
151
|
+
v-if="$slots.footer"
|
|
152
|
+
class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
|
|
153
|
+
>
|
|
154
|
+
<slot name="footer" :close="close" />
|
|
106
155
|
</div>
|
|
107
|
-
</
|
|
108
|
-
<div
|
|
109
|
-
v-if="$slots.footer"
|
|
110
|
-
class="flex shrink-0 justify-end gap-(--hn-inline-gap) px-(--hn-panel-p)"
|
|
111
|
-
>
|
|
112
|
-
<slot name="footer" :close="close" />
|
|
113
|
-
</div>
|
|
156
|
+
</template>
|
|
114
157
|
</Card>
|
|
115
158
|
</DialogContent>
|
|
116
159
|
</DialogPortal>
|