@sit-onyx/headless 1.0.0-beta.17 → 1.0.0-beta.19

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sit-onyx/headless",
3
3
  "description": "Headless composables for Vue",
4
- "version": "1.0.0-beta.17",
4
+ "version": "1.0.0-beta.19",
5
5
  "type": "module",
6
6
  "author": "Schwarz IT KG",
7
7
  "license": "Apache-2.0",
@@ -27,9 +27,9 @@
27
27
  "vue": ">= 3.5.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@vue/compiler-dom": "3.5.13",
31
- "vue": "3.5.13",
32
- "@sit-onyx/shared": "^1.0.0-beta.2"
30
+ "@vue/compiler-dom": "3.5.16",
31
+ "vue": "3.5.16",
32
+ "@sit-onyx/shared": "^1.0.0-beta.3"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "vue-tsc --build --force",
@@ -1,10 +1,10 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from "vue";
2
+ import { computed, ref, useTemplateRef } from "vue";
3
3
  import { createComboBox } from "./createComboBox";
4
4
 
5
5
  const options = ["a", "b", "c", "d"];
6
6
  const isExpanded = ref(false);
7
- const comboboxRef = ref<HTMLElement>();
7
+ const comboboxRef = useTemplateRef("combobox");
8
8
  const activeOption = ref("");
9
9
  const selectedOption = ref("");
