@proyecto-viviana/solidaria-components 0.2.2 → 0.2.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/dist/Color.d.ts +2 -6
- package/dist/Color.d.ts.map +1 -1
- package/dist/ComboBox.d.ts +3 -3
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/GridList.d.ts +2 -2
- package/dist/GridList.d.ts.map +1 -1
- package/dist/ListBox.d.ts +5 -5
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/Menu.d.ts +3 -3
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Select.d.ts +3 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Table.d.ts +2 -2
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +1 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/index.js +56 -56
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +56 -56
- package/dist/index.ssr.js.map +2 -2
- package/package.json +10 -8
- package/src/Autocomplete.tsx +174 -0
- package/src/Breadcrumbs.tsx +264 -0
- package/src/Button.tsx +238 -0
- package/src/Calendar.tsx +471 -0
- package/src/Checkbox.tsx +387 -0
- package/src/Color.tsx +1370 -0
- package/src/ComboBox.tsx +824 -0
- package/src/DateField.tsx +337 -0
- package/src/DatePicker.tsx +367 -0
- package/src/Dialog.tsx +262 -0
- package/src/Disclosure.tsx +439 -0
- package/src/GridList.tsx +511 -0
- package/src/Landmark.tsx +203 -0
- package/src/Link.tsx +201 -0
- package/src/ListBox.tsx +346 -0
- package/src/Menu.tsx +544 -0
- package/src/Meter.tsx +157 -0
- package/src/Modal.tsx +433 -0
- package/src/NumberField.tsx +542 -0
- package/src/Popover.tsx +540 -0
- package/src/ProgressBar.tsx +162 -0
- package/src/RadioGroup.tsx +356 -0
- package/src/RangeCalendar.tsx +462 -0
- package/src/SearchField.tsx +479 -0
- package/src/Select.tsx +734 -0
- package/src/Separator.tsx +130 -0
- package/src/Slider.tsx +500 -0
- package/src/Switch.tsx +213 -0
- package/src/Table.tsx +857 -0
- package/src/Tabs.tsx +552 -0
- package/src/TagGroup.tsx +421 -0
- package/src/TextField.tsx +271 -0
- package/src/TimeField.tsx +455 -0
- package/src/Toast.tsx +503 -0
- package/src/Toolbar.tsx +160 -0
- package/src/Tooltip.tsx +423 -0
- package/src/Tree.tsx +551 -0
- package/src/VisuallyHidden.tsx +60 -0
- package/src/contexts.ts +74 -0
- package/src/index.ts +620 -0
- package/src/utils.tsx +329 -0
- package/dist/index.jsx +0 -9056
- package/dist/index.jsx.map +0 -7
package/src/Popover.tsx
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Popover component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* A headless popover component that positions relative to a trigger element.
|
|
5
|
+
* Port of react-aria-components Popover.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
createContext,
|
|
11
|
+
createMemo,
|
|
12
|
+
createSignal,
|
|
13
|
+
createUniqueId,
|
|
14
|
+
splitProps,
|
|
15
|
+
useContext,
|
|
16
|
+
Show,
|
|
17
|
+
createEffect,
|
|
18
|
+
onCleanup,
|
|
19
|
+
} from 'solid-js'
|
|
20
|
+
import { Portal, isServer } from 'solid-js/web'
|
|
21
|
+
import {
|
|
22
|
+
createOverlayTrigger,
|
|
23
|
+
FocusScope,
|
|
24
|
+
type Placement,
|
|
25
|
+
type PlacementAxis,
|
|
26
|
+
} from '@proyecto-viviana/solidaria'
|
|
27
|
+
import { createOverlayTriggerState } from '@proyecto-viviana/solid-stately'
|
|
28
|
+
import {
|
|
29
|
+
type RenderChildren,
|
|
30
|
+
type ClassNameOrFunction,
|
|
31
|
+
type StyleOrFunction,
|
|
32
|
+
type SlotProps,
|
|
33
|
+
useRenderProps,
|
|
34
|
+
filterDOMProps,
|
|
35
|
+
dataAttr,
|
|
36
|
+
} from './utils'
|
|
37
|
+
import { PopoverTriggerContext } from './contexts'
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// TYPES
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
export interface PopoverRenderProps {
|
|
44
|
+
/**
|
|
45
|
+
* The name of the component that triggered the popover.
|
|
46
|
+
*/
|
|
47
|
+
trigger: string | null
|
|
48
|
+
/**
|
|
49
|
+
* The placement of the popover relative to the trigger.
|
|
50
|
+
*/
|
|
51
|
+
placement: PlacementAxis | null
|
|
52
|
+
/**
|
|
53
|
+
* Whether the popover is currently entering (for animations).
|
|
54
|
+
*/
|
|
55
|
+
isEntering: boolean
|
|
56
|
+
/**
|
|
57
|
+
* Whether the popover is currently exiting (for animations).
|
|
58
|
+
*/
|
|
59
|
+
isExiting: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PopoverProps extends SlotProps {
|
|
63
|
+
/** The children of the component - can be JSX or render function. */
|
|
64
|
+
children?: RenderChildren<PopoverRenderProps>
|
|
65
|
+
/** The CSS className for the element. */
|
|
66
|
+
class?: ClassNameOrFunction<PopoverRenderProps>
|
|
67
|
+
/** The inline style for the element. */
|
|
68
|
+
style?: StyleOrFunction<PopoverRenderProps>
|
|
69
|
+
/**
|
|
70
|
+
* The name of the component that triggered the popover.
|
|
71
|
+
*/
|
|
72
|
+
trigger?: string
|
|
73
|
+
/**
|
|
74
|
+
* The ref for the element which the popover positions itself with respect to.
|
|
75
|
+
* Required when used standalone (not within a trigger component).
|
|
76
|
+
*/
|
|
77
|
+
triggerRef?: () => Element | null
|
|
78
|
+
/**
|
|
79
|
+
* The placement of the element with respect to its anchor element.
|
|
80
|
+
* @default 'bottom'
|
|
81
|
+
*/
|
|
82
|
+
placement?: Placement
|
|
83
|
+
/**
|
|
84
|
+
* The placement padding that should be applied between the element and its
|
|
85
|
+
* surrounding container.
|
|
86
|
+
* @default 12
|
|
87
|
+
*/
|
|
88
|
+
containerPadding?: number
|
|
89
|
+
/**
|
|
90
|
+
* The additional offset applied along the main axis between the element and its
|
|
91
|
+
* anchor element.
|
|
92
|
+
* @default 8
|
|
93
|
+
*/
|
|
94
|
+
offset?: number
|
|
95
|
+
/**
|
|
96
|
+
* The additional offset applied along the cross axis between the element and its
|
|
97
|
+
* anchor element.
|
|
98
|
+
* @default 0
|
|
99
|
+
*/
|
|
100
|
+
crossOffset?: number
|
|
101
|
+
/**
|
|
102
|
+
* Whether the element should flip its orientation when there is insufficient room.
|
|
103
|
+
* @default true
|
|
104
|
+
*/
|
|
105
|
+
shouldFlip?: boolean
|
|
106
|
+
/**
|
|
107
|
+
* Whether the popover is non-modal (allows interaction outside).
|
|
108
|
+
*/
|
|
109
|
+
isNonModal?: boolean
|
|
110
|
+
/**
|
|
111
|
+
* Whether pressing Escape to close should be disabled.
|
|
112
|
+
*/
|
|
113
|
+
isKeyboardDismissDisabled?: boolean
|
|
114
|
+
/**
|
|
115
|
+
* Filter for which outside interactions should close the popover.
|
|
116
|
+
*/
|
|
117
|
+
shouldCloseOnInteractOutside?: (element: Element) => boolean
|
|
118
|
+
/** Whether the popover is open (controlled). */
|
|
119
|
+
isOpen?: boolean
|
|
120
|
+
/** Whether the popover opens by default (uncontrolled). */
|
|
121
|
+
defaultOpen?: boolean
|
|
122
|
+
/** Handler called when the popover's open state changes. */
|
|
123
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
124
|
+
/** Whether the popover is entering (for animations). */
|
|
125
|
+
isEntering?: boolean
|
|
126
|
+
/** Whether the popover is exiting (for animations). */
|
|
127
|
+
isExiting?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface PopoverTriggerProps {
|
|
131
|
+
/** The children - should include a trigger and popover content. */
|
|
132
|
+
children: JSX.Element
|
|
133
|
+
/** Whether the popover is open (controlled). */
|
|
134
|
+
isOpen?: boolean
|
|
135
|
+
/** Whether the popover is open by default (uncontrolled). */
|
|
136
|
+
defaultOpen?: boolean
|
|
137
|
+
/** Callback when open state changes. */
|
|
138
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================
|
|
142
|
+
// CONTEXTS
|
|
143
|
+
// ============================================
|
|
144
|
+
|
|
145
|
+
// Re-export from shared contexts
|
|
146
|
+
export { PopoverTriggerContext, usePopoverTrigger, type PopoverTriggerContextValue } from './contexts'
|
|
147
|
+
|
|
148
|
+
// Internal context for placement
|
|
149
|
+
export const PopoverContext = createContext<{ placement: () => PlacementAxis | null } | null>(null)
|
|
150
|
+
|
|
151
|
+
// ============================================
|
|
152
|
+
// POPOVER TRIGGER COMPONENT
|
|
153
|
+
// ============================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A PopoverTrigger opens a popover when a trigger element is pressed.
|
|
157
|
+
* Children should include a trigger element (e.g. Button) and the Popover.
|
|
158
|
+
*/
|
|
159
|
+
export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
|
|
160
|
+
const [local] = splitProps(props, ['isOpen', 'defaultOpen', 'onOpenChange'])
|
|
161
|
+
|
|
162
|
+
// Create overlay trigger state
|
|
163
|
+
const state = createOverlayTriggerState({
|
|
164
|
+
get isOpen() {
|
|
165
|
+
return local.isOpen
|
|
166
|
+
},
|
|
167
|
+
get defaultOpen() {
|
|
168
|
+
return local.defaultOpen
|
|
169
|
+
},
|
|
170
|
+
onOpenChange: local.onOpenChange,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Ref for the trigger element
|
|
174
|
+
let triggerRef: HTMLElement | null = null
|
|
175
|
+
const triggerId = createUniqueId()
|
|
176
|
+
|
|
177
|
+
// Create overlay trigger (for side effects like scroll close)
|
|
178
|
+
createOverlayTrigger(
|
|
179
|
+
{ type: 'dialog' },
|
|
180
|
+
state,
|
|
181
|
+
() => triggerRef
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
// Track if triggerRef has been set (to prevent buttons inside Popover from overwriting it)
|
|
185
|
+
let triggerRefSet = false
|
|
186
|
+
|
|
187
|
+
// Context value
|
|
188
|
+
const contextValue = createMemo(() => ({
|
|
189
|
+
state: {
|
|
190
|
+
isOpen: () => state.isOpen(),
|
|
191
|
+
open: () => state.open(),
|
|
192
|
+
close: () => state.close(),
|
|
193
|
+
toggle: () => state.toggle(),
|
|
194
|
+
},
|
|
195
|
+
triggerRef: () => {
|
|
196
|
+
return triggerRef
|
|
197
|
+
},
|
|
198
|
+
setTriggerRef: (el: HTMLElement | null) => {
|
|
199
|
+
// Only set the trigger ref once - the first button to register is the actual trigger
|
|
200
|
+
// This prevents buttons inside the Popover content from overwriting the trigger ref
|
|
201
|
+
if (!triggerRefSet && el) {
|
|
202
|
+
triggerRef = el
|
|
203
|
+
triggerRefSet = true
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
triggerId,
|
|
207
|
+
trigger: 'PopoverTrigger',
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
// Just render children inside the provider - the Button component will detect
|
|
211
|
+
// the PopoverTriggerContext and handle toggling. The Popover component will
|
|
212
|
+
// detect the context and use it for open state.
|
|
213
|
+
//
|
|
214
|
+
// IMPORTANT: In SolidJS, JSX children are lazily evaluated when they're part
|
|
215
|
+
// of JSX expression. So {props.children} here means the children (Button, Popover)
|
|
216
|
+
// will be evaluated inside the Provider context.
|
|
217
|
+
return (
|
|
218
|
+
<PopoverTriggerContext.Provider value={contextValue()}>
|
|
219
|
+
{props.children}
|
|
220
|
+
</PopoverTriggerContext.Provider>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================
|
|
225
|
+
// POPOVER COMPONENT
|
|
226
|
+
// ============================================
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A popover is an overlay element positioned relative to a trigger.
|
|
230
|
+
*/
|
|
231
|
+
export function Popover(props: PopoverProps): JSX.Element {
|
|
232
|
+
if (isServer) {
|
|
233
|
+
// On the server, return null - popovers should not render during SSR
|
|
234
|
+
return null as unknown as JSX.Element
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const [local, rest] = splitProps(props, [
|
|
238
|
+
'class',
|
|
239
|
+
'style',
|
|
240
|
+
'trigger',
|
|
241
|
+
'triggerRef',
|
|
242
|
+
'placement',
|
|
243
|
+
'containerPadding',
|
|
244
|
+
'offset',
|
|
245
|
+
'crossOffset',
|
|
246
|
+
'shouldFlip',
|
|
247
|
+
'isNonModal',
|
|
248
|
+
'isKeyboardDismissDisabled',
|
|
249
|
+
'shouldCloseOnInteractOutside',
|
|
250
|
+
'isOpen',
|
|
251
|
+
'defaultOpen',
|
|
252
|
+
'onOpenChange',
|
|
253
|
+
'isEntering',
|
|
254
|
+
'isExiting',
|
|
255
|
+
])
|
|
256
|
+
|
|
257
|
+
let popoverRef!: HTMLDivElement
|
|
258
|
+
|
|
259
|
+
// Get trigger context if available
|
|
260
|
+
const triggerContext = useContext(PopoverTriggerContext)
|
|
261
|
+
|
|
262
|
+
// Internal state for uncontrolled mode
|
|
263
|
+
const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false)
|
|
264
|
+
|
|
265
|
+
// Determine if open
|
|
266
|
+
const isOpen = (): boolean => {
|
|
267
|
+
if (local.isOpen !== undefined) return local.isOpen
|
|
268
|
+
if (triggerContext) {
|
|
269
|
+
return triggerContext.state.isOpen()
|
|
270
|
+
}
|
|
271
|
+
return internalOpen()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const close = () => {
|
|
275
|
+
if (local.isOpen !== undefined) {
|
|
276
|
+
local.onOpenChange?.(false)
|
|
277
|
+
} else if (triggerContext) {
|
|
278
|
+
triggerContext.state.close()
|
|
279
|
+
local.onOpenChange?.(false)
|
|
280
|
+
} else {
|
|
281
|
+
setInternalOpen(false)
|
|
282
|
+
local.onOpenChange?.(false)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Get trigger ref
|
|
287
|
+
const getTriggerRef = () => {
|
|
288
|
+
if (local.triggerRef) return local.triggerRef()
|
|
289
|
+
if (triggerContext) return triggerContext.triggerRef()
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Signal to track placement after positioning
|
|
294
|
+
const [placement, setPlacement] = createSignal<PlacementAxis | null>(null)
|
|
295
|
+
// Signal to track position styles
|
|
296
|
+
// Start with visibility hidden, then show after positioning
|
|
297
|
+
const [positionStyles, setPositionStyles] = createSignal({
|
|
298
|
+
top: '0px',
|
|
299
|
+
left: '0px',
|
|
300
|
+
visibility: 'hidden' as 'hidden' | 'visible',
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// Handle keyboard dismiss (Escape key)
|
|
304
|
+
createEffect(() => {
|
|
305
|
+
if (!isOpen()) return
|
|
306
|
+
if (local.isKeyboardDismissDisabled) return
|
|
307
|
+
|
|
308
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
309
|
+
if (e.key === 'Escape') {
|
|
310
|
+
e.preventDefault()
|
|
311
|
+
e.stopPropagation()
|
|
312
|
+
close()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
317
|
+
onCleanup(() => document.removeEventListener('keydown', handleKeyDown))
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Handle click outside to dismiss popover
|
|
321
|
+
createEffect(() => {
|
|
322
|
+
if (!isOpen()) return
|
|
323
|
+
if (local.isNonModal) return // Non-modal popovers don't auto-dismiss on outside click
|
|
324
|
+
|
|
325
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
326
|
+
const target = e.target as Element
|
|
327
|
+
const trigger = getTriggerRef()
|
|
328
|
+
|
|
329
|
+
// Don't close if clicking inside the popover
|
|
330
|
+
if (popoverRef && popoverRef.contains(target)) {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Don't close if clicking the trigger (it will toggle)
|
|
335
|
+
if (trigger && trigger.contains(target)) {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check custom filter
|
|
340
|
+
if (local.shouldCloseOnInteractOutside && !local.shouldCloseOnInteractOutside(target)) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
close()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Use capture phase to catch clicks before they bubble
|
|
348
|
+
// Small delay to avoid closing on the same click that opened it
|
|
349
|
+
const timeoutId = setTimeout(() => {
|
|
350
|
+
document.addEventListener('mousedown', handleClickOutside, true)
|
|
351
|
+
}, 0)
|
|
352
|
+
|
|
353
|
+
onCleanup(() => {
|
|
354
|
+
clearTimeout(timeoutId)
|
|
355
|
+
document.removeEventListener('mousedown', handleClickOutside, true)
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Calculate position based on trigger element
|
|
360
|
+
// Using position: fixed so we use viewport coordinates directly from getBoundingClientRect
|
|
361
|
+
const updatePosition = () => {
|
|
362
|
+
const trigger = getTriggerRef()
|
|
363
|
+
const popover = popoverRef
|
|
364
|
+
if (!trigger || !popover) return
|
|
365
|
+
|
|
366
|
+
const triggerRect = trigger.getBoundingClientRect()
|
|
367
|
+
// Use offsetWidth/offsetHeight which are more reliable than getBoundingClientRect
|
|
368
|
+
// when the element might be positioned off-screen initially
|
|
369
|
+
const popoverWidth = popover.offsetWidth
|
|
370
|
+
const popoverHeight = popover.offsetHeight
|
|
371
|
+
const offset = local.offset ?? 8
|
|
372
|
+
|
|
373
|
+
let top = 0
|
|
374
|
+
let left = 0
|
|
375
|
+
const placementValue = local.placement ?? 'bottom'
|
|
376
|
+
|
|
377
|
+
// Using viewport coordinates for position: fixed
|
|
378
|
+
switch (placementValue) {
|
|
379
|
+
case 'top':
|
|
380
|
+
case 'top start':
|
|
381
|
+
case 'top end':
|
|
382
|
+
top = triggerRect.top - popoverHeight - offset
|
|
383
|
+
left = triggerRect.left + (triggerRect.width - popoverWidth) / 2
|
|
384
|
+
setPlacement('top')
|
|
385
|
+
break
|
|
386
|
+
case 'bottom':
|
|
387
|
+
case 'bottom start':
|
|
388
|
+
case 'bottom end':
|
|
389
|
+
top = triggerRect.bottom + offset
|
|
390
|
+
left = triggerRect.left + (triggerRect.width - popoverWidth) / 2
|
|
391
|
+
setPlacement('bottom')
|
|
392
|
+
break
|
|
393
|
+
case 'left':
|
|
394
|
+
case 'left top':
|
|
395
|
+
case 'left bottom':
|
|
396
|
+
top = triggerRect.top + (triggerRect.height - popoverHeight) / 2
|
|
397
|
+
left = triggerRect.left - popoverWidth - offset
|
|
398
|
+
setPlacement('left')
|
|
399
|
+
break
|
|
400
|
+
case 'right':
|
|
401
|
+
case 'right top':
|
|
402
|
+
case 'right bottom':
|
|
403
|
+
top = triggerRect.top + (triggerRect.height - popoverHeight) / 2
|
|
404
|
+
left = triggerRect.right + offset
|
|
405
|
+
setPlacement('right')
|
|
406
|
+
break
|
|
407
|
+
default:
|
|
408
|
+
top = triggerRect.bottom + offset
|
|
409
|
+
left = triggerRect.left + (triggerRect.width - popoverWidth) / 2
|
|
410
|
+
setPlacement('bottom')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
setPositionStyles({
|
|
414
|
+
top: `${top}px`,
|
|
415
|
+
left: `${left}px`,
|
|
416
|
+
visibility: 'visible',
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Set up positioning effect - runs when open and trigger ref is available
|
|
421
|
+
createEffect(() => {
|
|
422
|
+
if (!isOpen()) return
|
|
423
|
+
|
|
424
|
+
const triggerElement = getTriggerRef()
|
|
425
|
+
if (!triggerElement) return
|
|
426
|
+
|
|
427
|
+
// Initial position calculation - use requestAnimationFrame to ensure
|
|
428
|
+
// the element is rendered and has proper dimensions
|
|
429
|
+
requestAnimationFrame(() => {
|
|
430
|
+
updatePosition()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// Update on scroll/resize
|
|
434
|
+
window.addEventListener('scroll', updatePosition, true)
|
|
435
|
+
window.addEventListener('resize', updatePosition)
|
|
436
|
+
|
|
437
|
+
onCleanup(() => {
|
|
438
|
+
window.removeEventListener('scroll', updatePosition, true)
|
|
439
|
+
window.removeEventListener('resize', updatePosition)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
// Render props values
|
|
444
|
+
const renderValues = createMemo<PopoverRenderProps>(() => ({
|
|
445
|
+
trigger: local.trigger ?? triggerContext?.trigger ?? null,
|
|
446
|
+
placement: placement(),
|
|
447
|
+
isEntering: local.isEntering ?? false,
|
|
448
|
+
isExiting: local.isExiting ?? false,
|
|
449
|
+
}))
|
|
450
|
+
|
|
451
|
+
// Resolve render props
|
|
452
|
+
const renderProps = useRenderProps(
|
|
453
|
+
{
|
|
454
|
+
children: props.children,
|
|
455
|
+
class: local.class,
|
|
456
|
+
style: local.style,
|
|
457
|
+
defaultClassName: 'solidaria-Popover',
|
|
458
|
+
},
|
|
459
|
+
renderValues
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
// Filter DOM props
|
|
463
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
|
|
464
|
+
|
|
465
|
+
// Check if we should render with dialog role
|
|
466
|
+
const shouldBeDialog = () => !local.isNonModal
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<Show when={isOpen() || local.isExiting}>
|
|
470
|
+
<Portal>
|
|
471
|
+
<PopoverContext.Provider value={{ placement: () => placement() }}>
|
|
472
|
+
<FocusScope contain={shouldBeDialog()} restoreFocus autoFocus>
|
|
473
|
+
<div
|
|
474
|
+
{...domProps()}
|
|
475
|
+
ref={popoverRef}
|
|
476
|
+
role={shouldBeDialog() ? 'dialog' : undefined}
|
|
477
|
+
tabIndex={shouldBeDialog() ? -1 : undefined}
|
|
478
|
+
class={renderProps.class()}
|
|
479
|
+
style={{
|
|
480
|
+
position: 'fixed',
|
|
481
|
+
'z-index': 100000,
|
|
482
|
+
...positionStyles(),
|
|
483
|
+
...renderProps.style(),
|
|
484
|
+
}}
|
|
485
|
+
data-trigger={local.trigger ?? triggerContext?.trigger}
|
|
486
|
+
data-placement={placement()}
|
|
487
|
+
data-entering={dataAttr(local.isEntering)}
|
|
488
|
+
data-exiting={dataAttr(local.isExiting)}
|
|
489
|
+
>
|
|
490
|
+
{renderProps.renderChildren()}
|
|
491
|
+
</div>
|
|
492
|
+
</FocusScope>
|
|
493
|
+
</PopoverContext.Provider>
|
|
494
|
+
</Portal>
|
|
495
|
+
</Show>
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ============================================
|
|
500
|
+
// OVERLAY ARROW COMPONENT
|
|
501
|
+
// ============================================
|
|
502
|
+
|
|
503
|
+
export interface OverlayArrowProps {
|
|
504
|
+
/** The children - should be an SVG or element for the arrow. */
|
|
505
|
+
children?: JSX.Element | ((placement: PlacementAxis | null) => JSX.Element)
|
|
506
|
+
/** The CSS className. */
|
|
507
|
+
class?: string
|
|
508
|
+
/** The inline style. */
|
|
509
|
+
style?: JSX.CSSProperties
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* An arrow element that points towards the trigger.
|
|
514
|
+
*/
|
|
515
|
+
export function OverlayArrow(props: OverlayArrowProps): JSX.Element {
|
|
516
|
+
const popoverContext = useContext(PopoverContext)
|
|
517
|
+
const placement = () => popoverContext?.placement() ?? null
|
|
518
|
+
|
|
519
|
+
const resolveChildren = () => {
|
|
520
|
+
const children = props.children
|
|
521
|
+
if (typeof children === 'function') {
|
|
522
|
+
return children(placement())
|
|
523
|
+
}
|
|
524
|
+
return children
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<div
|
|
529
|
+
class={props.class}
|
|
530
|
+
style={props.style}
|
|
531
|
+
data-placement={placement()}
|
|
532
|
+
aria-hidden="true"
|
|
533
|
+
role="presentation"
|
|
534
|
+
>
|
|
535
|
+
{resolveChildren()}
|
|
536
|
+
</div>
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export default Popover
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* Pre-wired headless progress bar component that combines aria hooks.
|
|
5
|
+
* Port of react-aria-components/src/ProgressBar.tsx
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
type ParentProps,
|
|
11
|
+
createContext,
|
|
12
|
+
createMemo,
|
|
13
|
+
splitProps,
|
|
14
|
+
} from 'solid-js';
|
|
15
|
+
import {
|
|
16
|
+
createProgressBar,
|
|
17
|
+
type AriaProgressBarProps,
|
|
18
|
+
} from '@proyecto-viviana/solidaria';
|
|
19
|
+
import {
|
|
20
|
+
type RenderChildren,
|
|
21
|
+
type ClassNameOrFunction,
|
|
22
|
+
type StyleOrFunction,
|
|
23
|
+
type SlotProps,
|
|
24
|
+
useRenderProps,
|
|
25
|
+
filterDOMProps,
|
|
26
|
+
} from './utils';
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// TYPES
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
export interface ProgressBarRenderProps {
|
|
33
|
+
/** The value as a percentage between the minimum and maximum (0-100). */
|
|
34
|
+
percentage: number | undefined;
|
|
35
|
+
/** A formatted version of the value. */
|
|
36
|
+
valueText: string | undefined;
|
|
37
|
+
/** Whether the progress bar is indeterminate. */
|
|
38
|
+
isIndeterminate: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProgressBarProps
|
|
42
|
+
extends AriaProgressBarProps,
|
|
43
|
+
SlotProps {
|
|
44
|
+
/** The children of the component. A function may be provided to receive render props. */
|
|
45
|
+
children?: RenderChildren<ProgressBarRenderProps>;
|
|
46
|
+
/** The CSS className for the element. */
|
|
47
|
+
class?: ClassNameOrFunction<ProgressBarRenderProps>;
|
|
48
|
+
/** The inline style for the element. */
|
|
49
|
+
style?: StyleOrFunction<ProgressBarRenderProps>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// CONTEXT
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
export const ProgressBarContext = createContext<ProgressBarProps | null>(null);
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// UTILITIES
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
function clamp(value: number, min: number, max: number): number {
|
|
63
|
+
return Math.min(Math.max(value, min), max);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================
|
|
67
|
+
// PROGRESSBAR COMPONENT
|
|
68
|
+
// ============================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Progress bars show either determinate or indeterminate progress of an operation
|
|
72
|
+
* over time.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* <ProgressBar value={50}>
|
|
77
|
+
* {({ percentage, valueText }) => (
|
|
78
|
+
* <>
|
|
79
|
+
* <Label>Loading...</Label>
|
|
80
|
+
* <span>{valueText}</span>
|
|
81
|
+
* <div class="bar" style={{ width: `${percentage}%` }} />
|
|
82
|
+
* </>
|
|
83
|
+
* )}
|
|
84
|
+
* </ProgressBar>
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function ProgressBar(props: ParentProps<ProgressBarProps>): JSX.Element {
|
|
88
|
+
const [local, ariaProps] = splitProps(props, [
|
|
89
|
+
'children',
|
|
90
|
+
'class',
|
|
91
|
+
'style',
|
|
92
|
+
'slot',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
// Get values for calculations
|
|
96
|
+
const value = () => ariaProps.value ?? 0;
|
|
97
|
+
const minValue = () => ariaProps.minValue ?? 0;
|
|
98
|
+
const maxValue = () => ariaProps.maxValue ?? 100;
|
|
99
|
+
const isIndeterminate = () => ariaProps.isIndeterminate ?? false;
|
|
100
|
+
|
|
101
|
+
// Create progress bar aria props
|
|
102
|
+
const progressAria = createProgressBar({
|
|
103
|
+
get value() { return ariaProps.value; },
|
|
104
|
+
get minValue() { return ariaProps.minValue; },
|
|
105
|
+
get maxValue() { return ariaProps.maxValue; },
|
|
106
|
+
get valueLabel() { return ariaProps.valueLabel; },
|
|
107
|
+
get isIndeterminate() { return ariaProps.isIndeterminate; },
|
|
108
|
+
get formatOptions() { return ariaProps.formatOptions; },
|
|
109
|
+
get label() { return ariaProps.label; },
|
|
110
|
+
get 'aria-label'() { return ariaProps['aria-label']; },
|
|
111
|
+
get 'aria-labelledby'() { return ariaProps['aria-labelledby']; },
|
|
112
|
+
get 'aria-describedby'() { return ariaProps['aria-describedby']; },
|
|
113
|
+
get 'aria-details'() { return ariaProps['aria-details']; },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Calculate percentage
|
|
117
|
+
const percentage = createMemo(() => {
|
|
118
|
+
if (isIndeterminate()) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const clampedValue = clamp(value(), minValue(), maxValue());
|
|
122
|
+
return ((clampedValue - minValue()) / (maxValue() - minValue())) * 100;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Get value text from aria props
|
|
126
|
+
const valueText = createMemo(() => {
|
|
127
|
+
return progressAria.progressBarProps['aria-valuetext'] as string | undefined;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Render props values
|
|
131
|
+
const renderValues = createMemo<ProgressBarRenderProps>(() => ({
|
|
132
|
+
percentage: percentage(),
|
|
133
|
+
valueText: valueText(),
|
|
134
|
+
isIndeterminate: isIndeterminate(),
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Resolve render props
|
|
138
|
+
const renderProps = useRenderProps(
|
|
139
|
+
{
|
|
140
|
+
children: props.children,
|
|
141
|
+
class: local.class,
|
|
142
|
+
style: local.style,
|
|
143
|
+
defaultClassName: 'solidaria-ProgressBar',
|
|
144
|
+
},
|
|
145
|
+
renderValues
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Filter DOM props
|
|
149
|
+
const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
{...domProps()}
|
|
154
|
+
{...progressAria.progressBarProps}
|
|
155
|
+
class={renderProps.class()}
|
|
156
|
+
style={renderProps.style()}
|
|
157
|
+
slot={local.slot}
|
|
158
|
+
>
|
|
159
|
+
{renderProps.renderChildren()}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|