@ui-sdk/vue 0.0.0-stage → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +10 -2
- package/dist/accordion.d.ts +561 -0
- package/dist/accordion.js +24 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +7 -0
- package/dist/alert-dialog.d.ts +231 -0
- package/dist/alert-dialog.js +33 -0
- package/dist/async-options.d.ts +18 -0
- package/dist/async-options.js +40 -0
- package/dist/autocomplete.d.ts +384 -0
- package/dist/autocomplete.js +344 -0
- package/dist/avatar.d.ts +40 -0
- package/dist/avatar.js +28 -0
- package/dist/button.d.ts +52 -0
- package/dist/button.js +43 -0
- package/dist/calendar-context.d.ts +10 -0
- package/dist/calendar-context.js +7 -0
- package/dist/calendar.d.ts +311 -0
- package/dist/calendar.js +349 -0
- package/dist/carousel.d.ts +236 -0
- package/dist/carousel.js +204 -0
- package/dist/checkbox.d.ts +211 -0
- package/dist/checkbox.js +151 -0
- package/dist/children.d.ts +41 -0
- package/dist/children.js +135 -0
- package/dist/collapsible.d.ts +54 -0
- package/dist/collapsible.js +18 -0
- package/dist/color-picker.d.ts +187 -0
- package/dist/color-picker.js +158 -0
- package/dist/color.d.ts +2 -0
- package/dist/color.js +21 -0
- package/dist/combobox.d.ts +517 -0
- package/dist/combobox.js +531 -0
- package/dist/command.d.ts +322 -0
- package/dist/command.js +345 -0
- package/dist/controller-target.d.ts +49 -0
- package/dist/controller-target.js +112 -0
- package/dist/create-parts.d.ts +126 -0
- package/dist/create-parts.js +206 -0
- package/dist/date-input.d.ts +133 -0
- package/dist/date-input.js +31 -0
- package/dist/date-picker.d.ts +328 -0
- package/dist/date-picker.js +354 -0
- package/dist/date.d.ts +2 -0
- package/dist/date.js +59 -0
- package/dist/dialog-parts.d.ts +259 -0
- package/dist/dialog-parts.js +300 -0
- package/dist/dialog.d.ts +251 -0
- package/dist/dialog.js +25 -0
- package/dist/drawer.d.ts +304 -0
- package/dist/drawer.js +373 -0
- package/dist/editor-context.d.ts +4 -0
- package/dist/editor-context.js +5 -0
- package/dist/field-context.d.ts +26 -0
- package/dist/field-context.js +44 -0
- package/dist/field.d.ts +154 -0
- package/dist/field.js +240 -0
- package/dist/file-input.d.ts +212 -0
- package/dist/file-input.js +255 -0
- package/dist/form-inputs.d.ts +16 -0
- package/dist/form-inputs.js +25 -0
- package/dist/gesture.d.ts +1 -0
- package/dist/gesture.js +15 -0
- package/dist/history.d.ts +2 -0
- package/dist/history.js +11 -0
- package/dist/hover-card.d.ts +137 -0
- package/dist/hover-card.js +153 -0
- package/dist/image-cropper.d.ts +74 -0
- package/dist/image-cropper.js +36 -0
- package/dist/image.d.ts +2 -0
- package/dist/image.js +5 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +60 -0
- package/dist/input.d.ts +120 -0
- package/dist/input.js +73 -0
- package/dist/item-icon.d.ts +34 -0
- package/dist/item-icon.js +63 -0
- package/dist/label.d.ts +16 -0
- package/dist/label.js +8 -0
- package/dist/lifecycle.d.ts +11 -0
- package/dist/lifecycle.js +91 -0
- package/dist/list-items.d.ts +38 -0
- package/dist/list-items.js +59 -0
- package/dist/list-text.d.ts +7 -0
- package/dist/list-text.js +15 -0
- package/dist/listbox.d.ts +273 -0
- package/dist/listbox.js +126 -0
- package/dist/menu.d.ts +678 -0
- package/dist/menu.js +607 -0
- package/dist/menubar-context.d.ts +5 -0
- package/dist/menubar-context.js +5 -0
- package/dist/menubar.d.ts +61 -0
- package/dist/menubar.js +48 -0
- package/dist/meter.d.ts +41 -0
- package/dist/meter.js +37 -0
- package/dist/navigation-menu.d.ts +228 -0
- package/dist/navigation-menu.js +195 -0
- package/dist/number-input.d.ts +176 -0
- package/dist/number-input.js +231 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +39 -0
- package/dist/nuxt/entries.json +977 -0
- package/dist/nuxt/module.d.ts +18 -0
- package/dist/nuxt/module.js +31 -0
- package/dist/nuxt/registrations.d.ts +22 -0
- package/dist/nuxt/registrations.js +22 -0
- package/dist/observe.d.ts +2 -0
- package/dist/observe.js +7 -0
- package/dist/otp-input.d.ts +142 -0
- package/dist/otp-input.js +130 -0
- package/dist/overlays.d.ts +40 -0
- package/dist/overlays.js +118 -0
- package/dist/pagination.d.ts +201 -0
- package/dist/pagination.js +146 -0
- package/dist/password-input.d.ts +144 -0
- package/dist/password-input.js +134 -0
- package/dist/popover.d.ts +211 -0
- package/dist/popover.js +258 -0
- package/dist/popup.d.ts +60 -0
- package/dist/popup.js +89 -0
- package/dist/preferences.d.ts +44 -0
- package/dist/preferences.js +102 -0
- package/dist/primitive.d.ts +2 -0
- package/dist/primitive.js +11 -0
- package/dist/progress.d.ts +41 -0
- package/dist/progress.js +37 -0
- package/dist/radio.d.ts +164 -0
- package/dist/radio.js +145 -0
- package/dist/rating.d.ts +149 -0
- package/dist/rating.js +146 -0
- package/dist/renderless.d.ts +4 -0
- package/dist/renderless.js +28 -0
- package/dist/resizable.d.ts +54 -0
- package/dist/resizable.js +22 -0
- package/dist/scroll-area.d.ts +72 -0
- package/dist/scroll-area.js +77 -0
- package/dist/segmented-control.d.ts +151 -0
- package/dist/segmented-control.js +110 -0
- package/dist/segmented-input.d.ts +76 -0
- package/dist/segmented-input.js +115 -0
- package/dist/select.d.ts +458 -0
- package/dist/select.js +468 -0
- package/dist/separator.d.ts +14 -0
- package/dist/separator.js +8 -0
- package/dist/shortcuts.d.ts +32 -0
- package/dist/shortcuts.js +56 -0
- package/dist/slider.d.ts +65 -0
- package/dist/slider.js +194 -0
- package/dist/switch.d.ts +103 -0
- package/dist/switch.js +71 -0
- package/dist/tabs.d.ts +338 -0
- package/dist/tabs.js +27 -0
- package/dist/text-control.d.ts +89 -0
- package/dist/text-control.js +51 -0
- package/dist/text-match.d.ts +2 -0
- package/dist/text-match.js +7 -0
- package/dist/textarea.d.ts +106 -0
- package/dist/textarea.js +68 -0
- package/dist/time-input.d.ts +131 -0
- package/dist/time-input.js +32 -0
- package/dist/timeline.d.ts +81 -0
- package/dist/timeline.js +30 -0
- package/dist/toast-parts.d.ts +203 -0
- package/dist/toast-parts.js +270 -0
- package/dist/toast.d.ts +2 -0
- package/dist/toast.js +21 -0
- package/dist/toggle-props.d.ts +22 -0
- package/dist/toggle-props.js +14 -0
- package/dist/toggle.d.ts +199 -0
- package/dist/toggle.js +129 -0
- package/dist/token-input.d.ts +136 -0
- package/dist/token-input.js +214 -0
- package/dist/toolbar-context.d.ts +7 -0
- package/dist/toolbar-context.js +20 -0
- package/dist/toolbar.d.ts +120 -0
- package/dist/toolbar.js +77 -0
- package/dist/tooltip.d.ts +160 -0
- package/dist/tooltip.js +224 -0
- package/dist/tree.d.ts +300 -0
- package/dist/tree.js +209 -0
- package/dist/trigger-menu.d.ts +452 -0
- package/dist/trigger-menu.js +254 -0
- package/dist/use-primitive.d.ts +132 -0
- package/dist/use-primitive.js +462 -0
- package/dist/waterfall.d.ts +201 -0
- package/dist/waterfall.js +171 -0
- package/docs/README.md +89 -0
- package/docs/agents.md +1523 -0
- package/docs/components/accordion.md +1369 -0
- package/docs/components/alert-dialog.md +799 -0
- package/docs/components/autocomplete.md +1857 -0
- package/docs/components/avatar.md +272 -0
- package/docs/components/button.md +435 -0
- package/docs/components/calendar.md +1589 -0
- package/docs/components/carousel.md +1171 -0
- package/docs/components/checkbox.md +1008 -0
- package/docs/components/collapsible.md +536 -0
- package/docs/components/color-picker.md +706 -0
- package/docs/components/combobox.md +3504 -0
- package/docs/components/command.md +1481 -0
- package/docs/components/date-input.md +536 -0
- package/docs/components/date-picker.md +1244 -0
- package/docs/components/dialog.md +1397 -0
- package/docs/components/drawer.md +1166 -0
- package/docs/components/field.md +538 -0
- package/docs/components/file-input.md +611 -0
- package/docs/components/hover-card.md +505 -0
- package/docs/components/image-cropper.md +556 -0
- package/docs/components/input.md +306 -0
- package/docs/components/label.md +203 -0
- package/docs/components/listbox.md +1004 -0
- package/docs/components/menu.md +1229 -0
- package/docs/components/menubar.md +443 -0
- package/docs/components/meter.md +315 -0
- package/docs/components/navigation-menu.md +1523 -0
- package/docs/components/number-input.md +1029 -0
- package/docs/components/otp-input.md +512 -0
- package/docs/components/pagination.md +613 -0
- package/docs/components/password-input.md +649 -0
- package/docs/components/popover.md +1448 -0
- package/docs/components/preferences.md +208 -0
- package/docs/components/progress.md +423 -0
- package/docs/components/radio.md +785 -0
- package/docs/components/rating.md +416 -0
- package/docs/components/resizable.md +910 -0
- package/docs/components/scroll-area.md +915 -0
- package/docs/components/segmented-control.md +435 -0
- package/docs/components/select.md +2015 -0
- package/docs/components/separator.md +229 -0
- package/docs/components/slider.md +1084 -0
- package/docs/components/switch.md +565 -0
- package/docs/components/tabs.md +944 -0
- package/docs/components/textarea.md +299 -0
- package/docs/components/time-input.md +224 -0
- package/docs/components/timeline.md +654 -0
- package/docs/components/toast.md +1224 -0
- package/docs/components/toggle.md +500 -0
- package/docs/components/token-input.md +953 -0
- package/docs/components/toolbar.md +739 -0
- package/docs/components/tooltip.md +818 -0
- package/docs/components/tree.md +1306 -0
- package/docs/components/trigger-menu.md +1223 -0
- package/docs/components/waterfall.md +717 -0
- package/docs/controllers.md +71 -0
- package/docs/core.md +8027 -0
- package/docs/custom-parts.md +89 -0
- package/docs/first-component.md +83 -0
- package/docs/forms.md +122 -0
- package/docs/installation.md +72 -0
- package/docs/introduction.md +32 -0
- package/docs/lists-and-menus.md +122 -0
- package/docs/llms.txt +1502 -0
- package/docs/parts-and-composition.md +85 -0
- package/docs/popups.md +108 -0
- package/docs/rendering.md +56 -0
- package/docs/server-rendering.md +180 -0
- package/docs/state-and-events.md +100 -0
- package/docs/styling-and-animation.md +148 -0
- package/docs/utilities/async-options.md +348 -0
- package/docs/utilities/shortcuts.md +159 -0
- package/package.json +352 -3
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# Hover Card
|
|
2
|
+
|
|
3
|
+
Preview a link on hover or keyboard focus while keeping its normal navigation.
|
|
4
|
+
|
|
5
|
+
A hover card shows a preview of where a link goes, like a person's profile, to someone who pauses on it. The link still works as a link.
|
|
6
|
+
|
|
7
|
+
```vue title="basic.vue"
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import {
|
|
10
|
+
HoverCard,
|
|
11
|
+
HoverCardContent,
|
|
12
|
+
HoverCardTrigger,
|
|
13
|
+
} from "@ui-sdk/vue/hover-card"
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<HoverCard>
|
|
18
|
+
<HoverCardTrigger href="https://example.com/people/hollis"
|
|
19
|
+
>@hollis</HoverCardTrigger
|
|
20
|
+
>
|
|
21
|
+
<HoverCardContent>
|
|
22
|
+
<div data-slot="row">
|
|
23
|
+
<span data-slot="initials" aria-hidden="true">HG</span>
|
|
24
|
+
<div data-slot="stack">
|
|
25
|
+
<p>Hollis Grant</p>
|
|
26
|
+
<p>
|
|
27
|
+
Design engineer on the payments team. Writes about forms and motion.
|
|
28
|
+
</p>
|
|
29
|
+
<p data-slot="hint">Joined March 2021</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</HoverCardContent>
|
|
33
|
+
</HoverCard>
|
|
34
|
+
</template>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Hover @hollis, or Tab to it and wait, to open Hollis Grant's profile preview. Move the pointer away or press Escape to close it.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @ui-sdk/vue @ui-sdk/core
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Composition
|
|
46
|
+
|
|
47
|
+
Use the following composition to build a `HoverCard`:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
HoverCard
|
|
51
|
+
├── HoverCardTrigger
|
|
52
|
+
└── HoverCardContent
|
|
53
|
+
└── HoverCardArrow
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The trigger is a link (an `<a>`, so give it an `href`), and pressing it follows the link. The content is the preview, and the arrow is optional.
|
|
57
|
+
|
|
58
|
+
The card only adds a preview for people who pause on the link: everything in it should be reachable from the page the link goes to, since touch and screen reader users may never open it. For a tip on a button, use a [Tooltip](./tooltip.md). For content that opens with a press, use a [Popover](./popover.md).
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
Import the parts and put them together:
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<script setup lang="ts">
|
|
66
|
+
import {
|
|
67
|
+
HoverCard,
|
|
68
|
+
HoverCardContent,
|
|
69
|
+
HoverCardTrigger,
|
|
70
|
+
} from "@ui-sdk/vue/hover-card"
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<HoverCard>
|
|
75
|
+
<HoverCardTrigger href="/people/hollis">@hollis</HoverCardTrigger>
|
|
76
|
+
<HoverCardContent>
|
|
77
|
+
<p>Hollis Grant</p>
|
|
78
|
+
<p>Design engineer on the payments team.</p>
|
|
79
|
+
</HoverCardContent>
|
|
80
|
+
</HoverCard>
|
|
81
|
+
</template>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Positioning
|
|
85
|
+
|
|
86
|
+
`side` picks where the card opens, `bottom` by default, and `align` lines it up with the start, center or end of the link. The card flips to the other side when there isn't room and shifts along the link to stay on screen.
|
|
87
|
+
|
|
88
|
+
`HoverCardArrow` follows the side the card actually lands on, including after a flip, and gets the same `data-side`. It points at the middle of the link and keeps `arrow-padding` away from the corners; `data-arrow-clamped` tells you when it can't reach the middle.
|
|
89
|
+
|
|
90
|
+
```vue title="side.vue"
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { CheckIcon, ChevronDownIcon } from "@lucide/vue"
|
|
93
|
+
import {
|
|
94
|
+
HoverCard,
|
|
95
|
+
HoverCardArrow,
|
|
96
|
+
HoverCardContent,
|
|
97
|
+
HoverCardTrigger,
|
|
98
|
+
} from "@ui-sdk/vue/hover-card"
|
|
99
|
+
import {
|
|
100
|
+
Select,
|
|
101
|
+
SelectContent,
|
|
102
|
+
SelectItem,
|
|
103
|
+
SelectItemIndicator,
|
|
104
|
+
SelectLabel,
|
|
105
|
+
SelectTrigger,
|
|
106
|
+
SelectValue,
|
|
107
|
+
} from "@ui-sdk/vue/select"
|
|
108
|
+
import { ref } from "vue"
|
|
109
|
+
|
|
110
|
+
const sides = [
|
|
111
|
+
{ value: "top", label: "Top" },
|
|
112
|
+
{ value: "right", label: "Right" },
|
|
113
|
+
{ value: "bottom", label: "Bottom" },
|
|
114
|
+
{ value: "left", label: "Left" },
|
|
115
|
+
]
|
|
116
|
+
const aligns = [
|
|
117
|
+
{ value: "start", label: "Start" },
|
|
118
|
+
{ value: "center", label: "Center" },
|
|
119
|
+
{ value: "end", label: "End" },
|
|
120
|
+
]
|
|
121
|
+
type Side = "top" | "right" | "bottom" | "left"
|
|
122
|
+
type Align = "start" | "center" | "end"
|
|
123
|
+
|
|
124
|
+
const side = ref<Side>("bottom")
|
|
125
|
+
const align = ref<Align>("center")
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<div data-slot="stack">
|
|
130
|
+
<div data-slot="controls">
|
|
131
|
+
<Select
|
|
132
|
+
:items="sides"
|
|
133
|
+
:value="side"
|
|
134
|
+
@value-change="(next) => next && (side = next as Side)"
|
|
135
|
+
>
|
|
136
|
+
<SelectLabel>Side</SelectLabel>
|
|
137
|
+
<SelectTrigger>
|
|
138
|
+
<SelectValue />
|
|
139
|
+
<ChevronDownIcon aria-hidden="true" />
|
|
140
|
+
</SelectTrigger>
|
|
141
|
+
<SelectContent>
|
|
142
|
+
<SelectItem v-for="option in sides" :key="option.value" :value="option.value">
|
|
143
|
+
{{ option.label }}
|
|
144
|
+
<SelectItemIndicator>
|
|
145
|
+
<CheckIcon aria-hidden="true" />
|
|
146
|
+
</SelectItemIndicator>
|
|
147
|
+
</SelectItem>
|
|
148
|
+
</SelectContent>
|
|
149
|
+
</Select>
|
|
150
|
+
<Select
|
|
151
|
+
:items="aligns"
|
|
152
|
+
:value="align"
|
|
153
|
+
@value-change="(next) => next && (align = next as Align)"
|
|
154
|
+
>
|
|
155
|
+
<SelectLabel>Align</SelectLabel>
|
|
156
|
+
<SelectTrigger>
|
|
157
|
+
<SelectValue />
|
|
158
|
+
<ChevronDownIcon aria-hidden="true" />
|
|
159
|
+
</SelectTrigger>
|
|
160
|
+
<SelectContent>
|
|
161
|
+
<SelectItem v-for="option in aligns" :key="option.value" :value="option.value">
|
|
162
|
+
{{ option.label }}
|
|
163
|
+
<SelectItemIndicator>
|
|
164
|
+
<CheckIcon aria-hidden="true" />
|
|
165
|
+
</SelectItemIndicator>
|
|
166
|
+
</SelectItem>
|
|
167
|
+
</SelectContent>
|
|
168
|
+
</Select>
|
|
169
|
+
</div>
|
|
170
|
+
<HoverCard>
|
|
171
|
+
<HoverCardTrigger href="https://example.com/people/ines"
|
|
172
|
+
>@ines</HoverCardTrigger
|
|
173
|
+
>
|
|
174
|
+
<HoverCardContent :side="side" :align="align" :side-offset="10">
|
|
175
|
+
<HoverCardArrow />
|
|
176
|
+
<p>Inès Moreau</p>
|
|
177
|
+
<p>Product designer. Leads the checkout redesign.</p>
|
|
178
|
+
</HoverCardContent>
|
|
179
|
+
</HoverCard>
|
|
180
|
+
</div>
|
|
181
|
+
</template>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Choose a side and an alignment, then hover @ines. The card opens there and its arrow points at the link.
|
|
185
|
+
|
|
186
|
+
`side-offset` is the gap to the link, 4 pixels by default. `align-offset` shifts the card along it, and `collision-padding` is the space kept from the edges of the window. `anchor` positions the card against something other than the link: an element, a CSS selector, a point like `{ x, y }`, or anything with `getBoundingClientRect()`.
|
|
187
|
+
|
|
188
|
+
```vue
|
|
189
|
+
<HoverCardContent
|
|
190
|
+
side="right"
|
|
191
|
+
align="start"
|
|
192
|
+
:side-offset="8"
|
|
193
|
+
:collision-padding="16"
|
|
194
|
+
:anchor="row"
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Delays
|
|
199
|
+
|
|
200
|
+
`delay` is how long the pointer rests on the link, or keyboard focus stays on it, before the card opens: 600 milliseconds by default. `close-delay` is how long it stays open once the pointer leaves the link and the card: 300 milliseconds by default. The pointer can cross the gap between the link and the card without closing it.
|
|
201
|
+
|
|
202
|
+
`disabled` stops the card from opening and leaves a plain link.
|
|
203
|
+
|
|
204
|
+
```vue
|
|
205
|
+
<HoverCard :delay="300" :close-delay="150" :disabled="!previews">
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Controlled
|
|
209
|
+
|
|
210
|
+
Bind `v-model:open`. Every change carries a [reason](#reasons) in `@open-change`, like `trigger-hover`, `trigger-focus`, `focus-out`, `escape-key` or `outside-press`. Call `details.cancel()` to stop it.
|
|
211
|
+
|
|
212
|
+
```vue
|
|
213
|
+
<script setup lang="ts">
|
|
214
|
+
const open = ref(false)
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<HoverCard v-model:open="open">
|
|
219
|
+
<!-- … -->
|
|
220
|
+
</HoverCard>
|
|
221
|
+
</template>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## One Card for Many Links
|
|
225
|
+
|
|
226
|
+
A feed or a list can share one card instead of rendering one per link. Connect the links and the card to one `createHoverCardController<Payload>()`. Each link passes a `payload`, like its person, and a typed content alias, such as `HoverCardContent<Person>`, reads it from `state.payload` in its default slot. Only the link that opened the card gets `data-open`. `controller.open({ payload })` opens the card from code.
|
|
227
|
+
|
|
228
|
+
```vue
|
|
229
|
+
<script setup lang="ts">
|
|
230
|
+
const profile = createHoverCardController<Person>()
|
|
231
|
+
const ProfileContent = HoverCardContent<Person>
|
|
232
|
+
</script>
|
|
233
|
+
|
|
234
|
+
<template>
|
|
235
|
+
<HoverCardTrigger
|
|
236
|
+
v-for="person in people"
|
|
237
|
+
:key="person.handle"
|
|
238
|
+
:controller="profile"
|
|
239
|
+
:payload="person"
|
|
240
|
+
:href="person.url"
|
|
241
|
+
>
|
|
242
|
+
{{ person.name }}
|
|
243
|
+
</HoverCardTrigger>
|
|
244
|
+
|
|
245
|
+
<HoverCard :controller="profile">
|
|
246
|
+
<ProfileContent v-slot="{ state }">
|
|
247
|
+
<p>{{ state.payload?.bio }}</p>
|
|
248
|
+
</ProfileContent>
|
|
249
|
+
</HoverCard>
|
|
250
|
+
</template>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
A card can have links of its own. Tab from a link with an open card moves into the card, and Tab past its end moves on to the page after the link. Focus leaving the card closes it.
|
|
254
|
+
|
|
255
|
+
## Container
|
|
256
|
+
|
|
257
|
+
Cards render in place and show in the top layer, so no ancestor clips or covers them. `container` renders the card somewhere else, for iframes, shadow roots or a specific stacking context. See the shared [popup guide](../popups.md).
|
|
258
|
+
|
|
259
|
+
```vue
|
|
260
|
+
<HoverCardContent :container="shadowRoot" />
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Animation
|
|
264
|
+
|
|
265
|
+
The content has `data-starting-style` while it opens and `data-ending-style` while it closes. Transition from those states, and the card stays mounted until the exit finishes. `@open-change-complete` runs after the transition, and `keep-mounted` keeps the closed card in the page, hidden, for animation libraries that drive the exit from state.
|
|
266
|
+
|
|
267
|
+
```css
|
|
268
|
+
[data-slot="hover-card-content"] {
|
|
269
|
+
transition:
|
|
270
|
+
opacity 150ms,
|
|
271
|
+
scale 150ms;
|
|
272
|
+
|
|
273
|
+
&:is([data-starting-style], [data-ending-style]) {
|
|
274
|
+
opacity: 0;
|
|
275
|
+
scale: 0.96;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Custom Rendering
|
|
281
|
+
|
|
282
|
+
Every part takes a `render` slot, which receives the props to bind and the part's state, and the default slot receives the state too.
|
|
283
|
+
|
|
284
|
+
```vue
|
|
285
|
+
<HoverCardTrigger href="/people/hollis">
|
|
286
|
+
<template #render="{ props }">
|
|
287
|
+
<a v-bind="props">
|
|
288
|
+
<span aria-hidden="true">HG</span>
|
|
289
|
+
Hollis Grant
|
|
290
|
+
</a>
|
|
291
|
+
</template>
|
|
292
|
+
</HoverCardTrigger>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Styling
|
|
296
|
+
|
|
297
|
+
Each part has a `data-slot`, such as `hover-card-content`, and shows its state through the attributes listed under [Data Attributes](#data-attributes). Use `data-open` on the trigger to mark the link whose card is showing, and `data-side` to style the content and arrow for the side they landed on. Put `transform-origin: var(--transform-origin)` on the content so it scales from the link.
|
|
298
|
+
|
|
299
|
+
Keep overflow visible around an arrow that extends outside the card. If the body needs to scroll, put scrolling on an inner element.
|
|
300
|
+
|
|
301
|
+
### HoverCardContent
|
|
302
|
+
- `--anchor-width`: Width of the anchor, usually the trigger. Use it to match widths.
|
|
303
|
+
- `--anchor-height`: Height of the anchor.
|
|
304
|
+
- `--available-width`: Space beside the popup, up to the viewport edge. Not inherited either.
|
|
305
|
+
- `--available-height`: Space between the anchor and the viewport edge. Use it for max-height. Not inherited, so a child that needs it copies it on the popup, like `[--list-height:var(--available-height)]`, and reads `max-h-(--list-height)`. The first popup registers both names for the whole page with `CSS.registerProperty`, so they don't inherit anywhere. A page that needs them to inherit calls it first, with `inherits: true`. An `@property` rule can't, since a registration in script wins.
|
|
306
|
+
- `--transform-origin`: The point on the anchor the popup opens from. Use it to scale from the right place.
|
|
307
|
+
- `--arrow-offset`: Where the anchor's center falls along the popup's edge, kept `arrowPadding` from the corners. Position a ::before arrow with it.
|
|
308
|
+
|
|
309
|
+
## Data Attributes
|
|
310
|
+
|
|
311
|
+
### HoverCardTrigger
|
|
312
|
+
`[data-slot="hover-card-trigger"]`
|
|
313
|
+
|
|
314
|
+
- `data-closed`: Present while the card is closed, including while it animates out.
|
|
315
|
+
- `data-open`: Present while the card is open. On a trigger, while it's open from that trigger.
|
|
316
|
+
|
|
317
|
+
### HoverCardContent
|
|
318
|
+
`[data-slot="hover-card-content"]`
|
|
319
|
+
|
|
320
|
+
- `data-closed`: Present while the card is closed, including while it animates out.
|
|
321
|
+
- `data-open`: Present while the card is open. On a trigger, while it's open from that trigger.
|
|
322
|
+
- `data-starting-style`: Present for the first frame. Style the state you animate from.
|
|
323
|
+
- `data-ending-style`: Present while the card animates out. Style the state you animate to.
|
|
324
|
+
|
|
325
|
+
### HoverCardArrow
|
|
326
|
+
`[data-slot="hover-card-arrow"]`
|
|
327
|
+
|
|
328
|
+
- `data-closed`: Present while the card is closed, including while it animates out.
|
|
329
|
+
- `data-open`: Present while the card is open. On a trigger, while it's open from that trigger.
|
|
330
|
+
|
|
331
|
+
## API Reference
|
|
332
|
+
|
|
333
|
+
Props are the same as below, and callbacks are events.
|
|
334
|
+
|
|
335
|
+
### HoverCard
|
|
336
|
+
|
|
337
|
+
- `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
|
|
338
|
+
- `open?` (`boolean`): Whether the card is open. Pass it to control the card.
|
|
339
|
+
- `defaultOpen?` (`boolean`): Whether the card starts open, when uncontrolled.
|
|
340
|
+
- `@open-change` (`((open: boolean, details: ChangeDetails<HoverCardOpenChangeReason>) => void)`): Runs when the card wants to open or close. `details.reason` says why, and `details.cancel()` stops it.
|
|
341
|
+
- `@open-change-complete` (`((open: boolean) => void)`): Runs once the card has finished opening or closing, after any animation.
|
|
342
|
+
- `@payload-change` (`((payload: unknown, details: ChangeDetails<HoverCardPayloadChangeReason>) => void)`): Runs when the payload the content shows changes: a trigger with another payload opens the card, or takes the open card over, which fires no `@open-change`, and so does a controller's `open({ payload })` on the open card. `details.cancel()` keeps the previous payload.
|
|
343
|
+
- `delay?` (`number`): Milliseconds the pointer rests on the trigger, or keyboard focus stays on it, before the card opens.
|
|
344
|
+
- `closeDelay?` (`number`): Milliseconds before the card closes once the pointer leaves the trigger and the card.
|
|
345
|
+
- `disabled?` (`boolean`): Stops the card from opening. The trigger keeps working as a link.
|
|
346
|
+
- `controller?` (`PopupController<any, HoverCardOpenChangeReason, {}, string, HoverCardPayloadChangeReason>`): Opens, closes and inspects the card from anywhere, and connects triggers outside it. Create one with `createHoverCardController()`.
|
|
347
|
+
- `debug?` (`boolean`): Logs every state change with its reason to the console, in development builds.
|
|
348
|
+
|
|
349
|
+
### HoverCardTrigger
|
|
350
|
+
|
|
351
|
+
A trigger outside the card also takes `controller`.
|
|
352
|
+
|
|
353
|
+
- `payload?` (`unknown`): Data for the content to render when this trigger opens the card, like the profile it links to.
|
|
354
|
+
- `controller?` (`PopupController<any, HoverCardOpenChangeReason, {}, string, HoverCardPayloadChangeReason>`): Connects a trigger outside the card, like every author link in a feed, to the card with this controller.
|
|
355
|
+
|
|
356
|
+
### HoverCardContent
|
|
357
|
+
|
|
358
|
+
- `side?` (`Side`): Which side of the anchor the popup opens on. Flips to the other side when there isn't room. Which side of the trigger the card opens on.
|
|
359
|
+
- `align?` (`Align`): How the popup lines up with the anchor along that side. `start` and `end` follow the reading direction. How the card lines up with the trigger along that side.
|
|
360
|
+
- `sideOffset?` (`number`): Gap between the anchor and the popup, in pixels. Gap between the trigger and the card, in pixels.
|
|
361
|
+
- `alignOffset?` (`number`): Shift along the anchor, in pixels. Shift along the trigger, in pixels.
|
|
362
|
+
- `collisionPadding?` (`number`): Space to keep from the viewport edges, in pixels.
|
|
363
|
+
- `arrowPadding?` (`number`): Space to keep between the arrow and the popup's corners, in pixels.
|
|
364
|
+
- `anchor?` (`string | Anchor | null`): Positions the popup against this instead of its trigger: an element, a CSS selector, a point like `{ x, y }`, or anything with `getBoundingClientRect()`. It's read on every check, so it can move. A controller's `open({ anchor })` wins for that open.
|
|
365
|
+
- `container?` (`DocumentFragment | Element | null`): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
|
|
366
|
+
- `keepMounted?` (`boolean`): Whether the card stays in the page, hidden, while closed. For animation libraries that drive the exit from state.
|
|
367
|
+
|
|
368
|
+
### HoverCardArrow
|
|
369
|
+
|
|
370
|
+
Points at the link from the side the card landed on. Renders a `div`.
|
|
371
|
+
|
|
372
|
+
### State
|
|
373
|
+
|
|
374
|
+
What render functions, state-based class names and slots receive, per part.
|
|
375
|
+
|
|
376
|
+
`HoverCardTrigger`:
|
|
377
|
+
|
|
378
|
+
- `open` (`boolean`): Whether the card is open from this trigger.
|
|
379
|
+
|
|
380
|
+
`HoverCardContent`:
|
|
381
|
+
|
|
382
|
+
- `open` (`boolean`): Whether the card is open. False while it animates out.
|
|
383
|
+
- `payload` (`Payload | undefined`): The payload of the last open, from the trigger that opened it or a controller, typed as the controller's `Payload`.
|
|
384
|
+
|
|
385
|
+
`HoverCardArrow`:
|
|
386
|
+
|
|
387
|
+
- `open` (`boolean`): Whether the card is open. False as soon as it starts closing, while it still animates out.
|
|
388
|
+
|
|
389
|
+
### Reasons
|
|
390
|
+
|
|
391
|
+
`details.reason` says why the card opened or closed.
|
|
392
|
+
|
|
393
|
+
- `trigger-hover` (`PointerEvent`): The pointer rested on a trigger, or left it, with hover opening.
|
|
394
|
+
- `trigger-focus` (`FocusEvent`): Keyboard focus reached a trigger that opens on focus, like a tooltip's.
|
|
395
|
+
- `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
|
|
396
|
+
- `focus-out` (`KeyboardEvent | FocusEvent`): Focus left the layer, or Tab moved it out.
|
|
397
|
+
- `escape-key` (`KeyboardEvent`): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
|
|
398
|
+
- `close-press` (`MouseEvent | KeyboardEvent | SubmitEvent`): A Close part was pressed, or a form with `method="dialog"` was submitted.
|
|
399
|
+
- `outside-press` (`MouseEvent`): A press outside the layer.
|
|
400
|
+
- `window-blur` (`FocusEvent`): The window lost focus.
|
|
401
|
+
- `back` (`Event`): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
|
|
402
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
403
|
+
|
|
404
|
+
For a payload change, it says what opened the card or took it over.
|
|
405
|
+
|
|
406
|
+
- `trigger-hover` (`PointerEvent`): The pointer rested on a trigger, or left it, with hover opening.
|
|
407
|
+
- `trigger-focus` (`FocusEvent`): Keyboard focus reached a trigger that opens on focus, like a tooltip's.
|
|
408
|
+
- `trigger-press` (`MouseEvent | KeyboardEvent | TouchEvent`): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its `touchend`.
|
|
409
|
+
- `imperative` (`none`): A controller or `api` call, or a property set on a web component.
|
|
410
|
+
|
|
411
|
+
### Controller
|
|
412
|
+
|
|
413
|
+
Create one with `createHoverCardController<Payload>()` and pass it as `controller`, to the card and to any
|
|
414
|
+
triggers outside it. `Payload` is the type of the data triggers and `open()` pass.
|
|
415
|
+
|
|
416
|
+
Every write method takes an optional `{ reason, event }`, passed to `@open-change`.
|
|
417
|
+
|
|
418
|
+
- `isOpen()` (`() => boolean`): Whether it's open. False as soon as it starts closing, while it still animates out.
|
|
419
|
+
- `setOpen(open, options?)` (`(open: boolean, options?: PopupChangeOptions<HoverCardOpenChangeReason> | undefined) => boolean`): Opens or closes it. Returns false when the change was canceled or it was already open or closed.
|
|
420
|
+
- `open(options?)` (`(options?: (PopupChangeOptions<HoverCardPayloadChangeReason> & { payload?: Payload | undefined; anchor?: Anchor | undefined; signal?: AbortSignal | undefined; }) | undefined) => Promise<...>`): Opens it, and resolves once it closes, with why, how and any return value. Resolves exactly once and never rejects: on the next close from any source, when `signal` aborts, or with reason `"unmount"` when the component leaves the page.
|
|
421
|
+
- `close(options?)` (`(options?: (PopupChangeOptions<HoverCardOpenChangeReason> & { returnValue?: string | undefined; }) | undefined) => boolean`): Closes it. `returnValue` is passed to the promise `open()` returned. Returns false when the close was canceled or it was already closed.
|
|
422
|
+
- `toggle(options?)` (`(options?: PopupChangeOptions<HoverCardOpenChangeReason> | undefined) => boolean`): Opens it when it's closed, and closes it when it's open. Returns false when the change was canceled.
|
|
423
|
+
- `getPayload()` (`() => Payload | undefined`): The payload of the last open, from a controller or the trigger that opened it.
|
|
424
|
+
- `focus()` (`() => void`): Moves focus to the trigger that opened it last, or the first trigger.
|
|
425
|
+
|
|
426
|
+
### Events
|
|
427
|
+
|
|
428
|
+
Events get the new state first, then the details. `v-model` only updates when `@open-change` wasn't canceled.
|
|
429
|
+
|
|
430
|
+
| Binding | On | Receives |
|
|
431
|
+
| ----------------------- | ----------- | --------------------------------------------------------------------------- |
|
|
432
|
+
| `v-model:open` | `HoverCard` | Whether the card is open |
|
|
433
|
+
| `@open-change` | `HoverCard` | `(open, details)`: the new state, and the [reason](#reasons) |
|
|
434
|
+
| `@open-change-complete` | `HoverCard` | `(open)`, after the animation |
|
|
435
|
+
| `@payload-change` | `HoverCard` | `(payload, details)`: the content's new payload, and the [reason](#reasons) |
|
|
436
|
+
|
|
437
|
+
## Accessibility
|
|
438
|
+
|
|
439
|
+
The trigger stays a plain link, and the card is a preview beside it, not a dialog. Every line below is an automated test.
|
|
440
|
+
|
|
441
|
+
### Keyboard
|
|
442
|
+
|
|
443
|
+
| Key | Does |
|
|
444
|
+
| ------ | ---------------------------------------------------------------------------- |
|
|
445
|
+
| Tab | On the link, opens the card after `delay`; with the card open, moves into it |
|
|
446
|
+
| Enter | Follows the link |
|
|
447
|
+
| Escape | Closes the card and leaves focus on the link |
|
|
448
|
+
|
|
449
|
+
Keyboard focus opens the card after the same `delay` as the pointer, so tabbing through a page of links doesn't flash a card at every stop. Focus leaving the card closes it.
|
|
450
|
+
|
|
451
|
+
Touch screens have no hover, so a tap follows the link without opening the card. Anything in the card should be on the page the link goes to.
|
|
452
|
+
|
|
453
|
+
### Tested Behavior
|
|
454
|
+
|
|
455
|
+
**Pointer**
|
|
456
|
+
|
|
457
|
+
- Resting the pointer on the link opens the card.
|
|
458
|
+
- The card waits for its delay before opening.
|
|
459
|
+
- Moving the pointer away closes the card after its close delay.
|
|
460
|
+
- Moving the pointer into the card keeps it open.
|
|
461
|
+
- Opening on hover leaves focus where it was.
|
|
462
|
+
- Clicking the link follows it.
|
|
463
|
+
|
|
464
|
+
**Keyboard**
|
|
465
|
+
|
|
466
|
+
- Keyboard focus on the link opens the card after the delay.
|
|
467
|
+
- Escape closes the card and leaves focus on the link.
|
|
468
|
+
- Enter follows the link.
|
|
469
|
+
|
|
470
|
+
**Positioning**
|
|
471
|
+
|
|
472
|
+
- The card opens below the link.
|
|
473
|
+
- The card opens on the requested side.
|
|
474
|
+
|
|
475
|
+
**Animation**
|
|
476
|
+
|
|
477
|
+
- The card animates in from its starting style.
|
|
478
|
+
- The card stays in the page while it animates out.
|
|
479
|
+
|
|
480
|
+
**Focus, touch and reasons**
|
|
481
|
+
|
|
482
|
+
- A click that focuses the link doesn't open the card from focus.
|
|
483
|
+
- Leaving the link before the delay keeps keyboard focus from opening it later.
|
|
484
|
+
- A tap follows the link without opening the card.
|
|
485
|
+
- Changes report why the card opened and closed.
|
|
486
|
+
|
|
487
|
+
Runs in React, Vue and web components.
|
|
488
|
+
|
|
489
|
+
Also tested:
|
|
490
|
+
|
|
491
|
+
**Enhancements**
|
|
492
|
+
|
|
493
|
+
- Tab from the link moves into the open card.
|
|
494
|
+
- Shift+Tab from the card returns to the link.
|
|
495
|
+
- Tab out of the card moves on past the link and closes it.
|
|
496
|
+
- Escape inside the card closes it and returns focus to the link.
|
|
497
|
+
- The card stays open while focus is in it, even when the pointer leaves.
|
|
498
|
+
|
|
499
|
+
Runs in React, Vue and web components.
|
|
500
|
+
|
|
501
|
+
Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:
|
|
502
|
+
|
|
503
|
+
- Announcements while tabbing into the card and out.
|
|
504
|
+
|
|
505
|
+
Runs in React, Vue and web components.
|