10
10
  const selectedIndex = computed<number | undefined>(() => {
@@ -49,7 +49,7 @@ defineExpose({ comboBox });
49
49
  </script>
50
50
 
51
51
  <template>
52
- <div ref="comboboxRef">
52
+ <div ref="combobox">
53
53
  <input
54
54
  v-bind="input"
55
55
  v-model="selectedOption"
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from "vue";
2
+ import { computed, ref, useTemplateRef } from "vue";
3
3
  import { createComboBox } from "./createComboBox";
4
4
 
5
5
  const options = ["a", "b", "c", "d"];
6
6
  const isExpanded = ref(false);
7
7
  const searchTerm = ref("");
8
- const comboboxRef = ref<HTMLElement>();
8
+ const comboboxRef = useTemplateRef("combobox");
9
9
  const activeOption = ref("");
10
10
  const filteredOptions = computed(() => options.filter((v) => v.includes(searchTerm.value)));
11
11
  const selectedIndex = computed<number | undefined>(() => {
@@ -53,7 +53,7 @@ defineExpose({ comboBox });
53
53
  </script>
54
54
 
55
55
  <template>
56
- <div ref="comboboxRef">
56
+ <div ref="combobox">
57
57
  <input v-bind="input" v-model="searchTerm" @keydown.arrow-down="isExpanded = true" />
58
58
 
59
59
  <button v-bind="button" type="button">
@@ -1,6 +1,7 @@
1
1
  import { computed, unref, useId, type MaybeRef, type Ref } from "vue";
2
2
  import { createBuilder } from "../../utils/builder";
3
3
  import { isPrintableCharacter, wasKeyPressed, type PressedKey } from "../../utils/keyboard";
4
+ import type { Nullable } from "../../utils/types";
4
5
  import { useOutsideClick } from "../helpers/useOutsideClick";
5
6
  import { useTypeAhead } from "../helpers/useTypeAhead";
6
7
  import {
@@ -48,7 +49,7 @@ export type CreateComboboxOptions<
48
49
  /**
49
50
  * Provides additional description for the listbox which displays the available options.
50
51
  */
51
- listDescription?: MaybeRef<string | undefined>;
52
+ listDescription?: MaybeRef<Nullable<string>>;
52
53
  /**
53
54
  * Controls the opened/visible state of the associated pop-up. When expanded the activeOption can be controlled via the keyboard.
54
55
  */
@@ -56,11 +57,11 @@ export type CreateComboboxOptions<
56
57
  /**
57
58
  * If expanded, the active option is the currently highlighted option of the controlled listbox.
58
59
  */
59
- activeOption: Ref<TValue | undefined>;
60
+ activeOption: Ref<Nullable<TValue>>;
60
61
  /**
61
62
  * Template ref to the component root (required to close combobox on outside click).
62
63
  */
63
- templateRef: Ref<HTMLElement | undefined>;
64
+ templateRef: Ref<Nullable<HTMLElement>>;
64
65
  /**
65
66
  * Hook when the popover should toggle.
66
67
  *
@@ -3,7 +3,7 @@ import { onBeforeMount, onBeforeUnmount, reactive, watchEffect, type Ref } from
3
3
  type DocumentEventType = keyof DocumentEventMap;
4
4
  type GlobalListener<K extends DocumentEventType = DocumentEventType> = (
5
5
  event: DocumentEventMap[K],
6
- ) => void;
6
+ ) => unknown;
7
7
 
8
8
  export type UseGlobalEventListenerOptions<K extends DocumentEventType> = {
9
9
  type: K;
@@ -13,8 +13,9 @@ describe("useOutsideClick", () => {
13
13
  expect(useOutsideClick).toBeDefined();
14
14
  });
15
15
 
16
- it("should detect outside clicks", () => {
16
+ it("should detect outside clicks", async () => {
17
17
  // ARRANGE
18
+ vi.useFakeTimers();
18
19
  const inside = ref(document.createElement("button"));
19
20
  document.body.appendChild(inside.value);
20
21
  const outside = ref(document.createElement("button"));
@@ -22,12 +23,24 @@ describe("useOutsideClick", () => {
22
23
 
23
24
  const onOutsideClick = vi.fn();
24
25
  useOutsideClick({ inside, onOutsideClick });
26
+
25
27
  // ACT
26
28
  const event = new MouseEvent("click", { bubbles: true });
27
29
  outside.value.dispatchEvent(event);
30
+
28
31
  // ASSERT
29
32
  expect(onOutsideClick).toHaveBeenCalledTimes(1);
30
33
  expect(onOutsideClick).toBeCalledWith(event);
34
+
35
+ // ACT
36
+ outside.value.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Tab" }));
37
+ await vi.runAllTimersAsync();
38
+
39
+ // ASSERT
40
+ expect(
41
+ onOutsideClick,
42
+ "should not trigger on Tab press when checkOnTab option is disabled",
43
+ ).toHaveBeenCalledTimes(1);
31
44
  });
32
45
 
33
46
  it("should detect outside clicks correctly for multiple inside elements", () => {
@@ -80,4 +93,25 @@ describe("useOutsideClick", () => {
80
93
  // ASSERT
81
94
  expect(onOutsideClick).toHaveBeenCalledTimes(1);
82
95
  });
96
+
97
+ it("should detect outside tab via keyboard", async () => {
98
+ // ARRANGE
99
+ vi.useFakeTimers();
100
+ const inside = ref(document.createElement("button"));
101
+ document.body.appendChild(inside.value);
102
+ const outside = ref(document.createElement("button"));
103
+ document.body.appendChild(outside.value);
104
+
105
+ const onOutsideClick = vi.fn();
106
+ useOutsideClick({ inside, onOutsideClick, checkOnTab: true });
107
+
108
+ // ACT
109
+ const event = new KeyboardEvent("keydown", { bubbles: true, key: "Tab" });
110
+ outside.value.dispatchEvent(event);
111
+ await vi.runAllTimersAsync();
112
+
113
+ // ASSERT
114
+ expect(onOutsideClick).toHaveBeenCalledTimes(1);
115
+ expect(onOutsideClick).toBeCalledWith(event);
116
+ });
83
117
  });
@@ -1,16 +1,23 @@
1
1
  import type { Arrayable } from "vitest"; // For an unknown reason removing this import will break the build of "demo-app" and "playground"
2
2
  import { toValue, type MaybeRefOrGetter, type Ref } from "vue";
3
+ import type { Nullable } from "../../utils/types";
3
4
  import { useGlobalEventListener } from "./useGlobalListener";
4
5
 
5
- export type UseOutsideClickOptions = {
6
+ export type UseOutsideClickOptions<TCheckOnTab extends boolean | undefined = undefined> = {
6
7
  /**
7
8
  * HTML element of the component where clicks should be ignored
8
9
  */
9
- inside: MaybeRefOrGetter<Arrayable<HTMLElement | undefined>>;
10
+ inside: MaybeRefOrGetter<Arrayable<Nullable<HTMLElement>>>;
10
11
  /**
11
12
  * Callback when an outside click occurred.
12
13
  */
13
- onOutsideClick: (event: MouseEvent) => void;
14
+ onOutsideClick: (
15
+ event: TCheckOnTab extends true ? MouseEvent | KeyboardEvent : MouseEvent,
16
+ ) => void;
17
+ /**
18
+ * Whether the outside focus should also be checked when pressing the Tab key.
19
+ */
20
+ checkOnTab?: TCheckOnTab;
14
21
  /**
15
22
  * If `true`, event listeners will be removed and no outside clicks will be captured.
16
23
  */
@@ -21,19 +28,42 @@ export type UseOutsideClickOptions = {
21
28
  * Composable for listening to click events that occur outside of a component.
22
29
  * Useful to e.g. close flyouts or tooltips.
23
30
  */
24
- export const useOutsideClick = ({ inside, onOutsideClick, disabled }: UseOutsideClickOptions) => {
31
+ export const useOutsideClick = <TCheckOnTab extends boolean | undefined>({
32
+ inside,
33
+ onOutsideClick,
34
+ disabled,
35
+ checkOnTab,
36
+ }: UseOutsideClickOptions<TCheckOnTab>) => {
37
+ const isOutsideClick = (target: EventTarget | null) => {
38
+ if (!target) return true;
39
+ const raw = toValue(inside);
40
+ const elements = Array.isArray(raw) ? raw : [raw];
41
+ return !elements.some((element) => element?.contains(target as Node | null));
42
+ };
43
+
25
44
  /**
26
45
  * Document click handle that closes then tooltip when clicked outside.
27
46
  * Should only be called when trigger is "click".
28
47
  */
29
- const listener = (event: MouseEvent) => {
30
- const raw = toValue(inside);
31
- const elements = Array.isArray(raw) ? raw : [raw];
32
- const isOutsideClick = !elements.some((element) =>
33
- element?.contains(event.target as HTMLElement),
34
- );
35
- if (isOutsideClick) onOutsideClick(event);
48
+ const clickListener = (event: MouseEvent) => {
49
+ if (isOutsideClick(event.target)) onOutsideClick(event);
36
50
  };
37
51
 
38
- useGlobalEventListener({ type: "click", listener, disabled });
52
+ useGlobalEventListener({ type: "click", listener: clickListener, disabled });
53
+
54
+ if (checkOnTab) {
55
+ const keydownListener = async (event: KeyboardEvent) => {
56
+ if (event.key !== "Tab") return;
57
+
58
+ // using setTimeout here to guarantee that side effects that might change the document.activeElement have run before checking
59
+ // the activeElement
60
+ await new Promise((resolve) => setTimeout(resolve));
61
+
62
+ if (isOutsideClick(document.activeElement)) {
63
+ onOutsideClick(event as Parameters<typeof onOutsideClick>[0]);
64
+ }
65
+ };
66
+
67
+ useGlobalEventListener({ type: "keydown", listener: keydownListener, disabled });
68
+ }
39
69
  };
@@ -1,5 +1,6 @@
1
1
  import { computed, nextTick, ref, unref, useId, watchEffect, type MaybeRef, type Ref } from "vue";
2
2
  import { createBuilder, type VBindAttributes } from "../../utils/builder";
3
+ import type { Nullable } from "../../utils/types";
3
4
  import { useTypeAhead } from "../helpers/useTypeAhead";
4
5
 
5
6
  export type ListboxValue = string | number | boolean;
@@ -12,11 +13,11 @@ export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends
12
13
  /**
13
14
  * Aria description for the listbox.
14
15
  */
15
- description?: MaybeRef<string | undefined>;
16
+ description?: MaybeRef<Nullable<string>>;
16
17
  /**
17
18
  * Value of currently (visually) active option.
18
19
  */
19
- activeOption: Ref<TValue | undefined>;
20
+ activeOption: Ref<Nullable<TValue>>;
20
21
  /**
21
22
  * Wether the listbox is controlled from the outside, e.g. by a combobox.
22
23
  * This disables keyboard events and makes the listbox not focusable.
@@ -29,7 +30,7 @@ export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends
29
30
  /**
30
31
  * Whether the listbox is multiselect.
31
32
  */
32
- multiple?: MaybeRef<TMultiple | undefined>;
33
+ multiple?: MaybeRef<Nullable<TMultiple>>;
33
34
  /**
34
35
  * Hook when an option is selected.
35
36
  */
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, type Ref } from "vue";
2
+ import { ref } from "vue";
3
3
  import { createMenuButton } from "./createMenuButton";
4
4
 
5
5
  const items = Array.from({ length: 10 }, (_, index) => {
@@ -10,7 +10,7 @@ const items = Array.from({ length: 10 }, (_, index) => {
10
10
  const activeItem = ref<string>();
11
11
  const isExpanded = ref(false);
12
12
  const onToggle = () => (isExpanded.value = !isExpanded.value);
13
- const trigger: Readonly<Ref<"click" | "hover">> = ref("hover");
13
+ const trigger = ref<"click" | "hover">("hover");
14
14
 
15
15
  const {
16
16
  elements: { root, button, menu, menuItem, listItem },
@@ -30,13 +30,6 @@ export const menuButtonTesting = async ({
30
30
  menu,
31
31
  menuItems,
32
32
  }: MenuButtonTestingOptions) => {
33
- const menuId = await menu.getAttribute("id");
34
- expect(menuId).toBeDefined();
35
- await expect(
36
- button,
37
- "navigation menu should have set the list ID to the aria-controls",
38
- ).toHaveAttribute("aria-controls", menuId!);
39
-
40
33
  await expect(
41
34
  button,
42
35
  'navigation menu should have an "aria-haspopup" attribute set to true',
@@ -80,18 +73,6 @@ export const menuButtonTesting = async ({
80
73
  await menu.press("ArrowUp");
81
74
  await expect(firstItem, "First item should be focused when pressing arrow up key").toBeFocused();
82
75
 
83
- await menu.press("ArrowRight");
84
- await expect(
85
- secondItem,
86
- "Second item should be focused when pressing arrow right key",
87
- ).toBeFocused();
88
-
89
- await menu.press("ArrowLeft");
90
- await expect(
91
- firstItem,
92
- "First item should be focused when pressing arrow left key",
93
- ).toBeFocused();
94
-
95
76
  await page.keyboard.press("Tab");
96
77
  await expect(button, "Button should be focused when pressing tab key").not.toBeFocused();
97
78
 
@@ -2,11 +2,13 @@ import { computed, useId, watch, type Ref } from "vue";
2
2
  import { createBuilder, createElRef } from "../../utils/builder";
3
3
  import { debounce } from "../../utils/timer";
4
4
  import { useGlobalEventListener } from "../helpers/useGlobalListener";
5
+ import { useOutsideClick } from "../helpers/useOutsideClick";
5
6
 
6
7
  type CreateMenuButtonOptions = {
7
8
  isExpanded: Readonly<Ref<boolean>>;
8
9
  trigger: Readonly<Ref<"hover" | "click">>;
9
10
  onToggle: () => void;
11
+ disabled?: Readonly<Ref<boolean>>;
10
12
  };
11
13
 
12
14
  /**
@@ -15,6 +17,7 @@ type CreateMenuButtonOptions = {
15
17
  export const createMenuButton = createBuilder((options: CreateMenuButtonOptions) => {
16
18
  const rootId = useId();
17
19
  const menuId = useId();
20
+ const rootRef = createElRef<HTMLElement>();
18
21
  const menuRef = createElRef<HTMLElement>();
19
22
  const buttonId = useId();
20
23
 
@@ -31,6 +34,7 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
31
34
  watch(options.isExpanded, () => updateDebouncedExpanded.abort()); // manually changing `isExpanded` should abort debounced action
32
35
 
33
36
  const setExpanded = (expanded: boolean, debounced = false) => {
37
+ if (options.disabled?.value) return;
34
38
  if (expanded === options.isExpanded.value) {
35
39
  updateDebouncedExpanded.abort();
36
40
  return;
@@ -50,7 +54,9 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
50
54
  const currentMenu = currentMenuItem?.closest('[role="menu"]') || menuRef.value;
51
55
  if (!currentMenu) return;
52
56
 
53
- const menuItems = [...currentMenu.querySelectorAll<HTMLElement>('[role="menuitem"]')];
57
+ const menuItems = Array.from(currentMenu.querySelectorAll<HTMLElement>('[role="menuitem"]'))
58
+ // filter out nested children
59
+ .filter((item) => item.closest('[role="menu"]') === currentMenu);
54
60
  let nextIndex = 0;
55
61
 
56
62
  if (currentMenuItem) {
@@ -78,12 +84,10 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
78
84
  const handleKeydown = (event: KeyboardEvent) => {
79
85
  switch (event.key) {
80
86
  case "ArrowDown":
81
- case "ArrowRight":
82
87
  event.preventDefault();
83
88
  focusRelativeItem("next");
84
89
  break;
85
90
  case "ArrowUp":
86
- case "ArrowLeft":
87
91
  event.preventDefault();
88
92
  focusRelativeItem("prev");
89
93
  break;
@@ -96,6 +100,8 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
96
100
  focusRelativeItem("last");
97
101
  break;
98
102
  case " ":
103
+ case "Enter":
104
+ if (event.target instanceof HTMLInputElement) break;
99
105
  event.preventDefault();
100
106
  (event.target as HTMLElement).click();
101
107
  break;
@@ -106,32 +112,29 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
106
112
  }
107
113
  };
108
114
 
109
- const triggerEvents = () => {
110
- if (options.trigger.value === "hover") {
111
- return {
112
- onMouseenter: () => setExpanded(true),
113
- onMouseleave: () => setExpanded(false, true),
114
- };
115
- }
116
- };
115
+ const triggerEvents = computed(() => {
116
+ if (options.trigger.value !== "hover") return;
117
+ return {
118
+ onMouseenter: () => setExpanded(true),
119
+ onMouseleave: () => setExpanded(false, true),
120
+ };
121
+ });
122
+
123
+ useOutsideClick({
124
+ inside: rootRef,
125
+ onOutsideClick: () => setExpanded(false),
126
+ disabled: computed(() => !options.isExpanded.value),
127
+ checkOnTab: true,
128
+ });
117
129
 
118
130
  return {
119
131
  elements: {
120
- root: {
132
+ root: computed(() => ({
121
133
  id: rootId,
122
134
  onKeydown: handleKeydown,
123
- ...triggerEvents(),
124
- onFocusout: (event) => {
125
- // if focus receiving element is not part of the menu button, then close
126
- if (
127
- rootId &&
128
- document.getElementById(rootId)?.contains(event.relatedTarget as HTMLElement)
129
- ) {
130
- return;
131
- }
132
- setExpanded(false);
133
- },
134
- },
135
+ ref: rootRef,
136
+ ...triggerEvents.value,
137
+ })),
135
138
  button: computed(
136
139
  () =>
137
140
  ({
@@ -142,6 +145,7 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
142
145
  onClick: () =>
143
146
  options.trigger.value == "click" ? setExpanded(!options.isExpanded.value) : undefined,
144
147
  id: buttonId,
148
+ disabled: options.disabled?.value,
145
149
  }) as const,
146
150
  ),
147
151
  menu: {
@@ -156,7 +160,25 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
156
160
  };
157
161
  });
158
162
 
159
- export const createMenuItems = createBuilder(() => {
163
+ type CreateMenuItemOptions = {
164
+ /**
165
+ * Called when the menu item should be opened (if it has nested children).
166
+ */
167
+ onOpen?: () => void;
168
+ };
169
+
170
+ export const createMenuItems = createBuilder((options?: CreateMenuItemOptions) => {
171
+ const onKeydown = (event: KeyboardEvent) => {
172
+ switch (event.key) {
173
+ case "ArrowRight":
174
+ case " ":
175
+ case "Enter":
176
+ event.preventDefault();
177
+ options?.onOpen?.();
178
+ break;
179
+ }
180
+ };
181
+
160
182
  return {
161
183
  elements: {
162
184
  listItem: {
@@ -166,6 +188,7 @@ export const createMenuItems = createBuilder(() => {
166
188
  "aria-current": data.active ? "page" : undefined,
167
189
  "aria-disabled": data.disabled,
168
190
  role: "menuitem",
191
+ onKeydown,
169
192
  }),
170
193
  },
171
194
  };
@@ -19,7 +19,7 @@ export const createNavigationMenu = createBuilder(({ navigationName }: CreateNav
19
19
  const getMenuButtons = () => {
20
20
  const nav = navId ? document.getElementById(navId) : undefined;
21
21
  if (!nav) return [];
22
- return [...nav.querySelectorAll<HTMLElement>("button[aria-expanded][aria-controls]")];
22
+ return Array.from(nav.querySelectorAll<HTMLElement>("button[aria-expanded][aria-controls]"));
23
23
  };
24
24
 
25
25
  const focusRelative = (trigger: HTMLElement, next: "next" | "previous") => {
@@ -2,7 +2,6 @@ import { test } from "@playwright/experimental-ct-vue";
2
2
  import TestTabs from "./TestTabs.vue";
3
3
  import { tabsTesting } from "./createTabs.testing";
4
4
 
5
- // eslint-disable-next-line playwright/expect-expect
6
5
  test("tabs", async ({ mount, page }) => {
7
6
  const component = await mount(<TestTabs />);
8
7
 
@@ -1,7 +1,6 @@
1
- import { computed, toRef, toValue, type MaybeRefOrGetter, type Ref } from "vue";
2
- import { createBuilder, createElRef } from "../../utils/builder";
1
+ import { computed, toRef, toValue, useId, type MaybeRefOrGetter, type Ref } from "vue";
2
+ import { createBuilder } from "../../utils/builder";
3
3
  import { useDismissible } from "../helpers/useDismissible";
4
- import { useOutsideClick } from "../helpers/useOutsideClick";
5
4
 
6
5
  export type CreateToggletipOptions = {
7
6
  toggleLabel: MaybeRefOrGetter<string>;
@@ -17,16 +16,9 @@ export type CreateToggletipOptions = {
17
16
  */
18
17
  export const createToggletip = createBuilder(
19
18
  ({ toggleLabel, isVisible }: CreateToggletipOptions) => {
20
- const triggerRef = createElRef<HTMLButtonElement>();
21
- const tooltipRef = createElRef<HTMLElement>();
22
- const _isVisible = toRef(isVisible ?? false);
19
+ const triggerId = useId();
23
20
 
24
- // close tooltip on outside click
25
- useOutsideClick({
26
- inside: computed(() => [triggerRef.value, tooltipRef.value]),
27
- onOutsideClick: () => (_isVisible.value = false),
28
- disabled: computed(() => !_isVisible.value),
29
- });
21
+ const _isVisible = toRef(isVisible ?? false);
30
22
 
31
23
  useDismissible({ isExpanded: _isVisible });
32
24
 
@@ -39,7 +31,7 @@ export const createToggletip = createBuilder(
39
31
  * Preferably a `button` element.
40
32
  */
41
33
  trigger: computed(() => ({
42
- ref: triggerRef,
34
+ id: triggerId,
43
35
  onClick: toggle,
44
36
  "aria-label": toValue(toggleLabel),
45
37
  })),
@@ -48,7 +40,12 @@ export const createToggletip = createBuilder(
48
40
  * Only simple, textual content is allowed.
49
41
  */
50
42
  tooltip: {
51
- ref: tooltipRef,
43
+ onToggle: (e: Event) => {
44
+ const tooltip = e.target as HTMLDialogElement;
45
+ _isVisible.value = tooltip.matches(":popover-open");
46
+ },
47
+ anchor: triggerId,
48
+ popover: "auto",
52
49
  role: "status",
53
50
  tabindex: "-1",
54
51
  },
@@ -57,6 +57,7 @@ export const createTooltip = createBuilder(({ debounce, isVisible }: CreateToolt
57
57
  * Only simple, textual and non-focusable content is allowed.
58
58
  */
59
59
  tooltip: {
60
+ popover: "manual",
60
61
  role: "tooltip",
61
62
  id: tooltipId,
62
63
  tabindex: "-1",
@@ -29,7 +29,7 @@ export type IteratedHeadlessElementFunc<
29
29
 
30
30
  export type HeadlessElementAttributes<A extends HTMLAttributes> =
31
31
  | VBindAttributes<A>
32
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the specific type doesn't matter here
33
33
  | IteratedHeadlessElementFunc<A, any>;
34
34
 
35
35
  export type HeadlessElements = Record<string, MaybeRef<HeadlessElementAttributes<HTMLAttributes>>>;
@@ -26,3 +26,8 @@ export type IsArray<TValue, TMultiple extends boolean = false> = TMultiple exten
26
26
  * A type that can be wrapped in an array.
27
27
  */
28
28
  export type Arrayable<T> = T | Array<T>;
29
+
30
+ /**
31
+ * Either the actual value or a nullish one.
32
+ */
33
+ export type Nullable<T = never> = T | null | undefined;