vue-composable-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/deploy.yml +21 -0
  2. package/.github/workflows/docs.yml +62 -0
  3. package/.vscode/extensions.json +7 -0
  4. package/LICENSE +21 -0
  5. package/README.md +3 -0
  6. package/docs/.vitepress/config.mts +40 -0
  7. package/docs/.vitepress/theme/index.js +4 -0
  8. package/docs/.vitepress/theme/style.scss +155 -0
  9. package/docs/components/combobox.md +174 -0
  10. package/docs/components/demos/combobox.vue +142 -0
  11. package/docs/components/demos/combobox_autocomplete.vue +75 -0
  12. package/docs/components/demos/combobox_tags.vue +127 -0
  13. package/docs/components/demos/input.vue +35 -0
  14. package/docs/components/demos/listbox.vue +64 -0
  15. package/docs/components/demos/menu.vue +93 -0
  16. package/docs/components/demos/popover.vue +16 -0
  17. package/docs/components/demos/tabs.vue +59 -0
  18. package/docs/components/demos/tooltip.vue +27 -0
  19. package/docs/components/listbox.md +9 -0
  20. package/docs/components/menu.md +121 -0
  21. package/docs/components/popover.md +82 -0
  22. package/docs/components/tabs.md +9 -0
  23. package/docs/components/tooltip.md +78 -0
  24. package/docs/composables/form-control.md +130 -0
  25. package/docs/composables/list-option.md +47 -0
  26. package/docs/composables/list.md +192 -0
  27. package/docs/composables/popover.md +90 -0
  28. package/docs/index.md +24 -0
  29. package/docs/intro.md +0 -0
  30. package/package.json +35 -0
  31. package/src/components/combobox/combobox.vue +229 -0
  32. package/src/components/combobox/comboboxFormInput.vue +33 -0
  33. package/src/components/combobox/comboboxOption.vue +52 -0
  34. package/src/components/combobox/comboboxOptions.vue +17 -0
  35. package/src/components/injectionKeys.ts +50 -0
  36. package/src/components/listbox/listbox.vue +91 -0
  37. package/src/components/listbox/listboxOption.vue +37 -0
  38. package/src/components/menu/menu.vue +100 -0
  39. package/src/components/menu/menuItem.vue +86 -0
  40. package/src/components/menu/menuItems.vue +51 -0
  41. package/src/components/popover/popover.vue +68 -0
  42. package/src/components/popover/popoverDialog.vue +35 -0
  43. package/src/components/tabs/tab.vue +35 -0
  44. package/src/components/tabs/tabContainer.vue +50 -0
  45. package/src/components/tabs/tabList.vue +52 -0
  46. package/src/components/tabs/tabPanel.vue +36 -0
  47. package/src/components/tooltip.vue +115 -0
  48. package/src/composables/formControl.ts +144 -0
  49. package/src/composables/list.ts +326 -0
  50. package/src/composables/listOption.ts +80 -0
  51. package/src/composables/popover.ts +306 -0
  52. package/src/main.ts +65 -0
  53. package/src/utils/element.ts +19 -0
  54. package/src/utils/id.ts +5 -0
  55. package/src/vite-env.d.ts +1 -0
  56. package/tsconfig.json +27 -0
  57. package/tsconfig.node.json +10 -0
  58. package/vite.config.ts +28 -0
