sb-floating-panel-vue 3.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 +84 -34
- package/dist/components/SbContainer.vue.d.ts +1 -0
- package/dist/components/SbFloating.vue.d.ts +5 -4
- package/dist/components/SbReference.vue.d.ts +2 -1
- package/dist/context.d.ts +11 -2
- package/dist/focus.d.ts +8 -0
- package/dist/sb-floating-panel.es.js +239 -133
- package/dist/sb-floating-panel.umd.js +1 -1
- package/dist/types.d.ts +42 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
## 🔥 Features
|
|
11
11
|
|
|
12
|
-
- 🧩 Three components (`SbContainer`, `SbReference`, `SbFloating`) for dropdowns, popovers
|
|
12
|
+
- 🧩 Three components (`SbContainer`, `SbReference`, `SbFloating`) for dropdowns, popovers and disclosure panels
|
|
13
13
|
- ⚡ Built on top of [`@floating-ui/vue`](https://floating-ui.com/): `offset`, `flip`, `shift`, `size` and `hide` already configured
|
|
14
14
|
- 🎨 Zero CSS shipped: the library adapts to your design system (plain CSS, Tailwind, CSS modules)
|
|
15
|
-
- ♿ Accessible by default: ARIA attributes,
|
|
15
|
+
- ♿ Accessible by default: ARIA attributes and naming, Tab order kept across the teleport, Escape, click outside, focus management
|
|
16
16
|
- 🔁 Fully reactive props, `v-model:open`, nested panels
|
|
17
17
|
|
|
18
18
|
---
|
|
@@ -58,9 +58,10 @@ import { SbContainer, SbReference, SbFloating } from 'sb-floating-panel-vue';
|
|
|
58
58
|
<template>
|
|
59
59
|
<SbContainer placement="bottom-start" match-width="min">
|
|
60
60
|
<template #default="{ close }">
|
|
61
|
-
<SbReference class="my-trigger"
|
|
62
|
-
<SbFloating
|
|
63
|
-
<
|
|
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>
|
|
64
65
|
</SbFloating>
|
|
65
66
|
</template>
|
|
66
67
|
</SbContainer>
|
|
@@ -86,7 +87,8 @@ Holds the state and the positioning. Renders no element of its own, only its slo
|
|
|
86
87
|
| `disabled` | `boolean` | `false` | Prevents opening; closes the panel if it is open |
|
|
87
88
|
| `trigger` | `'click' \| 'manual'` | `'click'` | `manual`: the reference does not toggle; open with `v-model:open`, slot props or methods |
|
|
88
89
|
| `closeOnEscape` | `boolean` | `true` | Closes on Escape |
|
|
89
|
-
| `closeOnClickOutside` | `boolean` | `true` | Closes on a pointer press outside the reference and the panel
|
|
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 |
|
|
90
92
|
|
|
91
93
|
| Event | Payload | Description |
|
|
92
94
|
|---------------|-----------|--------------------------|
|
|
@@ -94,7 +96,7 @@ Holds the state and the positioning. Renders no element of its own, only its slo
|
|
|
94
96
|
| `open` | — | The panel has opened |
|
|
95
97
|
| `close` | — | The panel has closed |
|
|
96
98
|
|
|
97
|
-
**Slot props**: `isOpen`, `open()`, `close()`, `toggle()`, `placement` (resolved after flip), `side`.
|
|
99
|
+
**Slot props**: `isOpen`, `open()`, `close()`, `toggle()`, `placement` (resolved after flip), `side`, `align`.
|
|
98
100
|
|
|
99
101
|
**Exposed methods** (template ref): `open()`, `close()`, `toggle()`, `update()` (recomputes the position).
|
|
100
102
|
|
|
@@ -102,12 +104,15 @@ Holds the state and the positioning. Renders no element of its own, only its slo
|
|
|
102
104
|
|
|
103
105
|
The trigger the panel is anchored to.
|
|
104
106
|
|
|
105
|
-
| Prop | Type
|
|
106
|
-
|
|
107
|
-
| `as` | `string` | `'button'`
|
|
108
|
-
| `
|
|
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 |
|
|
109
112
|
|
|
110
|
-
Sets `aria-expanded`, `aria-controls` (while open) and `data-sbf-state`. When the container is `disabled`, a `button` gets `disabled`,
|
|
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.
|
|
114
|
+
|
|
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.
|
|
111
116
|
|
|
112
117
|
### `<SbFloating>`
|
|
113
118
|
|
|
@@ -115,20 +120,20 @@ The panel, teleported to `body`. It is rendered while open and stays mounted unt
|
|
|
115
120
|
|
|
116
121
|
| Prop | Type | Default | Description |
|
|
117
122
|
|--------------------|-------------------------|------------|-----------------------------------------------------------------------------------------------------|
|
|
118
|
-
| `as` | `string`
|
|
119
|
-
| `role` | `string`
|
|
120
|
-
| `transition` | `string`
|
|
121
|
-
| `teleportTo` | `string \| HTMLElement`
|
|
122
|
-
| `teleportDisabled` | `boolean`
|
|
123
|
-
| `zIndex` | `number \| string`
|
|
124
|
-
| `autoFocus` | `boolean`
|
|
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 |
|
|
125
130
|
|
|
126
131
|
| Event | Description |
|
|
127
132
|
|---------------|----------------------------------------------------------------|
|
|
128
133
|
| `after-enter` | The enter transition has finished |
|
|
129
134
|
| `after-leave` | The leave transition has finished and the panel is removed |
|
|
130
135
|
|
|
131
|
-
When focus is inside the panel as it closes, it goes back to the reference.
|
|
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.
|
|
132
137
|
|
|
133
138
|
### Types
|
|
134
139
|
|
|
@@ -170,21 +175,21 @@ SbContainer ── provides the state ──┬─▶ SbReference
|
|
|
170
175
|
An `SbContainer` placed inside another one (e.g. a submenu in a menu panel) registers with its parent:
|
|
171
176
|
|
|
172
177
|
- a press inside the nested panel is not "outside" for the parent, so both stay open;
|
|
173
|
-
- Escape closes the innermost open panel first, then the next one;
|
|
178
|
+
- Escape closes the innermost open panel first, then the next one (more generally, the most recently opened panel, also between sibling containers);
|
|
174
179
|
- a press outside all of them closes them all.
|
|
175
180
|
|
|
176
181
|
### From click to open panel
|
|
177
182
|
|
|
178
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.
|
|
179
|
-
2. **Open state changes.** `SbReference` gets `aria-expanded="true"`, `aria-controls` and `data-sbf-state="open"`; the container starts listening for Escape and outside
|
|
180
|
-
3. **The panel mounts.** `SbFloating` renders the wrapper in the teleport target
|
|
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.
|
|
181
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.
|
|
182
|
-
5. **Positioned.** With `autoFocus
|
|
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.
|
|
183
188
|
|
|
184
|
-
Closing (click on the reference, `close()`, Escape, press outside, `v-model:open` set to `false`, or `disabled`):
|
|
189
|
+
Closing (click on the reference, `close()`, Escape, press outside, focus moving outside, `v-model:open` set to `false`, or `disabled`):
|
|
185
190
|
|
|
186
|
-
1. If focus is inside the panel, it goes back to the reference
|
|
187
|
-
2. The leave classes of your `transition
|
|
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`).
|
|
188
193
|
3. The wrapper is removed, Floating UI stops tracking, `SbFloating` emits `after-leave`.
|
|
189
194
|
|
|
190
195
|
---
|
|
@@ -210,7 +215,7 @@ Your styles, transitions and transforms go on the panel, so they never fight wit
|
|
|
210
215
|
| Attribute | Values | On | Use |
|
|
211
216
|
|------------------|--------------------------------------|--------------------------|---------------------------------------------|
|
|
212
217
|
| `data-sbf-state` | `open` / `closed` | reference | Open/closed styles (e.g. rotate an icon) |
|
|
213
|
-
| `data-sbf-state` | `open`
|
|
218
|
+
| `data-sbf-state` | `open` / `closed` | panel | `closed` from the start of the leave transition: style or animate the exit |
|
|
214
219
|
| `data-sbf-side` | `top` / `bottom` / `left` / `right` | panel | Real side after flip: directional animation |
|
|
215
220
|
| `data-sbf-align` | `start` / `end` / absent | panel | Alignment part of the placement |
|
|
216
221
|
|
|
@@ -223,9 +228,14 @@ Set on the wrapper and inherited by the panel:
|
|
|
223
228
|
| `--sbf-reference-width` | Width of the reference |
|
|
224
229
|
| `--sbf-available-width` | Horizontal space left before overflowing the viewport |
|
|
225
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` |
|
|
226
232
|
|
|
227
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`).
|
|
228
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
|
+
|
|
229
239
|
### Plain CSS
|
|
230
240
|
|
|
231
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.
|
|
@@ -246,6 +256,7 @@ Panels are teleported to `body`, so their styles cannot be `scoped` to the compo
|
|
|
246
256
|
/* <SbFloating transition="pop"> */
|
|
247
257
|
.pop-enter-active,
|
|
248
258
|
.pop-leave-active {
|
|
259
|
+
transform-origin: var(--sbf-transform-origin);
|
|
249
260
|
transition: opacity 150ms ease, transform 150ms ease;
|
|
250
261
|
}
|
|
251
262
|
.pop-enter-from,
|
|
@@ -257,6 +268,18 @@ Panels are teleported to `body`, so their styles cannot be `scoped` to the compo
|
|
|
257
268
|
/* direction from the side resolved after flip */
|
|
258
269
|
.pop-enter-from[data-sbf-side='bottom'] { transform: translateY(-6px); }
|
|
259
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
|
+
}
|
|
260
283
|
```
|
|
261
284
|
|
|
262
285
|
### Tailwind (v4)
|
|
@@ -266,7 +289,16 @@ Panels are teleported to `body`, so their styles cannot be `scoped` to the compo
|
|
|
266
289
|
Options
|
|
267
290
|
<span class="inline-block transition-transform group-data-[sbf-state=open]:rotate-180">▾</span>
|
|
268
291
|
</SbReference>
|
|
269
|
-
<SbFloating
|
|
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
|
+
>
|
|
270
302
|
…
|
|
271
303
|
</SbFloating>
|
|
272
304
|
```
|
|
@@ -275,11 +307,29 @@ Panels are teleported to `body`, so their styles cannot be `scoped` to the compo
|
|
|
275
307
|
|
|
276
308
|
## ♿ Accessibility
|
|
277
309
|
|
|
278
|
-
- `SbReference` gets `aria-expanded`, `aria-controls` (pointing to the panel `id`) and `aria-haspopup
|
|
279
|
-
- `SbFloating` gets an `id` generated with Vue `useId()` (SSR safe)
|
|
280
|
-
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
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).
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 🔀 Migrating from v3
|
|
324
|
+
|
|
325
|
+
The next major release changes these defaults:
|
|
326
|
+
|
|
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.
|
|
283
333
|
|
|
284
334
|
---
|
|
285
335
|
|
|
@@ -25,6 +25,7 @@ declare const __VLS_export: __VLS_WithSlots<import('vue').DefineComponent<Omit<S
|
|
|
25
25
|
trigger: import('..').SbTrigger;
|
|
26
26
|
closeOnEscape: boolean;
|
|
27
27
|
closeOnClickOutside: boolean;
|
|
28
|
+
closeOnFocusOut: boolean;
|
|
28
29
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>, {
|
|
29
30
|
default?: (props: SbContainerSlotProps) => unknown;
|
|
30
31
|
}>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { TransitionProps } from 'vue';
|
|
1
2
|
import { SbFloatingProps } from '../types';
|
|
2
|
-
declare var
|
|
3
|
+
declare var __VLS_23: {};
|
|
3
4
|
type __VLS_Slots = {} & {
|
|
4
|
-
default?: (props: typeof
|
|
5
|
+
default?: (props: typeof __VLS_23) => any;
|
|
5
6
|
};
|
|
6
7
|
declare const __VLS_base: import('vue').DefineComponent<SbFloatingProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
7
8
|
"after-enter": () => any;
|
|
@@ -11,8 +12,8 @@ declare const __VLS_base: import('vue').DefineComponent<SbFloatingProps, {}, {},
|
|
|
11
12
|
"onAfter-leave"?: (() => any) | undefined;
|
|
12
13
|
}>, {
|
|
13
14
|
as: string;
|
|
14
|
-
role: string;
|
|
15
|
-
transition: string;
|
|
15
|
+
role: string | null;
|
|
16
|
+
transition: string | TransitionProps;
|
|
16
17
|
teleportTo: string | HTMLElement;
|
|
17
18
|
teleportDisabled: boolean;
|
|
18
19
|
zIndex: string | number;
|
|
@@ -4,7 +4,8 @@ type __VLS_Slots = {} & {
|
|
|
4
4
|
default?: (props: typeof __VLS_10) => 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
|
-
as: string;
|
|
7
|
+
as: string | import('vue').Component;
|
|
8
|
+
id: string;
|
|
8
9
|
ariaHaspopup: "dialog" | "menu" | "listbox" | "tree" | "grid" | "true" | "false";
|
|
9
10
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
10
11
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
package/dist/context.d.ts
CHANGED
|
@@ -9,15 +9,24 @@ export interface SbFloatingContext extends FloatingPanel {
|
|
|
9
9
|
disabled: Readonly<Ref<boolean>>;
|
|
10
10
|
trigger: Readonly<Ref<SbTrigger>>;
|
|
11
11
|
floatingId: string;
|
|
12
|
+
/** Id of the mounted `SbReference`, used to name the panel with `aria-labelledby`. */
|
|
13
|
+
referenceId: Ref<string | undefined>;
|
|
14
|
+
/** `role` of the mounted `SbFloating`, used to derive `aria-haspopup` on the reference. */
|
|
15
|
+
floatingRole: Ref<string | null>;
|
|
16
|
+
/** Set when an outside press closes the panel on a focusable element, which keeps focus. */
|
|
17
|
+
skipFocusReturn: boolean;
|
|
12
18
|
open: () => void;
|
|
13
19
|
close: () => void;
|
|
14
20
|
toggle: () => void;
|
|
15
21
|
/** Whether an event target is inside the reference, the panel or a nested container. */
|
|
16
22
|
contains: (target: EventTarget | null) => boolean;
|
|
17
|
-
/** Whether a nested container is open. */
|
|
18
|
-
hasOpenChild: () => boolean;
|
|
19
23
|
/** Registers a nested container; returns the function that unregisters it. */
|
|
20
24
|
registerChild: (child: SbFloatingContext) => () => void;
|
|
21
25
|
}
|
|
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'>;
|
|
22
31
|
export declare const sbFloatingContextKey: InjectionKey<SbFloatingContext>;
|
|
23
32
|
export declare function useSbFloatingContext(component: string): SbFloatingContext;
|
package/dist/focus.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Marks the hidden elements placed around a teleported panel to keep the Tab order. */
|
|
2
|
+
export declare const FOCUS_GUARD_ATTRIBUTE = "data-sbf-focus-guard";
|
|
3
|
+
/** Whether pressing `target` moves focus to it (or to a focusable ancestor). */
|
|
4
|
+
export declare function isFocusable(target: EventTarget | null): boolean;
|
|
5
|
+
/** Elements reachable with Tab inside `root`, in document order (focus guards excluded). */
|
|
6
|
+
export declare function getTabbables(root: ParentNode): HTMLElement[];
|
|
7
|
+
/** First element reachable with Tab after `anchor` in document order, skipping the excluded ones. */
|
|
8
|
+
export declare function getNextTabbable(anchor: Element, exclude: (el: HTMLElement) => boolean): HTMLElement | undefined;
|
|
@@ -1,34 +1,37 @@
|
|
|
1
|
-
import { Teleport as e, Transition as t, computed as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createVNode as o, defineComponent as s, inject as c, mergeModels as l, mergeProps as u, nextTick as d, normalizeStyle as f,
|
|
2
|
-
import { autoUpdate as
|
|
1
|
+
import { Teleport as e, Transition as t, computed as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createVNode as o, defineComponent as s, inject as c, mergeModels as l, mergeProps as u, nextTick as d, normalizeStyle as f, onBeforeUnmount as p, onScopeDispose as m, openBlock as h, provide as g, ref as _, renderSlot as v, resolveDynamicComponent as y, toRef as b, toValue as x, unref as S, useAttrs as C, useId as w, useModel as T, watch as E, watchEffect as D, withCtx as O } from "vue";
|
|
2
|
+
import { autoUpdate as k, flip as A, hide as j, limitShift as M, offset as N, shift as P, size as F, useFloating as I } from "@floating-ui/vue";
|
|
3
3
|
//#region src/composables/useSbFloatingPanel.ts
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
var L = (e) => `${Math.max(0, e)}px`, R = (e) => {
|
|
5
|
+
let [t, n] = e.split("-"), r = n === "start" ? "0%" : n === "end" ? "100%" : "50%";
|
|
6
|
+
return t === "top" ? `${r} 100%` : t === "bottom" ? `${r} 0%` : t === "left" ? `100% ${r}` : `0% ${r}`;
|
|
7
|
+
};
|
|
8
|
+
function z(e) {
|
|
9
|
+
let t = _(null), r = _(null), i = n(() => {
|
|
10
|
+
let t = x(e.padding), n = x(e.matchWidth), r = x(e.placement).includes("-"), i = A({
|
|
8
11
|
padding: t,
|
|
9
12
|
crossAxis: "alignment",
|
|
10
13
|
fallbackAxisSideDirection: "end"
|
|
11
|
-
}), a =
|
|
14
|
+
}), a = P({
|
|
12
15
|
padding: t,
|
|
13
|
-
limiter:
|
|
16
|
+
limiter: M()
|
|
14
17
|
}), o = [
|
|
15
|
-
|
|
18
|
+
N(x(e.offset)),
|
|
16
19
|
...r ? [i, a] : [a, i],
|
|
17
|
-
|
|
20
|
+
F({
|
|
18
21
|
padding: t,
|
|
19
|
-
apply({ rects: e, elements: t, availableWidth: r, availableHeight: i }) {
|
|
20
|
-
let { style:
|
|
21
|
-
|
|
22
|
+
apply({ rects: e, elements: t, availableWidth: r, availableHeight: i, placement: a }) {
|
|
23
|
+
let { style: o } = t.floating, s = L(e.reference.width);
|
|
24
|
+
o.setProperty("--sbf-reference-width", s), o.setProperty("--sbf-transform-origin", R(a)), o.setProperty("--sbf-available-width", L(r)), o.setProperty("--sbf-available-height", L(i)), o.width = n === !0 ? s : "", o.minWidth = n === "min" ? s : "";
|
|
22
25
|
}
|
|
23
26
|
})
|
|
24
27
|
];
|
|
25
|
-
return
|
|
26
|
-
}), { floatingStyles: a, middlewareData: o, placement: s, isPositioned: c, update: l } =
|
|
27
|
-
open: () =>
|
|
28
|
-
placement: () =>
|
|
29
|
-
strategy: () =>
|
|
28
|
+
return x(e.hideWhenDetached) && o.push(j({ padding: t })), o;
|
|
29
|
+
}), { floatingStyles: a, middlewareData: o, placement: s, isPositioned: c, update: l } = I(t, r, {
|
|
30
|
+
open: () => x(e.isOpen),
|
|
31
|
+
placement: () => x(e.placement),
|
|
32
|
+
strategy: () => x(e.strategy),
|
|
30
33
|
middleware: i,
|
|
31
|
-
whileElementsMounted:
|
|
34
|
+
whileElementsMounted: k
|
|
32
35
|
});
|
|
33
36
|
return {
|
|
34
37
|
reference: t,
|
|
@@ -46,15 +49,46 @@ function F(e) {
|
|
|
46
49
|
}
|
|
47
50
|
//#endregion
|
|
48
51
|
//#region src/context.ts
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
var B = {
|
|
53
|
+
dialog: "dialog",
|
|
54
|
+
alertdialog: "dialog",
|
|
55
|
+
menu: "menu",
|
|
56
|
+
listbox: "listbox",
|
|
57
|
+
tree: "tree",
|
|
58
|
+
grid: "grid"
|
|
59
|
+
}, V = Symbol("SbFloatingContext");
|
|
60
|
+
function H(e) {
|
|
61
|
+
let t = c(V, null);
|
|
52
62
|
if (!t) throw Error(`[sb-floating-panel-vue] <${e}> must be used inside <SbContainer>.`);
|
|
53
63
|
return t;
|
|
54
64
|
}
|
|
55
65
|
//#endregion
|
|
66
|
+
//#region src/focus.ts
|
|
67
|
+
var U = "data-sbf-focus-guard", W = [
|
|
68
|
+
"a[href]",
|
|
69
|
+
"button:not([disabled])",
|
|
70
|
+
"input:not([disabled]):not([type=\"hidden\"])",
|
|
71
|
+
"select:not([disabled])",
|
|
72
|
+
"textarea:not([disabled])",
|
|
73
|
+
"summary",
|
|
74
|
+
"audio[controls]",
|
|
75
|
+
"video[controls]",
|
|
76
|
+
"[tabindex]",
|
|
77
|
+
"[contenteditable=\"true\"]"
|
|
78
|
+
].join(","), G = (e) => e.tabIndex >= 0 && !e.hasAttribute("data-sbf-focus-guard") && !e.closest("[inert]") && (e.checkVisibility?.() ?? !0);
|
|
79
|
+
function K(e) {
|
|
80
|
+
let t = e instanceof Element ? e.closest(W) : null;
|
|
81
|
+
return !!t && !t.closest("[inert]");
|
|
82
|
+
}
|
|
83
|
+
function q(e) {
|
|
84
|
+
return [...e.querySelectorAll(W)].filter(G);
|
|
85
|
+
}
|
|
86
|
+
function J(e, t) {
|
|
87
|
+
return q(e.ownerDocument).find((n) => !!(e.compareDocumentPosition(n) & Node.DOCUMENT_POSITION_FOLLOWING) && !e.contains(n) && !t(n));
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
56
90
|
//#region src/components/SbContainer.vue?vue&type=script&setup=true&lang.ts
|
|
57
|
-
var
|
|
91
|
+
var Y = [], X = /* @__PURE__ */ s({
|
|
58
92
|
inheritAttrs: !1,
|
|
59
93
|
__name: "SbContainer",
|
|
60
94
|
props: /*@__PURE__*/ l({
|
|
@@ -86,6 +120,10 @@ var R = /* @__PURE__ */ new WeakSet(), z = /* @__PURE__ */ s({
|
|
|
86
120
|
closeOnClickOutside: {
|
|
87
121
|
type: Boolean,
|
|
88
122
|
default: !0
|
|
123
|
+
},
|
|
124
|
+
closeOnFocusOut: {
|
|
125
|
+
type: Boolean,
|
|
126
|
+
default: !0
|
|
89
127
|
}
|
|
90
128
|
}, {
|
|
91
129
|
open: {
|
|
@@ -96,86 +134,118 @@ var R = /* @__PURE__ */ new WeakSet(), z = /* @__PURE__ */ s({
|
|
|
96
134
|
}),
|
|
97
135
|
emits: /*@__PURE__*/ l(["open", "close"], ["update:open"]),
|
|
98
136
|
setup(e, { expose: t, emit: r }) {
|
|
99
|
-
let i = e, a =
|
|
100
|
-
e && i.disabled || e !==
|
|
101
|
-
},
|
|
102
|
-
isOpen:
|
|
137
|
+
let i = e, a = T(e, "open"), o = r, s = i.defaultOpen, l = n(() => !i.disabled && (a.value ?? s)), u = (e) => {
|
|
138
|
+
e && i.disabled || e !== l.value && (a.value = e);
|
|
139
|
+
}, d = () => u(!0), f = () => u(!1), p = () => u(!l.value), h = z({
|
|
140
|
+
isOpen: l,
|
|
103
141
|
placement: () => i.placement,
|
|
104
142
|
strategy: () => i.strategy,
|
|
105
143
|
offset: () => i.offset,
|
|
106
144
|
padding: () => i.padding,
|
|
107
145
|
matchWidth: () => i.matchWidth,
|
|
108
146
|
hideWhenDetached: () => i.hideWhenDetached
|
|
109
|
-
}),
|
|
110
|
-
...
|
|
111
|
-
isOpen:
|
|
112
|
-
disabled:
|
|
113
|
-
trigger:
|
|
114
|
-
floatingId: `${
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
}), y = /* @__PURE__ */ new Set(), x = {
|
|
148
|
+
...h,
|
|
149
|
+
isOpen: l,
|
|
150
|
+
disabled: b(i, "disabled"),
|
|
151
|
+
trigger: b(i, "trigger"),
|
|
152
|
+
floatingId: `${w()}-floating`,
|
|
153
|
+
referenceId: _(),
|
|
154
|
+
floatingRole: _("dialog"),
|
|
155
|
+
skipFocusReturn: !1,
|
|
156
|
+
open: d,
|
|
157
|
+
close: f,
|
|
158
|
+
toggle: p,
|
|
159
|
+
contains: (e) => e instanceof Node ? h.reference.value?.contains(e) || h.floating.value?.contains(e) ? !0 : [...y].some((t) => t.contains(e)) : !1,
|
|
160
|
+
registerChild: (e) => (y.add(e), () => y.delete(e))
|
|
161
|
+
}, C = c(V, null);
|
|
162
|
+
C && m(C.registerChild(x)), g(V, x);
|
|
163
|
+
let D = (e) => {
|
|
164
|
+
e.key === "Escape" && Y[Y.length - 1] === x && (!i.closeOnEscape || e.defaultPrevented || e.isComposing || (e.preventDefault(), f()));
|
|
165
|
+
}, O = (e) => {
|
|
166
|
+
x.contains(e) || (x.skipFocusReturn = K(e), f());
|
|
167
|
+
}, k, A = (e) => {
|
|
168
|
+
if (k?.(), !i.closeOnClickOutside) return;
|
|
169
|
+
let t = e.composedPath()[0] ?? e.target;
|
|
170
|
+
if (e.pointerType !== "touch") {
|
|
171
|
+
O(t);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (x.contains(t)) return;
|
|
175
|
+
let n = (e) => {
|
|
176
|
+
k?.(), O(e.composedPath()[0] ?? e.target);
|
|
177
|
+
};
|
|
178
|
+
document.addEventListener("click", n, !0), k = () => {
|
|
179
|
+
document.removeEventListener("click", n, !0), k = void 0;
|
|
180
|
+
};
|
|
181
|
+
}, j = (e) => {
|
|
182
|
+
i.closeOnFocusOut && (x.contains(e.composedPath()[0] ?? e.target) || f());
|
|
127
183
|
};
|
|
128
|
-
return
|
|
129
|
-
e && typeof document < "u" && (document.addEventListener("keydown",
|
|
130
|
-
document.removeEventListener("keydown",
|
|
184
|
+
return E(l, (e, t, n) => {
|
|
185
|
+
e && typeof document < "u" && (Y.push(x), document.addEventListener("keydown", D), document.addEventListener("pointerdown", A, !0), document.addEventListener("focusin", j), n(() => {
|
|
186
|
+
document.removeEventListener("keydown", D), document.removeEventListener("pointerdown", A, !0), document.removeEventListener("focusin", j), k?.(), Y.splice(Y.indexOf(x), 1);
|
|
131
187
|
}));
|
|
132
|
-
}, { immediate: !0 }),
|
|
133
|
-
e && a.value && (a.value = !1);
|
|
188
|
+
}, { immediate: !0 }), E(l, (e) => o(e ? "open" : "close")), E(() => i.disabled, (e) => {
|
|
189
|
+
e && (a.value ?? s) && (a.value = !1);
|
|
134
190
|
}), t({
|
|
135
|
-
open:
|
|
136
|
-
close:
|
|
137
|
-
toggle:
|
|
138
|
-
update:
|
|
139
|
-
}), (e, t) =>
|
|
140
|
-
isOpen:
|
|
141
|
-
open:
|
|
142
|
-
close:
|
|
143
|
-
toggle:
|
|
144
|
-
placement:
|
|
145
|
-
side:
|
|
191
|
+
open: d,
|
|
192
|
+
close: f,
|
|
193
|
+
toggle: p,
|
|
194
|
+
update: h.update
|
|
195
|
+
}), (e, t) => v(e.$slots, "default", {
|
|
196
|
+
isOpen: l.value,
|
|
197
|
+
open: d,
|
|
198
|
+
close: f,
|
|
199
|
+
toggle: p,
|
|
200
|
+
placement: S(h).placement.value,
|
|
201
|
+
side: S(h).side.value,
|
|
202
|
+
align: S(h).align.value
|
|
146
203
|
});
|
|
147
204
|
}
|
|
148
|
-
}),
|
|
205
|
+
}), Z = /* @__PURE__ */ s({
|
|
149
206
|
__name: "SbReference",
|
|
150
207
|
props: {
|
|
151
208
|
as: { default: "button" },
|
|
152
|
-
|
|
209
|
+
id: { default: void 0 },
|
|
210
|
+
ariaHaspopup: { default: void 0 }
|
|
153
211
|
},
|
|
154
212
|
setup(e) {
|
|
155
|
-
let t = e, i =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
213
|
+
let t = e, i = C(), a = H("SbReference"), { isOpen: o, disabled: s, floatingId: c, floatingRole: l } = a, u = () => typeof t.as != "string" || t.as === "button" || t.as === "a" && i.href != null, d = `${w()}-reference`, f = n(() => t.id ?? d);
|
|
214
|
+
D(() => {
|
|
215
|
+
a.referenceId.value = f.value;
|
|
216
|
+
}), p(() => {
|
|
217
|
+
a.referenceId.value === f.value && (a.referenceId.value = void 0);
|
|
218
|
+
});
|
|
219
|
+
let m = n(() => t.ariaHaspopup ?? (l.value && Object.hasOwn(B, l.value) ? B[l.value] : void 0)), g = (e) => {
|
|
220
|
+
let t = e && typeof e == "object" && "$el" in e ? e.$el : e;
|
|
221
|
+
a.reference.value = t instanceof HTMLElement ? t : null;
|
|
222
|
+
}, _ = () => {
|
|
223
|
+
a.trigger.value === "click" && a.toggle();
|
|
224
|
+
}, b = (e) => {
|
|
225
|
+
let t = a.floating.value, n = t && q(t)[0];
|
|
226
|
+
n && (e.preventDefault(), n.focus());
|
|
227
|
+
}, x = (e) => {
|
|
228
|
+
e.key === "Tab" && !e.shiftKey && o.value ? b(e) : u() || e.key !== "Enter" && e.key !== " " || (e.preventDefault(), _());
|
|
161
229
|
};
|
|
162
|
-
return (e, n) => (
|
|
163
|
-
|
|
230
|
+
return (e, n) => (h(), r(y(t.as), {
|
|
231
|
+
id: f.value,
|
|
232
|
+
ref: g,
|
|
164
233
|
type: t.as === "button" ? "button" : void 0,
|
|
165
|
-
role:
|
|
166
|
-
tabindex:
|
|
167
|
-
disabled: t.as === "button" &&
|
|
168
|
-
"aria-disabled":
|
|
169
|
-
"aria-haspopup":
|
|
170
|
-
"aria-expanded":
|
|
171
|
-
"aria-controls":
|
|
172
|
-
"data-sbf-state":
|
|
173
|
-
onClick:
|
|
174
|
-
onKeydown:
|
|
234
|
+
role: u() ? void 0 : "button",
|
|
235
|
+
tabindex: u() ? void 0 : S(s) ? -1 : 0,
|
|
236
|
+
disabled: t.as === "button" && S(s) ? !0 : void 0,
|
|
237
|
+
"aria-disabled": t.as !== "button" && S(s) ? "true" : void 0,
|
|
238
|
+
"aria-haspopup": m.value,
|
|
239
|
+
"aria-expanded": S(o) ? "true" : "false",
|
|
240
|
+
"aria-controls": S(o) ? S(c) : void 0,
|
|
241
|
+
"data-sbf-state": S(o) ? "open" : "closed",
|
|
242
|
+
onClick: _,
|
|
243
|
+
onKeydown: x
|
|
175
244
|
}, {
|
|
176
|
-
default:
|
|
245
|
+
default: O(() => [v(e.$slots, "default")]),
|
|
177
246
|
_: 3
|
|
178
247
|
}, 40, [
|
|
248
|
+
"id",
|
|
179
249
|
"type",
|
|
180
250
|
"role",
|
|
181
251
|
"tabindex",
|
|
@@ -187,7 +257,7 @@ var R = /* @__PURE__ */ new WeakSet(), z = /* @__PURE__ */ s({
|
|
|
187
257
|
"data-sbf-state"
|
|
188
258
|
]));
|
|
189
259
|
}
|
|
190
|
-
}),
|
|
260
|
+
}), Q = /* @__PURE__ */ s({
|
|
191
261
|
inheritAttrs: !1,
|
|
192
262
|
__name: "SbFloating",
|
|
193
263
|
props: {
|
|
@@ -202,69 +272,105 @@ var R = /* @__PURE__ */ new WeakSet(), z = /* @__PURE__ */ s({
|
|
|
202
272
|
zIndex: { default: void 0 },
|
|
203
273
|
autoFocus: {
|
|
204
274
|
type: Boolean,
|
|
205
|
-
default: !
|
|
275
|
+
default: !0
|
|
206
276
|
}
|
|
207
277
|
},
|
|
208
278
|
emits: ["after-enter", "after-leave"],
|
|
209
279
|
setup(s, { emit: c }) {
|
|
210
|
-
let l = s, p = c,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
280
|
+
let l = s, p = c, m = C(), g = H("SbFloating"), { isOpen: b, floatingId: x, referenceId: w, side: T, align: k } = g;
|
|
281
|
+
D(() => {
|
|
282
|
+
g.floatingRole.value = l.role;
|
|
283
|
+
});
|
|
284
|
+
let A = _(b.value), j = _(null), M = (e) => {
|
|
285
|
+
g.floating.value = e instanceof HTMLElement ? e : null;
|
|
286
|
+
}, N = (e) => {
|
|
287
|
+
j.value = e instanceof HTMLElement ? e : null;
|
|
288
|
+
}, P = n(() => ({
|
|
289
|
+
...g.floatingStyles.value,
|
|
216
290
|
...l.zIndex === void 0 ? {} : { zIndex: l.zIndex }
|
|
217
|
-
})),
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
291
|
+
})), F = () => ({
|
|
292
|
+
"aria-labelledby": l.role && Object.hasOwn(B, l.role) && m["aria-label"] == null ? w.value : void 0,
|
|
293
|
+
...m
|
|
294
|
+
}), I = n(() => b.value && !l.teleportDisabled), L = {
|
|
295
|
+
[U]: "",
|
|
296
|
+
tabindex: 0,
|
|
297
|
+
style: {
|
|
298
|
+
position: "fixed",
|
|
299
|
+
opacity: 0,
|
|
300
|
+
outline: "none",
|
|
301
|
+
pointerEvents: "none"
|
|
302
|
+
}
|
|
303
|
+
}, R = n(() => {
|
|
304
|
+
let { transition: e } = l;
|
|
305
|
+
return e ? typeof e == "string" ? { name: e } : e : { css: !1 };
|
|
306
|
+
}), z = (e) => e.setAttribute("data-sbf-state", "closed"), V = () => p("after-enter"), W = () => {
|
|
307
|
+
b.value || (A.value = !1), p("after-leave");
|
|
308
|
+
}, G = (e) => e instanceof Node && !!j.value?.contains(e), K = (e) => {
|
|
309
|
+
let t = j.value;
|
|
310
|
+
if (!t) return;
|
|
311
|
+
let n = q(t);
|
|
312
|
+
((e === "first" ? n[0] : n[n.length - 1]) ?? t).focus({ preventScroll: !0 });
|
|
313
|
+
}, Y = (e) => {
|
|
314
|
+
G(e.relatedTarget) ? g.reference.value?.focus() : K("first");
|
|
315
|
+
}, X = (e) => {
|
|
316
|
+
if (!G(e.relatedTarget)) {
|
|
317
|
+
K("last");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
let t = g.reference.value;
|
|
321
|
+
((t && J(t, (e) => g.contains(e))) ?? t)?.focus();
|
|
322
|
+
}, Z = () => {
|
|
323
|
+
let e = j.value;
|
|
324
|
+
e && (e.querySelector("[autofocus]") ?? q(e)[0] ?? e).focus({ preventScroll: !0 });
|
|
222
325
|
};
|
|
223
|
-
return
|
|
224
|
-
|
|
225
|
-
|
|
326
|
+
return E(b, (e) => {
|
|
327
|
+
let t = g.skipFocusReturn;
|
|
328
|
+
if (g.skipFocusReturn = !1, e) {
|
|
329
|
+
A.value = !0;
|
|
226
330
|
return;
|
|
227
331
|
}
|
|
228
|
-
let
|
|
229
|
-
t &&
|
|
230
|
-
}),
|
|
231
|
-
e && l.autoFocus && (await d(),
|
|
232
|
-
}), (n, s) => (
|
|
332
|
+
let n = document.activeElement;
|
|
333
|
+
!t && n && g.floating.value?.contains(n) && g.reference.value?.focus({ preventScroll: !0 });
|
|
334
|
+
}), E(() => b.value && g.isPositioned.value, async (e) => {
|
|
335
|
+
e && l.autoFocus && (await d(), Z());
|
|
336
|
+
}), (n, s) => (h(), r(e, {
|
|
233
337
|
to: l.teleportTo,
|
|
234
338
|
disabled: l.teleportDisabled
|
|
235
|
-
}, [
|
|
339
|
+
}, [A.value ? (h(), a("div", {
|
|
236
340
|
key: 0,
|
|
237
|
-
ref:
|
|
238
|
-
style: f(
|
|
239
|
-
}, [
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
default: T(() => [x(y) ? (m(), r(v(l.as), u({ key: 0 }, n.$attrs, {
|
|
247
|
-
id: x(b),
|
|
248
|
-
ref: k,
|
|
249
|
-
role: l.role,
|
|
250
|
-
tabindex: l.autoFocus ? -1 : void 0,
|
|
251
|
-
"data-sbf-state": x(y) ? "open" : "closed",
|
|
252
|
-
"data-sbf-side": x(S),
|
|
253
|
-
"data-sbf-align": x(C)
|
|
341
|
+
ref: M,
|
|
342
|
+
style: f(P.value)
|
|
343
|
+
}, [
|
|
344
|
+
I.value ? (h(), a("span", u({ key: 0 }, L, { onFocus: Y }), null, 16)) : i("", !0),
|
|
345
|
+
o(t, u(R.value, {
|
|
346
|
+
appear: "",
|
|
347
|
+
onBeforeLeave: z,
|
|
348
|
+
onAfterEnter: V,
|
|
349
|
+
onAfterLeave: W
|
|
254
350
|
}), {
|
|
255
|
-
default:
|
|
351
|
+
default: O(() => [S(b) ? (h(), r(y(l.as), u({ key: 0 }, F(), {
|
|
352
|
+
id: S(x),
|
|
353
|
+
ref: N,
|
|
354
|
+
role: l.role ?? void 0,
|
|
355
|
+
tabindex: l.autoFocus ? -1 : void 0,
|
|
356
|
+
"data-sbf-state": "open",
|
|
357
|
+
"data-sbf-side": S(T),
|
|
358
|
+
"data-sbf-align": S(k)
|
|
359
|
+
}), {
|
|
360
|
+
default: O(() => [v(n.$slots, "default")]),
|
|
361
|
+
_: 3
|
|
362
|
+
}, 16, [
|
|
363
|
+
"id",
|
|
364
|
+
"role",
|
|
365
|
+
"tabindex",
|
|
366
|
+
"data-sbf-side",
|
|
367
|
+
"data-sbf-align"
|
|
368
|
+
])) : i("", !0)]),
|
|
256
369
|
_: 3
|
|
257
|
-
}, 16,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"tabindex",
|
|
261
|
-
"data-sbf-state",
|
|
262
|
-
"data-sbf-side",
|
|
263
|
-
"data-sbf-align"
|
|
264
|
-
])) : i("", !0)]),
|
|
265
|
-
_: 3
|
|
266
|
-
}, 8, ["name", "css"])], 4)) : i("", !0)], 8, ["to", "disabled"]));
|
|
370
|
+
}, 16),
|
|
371
|
+
I.value ? (h(), a("span", u({ key: 1 }, L, { onFocus: X }), null, 16)) : i("", !0)
|
|
372
|
+
], 4)) : i("", !0)], 8, ["to", "disabled"]));
|
|
267
373
|
}
|
|
268
374
|
});
|
|
269
375
|
//#endregion
|
|
270
|
-
export {
|
|
376
|
+
export { X as SbContainer, Q as SbFloating, Z as SbReference };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("vue"),require("@floating-ui/vue")):typeof define==`function`&&define.amd?define([`exports`,`vue`,`@floating-ui/vue`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.SbFloatingPanelVue={},e.Vue,e.FloatingUIVue))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=e=>`${Math.max(0,e)}px`;function i(e){let i=(0,t.ref)(null),a=(0,t.ref)(null),o=(0,t.computed)(()=>{let i=(0,t.toValue)(e.padding),a=(0,t.toValue)(e.matchWidth),o=(0,t.toValue)(e.placement).includes(`-`),s=(0,n.flip)({padding:i,crossAxis:`alignment`,fallbackAxisSideDirection:`end`}),c=(0,n.shift)({padding:i,limiter:(0,n.limitShift)()}),l=[(0,n.offset)((0,t.toValue)(e.offset)),...o?[s,c]:[c,s],(0,n.size)({padding:i,apply({rects:e,elements:t,availableWidth:n,availableHeight:i}){let{style:o}=t.floating,s=r(e.reference.width);o.setProperty(`--sbf-reference-width`,s),o.setProperty(`--sbf-available-width`,r(n)),o.setProperty(`--sbf-available-height`,r(i)),o.width=a===!0?s:``,o.minWidth=a===`min`?s:``}})];return(0,t.toValue)(e.hideWhenDetached)&&l.push((0,n.hide)({padding:i})),l}),{floatingStyles:s,middlewareData:c,placement:l,isPositioned:u,update:d}=(0,n.useFloating)(i,a,{open:()=>(0,t.toValue)(e.isOpen),placement:()=>(0,t.toValue)(e.placement),strategy:()=>(0,t.toValue)(e.strategy),middleware:o,whileElementsMounted:n.autoUpdate});return{reference:i,floating:a,floatingStyles:(0,t.computed)(()=>({...s.value,...c.value.hide?.referenceHidden?{visibility:`hidden`}:{}})),placement:l,side:(0,t.computed)(()=>l.value.split(`-`)[0]),align:(0,t.computed)(()=>l.value.split(`-`)[1]),isPositioned:u,update:d}}var a=Symbol(`SbFloatingContext`);function o(e){let n=(0,t.inject)(a,null);if(!n)throw Error(`[sb-floating-panel-vue] <${e}> must be used inside <SbContainer>.`);return n}var s=new WeakSet,c=(0,t.defineComponent)({inheritAttrs:!1,__name:`SbContainer`,props:(0,t.mergeModels)({placement:{default:`bottom-start`},strategy:{default:`absolute`},offset:{default:8},padding:{default:8},matchWidth:{type:[Boolean,String],default:!1},hideWhenDetached:{type:Boolean,default:!1},defaultOpen:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},trigger:{default:`click`},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0}},{open:{type:Boolean,default:void 0},openModifiers:{}}),emits:(0,t.mergeModels)([`open`,`close`],[`update:open`]),setup(e,{expose:n,emit:r}){let o=e,c=(0,t.useModel)(e,`open`),l=r,u=(0,t.computed)(()=>!o.disabled&&(c.value??o.defaultOpen)),d=e=>{e&&o.disabled||e!==u.value&&(c.value=e)},f=()=>d(!0),p=()=>d(!1),m=()=>d(!u.value),h=i({isOpen:u,placement:()=>o.placement,strategy:()=>o.strategy,offset:()=>o.offset,padding:()=>o.padding,matchWidth:()=>o.matchWidth,hideWhenDetached:()=>o.hideWhenDetached}),g=new Set,_={...h,isOpen:u,disabled:(0,t.toRef)(o,`disabled`),trigger:(0,t.toRef)(o,`trigger`),floatingId:`${(0,t.useId)()}-floating`,open:f,close:p,toggle:m,contains:e=>e instanceof Node?h.reference.value?.contains(e)||h.floating.value?.contains(e)?!0:[...g].some(t=>t.contains(e)):!1,hasOpenChild:()=>[...g].some(e=>e.isOpen.value||e.hasOpenChild()),registerChild:e=>(g.add(e),()=>g.delete(e))},v=(0,t.inject)(a,null);v&&(0,t.onScopeDispose)(v.registerChild(_)),(0,t.provide)(a,_);let y=e=>{e.key===`Escape`&&o.closeOnEscape&&!_.hasOpenChild()&&(s.has(e)||(s.add(e),p()))},b=e=>{o.closeOnClickOutside&&(_.contains(e.composedPath()[0]??e.target)||p())};return(0,t.watch)(u,(e,t,n)=>{e&&typeof document<`u`&&(document.addEventListener(`keydown`,y),document.addEventListener(`pointerdown`,b,!0),n(()=>{document.removeEventListener(`keydown`,y),document.removeEventListener(`pointerdown`,b,!0)}))},{immediate:!0}),(0,t.watch)(u,e=>l(e?`open`:`close`)),(0,t.watch)(()=>o.disabled,e=>{e&&c.value&&(c.value=!1)}),n({open:f,close:p,toggle:m,update:h.update}),(e,n)=>(0,t.renderSlot)(e.$slots,`default`,{isOpen:u.value,open:f,close:p,toggle:m,placement:(0,t.unref)(h).placement.value,side:(0,t.unref)(h).side.value})}}),l=(0,t.defineComponent)({__name:`SbReference`,props:{as:{default:`button`},ariaHaspopup:{default:`dialog`}},setup(e){let n=e,r=o(`SbReference`),{isOpen:i,disabled:a,floatingId:s}=r,c=(0,t.computed)(()=>n.as===`button`||n.as===`a`),l=e=>{r.reference.value=e instanceof HTMLElement?e:null},u=()=>{r.trigger.value===`click`&&r.toggle()},d=e=>{c.value||e.key!==`Enter`&&e.key!==` `||(e.preventDefault(),u())};return(e,r)=>((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(n.as),{ref:l,type:n.as===`button`?`button`:void 0,role:c.value?void 0:`button`,tabindex:c.value?void 0:(0,t.unref)(a)?-1:0,disabled:n.as===`button`&&(0,t.unref)(a)?!0:void 0,"aria-disabled":!c.value&&(0,t.unref)(a)?`true`:void 0,"aria-haspopup":n.ariaHaspopup,"aria-expanded":(0,t.unref)(i)?`true`:`false`,"aria-controls":(0,t.unref)(i)?(0,t.unref)(s):void 0,"data-sbf-state":(0,t.unref)(i)?`open`:`closed`,onClick:u,onKeydown:d},{default:(0,t.withCtx)(()=>[(0,t.renderSlot)(e.$slots,`default`)]),_:3},40,[`type`,`role`,`tabindex`,`disabled`,`aria-disabled`,`aria-haspopup`,`aria-expanded`,`aria-controls`,`data-sbf-state`]))}}),u=`a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]`,d=(0,t.defineComponent)({inheritAttrs:!1,__name:`SbFloating`,props:{as:{default:`div`},role:{default:`dialog`},transition:{default:void 0},teleportTo:{default:`body`},teleportDisabled:{type:Boolean,default:!1},zIndex:{default:void 0},autoFocus:{type:Boolean,default:!1}},emits:[`after-enter`,`after-leave`],setup(e,{emit:n}){let r=e,i=n,a=o(`SbFloating`),{isOpen:s,floatingId:c,side:l,align:d}=a,f=(0,t.ref)(s.value),p=(0,t.ref)(null),m=e=>{a.floating.value=e instanceof HTMLElement?e:null},h=e=>{p.value=e instanceof HTMLElement?e:null},g=(0,t.computed)(()=>({...a.floatingStyles.value,...r.zIndex===void 0?{}:{zIndex:r.zIndex}})),_=()=>i(`after-enter`),v=()=>{s.value||(f.value=!1),i(`after-leave`)},y=()=>{let e=p.value;e&&(e.querySelector(`[autofocus]`)??e.querySelector(u)??e).focus({preventScroll:!0})};return(0,t.watch)(s,e=>{if(e){f.value=!0;return}let t=document.activeElement;t&&a.floating.value?.contains(t)&&a.reference.value?.focus()}),(0,t.watch)(()=>s.value&&a.isPositioned.value,async e=>{e&&r.autoFocus&&(await(0,t.nextTick)(),y())}),(e,n)=>((0,t.openBlock)(),(0,t.createBlock)(t.Teleport,{to:r.teleportTo,disabled:r.teleportDisabled},[f.value?((0,t.openBlock)(),(0,t.createElementBlock)(`div`,{key:0,ref:m,style:(0,t.normalizeStyle)(g.value)},[(0,t.createVNode)(t.Transition,{name:r.transition,css:!!r.transition,appear:``,onAfterEnter:_,onAfterLeave:v},{default:(0,t.withCtx)(()=>[(0,t.unref)(s)?((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(r.as),(0,t.mergeProps)({key:0},e.$attrs,{id:(0,t.unref)(c),ref:h,role:r.role,tabindex:r.autoFocus?-1:void 0,"data-sbf-state":(0,t.unref)(s)?`open`:`closed`,"data-sbf-side":(0,t.unref)(l),"data-sbf-align":(0,t.unref)(d)}),{default:(0,t.withCtx)(()=>[(0,t.renderSlot)(e.$slots,`default`)]),_:3},16,[`id`,`role`,`tabindex`,`data-sbf-state`,`data-sbf-side`,`data-sbf-align`])):(0,t.createCommentVNode)(``,!0)]),_:3},8,[`name`,`css`])],4)):(0,t.createCommentVNode)(``,!0)],8,[`to`,`disabled`]))}});e.SbContainer=c,e.SbFloating=d,e.SbReference=l});
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("vue"),require("@floating-ui/vue")):typeof define==`function`&&define.amd?define([`exports`,`vue`,`@floating-ui/vue`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.SbFloatingPanelVue={},e.Vue,e.FloatingUIVue))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=e=>`${Math.max(0,e)}px`,i=e=>{let[t,n]=e.split(`-`),r=n===`start`?`0%`:n===`end`?`100%`:`50%`;return t===`top`?`${r} 100%`:t===`bottom`?`${r} 0%`:t===`left`?`100% ${r}`:`0% ${r}`};function a(e){let a=(0,t.ref)(null),o=(0,t.ref)(null),s=(0,t.computed)(()=>{let a=(0,t.toValue)(e.padding),o=(0,t.toValue)(e.matchWidth),s=(0,t.toValue)(e.placement).includes(`-`),c=(0,n.flip)({padding:a,crossAxis:`alignment`,fallbackAxisSideDirection:`end`}),l=(0,n.shift)({padding:a,limiter:(0,n.limitShift)()}),u=[(0,n.offset)((0,t.toValue)(e.offset)),...s?[c,l]:[l,c],(0,n.size)({padding:a,apply({rects:e,elements:t,availableWidth:n,availableHeight:a,placement:s}){let{style:c}=t.floating,l=r(e.reference.width);c.setProperty(`--sbf-reference-width`,l),c.setProperty(`--sbf-transform-origin`,i(s)),c.setProperty(`--sbf-available-width`,r(n)),c.setProperty(`--sbf-available-height`,r(a)),c.width=o===!0?l:``,c.minWidth=o===`min`?l:``}})];return(0,t.toValue)(e.hideWhenDetached)&&u.push((0,n.hide)({padding:a})),u}),{floatingStyles:c,middlewareData:l,placement:u,isPositioned:d,update:f}=(0,n.useFloating)(a,o,{open:()=>(0,t.toValue)(e.isOpen),placement:()=>(0,t.toValue)(e.placement),strategy:()=>(0,t.toValue)(e.strategy),middleware:s,whileElementsMounted:n.autoUpdate});return{reference:a,floating:o,floatingStyles:(0,t.computed)(()=>({...c.value,...l.value.hide?.referenceHidden?{visibility:`hidden`}:{}})),placement:u,side:(0,t.computed)(()=>u.value.split(`-`)[0]),align:(0,t.computed)(()=>u.value.split(`-`)[1]),isPositioned:d,update:f}}var o={dialog:`dialog`,alertdialog:`dialog`,menu:`menu`,listbox:`listbox`,tree:`tree`,grid:`grid`},s=Symbol(`SbFloatingContext`);function c(e){let n=(0,t.inject)(s,null);if(!n)throw Error(`[sb-floating-panel-vue] <${e}> must be used inside <SbContainer>.`);return n}var l=`data-sbf-focus-guard`,u=[`a[href]`,`button:not([disabled])`,`input:not([disabled]):not([type="hidden"])`,`select:not([disabled])`,`textarea:not([disabled])`,`summary`,`audio[controls]`,`video[controls]`,`[tabindex]`,`[contenteditable="true"]`].join(`,`),d=e=>e.tabIndex>=0&&!e.hasAttribute(`data-sbf-focus-guard`)&&!e.closest(`[inert]`)&&(e.checkVisibility?.()??!0);function f(e){let t=e instanceof Element?e.closest(u):null;return!!t&&!t.closest(`[inert]`)}function p(e){return[...e.querySelectorAll(u)].filter(d)}function m(e,t){return p(e.ownerDocument).find(n=>!!(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)&&!e.contains(n)&&!t(n))}var h=[],g=(0,t.defineComponent)({inheritAttrs:!1,__name:`SbContainer`,props:(0,t.mergeModels)({placement:{default:`bottom-start`},strategy:{default:`absolute`},offset:{default:8},padding:{default:8},matchWidth:{type:[Boolean,String],default:!1},hideWhenDetached:{type:Boolean,default:!1},defaultOpen:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},trigger:{default:`click`},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0},closeOnFocusOut:{type:Boolean,default:!0}},{open:{type:Boolean,default:void 0},openModifiers:{}}),emits:(0,t.mergeModels)([`open`,`close`],[`update:open`]),setup(e,{expose:n,emit:r}){let i=e,o=(0,t.useModel)(e,`open`),c=r,l=i.defaultOpen,u=(0,t.computed)(()=>!i.disabled&&(o.value??l)),d=e=>{e&&i.disabled||e!==u.value&&(o.value=e)},p=()=>d(!0),m=()=>d(!1),g=()=>d(!u.value),_=a({isOpen:u,placement:()=>i.placement,strategy:()=>i.strategy,offset:()=>i.offset,padding:()=>i.padding,matchWidth:()=>i.matchWidth,hideWhenDetached:()=>i.hideWhenDetached}),v=new Set,y={..._,isOpen:u,disabled:(0,t.toRef)(i,`disabled`),trigger:(0,t.toRef)(i,`trigger`),floatingId:`${(0,t.useId)()}-floating`,referenceId:(0,t.ref)(),floatingRole:(0,t.ref)(`dialog`),skipFocusReturn:!1,open:p,close:m,toggle:g,contains:e=>e instanceof Node?_.reference.value?.contains(e)||_.floating.value?.contains(e)?!0:[...v].some(t=>t.contains(e)):!1,registerChild:e=>(v.add(e),()=>v.delete(e))},b=(0,t.inject)(s,null);b&&(0,t.onScopeDispose)(b.registerChild(y)),(0,t.provide)(s,y);let x=e=>{e.key===`Escape`&&h[h.length-1]===y&&(!i.closeOnEscape||e.defaultPrevented||e.isComposing||(e.preventDefault(),m()))},S=e=>{y.contains(e)||(y.skipFocusReturn=f(e),m())},C,w=e=>{if(C?.(),!i.closeOnClickOutside)return;let t=e.composedPath()[0]??e.target;if(e.pointerType!==`touch`){S(t);return}if(y.contains(t))return;let n=e=>{C?.(),S(e.composedPath()[0]??e.target)};document.addEventListener(`click`,n,!0),C=()=>{document.removeEventListener(`click`,n,!0),C=void 0}},T=e=>{i.closeOnFocusOut&&(y.contains(e.composedPath()[0]??e.target)||m())};return(0,t.watch)(u,(e,t,n)=>{e&&typeof document<`u`&&(h.push(y),document.addEventListener(`keydown`,x),document.addEventListener(`pointerdown`,w,!0),document.addEventListener(`focusin`,T),n(()=>{document.removeEventListener(`keydown`,x),document.removeEventListener(`pointerdown`,w,!0),document.removeEventListener(`focusin`,T),C?.(),h.splice(h.indexOf(y),1)}))},{immediate:!0}),(0,t.watch)(u,e=>c(e?`open`:`close`)),(0,t.watch)(()=>i.disabled,e=>{e&&(o.value??l)&&(o.value=!1)}),n({open:p,close:m,toggle:g,update:_.update}),(e,n)=>(0,t.renderSlot)(e.$slots,`default`,{isOpen:u.value,open:p,close:m,toggle:g,placement:(0,t.unref)(_).placement.value,side:(0,t.unref)(_).side.value,align:(0,t.unref)(_).align.value})}}),_=(0,t.defineComponent)({__name:`SbReference`,props:{as:{default:`button`},id:{default:void 0},ariaHaspopup:{default:void 0}},setup(e){let n=e,r=(0,t.useAttrs)(),i=c(`SbReference`),{isOpen:a,disabled:s,floatingId:l,floatingRole:u}=i,d=()=>typeof n.as!=`string`||n.as===`button`||n.as===`a`&&r.href!=null,f=`${(0,t.useId)()}-reference`,m=(0,t.computed)(()=>n.id??f);(0,t.watchEffect)(()=>{i.referenceId.value=m.value}),(0,t.onBeforeUnmount)(()=>{i.referenceId.value===m.value&&(i.referenceId.value=void 0)});let h=(0,t.computed)(()=>n.ariaHaspopup??(u.value&&Object.hasOwn(o,u.value)?o[u.value]:void 0)),g=e=>{let t=e&&typeof e==`object`&&`$el`in e?e.$el:e;i.reference.value=t instanceof HTMLElement?t:null},_=()=>{i.trigger.value===`click`&&i.toggle()},v=e=>{let t=i.floating.value,n=t&&p(t)[0];n&&(e.preventDefault(),n.focus())},y=e=>{e.key===`Tab`&&!e.shiftKey&&a.value?v(e):d()||e.key!==`Enter`&&e.key!==` `||(e.preventDefault(),_())};return(e,r)=>((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(n.as),{id:m.value,ref:g,type:n.as===`button`?`button`:void 0,role:d()?void 0:`button`,tabindex:d()?void 0:(0,t.unref)(s)?-1:0,disabled:n.as===`button`&&(0,t.unref)(s)?!0:void 0,"aria-disabled":n.as!==`button`&&(0,t.unref)(s)?`true`:void 0,"aria-haspopup":h.value,"aria-expanded":(0,t.unref)(a)?`true`:`false`,"aria-controls":(0,t.unref)(a)?(0,t.unref)(l):void 0,"data-sbf-state":(0,t.unref)(a)?`open`:`closed`,onClick:_,onKeydown:y},{default:(0,t.withCtx)(()=>[(0,t.renderSlot)(e.$slots,`default`)]),_:3},40,[`id`,`type`,`role`,`tabindex`,`disabled`,`aria-disabled`,`aria-haspopup`,`aria-expanded`,`aria-controls`,`data-sbf-state`]))}}),v=(0,t.defineComponent)({inheritAttrs:!1,__name:`SbFloating`,props:{as:{default:`div`},role:{default:`dialog`},transition:{default:void 0},teleportTo:{default:`body`},teleportDisabled:{type:Boolean,default:!1},zIndex:{default:void 0},autoFocus:{type:Boolean,default:!0}},emits:[`after-enter`,`after-leave`],setup(e,{emit:n}){let r=e,i=n,a=(0,t.useAttrs)(),s=c(`SbFloating`),{isOpen:u,floatingId:d,referenceId:f,side:h,align:g}=s;(0,t.watchEffect)(()=>{s.floatingRole.value=r.role});let _=(0,t.ref)(u.value),v=(0,t.ref)(null),y=e=>{s.floating.value=e instanceof HTMLElement?e:null},b=e=>{v.value=e instanceof HTMLElement?e:null},x=(0,t.computed)(()=>({...s.floatingStyles.value,...r.zIndex===void 0?{}:{zIndex:r.zIndex}})),S=()=>({"aria-labelledby":r.role&&Object.hasOwn(o,r.role)&&a[`aria-label`]==null?f.value:void 0,...a}),C=(0,t.computed)(()=>u.value&&!r.teleportDisabled),w={[l]:``,tabindex:0,style:{position:`fixed`,opacity:0,outline:`none`,pointerEvents:`none`}},T=(0,t.computed)(()=>{let{transition:e}=r;return e?typeof e==`string`?{name:e}:e:{css:!1}}),E=e=>e.setAttribute(`data-sbf-state`,`closed`),D=()=>i(`after-enter`),O=()=>{u.value||(_.value=!1),i(`after-leave`)},k=e=>e instanceof Node&&!!v.value?.contains(e),A=e=>{let t=v.value;if(!t)return;let n=p(t);((e===`first`?n[0]:n[n.length-1])??t).focus({preventScroll:!0})},j=e=>{k(e.relatedTarget)?s.reference.value?.focus():A(`first`)},M=e=>{if(!k(e.relatedTarget)){A(`last`);return}let t=s.reference.value;((t&&m(t,e=>s.contains(e)))??t)?.focus()},N=()=>{let e=v.value;e&&(e.querySelector(`[autofocus]`)??p(e)[0]??e).focus({preventScroll:!0})};return(0,t.watch)(u,e=>{let t=s.skipFocusReturn;if(s.skipFocusReturn=!1,e){_.value=!0;return}let n=document.activeElement;!t&&n&&s.floating.value?.contains(n)&&s.reference.value?.focus({preventScroll:!0})}),(0,t.watch)(()=>u.value&&s.isPositioned.value,async e=>{e&&r.autoFocus&&(await(0,t.nextTick)(),N())}),(e,n)=>((0,t.openBlock)(),(0,t.createBlock)(t.Teleport,{to:r.teleportTo,disabled:r.teleportDisabled},[_.value?((0,t.openBlock)(),(0,t.createElementBlock)(`div`,{key:0,ref:y,style:(0,t.normalizeStyle)(x.value)},[C.value?((0,t.openBlock)(),(0,t.createElementBlock)(`span`,(0,t.mergeProps)({key:0},w,{onFocus:j}),null,16)):(0,t.createCommentVNode)(``,!0),(0,t.createVNode)(t.Transition,(0,t.mergeProps)(T.value,{appear:``,onBeforeLeave:E,onAfterEnter:D,onAfterLeave:O}),{default:(0,t.withCtx)(()=>[(0,t.unref)(u)?((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(r.as),(0,t.mergeProps)({key:0},S(),{id:(0,t.unref)(d),ref:b,role:r.role??void 0,tabindex:r.autoFocus?-1:void 0,"data-sbf-state":`open`,"data-sbf-side":(0,t.unref)(h),"data-sbf-align":(0,t.unref)(g)}),{default:(0,t.withCtx)(()=>[(0,t.renderSlot)(e.$slots,`default`)]),_:3},16,[`id`,`role`,`tabindex`,`data-sbf-side`,`data-sbf-align`])):(0,t.createCommentVNode)(``,!0)]),_:3},16),C.value?((0,t.openBlock)(),(0,t.createElementBlock)(`span`,(0,t.mergeProps)({key:1},w,{onFocus:M}),null,16)):(0,t.createCommentVNode)(``,!0)],4)):(0,t.createCommentVNode)(``,!0)],8,[`to`,`disabled`]))}});e.SbContainer=g,e.SbFloating=v,e.SbReference=_});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Component, TransitionProps } from 'vue';
|
|
1
2
|
/** Side of the reference the panel is placed on. */
|
|
2
3
|
export type SbSide = 'top' | 'right' | 'bottom' | 'left';
|
|
3
4
|
/** Alignment of the panel along the side. */
|
|
@@ -80,6 +81,12 @@ export interface SbContainerProps {
|
|
|
80
81
|
* @default true
|
|
81
82
|
*/
|
|
82
83
|
closeOnClickOutside?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Closes the panel when keyboard focus moves outside the reference and the panel
|
|
86
|
+
* (e.g. tabbing past the last element of the panel).
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
closeOnFocusOut?: boolean;
|
|
83
90
|
}
|
|
84
91
|
/**
|
|
85
92
|
* Props passed to the default slot of `SbContainer`.
|
|
@@ -97,6 +104,8 @@ export interface SbContainerSlotProps {
|
|
|
97
104
|
placement: SbPlacement;
|
|
98
105
|
/** Side of the resolved placement. */
|
|
99
106
|
side: SbSide;
|
|
107
|
+
/** Alignment of the resolved placement, `undefined` when centered. */
|
|
108
|
+
align: SbAlignment | undefined;
|
|
100
109
|
}
|
|
101
110
|
/**
|
|
102
111
|
* Methods exposed by `SbContainer` through a template ref.
|
|
@@ -116,14 +125,26 @@ export interface SbContainerExposed {
|
|
|
116
125
|
*/
|
|
117
126
|
export interface SbReferenceProps {
|
|
118
127
|
/**
|
|
119
|
-
* HTML tag to render.
|
|
120
|
-
*
|
|
128
|
+
* HTML tag or component to render.
|
|
129
|
+
* - `button` gets `type="button"`.
|
|
130
|
+
* - `a` with `href` and components are left as they are: a component is expected to render
|
|
131
|
+
* its own focusable element (e.g. the `<Button>` of your design system).
|
|
132
|
+
* - Any other tag (including `a` without `href`) gets `role="button"`, `tabindex="0"` and
|
|
133
|
+
* Enter/Space handling.
|
|
134
|
+
*
|
|
135
|
+
* When disabled, `button` gets `disabled`; anything else gets `aria-disabled="true"`.
|
|
121
136
|
* @default 'button'
|
|
122
137
|
*/
|
|
123
|
-
as?: string;
|
|
138
|
+
as?: string | Component;
|
|
124
139
|
/**
|
|
125
|
-
*
|
|
126
|
-
* @default
|
|
140
|
+
* `id` of the element. The panel is named after the reference with `aria-labelledby`.
|
|
141
|
+
* @default an id generated with `useId()`
|
|
142
|
+
*/
|
|
143
|
+
id?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Value of `aria-haspopup`. When unset it follows the `role` of `SbFloating`
|
|
146
|
+
* (`dialog`, `menu`, `listbox`, `tree`, `grid`; `alertdialog` gives `dialog`)
|
|
147
|
+
* and is left out for any other role.
|
|
127
148
|
*/
|
|
128
149
|
ariaHaspopup?: 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid' | 'true' | 'false';
|
|
129
150
|
}
|
|
@@ -137,15 +158,24 @@ export interface SbFloatingProps {
|
|
|
137
158
|
*/
|
|
138
159
|
as?: string;
|
|
139
160
|
/**
|
|
140
|
-
* ARIA role of the panel.
|
|
161
|
+
* ARIA role of the panel. With a popup role (`dialog`, `alertdialog`, `menu`, `listbox`,
|
|
162
|
+
* `tree`, `grid`), the panel is named after the reference with `aria-labelledby`, unless
|
|
163
|
+
* you pass `aria-label` or `aria-labelledby` yourself.
|
|
164
|
+
* `null` renders no role (disclosure pattern).
|
|
141
165
|
* @default 'dialog'
|
|
142
166
|
*/
|
|
143
|
-
role?: string;
|
|
167
|
+
role?: string | null;
|
|
144
168
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
169
|
+
* Animation of the panel, through a Vue `<Transition>` on the panel element.
|
|
170
|
+
* - A name: the classes of your CSS (`<name>-enter-active`, …).
|
|
171
|
+
* - An object of `<Transition>` props: e.g. `enterActiveClass`, `leaveToClass` with
|
|
172
|
+
* Tailwind utilities, or JavaScript hooks.
|
|
173
|
+
*
|
|
174
|
+
* With any transition, Vue also waits for CSS animations keyed on
|
|
175
|
+
* `data-sbf-state="closed"`, set on the panel when it starts leaving.
|
|
176
|
+
* No animation when unset.
|
|
147
177
|
*/
|
|
148
|
-
transition?: string;
|
|
178
|
+
transition?: string | TransitionProps;
|
|
149
179
|
/**
|
|
150
180
|
* Teleport target (CSS selector or element).
|
|
151
181
|
* @default 'body'
|
|
@@ -163,7 +193,8 @@ export interface SbFloatingProps {
|
|
|
163
193
|
/**
|
|
164
194
|
* Moves focus inside the panel once it is positioned: to the first element with
|
|
165
195
|
* `autofocus`, otherwise to the first focusable element, otherwise to the panel itself.
|
|
166
|
-
*
|
|
196
|
+
* Set it to `false` for panels that must not take focus, such as tooltips.
|
|
197
|
+
* @default true
|
|
167
198
|
*/
|
|
168
199
|
autoFocus?: boolean;
|
|
169
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sb-floating-panel-vue",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Headless Vue 3 floating panel components (dropdowns, popovers, menus) powered by @floating-ui/vue: positioning, accessibility and open state handled, zero CSS shipped.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vue",
|