sb-floating-panel-vue 4.0.0 → 5.0.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/README.md +175 -108
- package/dist/components/SbContainer.vue.d.ts +7 -3
- package/dist/components/SbFloating.vue.d.ts +6 -6
- package/dist/components/SbReference.vue.d.ts +2 -2
- package/dist/{context.d.ts → composables/useSbFloatingContext.d.ts} +20 -7
- package/dist/composables/useSbFloatingPanel.d.ts +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/sb-floating-panel.es.js +331 -182
- package/dist/sb-floating-panel.umd.js +1 -1
- package/dist/types.d.ts +59 -15
- package/dist/utils/aria.d.ts +5 -0
- package/dist/{focus.d.ts → utils/dom.d.ts} +2 -0
- package/dist/utils/navigation.d.ts +16 -0
- package/dist/utils/warn.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,25 +7,15 @@
|
|
|
7
7
|
[](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
|
|
8
8
|
[](https://github.com/stefanBid/sb-floating-panel-vue/actions/workflows/ci.yml)
|
|
9
9
|
|
|
10
|
+
Headless Vue 3 components for floating panels (dropdowns, menus, listboxes, popovers, tooltips) built on [`@floating-ui/vue`](https://floating-ui.com/). You write three components; the library handles placement, flipping, viewport collisions, open state, keyboard and accessibility. It never decides how your panel looks: no stylesheet, only data attributes and CSS variables your styles can read.
|
|
11
|
+
|
|
10
12
|
## 🔥 Features
|
|
11
13
|
|
|
12
|
-
- 🧩 Three components (`SbContainer`, `SbReference`, `SbFloating`) for dropdowns, popovers and disclosure panels
|
|
14
|
+
- 🧩 Three components (`SbContainer`, `SbReference`, `SbFloating`) for dropdowns, menus, listboxes, popovers, tooltips and disclosure panels
|
|
13
15
|
- ⚡ Built on top of [`@floating-ui/vue`](https://floating-ui.com/): `offset`, `flip`, `shift`, `size` and `hide` already configured
|
|
14
16
|
- 🎨 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
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## 👤 Author
|
|
21
|
-
|
|
22
|
-
Created with ❤️ by [Stefano Biddau](https://www.stefano-biddau.com/)
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## ❓ Why This Library?
|
|
27
|
-
|
|
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.
|
|
17
|
+
- ♿ Accessible by default: ARIA attributes and naming, the WAI-ARIA keyboard model of menus and listboxes, tooltips that meet WCAG 1.4.13, Tab order kept across the teleport, Escape, click outside, focus management
|
|
18
|
+
- 🔁 Fully reactive props, `v-model:open`, nested panels and submenus
|
|
29
19
|
|
|
30
20
|
---
|
|
31
21
|
|
|
@@ -50,45 +40,102 @@ There is no CSS file to import.
|
|
|
50
40
|
|
|
51
41
|
## 🧱 Usage
|
|
52
42
|
|
|
43
|
+
A popover: the panel opens on click, takes focus and gives it back to the trigger when it closes.
|
|
44
|
+
|
|
53
45
|
```vue
|
|
54
46
|
<script setup lang="ts">
|
|
55
47
|
import { SbContainer, SbReference, SbFloating } from 'sb-floating-panel-vue';
|
|
56
48
|
</script>
|
|
57
49
|
|
|
58
50
|
<template>
|
|
59
|
-
<SbContainer placement="bottom-start"
|
|
51
|
+
<SbContainer placement="bottom-start">
|
|
60
52
|
<template #default="{ close }">
|
|
61
|
-
<SbReference class="my-trigger">
|
|
53
|
+
<SbReference class="my-trigger">Share</SbReference>
|
|
62
54
|
<SbFloating transition="pop" class="my-panel">
|
|
63
|
-
<
|
|
64
|
-
<
|
|
55
|
+
<label for="link">Link</label>
|
|
56
|
+
<input id="link" value="https://example.com" readonly />
|
|
57
|
+
<button type="button" @click="close()">Done</button>
|
|
65
58
|
</SbFloating>
|
|
66
59
|
</template>
|
|
67
60
|
</SbContainer>
|
|
68
61
|
</template>
|
|
69
62
|
```
|
|
70
63
|
|
|
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
|
|
64
|
+
`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`, one of each per container.
|
|
65
|
+
|
|
66
|
+
### Menu
|
|
67
|
+
|
|
68
|
+
With `role="menu"` the library handles the keyboard: ArrowDown on the trigger opens the menu on its first item, the arrow keys, Home, End and typing move between items, Tab leaves (see [Keyboard](#keyboard)). Activating an item is up to its `click` handler.
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<SbContainer match-width="min">
|
|
72
|
+
<template #default="{ close }">
|
|
73
|
+
<SbReference class="my-trigger">Account</SbReference>
|
|
74
|
+
<SbFloating role="menu" class="my-panel">
|
|
75
|
+
<button v-for="item in ['Profile', 'Settings', 'Sign out']" :key="item" type="button" role="menuitem" @click="close()">
|
|
76
|
+
{{ item }}
|
|
77
|
+
</button>
|
|
78
|
+
</SbFloating>
|
|
79
|
+
</template>
|
|
80
|
+
</SbContainer>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A submenu is a menu `SbContainer` inside the menu panel, with `role="menuitem"` on its `SbReference`.
|
|
84
|
+
|
|
85
|
+
### Tooltip
|
|
86
|
+
|
|
87
|
+
```vue
|
|
88
|
+
<SbContainer placement="top" trigger="hover" :offset="6">
|
|
89
|
+
<SbReference class="my-trigger">Save</SbReference>
|
|
90
|
+
<SbFloating role="tooltip" class="my-tooltip">Saves a copy to your drive</SbFloating>
|
|
91
|
+
</SbContainer>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The tooltip opens on hover after `delay.open` and at once on keyboard focus, describes the trigger with `aria-describedby`, never takes focus, and stays open while the pointer moves onto it.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 📚 API
|
|
72
99
|
|
|
73
100
|
### `<SbContainer>`
|
|
74
101
|
|
|
75
102
|
Holds the state and the positioning. Renders no element of its own, only its slot.
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
|
91
|
-
|
|
104
|
+
**Positioning**
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Default | Description |
|
|
107
|
+
|--------------------|-------------------------|------------------|--------------------------------------------------------------------------------------|
|
|
108
|
+
| `placement` | `SbPlacement` | `'bottom-start'` | Preferred placement; the panel flips when there is not enough space |
|
|
109
|
+
| `strategy` | `'absolute' \| 'fixed'` | `'absolute'` | CSS position strategy. Use `fixed` inside scroll containers or fixed layouts |
|
|
110
|
+
| `offset` | `number` | `8` | Distance in px between reference and panel |
|
|
111
|
+
| `padding` | `number` | `8` | Minimum distance in px between the panel and the viewport edges |
|
|
112
|
+
| `matchWidth` | `boolean \| 'min'` | `false` | `true` sets `width`, `'min'` sets `min-width` to the reference width |
|
|
113
|
+
| `hideWhenDetached` | `boolean \| 'close'` | `false` | When the reference is scrolled out of view: `true` hides the panel (`visibility: hidden`) until it is back, `'close'` closes it |
|
|
114
|
+
|
|
115
|
+
**Open state**
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Default | Description |
|
|
118
|
+
|----------------|-----------|---------|--------------------------------------------------------------|
|
|
119
|
+
| `v-model:open` | `boolean` | — | Open state. Leave it unset to let the container manage it |
|
|
120
|
+
| `defaultOpen` | `boolean` | `false` | Initial open state when `v-model:open` is not used |
|
|
121
|
+
| `disabled` | `boolean` | `false` | Prevents opening; closes the panel if it is open |
|
|
122
|
+
| `trigger` | `SbTrigger` | `'click'` | How the reference opens the panel, see below |
|
|
123
|
+
| `delay` | `number \| { open?: number; close?: number }` | `{ open: 300, close: 150 }` | Milliseconds before the `hover` trigger opens and closes; a number sets both |
|
|
124
|
+
|
|
125
|
+
| `trigger` | Opens | Closes |
|
|
126
|
+
|------------|----------------------------------------------------------|------------------------------------------------------------------------|
|
|
127
|
+
| `'click'` | Click (Enter/Space) on the reference toggles the panel | Same, or any dismissal below |
|
|
128
|
+
| `'hover'` | Pointer over the reference after `delay.open`; keyboard focus at once (touch has no hover) | `delay.close` after the pointer leaves the reference and the panel, or when focus moves elsewhere |
|
|
129
|
+
| `'focus'` | Focus on the reference | Focus moving elsewhere |
|
|
130
|
+
| `'manual'` | Only `v-model:open`, the slot props or the exposed methods | Same, or any dismissal below |
|
|
131
|
+
|
|
132
|
+
**Dismissal**
|
|
133
|
+
|
|
134
|
+
| Prop | Type | Default | Description |
|
|
135
|
+
|-----------------------|-----------|---------|----------------------------------------------------------------------------------------------|
|
|
136
|
+
| `closeOnEscape` | `boolean` | `true` | Closes on Escape |
|
|
137
|
+
| `closeOnClickOutside` | `boolean` | `true` | Closes on a pointer press outside the reference and the panel (on touch, on the tap's click) |
|
|
138
|
+
| `closeOnFocusOut` | `boolean` | `true` | Closes when keyboard focus moves outside the reference and the panel |
|
|
92
139
|
|
|
93
140
|
| Event | Payload | Description |
|
|
94
141
|
|---------------|-----------|--------------------------|
|
|
@@ -96,9 +143,9 @@ Holds the state and the positioning. Renders no element of its own, only its slo
|
|
|
96
143
|
| `open` | — | The panel has opened |
|
|
97
144
|
| `close` | — | The panel has closed |
|
|
98
145
|
|
|
99
|
-
**Slot props**: `isOpen`, `open()`, `close()`, `toggle()`, `placement` (resolved after flip), `side`, `align
|
|
146
|
+
**Slot props**: `isOpen`, `open()`, `close()`, `toggle()`, `placement` (resolved after flip), `side`, `align`, `floatingId` and `referenceId` (the `id` of the panel and of the reference, for ARIA attributes of your own).
|
|
100
147
|
|
|
101
|
-
**Exposed
|
|
148
|
+
**Exposed** (template ref): `isOpen`, `open()`, `close()`, `toggle()`, `update()` (recomputes the position).
|
|
102
149
|
|
|
103
150
|
### `<SbReference>`
|
|
104
151
|
|
|
@@ -110,23 +157,23 @@ The trigger the panel is anchored to.
|
|
|
110
157
|
| `id` | `string` | generated | `id` of the element, used to name the panel |
|
|
111
158
|
| `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 |
|
|
112
159
|
|
|
113
|
-
Sets `aria-expanded`, `aria-controls` (while open) and `data-sbf-state
|
|
160
|
+
Sets `aria-expanded`, `aria-controls` (while open) and `data-sbf-state`; a tooltip reference gets `aria-describedby` instead (see [Tooltip](#tooltip-1)). 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.
|
|
114
161
|
|
|
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.
|
|
162
|
+
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. If the reference unmounts while the panel is open, the panel closes.
|
|
116
163
|
|
|
117
164
|
### `<SbFloating>`
|
|
118
165
|
|
|
119
166
|
The panel, teleported to `body`. It is rendered while open and stays mounted until its leave transition has finished.
|
|
120
167
|
|
|
121
|
-
| Prop
|
|
122
|
-
|
|
123
|
-
| `as`
|
|
124
|
-
| `role`
|
|
125
|
-
| `transition`
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
168
|
+
| Prop | Type | Default | Description |
|
|
169
|
+
|----------------------|----------------------------------|------------|-------------|
|
|
170
|
+
| `as` | `string \| Component` | `'div'` | Tag or component of the panel element; a component must render a single root element |
|
|
171
|
+
| `role` | `SbFloatingRole \| null` | `'dialog'` | ARIA role of the panel; `null` renders no role (disclosure pattern). Known roles are suggested by your editor, any string is accepted |
|
|
172
|
+
| `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 |
|
|
173
|
+
| `teleport` | `string \| HTMLElement \| false` | `'body'` | Teleport target (selector or element); `false` renders the panel in place |
|
|
174
|
+
| `zIndex` | `number \| string` | — | `z-index` of the positioned wrapper. Use this, not a `z-*` class on the panel (see [Stacking](#stacking)) |
|
|
175
|
+
| `autoFocus` | `boolean` | `true`, `false` with `role="tooltip"` or the `hover` / `focus` trigger | Once positioned, focuses the first `[autofocus]` or focusable element inside, else the panel itself (a menu: its first item; a listbox: its selected option) |
|
|
176
|
+
| `keyboardNavigation` | `boolean` | `true` | Handles the keyboard model of `role="menu"` and `role="listbox"` panels (see [Keyboard](#keyboard)). No effect with other roles |
|
|
130
177
|
|
|
131
178
|
| Event | Description |
|
|
132
179
|
|---------------|----------------------------------------------------------------|
|
|
@@ -149,48 +196,37 @@ import type {
|
|
|
149
196
|
SbSide,
|
|
150
197
|
SbAlignment,
|
|
151
198
|
SbStrategy,
|
|
199
|
+
SbFloatingRole,
|
|
152
200
|
} from 'sb-floating-panel-vue';
|
|
153
201
|
```
|
|
154
202
|
|
|
155
203
|
---
|
|
156
204
|
|
|
157
|
-
##
|
|
158
|
-
|
|
159
|
-
### Shared state
|
|
160
|
-
|
|
161
|
-
`SbContainer` owns the state (open state, positioning, ARIA ids) and shares it with its descendants through Vue `provide`/`inject`:
|
|
162
|
-
|
|
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.
|
|
180
|
-
|
|
181
|
-
### From click to open panel
|
|
182
|
-
|
|
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.
|
|
188
|
-
|
|
189
|
-
Closing (click on the reference, `close()`, Escape, press outside, focus moving outside, `v-model:open` set to `false`, or `disabled`):
|
|
205
|
+
## ♿ Accessibility
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
- `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.
|
|
208
|
+
- `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.
|
|
209
|
+
- **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 `:teleport="false"` nor around a tooltip.
|
|
210
|
+
- **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.
|
|
211
|
+
- <a id="keyboard"></a>**Keyboard.** With `role="menu"` or `role="listbox"` the components follow the WAI-ARIA [menu button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/) and [listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) patterns. Items are the elements with role `menuitem`, `menuitemcheckbox`, `menuitemradio` (menu) or `option` (listbox); they must be focusable (a `button`, or `tabindex="-1"`). Turn it off with `:keyboard-navigation="false"`.
|
|
212
|
+
|
|
213
|
+
| Key | On the reference | In the panel |
|
|
214
|
+
|-----|------------------|--------------|
|
|
215
|
+
| ArrowDown / ArrowUp | Opens on the first / last item (or the selected option), except with the `manual` trigger | Next / previous item; a menu wraps around, a listbox stops at its ends |
|
|
216
|
+
| Home / End | — | First / last item |
|
|
217
|
+
| Letters | — | Next item whose text starts with the typed letters |
|
|
218
|
+
| Enter / Space | Toggles | Clicks items that are not buttons or links |
|
|
219
|
+
| Tab / Shift+Tab | — | Menu: closes it (and the menus it opened from) and moves focus on, as from the reference |
|
|
220
|
+
| ArrowRight / ArrowLeft | A submenu trigger: ArrowRight opens the submenu on its first item | A submenu: ArrowLeft closes it and focuses its trigger |
|
|
221
|
+
|
|
222
|
+
A submenu is a menu `SbContainer` inside a menu panel; its arrows are mirrored in right-to-left text. Keys whose `keydown` you already handled with `event.preventDefault()`, and keys typed in text fields, are left alone.
|
|
223
|
+
- <a id="tooltip-1"></a>**Tooltip.** With `role="tooltip"` the panel follows the WAI-ARIA [tooltip](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) pattern: the reference gets no `aria-expanded` nor `aria-controls`, and while open `aria-describedby` points to the panel (pass your own `aria-describedby` on `SbReference` to replace it). Focus stays on the reference and no focus guards are rendered. Use it with `trigger="hover"`, which meets WCAG [1.4.13](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus): it opens on keyboard focus too, the pointer can move onto the tooltip within `delay.close`, and Escape dismisses it.
|
|
224
|
+
- **Dismissal.**
|
|
225
|
+
- Escape closes the open panel that has focus, else the most recently opened one. It is ignored when a widget inside has already handled it (`event.preventDefault()`) and while an IME composition is in progress.
|
|
226
|
+
- A press outside closes the panel; on touch, only a tap does, so starting a scroll keeps it open.
|
|
227
|
+
- Moving focus outside (e.g. tabbing past the last element) closes it.
|
|
228
|
+
- A container nested inside a panel counts as part of it.
|
|
229
|
+
- **What is up to you.** The library does not implement the keyboard model of `tree` and `grid` panels, nor selection: a listbox option or a menu item does what its `click` handler does (e.g. set `aria-selected` and close the panel). Use valid structure: items directly inside the `menu` / `listbox`, or inside `ul` / `li` with `role="none"`.
|
|
194
230
|
|
|
195
231
|
---
|
|
196
232
|
|
|
@@ -305,24 +341,61 @@ Panels are teleported to `body`, so their styles cannot be `scoped` to the compo
|
|
|
305
341
|
|
|
306
342
|
---
|
|
307
343
|
|
|
308
|
-
##
|
|
344
|
+
## 🧭 How It Works
|
|
309
345
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
-
|
|
346
|
+
### Shared state
|
|
347
|
+
|
|
348
|
+
`SbContainer` owns the state (open state, positioning, ARIA ids) and shares it with its descendants through Vue `provide`/`inject`:
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
SbContainer ── provides the state ──┬─▶ SbReference
|
|
352
|
+
└─▶ SbFloating
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- `SbReference` and `SbFloating` find their container on their own: you pass no refs, styles or handlers, and never write `.value`.
|
|
356
|
+
- The lookup follows the component tree, not the DOM, so it keeps working although `SbFloating` is teleported to `body`.
|
|
357
|
+
- They can sit at any depth inside the container slot, also inside your own components.
|
|
358
|
+
- Used outside an `SbContainer`, they throw: `[sb-floating-panel-vue] <SbFloating> must be used inside <SbContainer>.`
|
|
359
|
+
- In development, the console warns about a second `SbReference` or `SbFloating` in the same container and about an `as` component that renders no single root element. The warnings depend on `process.env.NODE_ENV`, which your bundler replaces: they are left out of production builds.
|
|
360
|
+
|
|
361
|
+
### Nested containers
|
|
362
|
+
|
|
363
|
+
An `SbContainer` placed inside another one (e.g. a submenu in a menu panel) registers with its parent:
|
|
364
|
+
|
|
365
|
+
- a press inside the nested panel is not "outside" for the parent, so both stay open;
|
|
366
|
+
- Escape closes the innermost open panel first, then the next one;
|
|
367
|
+
- a press outside all of them closes them all.
|
|
368
|
+
|
|
369
|
+
### From click to open panel
|
|
370
|
+
|
|
371
|
+
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.
|
|
372
|
+
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`.
|
|
373
|
+
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.
|
|
374
|
+
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.
|
|
375
|
+
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.
|
|
376
|
+
|
|
377
|
+
Closing (click on the reference, `close()`, Escape, press outside, focus moving outside, the pointer leaving with `trigger="hover"`, `v-model:open` set to `false`, `disabled`, or the reference unmounting):
|
|
378
|
+
|
|
379
|
+
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`.
|
|
380
|
+
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`).
|
|
381
|
+
3. The wrapper is removed, Floating UI stops tracking, `SbFloating` emits `after-leave`.
|
|
320
382
|
|
|
321
383
|
---
|
|
322
384
|
|
|
385
|
+
## 🔀 Migrating from v4
|
|
386
|
+
|
|
387
|
+
v5 is a breaking release.
|
|
388
|
+
|
|
389
|
+
- **`teleportTo` and `teleportDisabled` are replaced by `teleport`**: `teleport-to="#overlays"` becomes `teleport="#overlays"`, `teleport-disabled` becomes `:teleport="false"`.
|
|
390
|
+
- **Menus and listboxes handle the keyboard.** Remove your own arrow-key, Home/End and typeahead handlers on `role="menu"` and `role="listbox"` panels, or keep them and call `event.preventDefault()` on the keys they handle (the library then leaves those keys alone), or set `:keyboard-navigation="false"`. Tab now closes a menu.
|
|
391
|
+
- **Tooltips.** With `role="tooltip"` the reference no longer gets `aria-expanded` nor `aria-controls`, and gets `aria-describedby` pointing to the panel while open; `autoFocus` defaults to `false`. Replace a `trigger="manual"` tooltip opened with your own `@mouseenter` / `@focus` handlers by `trigger="hover"`.
|
|
392
|
+
- **`SbTrigger` also takes `'hover'` and `'focus'`**: a `switch` over it in your code may need the new cases.
|
|
393
|
+
- **The panel closes when its reference unmounts** while open.
|
|
394
|
+
- **Escape closes the open panel that has focus**, else the most recently opened one (it was always the most recently opened one).
|
|
395
|
+
|
|
323
396
|
## 🔀 Migrating from v3
|
|
324
397
|
|
|
325
|
-
|
|
398
|
+
v4 changed these defaults:
|
|
326
399
|
|
|
327
400
|
- **`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
401
|
- **`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.
|
|
@@ -382,21 +455,15 @@ v3 is a breaking release.
|
|
|
382
455
|
|
|
383
456
|
---
|
|
384
457
|
|
|
385
|
-
##
|
|
458
|
+
## 🤝 Contributing
|
|
386
459
|
|
|
387
|
-
|
|
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)
|
|
395
|
-
```
|
|
460
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) to run the playground, the tests and the build.
|
|
396
461
|
|
|
397
|
-
|
|
462
|
+
---
|
|
398
463
|
|
|
399
|
-
|
|
464
|
+
## 👤 Author
|
|
465
|
+
|
|
466
|
+
Created with ❤️ by [Stefano Biddau](https://www.stefano-biddau.com/)
|
|
400
467
|
|
|
401
468
|
---
|
|
402
469
|
|
|
@@ -4,25 +4,29 @@ export default _default;
|
|
|
4
4
|
declare const __VLS_export: __VLS_WithSlots<import('vue').DefineComponent<Omit<SbContainerProps, "open"> & {
|
|
5
5
|
open?: boolean | undefined;
|
|
6
6
|
}, SbContainerExposed, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
7
|
+
close: () => any;
|
|
7
8
|
open: () => any;
|
|
8
9
|
"update:open": (value: boolean | undefined) => any;
|
|
9
|
-
close: () => any;
|
|
10
10
|
}, string, import('vue').PublicProps, Readonly<Omit<SbContainerProps, "open"> & {
|
|
11
11
|
open?: boolean | undefined;
|
|
12
12
|
}> & Readonly<{
|
|
13
|
+
onClose?: (() => any) | undefined;
|
|
13
14
|
onOpen?: (() => any) | undefined;
|
|
14
15
|
"onUpdate:open"?: ((value: boolean | undefined) => any) | undefined;
|
|
15
|
-
onClose?: (() => any) | undefined;
|
|
16
16
|
}>, {
|
|
17
17
|
placement: import('..').SbPlacement;
|
|
18
18
|
strategy: import('..').SbStrategy;
|
|
19
19
|
padding: number;
|
|
20
20
|
offset: number;
|
|
21
21
|
matchWidth: boolean | "min";
|
|
22
|
-
hideWhenDetached: boolean;
|
|
22
|
+
hideWhenDetached: boolean | "close";
|
|
23
23
|
defaultOpen: boolean;
|
|
24
24
|
disabled: boolean;
|
|
25
25
|
trigger: import('..').SbTrigger;
|
|
26
|
+
delay: number | {
|
|
27
|
+
open?: number;
|
|
28
|
+
close?: number;
|
|
29
|
+
};
|
|
26
30
|
closeOnEscape: boolean;
|
|
27
31
|
closeOnClickOutside: boolean;
|
|
28
32
|
closeOnFocusOut: boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TransitionProps } from 'vue';
|
|
2
2
|
import { SbFloatingProps } from '../types';
|
|
3
|
-
declare var
|
|
3
|
+
declare var __VLS_25: {};
|
|
4
4
|
type __VLS_Slots = {} & {
|
|
5
|
-
default?: (props: typeof
|
|
5
|
+
default?: (props: typeof __VLS_25) => any;
|
|
6
6
|
};
|
|
7
7
|
declare const __VLS_base: import('vue').DefineComponent<SbFloatingProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
8
8
|
"after-enter": () => any;
|
|
@@ -11,13 +11,13 @@ declare const __VLS_base: import('vue').DefineComponent<SbFloatingProps, {}, {},
|
|
|
11
11
|
"onAfter-enter"?: (() => any) | undefined;
|
|
12
12
|
"onAfter-leave"?: (() => any) | undefined;
|
|
13
13
|
}>, {
|
|
14
|
-
as: string;
|
|
15
|
-
role:
|
|
14
|
+
as: string | import('vue').Component;
|
|
15
|
+
role: import('..').SbFloatingRole | null;
|
|
16
16
|
transition: string | TransitionProps;
|
|
17
|
-
|
|
18
|
-
teleportDisabled: boolean;
|
|
17
|
+
teleport: string | false | HTMLElement;
|
|
19
18
|
zIndex: string | number;
|
|
20
19
|
autoFocus: boolean;
|
|
20
|
+
keyboardNavigation: boolean;
|
|
21
21
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
22
22
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
23
23
|
declare const _default: typeof __VLS_export;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SbReferenceProps } from '../types';
|
|
2
|
-
declare var
|
|
2
|
+
declare var __VLS_14: {};
|
|
3
3
|
type __VLS_Slots = {} & {
|
|
4
|
-
default?: (props: typeof
|
|
4
|
+
default?: (props: typeof __VLS_14) => any;
|
|
5
5
|
};
|
|
6
6
|
declare const __VLS_base: import('vue').DefineComponent<SbReferenceProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<SbReferenceProps> & Readonly<{}>, {
|
|
7
7
|
as: string | import('vue').Component;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InjectionKey, Ref } from 'vue';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { SbTrigger } from '../types';
|
|
3
|
+
import { NavigableRole } from '../utils/navigation';
|
|
4
|
+
import { FloatingPanel } from './useSbFloatingPanel';
|
|
4
5
|
/**
|
|
5
6
|
* State shared by `SbContainer` with `SbReference` and `SbFloating`.
|
|
6
7
|
*/
|
|
@@ -13,20 +14,32 @@ export interface SbFloatingContext extends FloatingPanel {
|
|
|
13
14
|
referenceId: Ref<string | undefined>;
|
|
14
15
|
/** `role` of the mounted `SbFloating`, used to derive `aria-haspopup` on the reference. */
|
|
15
16
|
floatingRole: Ref<string | null>;
|
|
17
|
+
/** Role of the mounted `SbFloating` when the components handle its keyboard model. */
|
|
18
|
+
navigation: Ref<NavigableRole | null>;
|
|
19
|
+
/** Item to focus once the panel is open and positioned, asked by the arrow keys on the reference. */
|
|
20
|
+
focusRequest: Ref<'first' | 'last' | null>;
|
|
21
|
+
/** Container of the panel this one is nested in (e.g. the menu of a submenu). */
|
|
22
|
+
parent: SbFloatingContext | null;
|
|
16
23
|
/** Set when an outside press closes the panel on a focusable element, which keeps focus. */
|
|
17
24
|
skipFocusReturn: boolean;
|
|
25
|
+
/** Mounted `SbReference` and `SbFloating` components, to warn about extra ones. */
|
|
26
|
+
parts: {
|
|
27
|
+
reference: number;
|
|
28
|
+
floating: number;
|
|
29
|
+
};
|
|
30
|
+
/** Set while the container unmounts, when its parts must not change the open state. */
|
|
31
|
+
isUnmounting: boolean;
|
|
18
32
|
open: () => void;
|
|
19
33
|
close: () => void;
|
|
20
34
|
toggle: () => void;
|
|
35
|
+
/** Pointer entering or leaving the reference or the panel, for the `hover` trigger. */
|
|
36
|
+
hover: (event: PointerEvent) => void;
|
|
21
37
|
/** Whether an event target is inside the reference, the panel or a nested container. */
|
|
22
38
|
contains: (target: EventTarget | null) => boolean;
|
|
23
39
|
/** Registers a nested container; returns the function that unregisters it. */
|
|
24
40
|
registerChild: (child: SbFloatingContext) => () => void;
|
|
25
41
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Panel roles that make the reference a popup trigger, with their `aria-haspopup` value.
|
|
28
|
-
* Panels with these roles are also named after the reference.
|
|
29
|
-
*/
|
|
30
|
-
export declare const POPUP_ROLES: Record<string, 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid'>;
|
|
31
42
|
export declare const sbFloatingContextKey: InjectionKey<SbFloatingContext>;
|
|
43
|
+
/** Counts a mounted part of the container, warning about a second one; returns the release. */
|
|
44
|
+
export declare function claimPart(context: SbFloatingContext, part: 'reference' | 'floating', component: string): () => void;
|
|
32
45
|
export declare function useSbFloatingContext(component: string): SbFloatingContext;
|
|
@@ -11,7 +11,7 @@ export interface FloatingPanelOptions {
|
|
|
11
11
|
offset: MaybeRefOrGetter<number>;
|
|
12
12
|
padding: MaybeRefOrGetter<number>;
|
|
13
13
|
matchWidth: MaybeRefOrGetter<boolean | 'min'>;
|
|
14
|
-
hideWhenDetached: MaybeRefOrGetter<boolean>;
|
|
14
|
+
hideWhenDetached: MaybeRefOrGetter<boolean | 'close'>;
|
|
15
15
|
}
|
|
16
16
|
export interface FloatingPanel {
|
|
17
17
|
reference: Ref<HTMLElement | null>;
|
|
@@ -23,6 +23,8 @@ export interface FloatingPanel {
|
|
|
23
23
|
side: ComputedRef<SbSide>;
|
|
24
24
|
align: ComputedRef<SbAlignment | undefined>;
|
|
25
25
|
isPositioned: Readonly<Ref<boolean>>;
|
|
26
|
+
/** Whether the reference is scrolled out of view, with `hideWhenDetached`. */
|
|
27
|
+
referenceHidden: ComputedRef<boolean>;
|
|
26
28
|
update: () => void;
|
|
27
29
|
}
|
|
28
30
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default as SbContainer } from './components/SbContainer.vue';
|
|
2
2
|
export { default as SbReference } from './components/SbReference.vue';
|
|
3
3
|
export { default as SbFloating } from './components/SbFloating.vue';
|
|
4
|
-
export type { SbContainerProps, SbContainerSlotProps, SbContainerExposed, SbReferenceProps, SbFloatingProps, SbTrigger, SbPlacement, SbSide, SbAlignment, SbStrategy, } from './types';
|
|
4
|
+
export type { SbContainerProps, SbContainerSlotProps, SbContainerExposed, SbReferenceProps, SbFloatingProps, SbTrigger, SbPlacement, SbSide, SbAlignment, SbStrategy, SbFloatingRole, } from './types';
|