@proyecto-viviana/solidaria-components 0.2.4 → 0.2.9

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.
Files changed (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
package/src/Popover.tsx CHANGED
@@ -8,19 +8,21 @@
8
8
  import {
9
9
  type JSX,
10
10
  createContext,
11
+ createEffect,
11
12
  createMemo,
12
13
  createSignal,
13
14
  createUniqueId,
15
+ onCleanup,
14
16
  splitProps,
15
17
  useContext,
16
18
  Show,
17
- createEffect,
18
- onCleanup,
19
19
  } from 'solid-js'
20
20
  import { Portal, isServer } from 'solid-js/web'
21
21
  import {
22
22
  createOverlayTrigger,
23
+ createPopover,
23
24
  FocusScope,
25
+ useUNSAFE_PortalContext,
24
26
  type Placement,
25
27
  type PlacementAxis,
26
28
  } from '@proyecto-viviana/solidaria'
@@ -145,8 +147,13 @@ export interface PopoverTriggerProps {
145
147
  // Re-export from shared contexts
146
148
  export { PopoverTriggerContext, usePopoverTrigger, type PopoverTriggerContextValue } from './contexts'
147
149
 
148
- // Internal context for placement
149
- export const PopoverContext = createContext<{ placement: () => PlacementAxis | null } | null>(null)
150
+ interface PopoverContextValue {
151
+ placement: () => PlacementAxis | null
152
+ arrowProps: () => JSX.HTMLAttributes<HTMLElement>
153
+ }
154
+
155
+ // Internal context for placement + arrow
156
+ export const PopoverContext = createContext<PopoverContextValue | null>(null)
150
157
 
151
158
  // ============================================
152
159
  // POPOVER TRIGGER COMPONENT
@@ -181,9 +188,6 @@ export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
181
188
  () => triggerRef
182
189
  )
183
190
 
184
- // Track if triggerRef has been set (to prevent buttons inside Popover from overwriting it)
185
- let triggerRefSet = false
186
-
187
191
  // Context value
188
192
  const contextValue = createMemo(() => ({
189
193
  state: {
@@ -192,28 +196,17 @@ export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
192
196
  close: () => state.close(),
193
197
  toggle: () => state.toggle(),
194
198
  },
195
- triggerRef: () => {
196
- return triggerRef
197
- },
199
+ triggerRef: () => triggerRef,
198
200
  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) {
201
+ if (!el) return
202
+ if (!triggerRef || !triggerRef.isConnected) {
202
203
  triggerRef = el
203
- triggerRefSet = true
204
204
  }
205
205
  },
206
206
  triggerId,
207
207
  trigger: 'PopoverTrigger',
208
208
  }))
209
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
210
  return (
218
211
  <PopoverTriggerContext.Provider value={contextValue()}>
219
212
  {props.children}
@@ -290,160 +283,70 @@ export function Popover(props: PopoverProps): JSX.Element {
290
283
  return null
291
284
  }
292
285
 
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')
286
+ const popoverAria = createPopover(
287
+ {
288
+ triggerRef: getTriggerRef,
289
+ popoverRef: () => popoverRef ?? null,
290
+ get placement() {
291
+ return local.placement
292
+ },
293
+ get containerPadding() {
294
+ return local.containerPadding
295
+ },
296
+ get offset() {
297
+ return local.offset ?? 8
298
+ },
299
+ get crossOffset() {
300
+ return local.crossOffset
301
+ },
302
+ get shouldFlip() {
303
+ return local.shouldFlip
304
+ },
305
+ get isNonModal() {
306
+ return local.isNonModal
307
+ },
308
+ get isKeyboardDismissDisabled() {
309
+ return local.isKeyboardDismissDisabled
310
+ },
311
+ get shouldCloseOnInteractOutside() {
312
+ return local.shouldCloseOnInteractOutside
313
+ },
314
+ get trigger() {
315
+ return local.trigger ?? triggerContext?.trigger
316
+ },
317
+ },
318
+ {
319
+ isOpen,
320
+ open: () => {
321
+ if (local.isOpen !== undefined) {
322
+ local.onOpenChange?.(true)
323
+ } else if (triggerContext) {
324
+ triggerContext.state.open()
325
+ local.onOpenChange?.(true)
326
+ } else {
327
+ setInternalOpen(true)
328
+ local.onOpenChange?.(true)
329
+ }
330
+ },
331
+ close,
332
+ toggle: () => {
333
+ if (isOpen()) close()
334
+ else if (local.isOpen !== undefined) {
335
+ local.onOpenChange?.(true)
336
+ } else if (triggerContext) {
337
+ triggerContext.state.toggle()
338
+ } else {
339
+ setInternalOpen(true)
340
+ local.onOpenChange?.(true)
341
+ }
342
+ },
411
343
  }
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
- })
344
+ )
442
345
 
