@sit-onyx/headless 1.0.0-beta.3 → 1.0.0-beta.4

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.3",
4
+ "version": "1.0.0-beta.4",
5
5
  "type": "module",
6
6
  "author": "Schwarz IT KG",
7
7
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  import { computed, ref, unref, watchEffect, type MaybeRef, type Ref } from "vue";
2
2
  import { createId } from "../..";
3
- import { createBuilder, type HeadlessElementAttributes } from "../../utils/builder";
3
+ import { createBuilder, type VBindAttributes } from "../../utils/builder";
4
4
  import { useTypeAhead } from "../helpers/useTypeAhead";
5
5
 
6
6
  export type ListboxValue = string | number | boolean;
@@ -152,7 +152,7 @@ export const createListbox = createBuilder(
152
152
  }
153
153
  };
154
154
 
155
- const listbox = computed<HeadlessElementAttributes>(() =>
155
+ const listbox = computed<VBindAttributes>(() =>
156
156
  options.controlled
157
157
  ? {
158
158
  role: "listbox",
@@ -1,5 +1,5 @@
1
- import { computed, ref, type Ref } from "vue";
2
- import { createBuilder } from "../../utils/builder";
1
+ import { computed, type Ref } from "vue";
2
+ import { createBuilder, createElRef } from "../../utils/builder";
3
3
  import { createId } from "../../utils/id";
4
4
  import { debounce } from "../../utils/timer";
5
5
  import { useGlobalEventListener } from "../helpers/useGlobalListener";
@@ -16,7 +16,7 @@ export const createMenuButton = createBuilder(
16
16
  ({ isExpanded, onToggle }: CreateMenuButtonOptions) => {
17
17
  const rootId = createId("menu-button-root");
18
18
  const menuId = createId("menu-button-list");
19
- const menuRef = ref<HTMLElement>();
19
+ const menuRef = createElRef<HTMLElement>();
20
20
  const buttonId = createId("menu-button-button");
21
21
 
22
22
  useGlobalEventListener({
@@ -1,6 +1,6 @@
1
1
  import { computed, onBeforeMount, onBeforeUnmount, ref, unref, type MaybeRef } from "vue";
2
2
  import { createId } from "../..";
3
- import { createBuilder } from "../../utils/builder";
3
+ import { createBuilder, createElRef } from "../../utils/builder";
4
4
  import { useOutsideClick } from "../helpers/useOutsideClick";
5
5
 
6
6
  export type CreateTooltipOptions = {
@@ -22,7 +22,7 @@ export const TOOLTIP_TRIGGERS = ["hover", "click"] as const;
22
22
  export type TooltipTrigger = (typeof TOOLTIP_TRIGGERS)[number];
23
23
 
24
24
  export const createTooltip = createBuilder((options: CreateTooltipOptions) => {
25
- const rootRef = ref<HTMLElement>();
25
+ const rootRef = createElRef<HTMLElement>();
26
26
  const tooltipId = createId("tooltip");
27
27
  const _isVisible = ref(false);
28
28
  let timeout: ReturnType<typeof setTimeout> | undefined;
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./composables/listbox/createListbox";
3
3
  export * from "./composables/menuButton/createMenuButton";
4
4
  export * from "./composables/navigationMenu/createMenu";
5
5
  export * from "./composables/tooltip/createTooltip";
6
+ export * from "./utils/builder";
6
7
  export { createId } from "./utils/id";
7
8
  export { isPrintableCharacter, wasKeyPressed } from "./utils/keyboard";
8
9
  export { debounce } from "./utils/timer";
@@ -1,19 +1,37 @@
1
- import type { ComputedRef, HtmlHTMLAttributes, Ref, VNodeRef } from "vue";
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";
2
11
  import type { IfDefined } from "./types";
3
12
 
4
- export type ElementAttributes = HtmlHTMLAttributes & { ref?: VNodeRef };
13
+ /**
14
+ * `v-bind`able attributes as they are provided by the headless composables.
15
+ * `ref` is restricted to be a `HeadlessElRef` which only can by created through `createElRef`.
16
+ */
17
+ export type VBindAttributes<
18
+ A extends HTMLAttributes = HTMLAttributes,
19
+ E extends Element = Element,
20
+ > = A & {
21
+ ref?: VueTemplateRef<E>;
22
+ };
5
23
 
6
- export type IteratedHeadlessElementFunc<T extends Record<string, unknown>> = (
7
- opts: T,
8
- ) => ElementAttributes;
24
+ export type IteratedHeadlessElementFunc<
25
+ A extends HTMLAttributes,
26
+ T extends Record<string, unknown>,
27
+ > = (opts: T) => VBindAttributes<A>;
9
28
 
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- export type HeadlessElementAttributes = ElementAttributes | IteratedHeadlessElementFunc<any>;
29
+ export type HeadlessElementAttributes<A extends HTMLAttributes> =
30
+ | VBindAttributes<A>
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ | IteratedHeadlessElementFunc<A, any>;
12
33
 
13
- export type HeadlessElements = Record<
14
- string,
15
- HeadlessElementAttributes | ComputedRef<HeadlessElementAttributes>
16
- >;
34
+ export type HeadlessElements = Record<string, MaybeRef<HeadlessElementAttributes<HTMLAttributes>>>;
17
35
 
18
36
  export type HeadlessState = Record<string, Ref>;
19
37
 
@@ -28,6 +46,39 @@ export type HeadlessComposable<
28
46
 
29
47
  /**
30
48
  * We use this identity function to ensure the correct typings of the headless composables
49
+ * @example
50
+ * ```ts
51
+ * export const createTooltip = createBuilder(({ initialVisible }: CreateTooltipOptions) => {
52
+ * const tooltipId = createId("tooltip");
53
+ * const isVisible = ref(initialVisible);
54
+ *
55
+ * const hoverEvents = {
56
+ * onMouseover: () => (isVisible.value = true),
57
+ * onMouseout: () => (isVisible.value = false),
58
+ * onFocusin: () => (isVisible.value = true),
59
+ * onFocusout: () => (isVisible.value = false),
60
+ * };
61
+ *
62
+ * return {
63
+ * elements: {
64
+ * trigger: {
65
+ * "aria-describedby": tooltipId,
66
+ * ...hoverEvents,
67
+ * },
68
+ * tooltip: {
69
+ * role: "tooltip",
70
+ * id: tooltipId,
71
+ * tabindex: "-1",
72
+ * ...hoverEvents,
73
+ * },
74
+ * },
75
+ * state: {
76
+ * isVisible,
77
+ * },
78
+ * };
79
+ * });
80
+ *
81
+ * ```
31
82
  */
32
83
  export const createBuilder = <
33
84
  Args extends unknown[] = unknown[],
@@ -37,3 +88,47 @@ export const createBuilder = <
37
88
  >(
38
89
  builder: (...args: Args) => HeadlessComposable<Elements, State, Internals>,
39
90
  ) => builder;
91
+
92
+ type VueTemplateRefElement<E extends Element> = E | (ComponentPublicInstance & { $el: E }) | null;
93
+ type VueTemplateRef<E extends Element> = Ref<VueTemplateRefElement<E>>;
94
+
95
+ declare const HeadlessElRefSymbol: unique symbol;
96
+ type HeadlessElRef<E extends Element> = WritableComputedRef<E> & {
97
+ /**
98
+ * type differentiator
99
+ * ensures that only `createElRef` can be used for headless element ref bindings
100
+ */
101
+ [HeadlessElRefSymbol]: true;
102
+ };
103
+
104
+ /**
105
+ * Creates a special writeable computed that references a DOM Element.
106
+ * Vue Component references will be unwrapped.
107
+ * @example
108
+ * ```ts
109
+ * createBuilder() => {
110
+ * const buttonRef = createElRef<HtmlButtonElement>();
111
+ * return {
112
+ * elements: {
113
+ * button: {
114
+ * ref: buttonRef,
115
+ * },
116
+ * }
117
+ * };
118
+ * });
119
+ * ```
120
+ */
121
+ export function createElRef<E extends Element>(): HeadlessElRef<E>;
122
+ export function createElRef<
123
+ E extends Element,
124
+ V extends VueTemplateRefElement<E> = VueTemplateRefElement<E>,
125
+ >() {
126
+ const elementRef = shallowRef<E>();
127
+
128
+ return computed({
129
+ set: (element: V) => {
130
+ elementRef.value = element != null && "$el" in element ? element.$el : (element as E);
131
+ },
132
+ get: () => elementRef.value,
133
+ } as WritableComputedOptions<E>);
134
+ }