@sit-onyx/headless 1.0.0-beta.20 → 1.0.0-beta.22
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/dist/composables/comboBox/SelectOnlyCombobox.d.vue.ts +299 -0
- package/dist/composables/comboBox/TestCombobox.ct.d.ts +1 -0
- package/dist/composables/comboBox/TestCombobox.d.vue.ts +299 -0
- package/dist/composables/comboBox/createComboBox.d.ts +370 -0
- package/dist/composables/comboBox/createComboBox.testing.d.ts +10 -0
- package/dist/composables/helpers/useDismissible.d.ts +10 -0
- package/dist/composables/helpers/useGlobalListener.d.ts +10 -0
- package/dist/composables/helpers/useGlobalListener.spec.d.ts +1 -0
- package/dist/composables/helpers/useOutsideClick.d.ts +26 -0
- package/dist/composables/helpers/useOutsideClick.spec.d.ts +1 -0
- package/dist/composables/helpers/useTypeAhead.d.ts +11 -0
- package/dist/composables/helpers/useTypeAhead.spec.d.ts +1 -0
- package/dist/composables/listbox/TestListbox.ct.d.ts +1 -0
- package/dist/composables/listbox/TestListbox.d.vue.ts +2 -0
- package/dist/composables/listbox/createListbox.d.ts +102 -0
- package/dist/composables/listbox/createListbox.testing.d.ts +24 -0
- package/dist/composables/menuButton/TestMenuButton.ct.d.ts +1 -0
- package/dist/composables/menuButton/TestMenuButton.d.vue.ts +2 -0
- package/dist/composables/menuButton/createMenuButton.d.ts +78 -0
- package/dist/composables/menuButton/createMenuButton.testing.d.ts +24 -0
- package/dist/composables/navigationMenu/TestMenu.ct.d.ts +1 -0
- package/dist/composables/navigationMenu/TestMenu.d.vue.ts +2 -0
- package/dist/composables/navigationMenu/createMenu.d.ts +21 -0
- package/dist/composables/navigationMenu/createMenu.testing.d.ts +16 -0
- package/dist/composables/tabs/TestTabs.ct.d.ts +1 -0
- package/dist/composables/tabs/TestTabs.d.vue.ts +2 -0
- package/dist/composables/tabs/createTabs.d.ts +48 -0
- package/dist/composables/tabs/createTabs.testing.d.ts +13 -0
- package/dist/composables/tooltip/createToggletip.d.ts +36 -0
- package/dist/composables/tooltip/createTooltip.d.ts +42 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1088 -0
- package/dist/playwright.d.ts +5 -0
- package/dist/playwright.js +369 -0
- package/dist/utils/builder.d.ts +85 -0
- package/dist/utils/keyboard.d.ts +26 -0
- package/dist/utils/keyboard.spec.d.ts +1 -0
- package/dist/utils/math.d.ts +6 -0
- package/dist/utils/math.spec.d.ts +1 -0
- package/dist/utils/object.d.ts +5 -0
- package/dist/utils/object.spec.d.ts +1 -0
- package/dist/utils/timer.d.ts +10 -0
- package/{src/utils/types.ts → dist/utils/types.d.ts} +4 -12
- package/dist/utils/vitest.d.ts +12 -0
- package/package.json +20 -8
- package/src/composables/comboBox/SelectOnlyCombobox.vue +0 -90
- package/src/composables/comboBox/TestCombobox.ct.tsx +0 -24
- package/src/composables/comboBox/TestCombobox.vue +0 -84
- package/src/composables/comboBox/createComboBox.testing.ts +0 -168
- package/src/composables/comboBox/createComboBox.ts +0 -280
- package/src/composables/helpers/useDismissible.ts +0 -19
- package/src/composables/helpers/useGlobalListener.spec.ts +0 -93
- package/src/composables/helpers/useGlobalListener.ts +0 -64
- package/src/composables/helpers/useOutsideClick.spec.ts +0 -117
- package/src/composables/helpers/useOutsideClick.ts +0 -69
- package/src/composables/helpers/useTypeAhead.spec.ts +0 -29
- package/src/composables/helpers/useTypeAhead.ts +0 -26
- package/src/composables/listbox/TestListbox.ct.tsx +0 -17
- package/src/composables/listbox/TestListbox.vue +0 -92
- package/src/composables/listbox/createListbox.testing.ts +0 -141
- package/src/composables/listbox/createListbox.ts +0 -234
- package/src/composables/menuButton/TestMenuButton.ct.tsx +0 -14
- package/src/composables/menuButton/TestMenuButton.vue +0 -29
- package/src/composables/menuButton/createMenuButton.testing.ts +0 -91
- package/src/composables/menuButton/createMenuButton.ts +0 -206
- package/src/composables/navigationMenu/TestMenu.ct.tsx +0 -12
- package/src/composables/navigationMenu/TestMenu.vue +0 -16
- package/src/composables/navigationMenu/createMenu.testing.ts +0 -37
- package/src/composables/navigationMenu/createMenu.ts +0 -55
- package/src/composables/tabs/TestTabs.ct.tsx +0 -12
- package/src/composables/tabs/TestTabs.vue +0 -28
- package/src/composables/tabs/createTabs.testing.ts +0 -151
- package/src/composables/tabs/createTabs.ts +0 -129
- package/src/composables/tooltip/createToggletip.ts +0 -58
- package/src/composables/tooltip/createTooltip.ts +0 -71
- package/src/index.ts +0 -11
- package/src/playwright.ts +0 -5
- package/src/utils/builder.ts +0 -135
- package/src/utils/keyboard.spec.ts +0 -53
- package/src/utils/keyboard.ts +0 -351
- package/src/utils/math.spec.ts +0 -14
- package/src/utils/math.ts +0 -6
- package/src/utils/object.spec.ts +0 -33
- package/src/utils/object.ts +0 -8
- package/src/utils/timer.ts +0 -22
- package/src/utils/vitest.ts +0 -36
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { test } from "@playwright/experimental-ct-vue";
|
|
2
|
-
import TestTabs from "./TestTabs.vue";
|
|
3
|
-
import { tabsTesting } from "./createTabs.testing";
|
|
4
|
-
|
|
5
|
-
test("tabs", async ({ mount, page }) => {
|
|
6
|
-
const component = await mount(<TestTabs />);
|
|
7
|
-
|
|
8
|
-
await tabsTesting({
|
|
9
|
-
page,
|
|
10
|
-
tablist: component.getByRole("tablist"),
|
|
11
|
-
});
|
|
12
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import { ref } from "vue";
|
|
3
|
-
import { createTabs } from "./createTabs";
|
|
4
|
-
|
|
5
|
-
const selectedTab = ref("tab-1");
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
elements: { tablist, tab, tabpanel },
|
|
9
|
-
} = createTabs({
|
|
10
|
-
label: "Tablist label",
|
|
11
|
-
selectedTab,
|
|
12
|
-
onSelect: (tab) => (selectedTab.value = tab),
|
|
13
|
-
});
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<div>
|
|
18
|
-
<div v-bind="tablist">
|
|
19
|
-
<button v-bind="tab({ value: 'tab-1' })" type="button">Tab 1</button>
|
|
20
|
-
<button v-bind="tab({ value: 'tab-2' })" type="button">Tab 2</button>
|
|
21
|
-
<button v-bind="tab({ value: 'tab-3' })" type="button">Tab 3</button>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div v-if="selectedTab === 'tab-1'" v-bind="tabpanel({ value: 'tab-1' })">Tab content 1</div>
|
|
25
|
-
<div v-if="selectedTab === 'tab-2'" v-bind="tabpanel({ value: 'tab-2' })">Tab content 2</div>
|
|
26
|
-
<div v-if="selectedTab === 'tab-3'" v-bind="tabpanel({ value: 'tab-3' })">Tab content 3</div>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { expect } from "@playwright/experimental-ct-vue";
|
|
2
|
-
import type { Locator, Page } from "@playwright/test";
|
|
3
|
-
|
|
4
|
-
export type TabsTestingOptions = {
|
|
5
|
-
page: Page;
|
|
6
|
-
/**
|
|
7
|
-
* Locator of the tabs component. Must have at least 3 tabs where the first one is initially selected.
|
|
8
|
-
*/
|
|
9
|
-
tablist: Locator;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Playwright utility for executing accessibility testing for tabs.
|
|
14
|
-
* Will check aria attributes and keyboard shortcuts as defined in https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
15
|
-
*/
|
|
16
|
-
export const tabsTesting = async (options: TabsTestingOptions) => {
|
|
17
|
-
await expect(options.tablist, 'tablist element must have role "tablist"').toHaveRole("tablist");
|
|
18
|
-
await expect(options.tablist, "tablist must have an accessible label").toHaveAttribute(
|
|
19
|
-
"aria-label",
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
const firstTab = options.tablist.getByRole("tab").first();
|
|
23
|
-
const secondTab = options.tablist.getByRole("tab").nth(1);
|
|
24
|
-
const lastTab = options.tablist.getByRole("tab").last();
|
|
25
|
-
|
|
26
|
-
const { tabId, panelId } = await expectTabAttributes(firstTab, true);
|
|
27
|
-
await expectPanelAttributes(options.page.locator(`#${panelId}`), tabId);
|
|
28
|
-
|
|
29
|
-
// ACT (switch tab)
|
|
30
|
-
await secondTab.click();
|
|
31
|
-
|
|
32
|
-
const { tabId: tabId2, panelId: panelId2 } = await expectTabAttributes(secondTab, true);
|
|
33
|
-
await expectPanelAttributes(options.page.locator(`#${panelId2}`), tabId2);
|
|
34
|
-
await expect(secondTab, "second tab should be focused").toBeFocused();
|
|
35
|
-
|
|
36
|
-
await expect(options.page.getByRole("tabpanel"), "should hide previous panel").toHaveCount(1);
|
|
37
|
-
|
|
38
|
-
// keyboard support
|
|
39
|
-
await options.page.keyboard.press("ArrowLeft");
|
|
40
|
-
await expect(firstTab, "should focus previous tab when pressing arrow left").toBeFocused();
|
|
41
|
-
|
|
42
|
-
await options.page.keyboard.press("End");
|
|
43
|
-
await expect(lastTab, "should focus last tab when pressing End").toBeFocused();
|
|
44
|
-
|
|
45
|
-
await options.page.keyboard.press("ArrowRight");
|
|
46
|
-
await expect(
|
|
47
|
-
firstTab,
|
|
48
|
-
"should focus first tab when last tab is focused and pressing arrow right",
|
|
49
|
-
).toBeFocused();
|
|
50
|
-
|
|
51
|
-
await options.page.keyboard.press("ArrowRight");
|
|
52
|
-
await expect(secondTab, "should focus next tab when pressing arrow right").toBeFocused();
|
|
53
|
-
|
|
54
|
-
await options.page.keyboard.press("Home");
|
|
55
|
-
await expect(firstTab, "should focus first tab when pressing Home").toBeFocused();
|
|
56
|
-
|
|
57
|
-
await options.page.keyboard.press("ArrowLeft");
|
|
58
|
-
|
|
59
|
-
await expect(
|
|
60
|
-
lastTab,
|
|
61
|
-
"should focus last tab when first tab is focused and pressing arrow left",
|
|
62
|
-
).toBeFocused();
|
|
63
|
-
|
|
64
|
-
// should select when pressing Enter
|
|
65
|
-
await options.page.keyboard.press("Enter");
|
|
66
|
-
const { tabId: tabIdLast, panelId: panelIdLast } = await expectTabAttributes(lastTab, true);
|
|
67
|
-
await expectPanelAttributes(options.page.locator(`#${panelIdLast}`), tabIdLast);
|
|
68
|
-
|
|
69
|
-
// should select when pressing Space
|
|
70
|
-
await firstTab.focus();
|
|
71
|
-
await options.page.keyboard.press("Space");
|
|
72
|
-
const { tabId: tabIdFirst, panelId: panelIdFirst } = await expectTabAttributes(firstTab, true);
|
|
73
|
-
await expectPanelAttributes(options.page.locator(`#${panelIdFirst}`), tabIdFirst);
|
|
74
|
-
|
|
75
|
-
// should skip disabled tabs when using the keyboard
|
|
76
|
-
await firstTab.click();
|
|
77
|
-
await secondTab.evaluate((element) => (element.ariaDisabled = "true"));
|
|
78
|
-
await expect(secondTab, "should disable second tab when setting aria-disabled").toBeDisabled();
|
|
79
|
-
|
|
80
|
-
await options.page.keyboard.press("ArrowRight");
|
|
81
|
-
await expect(secondTab, "should not focus second tab if its aria-disabled").not.toBeFocused();
|
|
82
|
-
await expect(
|
|
83
|
-
options.tablist.getByRole("tab").nth(2),
|
|
84
|
-
"should focus next tab after disabled one when pressing arrow right",
|
|
85
|
-
).toBeFocused();
|
|
86
|
-
|
|
87
|
-
await options.page.keyboard.press("ArrowLeft");
|
|
88
|
-
await expect(
|
|
89
|
-
firstTab,
|
|
90
|
-
"should focus tab before disabled one when pressing arrow left",
|
|
91
|
-
).toBeFocused();
|
|
92
|
-
|
|
93
|
-
await secondTab.evaluate((element) => (element.ariaDisabled = null));
|
|
94
|
-
await firstTab.evaluate((element) => (element.ariaDisabled = "true"));
|
|
95
|
-
await options.page.keyboard.press("Home");
|
|
96
|
-
await expect(
|
|
97
|
-
secondTab,
|
|
98
|
-
"should focus second tab when pressing Home if first tab is disabled",
|
|
99
|
-
).toBeFocused();
|
|
100
|
-
|
|
101
|
-
await firstTab.evaluate((element) => (element.ariaDisabled = null));
|
|
102
|
-
await lastTab.evaluate((element) => (element.ariaDisabled = "true"));
|
|
103
|
-
await firstTab.focus();
|
|
104
|
-
await options.page.keyboard.press("End");
|
|
105
|
-
await expect(
|
|
106
|
-
options.tablist.getByRole("tab").nth(-2),
|
|
107
|
-
"should focus second last tab when pressing End if last tab is disabled",
|
|
108
|
-
).toBeFocused();
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Executes accessibility tests for a single tab.
|
|
113
|
-
*
|
|
114
|
-
* @param tab Locator of the tab.
|
|
115
|
-
* @param selected Whether the tab is expected to be selected
|
|
116
|
-
*/
|
|
117
|
-
const expectTabAttributes = async (tab: Locator, selected: boolean) => {
|
|
118
|
-
await expect(tab, 'tab must have role "tab"').toHaveRole("tab");
|
|
119
|
-
await expect(tab, "tab must have an ID").toHaveAttribute("id");
|
|
120
|
-
await expect(tab, 'tab must have "aria-selected" set').toHaveAttribute(
|
|
121
|
-
"aria-selected",
|
|
122
|
-
String(selected),
|
|
123
|
-
);
|
|
124
|
-
await expect(tab, 'tab must have "aria-controls" set').toHaveAttribute("aria-controls");
|
|
125
|
-
|
|
126
|
-
if (selected) {
|
|
127
|
-
await expect(tab, "selected tab should be focusable").toHaveAttribute("tabindex", "0");
|
|
128
|
-
} else {
|
|
129
|
-
await expect(tab, "unselected tab should NOT be focusable").toHaveAttribute("tabindex", "-1");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const tabId = (await tab.getAttribute("id"))!;
|
|
133
|
-
const panelId = (await tab.getAttribute("aria-controls"))!;
|
|
134
|
-
return { tabId, panelId };
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Executes accessibility tests for a single tab panel.
|
|
139
|
-
*
|
|
140
|
-
* @param panel Locator of the panel
|
|
141
|
-
* @param tabId Corresponding tab id
|
|
142
|
-
*/
|
|
143
|
-
const expectPanelAttributes = async (panel: Locator, tabId: string) => {
|
|
144
|
-
await expect(panel, "panel should be visible").toBeVisible();
|
|
145
|
-
await expect(panel, 'panel must have role "tabpanel"').toHaveRole("tabpanel");
|
|
146
|
-
await expect(panel, "panel must have an ID").toHaveAttribute("id");
|
|
147
|
-
await expect(panel, 'panel must have "aria-labelledby" set').toHaveAttribute(
|
|
148
|
-
"aria-labelledby",
|
|
149
|
-
tabId,
|
|
150
|
-
);
|
|
151
|
-
};
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { computed, unref, useId, type MaybeRef, type Ref } from "vue";
|
|
2
|
-
import { createBuilder } from "../../utils/builder";
|
|
3
|
-
|
|
4
|
-
type CreateTabsOptions<TKey extends PropertyKey = PropertyKey> = {
|
|
5
|
-
/**
|
|
6
|
-
* Label of the tablist.
|
|
7
|
-
*/
|
|
8
|
-
label: MaybeRef<string>;
|
|
9
|
-
/**
|
|
10
|
-
* Currently selected tab.
|
|
11
|
-
*/
|
|
12
|
-
selectedTab: Ref<TKey>;
|
|
13
|
-
/**
|
|
14
|
-
* Called when the user selects a tab.
|
|
15
|
-
*/
|
|
16
|
-
onSelect?: (selectedTabValue: TKey) => void;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Composable for implementing accessible tabs.
|
|
21
|
-
* Based on https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
|
|
22
|
-
*/
|
|
23
|
-
export const createTabs = createBuilder(<T extends PropertyKey>(options: CreateTabsOptions<T>) => {
|
|
24
|
-
/**
|
|
25
|
-
* Map for looking up tab and panel IDs for given tab keys/values defined by the user.
|
|
26
|
-
* Key = custom value from the user, value = random generated tab and panel ID
|
|
27
|
-
*/
|
|
28
|
-
const idMap = new Map<T, { tabId: string; panelId: string }>();
|
|
29
|
-
|
|
30
|
-
const getId = (value: T) => {
|
|
31
|
-
if (!idMap.has(value)) {
|
|
32
|
-
idMap.set(value, { tabId: useId(), panelId: useId() });
|
|
33
|
-
}
|
|
34
|
-
return idMap.get(value)!;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const handleKeydown = (event: KeyboardEvent) => {
|
|
38
|
-
const tab = event.target as Element;
|
|
39
|
-
|
|
40
|
-
const enabledTabs = Array.from(
|
|
41
|
-
tab.parentElement?.querySelectorAll('[role="tab"]') ?? [],
|
|
42
|
-
).filter((tab) => tab.ariaDisabled !== "true");
|
|
43
|
-
|
|
44
|
-
const currentTabIndex = enabledTabs.indexOf(tab);
|
|
45
|
-
|
|
46
|
-
const focusElement = (element?: Element | null) => {
|
|
47
|
-
if (element instanceof HTMLElement) element.focus();
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const focusFirstTab = () => focusElement(enabledTabs.at(0));
|
|
51
|
-
const focusLastTab = () => focusElement(enabledTabs.at(-1));
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Focuses the next/previous tab. Will ignore/skip disabled ones.
|
|
55
|
-
*/
|
|
56
|
-
const focusTab = (direction: "next" | "previous") => {
|
|
57
|
-
if (currentTabIndex === -1) return;
|
|
58
|
-
const newIndex = direction === "next" ? currentTabIndex + 1 : currentTabIndex - 1;
|
|
59
|
-
|
|
60
|
-
if (newIndex < 0) {
|
|
61
|
-
return focusLastTab();
|
|
62
|
-
} else if (newIndex >= enabledTabs.length) {
|
|
63
|
-
return focusFirstTab();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return focusElement(enabledTabs.at(newIndex));
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
switch (event.key) {
|
|
70
|
-
case "ArrowRight":
|
|
71
|
-
focusTab("next");
|
|
72
|
-
break;
|
|
73
|
-
case "ArrowLeft":
|
|
74
|
-
focusTab("previous");
|
|
75
|
-
break;
|
|
76
|
-
case "Home":
|
|
77
|
-
focusFirstTab();
|
|
78
|
-
break;
|
|
79
|
-
case "End":
|
|
80
|
-
focusLastTab();
|
|
81
|
-
break;
|
|
82
|
-
case "Enter":
|
|
83
|
-
case " ":
|
|
84
|
-
{
|
|
85
|
-
const tabEntry = Array.from(idMap.entries()).find(([, { tabId }]) => tabId === tab.id);
|
|
86
|
-
if (tabEntry) options.onSelect?.(tabEntry[0]);
|
|
87
|
-
}
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
elements: {
|
|
94
|
-
tablist: computed(() => ({
|
|
95
|
-
role: "tablist",
|
|
96
|
-
"aria-label": unref(options.label),
|
|
97
|
-
onKeydown: handleKeydown,
|
|
98
|
-
})),
|
|
99
|
-
tab: computed(() => {
|
|
100
|
-
return (data: { value: T; disabled?: boolean }) => {
|
|
101
|
-
const { tabId: selectedTabId } = getId(unref(options.selectedTab));
|
|
102
|
-
const { tabId, panelId } = getId(data.value);
|
|
103
|
-
const isSelected = tabId === selectedTabId;
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
id: tabId,
|
|
107
|
-
role: "tab",
|
|
108
|
-
"aria-selected": isSelected,
|
|
109
|
-
"aria-controls": panelId,
|
|
110
|
-
"aria-disabled": data.disabled ? true : undefined,
|
|
111
|
-
onClick: () => options.onSelect?.(data.value),
|
|
112
|
-
tabindex: isSelected && !data.disabled ? 0 : -1,
|
|
113
|
-
} as const;
|
|
114
|
-
};
|
|
115
|
-
}),
|
|
116
|
-
tabpanel: computed(() => {
|
|
117
|
-
return (data: { value: T }) => {
|
|
118
|
-
const { tabId, panelId } = getId(data.value);
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
id: panelId,
|
|
122
|
-
role: "tabpanel",
|
|
123
|
-
"aria-labelledby": tabId,
|
|
124
|
-
} as const;
|
|
125
|
-
};
|
|
126
|
-
}),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { computed, toRef, toValue, useId, type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
-
import { createBuilder } from "../../utils/builder";
|
|
3
|
-
import { useDismissible } from "../helpers/useDismissible";
|
|
4
|
-
|
|
5
|
-
export type CreateToggletipOptions = {
|
|
6
|
-
toggleLabel: MaybeRefOrGetter<string>;
|
|
7
|
-
isVisible?: Ref<boolean>;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create a toggletip as described in https://inclusive-components.design/tooltips-toggletips/
|
|
12
|
-
* Its visibility is toggled on click.
|
|
13
|
-
* Therefore a toggletip MUST NOT be used to describe the associated trigger element.
|
|
14
|
-
* Commonly this pattern uses a button with the ⓘ as the trigger element.
|
|
15
|
-
* To describe the associated element use `createTooltip`.
|
|
16
|
-
*/
|
|
17
|
-
export const createToggletip = createBuilder(
|
|
18
|
-
({ toggleLabel, isVisible }: CreateToggletipOptions) => {
|
|
19
|
-
const triggerId = useId();
|
|
20
|
-
|
|
21
|
-
const _isVisible = toRef(isVisible ?? false);
|
|
22
|
-
|
|
23
|
-
useDismissible({ isExpanded: _isVisible });
|
|
24
|
-
|
|
25
|
-
const toggle = () => (_isVisible.value = !_isVisible.value);
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
elements: {
|
|
29
|
-
/**
|
|
30
|
-
* The element which controls the toggletip visibility:
|
|
31
|
-
* Preferably a `button` element.
|
|
32
|
-
*/
|
|
33
|
-
trigger: computed(() => ({
|
|
34
|
-
id: triggerId,
|
|
35
|
-
onClick: toggle,
|
|
36
|
-
"aria-label": toValue(toggleLabel),
|
|
37
|
-
})),
|
|
38
|
-
/**
|
|
39
|
-
* The element with the relevant toggletip content.
|
|
40
|
-
* Only simple, textual content is allowed.
|
|
41
|
-
*/
|
|
42
|
-
tooltip: {
|
|
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",
|
|
49
|
-
role: "status",
|
|
50
|
-
tabindex: "-1",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
state: {
|
|
54
|
-
isVisible: _isVisible,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
);
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { computed, toRef, toValue, useId, type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
-
import { createBuilder } from "../../utils/builder";
|
|
3
|
-
import { useDismissible } from "../helpers/useDismissible";
|
|
4
|
-
|
|
5
|
-
export type CreateTooltipOptions = {
|
|
6
|
-
/**
|
|
7
|
-
* Number of milliseconds to use as debounce when showing/hiding the tooltip.
|
|
8
|
-
*/
|
|
9
|
-
debounce: MaybeRefOrGetter<number>;
|
|
10
|
-
isVisible?: Ref<boolean>;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Create a tooltip as described in https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role
|
|
15
|
-
* Its visibility is toggled on hover or focus.
|
|
16
|
-
* A tooltip MUST be used to describe the associated trigger element. E.g. The usage with the ⓘ would be incorrect.
|
|
17
|
-
* To provide contextual information use the `createToggletip`.
|
|
18
|
-
*/
|
|
19
|
-
export const createTooltip = createBuilder(({ debounce, isVisible }: CreateTooltipOptions) => {
|
|
20
|
-
const tooltipId = useId();
|
|
21
|
-
const _isVisible = toRef(isVisible ?? false);
|
|
22
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Debounced visible state that will only be toggled after a given timeout.
|
|
26
|
-
*/
|
|
27
|
-
const debouncedVisible = computed({
|
|
28
|
-
get: () => _isVisible.value,
|
|
29
|
-
set: (newValue) => {
|
|
30
|
-
clearTimeout(timeout);
|
|
31
|
-
timeout = setTimeout(() => {
|
|
32
|
-
_isVisible.value = newValue;
|
|
33
|
-
}, toValue(debounce));
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const hoverEvents = {
|
|
38
|
-
onMouseover: () => (debouncedVisible.value = true),
|
|
39
|
-
onMouseout: () => (debouncedVisible.value = false),
|
|
40
|
-
onFocusin: () => (_isVisible.value = true),
|
|
41
|
-
onFocusout: () => (_isVisible.value = false),
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
useDismissible({ isExpanded: _isVisible });
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
elements: {
|
|
48
|
-
/**
|
|
49
|
-
* The element which controls the tooltip visibility on hover.
|
|
50
|
-
*/
|
|
51
|
-
trigger: {
|
|
52
|
-
"aria-describedby": tooltipId,
|
|
53
|
-
...hoverEvents,
|
|
54
|
-
},
|
|
55
|
-
/**
|
|
56
|
-
* The element describing the tooltip.
|
|
57
|
-
* Only simple, textual and non-focusable content is allowed.
|
|
58
|
-
*/
|
|
59
|
-
tooltip: {
|
|
60
|
-
popover: "manual",
|
|
61
|
-
role: "tooltip",
|
|
62
|
-
id: tooltipId,
|
|
63
|
-
tabindex: "-1",
|
|
64
|
-
...hoverEvents,
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
state: {
|
|
68
|
-
isVisible: _isVisible,
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export * from "./composables/comboBox/createComboBox";
|
|
2
|
-
export * from "./composables/helpers/useGlobalListener";
|
|
3
|
-
export * from "./composables/listbox/createListbox";
|
|
4
|
-
export * from "./composables/menuButton/createMenuButton";
|
|
5
|
-
export * from "./composables/navigationMenu/createMenu";
|
|
6
|
-
export * from "./composables/tabs/createTabs";
|
|
7
|
-
export * from "./composables/tooltip/createToggletip";
|
|
8
|
-
export * from "./composables/tooltip/createTooltip";
|
|
9
|
-
export * from "./utils/builder";
|
|
10
|
-
export { isPrintableCharacter, wasKeyPressed } from "./utils/keyboard";
|
|
11
|
-
export { debounce } from "./utils/timer";
|
package/src/playwright.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export * from "./composables/comboBox/createComboBox.testing";
|
|
2
|
-
export * from "./composables/listbox/createListbox.testing";
|
|
3
|
-
export * from "./composables/menuButton/createMenuButton.testing";
|
|
4
|
-
export * from "./composables/navigationMenu/createMenu.testing";
|
|
5
|
-
export * from "./composables/tabs/createTabs.testing";
|
package/src/utils/builder.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
computed,
|
|
3
|
-
shallowRef,
|
|
4
|
-
type ComponentPublicInstance,
|
|
5
|
-
type HTMLAttributes,
|
|
6
|
-
type MaybeRef,
|
|
7
|
-
type Ref,
|
|
8
|
-
type WritableComputedOptions,
|
|
9
|
-
type WritableComputedRef,
|
|
10
|
-
} from "vue";
|
|
11
|
-
import type { IfDefined } from "./types";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Properties as they can be used by `v-bind` on an HTML element.
|
|
15
|
-
* This includes generic html attributes and the vue reserved `ref` property.
|
|
16
|
-
* `ref` is restricted to be a `HeadlessElRef` which only can by created through `createElRef`.
|
|
17
|
-
*/
|
|
18
|
-
export type VBindAttributes<
|
|
19
|
-
A extends HTMLAttributes = HTMLAttributes,
|
|
20
|
-
E extends Element = Element,
|
|
21
|
-
> = A & {
|
|
22
|
-
ref?: VueTemplateRef<E>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type IteratedHeadlessElementFunc<
|
|
26
|
-
A extends HTMLAttributes,
|
|
27
|
-
T extends Record<string, unknown>,
|
|
28
|
-
> = (opts: T) => VBindAttributes<A>;
|
|
29
|
-
|
|
30
|
-
export type HeadlessElementAttributes<A extends HTMLAttributes> =
|
|
31
|
-
| VBindAttributes<A>
|
|
32
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the specific type doesn't matter here
|
|
33
|
-
| IteratedHeadlessElementFunc<A, any>;
|
|
34
|
-
|
|
35
|
-
export type HeadlessElements = Record<string, MaybeRef<HeadlessElementAttributes<HTMLAttributes>>>;
|
|
36
|
-
|
|
37
|
-
export type HeadlessState = Record<string, Ref>;
|
|
38
|
-
|
|
39
|
-
export type HeadlessComposable<
|
|
40
|
-
Elements extends HeadlessElements,
|
|
41
|
-
State extends HeadlessState | undefined = undefined,
|
|
42
|
-
Internals extends object | undefined = undefined,
|
|
43
|
-
> = {
|
|
44
|
-
elements: Elements;
|
|
45
|
-
} & IfDefined<"internals", Internals> &
|
|
46
|
-
IfDefined<"state", State>;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* We use this identity function to ensure the correct typings of the headless composables
|
|
50
|
-
* @example
|
|
51
|
-
* ```ts
|
|
52
|
-
* export const createTooltip = createBuilder(({ initialVisible }: CreateTooltipOptions) => {
|
|
53
|
-
* const tooltipId = useId();
|
|
54
|
-
* const isVisible = ref(initialVisible);
|
|
55
|
-
*
|
|
56
|
-
* const hoverEvents = {
|
|
57
|
-
* onMouseover: () => (isVisible.value = true),
|
|
58
|
-
* onMouseout: () => (isVisible.value = false),
|
|
59
|
-
* onFocusin: () => (isVisible.value = true),
|
|
60
|
-
* onFocusout: () => (isVisible.value = false),
|
|
61
|
-
* };
|
|
62
|
-
*
|
|
63
|
-
* return {
|
|
64
|
-
* elements: {
|
|
65
|
-
* trigger: {
|
|
66
|
-
* "aria-describedby": tooltipId,
|
|
67
|
-
* ...hoverEvents,
|
|
68
|
-
* },
|
|
69
|
-
* tooltip: {
|
|
70
|
-
* role: "tooltip",
|
|
71
|
-
* id: tooltipId,
|
|
72
|
-
* tabindex: "-1",
|
|
73
|
-
* ...hoverEvents,
|
|
74
|
-
* },
|
|
75
|
-
* },
|
|
76
|
-
* state: {
|
|
77
|
-
* isVisible,
|
|
78
|
-
* },
|
|
79
|
-
* };
|
|
80
|
-
* });
|
|
81
|
-
*
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export const createBuilder = <
|
|
85
|
-
Args extends unknown[] = unknown[],
|
|
86
|
-
Elements extends HeadlessElements = HeadlessElements,
|
|
87
|
-
State extends HeadlessState | undefined = undefined,
|
|
88
|
-
Internals extends object | undefined = undefined,
|
|
89
|
-
>(
|
|
90
|
-
builder: (...args: Args) => HeadlessComposable<Elements, State, Internals>,
|
|
91
|
-
) => builder;
|
|
92
|
-
|
|
93
|
-
type VueTemplateRefElement<E extends Element> = E | (ComponentPublicInstance & { $el: E }) | null;
|
|
94
|
-
type VueTemplateRef<E extends Element> = Ref<VueTemplateRefElement<E>>;
|
|
95
|
-
|
|
96
|
-
declare const HeadlessElRefSymbol: unique symbol;
|
|
97
|
-
type HeadlessElRef<E extends Element> = WritableComputedRef<E> & {
|
|
98
|
-
/**
|
|
99
|
-
* type differentiator
|
|
100
|
-
* ensures that only `createElRef` can be used for headless element ref bindings
|
|
101
|
-
*/
|
|
102
|
-
[HeadlessElRefSymbol]: true;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Creates a special writeable computed that references a DOM Element.
|
|
107
|
-
* Vue Component references will be unwrapped.
|
|
108
|
-
* @example
|
|
109
|
-
* ```ts
|
|
110
|
-
* createBuilder() => {
|
|
111
|
-
* const buttonRef = createElRef<HtmlButtonElement>();
|
|
112
|
-
* return {
|
|
113
|
-
* elements: {
|
|
114
|
-
* button: {
|
|
115
|
-
* ref: buttonRef,
|
|
116
|
-
* },
|
|
117
|
-
* }
|
|
118
|
-
* };
|
|
119
|
-
* });
|
|
120
|
-
* ```
|
|
121
|
-
*/
|
|
122
|
-
export function createElRef<E extends Element>(): HeadlessElRef<E>;
|
|
123
|
-
export function createElRef<
|
|
124
|
-
E extends Element,
|
|
125
|
-
V extends VueTemplateRefElement<E> = VueTemplateRefElement<E>,
|
|
126
|
-
>() {
|
|
127
|
-
const elementRef = shallowRef<E>();
|
|
128
|
-
|
|
129
|
-
return computed({
|
|
130
|
-
set: (element: V) => {
|
|
131
|
-
elementRef.value = element != null && "$el" in element ? element.$el : (element as E);
|
|
132
|
-
},
|
|
133
|
-
get: () => elementRef.value,
|
|
134
|
-
} as WritableComputedOptions<E>);
|
|
135
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
2
|
-
import { isPrintableCharacter, wasKeyPressed } from "./keyboard";
|
|
3
|
-
|
|
4
|
-
test.each([
|
|
5
|
-
// ARRANGE
|
|
6
|
-
{ input: [{ key: "m", code: "KeyM" }, "m"], expected: true },
|
|
7
|
-
{ input: [{ key: "m", code: "KeyM" }, "m"], expected: true },
|
|
8
|
-
{ input: [{ key: "m", code: "KeyM" }, { key: "m" }], expected: true },
|
|
9
|
-
{ input: [{ key: "m", code: "KeyM" }, { code: "KeyM" }], expected: true },
|
|
10
|
-
{
|
|
11
|
-
input: [{ key: "m", code: "KeyM", altKey: false }, { code: "KeyM" }],
|
|
12
|
-
expected: true,
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
input: [
|
|
16
|
-
{ key: "m", code: "KeyM" },
|
|
17
|
-
{ code: "KeyM", altKey: true },
|
|
18
|
-
],
|
|
19
|
-
expected: false,
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
input: [
|
|
23
|
-
{ key: "m", code: "KeyM", shiftKey: true },
|
|
24
|
-
{ code: "KeyM", shiftKey: false },
|
|
25
|
-
],
|
|
26
|
-
expected: false,
|
|
27
|
-
},
|
|
28
|
-
] as { input: Parameters<typeof wasKeyPressed>; expected: boolean }[])(
|
|
29
|
-
"should return $expected for event $input.0 and pressed key $input.1",
|
|
30
|
-
({ input: [event, wasPressed], expected }) => {
|
|
31
|
-
// ACT
|
|
32
|
-
const result = wasKeyPressed(new KeyboardEvent("keydown", event), wasPressed);
|
|
33
|
-
|
|
34
|
-
// ASSERT
|
|
35
|
-
expect(result).toBe(expected);
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
test.each([
|
|
40
|
-
// ARRANGE
|
|
41
|
-
{ key: "a", expected: true },
|
|
42
|
-
{ key: "🎉", expected: true },
|
|
43
|
-
{ key: "あ", expected: true },
|
|
44
|
-
{ key: " ", expected: true },
|
|
45
|
-
{ key: "Meta", expected: false },
|
|
46
|
-
{ key: "Fn", expected: false },
|
|
47
|
-
])("should return $expected for key $key", ({ key, expected }) => {
|
|
48
|
-
// ACT
|
|
49
|
-
const result = isPrintableCharacter(key);
|
|
50
|
-
|
|
51
|
-
// ASSERT
|
|
52
|
-
expect(result).toBe(expected);
|
|
53
|
-
});
|