@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/TimeField.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  } from 'solid-js';
18
18
  import {
19
19
  createTimeField,
20
+ createTimeSegment,
20
21
  type AriaTimeFieldProps,
21
22
  } from '@proyecto-viviana/solidaria';
22
23
  import {
@@ -109,9 +110,20 @@ export interface TimeSegmentProps extends SlotProps {
109
110
  // CONTEXT
110
111
  // ============================================
111
112
 
112
- export const TimeFieldContext = createContext<TimeFieldState<TimeValue> | null>(null);
113
+ export interface TimeFieldContextValue {
114
+ state: TimeFieldState<TimeValue>;
115
+ aria: {
116
+ labelProps: Record<string, unknown>;
117
+ inputProps: Record<string, unknown>;
118
+ descriptionProps: Record<string, unknown>;
119
+ errorMessageProps: Record<string, unknown>;
120
+ };
121
+ }
113
122
 
114
- export function useTimeFieldContext(): TimeFieldState<TimeValue> {
123
+ export const TimeFieldContext = createContext<TimeFieldContextValue | null>(null);
124
+ export const TimeFieldStateContext = createContext<TimeFieldState<TimeValue> | null>(null);
125
+
126
+ function useTimeFieldContextValue(): TimeFieldContextValue {
115
127
  const context = useContext(TimeFieldContext);
116
128
  if (!context) {
117
129
  throw new Error('TimeField components must be used within a TimeField');
@@ -119,6 +131,10 @@ export function useTimeFieldContext(): TimeFieldState<TimeValue> {
119
131
  return context;
120
132
  }
121
133
 
134
+ export function useTimeFieldContext(): TimeFieldState<TimeValue> {
135
+ return useTimeFieldContextValue().state;
136
+ }
137
+
122
138
  // ============================================
123
139
  // TIME FIELD COMPONENT
124
140
  // ============================================
@@ -205,20 +221,32 @@ function TimeFieldInner<T extends TimeValue = TimeValue>(
205
221
  );
206
222
 
207
223
  return (
208
- <TimeFieldContext.Provider value={state as unknown as TimeFieldState<TimeValue>}>
209
- <div
210
- ref={setFieldRef}
211
- {...fieldAria.fieldProps}
212
- class={renderProps.class()}
213
- style={renderProps.style()}
214
- data-disabled={dataAttr(state.isDisabled())}
215
- data-readonly={dataAttr(state.isReadOnly())}
216
- data-required={dataAttr(state.isRequired())}
217
- data-invalid={dataAttr(state.isInvalid())}
224
+ <TimeFieldStateContext.Provider value={state as unknown as TimeFieldState<TimeValue>}>
225
+ <TimeFieldContext.Provider
226
+ value={{
227
+ state: state as unknown as TimeFieldState<TimeValue>,
228
+ aria: {
229
+ labelProps: fieldAria.labelProps,
230
+ inputProps: fieldAria.inputProps,
231
+ descriptionProps: fieldAria.descriptionProps,
232
+ errorMessageProps: fieldAria.errorMessageProps,
233
+ },
234
+ }}
218
235
  >
219
- {props.children as JSX.Element}
220
- </div>
221
- </TimeFieldContext.Provider>
236
+ <div
237
+ ref={setFieldRef}
238
+ {...fieldAria.fieldProps}
239
+ class={renderProps.class()}
240
+ style={renderProps.style()}
241
+ data-disabled={dataAttr(state.isDisabled())}
242
+ data-readonly={dataAttr(state.isReadOnly())}
243
+ data-required={dataAttr(state.isRequired())}
244
+ data-invalid={dataAttr(state.isInvalid())}
245
+ >
246
+ {props.children as JSX.Element}
247
+ </div>
248
+ </TimeFieldContext.Provider>
249
+ </TimeFieldStateContext.Provider>
222
250
  );
223
251
  }
224
252
 
@@ -230,7 +258,7 @@ function TimeFieldInner<T extends TimeValue = TimeValue>(
230
258
  * The input area containing time segments.
231
259
  */
232
260
  export function TimeInput(props: TimeInputProps): JSX.Element {
233
- const state = useTimeFieldContext();
261
+ const { state, aria } = useTimeFieldContextValue();
234
262
  const [isFocused, setIsFocused] = createSignal(false);
235
263
 
236
264
  // Render props values
@@ -251,7 +279,7 @@ export function TimeInput(props: TimeInputProps): JSX.Element {
251
279
 
252
280
  return (
253
281
  <div
254
- role="presentation"
282
+ {...aria.inputProps}
255
283
  class={renderProps.class()}
256
284
  style={renderProps.style()}
257
285
  data-disabled={dataAttr(state.isDisabled())}
@@ -275,126 +303,21 @@ export function TimeInput(props: TimeInputProps): JSX.Element {
275
303
  */
276
304
  export function TimeSegment(props: TimeSegmentProps): JSX.Element {
277
305
  const state = useTimeFieldContext();
278
- const [_segmentRef, setSegmentRef] = createSignal<HTMLDivElement | null>(null);
279
-
280
- // Create segment ARIA props
281
- // We use a simplified version for time segments
282
- const [isFocused, setIsFocused] = createSignal(false);
283
- const [enteredKeys, setEnteredKeys] = createSignal('');
284
-
285
- const isEditable = createMemo(() => {
286
- const seg = props.segment;
287
- return seg.isEditable && !state.isDisabled() && !state.isReadOnly();
288
- });
289
-
290
- const handleKeyDown = (e: KeyboardEvent) => {
291
- if (!isEditable()) return;
292
-
293
- const seg = props.segment;
294
- const type = seg.type;
295
-
296
- if (type === 'literal') return;
297
-
298
- switch (e.key) {
299
- case 'ArrowUp':
300
- e.preventDefault();
301
- state.incrementSegment(type);
302
- break;
303
- case 'ArrowDown':
304
- e.preventDefault();
305
- state.decrementSegment(type);
306
- break;
307
- case 'Backspace':
308
- case 'Delete':
309
- e.preventDefault();
310
- state.clearSegment(type);
311
- setEnteredKeys('');
312
- break;
313
- default:
314
- if (/^\d$/.test(e.key)) {
315
- e.preventDefault();
316
- const newKeys = enteredKeys() + e.key;
317
- const numValue = parseInt(newKeys, 10);
318
- const maxValue = seg.maxValue ?? 59;
319
- const minValue = seg.minValue ?? 0;
320
-
321
- if (numValue <= maxValue) {
322
- state.setSegment(type, numValue);
323
- if (numValue * 10 > maxValue || newKeys.length >= 2) {
324
- setEnteredKeys('');
325
- } else {
326
- setEnteredKeys(newKeys);
327
- }
328
- } else {
329
- const singleValue = parseInt(e.key, 10);
330
- if (singleValue >= minValue && singleValue <= maxValue) {
331
- state.setSegment(type, singleValue);
332
- }
333
- setEnteredKeys(e.key);
334
- }
335
- }
336
- break;
337
- }
338
- };
339
-
340
- const handleFocus = () => {
341
- setIsFocused(true);
342
- setEnteredKeys('');
343
- };
344
-
345
- const handleBlur = () => {
346
- setIsFocused(false);
347
- setEnteredKeys('');
348
- };
349
-
350
- // Segment props
351
- const segmentProps = createMemo(() => {
352
- const seg = props.segment;
353
- const type = seg.type;
306
+ const [segmentRef, setSegmentRef] = createSignal<HTMLDivElement | null>(null);
354
307
 
355
- if (type === 'literal') {
356
- return {
357
- 'aria-hidden': true,
358
- };
359
- }
360
-
361
- return {
362
- role: 'spinbutton' as const,
363
- tabIndex: isEditable() ? 0 : -1,
364
- 'aria-label': getTimeSegmentLabel(type),
365
- 'aria-valuenow': seg.value,
366
- 'aria-valuemin': seg.minValue,
367
- 'aria-valuemax': seg.maxValue,
368
- 'aria-valuetext': seg.isPlaceholder ? seg.placeholder : seg.text,
369
- 'aria-readonly': state.isReadOnly() || undefined,
370
- 'aria-disabled': state.isDisabled() || undefined,
371
- 'aria-invalid': state.isInvalid() || undefined,
372
- contentEditable: isEditable(),
373
- inputMode: 'numeric' as const,
374
- autoCorrect: 'off',
375
- enterKeyHint: 'next' as const,
376
- spellCheck: false,
377
- onKeyDown: handleKeyDown,
378
- onFocus: handleFocus,
379
- onBlur: handleBlur,
380
- onMouseDown: (e: MouseEvent) => {
381
- e.preventDefault();
382
- },
383
- };
384
- });
385
-
386
- const text = createMemo(() => {
387
- const seg = props.segment;
388
- return seg.isPlaceholder ? seg.placeholder : seg.text;
389
- });
308
+ const segmentAria = createTimeSegment(
309
+ { segment: props.segment },
310
+ state as unknown as TimeFieldState,
311
+ segmentRef
312
+ );
390
313
 
391
314
  // Render props values
392
315
  const renderValues = createMemo<TimeSegmentRenderProps>(() => ({
393
- isFocused: isFocused(),
394
- isEditable: isEditable(),
395
- isPlaceholder: props.segment.isPlaceholder,
316
+ isFocused: segmentAria.isFocused,
317
+ isEditable: segmentAria.isEditable,
318
+ isPlaceholder: segmentAria.isPlaceholder,
396
319
  type: props.segment.type,
397
- text: text(),
320
+ text: segmentAria.text,
398
321
  }));
399
322
 
400
323
  // Resolve render props
@@ -413,18 +336,18 @@ export function TimeSegment(props: TimeSegmentProps): JSX.Element {
413
336
  if (typeof props.children === 'function') {
414
337
  return renderProps.renderChildren();
415
338
  }
416
- return text();
339
+ return segmentAria.text;
417
340
  };
418
341
 
419
342
  return (
420
343
  <div
421
344
  ref={setSegmentRef}
422
- {...segmentProps()}
345
+ {...segmentAria.segmentProps}
423
346
  class={renderProps.class()}
424
347
  style={renderProps.style()}
425
- data-focused={dataAttr(isFocused())}
426
- data-editable={dataAttr(isEditable())}
427
- data-placeholder={dataAttr(props.segment.isPlaceholder)}
348
+ data-focused={dataAttr(segmentAria.isFocused)}
349
+ data-editable={dataAttr(segmentAria.isEditable)}
350
+ data-placeholder={dataAttr(segmentAria.isPlaceholder)}
428
351
  data-type={props.segment.type}
429
352
  >
430
353
  {getChildren()}
@@ -433,22 +356,49 @@ export function TimeSegment(props: TimeSegmentProps): JSX.Element {
433
356
  }
434
357
 
435
358
  // ============================================
436
- // HELPER FUNCTIONS
359
+ // LABEL / DESCRIPTION / ERROR
437
360
  // ============================================
438
361
 
439
- function getTimeSegmentLabel(type: TimeSegmentType['type']): string {
440
- switch (type) {
441
- case 'hour':
442
- return 'Hour';
443
- case 'minute':
444
- return 'Minute';
445
- case 'second':
446
- return 'Second';
447
- case 'dayPeriod':
448
- return 'AM/PM';
449
- default:
450
- return '';
451
- }
362
+ export interface TimeFieldLabelProps {
363
+ children?: JSX.Element;
364
+ class?: string;
365
+ }
366
+
367
+ export function TimeFieldLabel(props: TimeFieldLabelProps): JSX.Element {
368
+ const { aria } = useTimeFieldContextValue();
369
+ return (
370
+ <span {...aria.labelProps} class={props.class}>
371
+ {props.children}
372
+ </span>
373
+ );
374
+ }
375
+
376
+ export interface TimeFieldDescriptionProps {
377
+ children?: JSX.Element;
378
+ class?: string;
379
+ }
380
+
381
+ export function TimeFieldDescription(props: TimeFieldDescriptionProps): JSX.Element {
382
+ const { aria } = useTimeFieldContextValue();
383
+ return (
384
+ <p {...aria.descriptionProps} class={props.class}>
385
+ {props.children}
386
+ </p>
387
+ );
388
+ }
389
+
390
+ export interface TimeFieldErrorMessageProps {
391
+ children?: JSX.Element;
392
+ class?: string;
393
+ }
394
+
395
+ export function TimeFieldErrorMessage(props: TimeFieldErrorMessageProps): JSX.Element {
396
+ const { aria } = useTimeFieldContextValue();
397
+ return (
398
+ <p {...aria.errorMessageProps} class={props.class}>
399
+ {props.children}
400
+ </p>
401
+ );
452
402
  }
453
403
 
454
404
  // Re-export types
package/src/Toast.tsx CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  type JSX,
10
10
  createContext,
11
11
  createMemo,
12
+ createEffect,
13
+ onCleanup,
12
14
  splitProps,
13
15
  Show,
14
16
  useContext,
@@ -24,6 +26,7 @@ import {
24
26
  import {
25
27
  createToast,
26
28
  createToastRegion,
29
+ useUNSAFE_PortalContext,
27
30
  } from '@proyecto-viviana/solidaria';
28
31
  import {
29
32
  type RenderChildren,
@@ -101,6 +104,13 @@ export interface ToastProps {
101
104
 
102
105
  export const ToastContext = createContext<ToastState<ToastContent> | null>(null);
103
106
 
107
+ interface ToastAriaContextValue {
108
+ titleProps: JSX.HTMLAttributes<HTMLElement>;
109
+ descriptionProps: JSX.HTMLAttributes<HTMLElement>;
110
+ }
111
+
112
+ const ToastAriaContext = createContext<ToastAriaContextValue | null>(null);
113
+
104
114
  export function useToastContext(): ToastState<ToastContent> {
105
115
  const context = useContext(ToastContext);
106
116
  if (!context) {
@@ -116,7 +126,7 @@ export function useToastContext(): ToastState<ToastContent> {
116
126
  /** Default global toast queue that can be used for app-wide toasts. */
117
127
  export const globalToastQueue = new ToastQueue<ToastContent>({
118
128
  maxVisibleToasts: 5,
119
- hasExitAnimation: false, // TODO: Enable once exit animation handling is implemented
129
+ hasExitAnimation: true,
120
130
  });
121
131
 
122
132
  /**
@@ -202,6 +212,8 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
202
212
  'portal',
203
213
  'placement',
204
214
  ]);
215
+ const portalContext = useUNSAFE_PortalContext();
216
+ const portalContainer = () => portalContext.getContainer?.() ?? undefined;
205
217
 
206
218
  // Get state from context if not provided
207
219
  const contextState = useContext(ToastContext);
@@ -301,7 +313,7 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
301
313
  return (
302
314
  <Show when={hasToasts()}>
303
315
  <Show when={local.portal !== false} fallback={regionContent()}>
304
- <Portal>{regionContent()}</Portal>
316
+ <Portal mount={portalContainer()}>{regionContent()}</Portal>
305
317
  </Show>
306
318
  </Show>
307
319
  );
@@ -334,6 +346,8 @@ export function Toast(props: ToastProps): JSX.Element {
334
346
  'style',
335
347
  ]);
336
348
 
349
+ let toastRef!: HTMLDivElement;
350
+
337
351
  // Get state from context
338
352
  const state = useToastContext();
339
353
 
@@ -341,6 +355,8 @@ export function Toast(props: ToastProps): JSX.Element {
341
355
  const toastAria = createToast({
342
356
  toast: local.toast,
343
357
  state,
358
+ hasTitle: !!local.toast.content.title,
359
+ hasDescription: !!local.toast.content.description,
344
360
  });
345
361
 
346
362
  // Render props values
@@ -372,20 +388,92 @@ export function Toast(props: ToastProps): JSX.Element {
372
388
  return { 'pointer-events': 'auto' as const, ...custom } as JSX.CSSProperties;
373
389
  };
374
390
 
391
+ // Exit animation lifecycle:
392
+ // When animation becomes 'exiting', wait for CSS animations/transitions to finish,
393
+ // then call state.remove() to finalize removal from the queue.
394
+ // In JSDOM or when no animations are running, remove immediately.
395
+ // Reduced-motion is handled by CSS (shorter/no animations), so the lifecycle
396
+ // naturally completes faster when the user prefers reduced motion.
397
+ createEffect(() => {
398
+ if (local.toast.animation !== 'exiting') return;
399
+ if (!toastRef) {
400
+ state.remove(local.toast.key);
401
+ return;
402
+ }
403
+
404
+ // Check if the element supports the Web Animations API
405
+ if (!('getAnimations' in toastRef)) {
406
+ state.remove(local.toast.key);
407
+ return;
408
+ }
409
+
410
+ const animations = toastRef.getAnimations();
411
+ if (animations.length === 0) {
412
+ // No CSS animations/transitions running - remove immediately
413
+ state.remove(local.toast.key);
414
+ return;
415
+ }
416
+
417
+ // Wait for all running animations to finish, then remove
418
+ let canceled = false;
419
+ Promise.all(animations.map((a) => a.finished))
420
+ .then(() => {
421
+ if (!canceled) {
422
+ state.remove(local.toast.key);
423
+ }
424
+ })
425
+ .catch(() => {
426
+ // Animation was canceled (e.g. element removed) - still clean up
427
+ if (!canceled) {
428
+ state.remove(local.toast.key);
429
+ }
430
+ });
431
+
432
+ onCleanup(() => {
433
+ canceled = true;
434
+ });
435
+ });
436
+
375
437
  // Extract ref from toastProps to avoid type conflicts
376
438
  const { ref: _ref, ...cleanToastProps } = toastAria.toastProps as Record<string, unknown>;
377
439
 
440
+ // Ensure ARIA title/description IDs are present on rendered sub-components,
441
+ // even when children are pre-composed outside the Toast provider owner.
442
+ createEffect(() => {
443
+ if (!toastRef) return;
444
+
445
+ const titleId = (toastAria.titleProps as Record<string, unknown>).id as string | undefined;
446
+ const descriptionId = (toastAria.descriptionProps as Record<string, unknown>).id as string | undefined;
447
+
448
+ if (titleId) {
449
+ const titleEl = toastRef.querySelector('[data-solidaria-toast-title]');
450
+ if (titleEl instanceof HTMLElement) {
451
+ titleEl.id = titleId;
452
+ }
453
+ }
454
+
455
+ if (descriptionId) {
456
+ const descriptionEl = toastRef.querySelector('[data-solidaria-toast-description]');
457
+ if (descriptionEl instanceof HTMLElement) {
458
+ descriptionEl.id = descriptionId;
459
+ }
460
+ }
461
+ });
462
+
378
463
  return (
379
- <div
380
- {...domProps()}
381
- {...cleanToastProps}
382
- class={renderProps.class()}
383
- style={mergedStyle()}
384
- data-animation={local.toast.animation}
385
- data-type={local.toast.content.type}
386
- >
387
- {renderProps.renderChildren()}
388
- </div>
464
+ <ToastAriaContext.Provider value={{ titleProps: toastAria.titleProps, descriptionProps: toastAria.descriptionProps }}>
465
+ <div
466
+ ref={toastRef}
467
+ {...domProps()}
468
+ {...cleanToastProps}
469
+ class={renderProps.class()}
470
+ style={mergedStyle()}
471
+ data-animation={local.toast.animation}
472
+ data-type={local.toast.content.type}
473
+ >
474
+ {renderProps.renderChildren()}
475
+ </div>
476
+ </ToastAriaContext.Provider>
389
477
  );
390
478
  }
391
479
 
@@ -403,8 +491,11 @@ export interface ToastTitleProps {
403
491
  * ToastTitle renders the toast title with proper accessibility attributes.
404
492
  */
405
493
  export function ToastTitle(props: ToastTitleProps): JSX.Element {
494
+ const context = useContext(ToastAriaContext);
495
+ const { ref: _ref, ...ariaTitleProps } = (context?.titleProps ?? {}) as Record<string, unknown>;
496
+
406
497
  return (
407
- <div class={props.class} style={props.style}>
498
+ <div data-solidaria-toast-title="" {...ariaTitleProps} class={props.class} style={props.style}>
408
499
  {props.children}
409
500
  </div>
410
501
  );
@@ -420,8 +511,11 @@ export interface ToastDescriptionProps {
420
511
  * ToastDescription renders the toast description with proper accessibility attributes.
421
512
  */
422
513
  export function ToastDescription(props: ToastDescriptionProps): JSX.Element {
514
+ const context = useContext(ToastAriaContext);
515
+ const { ref: _ref, ...ariaDescriptionProps } = (context?.descriptionProps ?? {}) as Record<string, unknown>;
516
+
423
517
  return (
424
- <div class={props.class} style={props.style}>
518
+ <div data-solidaria-toast-description="" {...ariaDescriptionProps} class={props.class} style={props.style}>
425
519
  {props.children}
426
520
  </div>
427
521
  );
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ToggleButton component for solidaria-components
3
+ *
4
+ * A pre-wired headless toggle button that combines pressed + selected state.
5
+ * Port direction: react-aria-components/src/ToggleButton.tsx
6
+ */
7
+
8
+ import {
9
+ type JSX,
10
+ createContext,
11
+ createMemo,
12
+ splitProps,
13
+ useContext,
14
+ } from 'solid-js';
15
+ import {
16
+ createToggleButton,
17
+ createToggleButtonGroupItem,
18
+ createFocusRing,
19
+ createHover,
20
+ mergeProps,
21
+ type AriaToggleButtonProps,
22
+ } from '@proyecto-viviana/solidaria';
23
+ import type { Key } from '@proyecto-viviana/solid-stately';
24
+ import {
25
+ type RenderChildren,
26
+ type ClassNameOrFunction,
27
+ type StyleOrFunction,
28
+ type SlotProps,
29
+ useRenderProps,
30
+ filterDOMProps,
31
+ } from './utils';
32
+ import { useToggleButtonGroupStateContext } from './ToggleButtonGroup';
33
+
34
+ export interface ToggleButtonRenderProps {
35
+ isHovered: boolean;
36
+ isPressed: boolean;
37
+ isFocused: boolean;
38
+ isFocusVisible: boolean;
39
+ isDisabled: boolean;
40
+ isSelected: boolean;
41
+ }
42
+
43
+ export interface ToggleButtonProps
44
+ extends Omit<AriaToggleButtonProps, 'children'>,
45
+ SlotProps {
46
+ /** Key used when inside ToggleButtonGroup selection state. */
47
+ toggleKey?: Key;
48
+ /** Preferred group key prop, parity with RAC item id usage. */
49
+ id?: Key;
50
+ children?: RenderChildren<ToggleButtonRenderProps>;
51
+ class?: ClassNameOrFunction<ToggleButtonRenderProps>;
52
+ style?: StyleOrFunction<ToggleButtonRenderProps>;
53
+ }
54
+
55
+ export const ToggleButtonContext = createContext<ToggleButtonProps | null>(null);
56
+
57
+ function resolveDisabledValue(
58
+ isDisabled: AriaToggleButtonProps['isDisabled']
59
+ ): boolean {
60
+ if (typeof isDisabled === 'function') {
61
+ return isDisabled();
62
+ }
63
+ return !!isDisabled;
64
+ }
65
+
66
+ export function ToggleButton(props: ToggleButtonProps): JSX.Element {
67
+ const contextProps = useContext(ToggleButtonContext);
68
+ const mergedProps = (contextProps ? mergeProps(contextProps, props) : props) as ToggleButtonProps;
69
+
70
+ const [local, ariaProps] = splitProps(mergedProps, [
71
+ 'children',
72
+ 'class',
73
+ 'style',
74
+ 'slot',
75
+ 'toggleKey',
76
+ 'id',
77
+ ]);
78
+ const groupState = useToggleButtonGroupStateContext();
79
+ const groupKey = local.id ?? local.toggleKey;
80
+
81
+ const toggleAria = groupState && groupKey != null
82
+ ? createToggleButtonGroupItem(
83
+ {
84
+ ...ariaProps,
85
+ id: groupKey,
86
+ },
87
+ groupState
88
+ )
89
+ : createToggleButton(ariaProps);
90
+
91
+ const isDisabled = () =>
92
+ resolveDisabledValue(ariaProps.isDisabled) || !!groupState?.isDisabled;
93
+
94
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing();
95
+ const { isHovered, hoverProps } = createHover({
96
+ get isDisabled() {
97
+ return isDisabled();
98
+ },
99
+ });
100
+
101
+ const renderValues = createMemo<ToggleButtonRenderProps>(() => ({
102
+ isHovered: isHovered(),
103
+ isPressed: toggleAria.isPressed(),
104
+ isFocused: isFocused(),
105
+ isFocusVisible: isFocusVisible(),
106
+ isDisabled: isDisabled(),
107
+ isSelected: toggleAria.isSelected(),
108
+ }));
109
+
110
+ const renderProps = useRenderProps(
111
+ {
112
+ children: local.children,
113
+ class: local.class,
114
+ style: local.style,
115
+ defaultClassName: 'solidaria-ToggleButton',
116
+ },
117
+ renderValues
118
+ );
119
+
120
+ const domProps = createMemo(() => {
121
+ const filtered = filterDOMProps(ariaProps, { global: true });
122
+ delete (filtered as Record<string, unknown>).onClick;
123
+ delete (filtered as Record<string, unknown>).id;
124
+ return filtered;
125
+ });
126
+
127
+ const cleanButtonProps = () => {
128
+ const { ref: _ref1, ...rest } = toggleAria.buttonProps as Record<string, unknown>;
129
+ return rest;
130
+ };
131
+ const cleanFocusProps = () => {
132
+ const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
133
+ return rest;
134
+ };
135
+ const cleanHoverProps = () => {
136
+ const { ref: _ref3, ...rest } = hoverProps as Record<string, unknown>;
137
+ return rest;
138
+ };
139
+
140
+ return (
141
+ <button
142
+ {...domProps()}
143
+ {...cleanButtonProps()}
144
+ {...cleanFocusProps()}
145
+ {...cleanHoverProps()}
146
+ class={renderProps.class()}
147
+ style={renderProps.style()}
148
+ slot={local.slot}
149
+ data-pressed={toggleAria.isPressed() || undefined}
150
+ data-hovered={isHovered() || undefined}
151
+ data-focused={isFocused() || undefined}
152
+ data-focus-visible={isFocusVisible() || undefined}
153
+ data-disabled={isDisabled() || undefined}
154
+ data-selected={toggleAria.isSelected() || undefined}
155
+ >
156
+ {renderProps.renderChildren()}
157
+ </button>
158
+ );
159
+ }