443
346
  // Render props values
444
347
  const renderValues = createMemo<PopoverRenderProps>(() => ({
445
348
  trigger: local.trigger ?? triggerContext?.trigger ?? null,
446
- placement: placement(),
349
+ placement: popoverAria.placement(),
447
350
  isEntering: local.isEntering ?? false,
448
351
  isExiting: local.isExiting ?? false,
449
352
  }))
@@ -462,28 +365,65 @@ export function Popover(props: PopoverProps): JSX.Element {
462
365
  // Filter DOM props
463
366
  const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
464
367
 
368
+ // Remove style/ref from spread props to avoid collisions
369
+ const cleanPopoverProps = () => {
370
+ const { style: _style, ref: _ref, ...remaining } = popoverAria.popoverProps as Record<string, unknown>
371
+ return remaining
372
+ }
373
+
374
+ const mergedStyle = (): JSX.CSSProperties => {
375
+ const ariaStyle = (popoverAria.popoverProps as Record<string, unknown>).style as JSX.CSSProperties | undefined
376
+ const renderStyle = renderProps.style() || {}
377
+ return {
378
+ ...(ariaStyle ?? {}),
379
+ ...renderStyle,
380
+ }
381
+ }
382
+
465
383
  // Check if we should render with dialog role
466
384
  const shouldBeDialog = () => !local.isNonModal
385
+ const portalContext = useUNSAFE_PortalContext()
386
+ const portalContainer = () => portalContext.getContainer?.() ?? undefined
387
+
388
+ // Ensure Escape handling works even when popover content has no focusable children.
389
+ createEffect(() => {
390
+ if (!isOpen() || !shouldBeDialog()) return
391
+ if (!popoverRef) return
392
+ if (document.activeElement !== popoverRef) {
393
+ popoverRef.focus()
394
+ }
395
+ })
396
+
397
+ // Fallback Escape handling for environments where focus is not moved into the popover.
398
+ createEffect(() => {
399
+ if (!isOpen()) return
400
+ if (local.isKeyboardDismissDisabled) return
401
+
402
+ const onKeyDown = (event: KeyboardEvent) => {
403
+ if (event.key !== 'Escape') return
404
+ if (event.defaultPrevented) return
405
+ close()
406
+ }
407
+
408
+ document.addEventListener('keydown', onKeyDown)
409
+ onCleanup(() => document.removeEventListener('keydown', onKeyDown))
410
+ })
467
411
 
468
412
  return (
469
413
  <Show when={isOpen() || local.isExiting}>
470
- <Portal>
471
- <PopoverContext.Provider value={{ placement: () => placement() }}>
414
+ <Portal mount={portalContainer()}>
415
+ <PopoverContext.Provider value={{ placement: popoverAria.placement, arrowProps: () => popoverAria.arrowProps }}>
472
416
  <FocusScope contain={shouldBeDialog()} restoreFocus autoFocus>
473
417
  <div
474
418
  {...domProps()}
419
+ {...cleanPopoverProps()}
475
420
  ref={popoverRef}
476
421
  role={shouldBeDialog() ? 'dialog' : undefined}
477
422
  tabIndex={shouldBeDialog() ? -1 : undefined}
478
423
  class={renderProps.class()}
479
- style={{
480
- position: 'fixed',
481
- 'z-index': 100000,
482
- ...positionStyles(),
483
- ...renderProps.style(),
484
- }}
424
+ style={mergedStyle()}
485
425
  data-trigger={local.trigger ?? triggerContext?.trigger}
486
- data-placement={placement()}
426
+ data-placement={popoverAria.placement()}
487
427
  data-entering={dataAttr(local.isEntering)}
488
428
  data-exiting={dataAttr(local.isExiting)}
489
429
  >
@@ -516,6 +456,21 @@ export function OverlayArrow(props: OverlayArrowProps): JSX.Element {
516
456
  const popoverContext = useContext(PopoverContext)
517
457
  const placement = () => popoverContext?.placement() ?? null
518
458
 
459
+ const cleanArrowProps = () => {
460
+ const contextArrowProps = popoverContext?.arrowProps() as Record<string, unknown> | undefined
461
+ if (!contextArrowProps) return {}
462
+ const { style: _style, ref: _ref, ...rest } = contextArrowProps
463
+ return rest
464
+ }
465
+
466
+ const mergedStyle = () => {
467
+ const contextStyle = (popoverContext?.arrowProps() as Record<string, unknown> | undefined)?.style as JSX.CSSProperties | undefined
468
+ return {
469
+ ...(contextStyle ?? {}),
470
+ ...(props.style ?? {}),
471
+ }
472
+ }
473
+
519
474
  const resolveChildren = () => {
520
475
  const children = props.children
521
476
  if (typeof children === 'function') {
@@ -526,8 +481,9 @@ export function OverlayArrow(props: OverlayArrowProps): JSX.Element {
526
481
 
527
482
  return (
528
483
  <div
484
+ {...cleanArrowProps()}
529
485
  class={props.class}
530
- style={props.style}
486
+ style={mergedStyle()}
531
487
  data-placement={placement()}
532
488
  aria-hidden="true"
533
489
  role="presentation"
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Pressable component for solidaria-components
3
+ *
4
+ * A render-prop component that wraps createPress + createFocusable
5
+ * to make an element pressable. Port of React Aria's Pressable.
6
+ */
7
+
8
+ import { type JSX, children as resolveChildren, createEffect, onCleanup, splitProps } from 'solid-js';
9
+ import {
10
+ createPress,
11
+ createFocusable,
12
+ type CreatePressProps,
13
+ type CreateFocusableProps,
14
+ } from '@proyecto-viviana/solidaria';
15
+
16
+ // ============================================
17
+ // TYPES
18
+ // ============================================
19
+
20
+ export interface PressableProps extends CreatePressProps {
21
+ /** A single child element to make pressable. */
22
+ children: JSX.Element;
23
+ /** Ref callback. */
24
+ ref?: HTMLElement | ((el: HTMLElement) => void);
25
+ }
26
+
27
+ // ============================================
28
+ // COMPONENT
29
+ // ============================================
30
+
31
+ /**
32
+ * Makes its child element pressable and focusable.
33
+ * Combines createPress and createFocusable for full interaction support.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <Pressable onPress={() => console.log('pressed')}>
38
+ * <div role="button" tabIndex={0}>Click me</div>
39
+ * </Pressable>
40
+ * ```
41
+ */
42
+ export function Pressable(props: PressableProps): JSX.Element {
43
+ const [local, pressProps] = splitProps(props, ['children', 'ref']);
44
+
45
+ let ref!: HTMLElement;
46
+ const { pressProps: domPressProps } = createPress(pressProps);
47
+ const { focusableProps: domFocusableProps } = createFocusable(pressProps as CreateFocusableProps, () => ref);
48
+
49
+ const resolved = resolveChildren(() => local.children);
50
+
51
+ createEffect(() => {
52
+ const child = resolved() as HTMLElement;
53
+ if (child instanceof HTMLElement) {
54
+ ref = child;
55
+ // Forward ref
56
+ if (typeof local.ref === 'function') {
57
+ local.ref(child);
58
+ }
59
+
60
+ // Apply press props
61
+ const allProps = { ...domFocusableProps, ...domPressProps };
62
+ const listeners: Array<[string, EventListener]> = [];
63
+ for (const [key, handler] of Object.entries(allProps)) {
64
+ if (key.startsWith('on') && typeof handler === 'function') {
65
+ const eventName = key.slice(2).toLowerCase();
66
+ const listener = handler as EventListener;
67
+ child.addEventListener(eventName, listener);
68
+ listeners.push([eventName, listener]);
69
+ }
70
+ }
71
+
72
+ // Apply non-event press/focusable props (e.g. tabIndex/data attrs).
73
+ // Keep explicit child tabIndex to mirror mergeProps(child.props precedence).
74
+ for (const [key, value] of Object.entries(allProps)) {
75
+ if (key === 'ref' || (key.startsWith('on') && typeof value === 'function')) continue;
76
+
77
+ if (key === 'tabIndex') {
78
+ if (child.hasAttribute('tabindex')) continue;
79
+ if (value == null || value === false) {
80
+ child.removeAttribute('tabindex');
81
+ } else {
82
+ child.tabIndex = Number(value);
83
+ }
84
+ continue;
85
+ }
86
+
87
+ if (key.startsWith('aria-') || key.startsWith('data-') || key === 'id' || key === 'role') {
88
+ if (child.hasAttribute(key)) continue;
89
+ if (value == null || value === false) {
90
+ child.removeAttribute(key);
91
+ } else if (value === true) {
92
+ child.setAttribute(key, '');
93
+ } else {
94
+ child.setAttribute(key, String(value));
95
+ }
96
+ }
97
+ }
98
+
99
+ onCleanup(() => {
100
+ for (const [eventName, listener] of listeners) {
101
+ child.removeEventListener(eventName, listener);
102
+ }
103
+ });
104
+ }
105
+ });
106
+
107
+ return <>{resolved()}</>;
108
+ }
@@ -7,7 +7,6 @@
7
7
 
8
8
  import {
9
9
  type JSX,
10
- type ParentProps,
11
10
  createContext,
12
11
  createMemo,
13
12
  splitProps,
@@ -63,6 +62,11 @@ function clamp(value: number, min: number, max: number): number {
63
62
  return Math.min(Math.max(value, min), max);
64
63
  }
65
64
 
65
+ function getSafeRange(min: number, max: number): number {
66
+ const range = max - min;
67
+ return Number.isFinite(range) && range > 0 ? range : 1;
68
+ }
69
+
66
70
  // ============================================
67
71
  // PROGRESSBAR COMPONENT
68
72
  // ============================================
@@ -84,7 +88,7 @@ function clamp(value: number, min: number, max: number): number {
84
88
  * </ProgressBar>
85
89
  * ```
86
90
  */
87
- export function ProgressBar(props: ParentProps<ProgressBarProps>): JSX.Element {
91
+ export function ProgressBar(props: ProgressBarProps): JSX.Element {
88
92
  const [local, ariaProps] = splitProps(props, [
89
93
  'children',
90
94
  'class',
@@ -119,7 +123,7 @@ export function ProgressBar(props: ParentProps<ProgressBarProps>): JSX.Element {
119
123
  return undefined;
120
124
  }
121
125
  const clampedValue = clamp(value(), minValue(), maxValue());
122
- return ((clampedValue - minValue()) / (maxValue() - minValue())) * 100;
126
+ return ((clampedValue - minValue()) / getSafeRange(minValue(), maxValue())) * 100;
123
127
  });
124
128
 
125
129
  // Get value text from aria props
@@ -28,6 +28,10 @@ import {
28
28
  type RadioGroupProps as RadioGroupStateProps,
29
29
  } from '@proyecto-viviana/solid-stately';
30
30
  import { VisuallyHidden } from './VisuallyHidden';
31
+ import {
32
+ SelectionIndicatorContext,
33
+ type SelectionIndicatorContextValue,
34
+ } from './SelectionIndicator';
31
35
  import {
32
36
  type RenderChildren,
33
37
  type ClassNameOrFunction,
@@ -267,6 +271,10 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
267
271
  renderValues
268
272
  );
269
273
 
274
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
275
+ isSelected: radioAria.isSelected,
276
+ }));
277
+
270
278
  // Filter DOM props
271
279
  const domProps = createMemo(() => {
272
280
  const filtered = filterDOMProps(ariaProps, { global: true });
@@ -294,31 +302,33 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
294
302
  };
295
303
 
296
304
  return (
297
- <label
298
- {...domProps()}
299
- {...cleanLabelProps()}
300
- {...cleanHoverProps()}
301
- class={renderProps.class()}
302
- style={renderProps.style()}
303
- data-selected={radioAria.isSelected() || undefined}
304
- data-pressed={radioAria.isPressed() || undefined}
305
- data-hovered={isHovered() || undefined}
306
- data-focused={isFocused() || undefined}
307
- data-focus-visible={isFocusVisible() || undefined}
308
- data-disabled={radioAria.isDisabled || undefined}
309
- data-readonly={state.isReadOnly || undefined}
310
- data-invalid={state.isInvalid || undefined}
311
- data-required={state.isRequired || undefined}
312
- >
313
- <VisuallyHidden>
314
- <input
315
- ref={(el) => (inputRef = el)}
316
- {...cleanInputProps()}
317
- {...cleanFocusProps()}
318
- />
319
- </VisuallyHidden>
320
- {renderProps.renderChildren()}
321
- </label>
305
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
306
+ <label
307
+ {...domProps()}
308
+ {...cleanLabelProps()}
309
+ {...cleanHoverProps()}
310
+ class={renderProps.class()}
311
+ style={renderProps.style()}
312
+ data-selected={radioAria.isSelected() || undefined}
313
+ data-pressed={radioAria.isPressed() || undefined}
314
+ data-hovered={isHovered() || undefined}
315
+ data-focused={isFocused() || undefined}
316
+ data-focus-visible={isFocusVisible() || undefined}
317
+ data-disabled={radioAria.isDisabled || undefined}
318
+ data-readonly={state.isReadOnly || undefined}
319
+ data-invalid={state.isInvalid || undefined}
320
+ data-required={state.isRequired || undefined}
321
+ >
322
+ <VisuallyHidden>
323
+ <input
324
+ ref={(el) => (inputRef = el)}
325
+ {...cleanInputProps()}
326
+ {...cleanFocusProps()}
327
+ />
328
+ </VisuallyHidden>
329
+ {renderProps.renderChildren()}
330
+ </label>
331
+ </SelectionIndicatorContext.Provider>
322
332
  );
323
333
  }
324
334