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/position.ts
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import {
|
|
2
|
+
arrow as arrowMiddleware,
|
|
3
|
+
autoPlacement,
|
|
4
|
+
autoUpdate,
|
|
5
|
+
computePosition,
|
|
6
|
+
flip,
|
|
7
|
+
hide,
|
|
8
|
+
inline,
|
|
9
|
+
limitShift,
|
|
10
|
+
offset,
|
|
11
|
+
shift,
|
|
12
|
+
size,
|
|
13
|
+
type Middleware,
|
|
14
|
+
type Placement,
|
|
15
|
+
type ReferenceElement,
|
|
16
|
+
} from '@floating-ui/dom'
|
|
17
|
+
import { dom, type Host } from 'ajo-cloves'
|
|
18
|
+
import type { PopupPlacement } from './utils'
|
|
19
|
+
|
|
20
|
+
export type PositionProfile =
|
|
21
|
+
| 'popover'
|
|
22
|
+
| 'tooltip'
|
|
23
|
+
| 'menu'
|
|
24
|
+
| 'submenu'
|
|
25
|
+
| 'select'
|
|
26
|
+
| 'date'
|
|
27
|
+
| 'navigation'
|
|
28
|
+
| 'context'
|
|
29
|
+
| 'menubar'
|
|
30
|
+
| 'chart'
|
|
31
|
+
|
|
32
|
+
/** Raw geometry arguments reserved by the private positioning profiles. */
|
|
33
|
+
export type ReservedPositionArg =
|
|
34
|
+
| 'align'
|
|
35
|
+
| 'alignOffset'
|
|
36
|
+
| 'boundary'
|
|
37
|
+
| 'collisionPadding'
|
|
38
|
+
| 'constrain'
|
|
39
|
+
| 'middleware'
|
|
40
|
+
| 'platform'
|
|
41
|
+
| 'side'
|
|
42
|
+
| 'sideOffset'
|
|
43
|
+
| 'strategy'
|
|
44
|
+
|
|
45
|
+
/** Private real-or-virtual reference shape owned at the Adapter boundary. */
|
|
46
|
+
export type PositionReference = ReferenceElement
|
|
47
|
+
|
|
48
|
+
/** Private zero-area virtual reference shared by point-positioned families. */
|
|
49
|
+
export const pointReference = (
|
|
50
|
+
contextElement: Element | (() => Element),
|
|
51
|
+
point: () => { x: number; y: number },
|
|
52
|
+
): PositionReference => ({
|
|
53
|
+
get contextElement() {
|
|
54
|
+
return typeof contextElement === 'function' ? contextElement() : contextElement
|
|
55
|
+
},
|
|
56
|
+
getBoundingClientRect: () => {
|
|
57
|
+
const { x, y } = point()
|
|
58
|
+
return { x, y, top: y, right: x, bottom: y, left: x, width: 0, height: 0 }
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export type PositionElements = {
|
|
63
|
+
reference: PositionReference | null
|
|
64
|
+
floating: HTMLElement | null
|
|
65
|
+
arrow: HTMLElement | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PositionOptions = {
|
|
69
|
+
profile: PositionProfile
|
|
70
|
+
elements: () => PositionElements
|
|
71
|
+
placement?: () => PopupPlacement | undefined
|
|
72
|
+
gap?: () => number | undefined
|
|
73
|
+
boundary?: () => Element | null
|
|
74
|
+
/** Optional clipping boundary used only to decide whether the reference is hidden. */
|
|
75
|
+
referenceBoundary?: () => Element | null
|
|
76
|
+
referenceHidden?: (hidden: boolean) => void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type PositionView = {
|
|
80
|
+
/** Starts observation and resolves true after a current first commit. */
|
|
81
|
+
start(): Promise<boolean>
|
|
82
|
+
/** Coalesces a geometry update and resolves true after a current commit. */
|
|
83
|
+
update(): Promise<boolean>
|
|
84
|
+
/** Invalidates pending work and removes the active observation scope. */
|
|
85
|
+
stop(): void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type Policy = {
|
|
89
|
+
placement: Placement
|
|
90
|
+
gap: number
|
|
91
|
+
padding: number
|
|
92
|
+
inline?: boolean
|
|
93
|
+
size?: 'both' | 'width'
|
|
94
|
+
hidden?: boolean
|
|
95
|
+
crossAxis?: number
|
|
96
|
+
fallbackAxisSideDirection?: 'end' | 'start'
|
|
97
|
+
fallbackPlacements?: Placement[]
|
|
98
|
+
flipFirst?: boolean
|
|
99
|
+
writer?: 'transform'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const policies: Record<PositionProfile, Policy> = {
|
|
103
|
+
popover: { placement: 'bottom', gap: 4, padding: 8, inline: true, size: 'both', hidden: true },
|
|
104
|
+
tooltip: { placement: 'top', gap: 8, padding: 8, inline: true, size: 'width', hidden: true, fallbackAxisSideDirection: 'start' },
|
|
105
|
+
menu: { placement: 'bottom-start', gap: 4, padding: 4, size: 'both', hidden: true },
|
|
106
|
+
submenu: { placement: 'right-start', gap: 4, padding: 4, size: 'both', hidden: true, fallbackPlacements: ['left-start'] },
|
|
107
|
+
select: { placement: 'bottom-start', gap: 6, padding: 8, size: 'both', hidden: true },
|
|
108
|
+
date: { placement: 'bottom-start', gap: 6, padding: 8, size: 'both', hidden: true },
|
|
109
|
+
navigation: { placement: 'bottom', gap: 8, padding: 8, size: 'both', hidden: true },
|
|
110
|
+
context: { placement: 'bottom-start', gap: 2, padding: 4, size: 'both', hidden: true },
|
|
111
|
+
menubar: { placement: 'bottom-start', gap: 8, padding: 4, size: 'both', hidden: true, crossAxis: -4 },
|
|
112
|
+
chart: { placement: 'right', gap: 12, padding: 8, flipFirst: true, writer: 'transform' },
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const inactive = (): PositionView => ({
|
|
116
|
+
start: async () => false,
|
|
117
|
+
update: async () => false,
|
|
118
|
+
stop() { },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const connected = (reference: PositionReference) => {
|
|
122
|
+
if (dom(reference)) return reference.isConnected
|
|
123
|
+
return !reference.contextElement || reference.contextElement.isConnected
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const finite = (value: number | undefined, fallback: number) =>
|
|
127
|
+
Number.isFinite(value) ? Number(value) : fallback
|
|
128
|
+
|
|
129
|
+
const round = (value: number, target: Element) => {
|
|
130
|
+
const ratio = target.ownerDocument.defaultView?.devicePixelRatio || 1
|
|
131
|
+
return Math.round(value * ratio) / ratio
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const parts = (placement: Placement) => {
|
|
135
|
+
const [side, align = 'center'] = placement.split('-')
|
|
136
|
+
return { side, align }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const overflow = (padding: number, boundary: Element | null) => ({
|
|
140
|
+
padding,
|
|
141
|
+
...(boundary ? { boundary } : {}),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const middleware = (
|
|
145
|
+
policy: Policy,
|
|
146
|
+
preferred: PopupPlacement,
|
|
147
|
+
gap: number,
|
|
148
|
+
boundary: Element | null,
|
|
149
|
+
referenceBoundary: Element | null,
|
|
150
|
+
arrow: HTMLElement | null,
|
|
151
|
+
applySize: (
|
|
152
|
+
availableHeight: number,
|
|
153
|
+
availableWidth: number,
|
|
154
|
+
floating: HTMLElement,
|
|
155
|
+
referenceHeight: number,
|
|
156
|
+
referenceWidth: number,
|
|
157
|
+
) => void,
|
|
158
|
+
): Middleware[] => {
|
|
159
|
+
const detect = overflow(policy.padding, boundary)
|
|
160
|
+
const placement = preferred === 'auto' ? policy.placement : preferred
|
|
161
|
+
const aligned = placement.includes('-')
|
|
162
|
+
const move = shift({ ...detect, limiter: limitShift() })
|
|
163
|
+
const fallback = flip({
|
|
164
|
+
...detect,
|
|
165
|
+
fallbackAxisSideDirection: policy.fallbackAxisSideDirection ?? 'end',
|
|
166
|
+
...(policy.fallbackPlacements ? { fallbackPlacements: policy.fallbackPlacements } : {}),
|
|
167
|
+
})
|
|
168
|
+
const collision = preferred === 'auto'
|
|
169
|
+
? [autoPlacement(detect), move]
|
|
170
|
+
: aligned || policy.flipFirst ? [fallback, move] : [move, fallback]
|
|
171
|
+
const result: Middleware[] = []
|
|
172
|
+
// Floating UI requires inline() before offset() so a multiline rect reset
|
|
173
|
+
// recalculates the requested gap against the selected client rect.
|
|
174
|
+
if (policy.inline) result.push(inline())
|
|
175
|
+
result.push(offset({ mainAxis: gap, crossAxis: policy.crossAxis ?? 0 }))
|
|
176
|
+
result.push(...collision)
|
|
177
|
+
if (policy.size) result.push(size({
|
|
178
|
+
...detect,
|
|
179
|
+
apply({ availableHeight, availableWidth, elements, rects }) {
|
|
180
|
+
applySize(
|
|
181
|
+
availableHeight,
|
|
182
|
+
availableWidth,
|
|
183
|
+
elements.floating,
|
|
184
|
+
rects.reference.height,
|
|
185
|
+
rects.reference.width,
|
|
186
|
+
)
|
|
187
|
+
},
|
|
188
|
+
}))
|
|
189
|
+
if (arrow) result.push(arrowMiddleware({ element: arrow, padding: 8 }))
|
|
190
|
+
if (policy.hidden) {
|
|
191
|
+
result.push(hide(overflow(policy.padding, referenceBoundary ?? boundary)))
|
|
192
|
+
result.push(hide({ ...detect, strategy: 'escaped' }))
|
|
193
|
+
}
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const clearArrow = (arrow: HTMLElement | null) => {
|
|
198
|
+
if (!arrow) return
|
|
199
|
+
arrow.style.left = ''
|
|
200
|
+
arrow.style.right = ''
|
|
201
|
+
arrow.style.top = ''
|
|
202
|
+
arrow.style.bottom = ''
|
|
203
|
+
delete arrow.dataset.arrowUncentered
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const deactivateFloating = (floating: HTMLElement, profile: PositionProfile) => {
|
|
207
|
+
if (profile === 'chart') {
|
|
208
|
+
delete floating.dataset.positioned
|
|
209
|
+
floating.style.transform = ''
|
|
210
|
+
}
|
|
211
|
+
floating.style.willChange = ''
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const resetFloating = (floating: HTMLElement, profile: PositionProfile) => {
|
|
215
|
+
deactivateFloating(floating, profile)
|
|
216
|
+
floating.style.position = ''
|
|
217
|
+
floating.style.left = ''
|
|
218
|
+
floating.style.top = ''
|
|
219
|
+
floating.style.transformOrigin = ''
|
|
220
|
+
floating.style.boxSizing = ''
|
|
221
|
+
floating.style.maxWidth = ''
|
|
222
|
+
floating.style.maxHeight = ''
|
|
223
|
+
floating.style.removeProperty('--reference-width')
|
|
224
|
+
floating.style.removeProperty('--reference-height')
|
|
225
|
+
floating.style.removeProperty('--available-width')
|
|
226
|
+
floating.style.removeProperty('--available-height')
|
|
227
|
+
floating.style.removeProperty('--popup-arrow-center')
|
|
228
|
+
delete floating.dataset.placement
|
|
229
|
+
delete floating.dataset.side
|
|
230
|
+
delete floating.dataset.align
|
|
231
|
+
delete floating.dataset.referenceHidden
|
|
232
|
+
delete floating.dataset.escaped
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sizeState = (element: HTMLElement) => ({
|
|
236
|
+
boxSizing: element.style.boxSizing,
|
|
237
|
+
maxWidth: element.style.maxWidth,
|
|
238
|
+
maxHeight: element.style.maxHeight,
|
|
239
|
+
referenceWidth: element.style.getPropertyValue('--reference-width'),
|
|
240
|
+
referenceHeight: element.style.getPropertyValue('--reference-height'),
|
|
241
|
+
availableWidth: element.style.getPropertyValue('--available-width'),
|
|
242
|
+
availableHeight: element.style.getPropertyValue('--available-height'),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const restoreSize = (element: HTMLElement, state: ReturnType<typeof sizeState>) => {
|
|
246
|
+
element.style.boxSizing = state.boxSizing
|
|
247
|
+
element.style.maxWidth = state.maxWidth
|
|
248
|
+
element.style.maxHeight = state.maxHeight
|
|
249
|
+
element.style.setProperty('--reference-width', state.referenceWidth)
|
|
250
|
+
element.style.setProperty('--reference-height', state.referenceHeight)
|
|
251
|
+
element.style.setProperty('--available-width', state.availableWidth)
|
|
252
|
+
element.style.setProperty('--available-height', state.availableHeight)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const origin = (target: HTMLElement, side: string, align: string) => {
|
|
256
|
+
const horizontal = side === 'top' || side === 'bottom'
|
|
257
|
+
const rtl = horizontal && target.ownerDocument.defaultView?.getComputedStyle(target).direction === 'rtl'
|
|
258
|
+
const cross = align === 'center' ? '50%'
|
|
259
|
+
: align === 'start' ? rtl ? '100%' : '0%'
|
|
260
|
+
: rtl ? '0%' : '100%'
|
|
261
|
+
return side === 'top' ? `${cross} 100%`
|
|
262
|
+
: side === 'right' ? `0% ${cross}`
|
|
263
|
+
: side === 'bottom' ? `${cross} 0%`
|
|
264
|
+
: `100% ${cross}`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Private Floating UI Adapter shared by popup families and Chart. */
|
|
268
|
+
export const position = (host: Host, options: PositionOptions): PositionView => {
|
|
269
|
+
if (!dom(host) || !host.ownerDocument.defaultView) return inactive()
|
|
270
|
+
|
|
271
|
+
const policy = policies[options.profile]
|
|
272
|
+
let active = false
|
|
273
|
+
let cleanup: (() => void) | undefined
|
|
274
|
+
let current: PositionElements | undefined
|
|
275
|
+
let generation = 0
|
|
276
|
+
let hidden: boolean | undefined
|
|
277
|
+
let pending: Promise<boolean> | undefined
|
|
278
|
+
let pendingGeneration = -1
|
|
279
|
+
let requested = 0
|
|
280
|
+
let sizeGeneration = -1
|
|
281
|
+
let sizeRestore: (() => void) | undefined
|
|
282
|
+
|
|
283
|
+
const rollbackSize = (scope = sizeGeneration) => {
|
|
284
|
+
if (scope !== sizeGeneration) return
|
|
285
|
+
const restore = sizeRestore
|
|
286
|
+
sizeGeneration = -1
|
|
287
|
+
sizeRestore = undefined
|
|
288
|
+
restore?.()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const commitSize = (scope: number) => {
|
|
292
|
+
if (scope !== sizeGeneration) return
|
|
293
|
+
sizeGeneration = -1
|
|
294
|
+
sizeRestore = undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const stopScope = () => {
|
|
298
|
+
const dispose = cleanup
|
|
299
|
+
active = false
|
|
300
|
+
generation++
|
|
301
|
+
requested++
|
|
302
|
+
cleanup = undefined
|
|
303
|
+
pending = undefined
|
|
304
|
+
pendingGeneration = -1
|
|
305
|
+
rollbackSize()
|
|
306
|
+
if (current?.floating) deactivateFloating(current.floating, options.profile)
|
|
307
|
+
try {
|
|
308
|
+
dispose?.()
|
|
309
|
+
} catch (error) {
|
|
310
|
+
host.throw(error)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const valid = (version: number, elements: PositionElements) =>
|
|
315
|
+
active &&
|
|
316
|
+
version === generation &&
|
|
317
|
+
current?.reference === elements.reference &&
|
|
318
|
+
current.floating === elements.floating &&
|
|
319
|
+
current.arrow === elements.arrow &&
|
|
320
|
+
!host.signal.aborted &&
|
|
321
|
+
Boolean(elements.floating?.isConnected) &&
|
|
322
|
+
Boolean(elements.reference && connected(elements.reference))
|
|
323
|
+
const currentRequest = (scope: number, request: number, elements: PositionElements) =>
|
|
324
|
+
request === requested && valid(scope, elements)
|
|
325
|
+
|
|
326
|
+
const commit = (
|
|
327
|
+
elements: PositionElements,
|
|
328
|
+
result: Awaited<ReturnType<typeof computePosition>>,
|
|
329
|
+
) => {
|
|
330
|
+
const target = elements.floating!
|
|
331
|
+
const { side, align } = parts(result.placement)
|
|
332
|
+
const x = round(result.x, target)
|
|
333
|
+
const y = round(result.y, target)
|
|
334
|
+
|
|
335
|
+
if (policy.writer === 'transform') {
|
|
336
|
+
target.style.position = 'absolute'
|
|
337
|
+
target.style.left = '0px'
|
|
338
|
+
target.style.top = '0px'
|
|
339
|
+
target.style.transform = `translate(${x}px, ${y}px)`
|
|
340
|
+
target.style.willChange = 'transform'
|
|
341
|
+
if (target.dataset.positioned !== 'true') {
|
|
342
|
+
// Snap the first endpoint before Playa enables retarget motion.
|
|
343
|
+
target.getBoundingClientRect()
|
|
344
|
+
target.dataset.positioned = 'true'
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
target.style.position = 'fixed'
|
|
348
|
+
target.style.left = `${x}px`
|
|
349
|
+
target.style.top = `${y}px`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
target.dataset.placement = result.placement
|
|
353
|
+
target.dataset.side = side
|
|
354
|
+
target.dataset.align = align
|
|
355
|
+
|
|
356
|
+
const hiddenData = result.middlewareData.hide
|
|
357
|
+
const referenceHidden = Boolean(hiddenData?.referenceHidden)
|
|
358
|
+
const escaped = Boolean(hiddenData?.escaped)
|
|
359
|
+
if (referenceHidden) target.dataset.referenceHidden = 'true'
|
|
360
|
+
else delete target.dataset.referenceHidden
|
|
361
|
+
if (escaped) target.dataset.escaped = 'true'
|
|
362
|
+
else delete target.dataset.escaped
|
|
363
|
+
const notifyHidden = hidden !== referenceHidden
|
|
364
|
+
hidden = referenceHidden
|
|
365
|
+
|
|
366
|
+
const arrow = elements.arrow
|
|
367
|
+
const arrowData = result.middlewareData.arrow
|
|
368
|
+
clearArrow(arrow)
|
|
369
|
+
target.style.removeProperty('--popup-arrow-center')
|
|
370
|
+
if (policy.writer === 'transform') {
|
|
371
|
+
target.style.transformOrigin = ''
|
|
372
|
+
} else if (arrow && arrowData) {
|
|
373
|
+
if (arrowData.x != null) arrow.style.left = `${round(arrowData.x, target)}px`
|
|
374
|
+
if (arrowData.y != null) arrow.style.top = `${round(arrowData.y, target)}px`
|
|
375
|
+
const staticSide = side === 'top' ? 'bottom' : side === 'right' ? 'left' : side === 'bottom' ? 'top' : 'right'
|
|
376
|
+
const arrowSize = side === 'top' || side === 'bottom' ? arrow.offsetHeight : arrow.offsetWidth
|
|
377
|
+
arrow.style[staticSide] = `${round(-arrowSize / 2, target)}px`
|
|
378
|
+
if (arrowData.centerOffset !== 0) arrow.dataset.arrowUncentered = 'true'
|
|
379
|
+
|
|
380
|
+
const center = side === 'top' || side === 'bottom'
|
|
381
|
+
? `${round((arrowData.x ?? 0) + arrow.offsetWidth / 2, target)}px`
|
|
382
|
+
: `${round((arrowData.y ?? 0) + arrow.offsetHeight / 2, target)}px`
|
|
383
|
+
target.style.setProperty('--popup-arrow-center', center)
|
|
384
|
+
target.style.transformOrigin = side === 'top' ? `${center} bottom`
|
|
385
|
+
: side === 'right' ? `left ${center}`
|
|
386
|
+
: side === 'bottom' ? `${center} top`
|
|
387
|
+
: `right ${center}`
|
|
388
|
+
} else {
|
|
389
|
+
target.style.transformOrigin = origin(target, side, align)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return notifyHidden ? referenceHidden : undefined
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const calculate = async (scope: number, request: number) => {
|
|
396
|
+
const elements = current
|
|
397
|
+
if (!elements?.reference || !elements.floating) return false
|
|
398
|
+
const initialSize = sizeState(elements.floating)
|
|
399
|
+
const preferred = options.placement?.() ?? policy.placement
|
|
400
|
+
const placement = preferred === 'auto' ? policy.placement : preferred
|
|
401
|
+
try {
|
|
402
|
+
const result = await computePosition(elements.reference, elements.floating, {
|
|
403
|
+
placement,
|
|
404
|
+
strategy: policy.writer === 'transform' ? 'absolute' : 'fixed',
|
|
405
|
+
middleware: middleware(
|
|
406
|
+
policy,
|
|
407
|
+
preferred,
|
|
408
|
+
finite(options.gap?.(), policy.gap),
|
|
409
|
+
options.boundary?.() ?? null,
|
|
410
|
+
options.referenceBoundary?.() ?? null,
|
|
411
|
+
elements.arrow,
|
|
412
|
+
(availableHeight, availableWidth, target, referenceHeight, referenceWidth) => {
|
|
413
|
+
if (!currentRequest(scope, request, elements) || target !== elements.floating) return
|
|
414
|
+
if (sizeGeneration !== scope) {
|
|
415
|
+
rollbackSize()
|
|
416
|
+
sizeGeneration = scope
|
|
417
|
+
sizeRestore = () => restoreSize(target, initialSize)
|
|
418
|
+
}
|
|
419
|
+
const height = Math.max(0, availableHeight)
|
|
420
|
+
const width = Math.max(0, availableWidth)
|
|
421
|
+
target.style.boxSizing = 'border-box'
|
|
422
|
+
target.style.maxWidth = `${width}px`
|
|
423
|
+
if (policy.size === 'both') target.style.maxHeight = `${height}px`
|
|
424
|
+
target.style.setProperty('--reference-width', `${referenceWidth}px`)
|
|
425
|
+
target.style.setProperty('--reference-height', `${referenceHeight}px`)
|
|
426
|
+
target.style.setProperty('--available-width', `${width}px`)
|
|
427
|
+
target.style.setProperty('--available-height', `${height}px`)
|
|
428
|
+
},
|
|
429
|
+
),
|
|
430
|
+
})
|
|
431
|
+
if (!currentRequest(scope, request, elements)) {
|
|
432
|
+
rollbackSize(scope)
|
|
433
|
+
return false
|
|
434
|
+
}
|
|
435
|
+
const referenceHidden = commit(elements, result)
|
|
436
|
+
commitSize(scope)
|
|
437
|
+
if (referenceHidden !== undefined) options.referenceHidden?.(referenceHidden)
|
|
438
|
+
return true
|
|
439
|
+
} catch (error) {
|
|
440
|
+
rollbackSize(scope)
|
|
441
|
+
if (!currentRequest(scope, request, elements)) return false
|
|
442
|
+
throw error
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const drain = async (scope: number) => {
|
|
447
|
+
let committed = false
|
|
448
|
+
while (active && generation === scope) {
|
|
449
|
+
const expected = requested
|
|
450
|
+
committed = await calculate(scope, expected)
|
|
451
|
+
if (expected === requested) break
|
|
452
|
+
}
|
|
453
|
+
return committed
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const update = () => {
|
|
457
|
+
if (!active) return Promise.resolve(false)
|
|
458
|
+
requested++
|
|
459
|
+
const scope = generation
|
|
460
|
+
if (pending && pendingGeneration === scope) {
|
|
461
|
+
rollbackSize(scope)
|
|
462
|
+
return pending
|
|
463
|
+
}
|
|
464
|
+
const task = drain(scope)
|
|
465
|
+
pending = task
|
|
466
|
+
pendingGeneration = scope
|
|
467
|
+
const clear = () => {
|
|
468
|
+
if (pending !== task) return
|
|
469
|
+
pending = undefined
|
|
470
|
+
pendingGeneration = -1
|
|
471
|
+
}
|
|
472
|
+
void task.then(clear, clear)
|
|
473
|
+
return task
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const fail = (error: unknown) => {
|
|
477
|
+
if (host.signal.aborted) return
|
|
478
|
+
queueMicrotask(() => {
|
|
479
|
+
if (!host.signal.aborted) host.throw(error)
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const stop = () => {
|
|
484
|
+
stopScope()
|
|
485
|
+
hidden = undefined
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const start = async () => {
|
|
489
|
+
if (host.signal.aborted) {
|
|
490
|
+
stop()
|
|
491
|
+
return false
|
|
492
|
+
}
|
|
493
|
+
const next = options.elements()
|
|
494
|
+
if (!next.reference || !next.floating || !next.floating.isConnected || !connected(next.reference)) {
|
|
495
|
+
stop()
|
|
496
|
+
return false
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const previous = current
|
|
500
|
+
stopScope()
|
|
501
|
+
if (previous?.arrow !== next.arrow) clearArrow(previous?.arrow ?? null)
|
|
502
|
+
if (previous?.floating && previous.floating !== next.floating) resetFloating(previous.floating, options.profile)
|
|
503
|
+
current = next
|
|
504
|
+
active = true
|
|
505
|
+
hidden = undefined
|
|
506
|
+
requested = 0
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
let starting = true
|
|
510
|
+
cleanup = autoUpdate(next.reference, next.floating, () => {
|
|
511
|
+
if (!starting) void update().catch(fail)
|
|
512
|
+
})
|
|
513
|
+
starting = false
|
|
514
|
+
return await update()
|
|
515
|
+
} catch (error) {
|
|
516
|
+
stop()
|
|
517
|
+
throw error
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
host.signal.addEventListener('abort', stop, { once: true })
|
|
522
|
+
return { start, stop, update }
|
|
523
|
+
}
|
package/src/progress.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateless } from 'ajo'
|
|
2
|
+
import { clamp } from 'ajo-cloves'
|
|
3
|
+
import type { FixedArgs, OmitArg } from './utils'
|
|
4
|
+
import { toNumber } from './utils'
|
|
5
|
+
|
|
6
|
+
/** Props for a determinate or indeterminate progress bar. */
|
|
7
|
+
export type ProgressArgs = OmitArg<IntrinsicElements['div'], 'children' | 'max' | 'value'> & {
|
|
8
|
+
/** Current progress value. Omit or pass `null` for an indeterminate progress bar. */
|
|
9
|
+
value?: number | null
|
|
10
|
+
/** Maximum progress value. */
|
|
11
|
+
max?: number
|
|
12
|
+
/** Custom accessible value text. */
|
|
13
|
+
getValueLabel?: (value: number, max: number) => string
|
|
14
|
+
/** Classes for the indicator element. */
|
|
15
|
+
indicatorClass?: string
|
|
16
|
+
} & FixedArgs<'children'>
|
|
17
|
+
|
|
18
|
+
const state = (value: number | null, max: number) => {
|
|
19
|
+
if (value == null) return 'indeterminate'
|
|
20
|
+
if (value >= max) return 'complete'
|
|
21
|
+
return 'loading'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const percent = (value: number | null, max: number) =>
|
|
25
|
+
value == null || max <= 0 ? 0 : clamp((value / max) * 100, 0, 100)
|
|
26
|
+
|
|
27
|
+
/** Unstyled progress bar with determinate and indeterminate states. */
|
|
28
|
+
const Progress: Stateless<ProgressArgs> = ({
|
|
29
|
+
'aria-label': ariaLabel,
|
|
30
|
+
'aria-valuetext': ariaValueText,
|
|
31
|
+
getValueLabel,
|
|
32
|
+
indicatorClass,
|
|
33
|
+
max = 100,
|
|
34
|
+
role = 'progressbar',
|
|
35
|
+
value = null,
|
|
36
|
+
...attrs
|
|
37
|
+
}) => {
|
|
38
|
+
const maxValue = Math.max(1, toNumber(max, 100))
|
|
39
|
+
const current = value == null ? null : clamp(toNumber(value, 0), 0, maxValue)
|
|
40
|
+
const progress = percent(current, maxValue)
|
|
41
|
+
const progressState = state(current, maxValue)
|
|
42
|
+
const valueText = current == null
|
|
43
|
+
? ariaValueText
|
|
44
|
+
: ariaValueText ?? getValueLabel?.(current, maxValue)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
{...attrs}
|
|
49
|
+
aria-label={ariaLabel}
|
|
50
|
+
aria-valuemax={maxValue}
|
|
51
|
+
aria-valuemin="0"
|
|
52
|
+
aria-valuenow={current == null ? undefined : current}
|
|
53
|
+
aria-valuetext={valueText}
|
|
54
|
+
data-max={maxValue}
|
|
55
|
+
data-slot="progress"
|
|
56
|
+
data-state={progressState}
|
|
57
|
+
data-value={current == null ? undefined : current}
|
|
58
|
+
role={role}
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
class={indicatorClass}
|
|
63
|
+
data-slot="progress-indicator"
|
|
64
|
+
style={current == null ? undefined : `transform:translateX(-${100 - progress}%)`}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { Progress }
|