sb-floating-panel-vue 1.0.0 โ 4.0.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/README.md +305 -128
- package/dist/components/SbContainer.vue.d.ts +31 -61
- package/dist/components/SbFloating.vue.d.ts +22 -69
- package/dist/components/SbReference.vue.d.ts +11 -34
- package/dist/composables/useSbFloatingPanel.d.ts +26 -76
- package/dist/context.d.ts +32 -0
- package/dist/focus.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/sb-floating-panel.es.js +375 -218
- package/dist/sb-floating-panel.umd.js +1 -1
- package/dist/types.d.ts +200 -0
- package/package.json +32 -34
- package/dist/sb-floating-panel-vue.css +0 -1
package/README.md
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/sb-floating-panel-vue)
|
|
4
4
|
[](https://github.com/stefanBid/sb-floating-panel-vue/blob/main/LICENSE)
|
|
5
5
|
[](https://vuejs.org)
|
|
6
|
-
[](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
|
|
7
9
|
|
|
8
10
|
## ๐ฅ Features
|
|
9
11
|
|
|
10
|
-
- ๐งฉ
|
|
11
|
-
- โก Built on top of [`@floating-ui/vue`](https://floating-ui.com/)
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- ๐งฉ Three components (`SbContainer`, `SbReference`, `SbFloating`) for dropdowns, popovers and disclosure panels
|
|
13
|
+
- โก Built on top of [`@floating-ui/vue`](https://floating-ui.com/): `offset`, `flip`, `shift`, `size` and `hide` already configured
|
|
14
|
+
- ๐จ Zero CSS shipped: the library adapts to your design system (plain CSS, Tailwind, CSS modules)
|
|
15
|
+
- โฟ Accessible by default: ARIA attributes and naming, Tab order kept across the teleport, Escape, click outside, focus management
|
|
16
|
+
- ๐ Fully reactive props, `v-model:open`, nested panels
|
|
14
17
|
|
|
15
18
|
---
|
|
16
19
|
|
|
@@ -22,7 +25,7 @@ Created with โค๏ธ by [Stefano Biddau](https://www.stefano-biddau.com/)
|
|
|
22
25
|
|
|
23
26
|
## โ Why This Library?
|
|
24
27
|
|
|
25
|
-
`sb-floating-panel-vue`
|
|
28
|
+
`sb-floating-panel-vue` makes it easy to build floating panels in Vue 3 without touching positioning logic. You write three components; the library handles placement, flipping, viewport collisions, open state, accessibility and teleporting. It never decides how your panel looks: no stylesheet, no default visual style, only data attributes and CSS variables your styles can read.
|
|
26
29
|
|
|
27
30
|
---
|
|
28
31
|
|
|
@@ -30,197 +33,371 @@ Created with โค๏ธ by [Stefano Biddau](https://www.stefano-biddau.com/)
|
|
|
30
33
|
|
|
31
34
|
```bash
|
|
32
35
|
# npm
|
|
33
|
-
npm install sb-floating-panel-vue
|
|
36
|
+
npm install sb-floating-panel-vue vue @floating-ui/vue
|
|
34
37
|
|
|
35
38
|
# yarn
|
|
36
|
-
yarn add sb-floating-panel-vue
|
|
39
|
+
yarn add sb-floating-panel-vue vue @floating-ui/vue
|
|
37
40
|
|
|
38
41
|
# pnpm
|
|
39
|
-
pnpm add sb-floating-panel-vue
|
|
42
|
+
pnpm add sb-floating-panel-vue vue @floating-ui/vue
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
`vue` (`^3.5.13`) and `@floating-ui/vue` (`^2.0.1`) are peer dependencies: your app provides them, so only one copy of each ends up in the bundle. Vue is usually already installed in your app.
|
|
46
|
+
|
|
47
|
+
There is no CSS file to import.
|
|
48
|
+
|
|
42
49
|
---
|
|
43
50
|
|
|
44
|
-
##
|
|
51
|
+
## ๐งฑ Usage
|
|
52
|
+
|
|
53
|
+
```vue
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import { SbContainer, SbReference, SbFloating } from 'sb-floating-panel-vue';
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<SbContainer placement="bottom-start" match-width="min">
|
|
60
|
+
<template #default="{ close }">
|
|
61
|
+
<SbReference class="my-trigger">Account</SbReference>
|
|
62
|
+
<SbFloating transition="pop" class="my-panel">
|
|
63
|
+
<a href="/profile">Profile</a>
|
|
64
|
+
<button type="button" @click="close()">Sign out</button>
|
|
65
|
+
</SbFloating>
|
|
66
|
+
</template>
|
|
67
|
+
</SbContainer>
|
|
68
|
+
</template>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`SbContainer` shares its state with `SbReference` and `SbFloating` through `provide`/`inject`: no refs or styles to pass by hand. Both must be used inside an `SbContainer`.
|
|
72
|
+
|
|
73
|
+
### `<SbContainer>`
|
|
74
|
+
|
|
75
|
+
Holds the state and the positioning. Renders no element of its own, only its slot.
|
|
76
|
+
|
|
77
|
+
| Prop | Type | Default | Description |
|
|
78
|
+
|-----------------------|----------------------------------------|------------------|----------------------------------------------------------------------------------------------|
|
|
79
|
+
| `placement` | `SbPlacement` | `'bottom-start'` | Preferred placement; the panel flips when there is not enough space |
|
|
80
|
+
| `strategy` | `'absolute' \| 'fixed'` | `'absolute'` | CSS position strategy. Use `fixed` inside scroll containers or fixed layouts |
|
|
81
|
+
| `offset` | `number` | `8` | Distance in px between reference and panel |
|
|
82
|
+
| `padding` | `number` | `8` | Minimum distance in px between the panel and the viewport edges |
|
|
83
|
+
| `matchWidth` | `boolean \| 'min'` | `false` | `true` sets `width`, `'min'` sets `min-width` to the reference width |
|
|
84
|
+
| `hideWhenDetached` | `boolean` | `false` | Sets `visibility: hidden` when the reference is scrolled out of view |
|
|
85
|
+
| `v-model:open` | `boolean` | โ | Open state. Leave it unset to let the container manage it |
|
|
86
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state when `v-model:open` is not used |
|
|
87
|
+
| `disabled` | `boolean` | `false` | Prevents opening; closes the panel if it is open |
|
|
88
|
+
| `trigger` | `'click' \| 'manual'` | `'click'` | `manual`: the reference does not toggle; open with `v-model:open`, slot props or methods |
|
|
89
|
+
| `closeOnEscape` | `boolean` | `true` | Closes on Escape |
|
|
90
|
+
| `closeOnClickOutside` | `boolean` | `true` | Closes on a pointer press outside the reference and the panel (on touch, on the tap's click) |
|
|
91
|
+
| `closeOnFocusOut` | `boolean` | `true` | Closes when keyboard focus moves outside the reference and the panel |
|
|
92
|
+
|
|
93
|
+
| Event | Payload | Description |
|
|
94
|
+
|---------------|-----------|--------------------------|
|
|
95
|
+
| `update:open` | `boolean` | Used by `v-model:open` |
|
|
96
|
+
| `open` | โ | The panel has opened |
|
|
97
|
+
| `close` | โ | The panel has closed |
|
|
98
|
+
|
|
99
|
+
**Slot props**: `isOpen`, `open()`, `close()`, `toggle()`, `placement` (resolved after flip), `side`, `align`.
|
|
100
|
+
|
|
101
|
+
**Exposed methods** (template ref): `open()`, `close()`, `toggle()`, `update()` (recomputes the position).
|
|
45
102
|
|
|
46
|
-
|
|
103
|
+
### `<SbReference>`
|
|
47
104
|
|
|
48
|
-
|
|
105
|
+
The trigger the panel is anchored to.
|
|
49
106
|
|
|
107
|
+
| Prop | Type | Default | Description |
|
|
108
|
+
|----------------|-----------------------|----------------|--------------------------------------------------------------------------------------------------------------|
|
|
109
|
+
| `as` | `string \| Component` | `'button'` | Tag or component to render. `button` gets `type="button"`; `a` with `href` and components keep their own semantics; any other tag (also `a` without `href`) gets `role="button"`, `tabindex="0"` and Enter/Space |
|
|
110
|
+
| `id` | `string` | generated | `id` of the element, used to name the panel |
|
|
111
|
+
| `ariaHaspopup` | `'dialog' \| 'menu' \| 'listbox' \| 'tree' \| 'grid' \| 'true' \| 'false'` | follows `role` | Value of `aria-haspopup`. When unset it follows the `role` of the panel (`alertdialog` gives `dialog`) and is left out for other roles |
|
|
50
112
|
|
|
51
|
-
|
|
52
|
-
|------------|------------|----------|---------|--------------------------------------------------|
|
|
53
|
-
| placement | `Placement` | Yes | โ | Placement of the floating panel |
|
|
54
|
-
| strategy | `'absolute'` \| `'fixed'` | Yes | โ | CSS positioning strategy |
|
|
55
|
-
| offsetValue| `number` | Yes | โ | Offset between reference and floating |
|
|
56
|
-
| hasArrow | `boolean` | No | `false` | Whether to render arrow |
|
|
57
|
-
| hasResize | `boolean` | No | `false` | Sync width of floating to reference |
|
|
113
|
+
Sets `aria-expanded`, `aria-controls` (while open) and `data-sbf-state`. When the container is `disabled`, a `button` gets `disabled`, anything else gets `aria-disabled="true"` (and non-interactive tags `tabindex="-1"`). Your own `class`, attributes and listeners (`@click`, `@mouseenter`, โฆ) are applied to the element; the click is not stopped.
|
|
58
114
|
|
|
59
|
-
|
|
115
|
+
With a component (e.g. the `Button` of your design system), its root element becomes the reference, so the component must render a single root element that accepts the attributes and listeners above. While the panel is open, Tab on the reference moves focus into the panel.
|
|
60
116
|
|
|
61
|
-
|
|
62
|
-
|-----------------------|--------------------------------------|----------------------------------------|
|
|
63
|
-
| reference | `Ref<HTMLElement \| null>` | Ref for the trigger element |
|
|
64
|
-
| floating | `Ref<FloatingElement \| null>` | Ref for the floating panel |
|
|
65
|
-
| floatingArrow | `Ref<HTMLElement \| null>` | Ref for the arrow element |
|
|
66
|
-
| floatingPlacement | `Ref<Placement>` | The actual resolved placement |
|
|
67
|
-
| floatingStyle | `ComputedRef<CSSProperties>` | Style for the floating panel |
|
|
68
|
-
| floatingArrowStyle | `ComputedRef<CSSProperties>` | Style for the arrow |
|
|
69
|
-
| isOpen | `Ref<boolean>` | Panel visibility |
|
|
70
|
-
| toggle / open / close | `() => void` | Control methods to toggle visibility |
|
|
117
|
+
### `<SbFloating>`
|
|
71
118
|
|
|
119
|
+
The panel, teleported to `body`. It is rendered while open and stays mounted until its leave transition has finished.
|
|
72
120
|
|
|
73
|
-
|
|
74
|
-
|
|
121
|
+
| Prop | Type | Default | Description |
|
|
122
|
+
|--------------------|-------------------------|------------|-----------------------------------------------------------------------------------------------------|
|
|
123
|
+
| `as` | `string` | `'div'` | Tag of the panel element |
|
|
124
|
+
| `role` | `string \| null` | `'dialog'` | ARIA role of the panel; `null` renders no role (disclosure pattern) |
|
|
125
|
+
| `transition` | `string \| TransitionProps` | โ | Name of a Vue `<Transition>` defined in your CSS, or an object of `<Transition>` props (e.g. Tailwind classes). No animation when unset |
|
|
126
|
+
| `teleportTo` | `string \| HTMLElement` | `'body'` | Teleport target |
|
|
127
|
+
| `teleportDisabled` | `boolean` | `false` | Renders the panel in place |
|
|
128
|
+
| `zIndex` | `number \| string` | โ | `z-index` of the positioned wrapper. Use this, not a `z-*` class on the panel (see [Stacking](#stacking)) |
|
|
129
|
+
| `autoFocus` | `boolean` | `true` | Once positioned, focuses the first `[autofocus]` or focusable element inside, else the panel itself. Set it to `false` for panels that must not take focus, such as tooltips |
|
|
130
|
+
|
|
131
|
+
| Event | Description |
|
|
132
|
+
|---------------|----------------------------------------------------------------|
|
|
133
|
+
| `after-enter` | The enter transition has finished |
|
|
134
|
+
| `after-leave` | The leave transition has finished and the panel is removed |
|
|
135
|
+
|
|
136
|
+
With a popup role (`dialog`, `alertdialog`, `menu`, `listbox`, `tree`, `grid`), the panel is named after the reference with `aria-labelledby`, unless you pass `aria-label` or `aria-labelledby` yourself. When focus is inside the panel as it closes, it goes back to the reference.
|
|
137
|
+
|
|
138
|
+
### Types
|
|
75
139
|
|
|
76
|
-
In your script tag, import the composable and use it:
|
|
77
140
|
```ts
|
|
78
|
-
import {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
strategy: 'absolute',
|
|
91
|
-
offsetValue: 12,
|
|
92
|
-
hasArrow: true,
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
In your template, you can bind the refs and styles:
|
|
96
|
-
```html
|
|
97
|
-
<template>
|
|
98
|
-
<button ref="reference" @click="toggle()">Open Panel</button>
|
|
99
|
-
<div v-if="isOpen" ref="floating" :style="floatingStyle">
|
|
100
|
-
Panel
|
|
101
|
-
<div ref="floatingArrow" :style="floatingArrowStyle" />
|
|
102
|
-
</div>
|
|
103
|
-
</template>
|
|
141
|
+
import type {
|
|
142
|
+
SbContainerProps,
|
|
143
|
+
SbContainerSlotProps,
|
|
144
|
+
SbContainerExposed,
|
|
145
|
+
SbReferenceProps,
|
|
146
|
+
SbFloatingProps,
|
|
147
|
+
SbTrigger,
|
|
148
|
+
SbPlacement,
|
|
149
|
+
SbSide,
|
|
150
|
+
SbAlignment,
|
|
151
|
+
SbStrategy,
|
|
152
|
+
} from 'sb-floating-panel-vue';
|
|
104
153
|
```
|
|
105
154
|
|
|
106
155
|
---
|
|
107
156
|
|
|
108
|
-
##
|
|
157
|
+
## ๐งญ How It Works
|
|
109
158
|
|
|
110
|
-
###
|
|
159
|
+
### Shared state
|
|
111
160
|
|
|
112
|
-
|
|
161
|
+
`SbContainer` owns the state (open state, positioning, ARIA ids) and shares it with its descendants through Vue `provide`/`inject`:
|
|
113
162
|
|
|
114
|
-
```ts
|
|
115
|
-
import 'sb-floating-panel-vue/style.css';
|
|
116
163
|
```
|
|
164
|
+
SbContainer โโ provides the state โโโฌโโถ SbReference
|
|
165
|
+
โโโถ SbFloating
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- `SbReference` and `SbFloating` find their container on their own: you pass no refs, styles or handlers, and never write `.value`.
|
|
169
|
+
- The lookup follows the component tree, not the DOM, so it keeps working although `SbFloating` is teleported to `body`.
|
|
170
|
+
- They can sit at any depth inside the container slot, also inside your own components.
|
|
171
|
+
- Used outside an `SbContainer`, they throw: `[sb-floating-panel-vue] <SbFloating> must be used inside <SbContainer>.`
|
|
172
|
+
|
|
173
|
+
### Nested containers
|
|
174
|
+
|
|
175
|
+
An `SbContainer` placed inside another one (e.g. a submenu in a menu panel) registers with its parent:
|
|
176
|
+
|
|
177
|
+
- a press inside the nested panel is not "outside" for the parent, so both stay open;
|
|
178
|
+
- Escape closes the innermost open panel first, then the next one (more generally, the most recently opened panel, also between sibling containers);
|
|
179
|
+
- a press outside all of them closes them all.
|
|
117
180
|
|
|
118
|
-
###
|
|
181
|
+
### From click to open panel
|
|
119
182
|
|
|
120
|
-
|
|
183
|
+
1. **Click** on `SbReference` (or Enter/Space on a non-interactive tag). With `trigger="click"` and the container not `disabled`, the container toggles: it emits `update:open` and, without `v-model:open`, updates its own state.
|
|
184
|
+
2. **Open state changes.** `SbReference` gets `aria-expanded="true"`, `aria-controls` and `data-sbf-state="open"`; the container starts listening for Escape, outside presses and focus moving outside (only while open) and emits `open`.
|
|
185
|
+
3. **The panel mounts.** `SbFloating` renders the wrapper in the teleport target, with the panel and two hidden focus guards inside it; the enter classes of your `transition`, if any, are applied to the panel.
|
|
186
|
+
4. **Positioning.** Floating UI starts tracking the reference (scroll, resize, layout changes) and computes the position with `offset`, `flip`, `shift`, `size` and, with `hideWhenDetached`, `hide`. `size` writes the `--sbf-*` variables and applies `matchWidth`; the position is written on the wrapper.
|
|
187
|
+
5. **Positioned.** With `autoFocus` (the default), focus moves inside the panel now, not earlier, so the page does not jump to the panel's initial (0, 0) position.
|
|
121
188
|
|
|
122
|
-
|
|
189
|
+
Closing (click on the reference, `close()`, Escape, press outside, focus moving outside, `v-model:open` set to `false`, or `disabled`):
|
|
123
190
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| strategy | `'absolute' \| 'fixed'` | `'absolute'` | CSS positioning strategy for the floating panel |
|
|
128
|
-
| offsetValue | `number` | `15` | Offset in pixels between reference and floating panel |
|
|
129
|
-
| hasArrow | `boolean` | `false` | Whether to render an arrow pointing to the reference |
|
|
130
|
-
| hasResize | `boolean` | `false` | Whether the floating panel should sync its width with the reference |
|
|
191
|
+
1. If focus is inside the panel, it goes back to the reference without scrolling the page; it stays where it is when a press outside lands on another focusable element. The container emits `close`.
|
|
192
|
+
2. The panel gets `data-sbf-state="closed"` and the leave classes of your `transition`; the wrapper stays mounted and positioned until the animation ends (immediately without `transition`).
|
|
193
|
+
3. The wrapper is removed, Floating UI stops tracking, `SbFloating` emits `after-leave`.
|
|
131
194
|
|
|
195
|
+
---
|
|
132
196
|
|
|
197
|
+
## ๐จ Styling
|
|
133
198
|
|
|
134
|
-
|
|
199
|
+
The library writes values on the elements; your CSS reads them. `SbFloating` renders two elements:
|
|
135
200
|
|
|
136
|
-
|
|
201
|
+
```html
|
|
202
|
+
<!-- wrapper: positioning only (Floating UI styles, z-index, CSS variables) -->
|
|
203
|
+
<div style="position: absolute; transform: translate(120px, 40px); --sbf-reference-width: 180px; โฆ">
|
|
204
|
+
<!-- panel: your class, style and attributes -->
|
|
205
|
+
<div class="my-panel" role="dialog" data-sbf-state="open" data-sbf-side="bottom" data-sbf-align="start">
|
|
206
|
+
โฆ
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
```
|
|
137
210
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
|
211
|
+
Your styles, transitions and transforms go on the panel, so they never fight with the positioning `transform` of the wrapper.
|
|
212
|
+
|
|
213
|
+
### Data attributes
|
|
214
|
+
|
|
215
|
+
| Attribute | Values | On | Use |
|
|
216
|
+
|------------------|--------------------------------------|--------------------------|---------------------------------------------|
|
|
217
|
+
| `data-sbf-state` | `open` / `closed` | reference | Open/closed styles (e.g. rotate an icon) |
|
|
218
|
+
| `data-sbf-state` | `open` / `closed` | panel | `closed` from the start of the leave transition: style or animate the exit |
|
|
219
|
+
| `data-sbf-side` | `top` / `bottom` / `left` / `right` | panel | Real side after flip: directional animation |
|
|
220
|
+
| `data-sbf-align` | `start` / `end` / absent | panel | Alignment part of the placement |
|
|
221
|
+
|
|
222
|
+
### CSS variables
|
|
223
|
+
|
|
224
|
+
Set on the wrapper and inherited by the panel:
|
|
225
|
+
|
|
226
|
+
| Variable | Meaning |
|
|
227
|
+
|--------------------------|------------------------------------------------------|
|
|
228
|
+
| `--sbf-reference-width` | Width of the reference |
|
|
229
|
+
| `--sbf-available-width` | Horizontal space left before overflowing the viewport |
|
|
230
|
+
| `--sbf-available-height` | Vertical space left before overflowing the viewport |
|
|
231
|
+
| `--sbf-transform-origin` | Corner or edge of the panel facing the reference, for `transform-origin` |
|
|
232
|
+
|
|
233
|
+
All attributes and variables use the `sbf` prefix, so they do not clash with other libraries in the same app (e.g. Radix / Reka UI `data-state`).
|
|
234
|
+
|
|
235
|
+
### Stacking
|
|
236
|
+
|
|
237
|
+
The wrapper is positioned with a CSS `transform`, which creates a stacking context: a `z-index` on the panel (e.g. a `z-50` class) only orders it inside the wrapper and has no effect on the rest of the page. Set the `zIndex` prop of `SbFloating` instead, which goes on the wrapper.
|
|
238
|
+
|
|
239
|
+
### Plain CSS
|
|
240
|
+
|
|
241
|
+
Panels are teleported to `body`, so their styles cannot be `scoped` to the component that renders them: use global styles, CSS modules or utility classes.
|
|
242
|
+
|
|
243
|
+
```css
|
|
244
|
+
.my-panel {
|
|
245
|
+
background: white;
|
|
246
|
+
border: 1px solid #ddd;
|
|
247
|
+
border-radius: 8px;
|
|
248
|
+
max-height: var(--sbf-available-height);
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.my-trigger[data-sbf-state='open'] .chevron {
|
|
253
|
+
transform: rotate(180deg);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* <SbFloating transition="pop"> */
|
|
257
|
+
.pop-enter-active,
|
|
258
|
+
.pop-leave-active {
|
|
259
|
+
transform-origin: var(--sbf-transform-origin);
|
|
260
|
+
transition: opacity 150ms ease, transform 150ms ease;
|
|
261
|
+
}
|
|
262
|
+
.pop-enter-from,
|
|
263
|
+
.pop-leave-to {
|
|
264
|
+
opacity: 0;
|
|
265
|
+
transform: scale(0.96);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* direction from the side resolved after flip */
|
|
269
|
+
.pop-enter-from[data-sbf-side='bottom'] { transform: translateY(-6px); }
|
|
270
|
+
.pop-enter-from[data-sbf-side='top'] { transform: translateY(6px); }
|
|
271
|
+
|
|
272
|
+
/* respect the user's motion setting */
|
|
273
|
+
@media (prefers-reduced-motion: reduce) {
|
|
274
|
+
.pop-enter-active,
|
|
275
|
+
.pop-leave-active {
|
|
276
|
+
transition: opacity 150ms ease;
|
|
277
|
+
}
|
|
278
|
+
.pop-enter-from,
|
|
279
|
+
.pop-leave-to {
|
|
280
|
+
transform: none;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
143
284
|
|
|
285
|
+
### Tailwind (v4)
|
|
144
286
|
|
|
145
|
-
|
|
287
|
+
```vue
|
|
288
|
+
<SbReference class="group rounded border px-3 py-2">
|
|
289
|
+
Options
|
|
290
|
+
<span class="inline-block transition-transform group-data-[sbf-state=open]:rotate-180">โพ</span>
|
|
291
|
+
</SbReference>
|
|
292
|
+
<SbFloating
|
|
293
|
+
:z-index="50"
|
|
294
|
+
:transition="{
|
|
295
|
+
enterActiveClass: 'transition duration-150 ease-out motion-reduce:transition-none',
|
|
296
|
+
enterFromClass: 'opacity-0 scale-95',
|
|
297
|
+
leaveActiveClass: 'transition duration-100 ease-in motion-reduce:transition-none',
|
|
298
|
+
leaveToClass: 'opacity-0 scale-95',
|
|
299
|
+
}"
|
|
300
|
+
class="max-h-(--sbf-available-height) w-(--sbf-reference-width) origin-(--sbf-transform-origin) overflow-y-auto rounded border bg-white shadow-lg"
|
|
301
|
+
>
|
|
302
|
+
โฆ
|
|
303
|
+
</SbFloating>
|
|
304
|
+
```
|
|
146
305
|
|
|
147
|
-
|
|
306
|
+
---
|
|
148
307
|
|
|
149
|
-
|
|
150
|
-
|---------------------|----------------------------------------------|-----------------|-----------------------------------------------------------------------------|
|
|
151
|
-
| floatingRef | `Ref<HTMLElement \| null>` | **required** | The target element for the floating panel (popover, tooltip, etc.) |
|
|
152
|
-
| floatingArrowRef | `Ref<HTMLElement \| null>` | optional | The element used as the floating arrow |
|
|
153
|
-
| isOpen | `boolean` | **required** | Controls visibility of the floating panel |
|
|
154
|
-
| floatingPlacement | `Placement` | **required** | Current placement as resolved by `@floating-ui/vue` |
|
|
155
|
-
| floatingStyle | `CSSProperties` | **required** | Inline style object applied to the floating panel |
|
|
156
|
-
| floatingArrowStyle | `CSSProperties` | optional | Inline style object applied to the arrow element |
|
|
157
|
-
| arrowDimensions | `number` | optional | Size (width/height) of the arrow in pixels |
|
|
158
|
-
| zIndex | `number` | `undefined` | Custom z-index applied to the floating and arrow elements |
|
|
159
|
-
| animation | `'fade' \| 'scale-fade' \| 'none'` | `'scale-fade'` | Defines the animation style for enter/leave transitions |
|
|
160
|
-
| duration | `number` | `300` | Duration in ms for the transition animations |
|
|
308
|
+
## โฟ Accessibility
|
|
161
309
|
|
|
310
|
+
- `SbReference` gets an `id`, `aria-expanded`, `aria-controls` (pointing to the panel `id`) and `aria-haspopup` (following the panel `role`); non-interactive tags get `role="button"`, `tabindex="0"` and Enter/Space.
|
|
311
|
+
- `SbFloating` gets an `id` generated with Vue `useId()` (SSR safe), a `role` (`dialog` by default) and, with a popup role (`dialog`, `alertdialog`, `menu`, `listbox`, `tree`, `grid`), `aria-labelledby` pointing to the reference.
|
|
312
|
+
- **Tab order.** The panel is teleported to the end of `body`, so the library keeps the order a reader expects: Tab on the open reference enters the panel, Shift+Tab from its first element returns to the reference, Tab from its last element moves on to what follows the reference. Two hidden focus guards (`data-sbf-focus-guard`) around the panel do this; they are not rendered with `teleportDisabled`.
|
|
313
|
+
- **Focus.** By default (`autoFocus`) focus moves inside the panel once it is positioned. When the panel closes with focus inside it, focus returns to the reference without scrolling the page.
|
|
314
|
+
- **Dismissal.**
|
|
315
|
+
- Escape closes the most recently opened panel. It is ignored when a widget inside has already handled it (`event.preventDefault()`) and while an IME composition is in progress.
|
|
316
|
+
- A press outside closes the panel; on touch, only a tap does, so starting a scroll keeps it open.
|
|
317
|
+
- Moving focus outside (e.g. tabbing past the last element) closes it.
|
|
318
|
+
- A container nested inside a panel counts as part of it.
|
|
319
|
+
- **What is up to you.** The library does not implement the keyboard model of composite widgets: for `role="menu"`, `listbox` or `grid` panels, add the arrow-key navigation, Home/End and typeahead those roles promise to screen-reader users, and use valid structure (`menuitem` elements directly inside `menu`, no `ul > li` in between). A tooltip panel should use `role="tooltip"` and `:auto-focus="false"`, with `aria-describedby` on the trigger pointing to an element with your own `id` inside the panel (the [playground](playground/App.vue) has an example).
|
|
162
320
|
|
|
163
321
|
---
|
|
164
322
|
|
|
165
|
-
|
|
323
|
+
## ๐ Migrating from v3
|
|
166
324
|
|
|
167
|
-
|
|
325
|
+
The next major release changes these defaults:
|
|
168
326
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
327
|
+
- **`autoFocus` is `true`.** Focus moves inside the panel when it opens. Set `:auto-focus="false"` on panels that must not take focus, such as tooltips.
|
|
328
|
+
- **`ariaHaspopup` follows the panel `role`** instead of defaulting to `dialog`: drop the matching `aria-haspopup` you set by hand, keep it only where it differs from the role.
|
|
329
|
+
- **Focus moving outside closes the panel.** Set `:close-on-focus-out="false"` to keep the previous behaviour.
|
|
330
|
+
- **The panel is named after the reference** with `aria-labelledby`; pass `aria-label` or `aria-labelledby` to name it differently.
|
|
331
|
+
- **Escape** no longer closes the panel when a widget inside called `preventDefault()` on it, and it now calls `preventDefault()` itself.
|
|
332
|
+
- **`data-sbf-state` on the panel** becomes `closed` during the leave transition.
|
|
173
333
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## ๐ Migrating from v2
|
|
337
|
+
|
|
338
|
+
v3 is a breaking release.
|
|
339
|
+
|
|
340
|
+
- **The composable is no longer exported.** Use the components; for full control use `@floating-ui/vue` directly.
|
|
341
|
+
- **No refs or styles in the slot.** Remove `reference-ref`, `floating-ref`, `is-open`, `floating-placement`, `floating-style` and `:on-click`: the components share their state.
|
|
342
|
+
- **No arrow.** `hasArrow`, `floating-arrow-ref`, `floating-arrow-style` and `arrow-dimensions` are gone.
|
|
343
|
+
- **No CSS.** Remove `import 'sb-floating-panel-vue/style.css'`. `animation` and `duration` are replaced by `transition`, the name of your own `<Transition>` classes.
|
|
344
|
+
- **Renamed props:** `offsetValue` โ `offset` (default `15` โ `8`), `hasResize` โ `matchWidth`.
|
|
345
|
+
- **New defaults:** `padding: 8` from the viewport edges; Escape and click outside close the panel.
|
|
346
|
+
- `SbContainer` no longer renders a `<div>`; `SbReference` no longer stops click propagation and sets `type="button"`.
|
|
347
|
+
|
|
348
|
+
```vue
|
|
349
|
+
<!-- v2 -->
|
|
350
|
+
<SbContainer :has-resize="true">
|
|
351
|
+
<template #default="{ reference, floating, floatingPlacement, floatingStyle, isOpen, toggle }">
|
|
352
|
+
<SbReference :reference-ref="reference" :on-click="toggle">Toggle</SbReference>
|
|
188
353
|
<SbFloating
|
|
189
354
|
:is-open="isOpen.value"
|
|
190
355
|
:floating-ref="floating"
|
|
191
|
-
:floating-arrow-ref="floatingArrow"
|
|
192
356
|
:floating-placement="floatingPlacement.value"
|
|
193
357
|
:floating-style="floatingStyle.value"
|
|
194
|
-
:floating-arrow-style="floatingArrowStyle.value"
|
|
195
|
-
:arrow-dimensions="12"
|
|
196
|
-
:z-index="200"
|
|
197
358
|
>
|
|
198
359
|
Hello Panel!
|
|
199
360
|
</SbFloating>
|
|
200
361
|
</template>
|
|
201
362
|
</SbContainer>
|
|
363
|
+
|
|
364
|
+
<!-- v3 -->
|
|
365
|
+
<SbContainer match-width>
|
|
366
|
+
<SbReference>Toggle</SbReference>
|
|
367
|
+
<SbFloating>Hello Panel!</SbFloating>
|
|
368
|
+
</SbContainer>
|
|
202
369
|
```
|
|
203
370
|
|
|
204
371
|
---
|
|
205
372
|
|
|
206
373
|
## โ ๏ธ Directive Note (Teleport Limitation)
|
|
207
374
|
|
|
208
|
-
|
|
375
|
+
`<SbFloating>` renders a `<teleport>` root, so a custom directive applied directly to the component cannot be inherited and Vue warns about it. Move the directive to an element inside the slot:
|
|
209
376
|
|
|
210
|
-
```
|
|
211
|
-
|
|
377
|
+
```vue
|
|
378
|
+
<SbFloating>
|
|
379
|
+
<div v-my-directive>...</div>
|
|
380
|
+
</SbFloating>
|
|
212
381
|
```
|
|
213
382
|
|
|
214
|
-
|
|
383
|
+
---
|
|
215
384
|
|
|
216
|
-
|
|
385
|
+
## ๐ ๏ธ Development
|
|
217
386
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
387
|
+
```bash
|
|
388
|
+
npm install
|
|
389
|
+
npm run dev # playground at http://localhost:5173 (playground/App.vue)
|
|
390
|
+
npm run lint # ESLint
|
|
391
|
+
npm run check # type check of src/, playground/ and test/
|
|
392
|
+
npm test # Vitest + @vue/test-utils (happy-dom)
|
|
393
|
+
npm run test:coverage # tests with V8 coverage report in coverage/
|
|
394
|
+
npm run build # library build in dist/ (ES and UMD, with .d.ts)
|
|
222
395
|
```
|
|
223
396
|
|
|
397
|
+
The playground imports the components from `src/`, so changes reload instantly. It is not part of the published package.
|
|
398
|
+
|
|
399
|
+
On every push to `main`, CI runs the tests with coverage and `scripts/coverage-badge.mjs` publishes the coverage badges (lines, statements, branches, functions) as SVG files on the `badges` branch, which the badges above read. No external service is involved.
|
|
400
|
+
|
|
224
401
|
---
|
|
225
402
|
|
|
226
403
|
## ๐ License
|
|
@@ -1,65 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
* Props for the SbContainer component.
|
|
4
|
-
* Serves as a controller and provider for the floating panel system.
|
|
5
|
-
*/
|
|
6
|
-
interface SbContainerProps {
|
|
7
|
-
/**
|
|
8
|
-
* Placement of the floating panel relative to the reference.
|
|
9
|
-
* @default 'bottom-start'
|
|
10
|
-
*/
|
|
11
|
-
placement?: Placement;
|
|
12
|
-
/**
|
|
13
|
-
* Positioning strategy to use ('absolute' or 'fixed').
|
|
14
|
-
* @default 'absolute'
|
|
15
|
-
*/
|
|
16
|
-
strategy?: Strategy;
|
|
17
|
-
/**
|
|
18
|
-
* Pixel offset between the reference and floating panel.
|
|
19
|
-
* @default 15
|
|
20
|
-
*/
|
|
21
|
-
offsetValue?: number;
|
|
22
|
-
/**
|
|
23
|
-
* Whether to render an arrow pointing toward the reference element.
|
|
24
|
-
* @default false
|
|
25
|
-
*/
|
|
26
|
-
hasArrow?: boolean;
|
|
27
|
-
/**
|
|
28
|
-
* When true, syncs the floating panel width to the reference width.
|
|
29
|
-
* @default false
|
|
30
|
-
*/
|
|
31
|
-
hasResize?: boolean;
|
|
32
|
-
}
|
|
33
|
-
declare function __VLS_template(): {
|
|
34
|
-
attrs: Partial<{}>;
|
|
35
|
-
slots: {
|
|
36
|
-
default?(_: {
|
|
37
|
-
reference: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
|
|
38
|
-
floating: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
|
|
39
|
-
floatingArrow: import('vue').Ref<HTMLElement | null, HTMLElement | null>;
|
|
40
|
-
floatingPlacement: import('vue').Ref<Placement, Placement>;
|
|
41
|
-
floatingStyle: import('vue').ComputedRef<import('vue').CSSProperties>;
|
|
42
|
-
floatingArrowStyle: import('vue').ComputedRef<import('vue').CSSProperties>;
|
|
43
|
-
isOpen: import('vue').Ref<boolean, boolean>;
|
|
44
|
-
toggle: () => void;
|
|
45
|
-
open: () => void;
|
|
46
|
-
close: () => void;
|
|
47
|
-
}): any;
|
|
48
|
-
};
|
|
49
|
-
refs: {};
|
|
50
|
-
rootEl: any;
|
|
51
|
-
};
|
|
52
|
-
type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
|
|
53
|
-
declare const __VLS_component: import('vue').DefineComponent<SbContainerProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<SbContainerProps> & Readonly<{}>, {
|
|
54
|
-
placement: Placement;
|
|
55
|
-
strategy: Strategy;
|
|
56
|
-
offsetValue: number;
|
|
57
|
-
hasArrow: boolean;
|
|
58
|
-
hasResize: boolean;
|
|
59
|
-
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
60
|
-
declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
|
|
1
|
+
import { SbContainerExposed, SbContainerProps, SbContainerSlotProps } from '../types';
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
61
3
|
export default _default;
|
|
62
|
-
|
|
4
|
+
declare const __VLS_export: __VLS_WithSlots<import('vue').DefineComponent<Omit<SbContainerProps, "open"> & {
|
|
5
|
+
open?: boolean | undefined;
|
|
6
|
+
}, SbContainerExposed, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
7
|
+
open: () => any;
|
|
8
|
+
"update:open": (value: boolean | undefined) => any;
|
|
9
|
+
close: () => any;
|
|
10
|
+
}, string, import('vue').PublicProps, Readonly<Omit<SbContainerProps, "open"> & {
|
|
11
|
+
open?: boolean | undefined;
|
|
12
|
+
}> & Readonly<{
|
|
13
|
+
onOpen?: (() => any) | undefined;
|
|
14
|
+
"onUpdate:open"?: ((value: boolean | undefined) => any) | undefined;
|
|
15
|
+
onClose?: (() => any) | undefined;
|
|
16
|
+
}>, {
|
|
17
|
+
placement: import('..').SbPlacement;
|
|
18
|
+
strategy: import('..').SbStrategy;
|
|
19
|
+
padding: number;
|
|
20
|
+
offset: number;
|
|
21
|
+
matchWidth: boolean | "min";
|
|
22
|
+
hideWhenDetached: boolean;
|
|
23
|
+
defaultOpen: boolean;
|
|
24
|
+
disabled: boolean;
|
|
25
|
+
trigger: import('..').SbTrigger;
|
|
26
|
+
closeOnEscape: boolean;
|
|
27
|
+
closeOnClickOutside: boolean;
|
|
28
|
+
closeOnFocusOut: boolean;
|
|
29
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>, {
|
|
30
|
+
default?: (props: SbContainerSlotProps) => unknown;
|
|
31
|
+
}>;
|
|
32
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
63
33
|
new (): {
|
|
64
34
|
$slots: S;
|
|
65
35
|
};
|