ajo-ui 0.1.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/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
package/src/popup.ts
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import type { Host } from 'ajo-cloves'
|
|
2
|
+
import { callRef, controlled, dismiss, dom, hover, id, resize } from 'ajo-cloves'
|
|
3
|
+
import { closePopover, openPopover, popoverOpen } from './native'
|
|
4
|
+
import { position, type PositionProfile, type PositionReference } from './position'
|
|
5
|
+
import { popupStyle, type PopupPosition } from './utils'
|
|
6
|
+
|
|
7
|
+
type ContentAttrsOptions<Element extends HTMLElement> = {
|
|
8
|
+
id?: unknown
|
|
9
|
+
open?: boolean
|
|
10
|
+
ref?: unknown
|
|
11
|
+
setContent?: (element: Element | null) => void
|
|
12
|
+
style?: unknown
|
|
13
|
+
tabindex?: number | string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Builds native/manual popup attrs and a composed content ref. */
|
|
17
|
+
export const contentAttrs = <Element extends HTMLElement>(options: ContentAttrsOptions<Element>): Record<string, unknown> => {
|
|
18
|
+
const { id, open, ref, setContent, style, tabindex } = options
|
|
19
|
+
const attrs: Record<string, unknown> = {
|
|
20
|
+
popover: 'manual',
|
|
21
|
+
style: typeof style === 'string' ? style : popupStyle(),
|
|
22
|
+
}
|
|
23
|
+
if ('id' in options) attrs.id = id
|
|
24
|
+
if ('open' in options) attrs['data-state'] = open ? 'open' : 'closed'
|
|
25
|
+
if ('tabindex' in options) attrs.tabindex = tabindex
|
|
26
|
+
if ('ref' in options || 'setContent' in options) {
|
|
27
|
+
attrs.ref = (element: Element | null) => {
|
|
28
|
+
setContent?.(element)
|
|
29
|
+
callRef(ref, element)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return attrs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PopupView<Trigger extends HTMLElement = HTMLElement, Content extends HTMLElement = HTMLDivElement> = {
|
|
36
|
+
readonly open: boolean
|
|
37
|
+
readonly trigger: Trigger | null
|
|
38
|
+
readonly content: Content | null
|
|
39
|
+
readonly reference: PositionReference | null
|
|
40
|
+
readonly triggerId: string
|
|
41
|
+
readonly contentId: string
|
|
42
|
+
/** Adopts the trigger's rendered id synchronously, or restores the generated id. */
|
|
43
|
+
adoptTriggerId(id?: unknown): string
|
|
44
|
+
/** Composes caller declarations with the live styles owned by popup positioning. */
|
|
45
|
+
contentStyle(style?: unknown): string
|
|
46
|
+
/** Owns the internal arrow probe's stable ref and live positioning style. */
|
|
47
|
+
arrowAttrs(): {
|
|
48
|
+
ref: (element: HTMLElement | null) => void
|
|
49
|
+
style: string
|
|
50
|
+
}
|
|
51
|
+
/** Reads controlled state and current root positioning once per render. */
|
|
52
|
+
sync(open: boolean | null | undefined, position?: PopupPosition): boolean
|
|
53
|
+
setOpen(open: boolean, event?: Event): void
|
|
54
|
+
/** Seeds uncontrolled state without notifying onOpenChange. */
|
|
55
|
+
init(open: boolean): void
|
|
56
|
+
close(event?: Event): void
|
|
57
|
+
hold(zone: string, event: Event): void
|
|
58
|
+
release(zone: string, event: Event): void
|
|
59
|
+
cancelHover(): void
|
|
60
|
+
setTrigger(element: Trigger | null): void
|
|
61
|
+
setContent(element: Content | null): void
|
|
62
|
+
setReference(element: PositionReference | null): void
|
|
63
|
+
/** Requests a coalesced geometry update for a proven manual case. */
|
|
64
|
+
update(): void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type PopupOptions<View> = {
|
|
68
|
+
prefix: string
|
|
69
|
+
profile: Exclude<PositionProfile, 'chart'>
|
|
70
|
+
initialOpen: boolean
|
|
71
|
+
disabled?: () => boolean
|
|
72
|
+
hover?: {
|
|
73
|
+
openDelay: () => number
|
|
74
|
+
closeDelay: () => number
|
|
75
|
+
}
|
|
76
|
+
onOpenChange?: (open: boolean, event?: Event) => void
|
|
77
|
+
/** Runs after current first geometry commit or after synchronous close. */
|
|
78
|
+
onSync?: (open: boolean, view: View) => void
|
|
79
|
+
reference?: (view: View) => PositionReference | null
|
|
80
|
+
source?: (view: View) => HTMLElement | null
|
|
81
|
+
boundary?: (view: View) => Element | null
|
|
82
|
+
/** Optional parent clip used only by reference-hidden detection. */
|
|
83
|
+
referenceBoundary?: (view: View) => Element | null
|
|
84
|
+
referenceHidden?: 'close' | 'hide' | 'none'
|
|
85
|
+
/** Refreshes native source identity when an open reference tuple changes. */
|
|
86
|
+
reopenOnReferenceChange?: boolean
|
|
87
|
+
/** Fires after a current explicit geometry commit and the surface is revealed. */
|
|
88
|
+
onPosition?: (view: View) => void
|
|
89
|
+
dismiss?: {
|
|
90
|
+
prevent?: boolean
|
|
91
|
+
escape?: false | 'host' | 'document'
|
|
92
|
+
outside?: boolean
|
|
93
|
+
inside?: (view: View) => (Element | null | undefined)[]
|
|
94
|
+
onDismiss?: (event: Event, view: View) => void
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const stacks = new WeakMap<Document, object[]>()
|
|
99
|
+
const handled = new WeakSet<Event>()
|
|
100
|
+
|
|
101
|
+
const declaration = (name: string, value: string, priority = '') =>
|
|
102
|
+
value ? `${name}:${value}${priority ? '!important' : ''}` : ''
|
|
103
|
+
|
|
104
|
+
const contentOwnedStyle = (element: HTMLElement, arrow: boolean) => [
|
|
105
|
+
declaration('position', element.style.position),
|
|
106
|
+
declaration('left', element.style.left),
|
|
107
|
+
declaration('top', element.style.top),
|
|
108
|
+
declaration('transform-origin', element.style.transformOrigin),
|
|
109
|
+
declaration('box-sizing', element.style.boxSizing),
|
|
110
|
+
declaration('max-width', element.style.maxWidth),
|
|
111
|
+
declaration('max-height', element.style.maxHeight),
|
|
112
|
+
declaration('visibility', element.style.visibility, element.style.getPropertyPriority('visibility')),
|
|
113
|
+
declaration('pointer-events', element.style.pointerEvents, element.style.getPropertyPriority('pointer-events')),
|
|
114
|
+
declaration('--reference-width', element.style.getPropertyValue('--reference-width')),
|
|
115
|
+
declaration('--reference-height', element.style.getPropertyValue('--reference-height')),
|
|
116
|
+
declaration('--available-width', element.style.getPropertyValue('--available-width')),
|
|
117
|
+
declaration('--available-height', element.style.getPropertyValue('--available-height')),
|
|
118
|
+
declaration('--popup-arrow-center', element.style.getPropertyValue('--popup-arrow-center')),
|
|
119
|
+
arrow ? 'overflow:visible' : '',
|
|
120
|
+
].filter(Boolean).join(';')
|
|
121
|
+
|
|
122
|
+
const arrowOwnedStyle = (element: HTMLElement | null) => element ? [
|
|
123
|
+
declaration('left', element.style.left),
|
|
124
|
+
declaration('right', element.style.right),
|
|
125
|
+
declaration('top', element.style.top),
|
|
126
|
+
declaration('bottom', element.style.bottom),
|
|
127
|
+
].filter(Boolean).join(';') : ''
|
|
128
|
+
|
|
129
|
+
const stack = (element: HTMLElement) => {
|
|
130
|
+
const document = element.ownerDocument
|
|
131
|
+
let current = stacks.get(document)
|
|
132
|
+
if (!current) stacks.set(document, current = [])
|
|
133
|
+
return current
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const remove = (view: object, element: HTMLElement | null) => {
|
|
137
|
+
if (!element) return
|
|
138
|
+
const current = stack(element)
|
|
139
|
+
const index = current.indexOf(view)
|
|
140
|
+
if (index >= 0) current.splice(index, 1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const push = (view: object, element: HTMLElement) => {
|
|
144
|
+
const current = stack(element)
|
|
145
|
+
const index = current.indexOf(view)
|
|
146
|
+
if (index >= 0) current.splice(index, 1)
|
|
147
|
+
current.push(view)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const top = (view: object, element: HTMLElement | null) => {
|
|
151
|
+
if (!element) return false
|
|
152
|
+
const current = stack(element)
|
|
153
|
+
return current[current.length - 1] === view
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Private popup Module: Ajo interaction/native lifecycle over the position Adapter. */
|
|
157
|
+
export const popup = <
|
|
158
|
+
Trigger extends HTMLElement = HTMLElement,
|
|
159
|
+
Content extends HTMLElement = HTMLDivElement,
|
|
160
|
+
View extends PopupView<Trigger, Content> = PopupView<Trigger, Content>,
|
|
161
|
+
>(host: Host, options: PopupOptions<View>): View => {
|
|
162
|
+
const rootId = id(options.prefix)
|
|
163
|
+
const contentId = `${rootId}-content`
|
|
164
|
+
const state = controlled<boolean>(host, {
|
|
165
|
+
fallback: options.initialOpen,
|
|
166
|
+
onChange: options.onOpenChange,
|
|
167
|
+
})
|
|
168
|
+
let arrow: HTMLElement | null = null
|
|
169
|
+
let content: Content | null = null
|
|
170
|
+
let contentInputStyle: unknown
|
|
171
|
+
let opened = state.value
|
|
172
|
+
let overflowElement: Content | null = null
|
|
173
|
+
let preferred: PopupPosition = {}
|
|
174
|
+
let referenceElement: PositionReference | null = null
|
|
175
|
+
let referenceIsHidden = false
|
|
176
|
+
let shown = false
|
|
177
|
+
let trigger: Trigger | null = null
|
|
178
|
+
const generatedTriggerId = `${rootId}-trigger`
|
|
179
|
+
let triggerId = generatedTriggerId
|
|
180
|
+
let version = 0
|
|
181
|
+
let view: View
|
|
182
|
+
let opening = false
|
|
183
|
+
let positionTask: Promise<boolean> | undefined
|
|
184
|
+
let reopen = false
|
|
185
|
+
let scheduled = false
|
|
186
|
+
const inputStyle = dom(host) ? host.ownerDocument.createElement('span').style : null
|
|
187
|
+
let inputPointerEvents = { priority: '', value: '' }
|
|
188
|
+
let inputVisibility = { priority: '', value: '' }
|
|
189
|
+
|
|
190
|
+
const rememberContentStyle = (style: unknown) => {
|
|
191
|
+
contentInputStyle = style
|
|
192
|
+
if (!inputStyle) return
|
|
193
|
+
inputStyle.cssText = typeof style === 'string' ? style : ''
|
|
194
|
+
inputPointerEvents = {
|
|
195
|
+
priority: inputStyle.getPropertyPriority('pointer-events'),
|
|
196
|
+
value: inputStyle.getPropertyValue('pointer-events'),
|
|
197
|
+
}
|
|
198
|
+
inputVisibility = {
|
|
199
|
+
priority: inputStyle.getPropertyPriority('visibility'),
|
|
200
|
+
value: inputStyle.getPropertyValue('visibility'),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const restore = (target: Content, name: string, input: { priority: string; value: string }) => {
|
|
204
|
+
if (input.value) target.style.setProperty(name, input.value, input.priority)
|
|
205
|
+
else target.style.removeProperty(name)
|
|
206
|
+
}
|
|
207
|
+
const conceal = (target: Content, hidden: boolean) => {
|
|
208
|
+
if (hidden) {
|
|
209
|
+
target.style.setProperty('visibility', 'hidden', 'important')
|
|
210
|
+
target.style.setProperty('pointer-events', 'none', 'important')
|
|
211
|
+
} else {
|
|
212
|
+
restore(target, 'visibility', inputVisibility)
|
|
213
|
+
restore(target, 'pointer-events', inputPointerEvents)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const reference = () => options.reference?.(view) ?? referenceElement ?? trigger
|
|
218
|
+
const connectedSource = (element: HTMLElement | null | undefined) => element?.isConnected ? element : null
|
|
219
|
+
const source = () => connectedSource(options.source?.(view)) ?? connectedSource(trigger)
|
|
220
|
+
const clearOverflow = () => {
|
|
221
|
+
if (!overflowElement) return
|
|
222
|
+
const target = overflowElement
|
|
223
|
+
overflowElement = null
|
|
224
|
+
if (target === content) {
|
|
225
|
+
target.setAttribute('style', popupStyle(contentInputStyle, contentOwnedStyle(target, false)))
|
|
226
|
+
} else {
|
|
227
|
+
target.style.overflow = ''
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const syncOverflow = () => {
|
|
231
|
+
if (overflowElement && (overflowElement !== content || !arrow)) clearOverflow()
|
|
232
|
+
if (!content || !arrow) return
|
|
233
|
+
overflowElement = content
|
|
234
|
+
content.style.overflow = 'visible'
|
|
235
|
+
}
|
|
236
|
+
const reveal = (target: Content) => {
|
|
237
|
+
conceal(target, options.referenceHidden === 'hide' && referenceIsHidden)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const geometry = position(host, {
|
|
241
|
+
profile: options.profile,
|
|
242
|
+
elements: () => ({ reference: reference(), floating: content, arrow }),
|
|
243
|
+
placement: () => preferred.placement,
|
|
244
|
+
gap: () => preferred.gap,
|
|
245
|
+
boundary: () => options.boundary?.(view) ?? null,
|
|
246
|
+
referenceBoundary: () => options.referenceBoundary?.(view) ?? null,
|
|
247
|
+
referenceHidden(hidden) {
|
|
248
|
+
referenceIsHidden = hidden
|
|
249
|
+
if (!content) return
|
|
250
|
+
if (options.referenceHidden === 'hide') {
|
|
251
|
+
if (hidden || shown) conceal(content, hidden)
|
|
252
|
+
} else if (hidden && options.referenceHidden === 'close') {
|
|
253
|
+
view.close()
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const intent = options.hover ? hover(host, {
|
|
259
|
+
openDelay: options.hover.openDelay,
|
|
260
|
+
closeDelay: options.hover.closeDelay,
|
|
261
|
+
onChange: (next, event) => setOpen(next, event),
|
|
262
|
+
}) : undefined
|
|
263
|
+
|
|
264
|
+
const closeTarget = (target: Content) => {
|
|
265
|
+
let error: unknown
|
|
266
|
+
try {
|
|
267
|
+
if (!closePopover(target)) error = new Error('Failed to close the native popover')
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
error = cause
|
|
270
|
+
} finally {
|
|
271
|
+
conceal(target, false)
|
|
272
|
+
}
|
|
273
|
+
return error
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const discard = (target: Content) => {
|
|
277
|
+
remove(view, target)
|
|
278
|
+
target.dataset.state = 'closed'
|
|
279
|
+
const error = closeTarget(target)
|
|
280
|
+
if (error !== undefined && !host.signal.aborted) host.throw(error)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const closeCurrent = (target = content, notify = true) => {
|
|
284
|
+
const wasShown = shown
|
|
285
|
+
if (target) target.dataset.state = 'closed'
|
|
286
|
+
version++
|
|
287
|
+
geometry.stop()
|
|
288
|
+
remove(view, target)
|
|
289
|
+
shown = false
|
|
290
|
+
positionTask = undefined
|
|
291
|
+
referenceIsHidden = false
|
|
292
|
+
if (!target) return
|
|
293
|
+
const error = closeTarget(target)
|
|
294
|
+
if (wasShown && notify) options.onSync?.(false, view)
|
|
295
|
+
if (error !== undefined && !host.signal.aborted) host.throw(error)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const openCurrent = async () => {
|
|
299
|
+
const target = content
|
|
300
|
+
const currentReference = reference()
|
|
301
|
+
if (!target || !opened || host.signal.aborted) return false
|
|
302
|
+
if (!currentReference) {
|
|
303
|
+
view.close()
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
const wasShown = shown
|
|
307
|
+
const wasNativeOpen = popoverOpen(target)
|
|
308
|
+
const token = ++version
|
|
309
|
+
if (!wasShown) target.dataset.state = 'closed'
|
|
310
|
+
conceal(target, true)
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
if (!openPopover(target, source())) {
|
|
314
|
+
throw new Error('Failed to open the native popover')
|
|
315
|
+
}
|
|
316
|
+
if (!wasNativeOpen) push(view, target)
|
|
317
|
+
const committed = await geometry.start()
|
|
318
|
+
if (
|
|
319
|
+
!committed ||
|
|
320
|
+
token !== version ||
|
|
321
|
+
!opened ||
|
|
322
|
+
content !== target ||
|
|
323
|
+
!popoverOpen(target)
|
|
324
|
+
) {
|
|
325
|
+
geometry.stop()
|
|
326
|
+
if (reopen && opened && !host.signal.aborted) {
|
|
327
|
+
if (content === target) conceal(target, true)
|
|
328
|
+
else discard(target)
|
|
329
|
+
} else if (opened && content === target && !host.signal.aborted) {
|
|
330
|
+
view.close()
|
|
331
|
+
} else if (content === target) {
|
|
332
|
+
closeCurrent(target)
|
|
333
|
+
} else {
|
|
334
|
+
discard(target)
|
|
335
|
+
}
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
shown = true
|
|
340
|
+
target.dataset.state = 'open'
|
|
341
|
+
reveal(target)
|
|
342
|
+
options.onPosition?.(view)
|
|
343
|
+
if (!wasShown) options.onSync?.(true, view)
|
|
344
|
+
return true
|
|
345
|
+
} catch (error) {
|
|
346
|
+
geometry.stop()
|
|
347
|
+
if (opened && content === target && !host.signal.aborted) view.close()
|
|
348
|
+
else if (content === target) closeCurrent(target)
|
|
349
|
+
else discard(target)
|
|
350
|
+
throw error
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const run = async () => {
|
|
355
|
+
scheduled = false
|
|
356
|
+
if (host.signal.aborted) {
|
|
357
|
+
reopen = false
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
if (opening) return
|
|
361
|
+
opening = true
|
|
362
|
+
try {
|
|
363
|
+
do {
|
|
364
|
+
reopen = false
|
|
365
|
+
if (opened) await openCurrent()
|
|
366
|
+
else closeCurrent()
|
|
367
|
+
} while (reopen)
|
|
368
|
+
} finally {
|
|
369
|
+
opening = false
|
|
370
|
+
if (reopen) schedule()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function schedule() {
|
|
375
|
+
if (host.signal.aborted) return
|
|
376
|
+
reopen = true
|
|
377
|
+
if (scheduled || opening) return
|
|
378
|
+
scheduled = true
|
|
379
|
+
queueMicrotask(() => {
|
|
380
|
+
if (host.signal.aborted) {
|
|
381
|
+
scheduled = false
|
|
382
|
+
reopen = false
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
void run().catch(report)
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const report = (error: unknown) => {
|
|
390
|
+
if (host.signal.aborted) return
|
|
391
|
+
queueMicrotask(() => {
|
|
392
|
+
if (!host.signal.aborted) host.throw(error)
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const update = () => {
|
|
397
|
+
if (!opened || host.signal.aborted) return
|
|
398
|
+
if (!shown && !opening) {
|
|
399
|
+
schedule()
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
const target = content
|
|
403
|
+
const pending = geometry.update()
|
|
404
|
+
if (opening || pending === positionTask) return
|
|
405
|
+
positionTask = pending
|
|
406
|
+
const clear = () => {
|
|
407
|
+
if (positionTask === pending) positionTask = undefined
|
|
408
|
+
}
|
|
409
|
+
void pending.then(committed => {
|
|
410
|
+
clear()
|
|
411
|
+
if (committed && opened && shown && target === content) options.onPosition?.(view)
|
|
412
|
+
}, error => {
|
|
413
|
+
clear()
|
|
414
|
+
report(error)
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const arrowSize = resize(host, {
|
|
419
|
+
target: () => opened ? arrow : null,
|
|
420
|
+
onResize: update,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
const restart = (reopenSource = false) => {
|
|
424
|
+
if (!opened || host.signal.aborted) return
|
|
425
|
+
version++
|
|
426
|
+
geometry.stop()
|
|
427
|
+
positionTask = undefined
|
|
428
|
+
referenceIsHidden = false
|
|
429
|
+
if (reopenSource && content && popoverOpen(content)) {
|
|
430
|
+
const error = closeTarget(content)
|
|
431
|
+
if (error !== undefined) report(error)
|
|
432
|
+
}
|
|
433
|
+
if (content) {
|
|
434
|
+
conceal(content, true)
|
|
435
|
+
}
|
|
436
|
+
schedule()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const setOpen = (next: boolean, event?: Event) => {
|
|
440
|
+
if (host.signal.aborted) return
|
|
441
|
+
if (options.disabled?.() && next) return
|
|
442
|
+
if (next === opened) return
|
|
443
|
+
state.set(next, event)
|
|
444
|
+
opened = state.value
|
|
445
|
+
intent?.sync(opened)
|
|
446
|
+
arrowSize.sync()
|
|
447
|
+
if (opened) schedule()
|
|
448
|
+
else closeCurrent()
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
view = {
|
|
452
|
+
get open() { return opened },
|
|
453
|
+
get trigger() { return trigger },
|
|
454
|
+
get content() { return content },
|
|
455
|
+
get reference() { return referenceElement },
|
|
456
|
+
get triggerId() { return triggerId },
|
|
457
|
+
get contentId() { return contentId },
|
|
458
|
+
adoptTriggerId(id) {
|
|
459
|
+
triggerId = typeof id === 'string' && id ? id : generatedTriggerId
|
|
460
|
+
return triggerId
|
|
461
|
+
},
|
|
462
|
+
contentStyle(style) {
|
|
463
|
+
rememberContentStyle(style)
|
|
464
|
+
return popupStyle(style, content ? contentOwnedStyle(content, Boolean(arrow)) : '')
|
|
465
|
+
},
|
|
466
|
+
arrowAttrs() {
|
|
467
|
+
return {
|
|
468
|
+
ref: setArrow,
|
|
469
|
+
style: arrowOwnedStyle(arrow),
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
sync(open, next = {}) {
|
|
473
|
+
const changed = preferred.placement !== next.placement || preferred.gap !== next.gap
|
|
474
|
+
preferred = next
|
|
475
|
+
const previous = opened
|
|
476
|
+
opened = state.sync(open ?? undefined)
|
|
477
|
+
intent?.sync(opened)
|
|
478
|
+
arrowSize.sync()
|
|
479
|
+
if (!opened) {
|
|
480
|
+
if (previous || shown) closeCurrent()
|
|
481
|
+
} else if (!shown && !opening) {
|
|
482
|
+
schedule()
|
|
483
|
+
} else if (changed) {
|
|
484
|
+
update()
|
|
485
|
+
}
|
|
486
|
+
return opened
|
|
487
|
+
},
|
|
488
|
+
setOpen,
|
|
489
|
+
init(open) {
|
|
490
|
+
if (state.controlled || open === opened) return
|
|
491
|
+
state.init(open)
|
|
492
|
+
opened = open
|
|
493
|
+
intent?.sync(opened)
|
|
494
|
+
arrowSize.sync()
|
|
495
|
+
if (opened) schedule()
|
|
496
|
+
else closeCurrent()
|
|
497
|
+
},
|
|
498
|
+
close: (event?: Event) => setOpen(false, event),
|
|
499
|
+
hold(zone, event) {
|
|
500
|
+
if (!options.disabled?.()) intent?.hold(zone, event)
|
|
501
|
+
},
|
|
502
|
+
release: (zone, event) => intent?.release(zone, event),
|
|
503
|
+
cancelHover: () => intent?.cancel(),
|
|
504
|
+
setTrigger(element) {
|
|
505
|
+
if (element === trigger) return
|
|
506
|
+
const previousReference = reference()
|
|
507
|
+
const previousSource = source()
|
|
508
|
+
trigger = element
|
|
509
|
+
if (element?.id && element.id !== triggerId) {
|
|
510
|
+
triggerId = element.id
|
|
511
|
+
queueMicrotask(() => host.next())
|
|
512
|
+
}
|
|
513
|
+
const referenceChanged = reference() !== previousReference
|
|
514
|
+
const sourceChanged = source() !== previousSource
|
|
515
|
+
if (referenceChanged || sourceChanged) restart(sourceChanged)
|
|
516
|
+
},
|
|
517
|
+
setContent(element) {
|
|
518
|
+
if (element === content) return
|
|
519
|
+
const previous = content
|
|
520
|
+
if (previous) closeCurrent(previous, false)
|
|
521
|
+
content = element
|
|
522
|
+
syncOverflow()
|
|
523
|
+
if (element) element.dataset.state = 'closed'
|
|
524
|
+
if (opened) schedule()
|
|
525
|
+
},
|
|
526
|
+
setReference(element) {
|
|
527
|
+
if (element === referenceElement) return
|
|
528
|
+
const previousReference = reference()
|
|
529
|
+
const previousSource = source()
|
|
530
|
+
referenceElement = element
|
|
531
|
+
const referenceChanged = reference() !== previousReference
|
|
532
|
+
const sourceChanged = source() !== previousSource
|
|
533
|
+
if (referenceChanged || sourceChanged) restart(sourceChanged || options.reopenOnReferenceChange)
|
|
534
|
+
},
|
|
535
|
+
update,
|
|
536
|
+
} as View
|
|
537
|
+
|
|
538
|
+
function setArrow(element: HTMLElement | null) {
|
|
539
|
+
if (element === arrow) return
|
|
540
|
+
arrow = element
|
|
541
|
+
syncOverflow()
|
|
542
|
+
arrowSize.sync()
|
|
543
|
+
restart()
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (options.dismiss) dismiss(host, {
|
|
547
|
+
active: () => opened && Boolean(content && popoverOpen(content)) && top(view, content),
|
|
548
|
+
inside: () => options.dismiss?.inside?.(view) ?? [trigger, content],
|
|
549
|
+
prevent: options.dismiss.prevent,
|
|
550
|
+
escape: options.dismiss.escape ?? 'document',
|
|
551
|
+
outside: options.dismiss.outside,
|
|
552
|
+
onDismiss(event) {
|
|
553
|
+
if (handled.has(event)) return
|
|
554
|
+
handled.add(event)
|
|
555
|
+
if (options.dismiss?.onDismiss) options.dismiss.onDismiss(event, view)
|
|
556
|
+
else view.close(event)
|
|
557
|
+
},
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
host.signal.addEventListener('abort', () => {
|
|
561
|
+
opened = false
|
|
562
|
+
reopen = false
|
|
563
|
+
scheduled = false
|
|
564
|
+
intent?.sync(false)
|
|
565
|
+
closeCurrent(content, false)
|
|
566
|
+
clearOverflow()
|
|
567
|
+
}, { once: true })
|
|
568
|
+
return view
|
|
569
|
+
}
|