@proyecto-viviana/solidaria-components 0.2.5 → 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
@@ -0,0 +1,213 @@
1
+ /**
2
+ * DropZone primitive for solidaria-components.
3
+ *
4
+ * A drop target area for drag and drop operations.
5
+ * Parity target: react-aria-components/src/DropZone.tsx
6
+ */
7
+
8
+ import { type JSX, createContext, createMemo, createSignal, splitProps } from 'solid-js';
9
+ import {
10
+ createDrop,
11
+ createFocusRing,
12
+ createHover,
13
+ readFromDataTransfer,
14
+ type HoverEvents,
15
+ type AriaDropOptions,
16
+ } from '@proyecto-viviana/solidaria';
17
+ import {
18
+ type ClassNameOrFunction,
19
+ type StyleOrFunction,
20
+ type RenderChildren,
21
+ type SlotProps,
22
+ useRenderProps,
23
+ filterDOMProps,
24
+ } from './utils';
25
+ import { VisuallyHidden } from './VisuallyHidden';
26
+
27
+ export interface DropZoneRenderProps {
28
+ isHovered: boolean;
29
+ isFocused: boolean;
30
+ isFocusVisible: boolean;
31
+ isDropTarget: boolean;
32
+ isDisabled: boolean;
33
+ }
34
+
35
+ export interface DropZoneProps
36
+ extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'style' | 'onDrop'>,
37
+ Omit<AriaDropOptions, 'hasDropButton'>,
38
+ HoverEvents,
39
+ SlotProps {
40
+ children?: RenderChildren<DropZoneRenderProps>;
41
+ class?: ClassNameOrFunction<DropZoneRenderProps>;
42
+ style?: StyleOrFunction<DropZoneRenderProps>;
43
+ }
44
+
45
+ export const DropZoneContext = createContext<DropZoneProps | null>(null);
46
+ const DEFAULT_DROPZONE_LABEL = 'Drop files';
47
+ const FOCUSABLE_SELECTOR = [
48
+ 'a[href]',
49
+ 'button:not([disabled])',
50
+ 'input:not([disabled]):not([type="hidden"])',
51
+ 'select:not([disabled])',
52
+ 'textarea:not([disabled])',
53
+ '[tabindex]:not([tabindex="-1"])',
54
+ '[contenteditable="true"]',
55
+ ].join(',');
56
+
57
+ function isFocusableElement(target: Element): boolean {
58
+ return target instanceof HTMLElement && target.matches(FOCUSABLE_SELECTOR);
59
+ }
60
+
61
+ /**
62
+ * A drop zone is an area into which one or multiple objects can be dropped.
63
+ */
64
+ export function DropZone(props: DropZoneProps): JSX.Element {
65
+ const [local, dropProps, hoverEventProps, domProps] = splitProps(
66
+ props,
67
+ ['children', 'class', 'style', 'slot', 'aria-label', 'aria-labelledby'],
68
+ ['getDropOperation', 'getDropOperationForPoint', 'onDropEnter', 'onDropMove', 'onDropActivate', 'onDropExit', 'onDrop', 'isDisabled'],
69
+ ['onHoverStart', 'onHoverEnd', 'onHoverChange']
70
+ );
71
+
72
+ const [dropZoneRef, setDropZoneRef] = createSignal<HTMLDivElement | null>(null);
73
+ const [dropButtonRef, setDropButtonRef] = createSignal<HTMLButtonElement | null>(null);
74
+
75
+ const dropAria = createDrop(() => ({
76
+ getDropOperation: dropProps.getDropOperation,
77
+ getDropOperationForPoint: dropProps.getDropOperationForPoint,
78
+ onDropEnter: dropProps.onDropEnter,
79
+ onDropMove: dropProps.onDropMove,
80
+ onDropActivate: dropProps.onDropActivate,
81
+ onDropExit: dropProps.onDropExit,
82
+ onDrop: dropProps.onDrop,
83
+ hasDropButton: true,
84
+ isDisabled: dropProps.isDisabled,
85
+ }));
86
+
87
+ const { isHovered, hoverProps } = createHover(() => ({
88
+ isDisabled: dropProps.isDisabled,
89
+ onHoverStart: hoverEventProps.onHoverStart,
90
+ onHoverEnd: hoverEventProps.onHoverEnd,
91
+ onHoverChange: hoverEventProps.onHoverChange,
92
+ }));
93
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing();
94
+
95
+ const renderValues = createMemo<DropZoneRenderProps>(() => ({
96
+ isHovered: isHovered(),
97
+ isFocused: isFocused(),
98
+ isFocusVisible: isFocusVisible(),
99
+ isDropTarget: dropAria.isDropTarget,
100
+ isDisabled: !!dropProps.isDisabled,
101
+ }));
102
+
103
+ const renderProps = useRenderProps(
104
+ {
105
+ children: local.children,
106
+ class: local.class,
107
+ style: local.style,
108
+ defaultClassName: 'solidaria-DropZone',
109
+ },
110
+ renderValues
111
+ );
112
+
113
+ const filteredDomProps = createMemo(() => filterDOMProps(domProps, { global: true }));
114
+
115
+ const cleanDropProps = () => {
116
+ const { ref: _ref, ...rest } = dropAria.dropProps as Record<string, unknown>;
117
+ return rest;
118
+ };
119
+ const cleanHoverProps = () => {
120
+ const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
121
+ return rest;
122
+ };
123
+ const cleanFocusProps = () => {
124
+ const { ref: _ref, ...rest } = focusProps as Record<string, unknown>;
125
+ return rest;
126
+ };
127
+ const cleanDropButtonProps = () => {
128
+ const { ref: _ref, ...rest } = dropAria.dropButtonProps as Record<string, unknown>;
129
+ return rest;
130
+ };
131
+
132
+ const onRootClick: JSX.EventHandler<HTMLDivElement, MouseEvent> = (e) => {
133
+ const onClick = (filteredDomProps() as JSX.HTMLAttributes<HTMLDivElement>).onClick as
134
+ | JSX.EventHandler<HTMLDivElement, MouseEvent>
135
+ | undefined;
136
+ onClick?.(e);
137
+ if (e.defaultPrevented || dropProps.isDisabled) {
138
+ return;
139
+ }
140
+
141
+ const root = dropZoneRef();
142
+ const hiddenButton = dropButtonRef();
143
+ if (!root || !hiddenButton) {
144
+ return;
145
+ }
146
+
147
+ let target: Element | null = e.target instanceof Element ? e.target : root;
148
+ while (target && root.contains(target)) {
149
+ if (isFocusableElement(target)) {
150
+ return;
151
+ }
152
+ if (target === root) {
153
+ hiddenButton.focus();
154
+ return;
155
+ }
156
+ target = target.parentElement;
157
+ }
158
+ };
159
+
160
+ const onHiddenButtonPaste: JSX.EventHandler<HTMLButtonElement, ClipboardEvent> = (e) => {
161
+ if (dropProps.isDisabled || !e.clipboardData) {
162
+ return;
163
+ }
164
+
165
+ const items = readFromDataTransfer(e.clipboardData);
166
+ if (items.length === 0) {
167
+ return;
168
+ }
169
+
170
+ e.preventDefault();
171
+ dropProps.onDrop?.({
172
+ type: 'drop',
173
+ x: 0,
174
+ y: 0,
175
+ items,
176
+ dropOperation: 'copy',
177
+ });
178
+ };
179
+
180
+ const dropButtonAriaLabel = createMemo(
181
+ () => local['aria-label'] ?? (!local['aria-labelledby'] ? DEFAULT_DROPZONE_LABEL : undefined)
182
+ );
183
+
184
+ return (
185
+ <div
186
+ ref={setDropZoneRef}
187
+ {...filteredDomProps()}
188
+ {...cleanDropProps()}
189
+ {...cleanHoverProps()}
190
+ onClick={onRootClick}
191
+ class={renderProps.class()}
192
+ style={renderProps.style()}
193
+ slot={local.slot}
194
+ data-hovered={isHovered() || undefined}
195
+ data-focused={isFocused() || undefined}
196
+ data-focus-visible={isFocusVisible() || undefined}
197
+ data-drop-target={dropAria.isDropTarget || undefined}
198
+ data-disabled={dropProps.isDisabled || undefined}
199
+ >
200
+ <VisuallyHidden>
201
+ <button
202
+ ref={setDropButtonRef}
203
+ {...cleanDropButtonProps()}
204
+ {...cleanFocusProps()}
205
+ aria-label={dropButtonAriaLabel()}
206
+ aria-labelledby={local['aria-labelledby']}
207
+ onPaste={onHiddenButtonPaste}
208
+ />
209
+ </VisuallyHidden>
210
+ {renderProps.renderChildren()}
211
+ </div>
212
+ );
213
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * FieldError primitive for solidaria-components.
3
+ *
4
+ * Displays validation errors for a field from context or explicit validation prop.
5
+ * Port direction: react-aria-components/src/FieldError.tsx
6
+ */
7
+
8
+ import { type JSX, createContext, createMemo, splitProps, useContext } from 'solid-js';
9
+ import type { ValidationResult } from '@proyecto-viviana/solid-stately';
10
+ import {
11
+ type ClassNameOrFunction,
12
+ type StyleOrFunction,
13
+ type RenderChildren,
14
+ type SlotProps,
15
+ useRenderProps,
16
+ filterDOMProps,
17
+ } from './utils';
18
+
19
+ export type FieldErrorRenderProps = ValidationResult;
20
+
21
+ export interface FieldErrorProps
22
+ extends Omit<JSX.HTMLAttributes<HTMLElement>, 'children' | 'class' | 'style'>,
23
+ SlotProps {
24
+ /** Validation result. Falls back to context when omitted. */
25
+ validation?: ValidationResult | null;
26
+ /** The children of the component. */
27
+ children?: RenderChildren<FieldErrorRenderProps>;
28
+ /** The CSS className for the element. */
29
+ class?: ClassNameOrFunction<FieldErrorRenderProps>;
30
+ /** The inline style for the element. */
31
+ style?: StyleOrFunction<FieldErrorRenderProps>;
32
+ }
33
+
34
+ export const FieldErrorContext = createContext<ValidationResult | null>(null);
35
+
36
+ export function FieldError(props: FieldErrorProps): JSX.Element | null {
37
+ const contextValidation = useContext(FieldErrorContext);
38
+ const [local, domProps] = splitProps(props, ['validation', 'children', 'class', 'style', 'slot']);
39
+
40
+ const validation = createMemo<ValidationResult | null>(() => local.validation ?? contextValidation);
41
+ if (!validation()?.isInvalid) return null;
42
+
43
+ const renderProps = useRenderProps(
44
+ {
45
+ children: local.children ?? validation()!.validationErrors.join(' '),
46
+ class: local.class,
47
+ style: local.style,
48
+ defaultClassName: 'solidaria-FieldError',
49
+ },
50
+ () => validation()!
51
+ );
52
+
53
+ if (renderProps.renderChildren() == null) return null;
54
+
55
+ const filteredDomProps = filterDOMProps(domProps, { global: true });
56
+
57
+ return (
58
+ <div
59
+ {...filteredDomProps}
60
+ slot={local.slot ?? 'errorMessage'}
61
+ class={renderProps.class()}
62
+ style={renderProps.style()}
63
+ >
64
+ {renderProps.renderChildren()}
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * FileTrigger primitive for solidaria-components.
3
+ *
4
+ * Opens the native file picker from any pressable child.
5
+ * Parity target: react-aria-components/src/FileTrigger.tsx
6
+ */
7
+
8
+ import { type JSX, createSignal, splitProps } from 'solid-js';
9
+ import { createPress, type PressEvent } from '@proyecto-viviana/solidaria';
10
+
11
+ export interface FileTriggerProps
12
+ extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'type' | 'onChange' | 'children' | 'onSelect'> {
13
+ /** Mime types accepted by the file picker. */
14
+ acceptedFileTypes?: ReadonlyArray<string>;
15
+ /** Whether multiple files can be selected. */
16
+ allowsMultiple?: boolean;
17
+ /** Default camera capture mode for mobile devices. */
18
+ defaultCamera?: 'user' | 'environment';
19
+ /** Enables directory selection in supported browsers. */
20
+ acceptDirectory?: boolean;
21
+ /** Called when user selection changes. */
22
+ onSelect?: (files: FileList | null) => void;
23
+ /** Trigger content (typically a button). */
24
+ children?: JSX.Element;
25
+ }
26
+
27
+ /**
28
+ * A FileTrigger allows a user to access the file system using a custom trigger.
29
+ */
30
+ export function FileTrigger(props: FileTriggerProps): JSX.Element {
31
+ const [local, inputProps] = splitProps(props, [
32
+ 'acceptedFileTypes',
33
+ 'allowsMultiple',
34
+ 'defaultCamera',
35
+ 'acceptDirectory',
36
+ 'onSelect',
37
+ 'children',
38
+ 'disabled',
39
+ ]);
40
+
41
+ const [inputRef, setInputRef] = createSignal<HTMLInputElement | null>(null);
42
+
43
+ const openFilePicker = () => {
44
+ const input = inputRef();
45
+ if (!input) return;
46
+ if (input.value) input.value = '';
47
+ input.click();
48
+ };
49
+
50
+ const { pressProps } = createPress({
51
+ get isDisabled() {
52
+ return !!local.disabled;
53
+ },
54
+ onPress: (_e: PressEvent) => {
55
+ openFilePicker();
56
+ },
57
+ });
58
+
59
+ const onInputChange: JSX.EventHandler<HTMLInputElement, Event> = (e) => {
60
+ local.onSelect?.(e.currentTarget.files);
61
+ };
62
+
63
+ return (
64
+ <>
65
+ <span {...pressProps}>
66
+ {local.children}
67
+ </span>
68
+ <input
69
+ {...inputProps}
70
+ ref={setInputRef}
71
+ type="file"
72
+ style={{ display: 'none' }}
73
+ accept={local.acceptedFileTypes?.join(',')}
74
+ multiple={local.allowsMultiple}
75
+ capture={local.defaultCamera}
76
+ disabled={local.disabled}
77
+ // @ts-expect-error Non-standard attribute supported by WebKit browsers.
78
+ webkitdirectory={local.acceptDirectory ? '' : undefined}
79
+ onChange={onInputChange}
80
+ />
81
+ </>
82
+ );
83
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Focusable component for solidaria-components
3
+ *
4
+ * A render-prop component that wraps createFocusable to make an element
5
+ * focusable and capable of auto focus. Port of React Aria's Focusable.
6
+ */
7
+
8
+ import { type JSX, children as resolveChildren, createEffect, onCleanup, splitProps } from 'solid-js';
9
+ import { createFocusable, type CreateFocusableProps } from '@proyecto-viviana/solidaria';
10
+
11
+ // ============================================
12
+ // TYPES
13
+ // ============================================
14
+
15
+ export interface FocusableProps extends CreateFocusableProps {
16
+ /** A single child element to make focusable. */
17
+ children: JSX.Element;
18
+ /** Ref callback. */
19
+ ref?: HTMLElement | ((el: HTMLElement) => void);
20
+ }
21
+
22
+ // ============================================
23
+ // COMPONENT
24
+ // ============================================
25
+
26
+ /**
27
+ * Makes its child element focusable and capable of auto focus.
28
+ * Applies focus and keyboard event handlers from createFocusable.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <Focusable onFocus={() => console.log('focused')}>
33
+ * <div tabIndex={0}>Focusable content</div>
34
+ * </Focusable>
35
+ * ```
36
+ */
37
+ export function Focusable(props: FocusableProps): JSX.Element {
38
+ const [local, focusableProps] = splitProps(props, ['children', 'ref']);
39
+
40
+ let ref!: HTMLElement;
41
+ const { focusableProps: domProps } = createFocusable(focusableProps, () => ref);
42
+
43
+ const resolved = resolveChildren(() => local.children);
44
+
45
+ createEffect(() => {
46
+ const child = resolved() as HTMLElement;
47
+ if (child instanceof HTMLElement) {
48
+ ref = child;
49
+ // Forward ref
50
+ if (typeof local.ref === 'function') {
51
+ local.ref(child);
52
+ }
53
+
54
+ // Apply focusable props to the child element
55
+ const props = domProps;
56
+ const listeners: Array<[string, EventListener]> = [];
57
+ const addListener = (eventName: string, handler: unknown) => {
58
+ if (typeof handler === 'function') {
59
+ const listener = handler as EventListener;
60
+ child.addEventListener(eventName, listener);
61
+ listeners.push([eventName, listener]);
62
+ }
63
+ };
64
+
65
+ addListener('focus', props.onFocus);
66
+ addListener('blur', props.onBlur);
67
+ addListener('keydown', props.onKeyDown);
68
+ addListener('keyup', props.onKeyUp);
69
+
70
+ // Apply non-event focusable props (e.g. tabIndex/aria/data attrs).
71
+ // Keep explicit child tabIndex to mirror mergeProps(child.props precedence).
72
+ for (const [key, value] of Object.entries(props)) {
73
+ if (key === 'ref' || (key.startsWith('on') && typeof value === 'function')) continue;
74
+
75
+ if (key === 'tabIndex') {
76
+ if (child.hasAttribute('tabindex')) continue;
77
+ if (value == null || value === false) {
78
+ child.removeAttribute('tabindex');
79
+ } else {
80
+ child.tabIndex = Number(value);
81
+ }
82
+ continue;
83
+ }
84
+
85
+ if (key.startsWith('aria-') || key.startsWith('data-') || key === 'id' || key === 'role') {
86
+ if (child.hasAttribute(key)) continue;
87
+ if (value == null || value === false) {
88
+ child.removeAttribute(key);
89
+ } else if (value === true) {
90
+ child.setAttribute(key, '');
91
+ } else {
92
+ child.setAttribute(key, String(value));
93
+ }
94
+ }
95
+ }
96
+
97
+ onCleanup(() => {
98
+ for (const [eventName, listener] of listeners) {
99
+ child.removeEventListener(eventName, listener);
100
+ }
101
+ });
102
+ }
103
+ });
104
+
105
+ return <>{resolved()}</>;
106
+ }
package/src/Form.tsx ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Form primitive for solidaria-components.
3
+ *
4
+ * Provides form-level validation behavior and server validation context.
5
+ * Port direction: react-aria-components/src/Form.tsx
6
+ */
7
+
8
+ import { type JSX, createContext, splitProps } from 'solid-js';
9
+ import {
10
+ FormValidationContext,
11
+ type ValidationErrors,
12
+ type ValidationBehavior,
13
+ } from '@proyecto-viviana/solid-stately';
14
+ import {
15
+ type ClassNameOrFunction,
16
+ type StyleOrFunction,
17
+ type RenderChildren,
18
+ type SlotProps,
19
+ useRenderProps,
20
+ filterDOMProps,
21
+ } from './utils';
22
+
23
+ export interface FormRenderProps {
24
+ validationBehavior: ValidationBehavior;
25
+ }
26
+
27
+ export interface FormProps
28
+ extends Omit<JSX.FormHTMLAttributes<HTMLFormElement>, 'children' | 'class' | 'style'>,
29
+ SlotProps {
30
+ /** Server-side validation errors keyed by field name. */
31
+ validationErrors?: ValidationErrors;
32
+ /** Validation behavior mode. */
33
+ validationBehavior?: ValidationBehavior;
34
+ /** The children of the component. */
35
+ children?: RenderChildren<FormRenderProps>;
36
+ /** The CSS className for the element. */
37
+ class?: ClassNameOrFunction<FormRenderProps>;
38
+ /** The inline style for the element. */
39
+ style?: StyleOrFunction<FormRenderProps>;
40
+ }
41
+
42
+ export const FormContext = createContext<FormProps | null>(null);
43
+
44
+ export function Form(props: FormProps): JSX.Element {
45
+ const [local, domProps] = splitProps(props, [
46
+ 'validationErrors',
47
+ 'validationBehavior',
48
+ 'children',
49
+ 'class',
50
+ 'style',
51
+ 'slot',
52
+ ]);
53
+
54
+ const validationBehavior: ValidationBehavior = local.validationBehavior ?? 'native';
55
+ const errors = local.validationErrors ?? {};
56
+ const renderProps = useRenderProps(
57
+ {
58
+ children: local.children,
59
+ class: local.class,
60
+ style: local.style,
61
+ defaultClassName: 'solidaria-Form',
62
+ },
63
+ () => ({
64
+ validationBehavior,
65
+ })
66
+ );
67
+
68
+ const filteredDomProps = filterDOMProps(domProps, { global: true });
69
+
70
+ return (
71
+ <form
72
+ {...filteredDomProps}
73
+ noValidate={validationBehavior !== 'native'}
74
+ class={renderProps.class()}
75
+ style={renderProps.style()}
76
+ slot={local.slot}
77
+ >
78
+ <FormContext.Provider value={{ ...props, validationBehavior }}>
79
+ <FormValidationContext.Provider value={errors}>
80
+ {renderProps.renderChildren()}
81
+ </FormValidationContext.Provider>
82
+ </FormContext.Provider>
83
+ </form>
84
+ );
85
+ }