@@ -0,0 +1,326 @@
1
+ import type { ComputedRef, MaybeRef, Ref, ShallowRef, InjectionKey } from "vue";
2
+ import {
3
+ ref,
4
+ shallowRef,
5
+ unref,
6
+ provide,
7
+ computed,
8
+ readonly,
9
+ shallowReadonly,
10
+ triggerRef,
11
+ } from "vue";
12
+ import { useHTMLElement, TemplateRefOrSelector } from "../utils/element";
13
+
14
+ export type ItemValue = unknown;
15
+ export interface Item {
16
+ id: number;
17
+ element: Ref<HTMLElement | undefined>;
18
+ value: ItemValue;
19
+ disabled: MaybeRef<boolean>;
20
+ }
21
+
22
+ export interface ActivationOptions {
23
+ /**
24
+ * Focus the corresponding HTML element
25
+ *
26
+ * @defaultValue `false`
27
+ */
28
+ focus?: boolean;
29
+ }
30
+
31
+ interface NavigationOptions {
32
+ /**
33
+ * Infinitely cycle throught the items
34
+ *
35
+ * @defaultValue `false`
36
+ */
37
+ loop?: boolean;
38
+ }
39
+
40
+ export interface UseListReturn {
41
+ /**
42
+ * Array of items
43
+ */
44
+ items: Readonly<ShallowRef<Item[]>>;
45
+ /**
46
+ * Multiselection flag
47
+ */
48
+ isMultiselectable: Readonly<Ref<boolean>>;
49
+ /**
50
+ * Currently active item
51
+ */
52
+ activeItem: ComputedRef<Item | undefined>;
53
+ /**
54
+ * Select the currently active item
55
+ */
56
+ selectActiveItem: () => void;
57
+ /**
58
+ * Activate the currently selected item
59
+ */
60
+ activateSelectedItem: (options?: ActivationOptions) => void;
61
+ /**
62
+ * Activate the first item
63
+ */
64
+ activateFirstItem: (options?: ActivationOptions) => void;
65
+ /**
66
+ * Activate the last item
67
+ */
68
+ activateLastItem: (options?: ActivationOptions) => void;
69
+ /**
70
+ * Activate next item
71
+ */
72
+ activateNextItem: (options?: ActivationOptions & NavigationOptions) => void;
73
+ /**
74
+ * Activate previous item
75
+ */
76
+ activatePrevItem: (options?: ActivationOptions & NavigationOptions) => void;
77
+ }
78
+
79
+ interface UseListInjectionKey {
80
+ /**
81
+ * The list value
82
+ */
83
+ value: Readonly<Ref<unknown>>;
84
+ /**
85
+ * Select an item by ID
86
+ */
87
+ selectItem: (itemId: number) => void;
88
+ /**
89
+ * Activate an item by ID
90
+ */
91
+ activateItem: (itemId: number, options?: ActivationOptions) => void;
92
+ /**
93
+ * Check if an item is selected
94
+ */
95
+ isSelected: (itemId: number) => boolean;
96
+ /**
97
+ * Check if an item is active
98
+ */
99
+ isActive: (itemId: number) => boolean;
100
+ /**
101
+ * Add an item to the list
102
+ *
103
+ * @returns New item ID
104
+ */
105
+ addItem: (
106
+ itemEl: TemplateRefOrSelector,
107
+ itemVal: ItemValue,
108
+ disabled?: MaybeRef<boolean>
109
+ ) => number;
110
+ /**
111
+ * Remove an item from the list by ID
112
+ */
113
+ removeItem: (itemId: number) => void;
114
+ }
115
+
116
+ export const injectList: InjectionKey<UseListInjectionKey> = Symbol();
117
+
118
+ function isValuesEqual(v1: ItemValue, v2: ItemValue) {
119
+ const [_v1, _v2] = [unref(v1), unref(v2)];
120
+ if (typeof _v1 === "object") {
121
+ if (typeof _v2 === "object") {
122
+ return JSON.stringify(_v1) === JSON.stringify(_v2);
123
+ }
124
+ return false;
125
+ }
126
+ return _v1 === _v2;
127
+ }
128
+
129
+ /**
130
+ * Creates a reactive list of HTML elements with assigned values.
131
+ *
132
+ * @param value A ref to store the list value. Set to an array to enable multiselection.
133
+ */
134
+ export function useList(value: Ref<unknown>): UseListReturn {
135
+ let lastId = 0;
136
+
137
+ const items: ShallowRef<Item[]> = shallowRef([]);
138
+ const activeItemId: Ref<number | undefined> = ref();
139
+
140
+ const isMultiselectable = computed(() => Array.isArray(value.value));
141
+
142
+ function getItem(itemId: number) {
143
+ const item = items.value.find((_item) => {
144
+ return _item.id === itemId;
145
+ });
146
+ if (!item) throw new Error(`Item not found: (${itemId})`);
147
+ return item;
148
+ }
149
+
150
+ function focusItem(item: Item) {
151
+ const el = unref(item.element);
152
+ if (!el) return;
153
+ el.focus();
154
+ return el;
155
+ }
156
+
157
+ function activateItem(
158
+ item?: Item,
159
+ { focus = false }: ActivationOptions = {}
160
+ ) {
161
+ activeItemId.value = item?.id ?? undefined;
162
+ if (item !== undefined && focus) focusItem(item);
163
+ }
164
+
165
+ function activateItemById(
166
+ itemId: number,
167
+ { focus = false }: ActivationOptions = {}
168
+ ) {
169
+ const item = getItem(itemId);
170
+ return activateItem(item, { focus });
171
+ }
172
+
173
+ function isActive(itemId: number) {
174
+ return activeItemId.value === itemId;
175
+ }
176
+
177
+ function addItem(
178
+ itemEl: TemplateRefOrSelector,
179
+ itemVal: ItemValue,
180
+ disabled: MaybeRef<boolean> = false
181
+ ) {
182
+ const el = useHTMLElement(itemEl);
183
+ const id = lastId++;
184
+ items.value.push({
185
+ id,
186
+ value: itemVal,
187
+ element: el,
188
+ disabled,
189
+ });
190
+ triggerRef(items);
191
+ return id;
192
+ }
193
+
194
+ function removeItem(itemId: number) {
195
+ const item = getItem(itemId);
196
+ const idx = items.value.indexOf(item);
197
+ items.value.splice(idx, 1);
198
+ triggerRef(items);
199
+ if (isActive(itemId)) {
200
+ activateItem();
201
+ }
202
+ }
203
+
204
+ function selectItem(itemId: number) {
205
+ const item = getItem(itemId);
206
+ if (unref(item.disabled)) return;
207
+ const v = unref(item.value);
208
+ if (v === undefined) return;
209
+ if (unref(isMultiselectable)) {
210
+ const idx = (value.value as ItemValue[]).findIndex((iv) =>
211
+ isValuesEqual(v, iv)
212
+ );
213
+ if (idx > -1) {
214
+ (value.value as ItemValue[]).splice(idx, 1);
215
+ } else {
216
+ value.value = [...(value.value as ItemValue[]), v];
217
+ }
218
+ } else {
219
+ value.value = v;
220
+ }
221
+ activateItem(item);
222
+ }
223
+
224
+ function isSelected(item: Item) {
225
+ return unref(isMultiselectable)
226
+ ? !!(value.value as ItemValue[]).find((iv) =>
227
+ isValuesEqual(item.value, iv)
228
+ )
229
+ : isValuesEqual(value, item.value);
230
+ }
231
+
232
+ function isSelectedById(itemId: number) {
233
+ try {
234
+ const item = getItem(itemId);
235
+ return isSelected(item);
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+
241
+ function activateSelectedItem({ focus = false }: ActivationOptions = {}) {
242
+ for (let item of items.value) {
243
+ if (isSelected(item)) {
244
+ return activateItem(item, { focus });
245
+ }
246
+ }
247
+ }
248
+
249
+ function activateFirstItem({ focus = false }: ActivationOptions = {}) {
250
+ if (!items.value.length) return;
251
+ return activateItem(items.value[0], { focus });
252
+ }
253
+
254
+ function activateLastItem({ focus = false }: ActivationOptions = {}) {
255
+ if (!items.value.length) return;
256
+ return activateItem(items.value[items.value.length - 1], { focus });
257
+ }
258
+
259
+ function activateNextItem({
260
+ focus = false,
261
+ loop = false,
262
+ }: ActivationOptions & NavigationOptions = {}) {
263
+ if (activeItemId.value === undefined) return activateFirstItem({ focus });
264
+ const item = getItem(activeItemId.value);
265
+ const idx = items.value.indexOf(item);
266
+ let nextItem: Item;
267
+ if (idx + 1 < items.value.length) {
268
+ nextItem = items.value[idx + 1];
269
+ } else {
270
+ if (loop) {
271
+ nextItem = items.value[0];
272
+ } else {
273
+ return;
274
+ }
275
+ }
276
+ return activateItem(nextItem, { focus });
277
+ }
278
+
279
+ function activatePrevItem({
280
+ focus = false,
281
+ loop = false,
282
+ }: ActivationOptions & NavigationOptions = {}) {
283
+ if (activeItemId.value === undefined) return activateLastItem({ focus });
284
+ const item = getItem(activeItemId.value);
285
+ const idx = items.value.indexOf(item);
286
+ let prevItem: Item;
287
+ if (idx - 1 > -1) {
288
+ prevItem = items.value[idx - 1];
289
+ } else {
290
+ if (loop) {
291
+ prevItem = items.value[items.value.length - 1];
292
+ } else {
293
+ return;
294
+ }
295
+ }
296
+ return activateItem(prevItem, { focus });
297
+ }
298
+
299
+ provide(injectList, {
300
+ value: readonly(value),
301
+ selectItem,
302
+ activateItem: activateItemById,
303
+ isSelected: isSelectedById,
304
+ isActive,
305
+ addItem,
306
+ removeItem,
307
+ });
308
+
309
+ return {
310
+ items: shallowReadonly(items),
311
+ isMultiselectable: readonly(isMultiselectable),
312
+ activeItem: computed(() =>
313
+ activeItemId.value !== undefined ? getItem(activeItemId.value) : undefined
314
+ ),
315
+ selectActiveItem: () => {
316
+ if (activeItemId.value !== undefined) {
317
+ selectItem(activeItemId.value);
318
+ }
319
+ },
320
+ activateSelectedItem,
321
+ activateFirstItem,
322
+ activateLastItem,
323
+ activateNextItem,
324
+ activatePrevItem,
325
+ };
326
+ }
@@ -0,0 +1,80 @@
1
+ import type { ComputedRef, MaybeRef } from "vue";
2
+ import {
3
+ ref,
4
+ unref,
5
+ inject,
6
+ computed,
7
+ onBeforeUnmount,
8
+ onBeforeMount,
9
+ } from "vue";
10
+
11
+ import type { TemplateRefOrSelector } from "../utils/element";
12
+
13
+ import type { ItemValue, ActivationOptions } from "./list";
14
+ import { injectList } from "./list";
15
+
16
+ interface UseListOptionReturn {
17
+ /**
18
+ * Whether the item is selected
19
+ */
20
+ isSelected: ComputedRef<boolean>;
21
+ /**
22
+ * Whether the item is active
23
+ */
24
+ isActive: ComputedRef<boolean>;
25
+ /**
26
+ * Select the item
27
+ */
28
+ select: () => void;
29
+ /**
30
+ * Activate the item
31
+ */
32
+ activate: (options?: ActivationOptions) => void;
33
+ }
34
+
35
+ /**
36
+ * Registers an HTML element and an associated value in the list created in the parent component
37
+ *
38
+ * @param el A template ref or a CSS selector
39
+ * @param value A value of any type
40
+ * @param disabled Whether the item is disabled (false by default)
41
+ */
42
+ export function useListOption(
43
+ el: TemplateRefOrSelector,
44
+ value: ItemValue,
45
+ disabled?: MaybeRef<boolean>
46
+ ): UseListOptionReturn {
47
+ const list = inject(injectList);
48
+ if (!list) {
49
+ throw new Error("List injectable context is not provided");
50
+ }
51
+
52
+ const itemId = ref(-1);
53
+
54
+ const _isSelected = computed(() => list.isSelected(itemId.value));
55
+ const _isActive = computed(() => list.isActive(itemId.value));
56
+
57
+ function select() {
58
+ list!.selectItem(itemId.value);
59
+ }
60
+
61
+ function activate({ focus = true } = {}) {
62
+ if (unref(disabled)) return;
63
+ list!.activateItem(itemId.value, { focus });
64
+ }
65
+
66
+ onBeforeMount(() => {
67
+ itemId.value = list.addItem(el, value, disabled);
68
+ });
69
+
70
+ onBeforeUnmount(() => {
71
+ list.removeItem(itemId.value);
72
+ });
73
+
74
+ return {
75
+ isSelected: _isSelected,
76
+ isActive: _isActive,
77
+ select,
78
+ activate,
79
+ };
80
+ }
@@ -0,0 +1,306 @@
1
+ import type { Ref } from "vue";
2
+ import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
3
+ import { useHTMLElement, TemplateRefOrSelector } from "../utils/element";
4
+
5
+ interface UsePopoverOptions {
6
+ /**
7
+ * The direction of the popover dialog
8
+ *
9
+ * @defaultValue `"down"`
10
+ */
11
+ direction?: "up" | "down" | "left" | "right";
12
+ /**
13
+ * The alignment of the popover dialog
14
+ *
15
+ * @defaultValue `"left"`
16
+ */
17
+ align?: "top" | "bottom" | "left" | "right" | "center" | "stretch";
18
+ /**
19
+ * The CSS position of the popover dialog
20
+ *
21
+ * @defaultValue `"fixed"`
22
+ */
23
+ position?: "fixed" | "absolute";
24
+ /**
25
+ * Close the popover on a click outside of the dialog
26
+ *
27
+ * @defaultValue `true`
28
+ */
29
+ closeOnOutsideClick?: boolean;
30
+ /**
31
+ * Close the popover on a click inside of the dialog
32
+ *
33
+ * @defaultValue `false`
34
+ */
35
+ closeOnInsideClick?: boolean;
36
+ }
37
+
38
+ interface FocusOptions {
39
+ /**
40
+ * Focus the popover/activator
41
+ *
42
+ * @defaultValue `true`
43
+ */
44
+ focus?: boolean;
45
+ }
46
+
47
+ export interface UsePopoverReturn {
48
+ /**
49
+ * The popover state
50
+ */
51
+ isOpen: Ref<boolean>;
52
+ /**
53
+ * Open the popover
54
+ */
55
+ open: (options?: FocusOptions) => void;
56
+ /**
57
+ * Close the popover
58
+ */
59
+ close: (options?: FocusOptions) => void;
60
+ /**
61
+ * Toggle the popover
62
+ */
63
+ toggle: (options?: FocusOptions) => void;
64
+ }
65
+
66
+ /**
67
+ *
68
+ * @param activatorEl A template ref or a CSS selector
69
+ * @param popupEl A template ref or a CSS selector
70
+ * @param options Popover options
71
+ */
72
+ export function usePopover(
73
+ activatorEl: TemplateRefOrSelector,
74
+ popupEl: TemplateRefOrSelector,
75
+ {
76
+ direction = "down",
77
+ align = "left",
78
+ position = "fixed",
79
+ closeOnOutsideClick = true,
80
+ closeOnInsideClick = false,
81
+ }: UsePopoverOptions = {}
82
+ ): UsePopoverReturn {
83
+ const isOpen = ref(false);
84
+
85
+ function getActivator() {
86
+ const el = useHTMLElement(activatorEl);
87
+ if (!el.value) throw new Error("Activator element is undefined");
88
+ return el.value;
89
+ }
90
+
91
+ function getPopup() {
92
+ const el = useHTMLElement(popupEl);
93
+ if (!el.value) throw new Error("Popover element is undefined");
94
+ return el.value;
95
+ }
96
+
97
+ function open({ focus = true }: FocusOptions = {}) {
98
+ isOpen.value = true;
99
+ if (focus)
100
+ nextTick(() => {
101
+ const popover = getPopup();
102
+ popover.focus();
103
+ });
104
+ }
105
+
106
+ function close({ focus = true }: FocusOptions = {}) {
107
+ isOpen.value = false;
108
+ if (focus) {
109
+ const activator = getActivator();
110
+ activator.focus();
111
+ }
112
+ }
113
+
114
+ function toggle(options?: FocusOptions) {
115
+ if (isOpen.value) {
116
+ close(options);
117
+ } else {
118
+ open(options);
119
+ }
120
+ }
121
+
122
+ function calcPosition() {
123
+ if (!isOpen.value) return;
124
+
125
+ const activator = getActivator();
126
+ const popover = getPopup();
127
+
128
+ popover.style.position = position;
129
+ const {
130
+ top: aTop,
131
+ right: aRight,
132
+ bottom: aBottom,
133
+ left: aLeft,
134
+ width: aWidth,
135
+ height: aHeight,
136
+ } = activator.getBoundingClientRect();
137
+
138
+ const popoverHeight = popover.offsetHeight;
139
+ const popoverWidth = popover.offsetWidth; //> aWidth ? popover.offsetWidth : aWidth;
140
+
141
+ const viewportHeight = document.documentElement.clientHeight;
142
+ const viewportWidth = document.documentElement.clientWidth;
143
+
144
+ // reverse menu direction if it doesnt fit in the viewport
145
+ let _direction = direction;
146
+
147
+ if (direction == "up") {
148
+ if (aTop - popoverHeight < 0) {
149
+ _direction = "down";
150
+ }
151
+ } else if (direction == "down") {
152
+ if (aBottom + popoverHeight > viewportHeight) {
153
+ _direction = "up";
154
+ }
155
+ } else if (direction == "right") {
156
+ if (aRight + popoverWidth > viewportWidth) {
157
+ _direction = "left";
158
+ }
159
+ } else if (direction == "left") {
160
+ if (aLeft - popoverWidth < 0) {
161
+ _direction = "right";
162
+ }
163
+ }
164
+
165
+ let _align = align;
166
+
167
+ if (align == "left") {
168
+ if (aLeft + popoverWidth > viewportWidth) {
169
+ _align = "right";
170
+ }
171
+ } else if (align == "right") {
172
+ if (aRight - popoverWidth < 0) {
173
+ _align = "left";
174
+ }
175
+ } else if (align == "top") {
176
+ if (aTop + popoverHeight > viewportHeight) {
177
+ _align = "bottom";
178
+ }
179
+ } else if (align == "bottom") {
180
+ if (aBottom - popoverHeight < 0) {
181
+ _align = "top";
182
+ }
183
+ }
184
+
185
+ const _position = {
186
+ top: "auto",
187
+ bottom: "auto",
188
+ left: "auto",
189
+ right: "auto",
190
+ width: "auto",
191
+ };
192
+
193
+ const isFixed = position === "fixed";
194
+
195
+ if (_direction == "up") {
196
+ _position.bottom = `${
197
+ aHeight + (isFixed ? viewportHeight - aBottom : 0)
198
+ }px`;
199
+ } else if (_direction == "down") {
200
+ _position.top = `${aHeight + (isFixed ? aTop : 0)}px`;
201
+ } else if (_direction == "right") {
202
+ _position.left = `${aWidth + (isFixed ? aLeft : 0)}px`;
203
+ } else if (_direction == "left") {
204
+ _position.right = `${aWidth + (isFixed ? viewportWidth - aRight : 0)}px`;
205
+ }
206
+
207
+ if (["up", "down"].includes(_direction)) {
208
+ if (_align == "right") {
209
+ _position.right = `${isFixed ? viewportWidth - aRight : 0}px`;
210
+ } else if (_align == "left" || _align == "stretch") {
211
+ _position.left = `${isFixed ? aLeft : 0}px`;
212
+ } else if (_align == "center") {
213
+ _position.left = `${
214
+ (isFixed ? aLeft : 0) - (popoverWidth - aWidth) / 2
215
+ }px`;
216
+ }
217
+ } else {
218
+ if (_align == "bottom") {
219
+ _position.bottom = `${isFixed ? viewportHeight - aBottom : 0}px`;
220
+ } else if (_align == "top") {
221
+ _position.top = `${isFixed ? aTop : 0}px`;
222
+ } else if (_align == "center") {
223
+ _position.top = `${
224
+ (isFixed ? aTop : 0) - (popoverHeight - aHeight) / 2
225
+ }px`;
226
+ }
227
+ }
228
+
229
+ if (["up", "down"].includes(_direction) && _align === "stretch") {
230
+ _position["width"] = aWidth + "px";
231
+ }
232
+
233
+ for (const prop in _position) {
234
+ popover.style[prop as keyof typeof _position] =
235
+ _position[prop as keyof typeof _position];
236
+ }
237
+ }
238
+
239
+ watch(
240
+ isOpen,
241
+ (val) => {
242
+ if (val) {
243
+ nextTick(() => {
244
+ calcPosition();
245
+ });
246
+ }
247
+ },
248
+ { flush: "pre" }
249
+ );
250
+
251
+ function onWindowClick(evt: MouseEvent) {
252
+ const target = evt.target as HTMLElement;
253
+
254
+ // ignore activator clicks
255
+ const activator = getActivator();
256
+ if (activator.contains(target)) return;
257
+
258
+ try {
259
+ const popover = getPopup();
260
+ // inside
261
+ if (popover.contains(target)) {
262
+ if (closeOnInsideClick) {
263
+ close();
264
+ }
265
+ }
266
+ // outside
267
+ else {
268
+ if (closeOnOutsideClick) {
269
+ isOpen.value = false;
270
+ }
271
+ }
272
+ } catch (e) {}
273
+ }
274
+
275
+ const resizeObserver = new ResizeObserver(calcPosition);
276
+ const abortController = new AbortController();
277
+
278
+ onMounted(() => {
279
+ window.addEventListener("resize", calcPosition, {
280
+ passive: true,
281
+ signal: abortController.signal,
282
+ });
283
+ window.addEventListener("scroll", calcPosition, {
284
+ passive: true,
285
+ //capture: true,
286
+ signal: abortController.signal,
287
+ });
288
+ window.addEventListener("click", onWindowClick, {
289
+ passive: true,
290
+ signal: abortController.signal,
291
+ });
292
+ resizeObserver.observe(getActivator());
293
+ });
294
+
295
+ onBeforeUnmount(() => {
296
+ abortController.abort();
297
+ resizeObserver.disconnect();
298
+ });
299
+
300
+ return {
301
+ isOpen,
302
+ open,
303
+ close,
304
+ toggle,
305
+ };
306
+ }