@zag-js/tooltip 0.9.1 → 0.10.0

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/tooltip",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Core logic for the tooltip widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -17,7 +17,8 @@
17
17
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/tooltip",
18
18
  "sideEffects": false,
19
19
  "files": [
20
- "dist/**/*"
20
+ "dist",
21
+ "src"
21
22
  ],
22
23
  "publishConfig": {
23
24
  "access": "public"
@@ -26,14 +27,14 @@
26
27
  "url": "https://github.com/chakra-ui/zag/issues"
27
28
  },
28
29
  "dependencies": {
29
- "@zag-js/anatomy": "0.9.1",
30
- "@zag-js/core": "0.9.1",
31
- "@zag-js/popper": "0.9.1",
32
- "@zag-js/dom-query": "0.9.1",
33
- "@zag-js/dom-event": "0.9.1",
34
- "@zag-js/utils": "0.9.1",
35
- "@zag-js/visually-hidden": "0.9.1",
36
- "@zag-js/types": "0.9.1"
30
+ "@zag-js/anatomy": "0.10.0",
31
+ "@zag-js/core": "0.10.0",
32
+ "@zag-js/popper": "0.10.0",
33
+ "@zag-js/dom-query": "0.10.0",
34
+ "@zag-js/dom-event": "0.10.0",
35
+ "@zag-js/utils": "0.10.0",
36
+ "@zag-js/visually-hidden": "0.10.0",
37
+ "@zag-js/types": "0.10.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "clean-package": "2.2.0"
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { anatomy } from "./tooltip.anatomy"
2
+ export { connect } from "./tooltip.connect"
3
+ export { machine } from "./tooltip.machine"
4
+ export type { UserDefinedContext as Context } from "./tooltip.types"
@@ -0,0 +1,4 @@
1
+ import { createAnatomy } from "@zag-js/anatomy"
2
+
3
+ export const anatomy = createAnatomy("tooltip").parts("trigger", "arrow", "arrowTip", "positioner", "content", "label")
4
+ export const parts = anatomy.build()
@@ -0,0 +1,136 @@
1
+ import { dataAttr } from "@zag-js/dom-query"
2
+ import { getPlacementStyles, PositioningOptions } from "@zag-js/popper"
3
+ import type { NormalizeProps, PropTypes } from "@zag-js/types"
4
+ import { visuallyHiddenStyle } from "@zag-js/visually-hidden"
5
+ import { parts } from "./tooltip.anatomy"
6
+ import { dom } from "./tooltip.dom"
7
+ import { store } from "./tooltip.store"
8
+ import type { Send, State } from "./tooltip.types"
9
+
10
+ export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
11
+ const id = state.context.id
12
+ const hasAriaLabel = state.context.hasAriaLabel
13
+
14
+ const isOpen = state.hasTag("open")
15
+
16
+ const triggerId = dom.getTriggerId(state.context)
17
+ const contentId = dom.getContentId(state.context)
18
+
19
+ const isDisabled = state.context.disabled
20
+
21
+ const popperStyles = getPlacementStyles({
22
+ placement: state.context.currentPlacement,
23
+ })
24
+
25
+ return {
26
+ /**
27
+ * Whether the tooltip is open.
28
+ */
29
+ isOpen,
30
+ /**
31
+ * Function to open the tooltip.
32
+ */
33
+ open() {
34
+ send("OPEN")
35
+ },
36
+ /**
37
+ * Function to close the tooltip.
38
+ */
39
+ close() {
40
+ send("CLOSE")
41
+ },
42
+ /**
43
+ * Returns the animation state of the tooltip.
44
+ */
45
+ getAnimationState() {
46
+ return {
47
+ enter: store.prevId === null && id === store.id,
48
+ exit: store.id === null,
49
+ }
50
+ },
51
+ /**
52
+ * Function to reposition the popover
53
+ */
54
+ setPositioning(options: Partial<PositioningOptions> = {}) {
55
+ send({ type: "SET_POSITIONING", options })
56
+ },
57
+
58
+ triggerProps: normalize.button({
59
+ ...parts.trigger.attrs,
60
+ id: triggerId,
61
+ "data-expanded": dataAttr(isOpen),
62
+ "aria-describedby": isOpen ? contentId : undefined,
63
+ onClick() {
64
+ send("CLICK")
65
+ },
66
+ onFocus() {
67
+ send("FOCUS")
68
+ },
69
+ onBlur() {
70
+ if (id === store.id) {
71
+ send("BLUR")
72
+ }
73
+ },
74
+ onPointerDown() {
75
+ if (isDisabled) return
76
+ if (id === store.id) {
77
+ send("POINTER_DOWN")
78
+ }
79
+ },
80
+ onPointerMove() {
81
+ if (isDisabled) return
82
+ send("POINTER_ENTER")
83
+ },
84
+ onPointerLeave() {
85
+ if (isDisabled) return
86
+ send("POINTER_LEAVE")
87
+ },
88
+ onPointerCancel() {
89
+ if (isDisabled) return
90
+ send("POINTER_LEAVE")
91
+ },
92
+ }),
93
+
94
+ arrowProps: normalize.element({
95
+ id: dom.getArrowId(state.context),
96
+ ...parts.arrow.attrs,
97
+ style: popperStyles.arrow,
98
+ }),
99
+
100
+ arrowTipProps: normalize.element({
101
+ ...parts.arrowTip.attrs,
102
+ style: popperStyles.arrowTip,
103
+ }),
104
+
105
+ positionerProps: normalize.element({
106
+ id: dom.getPositionerId(state.context),
107
+ ...parts.positioner.attrs,
108
+ style: popperStyles.floating,
109
+ }),
110
+
111
+ contentProps: normalize.element({
112
+ ...parts.content.attrs,
113
+ hidden: !isOpen,
114
+ role: hasAriaLabel ? undefined : "tooltip",
115
+ id: hasAriaLabel ? undefined : contentId,
116
+ "data-placement": state.context.currentPlacement,
117
+ onPointerEnter() {
118
+ send("TOOLTIP_POINTER_ENTER")
119
+ },
120
+ onPointerLeave() {
121
+ send("TOOLTIP_POINTER_LEAVE")
122
+ },
123
+ style: {
124
+ pointerEvents: state.context.interactive ? "auto" : "none",
125
+ },
126
+ }),
127
+
128
+ labelProps: normalize.element({
129
+ ...parts.label.attrs,
130
+ id: contentId,
131
+ role: "tooltip",
132
+ style: visuallyHiddenStyle,
133
+ children: state.context["aria-label"],
134
+ }),
135
+ }
136
+ }
@@ -0,0 +1,15 @@
1
+ import { createScope, getScrollParent } from "@zag-js/dom-query"
2
+ import type { MachineContext as Ctx } from "./tooltip.types"
3
+
4
+ export const dom = createScope({
5
+ getTriggerId: (ctx: Ctx) => ctx.ids?.trigger ?? `tooltip:${ctx.id}:trigger`,
6
+ getContentId: (ctx: Ctx) => ctx.ids?.content ?? `tooltip:${ctx.id}:content`,
7
+ getArrowId: (ctx: Ctx) => ctx.ids?.arrow ?? `tooltip:${ctx.id}:arrow`,
8
+ getPositionerId: (ctx: Ctx) => ctx.ids?.positioner ?? `tooltip:${ctx.id}:popper`,
9
+
10
+ getTriggerEl: (ctx: Ctx) => dom.getById(ctx, dom.getTriggerId(ctx)),
11
+ getContentEl: (ctx: Ctx) => dom.getById(ctx, dom.getContentId(ctx)),
12
+ getPositionerEl: (ctx: Ctx) => dom.getById(ctx, dom.getPositionerId(ctx)),
13
+ getArrowEl: (ctx: Ctx) => dom.getById(ctx, dom.getArrowId(ctx)),
14
+ getScrollParent: (ctx: Ctx) => getScrollParent(dom.getTriggerEl(ctx)!),
15
+ })
@@ -0,0 +1,238 @@
1
+ import { createMachine, subscribe } from "@zag-js/core"
2
+ import { addDomEvent } from "@zag-js/dom-event"
3
+ import { getScrollParents, isHTMLElement, isSafari } from "@zag-js/dom-query"
4
+ import { getPlacement } from "@zag-js/popper"
5
+ import { compact } from "@zag-js/utils"
6
+ import { dom } from "./tooltip.dom"
7
+ import { store } from "./tooltip.store"
8
+ import type { MachineContext, MachineState, UserDefinedContext } from "./tooltip.types"
9
+
10
+ export function machine(userContext: UserDefinedContext) {
11
+ const ctx = compact(userContext)
12
+ return createMachine<MachineContext, MachineState>(
13
+ {
14
+ id: "tooltip",
15
+ initial: "closed",
16
+
17
+ context: {
18
+ openDelay: 1000,
19
+ closeDelay: 500,
20
+ closeOnPointerDown: true,
21
+ closeOnEsc: true,
22
+ interactive: true,
23
+ currentPlacement: undefined,
24
+ ...ctx,
25
+ positioning: {
26
+ placement: "bottom",
27
+ ...ctx.positioning,
28
+ },
29
+ },
30
+
31
+ computed: {
32
+ hasAriaLabel: (ctx) => !!ctx["aria-label"],
33
+ },
34
+
35
+ watch: {
36
+ disabled: ["closeIfDisabled"],
37
+ open: ["toggleVisibility"],
38
+ },
39
+
40
+ on: {
41
+ OPEN: "open",
42
+ CLOSE: "closed",
43
+ },
44
+
45
+ states: {
46
+ closed: {
47
+ tags: ["closed"],
48
+ entry: ["clearGlobalId", "invokeOnClose"],
49
+ on: {
50
+ FOCUS: "open",
51
+ POINTER_ENTER: [
52
+ {
53
+ guard: "noVisibleTooltip",
54
+ target: "opening",
55
+ },
56
+ { target: "open" },
57
+ ],
58
+ },
59
+ },
60
+
61
+ opening: {
62
+ tags: ["closed"],
63
+ activities: ["trackScroll", "trackPointerlockChange"],
64
+ after: {
65
+ OPEN_DELAY: "open",
66
+ },
67
+ on: {
68
+ POINTER_LEAVE: "closed",
69
+ BLUR: "closed",
70
+ SCROLL: "closed",
71
+ POINTER_LOCK_CHANGE: "closed",
72
+ POINTER_DOWN: {
73
+ guard: "closeOnPointerDown",
74
+ target: "closed",
75
+ },
76
+ },
77
+ },
78
+
79
+ open: {
80
+ tags: ["open"],
81
+ activities: [
82
+ "trackEscapeKey",
83
+ "trackDisabledTriggerOnSafari",
84
+ "trackScroll",
85
+ "trackPointerlockChange",
86
+ "trackPositioning",
87
+ ],
88
+ entry: ["setGlobalId", "invokeOnOpen"],
89
+ on: {
90
+ POINTER_LEAVE: [
91
+ {
92
+ guard: "isVisible",
93
+ target: "closing",
94
+ },
95
+ { target: "closed" },
96
+ ],
97
+ BLUR: "closed",
98
+ ESCAPE: "closed",
99
+ SCROLL: "closed",
100
+ POINTER_LOCK_CHANGE: "closed",
101
+ TOOLTIP_POINTER_LEAVE: {
102
+ guard: "isInteractive",
103
+ target: "closing",
104
+ },
105
+ POINTER_DOWN: {
106
+ guard: "closeOnPointerDown",
107
+ target: "closed",
108
+ },
109
+ CLICK: "closed",
110
+ SET_POSITIONING: {
111
+ actions: "setPositioning",
112
+ },
113
+ },
114
+ },
115
+
116
+ closing: {
117
+ tags: ["open"],
118
+ activities: ["trackStore", "trackPositioning"],
119
+ after: {
120
+ CLOSE_DELAY: "closed",
121
+ },
122
+ on: {
123
+ FORCE_CLOSE: "closed",
124
+ POINTER_ENTER: "open",
125
+ TOOLTIP_POINTER_ENTER: {
126
+ guard: "isInteractive",
127
+ target: "open",
128
+ },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ {
134
+ activities: {
135
+ trackPositioning(ctx) {
136
+ ctx.currentPlacement = ctx.positioning.placement
137
+ const getPositionerEl = () => dom.getPositionerEl(ctx)
138
+ return getPlacement(dom.getTriggerEl(ctx), getPositionerEl, {
139
+ ...ctx.positioning,
140
+ defer: true,
141
+ onComplete(data) {
142
+ ctx.currentPlacement = data.placement
143
+ },
144
+ onCleanup() {
145
+ ctx.currentPlacement = undefined
146
+ },
147
+ })
148
+ },
149
+ trackPointerlockChange(ctx, _evt, { send }) {
150
+ const onChange = () => send("POINTER_LOCK_CHANGE")
151
+ return addDomEvent(dom.getDoc(ctx), "pointerlockchange", onChange, false)
152
+ },
153
+ trackScroll(ctx, _evt, { send }) {
154
+ const trigger = dom.getTriggerEl(ctx)
155
+ if (!trigger) return
156
+ const cleanups = getScrollParents(trigger).map((el) => {
157
+ const opts = { passive: true, capture: true } as const
158
+ return addDomEvent(el, "scroll", () => send("SCROLL"), opts)
159
+ })
160
+ return () => {
161
+ cleanups.forEach((fn) => fn?.())
162
+ }
163
+ },
164
+ trackStore(ctx, _evt, { send }) {
165
+ return subscribe(store, () => {
166
+ if (store.id !== ctx.id) {
167
+ send("FORCE_CLOSE")
168
+ }
169
+ })
170
+ },
171
+ trackDisabledTriggerOnSafari(ctx, _evt, { send }) {
172
+ if (!isSafari()) return
173
+ const doc = dom.getDoc(ctx)
174
+ return addDomEvent(doc, "pointermove", (event) => {
175
+ const selector = "[data-part=trigger][data-expanded]"
176
+ if (isHTMLElement(event.target) && event.target.closest(selector)) return
177
+ send("POINTER_LEAVE")
178
+ })
179
+ },
180
+ trackEscapeKey(ctx, _evt, { send }) {
181
+ if (!ctx.closeOnEsc) return
182
+ const doc = dom.getDoc(ctx)
183
+ return addDomEvent(doc, "keydown", (event) => {
184
+ if (event.key === "Escape") {
185
+ send("ESCAPE")
186
+ }
187
+ })
188
+ },
189
+ },
190
+ actions: {
191
+ setGlobalId(ctx) {
192
+ store.setId(ctx.id)
193
+ },
194
+ clearGlobalId(ctx) {
195
+ if (ctx.id === store.id) {
196
+ store.setId(null)
197
+ }
198
+ },
199
+ invokeOnOpen(ctx, evt) {
200
+ const omit = ["TOOLTIP_POINTER_ENTER", "POINTER_ENTER"]
201
+ if (!omit.includes(evt.type)) {
202
+ ctx.onOpen?.()
203
+ }
204
+ },
205
+ invokeOnClose(ctx) {
206
+ ctx.onClose?.()
207
+ },
208
+ closeIfDisabled(ctx, _evt, { send }) {
209
+ if (ctx.disabled) {
210
+ send("CLOSE")
211
+ }
212
+ },
213
+ setPositioning(ctx, evt) {
214
+ const getPositionerEl = () => dom.getPositionerEl(ctx)
215
+ getPlacement(dom.getTriggerEl(ctx), getPositionerEl, {
216
+ ...ctx.positioning,
217
+ ...evt.options,
218
+ defer: true,
219
+ listeners: false,
220
+ })
221
+ },
222
+ toggleVisibility(ctx, _evt, { send }) {
223
+ send({ type: ctx.open ? "OPEN" : "CLOSE", src: "controlled" })
224
+ },
225
+ },
226
+ guards: {
227
+ closeOnPointerDown: (ctx) => ctx.closeOnPointerDown,
228
+ noVisibleTooltip: () => store.id === null,
229
+ isVisible: (ctx) => ctx.id === store.id,
230
+ isInteractive: (ctx) => ctx.interactive,
231
+ },
232
+ delays: {
233
+ OPEN_DELAY: (ctx) => ctx.openDelay,
234
+ CLOSE_DELAY: (ctx) => ctx.closeDelay,
235
+ },
236
+ },
237
+ )
238
+ }
@@ -0,0 +1,18 @@
1
+ import { proxy } from "@zag-js/core"
2
+
3
+ type Id = string | null
4
+
5
+ type Store = {
6
+ id: Id
7
+ prevId: Id
8
+ setId: (val: Id) => void
9
+ }
10
+
11
+ export const store = proxy<Store>({
12
+ id: null,
13
+ prevId: null,
14
+ setId(val) {
15
+ this.prevId = this.id
16
+ this.id = val
17
+ },
18
+ })
@@ -0,0 +1,95 @@
1
+ import type { StateMachine as S } from "@zag-js/core"
2
+ import type { Placement, PositioningOptions } from "@zag-js/popper"
3
+ import type { CommonProperties, RequiredBy, RootProperties } from "@zag-js/types"
4
+
5
+ type ElementIds = Partial<{
6
+ trigger: string
7
+ content: string
8
+ arrow: string
9
+ positioner: string
10
+ }>
11
+
12
+ type PublicContext = CommonProperties & {
13
+ /**
14
+ * The ids of the elements in the tooltip. Useful for composition.
15
+ */
16
+ ids?: ElementIds
17
+ /**
18
+ * The `id` of the tooltip.
19
+ */
20
+ id: string
21
+ /**
22
+ * The open delay of the tooltip.
23
+ */
24
+ openDelay: number
25
+ /**
26
+ * The close delay of the tooltip.
27
+ */
28
+ closeDelay: number
29
+ /**
30
+ * Whether to close the tooltip on pointerdown.
31
+ */
32
+ closeOnPointerDown: boolean
33
+ /**
34
+ * Whether to close the tooltip when the Escape key is pressed.
35
+ */
36
+ closeOnEsc?: boolean
37
+ /**
38
+ * Whether the tooltip's content is interactive.
39
+ * In this mode, the tooltip will remain open when user hovers over the content.
40
+ * @see https://www.w3.org/TR/WCAG21/#content-on-hover-or-focus
41
+ */
42
+ interactive: boolean
43
+ /**
44
+ * Function called when the tooltip is opened.
45
+ */
46
+ onOpen?: VoidFunction
47
+ /**
48
+ * Function called when the tooltip is closed.
49
+ */
50
+ onClose?: VoidFunction
51
+ /**
52
+ * Custom label for the tooltip.
53
+ */
54
+ "aria-label"?: string
55
+ /**
56
+ * The user provided options used to position the popover content
57
+ */
58
+ positioning: PositioningOptions
59
+ /**
60
+ * Whether the tooltip is disabled
61
+ */
62
+ disabled?: boolean
63
+ /**
64
+ * Whether the tooltip is open
65
+ */
66
+ open?: boolean
67
+ }
68
+
69
+ export type UserDefinedContext = RequiredBy<PublicContext, "id">
70
+
71
+ type ComputedContext = Readonly<{
72
+ /**
73
+ * @computed Whether an `aria-label` is set.
74
+ */
75
+ readonly hasAriaLabel: boolean
76
+ }>
77
+
78
+ type PrivateContext = RootProperties & {
79
+ /**
80
+ * @internal
81
+ * The computed placement of the tooltip.
82
+ */
83
+ currentPlacement?: Placement
84
+ }
85
+
86
+ export type MachineContext = PublicContext & ComputedContext & PrivateContext
87
+
88
+ export type MachineState = {
89
+ value: "opening" | "open" | "closing" | "closed"
90
+ tags: "open" | "closed"
91
+ }
92
+
93
+ export type State = S.State<MachineContext, MachineState>
94
+
95
+ export type Send = S.Send<S.AnyEventObject>