@sit-onyx/headless 1.0.0-beta.5 → 1.0.0-beta.7

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.5",
4
+ "version": "1.0.0-beta.7",
5
5
  "type": "module",
6
6
  "author": "Schwarz IT KG",
7
7
  "license": "Apache-2.0",
@@ -10,6 +10,7 @@ import {
10
10
  type ListboxValue,
11
11
  } from "../listbox/createListbox";
12
12
 
13
+ /** See https://w3c.github.io/aria/#aria-autocomplete */
13
14
  export type ComboboxAutoComplete = "none" | "list" | "both";
14
15
 
15
16
  export const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
@@ -19,11 +20,15 @@ export const CLOSING_KEYS: PressedKey[] = [
19
20
  "Enter",
20
21
  "Tab",
21
22
  ];
22
- const SELECTING_KEYS_SINGLE: PressedKey[] = ["Enter", " "];
23
- const SELECTING_KEYS_MULTIPLE: PressedKey[] = ["Enter"];
24
23
 
25
- const isSelectingKey = (event: KeyboardEvent, isMultiselect?: boolean) => {
26
- const selectingKeys = isMultiselect ? SELECTING_KEYS_MULTIPLE : SELECTING_KEYS_SINGLE;
24
+ const SELECTING_KEYS: PressedKey[] = ["Enter"];
25
+
26
+ /**
27
+ * if the a search input is included, space should not be used to select
28
+ * TODO: idea for the future: move this distinction to the listbox?
29
+ */
30
+ const isSelectingKey = (event: KeyboardEvent, withSpace?: boolean) => {
31
+ const selectingKeys = withSpace ? [...SELECTING_KEYS, " "] : SELECTING_KEYS;
27
32
  return isKeyOfGroup(event, selectingKeys);
28
33
  };
29
34
 
@@ -41,6 +46,10 @@ export type CreateComboboxOptions<
41
46
  * Labels the listbox which displays the available options. E.g. the list label could be "Countries" for a combobox which is labelled "Country".
42
47
  */
43
48
  listLabel: MaybeRef<string>;
49
+ /**
50
+ * Provides additional description for the listbox which displays the available options.
51
+ */
52
+ listDescription?: MaybeRef<string | undefined>;
44
53
  /**
45
54
  * Controls the opened/visible state of the associated pop-up. When expanded the activeOption can be controlled via the keyboard.
46
55
  */
@@ -107,6 +116,7 @@ export const createComboBox = createBuilder(
107
116
  multiple: multipleRef,
108
117
  label,
109
118
  listLabel,
119
+ listDescription,
110
120
  isExpanded: isExpandedRef,
111
121
  activeOption,
112
122
  onToggle,
@@ -183,7 +193,7 @@ export const createComboBox = createBuilder(
183
193
  }
184
194
  return onActivateFirst?.();
185
195
  }
186
- if (isSelectingKey(event, multiple.value)) {
196
+ if (isSelectingKey(event, autocomplete.value === "none")) {
187
197
  return handleSelect(activeOption.value!);
188
198
  }
189
199
  if (isExpanded.value && isKeyOfGroup(event, CLOSING_KEYS)) {
@@ -213,9 +223,11 @@ export const createComboBox = createBuilder(
213
223
  internals: { getOptionId },
214
224
  } = createListbox({
215
225
  label: listLabel,
226
+ description: listDescription,
216
227
  multiple,
217
228
  controlled: true,
218
229
  activeOption,
230
+ isExpanded,
219
231
  onSelect: handleSelect,
220
232
  });
221
233
 
@@ -30,7 +30,9 @@ const {
30
30
  elements: { listbox, option: headlessOption },
31
31
  } = createListbox({
32
32
  label: "Test listbox",
33
+ description: "Test description",
33
34
  activeOption,
35
+ isExpanded: true,
34
36
  onSelect: (id) => {
35
37
  selectedOption.value = selectedOption.value === id ? undefined : id;
36
38
  },
@@ -1,4 +1,4 @@
1
- import { computed, ref, unref, watchEffect, type MaybeRef, type Ref } from "vue";
1
+ import { computed, nextTick, ref, unref, watchEffect, type MaybeRef, type Ref } from "vue";
2
2
  import { createId } from "../..";
3
3
  import { createBuilder, type VBindAttributes } from "../../utils/builder";
4
4
  import { useTypeAhead } from "../helpers/useTypeAhead";
@@ -10,6 +10,10 @@ export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends
10
10
  * Aria label for the listbox.
11
11
  */
12
12
  label: MaybeRef<string>;
13
+ /**
14
+ * Aria description for the listbox.
15
+ */
16
+ description?: MaybeRef<string | undefined>;
13
17
  /**
14
18
  * Value of currently (visually) active option.
15
19
  */
@@ -19,6 +23,10 @@ export type CreateListboxOptions<TValue extends ListboxValue, TMultiple extends
19
23
  * This disables keyboard events and makes the listbox not focusable.
20
24
  */
21
25
  controlled?: boolean;
26
+ /**
27
+ * Controls the opened/visible state of the listbox. When expanded the activeOption can be controlled via the keyboard.
28
+ */
29
+ isExpanded?: MaybeRef<boolean>;
22
30
  /**
23
31
  * Whether the listbox is multiselect.
24
32
  */
@@ -77,6 +85,7 @@ export const createListbox = createBuilder(
77
85
  options: CreateListboxOptions<TValue, TMultiple>,
78
86
  ) => {
79
87
  const isMultiselect = computed(() => unref(options.multiple) ?? false);
88
+ const isExpanded = computed(() => unref(options.isExpanded) ?? false);
80
89
 
81
90
  /**
82
91
  * Map for option IDs. key = option value, key = ID for the HTML element
@@ -96,11 +105,18 @@ export const createListbox = createBuilder(
96
105
  const isFocused = ref(false);
97
106
 
98
107
  // scroll currently active option into view if needed
99
- watchEffect(() => {
100
- if (options.activeOption.value == undefined || (!isFocused.value && !options.controlled))
108
+ watchEffect(async () => {
109
+ if (
110
+ !isExpanded.value ||
111
+ options.activeOption.value == undefined ||
112
+ (!isFocused.value && !options.controlled)
113
+ )
101
114
  return;
102
115
  const id = getOptionId(options.activeOption.value);
103
- document.getElementById(id)?.scrollIntoView({ block: "nearest", inline: "nearest" });
116
+
117
+ await nextTick(() => {
118
+ document.getElementById(id)?.scrollIntoView({ block: "end", inline: "nearest" });
119
+ });
104
120
  });
105
121
 
106
122
  const typeAhead = useTypeAhead((inputString) => options.onTypeAhead?.(inputString));
@@ -158,12 +174,14 @@ export const createListbox = createBuilder(
158
174
  role: "listbox",
159
175
  "aria-multiselectable": isMultiselect.value,
160
176
  "aria-label": unref(options.label),
177
+ "aria-description": options.description,
161
178
  tabindex: "-1",
162
179
  }
163
180
  : {
164
181
  role: "listbox",
165
182
  "aria-multiselectable": isMultiselect.value,
166
183
  "aria-label": unref(options.label),
184
+ "aria-description": options.description,
167
185
  tabindex: "0",
168
186
  "aria-activedescendant":
169
187
  options.activeOption.value != undefined
@@ -1,6 +1,6 @@
1
- import { vi, type Awaitable } from "vitest";
1
+ import { vi } from "vitest";
2
2
 
3
- type Callback = () => Awaitable<void>;
3
+ type Callback = () => void | (() => Promise<void>);
4
4
 
5
5
  /**
6
6
  * Mocks the following vue lifecycle functions: