@rokkit/actions 1.0.0-next.153 → 1.0.0-next.154
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/index.d.ts +1 -0
- package/dist/navigator.d.ts +2 -1
- package/dist/tooltip.svelte.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/navigator.js +17 -1
- package/src/tooltip.svelte.js +149 -0
package/dist/index.d.ts
CHANGED
|
@@ -15,4 +15,5 @@ export { reveal } from "./reveal.svelte.js";
|
|
|
15
15
|
export { hoverLift } from "./hover-lift.svelte.js";
|
|
16
16
|
export { magnetic } from "./magnetic.svelte.js";
|
|
17
17
|
export { ripple } from "./ripple.svelte.js";
|
|
18
|
+
export { tooltip } from "./tooltip.svelte.js";
|
|
18
19
|
export { buildKeymap, resolveAction, ACTIONS } from "./keymap.js";
|
package/dist/navigator.d.ts
CHANGED
|
@@ -2,12 +2,13 @@ export class Navigator {
|
|
|
2
2
|
/**
|
|
3
3
|
* @param {HTMLElement} root
|
|
4
4
|
* @param {import('@rokkit/states').Wrapper} wrapper
|
|
5
|
-
* @param {{ orientation?: string, dir?: string, collapsible?: boolean }} [options]
|
|
5
|
+
* @param {{ orientation?: string, dir?: string, collapsible?: boolean, containScroll?: boolean }} [options]
|
|
6
6
|
*/
|
|
7
7
|
constructor(root: HTMLElement, wrapper: import("@rokkit/states").Wrapper, options?: {
|
|
8
8
|
orientation?: string;
|
|
9
9
|
dir?: string;
|
|
10
10
|
collapsible?: boolean;
|
|
11
|
+
containScroll?: boolean;
|
|
11
12
|
});
|
|
12
13
|
destroy(): void;
|
|
13
14
|
#private;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function tooltip(node: any, options?: {}): void;
|
package/package.json
CHANGED
package/src/index.js
CHANGED
package/src/navigator.js
CHANGED
|
@@ -70,6 +70,7 @@ export class Navigator {
|
|
|
70
70
|
#root
|
|
71
71
|
#wrapper
|
|
72
72
|
#keymap
|
|
73
|
+
#containScroll
|
|
73
74
|
|
|
74
75
|
// Typeahead state
|
|
75
76
|
#buffer = ''
|
|
@@ -78,17 +79,21 @@ export class Navigator {
|
|
|
78
79
|
/**
|
|
79
80
|
* @param {HTMLElement} root
|
|
80
81
|
* @param {import('@rokkit/states').Wrapper} wrapper
|
|
81
|
-
* @param {{ orientation?: string, dir?: string, collapsible?: boolean }} [options]
|
|
82
|
+
* @param {{ orientation?: string, dir?: string, collapsible?: boolean, containScroll?: boolean }} [options]
|
|
82
83
|
*/
|
|
83
84
|
constructor(root, wrapper, options = {}) {
|
|
84
85
|
this.#root = root
|
|
85
86
|
this.#wrapper = wrapper
|
|
86
87
|
this.#keymap = buildKeymap(options)
|
|
88
|
+
this.#containScroll = options.containScroll ?? false
|
|
87
89
|
|
|
88
90
|
root.addEventListener('keydown', this.#onKeydown)
|
|
89
91
|
root.addEventListener('click', this.#onClick)
|
|
90
92
|
root.addEventListener('focusin', this.#onFocusin)
|
|
91
93
|
root.addEventListener('focusout', this.#onFocusout)
|
|
94
|
+
if (this.#containScroll) {
|
|
95
|
+
root.addEventListener('wheel', this.#onWheel, { passive: false })
|
|
96
|
+
}
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
destroy() {
|
|
@@ -96,6 +101,9 @@ export class Navigator {
|
|
|
96
101
|
this.#root.removeEventListener('click', this.#onClick)
|
|
97
102
|
this.#root.removeEventListener('focusin', this.#onFocusin)
|
|
98
103
|
this.#root.removeEventListener('focusout', this.#onFocusout)
|
|
104
|
+
if (this.#containScroll) {
|
|
105
|
+
this.#root.removeEventListener('wheel', this.#onWheel)
|
|
106
|
+
}
|
|
99
107
|
this.#clearTypeahead()
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -161,6 +169,14 @@ export class Navigator {
|
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
// ─── Wheel ──────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
#onWheel = (/** @type {WheelEvent} */ event) => {
|
|
175
|
+
// Prevent the wheel event from bubbling to parent scroll containers.
|
|
176
|
+
// Native scroll chaining is handled via CSS overscroll-behavior: contain.
|
|
177
|
+
event.stopPropagation()
|
|
178
|
+
}
|
|
179
|
+
|
|
164
180
|
// ─── Focusout ───────────────────────────────────────────────────────────
|
|
165
181
|
|
|
166
182
|
#onFocusout = (/** @type {FocusEvent} */ event) => {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip action — attaches an accessible tooltip to any element.
|
|
3
|
+
*
|
|
4
|
+
* Triggers on mouseenter (with configurable delay) and focusin.
|
|
5
|
+
* Hides on mouseleave, focusout, and Escape.
|
|
6
|
+
* Auto-flips position when the preferred side overflows the viewport.
|
|
7
|
+
*
|
|
8
|
+
* @param {HTMLElement} node
|
|
9
|
+
* @param {object} [options]
|
|
10
|
+
* @param {string} [options.content=''] Tooltip text
|
|
11
|
+
* @param {'top'|'bottom'|'left'|'right'} [options.position='top'] Preferred position
|
|
12
|
+
* @param {number} [options.delay=300] Show delay in ms
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const GAP = 6
|
|
16
|
+
|
|
17
|
+
function uid() {
|
|
18
|
+
return `tt-${Math.random().toString(36).slice(2, 9)}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPositionedAncestor(el) {
|
|
22
|
+
let parent = el.parentElement
|
|
23
|
+
while (parent && parent !== document.body) {
|
|
24
|
+
const pos = getComputedStyle(parent).position
|
|
25
|
+
if (['relative', 'absolute', 'fixed', 'sticky'].includes(pos)) return parent
|
|
26
|
+
parent = parent.parentElement
|
|
27
|
+
}
|
|
28
|
+
return document.body
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveFlip(triggerRect, tooltipRect, preferred) {
|
|
32
|
+
const vw = window.innerWidth
|
|
33
|
+
const vh = window.innerHeight
|
|
34
|
+
const fits = {
|
|
35
|
+
top: triggerRect.top >= tooltipRect.height + GAP,
|
|
36
|
+
bottom: triggerRect.bottom + tooltipRect.height + GAP <= vh,
|
|
37
|
+
left: triggerRect.left >= tooltipRect.width + GAP,
|
|
38
|
+
right: triggerRect.right + tooltipRect.width + GAP <= vw
|
|
39
|
+
}
|
|
40
|
+
if (fits[preferred]) return preferred
|
|
41
|
+
const flip = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }
|
|
42
|
+
if (fits[flip[preferred]]) return flip[preferred]
|
|
43
|
+
return Object.keys(fits).find((p) => fits[p]) ?? preferred
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function positionTooltip(trigger, tooltipEl, preferred) {
|
|
47
|
+
const triggerRect = trigger.getBoundingClientRect()
|
|
48
|
+
const container = tooltipEl.parentElement
|
|
49
|
+
const containerRect = container.getBoundingClientRect()
|
|
50
|
+
|
|
51
|
+
// Measure tooltip without layout thrash
|
|
52
|
+
tooltipEl.style.visibility = 'hidden'
|
|
53
|
+
tooltipEl.style.position = 'absolute'
|
|
54
|
+
const tooltipRect = tooltipEl.getBoundingClientRect()
|
|
55
|
+
tooltipEl.style.visibility = ''
|
|
56
|
+
|
|
57
|
+
const pos = resolveFlip(triggerRect, tooltipRect, preferred)
|
|
58
|
+
tooltipEl.setAttribute('data-tooltip-position', pos)
|
|
59
|
+
|
|
60
|
+
let top, left
|
|
61
|
+
switch (pos) {
|
|
62
|
+
case 'top':
|
|
63
|
+
top = triggerRect.top - containerRect.top - tooltipRect.height - GAP
|
|
64
|
+
left = triggerRect.left - containerRect.left + (triggerRect.width - tooltipRect.width) / 2
|
|
65
|
+
break
|
|
66
|
+
case 'bottom':
|
|
67
|
+
top = triggerRect.bottom - containerRect.top + GAP
|
|
68
|
+
left = triggerRect.left - containerRect.left + (triggerRect.width - tooltipRect.width) / 2
|
|
69
|
+
break
|
|
70
|
+
case 'left':
|
|
71
|
+
top = triggerRect.top - containerRect.top + (triggerRect.height - tooltipRect.height) / 2
|
|
72
|
+
left = triggerRect.left - containerRect.left - tooltipRect.width - GAP
|
|
73
|
+
break
|
|
74
|
+
case 'right':
|
|
75
|
+
top = triggerRect.top - containerRect.top + (triggerRect.height - tooltipRect.height) / 2
|
|
76
|
+
left = triggerRect.right - containerRect.left + GAP
|
|
77
|
+
break
|
|
78
|
+
default:
|
|
79
|
+
top = 0
|
|
80
|
+
left = 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tooltipEl.style.top = `${top}px`
|
|
84
|
+
tooltipEl.style.left = `${left}px`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function tooltip(node, options = {}) {
|
|
88
|
+
$effect(() => {
|
|
89
|
+
const opts = { content: '', position: 'top', delay: 300, ...options }
|
|
90
|
+
const id = uid()
|
|
91
|
+
|
|
92
|
+
const el = document.createElement('div')
|
|
93
|
+
el.setAttribute('data-tooltip-content', '')
|
|
94
|
+
el.setAttribute('data-tooltip-position', opts.position)
|
|
95
|
+
el.setAttribute('data-tooltip-visible', 'false')
|
|
96
|
+
el.id = id
|
|
97
|
+
el.setAttribute('role', 'tooltip')
|
|
98
|
+
el.textContent = opts.content
|
|
99
|
+
|
|
100
|
+
node.setAttribute('data-tooltip-trigger', '')
|
|
101
|
+
node.setAttribute('aria-describedby', id)
|
|
102
|
+
|
|
103
|
+
const container = getPositionedAncestor(node)
|
|
104
|
+
container.appendChild(el)
|
|
105
|
+
|
|
106
|
+
let timer = null
|
|
107
|
+
|
|
108
|
+
function show() {
|
|
109
|
+
positionTooltip(node, el, opts.position)
|
|
110
|
+
el.setAttribute('data-tooltip-visible', 'true')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hide() {
|
|
114
|
+
clearTimeout(timer)
|
|
115
|
+
el.setAttribute('data-tooltip-visible', 'false')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onMouseEnter() {
|
|
119
|
+
timer = setTimeout(show, opts.delay)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function onMouseLeave() {
|
|
123
|
+
clearTimeout(timer)
|
|
124
|
+
hide()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function onKeydown(e) {
|
|
128
|
+
if (e.key === 'Escape') hide()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
node.addEventListener('mouseenter', onMouseEnter)
|
|
132
|
+
node.addEventListener('mouseleave', onMouseLeave)
|
|
133
|
+
node.addEventListener('focusin', show)
|
|
134
|
+
node.addEventListener('focusout', hide)
|
|
135
|
+
node.addEventListener('keydown', onKeydown)
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
clearTimeout(timer)
|
|
139
|
+
node.removeAttribute('data-tooltip-trigger')
|
|
140
|
+
node.removeAttribute('aria-describedby')
|
|
141
|
+
node.removeEventListener('mouseenter', onMouseEnter)
|
|
142
|
+
node.removeEventListener('mouseleave', onMouseLeave)
|
|
143
|
+
node.removeEventListener('focusin', show)
|
|
144
|
+
node.removeEventListener('focusout', hide)
|
|
145
|
+
node.removeEventListener('keydown', onKeydown)
|
|
146
|
+
el.remove()
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|