@sit-onyx/headless 1.0.0-beta.16 → 1.0.0-beta.18
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 +2 -2
- package/src/composables/comboBox/SelectOnlyCombobox.vue +3 -3
- package/src/composables/comboBox/TestCombobox.vue +3 -3
- package/src/composables/comboBox/createComboBox.ts +4 -3
- package/src/composables/helpers/useOutsideClick.ts +4 -4
- package/src/composables/listbox/createListbox.ts +4 -3
- package/src/composables/menuButton/createMenuButton.testing.ts +0 -7
- package/src/composables/menuButton/createMenuButton.ts +5 -1
- package/src/composables/navigationMenu/createMenu.ts +1 -1
- package/src/composables/tabs/TestTabs.ct.tsx +0 -1
- package/src/composables/tooltip/createToggletip.ts +11 -14
- package/src/composables/tooltip/createTooltip.ts +1 -0
- package/src/utils/builder.ts +1 -1
- package/src/utils/types.ts +7 -4
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.
|
|
4
|
+
"version": "1.0.0-beta.18",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Schwarz IT KG",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@vue/compiler-dom": "3.5.13",
|
|
31
31
|
"vue": "3.5.13",
|
|
32
|
-
"@sit-onyx/shared": "^1.0.0-beta.
|
|
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 =
|
|
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="
|
|
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 =
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
64
|
+
templateRef: Ref<Nullable<HTMLElement>>;
|
|
64
65
|
/**
|
|
65
66
|
* Hook when the popover should toggle.
|
|
66
67
|
*
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { Arrayable } from "vitest";
|
|
2
|
-
import { toValue, type Ref } from "vue";
|
|
3
|
-
import type {
|
|
1
|
+
import type { Arrayable } from "vitest"; // For an unknown reason removing this import will break the build of "demo-app" and "playground"
|
|
2
|
+
import { toValue, type MaybeRefOrGetter, type Ref } from "vue";
|
|
3
|
+
import type { Nullable } from "../../utils/types";
|
|
4
4
|
import { useGlobalEventListener } from "./useGlobalListener";
|
|
5
5
|
|
|
6
6
|
export type UseOutsideClickOptions = {
|
|
7
7
|
/**
|
|
8
8
|
* HTML element of the component where clicks should be ignored
|
|
9
9
|
*/
|
|
10
|
-
inside:
|
|
10
|
+
inside: MaybeRefOrGetter<Arrayable<Nullable<HTMLElement>>>;
|
|
11
11
|
/**
|
|
12
12
|
* Callback when an outside click occurred.
|
|
13
13
|
*/
|
|
@@ -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
|
|
16
|
+
description?: MaybeRef<Nullable<string>>;
|
|
16
17
|
/**
|
|
17
18
|
* Value of currently (visually) active option.
|
|
18
19
|
*/
|
|
19
|
-
activeOption: Ref<TValue
|
|
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
|
|
33
|
+
multiple?: MaybeRef<Nullable<TMultiple>>;
|
|
33
34
|
/**
|
|
34
35
|
* Hook when an option is selected.
|
|
35
36
|
*/
|
|
@@ -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',
|
|
@@ -7,6 +7,7 @@ type CreateMenuButtonOptions = {
|
|
|
7
7
|
isExpanded: Readonly<Ref<boolean>>;
|
|
8
8
|
trigger: Readonly<Ref<"hover" | "click">>;
|
|
9
9
|
onToggle: () => void;
|
|
10
|
+
disabled?: Readonly<Ref<boolean>>;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -31,6 +32,7 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
|
|
|
31
32
|
watch(options.isExpanded, () => updateDebouncedExpanded.abort()); // manually changing `isExpanded` should abort debounced action
|
|
32
33
|
|
|
33
34
|
const setExpanded = (expanded: boolean, debounced = false) => {
|
|
35
|
+
if (options.disabled?.value) return;
|
|
34
36
|
if (expanded === options.isExpanded.value) {
|
|
35
37
|
updateDebouncedExpanded.abort();
|
|
36
38
|
return;
|
|
@@ -50,7 +52,7 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
|
|
|
50
52
|
const currentMenu = currentMenuItem?.closest('[role="menu"]') || menuRef.value;
|
|
51
53
|
if (!currentMenu) return;
|
|
52
54
|
|
|
53
|
-
const menuItems =
|
|
55
|
+
const menuItems = Array.from(currentMenu.querySelectorAll<HTMLElement>('[role="menuitem"]'));
|
|
54
56
|
let nextIndex = 0;
|
|
55
57
|
|
|
56
58
|
if (currentMenuItem) {
|
|
@@ -96,6 +98,7 @@ export const createMenuButton = createBuilder((options: CreateMenuButtonOptions)
|
|
|
96
98
|
focusRelativeItem("last");
|
|
97
99
|
break;
|
|
98
100
|
case " ":
|
|
101
|
+
if (event.target instanceof HTMLInputElement) break;
|
|
99
102
|
event.preventDefault();
|
|
100
103
|
(event.target as HTMLElement).click();
|
|
101
104
|
break;
|
|
@@ -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: {
|
|
@@ -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
|
|
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
|
|
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
|
|
21
|
-
const tooltipRef = createElRef<HTMLElement>();
|
|
22
|
-
const _isVisible = toRef(isVisible ?? false);
|
|
19
|
+
const triggerId = useId();
|
|
23
20
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|
package/src/utils/builder.ts
CHANGED
|
@@ -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>>>;
|
package/src/utils/types.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Adds the entry with the key `Key` and the value of type `TValue` to a record when it is defined.
|
|
5
3
|
* Then the entry is either undefined or exists without being optional.
|
|
@@ -25,6 +23,11 @@ export type IsArray<TValue, TMultiple extends boolean = false> = TMultiple exten
|
|
|
25
23
|
: TValue;
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
26
|
+
* A type that can be wrapped in an array.
|
|
27
|
+
*/
|
|
28
|
+
export type Arrayable<T> = T | Array<T>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Either the actual value or a nullish one.
|
|
29
32
|
*/
|
|
30
|
-
export type
|
|
33
|
+
export type Nullable<T> = T | null | undefined;
|