sb-floating-panel-vue 5.0.3 → 5.1.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 CHANGED
@@ -65,18 +65,16 @@ import { SbContainer, SbReference, SbFloating } from 'sb-floating-panel-vue';
65
65
 
66
66
  ### Menu
67
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.
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 runs its `click` handler; with `close-on-select` the menu then closes and focus goes back to the trigger.
69
69
 
70
70
  ```vue
71
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>
72
+ <SbReference class="my-trigger">Account</SbReference>
73
+ <SbFloating role="menu" close-on-select class="my-panel">
74
+ <button v-for="item in ['Profile', 'Settings', 'Sign out']" :key="item" type="button" role="menuitem" @click="go(item)">
75
+ {{ item }}
76
+ </button>
77
+ </SbFloating>
80
78
  </SbContainer>
81
79
  ```
82
80
 
@@ -174,6 +172,7 @@ The panel, teleported to `body`. It is rendered while open and stays mounted unt
174
172
  | `zIndex` | `number \| string` | — | `z-index` of the positioned wrapper. Use this, not a `z-*` class on the panel (see [Stacking](#stacking)) |
175
173
  | `autoFocus` | `boolean` | `true`, `false` with `role="tooltip"`, the `hover` / `focus` trigger or a text field reference | 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
174
  | `keyboardNavigation` | `boolean` | `true` | Handles the keyboard model of `role="menu"` and `role="listbox"` panels (see [Keyboard](#keyboard)). No effect with other roles |
175
+ | `closeOnSelect` | `boolean` | `false` | With `role="menu"` / `"listbox"`, activating a `menuitem` / `option` closes the panel (a submenu: with its parent menus) and gives focus back to the reference, after the item's `click` handler. Not for `menuitemcheckbox` / `menuitemradio`, submenu triggers, disabled items or a listbox with `aria-multiselectable="true"`; `event.preventDefault()` in the handler keeps it open |
177
176
 
178
177
  | Event | Description |
179
178
  |---------------|----------------------------------------------------------------|
@@ -182,6 +181,25 @@ The panel, teleported to `body`. It is rendered while open and stays mounted unt
182
181
 
183
182
  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.
184
183
 
184
+ ### `useSbFloating()`
185
+
186
+ The open state and actions of the closest `SbContainer`, for your own components inside it: a `MenuItem` of your design system can close the menu without `close` passed down from the slot.
187
+
188
+ ```ts
189
+ import { useSbFloating } from 'sb-floating-panel-vue';
190
+
191
+ const { isOpen, open, close, toggle } = useSbFloating();
192
+ ```
193
+
194
+ | Returns | Type | Description |
195
+ |----------|--------------------------|---------------------------|
196
+ | `isOpen` | `Readonly<Ref<boolean>>` | Whether the panel is open |
197
+ | `open` | `() => void` | Opens the panel |
198
+ | `close` | `() => void` | Closes the panel |
199
+ | `toggle` | `() => void` | Toggles the panel |
200
+
201
+ Call it in `setup` of a component rendered inside an `SbContainer` (in the reference, the panel or anywhere in its slot); outside one it throws.
202
+
185
203
  ### Types
186
204
 
187
205
  ```ts
@@ -197,6 +215,7 @@ import type {
197
215
  SbAlignment,
198
216
  SbStrategy,
199
217
  SbFloatingRole,
218
+ UseSbFloatingReturn,
200
219
  } from 'sb-floating-panel-vue';
201
220
  ```
202
221
 
@@ -226,7 +245,7 @@ import type {
226
245
  - A press outside closes the panel; on touch, only a tap does, so starting a scroll keeps it open.
227
246
  - Moving focus outside (e.g. tabbing past the last element) closes it.
228
247
  - 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"`.
248
+ - **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`); add `closeOnSelect` to close the panel after it. Use valid structure: items directly inside the `menu` / `listbox`, or inside `ul` / `li` with `role="none"`.
230
249
  - <a id="text-field-references"></a>**Text field references.** `<SbReference as="input">` (or `textarea`) stays a text field: no `role="button"`, Enter and Space type as usual, a click opens the panel without closing it, and focus stays in the field when the panel opens. The [combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) (autocomplete with focus kept in the field and `aria-activedescendant`) is not supported: with a `menu` or `listbox` panel the arrow keys move focus into the panel, and the console warns about it in development.
231
250
 
232
251
  ---
@@ -355,9 +374,9 @@ SbContainer ── provides the state ──┬─▶ SbReference
355
374
 
356
375
  - `SbReference` and `SbFloating` find their container on their own: you pass no refs, styles or handlers, and never write `.value`.
357
376
  - The lookup follows the component tree, not the DOM, so it keeps working although `SbFloating` is teleported to `body`.
358
- - They can sit at any depth inside the container slot, also inside your own components.
377
+ - They can sit at any depth inside the container slot, also inside your own components, which can read the same state with [`useSbFloating()`](#usesbfloating).
359
378
  - Used outside an `SbContainer`, they throw: `[sb-floating-panel-vue] <SbFloating> must be used inside <SbContainer>.`
360
- - In development, the console warns about a second `SbReference` or `SbFloating` in the same container about an `as` component that renders no single root element, about a text field reference of a `menu` or `listbox` panel, about a `tooltip` opened by click, and, the first time the arrow keys look for them, about a `menu` or `listbox` with no items or with items that cannot take focus. The warnings depend on `process.env.NODE_ENV`, which your bundler replaces: they are left out of production builds.
379
+ - In development, the console warns about a second `SbReference` or `SbFloating` in the same container, about an `as` component that renders no single root element, about a text field reference of a `menu` or `listbox` panel, about a `tooltip` opened by click, and, the first time the arrow keys look for them, about a `menu` or `listbox` with no items or with items that cannot take focus. The warnings depend on `process.env.NODE_ENV`, which your bundler replaces: they are left out of production builds.
361
380
 
362
381
  ### Nested containers
363
382
 
@@ -1,8 +1,8 @@
1
1
  import { TransitionProps } from 'vue';
2
2
  import { SbFloatingProps } from '../types';
3
- declare var __VLS_25: {};
3
+ declare var __VLS_26: {};
4
4
  type __VLS_Slots = {} & {
5
- default?: (props: typeof __VLS_25) => any;
5
+ default?: (props: typeof __VLS_26) => any;
6
6
  };
7
7
  declare const __VLS_base: import('vue').DefineComponent<SbFloatingProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
8
8
  "after-enter": () => any;
@@ -18,6 +18,7 @@ declare const __VLS_base: import('vue').DefineComponent<SbFloatingProps, {}, {},
18
18
  zIndex: string | number;
19
19
  autoFocus: boolean;
20
20
  keyboardNavigation: boolean;
21
+ closeOnSelect: boolean;
21
22
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
22
23
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
23
24
  declare const _default: typeof __VLS_export;
@@ -0,0 +1,6 @@
1
+ import { UseSbFloatingReturn } from '../types';
2
+ /**
3
+ * Open state and actions of the closest `SbContainer`, for your own components inside it
4
+ * (e.g. a `MenuItem` that closes the menu), without passing `close` down from the slot.
5
+ */
6
+ export declare function useSbFloating(): UseSbFloatingReturn;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
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, SbFloatingRole, } from './types';
4
+ export { useSbFloating } from './composables/useSbFloating';
5
+ export type { SbContainerProps, SbContainerSlotProps, SbContainerExposed, SbReferenceProps, SbFloatingProps, SbTrigger, SbPlacement, SbSide, SbAlignment, SbStrategy, SbFloatingRole, UseSbFloatingReturn, } from './types';
@@ -1,62 +1,62 @@
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, onMounted as m, onScopeDispose as h, openBlock as g, provide as _, ref as v, renderSlot as y, resolveDynamicComponent as b, toRef as x, toValue as S, unref as C, useAttrs as w, useId as T, useModel as E, watch as D, watchEffect as O, withCtx as k } from "vue";
2
- import { autoUpdate as A, flip as j, hide as M, limitShift as N, offset as P, shift as F, size as ee, useFloating as te } from "@floating-ui/vue";
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, onMounted as m, onScopeDispose as h, openBlock as g, provide as _, readonly as v, ref as y, renderSlot as b, resolveDynamicComponent as x, toRef as S, toValue as C, unref as w, useAttrs as T, useId as E, useModel as D, watch as O, watchEffect as k, withCtx as A } from "vue";
2
+ import { autoUpdate as j, flip as M, hide as N, limitShift as P, offset as F, shift as ee, size as te, useFloating as I } from "@floating-ui/vue";
3
3
  //#region src/utils/warn.ts
4
- var I = (() => {
4
+ var L = (() => {
5
5
  try {
6
6
  return process.env.NODE_ENV !== "production";
7
7
  } catch {
8
8
  return !1;
9
9
  }
10
10
  })();
11
- function L(e) {
12
- I && console.warn(`[sb-floating-panel-vue] ${e}`);
11
+ function R(e) {
12
+ L && console.warn(`[sb-floating-panel-vue] ${e}`);
13
13
  }
14
14
  //#endregion
15
15
  //#region src/composables/useSbFloatingContext.ts
16
- var R = Symbol("SbFloatingContext");
17
- function ne(e, t, n) {
18
- return ++e.parts[t] > 1 && L(`<SbContainer> has more than one <${n}>: they share one open state and position, so only the last one works. Use one SbContainer for each panel.`), () => e.parts[t]--;
16
+ var z = Symbol("SbFloatingContext");
17
+ function B(e, t, n) {
18
+ return ++e.parts[t] > 1 && R(`<SbContainer> has more than one <${n}>: they share one open state and position, so only the last one works. Use one SbContainer for each panel.`), () => e.parts[t]--;
19
19
  }
20
- function z(e) {
21
- let t = c(R, null);
20
+ function ne(e) {
21
+ let t = c(z, null);
22
22
  if (!t) throw Error(`[sb-floating-panel-vue] <${e}> must be used inside <SbContainer>.`);
23
23
  return t;
24
24
  }
25
25
  //#endregion
26
26
  //#region src/composables/useSbFloatingPanel.ts
27
- var B = (e) => `${Math.max(0, e)}px`, V = (e, t) => {
27
+ var V = (e) => `${Math.max(0, e)}px`, re = (e, t) => {
28
28
  let [n, r] = e.split("-"), i = t && (n === "top" || n === "bottom"), a = r === "start" ? i ? "100%" : "0%" : r === "end" ? i ? "0%" : "100%" : "50%";
29
29
  return n === "top" ? `${a} 100%` : n === "bottom" ? `${a} 0%` : n === "left" ? `100% ${a}` : `0% ${a}`;
30
30
  };
31
- function re(e) {
32
- let t = v(null), r = v(null), i = n(() => {
33
- let t = S(e.padding), n = S(e.matchWidth), r = S(e.placement).includes("-"), i = j({
31
+ function ie(e) {
32
+ let t = y(null), r = y(null), i = n(() => {
33
+ let t = C(e.padding), n = C(e.matchWidth), r = C(e.placement).includes("-"), i = M({
34
34
  padding: t,
35
35
  crossAxis: "alignment",
36
36
  fallbackAxisSideDirection: "end"
37
- }), a = F({
37
+ }), a = ee({
38
38
  padding: t,
39
- limiter: N()
39
+ limiter: P()
40
40
  }), o = [
41
- P(S(e.offset)),
41
+ F(C(e.offset)),
42
42
  ...r ? [i, a] : [a, i],
43
- ee({
43
+ te({
44
44
  padding: t,
45
45
  apply({ rects: e, elements: t, availableWidth: r, availableHeight: i, placement: a }) {
46
- let { style: o } = t.floating, s = B(e.reference.width);
46
+ let { style: o } = t.floating, s = V(e.reference.width);
47
47
  o.setProperty("--sbf-reference-width", s);
48
48
  let c = getComputedStyle(t.floating).direction === "rtl";
49
- o.setProperty("--sbf-transform-origin", V(a, c)), o.setProperty("--sbf-available-width", B(r)), o.setProperty("--sbf-available-height", B(i)), o.width = n === !0 ? s : "", o.minWidth = n === "min" ? s : "";
49
+ o.setProperty("--sbf-transform-origin", re(a, c)), o.setProperty("--sbf-available-width", V(r)), o.setProperty("--sbf-available-height", V(i)), o.width = n === !0 ? s : "", o.minWidth = n === "min" ? s : "";
50
50
  }
51
51
  })
52
52
  ];
53
- return S(e.hideWhenDetached) && o.push(M({ padding: t })), o;
54
- }), { floatingStyles: a, middlewareData: o, placement: s, isPositioned: c, update: l } = te(t, r, {
55
- open: () => S(e.isOpen),
56
- placement: () => S(e.placement),
57
- strategy: () => S(e.strategy),
53
+ return C(e.hideWhenDetached) && o.push(N({ padding: t })), o;
54
+ }), { floatingStyles: a, middlewareData: o, placement: s, isPositioned: c, update: l } = I(t, r, {
55
+ open: () => C(e.isOpen),
56
+ placement: () => C(e.placement),
57
+ strategy: () => C(e.strategy),
58
58
  middleware: i,
59
- whileElementsMounted: A
59
+ whileElementsMounted: j
60
60
  }), u = n(() => !!o.value.hide?.referenceHidden);
61
61
  return {
62
62
  reference: t,
@@ -75,7 +75,7 @@ function re(e) {
75
75
  }
76
76
  //#endregion
77
77
  //#region src/utils/dom.ts
78
- var ie = "data-sbf-focus-guard", H = [
78
+ var ae = "data-sbf-focus-guard", H = [
79
79
  "a[href]",
80
80
  "button:not([disabled])",
81
81
  "input:not([disabled]):not([type=\"hidden\"])",
@@ -86,25 +86,25 @@ var ie = "data-sbf-focus-guard", H = [
86
86
  "video[controls]",
87
87
  "[tabindex]",
88
88
  "[contenteditable=\"true\"]"
89
- ].join(","), ae = (e) => e.tabIndex >= 0 && !e.hasAttribute("data-sbf-focus-guard") && !e.closest("[inert]") && (e.checkVisibility?.() ?? !0);
90
- function U(e) {
89
+ ].join(","), oe = (e) => e.tabIndex >= 0 && !e.hasAttribute("data-sbf-focus-guard") && !e.closest("[inert]") && (e.checkVisibility?.() ?? !0);
90
+ function se(e) {
91
91
  let t = e && typeof e == "object" && "$el" in e ? e.$el : e;
92
92
  return t instanceof HTMLElement ? t : null;
93
93
  }
94
- function oe(e) {
94
+ function U(e) {
95
95
  let t = e instanceof Element ? e.closest(H) : null;
96
96
  return !!t && !t.closest("[inert]");
97
97
  }
98
- var se = (e) => e.matches(H) && !e.closest("[inert]");
98
+ var ce = (e) => e.matches(H) && !e.closest("[inert]");
99
99
  function W(e) {
100
- return [...e.querySelectorAll(H)].filter(ae);
100
+ return [...e.querySelectorAll(H)].filter(oe);
101
101
  }
102
102
  function G(e, t) {
103
103
  return W(e.ownerDocument).find((n) => !!(e.compareDocumentPosition(n) & Node.DOCUMENT_POSITION_FOLLOWING) && !e.contains(n) && !t(n));
104
104
  }
105
105
  //#endregion
106
106
  //#region src/components/SbContainer.vue?vue&type=script&setup=true&lang.ts
107
- var K = [], q = /* @__PURE__ */ s({
107
+ var K = [], le = /* @__PURE__ */ s({
108
108
  inheritAttrs: !1,
109
109
  __name: "SbContainer",
110
110
  props: /*@__PURE__*/ l({
@@ -154,12 +154,12 @@ var K = [], q = /* @__PURE__ */ s({
154
154
  }),
155
155
  emits: /*@__PURE__*/ l(["open", "close"], ["update:open"]),
156
156
  setup(e, { expose: t, emit: r }) {
157
- let i = e, a = E(e, "open"), o = r, s = i.defaultOpen, l = n(() => !i.disabled && (a.value ?? s)), u;
157
+ let i = e, a = D(e, "open"), o = r, s = i.defaultOpen, l = n(() => !i.disabled && (a.value ?? s)), u;
158
158
  h(() => clearTimeout(u));
159
159
  let d = (e) => {
160
160
  clearTimeout(u), !(e && i.disabled) && e !== l.value && (a.value = e);
161
- }, f = () => d(!0), m = () => d(!1), g = () => d(!l.value), b = (e) => {
162
- if (e.pointerType === "touch" || (O?.hover(e), i.trigger !== "hover")) return;
161
+ }, f = () => d(!0), m = () => d(!1), g = () => d(!l.value), v = (e) => {
162
+ if (e.pointerType === "touch" || (T?.hover(e), i.trigger !== "hover")) return;
163
163
  let t = e.type === "pointerenter" || k.contains(e.relatedTarget);
164
164
  if (clearTimeout(u), t === l.value) return;
165
165
  let { delay: n } = i, r = typeof n == "number" ? {
@@ -167,7 +167,7 @@ var K = [], q = /* @__PURE__ */ s({
167
167
  close: n
168
168
  } : n, a = (t ? r.open : r.close) ?? (t ? 300 : 150);
169
169
  u = setTimeout(() => d(t), a);
170
- }, S = re({
170
+ }, x = ie({
171
171
  isOpen: l,
172
172
  placement: () => i.placement,
173
173
  strategy: () => i.strategy,
@@ -175,17 +175,17 @@ var K = [], q = /* @__PURE__ */ s({
175
175
  padding: () => i.padding,
176
176
  matchWidth: () => i.matchWidth,
177
177
  hideWhenDetached: () => i.hideWhenDetached
178
- }), w = /* @__PURE__ */ new Set(), O = c(R, null), k = {
179
- ...S,
178
+ }), C = /* @__PURE__ */ new Set(), T = c(z, null), k = {
179
+ ...x,
180
180
  isOpen: l,
181
- disabled: x(i, "disabled"),
182
- trigger: x(i, "trigger"),
183
- floatingId: `${T()}-floating`,
184
- referenceId: v(),
185
- floatingRole: v("dialog"),
186
- navigation: v(null),
187
- focusRequest: v(null),
188
- parent: O,
181
+ disabled: S(i, "disabled"),
182
+ trigger: S(i, "trigger"),
183
+ floatingId: `${E()}-floating`,
184
+ referenceId: y(),
185
+ floatingRole: y("dialog"),
186
+ navigation: y(null),
187
+ focusRequest: y(null),
188
+ parent: T,
189
189
  skipFocusReturn: !1,
190
190
  parts: {
191
191
  reference: 0,
@@ -195,18 +195,18 @@ var K = [], q = /* @__PURE__ */ s({
195
195
  open: f,
196
196
  close: m,
197
197
  toggle: g,
198
- hover: b,
199
- contains: (e) => e instanceof Node ? S.reference.value?.contains(e) || S.floating.value?.contains(e) ? !0 : [...w].some((t) => t.contains(e)) : !1,
200
- registerChild: (e) => (w.add(e), () => w.delete(e))
198
+ hover: v,
199
+ contains: (e) => e instanceof Node ? x.reference.value?.contains(e) || x.floating.value?.contains(e) ? !0 : [...C].some((t) => t.contains(e)) : !1,
200
+ registerChild: (e) => (C.add(e), () => C.delete(e))
201
201
  };
202
- O && h(O.registerChild(k)), _(R, k), p(() => k.isUnmounting = !0);
202
+ T && h(T.registerChild(k)), _(z, k), p(() => k.isUnmounting = !0);
203
203
  let A = (e) => {
204
204
  let t = e.composedPath()[0] ?? e.target;
205
205
  return K.findLast((e) => e.contains(t)) ?? K[K.length - 1];
206
206
  }, j = (e) => {
207
207
  e.key === "Escape" && A(e) === k && (!i.closeOnEscape || e.defaultPrevented || e.isComposing || (e.preventDefault(), m()));
208
208
  }, M = (e) => {
209
- k.contains(e) || (k.skipFocusReturn = oe(e), m());
209
+ k.contains(e) || (k.skipFocusReturn = U(e), m());
210
210
  }, N, P = (e) => {
211
211
  if (N?.(), !i.closeOnClickOutside) return;
212
212
  let t = e.composedPath()[0] ?? e.target;
@@ -224,13 +224,13 @@ var K = [], q = /* @__PURE__ */ s({
224
224
  }, F = (e) => {
225
225
  i.closeOnFocusOut && (k.contains(e.composedPath()[0] ?? e.target) || m());
226
226
  };
227
- return D(l, (e, t, n) => {
227
+ return O(l, (e, t, n) => {
228
228
  e && typeof document < "u" && (K.push(k), document.addEventListener("keydown", j), document.addEventListener("pointerdown", P, !0), document.addEventListener("focusin", F), n(() => {
229
229
  document.removeEventListener("keydown", j), document.removeEventListener("pointerdown", P, !0), document.removeEventListener("focusin", F), N?.(), K.splice(K.indexOf(k), 1);
230
230
  }));
231
- }, { immediate: !0 }), D(l, (e) => o(e ? "open" : "close")), D(S.referenceHidden, (e) => {
231
+ }, { immediate: !0 }), O(l, (e) => o(e ? "open" : "close")), O(x.referenceHidden, (e) => {
232
232
  e && i.hideWhenDetached === "close" && m();
233
- }), D(() => i.disabled, (e) => {
233
+ }), O(() => i.disabled, (e) => {
234
234
  e && (a.value ?? s) && (a.value = !1);
235
235
  }), t({
236
236
  get isOpen() {
@@ -239,40 +239,40 @@ var K = [], q = /* @__PURE__ */ s({
239
239
  open: f,
240
240
  close: m,
241
241
  toggle: g,
242
- update: S.update
243
- }), (e, t) => y(e.$slots, "default", {
242
+ update: x.update
243
+ }), (e, t) => b(e.$slots, "default", {
244
244
  isOpen: l.value,
245
245
  open: f,
246
246
  close: m,
247
247
  toggle: g,
248
- placement: C(S).placement.value,
249
- side: C(S).side.value,
250
- align: C(S).align.value,
248
+ placement: w(x).placement.value,
249
+ side: w(x).side.value,
250
+ align: w(x).align.value,
251
251
  floatingId: k.floatingId,
252
252
  referenceId: k.referenceId.value
253
253
  });
254
254
  }
255
- }), J = {
255
+ }), q = {
256
256
  dialog: "dialog",
257
257
  alertdialog: "dialog",
258
258
  menu: "menu",
259
259
  listbox: "listbox",
260
260
  tree: "tree",
261
261
  grid: "grid"
262
- }, Y = {
262
+ }, J = {
263
263
  menu: "[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]",
264
264
  listbox: "[role=\"option\"]"
265
- }, ce = (e) => !!e && Object.hasOwn(Y, e);
266
- function le(e, t) {
267
- return [...e.querySelectorAll(Y[t])].filter((n) => n.closest(`[role="${t}"]`) === e && !n.matches(":disabled") && (n.checkVisibility?.() ?? !0));
265
+ }, ue = (e) => !!e && Object.hasOwn(J, e);
266
+ function de(e, t) {
267
+ return [...e.querySelectorAll(J[t])].filter((n) => n.closest(`[role="${t}"]`) === e && !n.matches(":disabled") && (n.checkVisibility?.() ?? !0));
268
268
  }
269
- var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])"), ue = (e) => getComputedStyle(e).direction === "rtl" ? {
269
+ var Y = (e) => e instanceof Element && !!e.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])"), fe = (e) => getComputedStyle(e).direction === "rtl" ? {
270
270
  open: "ArrowLeft",
271
271
  close: "ArrowRight"
272
272
  } : {
273
273
  open: "ArrowRight",
274
274
  close: "ArrowLeft"
275
- }, de = /* @__PURE__ */ s({
275
+ }, X = /* @__PURE__ */ s({
276
276
  __name: "SbReference",
277
277
  props: {
278
278
  as: { default: "button" },
@@ -280,26 +280,26 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
280
280
  ariaHaspopup: { default: void 0 }
281
281
  },
282
282
  setup(e) {
283
- let t = e, i = w(), a = z("SbReference"), { isOpen: o, disabled: s, floatingId: c, floatingRole: l } = a, u = ne(a, "reference", "SbReference"), d = [
283
+ let t = e, i = T(), a = ne("SbReference"), { isOpen: o, disabled: s, floatingId: c, floatingRole: l } = a, u = B(a, "reference", "SbReference"), d = [
284
284
  "button",
285
285
  "input",
286
286
  "textarea",
287
287
  "select"
288
- ], f = () => typeof t.as != "string" || d.includes(t.as) || t.as === "a" && i.href != null, h = `${T()}-reference`, _ = n(() => t.id ?? h);
289
- O(() => {
288
+ ], f = () => typeof t.as != "string" || d.includes(t.as) || t.as === "a" && i.href != null, h = `${E()}-reference`, _ = n(() => t.id ?? h);
289
+ k(() => {
290
290
  a.referenceId.value = _.value;
291
291
  }), m(() => {
292
292
  let e = a.reference.value;
293
- e ? X(e) && a.navigation.value && L(`<SbReference> is a text field with a role="${a.navigation.value}" panel: the arrow keys move focus into the panel, so typing stops there. The combobox pattern (focus kept in the field with aria-activedescendant) is not supported.`) : L("<SbReference> rendered no element, so the panel cannot be positioned: a component in `as` must render a single root element.");
293
+ e ? Y(e) && a.navigation.value && R(`<SbReference> is a text field with a role="${a.navigation.value}" panel: the arrow keys move focus into the panel, so typing stops there. The combobox pattern (focus kept in the field with aria-activedescendant) is not supported.`) : R("<SbReference> rendered no element, so the panel cannot be positioned: a component in `as` must render a single root element.");
294
294
  }), p(() => {
295
295
  a.referenceId.value === _.value && (a.referenceId.value = void 0), u(), !a.parts.reference && !a.isUnmounting && a.close();
296
296
  });
297
- let v = n(() => t.ariaHaspopup ?? (l.value && Object.hasOwn(J, l.value) ? J[l.value] : void 0)), x = (e) => {
298
- a.reference.value = U(e);
297
+ let v = n(() => t.ariaHaspopup ?? (l.value && Object.hasOwn(q, l.value) ? q[l.value] : void 0)), y = (e) => {
298
+ a.reference.value = se(e);
299
299
  }, S = () => {
300
- a.trigger.value === "click" && (X(a.reference.value) ? a.open() : a.toggle());
301
- }, E = n(() => l.value === "tooltip"), D = () => a.trigger.value === "hover" || a.trigger.value === "focus", A = () => a.parent?.navigation.value === "menu", j = (e) => {
302
- D() && !A() && !a.contains(e.relatedTarget) && a.open();
300
+ a.trigger.value === "click" && (Y(a.reference.value) ? a.open() : a.toggle());
301
+ }, C = n(() => l.value === "tooltip"), D = () => a.trigger.value === "hover" || a.trigger.value === "focus", O = () => a.parent?.navigation.value === "menu", j = (e) => {
302
+ D() && !O() && !a.contains(e.relatedTarget) && a.open();
303
303
  }, M = (e) => {
304
304
  D() && !a.contains(e.relatedTarget) && a.close();
305
305
  }, N = (e) => {
@@ -307,41 +307,41 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
307
307
  n && (e.preventDefault(), n.focus());
308
308
  }, P = (e) => {
309
309
  if (a.navigation.value && !s.value && a.trigger.value !== "manual") {
310
- if (A()) {
310
+ if (O()) {
311
311
  let t = e.currentTarget;
312
- return e.key === ue(t).open ? "first" : void 0;
312
+ return e.key === fe(t).open ? "first" : void 0;
313
313
  }
314
314
  return e.key === "ArrowDown" ? "first" : e.key === "ArrowUp" ? "last" : void 0;
315
315
  }
316
316
  }, F = (e) => {
317
- if (e.key === "Tab" && !e.shiftKey && o.value && !A()) {
317
+ if (e.key === "Tab" && !e.shiftKey && o.value && !O()) {
318
318
  N(e);
319
319
  return;
320
320
  }
321
321
  let t = P(e);
322
322
  t ? (e.preventDefault(), a.focusRequest.value = t, a.open()) : f() || e.key !== "Enter" && e.key !== " " || (e.preventDefault(), S());
323
323
  };
324
- return (e, n) => (g(), r(b(t.as), {
324
+ return (e, n) => (g(), r(x(t.as), {
325
325
  id: _.value,
326
- ref: x,
326
+ ref: y,
327
327
  type: t.as === "button" ? "button" : void 0,
328
328
  role: f() ? void 0 : "button",
329
- tabindex: f() ? void 0 : C(s) ? -1 : 0,
330
- disabled: t.as === "button" && C(s) ? !0 : void 0,
331
- "aria-disabled": t.as !== "button" && C(s) ? "true" : void 0,
329
+ tabindex: f() ? void 0 : w(s) ? -1 : 0,
330
+ disabled: t.as === "button" && w(s) ? !0 : void 0,
331
+ "aria-disabled": t.as !== "button" && w(s) ? "true" : void 0,
332
332
  "aria-haspopup": v.value,
333
- "aria-expanded": E.value ? void 0 : C(o) ? "true" : "false",
334
- "aria-controls": C(o) && !E.value ? C(c) : void 0,
335
- "aria-describedby": C(o) && E.value ? C(c) : void 0,
336
- "data-sbf-state": C(o) ? "open" : "closed",
333
+ "aria-expanded": C.value ? void 0 : w(o) ? "true" : "false",
334
+ "aria-controls": w(o) && !C.value ? w(c) : void 0,
335
+ "aria-describedby": w(o) && C.value ? w(c) : void 0,
336
+ "data-sbf-state": w(o) ? "open" : "closed",
337
337
  onClick: S,
338
338
  onKeydown: F,
339
339
  onFocusin: j,
340
340
  onFocusout: M,
341
- onPointerenter: C(a).hover,
342
- onPointerleave: C(a).hover
341
+ onPointerenter: w(a).hover,
342
+ onPointerleave: w(a).hover
343
343
  }, {
344
- default: k(() => [y(e.$slots, "default")]),
344
+ default: A(() => [b(e.$slots, "default")]),
345
345
  _: 3
346
346
  }, 40, [
347
347
  "id",
@@ -359,7 +359,7 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
359
359
  "onPointerleave"
360
360
  ]));
361
361
  }
362
- }), Z = /* @__PURE__ */ s({
362
+ }), pe = ":disabled, [aria-disabled=\"true\"], [aria-haspopup]:not([aria-haspopup=\"false\"])", me = /* @__PURE__ */ s({
363
363
  inheritAttrs: !1,
364
364
  __name: "SbFloating",
365
365
  props: {
@@ -378,30 +378,34 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
378
378
  keyboardNavigation: {
379
379
  type: Boolean,
380
380
  default: !0
381
+ },
382
+ closeOnSelect: {
383
+ type: Boolean,
384
+ default: !1
381
385
  }
382
386
  },
383
387
  emits: ["after-enter", "after-leave"],
384
388
  setup(s, { emit: c }) {
385
- let l = s, _ = c, x = w(), S = z("SbFloating");
386
- p(ne(S, "floating", "SbFloating"));
387
- let { isOpen: T, floatingId: E, referenceId: A, side: j, align: M } = S;
388
- O(() => {
389
- S.floatingRole.value = l.role, S.navigation.value = l.keyboardNavigation && ce(l.role) ? l.role : null;
389
+ let l = s, _ = c, v = T(), S = ne("SbFloating");
390
+ p(B(S, "floating", "SbFloating"));
391
+ let { isOpen: C, floatingId: E, referenceId: D, side: j, align: M } = S;
392
+ k(() => {
393
+ S.floatingRole.value = l.role, S.navigation.value = l.keyboardNavigation && ue(l.role) ? l.role : null;
390
394
  }), m(() => {
391
- l.role === "tooltip" && S.trigger.value === "click" && L("<SbFloating role=\"tooltip\"> opens on click: a tooltip shows on hover and focus. Set trigger=\"hover\" or \"focus\" on SbContainer, or use another role for a panel opened by click.");
395
+ l.role === "tooltip" && S.trigger.value === "click" && R("<SbFloating role=\"tooltip\"> opens on click: a tooltip shows on hover and focus. Set trigger=\"hover\" or \"focus\" on SbContainer, or use another role for a panel opened by click.");
392
396
  });
393
- let N = v(T.value), P = v(null), F = (e) => {
397
+ let N = y(C.value), P = y(null), F = (e) => {
394
398
  S.floating.value = e instanceof HTMLElement ? e : null;
395
399
  }, ee = (e) => {
396
- P.value = U(e);
400
+ P.value = se(e);
397
401
  }, te = n(() => ({
398
402
  ...S.floatingStyles.value,
399
403
  ...l.zIndex === void 0 ? {} : { zIndex: l.zIndex }
400
404
  })), I = () => ({
401
- "aria-labelledby": l.role && Object.hasOwn(J, l.role) && x["aria-label"] == null ? A.value : void 0,
402
- ...x
403
- }), R = n(() => l.autoFocus ?? (l.role !== "tooltip" && S.trigger.value !== "hover" && S.trigger.value !== "focus" && !X(S.reference.value))), B = n(() => T.value && l.teleport !== !1 && l.role !== "tooltip"), V = {
404
- [ie]: "",
405
+ "aria-labelledby": l.role && Object.hasOwn(q, l.role) && v["aria-label"] == null ? D.value : void 0,
406
+ ...v
407
+ }), L = n(() => l.autoFocus ?? (l.role !== "tooltip" && S.trigger.value !== "hover" && S.trigger.value !== "focus" && !Y(S.reference.value))), z = n(() => C.value && l.teleport !== !1 && l.role !== "tooltip"), V = {
408
+ [ae]: "",
405
409
  tabindex: 0,
406
410
  style: {
407
411
  position: "fixed",
@@ -412,73 +416,87 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
412
416
  }, re = n(() => {
413
417
  let { transition: e } = l;
414
418
  return e ? typeof e == "string" ? { name: e } : e : { css: !1 };
415
- }), H = (e) => e.setAttribute("data-sbf-state", "closed"), ae = () => _("after-enter"), oe = () => {
416
- T.value || (N.value = !1), _("after-leave");
417
- }, K = (e) => e instanceof Node && !!P.value?.contains(e), q = (e) => {
419
+ }), ie = (e) => e.setAttribute("data-sbf-state", "closed"), H = () => _("after-enter"), oe = () => {
420
+ C.value || (N.value = !1), _("after-leave");
421
+ }, U = (e) => e instanceof Node && !!P.value?.contains(e), K = (e) => {
418
422
  let t = P.value;
419
423
  if (!t) return;
420
424
  let n = W(t);
421
425
  ((e === "first" ? n[0] : n[n.length - 1]) ?? t).focus({ preventScroll: !0 });
422
- }, Y = (e) => {
423
- K(e.relatedTarget) ? S.reference.value?.focus() : q("first");
424
- }, de = (e) => {
425
- if (!K(e.relatedTarget)) {
426
- q("last");
426
+ }, le = (e) => {
427
+ U(e.relatedTarget) ? S.reference.value?.focus() : K("first");
428
+ }, J = (e) => {
429
+ if (!U(e.relatedTarget)) {
430
+ K("last");
427
431
  return;
428
432
  }
429
433
  let t = S.reference.value;
430
434
  ((t && G(t, (e) => S.contains(e))) ?? t)?.focus();
431
- }, Z = () => {
435
+ }, X = () => {
432
436
  let e = S.navigation.value;
433
- return P.value && e ? le(P.value, e) : [];
434
- }, fe = {
437
+ return P.value && e ? de(P.value, e) : [];
438
+ }, me = {
439
+ menu: "[role=\"menuitem\"]",
440
+ listbox: "[role=\"option\"]"
441
+ }, he = {
435
442
  menu: "role=\"menuitem\", \"menuitemcheckbox\" or \"menuitemradio\"",
436
443
  listbox: "role=\"option\""
437
- }, pe = !1, me = (e) => {
444
+ }, ge = !1, _e = (e) => {
438
445
  let t = S.navigation.value;
439
- if (pe || !t) return;
440
- let n = e.length ? e.every(se) ? null : "has items that cannot take focus, so the arrow keys cannot move to them: make each one a button, or give it tabindex=\"-1\"." : `has no items the arrow keys can move to: items need ${fe[t]} and must not be disabled.`;
441
- n && (pe = !0, L(`<SbFloating role="${t}"> ${n}`));
442
- }, he = (e) => {
446
+ if (ge || !t) return;
447
+ let n = e.length ? e.every(ce) ? null : "has items that cannot take focus, so the arrow keys cannot move to them: make each one a button, or give it tabindex=\"-1\"." : `has no items the arrow keys can move to: items need ${he[t]} and must not be disabled.`;
448
+ n && (ge = !0, R(`<SbFloating role="${t}"> ${n}`));
449
+ }, ve = (e) => {
443
450
  let t = P.value;
444
451
  if (!t) return;
445
- let n = Z();
446
- e && me(n), (n.find((e) => e.getAttribute("aria-selected") === "true") ?? (e === "last" ? n[n.length - 1] : e ? n[0] : void 0) ?? t.querySelector("[autofocus]") ?? n[0] ?? W(t)[0] ?? t).focus({ preventScroll: !0 });
447
- }, ge = () => {
452
+ let n = X();
453
+ e && _e(n), (n.find((e) => e.getAttribute("aria-selected") === "true") ?? (e === "last" ? n[n.length - 1] : e ? n[0] : void 0) ?? t.querySelector("[autofocus]") ?? n[0] ?? W(t)[0] ?? t).focus({ preventScroll: !0 });
454
+ }, Z = () => {
448
455
  let e = S.focusRequest.value;
449
456
  return S.focusRequest.value = null, e;
450
457
  }, Q = "", $;
451
458
  h(() => clearTimeout($));
452
- let _e = (e, t, n) => {
459
+ let ye = (e, t, n) => {
453
460
  Q += n.toLowerCase(), clearTimeout($), $ = setTimeout(() => Q = "", 500);
454
461
  let r = [...Q].every((e) => e === Q[0]) ? Q[0] : Q, i = Math.max(t, 0), a = [...e.slice(i), ...e.slice(0, i)];
455
462
  return r.length === 1 && t >= 0 && a.push(a.shift()), a.find((e) => e.textContent?.trim().toLowerCase().startsWith(r));
456
- }, ve = (e) => {
457
- let t = S, n = [t];
458
- for (; t.parent?.navigation.value === "menu";) t = t.parent, n.push(t);
459
- let r = t.reference.value, i = e ? r : (r && G(r, (e) => t.contains(e))) ?? r;
460
- for (let e of n) e.skipFocusReturn = !0, e.close();
461
- i?.focus();
462
- }, ye = (e) => {
463
+ }, be = () => {
464
+ let e = [S];
465
+ for (; e[e.length - 1].parent?.navigation.value === "menu";) e.push(e[e.length - 1].parent);
466
+ return e;
467
+ }, xe = (e, t) => {
468
+ for (let t of e) t.skipFocusReturn = !0, t.close();
469
+ t?.focus({ preventScroll: !0 });
470
+ }, Se = (e) => {
471
+ let t = be(), n = t[t.length - 1], r = n.reference.value, i = e ? r : (r && G(r, (e) => n.contains(e))) ?? r;
472
+ xe(t, i);
473
+ }, Ce = (e) => {
474
+ let { role: t } = l, n = P.value;
475
+ if (!l.closeOnSelect || e.defaultPrevented || !n || !ue(t)) return;
476
+ let r = e.target.closest(me[t]);
477
+ if (!r || r.closest(`[role="${t}"]`) !== n || r.matches(pe) || t === "listbox" && n.getAttribute("aria-multiselectable") === "true") return;
478
+ let i = t === "menu" ? be() : [S];
479
+ xe(i, i[i.length - 1].reference.value);
480
+ }, we = (e) => {
463
481
  let t = S.navigation.value;
464
- if (!t || e.defaultPrevented || e.isComposing || X(e.target)) return;
465
- let n = Z(), r = n.findIndex((t) => t.contains(e.target)), i = n.length - 1, a = t === "menu", o;
482
+ if (!t || e.defaultPrevented || e.isComposing || Y(e.target)) return;
483
+ let n = X(), r = n.findIndex((t) => t.contains(e.target)), i = n.length - 1, a = t === "menu", o;
466
484
  if ([
467
485
  "ArrowDown",
468
486
  "ArrowUp",
469
487
  "Home",
470
488
  "End"
471
- ].includes(e.key) && me(n), e.key === "ArrowDown") o = n[r < i ? r + 1 : a ? 0 : i];
489
+ ].includes(e.key) && _e(n), e.key === "ArrowDown") o = n[r < i ? r + 1 : a ? 0 : i];
472
490
  else if (e.key === "ArrowUp") o = n[r > 0 ? r - 1 : a || r < 0 ? i : 0];
473
491
  else if (e.key === "Home") o = n[0];
474
492
  else if (e.key === "End") o = n[i];
475
493
  else if (e.key === "Tab" && t === "menu") {
476
- e.preventDefault(), ve(e.shiftKey);
494
+ e.preventDefault(), Se(e.shiftKey);
477
495
  return;
478
- } else if (t === "menu" && S.parent?.navigation.value === "menu" && P.value && e.key === ue(P.value).close) {
496
+ } else if (t === "menu" && S.parent?.navigation.value === "menu" && P.value && e.key === fe(P.value).close) {
479
497
  e.preventDefault(), S.close();
480
498
  return;
481
- } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key !== " " || Q)) e.preventDefault(), o = _e(n, r, e.key);
499
+ } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key !== " " || Q)) e.preventDefault(), o = ye(n, r, e.key);
482
500
  else if ((e.key === "Enter" || e.key === " ") && e.target === n[r]) {
483
501
  let t = n[r];
484
502
  if (t.matches("button, a[href], input, summary")) return;
@@ -487,7 +505,7 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
487
505
  }
488
506
  o && (e.preventDefault(), o.focus());
489
507
  };
490
- return D(T, (e) => {
508
+ return O(C, (e) => {
491
509
  let t = S.skipFocusReturn;
492
510
  if (S.skipFocusReturn = !1, e) {
493
511
  N.value = !0;
@@ -495,12 +513,12 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
495
513
  }
496
514
  let n = document.activeElement;
497
515
  !t && n && S.floating.value?.contains(n) && S.reference.value?.focus({ preventScroll: !0 });
498
- }), D(() => T.value && S.isPositioned.value, async (e) => {
516
+ }), O(() => C.value && S.isPositioned.value, async (e) => {
499
517
  if (!e) return;
500
- let t = ge();
501
- (t || R.value) && (await d(), he(t));
502
- }), D(S.focusRequest, (e) => {
503
- e && T.value && S.isPositioned.value && he(ge());
518
+ let t = Z();
519
+ (t || L.value) && (await d(), ve(t));
520
+ }), O(S.focusRequest, (e) => {
521
+ e && C.value && S.isPositioned.value && ve(Z());
504
522
  }), (n, s) => (g(), r(e, {
505
523
  to: l.teleport || "body",
506
524
  disabled: l.teleport === !1
@@ -508,27 +526,28 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
508
526
  key: 0,
509
527
  ref: F,
510
528
  style: f(te.value),
511
- onPointerenter: s[0] ||= (...e) => C(S).hover && C(S).hover(...e),
512
- onPointerleave: s[1] ||= (...e) => C(S).hover && C(S).hover(...e)
529
+ onPointerenter: s[0] ||= (...e) => w(S).hover && w(S).hover(...e),
530
+ onPointerleave: s[1] ||= (...e) => w(S).hover && w(S).hover(...e)
513
531
  }, [
514
- B.value ? (g(), a("span", u({ key: 0 }, V, { onFocus: Y }), null, 16)) : i("", !0),
532
+ z.value ? (g(), a("span", u({ key: 0 }, V, { onFocus: le }), null, 16)) : i("", !0),
515
533
  o(t, u(re.value, {
516
534
  appear: "",
517
- onBeforeLeave: H,
518
- onAfterEnter: ae,
535
+ onBeforeLeave: ie,
536
+ onAfterEnter: H,
519
537
  onAfterLeave: oe
520
538
  }), {
521
- default: k(() => [C(T) ? (g(), r(b(l.as), u({ key: 0 }, I(), {
522
- id: C(E),
539
+ default: A(() => [w(C) ? (g(), r(x(l.as), u({ key: 0 }, I(), {
540
+ id: w(E),
523
541
  ref: ee,
524
542
  role: l.role ?? void 0,
525
- tabindex: R.value ? -1 : void 0,
543
+ tabindex: L.value ? -1 : void 0,
526
544
  "data-sbf-state": "open",
527
- "data-sbf-side": C(j),
528
- "data-sbf-align": C(M),
529
- onKeydown: ye
545
+ "data-sbf-side": w(j),
546
+ "data-sbf-align": w(M),
547
+ onKeydown: we,
548
+ onClick: Ce
530
549
  }), {
531
- default: k(() => [y(n.$slots, "default")]),
550
+ default: A(() => [b(n.$slots, "default")]),
532
551
  _: 3
533
552
  }, 16, [
534
553
  "id",
@@ -539,9 +558,22 @@ var X = (e) => e instanceof Element && !!e.closest("input, textarea, select, [co
539
558
  ])) : i("", !0)]),
540
559
  _: 3
541
560
  }, 16),
542
- B.value ? (g(), a("span", u({ key: 1 }, V, { onFocus: de }), null, 16)) : i("", !0)
561
+ z.value ? (g(), a("span", u({ key: 1 }, V, { onFocus: J }), null, 16)) : i("", !0)
543
562
  ], 36)) : i("", !0)], 8, ["to", "disabled"]));
544
563
  }
545
564
  });
546
565
  //#endregion
547
- export { q as SbContainer, Z as SbFloating, de as SbReference };
566
+ //#region src/composables/useSbFloating.ts
567
+ function he() {
568
+ let e = c(z, null);
569
+ if (!e) throw Error("[sb-floating-panel-vue] useSbFloating() must be called in a component inside <SbContainer>.");
570
+ let { isOpen: t, open: n, close: r, toggle: i } = e;
571
+ return {
572
+ isOpen: v(t),
573
+ open: n,
574
+ close: r,
575
+ toggle: i
576
+ };
577
+ }
578
+ //#endregion
579
+ export { le as SbContainer, me as SbFloating, X as SbReference, he as useSbFloating };
@@ -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=(()=>{try{return process.env.NODE_ENV!==`production`}catch{return!1}})();function i(e){r&&console.warn(`[sb-floating-panel-vue] ${e}`)}var a=Symbol(`SbFloatingContext`);function o(e,t,n){return++e.parts[t]>1&&i(`<SbContainer> has more than one <${n}>: they share one open state and position, so only the last one works. Use one SbContainer for each panel.`),()=>e.parts[t]--}function s(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 c=e=>`${Math.max(0,e)}px`,l=(e,t)=>{let[n,r]=e.split(`-`),i=t&&(n===`top`||n===`bottom`),a=r===`start`?i?`100%`:`0%`:r===`end`?i?`0%`:`100%`:`50%`;return n===`top`?`${a} 100%`:n===`bottom`?`${a} 0%`:n===`left`?`100% ${a}`:`0% ${a}`};function u(e){let r=(0,t.ref)(null),i=(0,t.ref)(null),a=(0,t.computed)(()=>{let r=(0,t.toValue)(e.padding),i=(0,t.toValue)(e.matchWidth),a=(0,t.toValue)(e.placement).includes(`-`),o=(0,n.flip)({padding:r,crossAxis:`alignment`,fallbackAxisSideDirection:`end`}),s=(0,n.shift)({padding:r,limiter:(0,n.limitShift)()}),u=[(0,n.offset)((0,t.toValue)(e.offset)),...a?[o,s]:[s,o],(0,n.size)({padding:r,apply({rects:e,elements:t,availableWidth:n,availableHeight:r,placement:a}){let{style:o}=t.floating,s=c(e.reference.width);o.setProperty(`--sbf-reference-width`,s);let u=getComputedStyle(t.floating).direction===`rtl`;o.setProperty(`--sbf-transform-origin`,l(a,u)),o.setProperty(`--sbf-available-width`,c(n)),o.setProperty(`--sbf-available-height`,c(r)),o.width=i===!0?s:``,o.minWidth=i===`min`?s:``}})];return(0,t.toValue)(e.hideWhenDetached)&&u.push((0,n.hide)({padding:r})),u}),{floatingStyles:o,middlewareData:s,placement:u,isPositioned:d,update:f}=(0,n.useFloating)(r,i,{open:()=>(0,t.toValue)(e.isOpen),placement:()=>(0,t.toValue)(e.placement),strategy:()=>(0,t.toValue)(e.strategy),middleware:a,whileElementsMounted:n.autoUpdate}),p=(0,t.computed)(()=>!!s.value.hide?.referenceHidden);return{reference:r,floating:i,floatingStyles:(0,t.computed)(()=>({...o.value,...p.value?{visibility:`hidden`}:{}})),placement:u,side:(0,t.computed)(()=>u.value.split(`-`)[0]),align:(0,t.computed)(()=>u.value.split(`-`)[1]),isPositioned:d,referenceHidden:p,update:f}}var d=`data-sbf-focus-guard`,f=[`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(`,`),p=e=>e.tabIndex>=0&&!e.hasAttribute(`data-sbf-focus-guard`)&&!e.closest(`[inert]`)&&(e.checkVisibility?.()??!0);function m(e){let t=e&&typeof e==`object`&&`$el`in e?e.$el:e;return t instanceof HTMLElement?t:null}function h(e){let t=e instanceof Element?e.closest(f):null;return!!t&&!t.closest(`[inert]`)}var g=e=>e.matches(f)&&!e.closest(`[inert]`);function _(e){return[...e.querySelectorAll(f)].filter(p)}function v(e,t){return _(e.ownerDocument).find(n=>!!(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)&&!e.contains(n)&&!t(n))}var y=[],b=(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,String],default:!1},defaultOpen:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},trigger:{default:`click`},delay:{default:()=>({open:300,close:150})},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`),s=r,c=i.defaultOpen,l=(0,t.computed)(()=>!i.disabled&&(o.value??c)),d;(0,t.onScopeDispose)(()=>clearTimeout(d));let f=e=>{clearTimeout(d),!(e&&i.disabled)&&e!==l.value&&(o.value=e)},p=()=>f(!0),m=()=>f(!1),g=()=>f(!l.value),_=e=>{if(e.pointerType===`touch`||(x?.hover(e),i.trigger!==`hover`))return;let t=e.type===`pointerenter`||S.contains(e.relatedTarget);if(clearTimeout(d),t===l.value)return;let{delay:n}=i,r=typeof n==`number`?{open:n,close:n}:n,a=(t?r.open:r.close)??(t?300:150);d=setTimeout(()=>f(t),a)},v=u({isOpen:l,placement:()=>i.placement,strategy:()=>i.strategy,offset:()=>i.offset,padding:()=>i.padding,matchWidth:()=>i.matchWidth,hideWhenDetached:()=>i.hideWhenDetached}),b=new Set,x=(0,t.inject)(a,null),S={...v,isOpen:l,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`),navigation:(0,t.ref)(null),focusRequest:(0,t.ref)(null),parent:x,skipFocusReturn:!1,parts:{reference:0,floating:0},isUnmounting:!1,open:p,close:m,toggle:g,hover:_,contains:e=>e instanceof Node?v.reference.value?.contains(e)||v.floating.value?.contains(e)?!0:[...b].some(t=>t.contains(e)):!1,registerChild:e=>(b.add(e),()=>b.delete(e))};x&&(0,t.onScopeDispose)(x.registerChild(S)),(0,t.provide)(a,S),(0,t.onBeforeUnmount)(()=>S.isUnmounting=!0);let C=e=>{let t=e.composedPath()[0]??e.target;return y.findLast(e=>e.contains(t))??y[y.length-1]},w=e=>{e.key===`Escape`&&C(e)===S&&(!i.closeOnEscape||e.defaultPrevented||e.isComposing||(e.preventDefault(),m()))},T=e=>{S.contains(e)||(S.skipFocusReturn=h(e),m())},E,D=e=>{if(E?.(),!i.closeOnClickOutside)return;let t=e.composedPath()[0]??e.target;if(e.pointerType!==`touch`){T(t);return}if(S.contains(t))return;let n=e=>{E?.(),T(e.composedPath()[0]??e.target)};document.addEventListener(`click`,n,!0),E=()=>{document.removeEventListener(`click`,n,!0),E=void 0}},O=e=>{i.closeOnFocusOut&&(S.contains(e.composedPath()[0]??e.target)||m())};return(0,t.watch)(l,(e,t,n)=>{e&&typeof document<`u`&&(y.push(S),document.addEventListener(`keydown`,w),document.addEventListener(`pointerdown`,D,!0),document.addEventListener(`focusin`,O),n(()=>{document.removeEventListener(`keydown`,w),document.removeEventListener(`pointerdown`,D,!0),document.removeEventListener(`focusin`,O),E?.(),y.splice(y.indexOf(S),1)}))},{immediate:!0}),(0,t.watch)(l,e=>s(e?`open`:`close`)),(0,t.watch)(v.referenceHidden,e=>{e&&i.hideWhenDetached===`close`&&m()}),(0,t.watch)(()=>i.disabled,e=>{e&&(o.value??c)&&(o.value=!1)}),n({get isOpen(){return l.value},open:p,close:m,toggle:g,update:v.update}),(e,n)=>(0,t.renderSlot)(e.$slots,`default`,{isOpen:l.value,open:p,close:m,toggle:g,placement:(0,t.unref)(v).placement.value,side:(0,t.unref)(v).side.value,align:(0,t.unref)(v).align.value,floatingId:S.floatingId,referenceId:S.referenceId.value})}}),x={dialog:`dialog`,alertdialog:`dialog`,menu:`menu`,listbox:`listbox`,tree:`tree`,grid:`grid`},S={menu:`[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]`,listbox:`[role="option"]`},C=e=>!!e&&Object.hasOwn(S,e);function w(e,t){return[...e.querySelectorAll(S[t])].filter(n=>n.closest(`[role="${t}"]`)===e&&!n.matches(`:disabled`)&&(n.checkVisibility?.()??!0))}var T=e=>e instanceof Element&&!!e.closest(`input, textarea, select, [contenteditable]:not([contenteditable="false"])`),E=e=>getComputedStyle(e).direction===`rtl`?{open:`ArrowLeft`,close:`ArrowRight`}:{open:`ArrowRight`,close:`ArrowLeft`},D=(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)(),a=s(`SbReference`),{isOpen:c,disabled:l,floatingId:u,floatingRole:d}=a,f=o(a,`reference`,`SbReference`),p=[`button`,`input`,`textarea`,`select`],h=()=>typeof n.as!=`string`||p.includes(n.as)||n.as===`a`&&r.href!=null,g=`${(0,t.useId)()}-reference`,v=(0,t.computed)(()=>n.id??g);(0,t.watchEffect)(()=>{a.referenceId.value=v.value}),(0,t.onMounted)(()=>{let e=a.reference.value;e?T(e)&&a.navigation.value&&i(`<SbReference> is a text field with a role="${a.navigation.value}" panel: the arrow keys move focus into the panel, so typing stops there. The combobox pattern (focus kept in the field with aria-activedescendant) is not supported.`):i("<SbReference> rendered no element, so the panel cannot be positioned: a component in `as` must render a single root element.")}),(0,t.onBeforeUnmount)(()=>{a.referenceId.value===v.value&&(a.referenceId.value=void 0),f(),!a.parts.reference&&!a.isUnmounting&&a.close()});let y=(0,t.computed)(()=>n.ariaHaspopup??(d.value&&Object.hasOwn(x,d.value)?x[d.value]:void 0)),b=e=>{a.reference.value=m(e)},S=()=>{a.trigger.value===`click`&&(T(a.reference.value)?a.open():a.toggle())},C=(0,t.computed)(()=>d.value===`tooltip`),w=()=>a.trigger.value===`hover`||a.trigger.value===`focus`,D=()=>a.parent?.navigation.value===`menu`,O=e=>{w()&&!D()&&!a.contains(e.relatedTarget)&&a.open()},k=e=>{w()&&!a.contains(e.relatedTarget)&&a.close()},A=e=>{let t=a.floating.value,n=t&&_(t)[0];n&&(e.preventDefault(),n.focus())},j=e=>{if(a.navigation.value&&!l.value&&a.trigger.value!==`manual`){if(D()){let t=e.currentTarget;return e.key===E(t).open?`first`:void 0}return e.key===`ArrowDown`?`first`:e.key===`ArrowUp`?`last`:void 0}},M=e=>{if(e.key===`Tab`&&!e.shiftKey&&c.value&&!D()){A(e);return}let t=j(e);t?(e.preventDefault(),a.focusRequest.value=t,a.open()):h()||e.key!==`Enter`&&e.key!==` `||(e.preventDefault(),S())};return(e,r)=>((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(n.as),{id:v.value,ref:b,type:n.as===`button`?`button`:void 0,role:h()?void 0:`button`,tabindex:h()?void 0:(0,t.unref)(l)?-1:0,disabled:n.as===`button`&&(0,t.unref)(l)?!0:void 0,"aria-disabled":n.as!==`button`&&(0,t.unref)(l)?`true`:void 0,"aria-haspopup":y.value,"aria-expanded":C.value?void 0:(0,t.unref)(c)?`true`:`false`,"aria-controls":(0,t.unref)(c)&&!C.value?(0,t.unref)(u):void 0,"aria-describedby":(0,t.unref)(c)&&C.value?(0,t.unref)(u):void 0,"data-sbf-state":(0,t.unref)(c)?`open`:`closed`,onClick:S,onKeydown:M,onFocusin:O,onFocusout:k,onPointerenter:(0,t.unref)(a).hover,onPointerleave:(0,t.unref)(a).hover},{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`,`aria-describedby`,`data-sbf-state`,`onPointerenter`,`onPointerleave`]))}}),O=(0,t.defineComponent)({inheritAttrs:!1,__name:`SbFloating`,props:{as:{default:`div`},role:{default:`dialog`},transition:{default:void 0},teleport:{type:[String,Boolean],default:`body`},zIndex:{default:void 0},autoFocus:{type:Boolean,default:void 0},keyboardNavigation:{type:Boolean,default:!0}},emits:[`after-enter`,`after-leave`],setup(e,{emit:n}){let r=e,a=n,c=(0,t.useAttrs)(),l=s(`SbFloating`);(0,t.onBeforeUnmount)(o(l,`floating`,`SbFloating`));let{isOpen:u,floatingId:f,referenceId:p,side:h,align:y}=l;(0,t.watchEffect)(()=>{l.floatingRole.value=r.role,l.navigation.value=r.keyboardNavigation&&C(r.role)?r.role:null}),(0,t.onMounted)(()=>{r.role===`tooltip`&&l.trigger.value===`click`&&i(`<SbFloating role="tooltip"> opens on click: a tooltip shows on hover and focus. Set trigger="hover" or "focus" on SbContainer, or use another role for a panel opened by click.`)});let b=(0,t.ref)(u.value),S=(0,t.ref)(null),D=e=>{l.floating.value=e instanceof HTMLElement?e:null},O=e=>{S.value=m(e)},k=(0,t.computed)(()=>({...l.floatingStyles.value,...r.zIndex===void 0?{}:{zIndex:r.zIndex}})),A=()=>({"aria-labelledby":r.role&&Object.hasOwn(x,r.role)&&c[`aria-label`]==null?p.value:void 0,...c}),j=(0,t.computed)(()=>r.autoFocus??(r.role!==`tooltip`&&l.trigger.value!==`hover`&&l.trigger.value!==`focus`&&!T(l.reference.value))),M=(0,t.computed)(()=>u.value&&r.teleport!==!1&&r.role!==`tooltip`),N={[d]:``,tabindex:0,style:{position:`fixed`,opacity:0,outline:`none`,pointerEvents:`none`}},P=(0,t.computed)(()=>{let{transition:e}=r;return e?typeof e==`string`?{name:e}:e:{css:!1}}),F=e=>e.setAttribute(`data-sbf-state`,`closed`),I=()=>a(`after-enter`),L=()=>{u.value||(b.value=!1),a(`after-leave`)},R=e=>e instanceof Node&&!!S.value?.contains(e),z=e=>{let t=S.value;if(!t)return;let n=_(t);((e===`first`?n[0]:n[n.length-1])??t).focus({preventScroll:!0})},B=e=>{R(e.relatedTarget)?l.reference.value?.focus():z(`first`)},V=e=>{if(!R(e.relatedTarget)){z(`last`);return}let t=l.reference.value;((t&&v(t,e=>l.contains(e)))??t)?.focus()},H=()=>{let e=l.navigation.value;return S.value&&e?w(S.value,e):[]},U={menu:`role="menuitem", "menuitemcheckbox" or "menuitemradio"`,listbox:`role="option"`},W=!1,G=e=>{let t=l.navigation.value;if(W||!t)return;let n=e.length?e.every(g)?null:`has items that cannot take focus, so the arrow keys cannot move to them: make each one a button, or give it tabindex="-1".`:`has no items the arrow keys can move to: items need ${U[t]} and must not be disabled.`;n&&(W=!0,i(`<SbFloating role="${t}"> ${n}`))},K=e=>{let t=S.value;if(!t)return;let n=H();e&&G(n),(n.find(e=>e.getAttribute(`aria-selected`)===`true`)??(e===`last`?n[n.length-1]:e?n[0]:void 0)??t.querySelector(`[autofocus]`)??n[0]??_(t)[0]??t).focus({preventScroll:!0})},q=()=>{let e=l.focusRequest.value;return l.focusRequest.value=null,e},J=``,Y;(0,t.onScopeDispose)(()=>clearTimeout(Y));let X=(e,t,n)=>{J+=n.toLowerCase(),clearTimeout(Y),Y=setTimeout(()=>J=``,500);let r=[...J].every(e=>e===J[0])?J[0]:J,i=Math.max(t,0),a=[...e.slice(i),...e.slice(0,i)];return r.length===1&&t>=0&&a.push(a.shift()),a.find(e=>e.textContent?.trim().toLowerCase().startsWith(r))},Z=e=>{let t=l,n=[t];for(;t.parent?.navigation.value===`menu`;)t=t.parent,n.push(t);let r=t.reference.value,i=e?r:(r&&v(r,e=>t.contains(e)))??r;for(let e of n)e.skipFocusReturn=!0,e.close();i?.focus()},Q=e=>{let t=l.navigation.value;if(!t||e.defaultPrevented||e.isComposing||T(e.target))return;let n=H(),r=n.findIndex(t=>t.contains(e.target)),i=n.length-1,a=t===`menu`,o;if([`ArrowDown`,`ArrowUp`,`Home`,`End`].includes(e.key)&&G(n),e.key===`ArrowDown`)o=n[r<i?r+1:a?0:i];else if(e.key===`ArrowUp`)o=n[r>0?r-1:a||r<0?i:0];else if(e.key===`Home`)o=n[0];else if(e.key===`End`)o=n[i];else if(e.key===`Tab`&&t===`menu`){e.preventDefault(),Z(e.shiftKey);return}else if(t===`menu`&&l.parent?.navigation.value===`menu`&&S.value&&e.key===E(S.value).close){e.preventDefault(),l.close();return}else if(e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(e.key!==` `||J))e.preventDefault(),o=X(n,r,e.key);else if((e.key===`Enter`||e.key===` `)&&e.target===n[r]){let t=n[r];if(t.matches(`button, a[href], input, summary`))return;e.preventDefault(),t.getAttribute(`aria-disabled`)!==`true`&&t.click();return}o&&(e.preventDefault(),o.focus())};return(0,t.watch)(u,e=>{let t=l.skipFocusReturn;if(l.skipFocusReturn=!1,e){b.value=!0;return}let n=document.activeElement;!t&&n&&l.floating.value?.contains(n)&&l.reference.value?.focus({preventScroll:!0})}),(0,t.watch)(()=>u.value&&l.isPositioned.value,async e=>{if(!e)return;let n=q();(n||j.value)&&(await(0,t.nextTick)(),K(n))}),(0,t.watch)(l.focusRequest,e=>{e&&u.value&&l.isPositioned.value&&K(q())}),(e,n)=>((0,t.openBlock)(),(0,t.createBlock)(t.Teleport,{to:r.teleport||`body`,disabled:r.teleport===!1},[b.value?((0,t.openBlock)(),(0,t.createElementBlock)(`div`,{key:0,ref:D,style:(0,t.normalizeStyle)(k.value),onPointerenter:n[0]||=(...e)=>(0,t.unref)(l).hover&&(0,t.unref)(l).hover(...e),onPointerleave:n[1]||=(...e)=>(0,t.unref)(l).hover&&(0,t.unref)(l).hover(...e)},[M.value?((0,t.openBlock)(),(0,t.createElementBlock)(`span`,(0,t.mergeProps)({key:0},N,{onFocus:B}),null,16)):(0,t.createCommentVNode)(``,!0),(0,t.createVNode)(t.Transition,(0,t.mergeProps)(P.value,{appear:``,onBeforeLeave:F,onAfterEnter:I,onAfterLeave:L}),{default:(0,t.withCtx)(()=>[(0,t.unref)(u)?((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(r.as),(0,t.mergeProps)({key:0},A(),{id:(0,t.unref)(f),ref:O,role:r.role??void 0,tabindex:j.value?-1:void 0,"data-sbf-state":`open`,"data-sbf-side":(0,t.unref)(h),"data-sbf-align":(0,t.unref)(y),onKeydown:Q}),{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),M.value?((0,t.openBlock)(),(0,t.createElementBlock)(`span`,(0,t.mergeProps)({key:1},N,{onFocus:V}),null,16)):(0,t.createCommentVNode)(``,!0)],36)):(0,t.createCommentVNode)(``,!0)],8,[`to`,`disabled`]))}});e.SbContainer=b,e.SbFloating=O,e.SbReference=D});
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=(()=>{try{return process.env.NODE_ENV!==`production`}catch{return!1}})();function i(e){r&&console.warn(`[sb-floating-panel-vue] ${e}`)}var a=Symbol(`SbFloatingContext`);function o(e,t,n){return++e.parts[t]>1&&i(`<SbContainer> has more than one <${n}>: they share one open state and position, so only the last one works. Use one SbContainer for each panel.`),()=>e.parts[t]--}function s(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 c=e=>`${Math.max(0,e)}px`,l=(e,t)=>{let[n,r]=e.split(`-`),i=t&&(n===`top`||n===`bottom`),a=r===`start`?i?`100%`:`0%`:r===`end`?i?`0%`:`100%`:`50%`;return n===`top`?`${a} 100%`:n===`bottom`?`${a} 0%`:n===`left`?`100% ${a}`:`0% ${a}`};function u(e){let r=(0,t.ref)(null),i=(0,t.ref)(null),a=(0,t.computed)(()=>{let r=(0,t.toValue)(e.padding),i=(0,t.toValue)(e.matchWidth),a=(0,t.toValue)(e.placement).includes(`-`),o=(0,n.flip)({padding:r,crossAxis:`alignment`,fallbackAxisSideDirection:`end`}),s=(0,n.shift)({padding:r,limiter:(0,n.limitShift)()}),u=[(0,n.offset)((0,t.toValue)(e.offset)),...a?[o,s]:[s,o],(0,n.size)({padding:r,apply({rects:e,elements:t,availableWidth:n,availableHeight:r,placement:a}){let{style:o}=t.floating,s=c(e.reference.width);o.setProperty(`--sbf-reference-width`,s);let u=getComputedStyle(t.floating).direction===`rtl`;o.setProperty(`--sbf-transform-origin`,l(a,u)),o.setProperty(`--sbf-available-width`,c(n)),o.setProperty(`--sbf-available-height`,c(r)),o.width=i===!0?s:``,o.minWidth=i===`min`?s:``}})];return(0,t.toValue)(e.hideWhenDetached)&&u.push((0,n.hide)({padding:r})),u}),{floatingStyles:o,middlewareData:s,placement:u,isPositioned:d,update:f}=(0,n.useFloating)(r,i,{open:()=>(0,t.toValue)(e.isOpen),placement:()=>(0,t.toValue)(e.placement),strategy:()=>(0,t.toValue)(e.strategy),middleware:a,whileElementsMounted:n.autoUpdate}),p=(0,t.computed)(()=>!!s.value.hide?.referenceHidden);return{reference:r,floating:i,floatingStyles:(0,t.computed)(()=>({...o.value,...p.value?{visibility:`hidden`}:{}})),placement:u,side:(0,t.computed)(()=>u.value.split(`-`)[0]),align:(0,t.computed)(()=>u.value.split(`-`)[1]),isPositioned:d,referenceHidden:p,update:f}}var d=`data-sbf-focus-guard`,f=[`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(`,`),p=e=>e.tabIndex>=0&&!e.hasAttribute(`data-sbf-focus-guard`)&&!e.closest(`[inert]`)&&(e.checkVisibility?.()??!0);function m(e){let t=e&&typeof e==`object`&&`$el`in e?e.$el:e;return t instanceof HTMLElement?t:null}function h(e){let t=e instanceof Element?e.closest(f):null;return!!t&&!t.closest(`[inert]`)}var g=e=>e.matches(f)&&!e.closest(`[inert]`);function _(e){return[...e.querySelectorAll(f)].filter(p)}function v(e,t){return _(e.ownerDocument).find(n=>!!(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)&&!e.contains(n)&&!t(n))}var y=[],b=(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,String],default:!1},defaultOpen:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},trigger:{default:`click`},delay:{default:()=>({open:300,close:150})},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`),s=r,c=i.defaultOpen,l=(0,t.computed)(()=>!i.disabled&&(o.value??c)),d;(0,t.onScopeDispose)(()=>clearTimeout(d));let f=e=>{clearTimeout(d),!(e&&i.disabled)&&e!==l.value&&(o.value=e)},p=()=>f(!0),m=()=>f(!1),g=()=>f(!l.value),_=e=>{if(e.pointerType===`touch`||(x?.hover(e),i.trigger!==`hover`))return;let t=e.type===`pointerenter`||S.contains(e.relatedTarget);if(clearTimeout(d),t===l.value)return;let{delay:n}=i,r=typeof n==`number`?{open:n,close:n}:n,a=(t?r.open:r.close)??(t?300:150);d=setTimeout(()=>f(t),a)},v=u({isOpen:l,placement:()=>i.placement,strategy:()=>i.strategy,offset:()=>i.offset,padding:()=>i.padding,matchWidth:()=>i.matchWidth,hideWhenDetached:()=>i.hideWhenDetached}),b=new Set,x=(0,t.inject)(a,null),S={...v,isOpen:l,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`),navigation:(0,t.ref)(null),focusRequest:(0,t.ref)(null),parent:x,skipFocusReturn:!1,parts:{reference:0,floating:0},isUnmounting:!1,open:p,close:m,toggle:g,hover:_,contains:e=>e instanceof Node?v.reference.value?.contains(e)||v.floating.value?.contains(e)?!0:[...b].some(t=>t.contains(e)):!1,registerChild:e=>(b.add(e),()=>b.delete(e))};x&&(0,t.onScopeDispose)(x.registerChild(S)),(0,t.provide)(a,S),(0,t.onBeforeUnmount)(()=>S.isUnmounting=!0);let C=e=>{let t=e.composedPath()[0]??e.target;return y.findLast(e=>e.contains(t))??y[y.length-1]},w=e=>{e.key===`Escape`&&C(e)===S&&(!i.closeOnEscape||e.defaultPrevented||e.isComposing||(e.preventDefault(),m()))},T=e=>{S.contains(e)||(S.skipFocusReturn=h(e),m())},E,D=e=>{if(E?.(),!i.closeOnClickOutside)return;let t=e.composedPath()[0]??e.target;if(e.pointerType!==`touch`){T(t);return}if(S.contains(t))return;let n=e=>{E?.(),T(e.composedPath()[0]??e.target)};document.addEventListener(`click`,n,!0),E=()=>{document.removeEventListener(`click`,n,!0),E=void 0}},O=e=>{i.closeOnFocusOut&&(S.contains(e.composedPath()[0]??e.target)||m())};return(0,t.watch)(l,(e,t,n)=>{e&&typeof document<`u`&&(y.push(S),document.addEventListener(`keydown`,w),document.addEventListener(`pointerdown`,D,!0),document.addEventListener(`focusin`,O),n(()=>{document.removeEventListener(`keydown`,w),document.removeEventListener(`pointerdown`,D,!0),document.removeEventListener(`focusin`,O),E?.(),y.splice(y.indexOf(S),1)}))},{immediate:!0}),(0,t.watch)(l,e=>s(e?`open`:`close`)),(0,t.watch)(v.referenceHidden,e=>{e&&i.hideWhenDetached===`close`&&m()}),(0,t.watch)(()=>i.disabled,e=>{e&&(o.value??c)&&(o.value=!1)}),n({get isOpen(){return l.value},open:p,close:m,toggle:g,update:v.update}),(e,n)=>(0,t.renderSlot)(e.$slots,`default`,{isOpen:l.value,open:p,close:m,toggle:g,placement:(0,t.unref)(v).placement.value,side:(0,t.unref)(v).side.value,align:(0,t.unref)(v).align.value,floatingId:S.floatingId,referenceId:S.referenceId.value})}}),x={dialog:`dialog`,alertdialog:`dialog`,menu:`menu`,listbox:`listbox`,tree:`tree`,grid:`grid`},S={menu:`[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]`,listbox:`[role="option"]`},C=e=>!!e&&Object.hasOwn(S,e);function w(e,t){return[...e.querySelectorAll(S[t])].filter(n=>n.closest(`[role="${t}"]`)===e&&!n.matches(`:disabled`)&&(n.checkVisibility?.()??!0))}var T=e=>e instanceof Element&&!!e.closest(`input, textarea, select, [contenteditable]:not([contenteditable="false"])`),E=e=>getComputedStyle(e).direction===`rtl`?{open:`ArrowLeft`,close:`ArrowRight`}:{open:`ArrowRight`,close:`ArrowLeft`},D=(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)(),a=s(`SbReference`),{isOpen:c,disabled:l,floatingId:u,floatingRole:d}=a,f=o(a,`reference`,`SbReference`),p=[`button`,`input`,`textarea`,`select`],h=()=>typeof n.as!=`string`||p.includes(n.as)||n.as===`a`&&r.href!=null,g=`${(0,t.useId)()}-reference`,v=(0,t.computed)(()=>n.id??g);(0,t.watchEffect)(()=>{a.referenceId.value=v.value}),(0,t.onMounted)(()=>{let e=a.reference.value;e?T(e)&&a.navigation.value&&i(`<SbReference> is a text field with a role="${a.navigation.value}" panel: the arrow keys move focus into the panel, so typing stops there. The combobox pattern (focus kept in the field with aria-activedescendant) is not supported.`):i("<SbReference> rendered no element, so the panel cannot be positioned: a component in `as` must render a single root element.")}),(0,t.onBeforeUnmount)(()=>{a.referenceId.value===v.value&&(a.referenceId.value=void 0),f(),!a.parts.reference&&!a.isUnmounting&&a.close()});let y=(0,t.computed)(()=>n.ariaHaspopup??(d.value&&Object.hasOwn(x,d.value)?x[d.value]:void 0)),b=e=>{a.reference.value=m(e)},S=()=>{a.trigger.value===`click`&&(T(a.reference.value)?a.open():a.toggle())},C=(0,t.computed)(()=>d.value===`tooltip`),w=()=>a.trigger.value===`hover`||a.trigger.value===`focus`,D=()=>a.parent?.navigation.value===`menu`,O=e=>{w()&&!D()&&!a.contains(e.relatedTarget)&&a.open()},k=e=>{w()&&!a.contains(e.relatedTarget)&&a.close()},A=e=>{let t=a.floating.value,n=t&&_(t)[0];n&&(e.preventDefault(),n.focus())},j=e=>{if(a.navigation.value&&!l.value&&a.trigger.value!==`manual`){if(D()){let t=e.currentTarget;return e.key===E(t).open?`first`:void 0}return e.key===`ArrowDown`?`first`:e.key===`ArrowUp`?`last`:void 0}},M=e=>{if(e.key===`Tab`&&!e.shiftKey&&c.value&&!D()){A(e);return}let t=j(e);t?(e.preventDefault(),a.focusRequest.value=t,a.open()):h()||e.key!==`Enter`&&e.key!==` `||(e.preventDefault(),S())};return(e,r)=>((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(n.as),{id:v.value,ref:b,type:n.as===`button`?`button`:void 0,role:h()?void 0:`button`,tabindex:h()?void 0:(0,t.unref)(l)?-1:0,disabled:n.as===`button`&&(0,t.unref)(l)?!0:void 0,"aria-disabled":n.as!==`button`&&(0,t.unref)(l)?`true`:void 0,"aria-haspopup":y.value,"aria-expanded":C.value?void 0:(0,t.unref)(c)?`true`:`false`,"aria-controls":(0,t.unref)(c)&&!C.value?(0,t.unref)(u):void 0,"aria-describedby":(0,t.unref)(c)&&C.value?(0,t.unref)(u):void 0,"data-sbf-state":(0,t.unref)(c)?`open`:`closed`,onClick:S,onKeydown:M,onFocusin:O,onFocusout:k,onPointerenter:(0,t.unref)(a).hover,onPointerleave:(0,t.unref)(a).hover},{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`,`aria-describedby`,`data-sbf-state`,`onPointerenter`,`onPointerleave`]))}}),O=`:disabled, [aria-disabled="true"], [aria-haspopup]:not([aria-haspopup="false"])`,k=(0,t.defineComponent)({inheritAttrs:!1,__name:`SbFloating`,props:{as:{default:`div`},role:{default:`dialog`},transition:{default:void 0},teleport:{type:[String,Boolean],default:`body`},zIndex:{default:void 0},autoFocus:{type:Boolean,default:void 0},keyboardNavigation:{type:Boolean,default:!0},closeOnSelect:{type:Boolean,default:!1}},emits:[`after-enter`,`after-leave`],setup(e,{emit:n}){let r=e,a=n,c=(0,t.useAttrs)(),l=s(`SbFloating`);(0,t.onBeforeUnmount)(o(l,`floating`,`SbFloating`));let{isOpen:u,floatingId:f,referenceId:p,side:h,align:y}=l;(0,t.watchEffect)(()=>{l.floatingRole.value=r.role,l.navigation.value=r.keyboardNavigation&&C(r.role)?r.role:null}),(0,t.onMounted)(()=>{r.role===`tooltip`&&l.trigger.value===`click`&&i(`<SbFloating role="tooltip"> opens on click: a tooltip shows on hover and focus. Set trigger="hover" or "focus" on SbContainer, or use another role for a panel opened by click.`)});let b=(0,t.ref)(u.value),S=(0,t.ref)(null),D=e=>{l.floating.value=e instanceof HTMLElement?e:null},k=e=>{S.value=m(e)},A=(0,t.computed)(()=>({...l.floatingStyles.value,...r.zIndex===void 0?{}:{zIndex:r.zIndex}})),j=()=>({"aria-labelledby":r.role&&Object.hasOwn(x,r.role)&&c[`aria-label`]==null?p.value:void 0,...c}),M=(0,t.computed)(()=>r.autoFocus??(r.role!==`tooltip`&&l.trigger.value!==`hover`&&l.trigger.value!==`focus`&&!T(l.reference.value))),N=(0,t.computed)(()=>u.value&&r.teleport!==!1&&r.role!==`tooltip`),P={[d]:``,tabindex:0,style:{position:`fixed`,opacity:0,outline:`none`,pointerEvents:`none`}},F=(0,t.computed)(()=>{let{transition:e}=r;return e?typeof e==`string`?{name:e}:e:{css:!1}}),I=e=>e.setAttribute(`data-sbf-state`,`closed`),L=()=>a(`after-enter`),R=()=>{u.value||(b.value=!1),a(`after-leave`)},z=e=>e instanceof Node&&!!S.value?.contains(e),B=e=>{let t=S.value;if(!t)return;let n=_(t);((e===`first`?n[0]:n[n.length-1])??t).focus({preventScroll:!0})},V=e=>{z(e.relatedTarget)?l.reference.value?.focus():B(`first`)},H=e=>{if(!z(e.relatedTarget)){B(`last`);return}let t=l.reference.value;((t&&v(t,e=>l.contains(e)))??t)?.focus()},U=()=>{let e=l.navigation.value;return S.value&&e?w(S.value,e):[]},W={menu:`[role="menuitem"]`,listbox:`[role="option"]`},G={menu:`role="menuitem", "menuitemcheckbox" or "menuitemradio"`,listbox:`role="option"`},K=!1,q=e=>{let t=l.navigation.value;if(K||!t)return;let n=e.length?e.every(g)?null:`has items that cannot take focus, so the arrow keys cannot move to them: make each one a button, or give it tabindex="-1".`:`has no items the arrow keys can move to: items need ${G[t]} and must not be disabled.`;n&&(K=!0,i(`<SbFloating role="${t}"> ${n}`))},J=e=>{let t=S.value;if(!t)return;let n=U();e&&q(n),(n.find(e=>e.getAttribute(`aria-selected`)===`true`)??(e===`last`?n[n.length-1]:e?n[0]:void 0)??t.querySelector(`[autofocus]`)??n[0]??_(t)[0]??t).focus({preventScroll:!0})},Y=()=>{let e=l.focusRequest.value;return l.focusRequest.value=null,e},X=``,Z;(0,t.onScopeDispose)(()=>clearTimeout(Z));let ee=(e,t,n)=>{X+=n.toLowerCase(),clearTimeout(Z),Z=setTimeout(()=>X=``,500);let r=[...X].every(e=>e===X[0])?X[0]:X,i=Math.max(t,0),a=[...e.slice(i),...e.slice(0,i)];return r.length===1&&t>=0&&a.push(a.shift()),a.find(e=>e.textContent?.trim().toLowerCase().startsWith(r))},Q=()=>{let e=[l];for(;e[e.length-1].parent?.navigation.value===`menu`;)e.push(e[e.length-1].parent);return e},$=(e,t)=>{for(let t of e)t.skipFocusReturn=!0,t.close();t?.focus({preventScroll:!0})},te=e=>{let t=Q(),n=t[t.length-1],r=n.reference.value,i=e?r:(r&&v(r,e=>n.contains(e)))??r;$(t,i)},ne=e=>{let{role:t}=r,n=S.value;if(!r.closeOnSelect||e.defaultPrevented||!n||!C(t))return;let i=e.target.closest(W[t]);if(!i||i.closest(`[role="${t}"]`)!==n||i.matches(O)||t===`listbox`&&n.getAttribute(`aria-multiselectable`)===`true`)return;let a=t===`menu`?Q():[l];$(a,a[a.length-1].reference.value)},re=e=>{let t=l.navigation.value;if(!t||e.defaultPrevented||e.isComposing||T(e.target))return;let n=U(),r=n.findIndex(t=>t.contains(e.target)),i=n.length-1,a=t===`menu`,o;if([`ArrowDown`,`ArrowUp`,`Home`,`End`].includes(e.key)&&q(n),e.key===`ArrowDown`)o=n[r<i?r+1:a?0:i];else if(e.key===`ArrowUp`)o=n[r>0?r-1:a||r<0?i:0];else if(e.key===`Home`)o=n[0];else if(e.key===`End`)o=n[i];else if(e.key===`Tab`&&t===`menu`){e.preventDefault(),te(e.shiftKey);return}else if(t===`menu`&&l.parent?.navigation.value===`menu`&&S.value&&e.key===E(S.value).close){e.preventDefault(),l.close();return}else if(e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&(e.key!==` `||X))e.preventDefault(),o=ee(n,r,e.key);else if((e.key===`Enter`||e.key===` `)&&e.target===n[r]){let t=n[r];if(t.matches(`button, a[href], input, summary`))return;e.preventDefault(),t.getAttribute(`aria-disabled`)!==`true`&&t.click();return}o&&(e.preventDefault(),o.focus())};return(0,t.watch)(u,e=>{let t=l.skipFocusReturn;if(l.skipFocusReturn=!1,e){b.value=!0;return}let n=document.activeElement;!t&&n&&l.floating.value?.contains(n)&&l.reference.value?.focus({preventScroll:!0})}),(0,t.watch)(()=>u.value&&l.isPositioned.value,async e=>{if(!e)return;let n=Y();(n||M.value)&&(await(0,t.nextTick)(),J(n))}),(0,t.watch)(l.focusRequest,e=>{e&&u.value&&l.isPositioned.value&&J(Y())}),(e,n)=>((0,t.openBlock)(),(0,t.createBlock)(t.Teleport,{to:r.teleport||`body`,disabled:r.teleport===!1},[b.value?((0,t.openBlock)(),(0,t.createElementBlock)(`div`,{key:0,ref:D,style:(0,t.normalizeStyle)(A.value),onPointerenter:n[0]||=(...e)=>(0,t.unref)(l).hover&&(0,t.unref)(l).hover(...e),onPointerleave:n[1]||=(...e)=>(0,t.unref)(l).hover&&(0,t.unref)(l).hover(...e)},[N.value?((0,t.openBlock)(),(0,t.createElementBlock)(`span`,(0,t.mergeProps)({key:0},P,{onFocus:V}),null,16)):(0,t.createCommentVNode)(``,!0),(0,t.createVNode)(t.Transition,(0,t.mergeProps)(F.value,{appear:``,onBeforeLeave:I,onAfterEnter:L,onAfterLeave:R}),{default:(0,t.withCtx)(()=>[(0,t.unref)(u)?((0,t.openBlock)(),(0,t.createBlock)((0,t.resolveDynamicComponent)(r.as),(0,t.mergeProps)({key:0},j(),{id:(0,t.unref)(f),ref:k,role:r.role??void 0,tabindex:M.value?-1:void 0,"data-sbf-state":`open`,"data-sbf-side":(0,t.unref)(h),"data-sbf-align":(0,t.unref)(y),onKeydown:re,onClick:ne}),{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),N.value?((0,t.openBlock)(),(0,t.createElementBlock)(`span`,(0,t.mergeProps)({key:1},P,{onFocus:H}),null,16)):(0,t.createCommentVNode)(``,!0)],36)):(0,t.createCommentVNode)(``,!0)],8,[`to`,`disabled`]))}});function A(){let e=(0,t.inject)(a,null);if(!e)throw Error(`[sb-floating-panel-vue] useSbFloating() must be called in a component inside <SbContainer>.`);let{isOpen:n,open:r,close:i,toggle:o}=e;return{isOpen:(0,t.readonly)(n),open:r,close:i,toggle:o}}e.SbContainer=b,e.SbFloating=k,e.SbReference=D,e.useSbFloating=A});
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, TransitionProps } from 'vue';
1
+ import { Component, Ref, TransitionProps } from 'vue';
2
2
  /** Side of the reference the panel is placed on. */
3
3
  export type SbSide = 'top' | 'right' | 'bottom' | 'left';
4
4
  /** Alignment of the panel along the side. */
@@ -246,4 +246,26 @@ export interface SbFloatingProps {
246
246
  * @default true
247
247
  */
248
248
  keyboardNavigation?: boolean;
249
+ /**
250
+ * With `role="menu"` or `role="listbox"`: activating an item (click, Enter or Space) closes
251
+ * the panel and gives focus back to the reference, after the `click` handler of the item.
252
+ * - Menu: `menuitem` items, not `menuitemcheckbox` / `menuitemradio` nor submenu triggers.
253
+ * The menus it opened from close too.
254
+ * - Listbox: `option` items, unless the listbox is `aria-multiselectable="true"`.
255
+ *
256
+ * Disabled items are left out. Call `event.preventDefault()` in the `click` handler of an
257
+ * item to keep the panel open.
258
+ * @default false
259
+ */
260
+ closeOnSelect?: boolean;
261
+ }
262
+ /**
263
+ * Returned by `useSbFloating()`: the open state of the closest `SbContainer` and its actions.
264
+ */
265
+ export interface UseSbFloatingReturn {
266
+ /** Whether the panel is open. */
267
+ isOpen: Readonly<Ref<boolean>>;
268
+ open: () => void;
269
+ close: () => void;
270
+ toggle: () => void;
249
271
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sb-floating-panel-vue",
3
- "version": "5.0.3",
3
+ "version": "5.1.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",