@proyecto-viviana/solidaria-components 0.1.3 → 0.2.2
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 +6 -2
- 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 +15 -15
- package/dist/index.js.map +2 -2
- package/dist/index.jsx +9056 -0
- package/dist/index.jsx.map +7 -0
- package/dist/index.ssr.js +15 -15
- package/dist/index.ssr.js.map +2 -2
- package/package.json +8 -10
- package/src/Autocomplete.tsx +0 -174
- package/src/Breadcrumbs.tsx +0 -264
- package/src/Button.tsx +0 -238
- package/src/Calendar.tsx +0 -471
- package/src/Checkbox.tsx +0 -387
- package/src/Color.tsx +0 -1370
- package/src/ComboBox.tsx +0 -824
- package/src/DateField.tsx +0 -337
- package/src/DatePicker.tsx +0 -367
- package/src/Dialog.tsx +0 -262
- package/src/Disclosure.tsx +0 -439
- package/src/GridList.tsx +0 -511
- package/src/Landmark.tsx +0 -203
- package/src/Link.tsx +0 -201
- package/src/ListBox.tsx +0 -346
- package/src/Menu.tsx +0 -544
- package/src/Meter.tsx +0 -157
- package/src/Modal.tsx +0 -433
- package/src/NumberField.tsx +0 -542
- package/src/Popover.tsx +0 -540
- package/src/ProgressBar.tsx +0 -162
- package/src/RadioGroup.tsx +0 -356
- package/src/RangeCalendar.tsx +0 -462
- package/src/SearchField.tsx +0 -479
- package/src/Select.tsx +0 -734
- package/src/Separator.tsx +0 -130
- package/src/Slider.tsx +0 -500
- package/src/Switch.tsx +0 -213
- package/src/Table.tsx +0 -857
- package/src/Tabs.tsx +0 -552
- package/src/TagGroup.tsx +0 -421
- package/src/TextField.tsx +0 -271
- package/src/TimeField.tsx +0 -455
- package/src/Toast.tsx +0 -503
- package/src/Toolbar.tsx +0 -160
- package/src/Tooltip.tsx +0 -423
- package/src/Tree.tsx +0 -551
- package/src/VisuallyHidden.tsx +0 -60
- package/src/contexts.ts +0 -74
- package/src/index.ts +0 -620
- package/src/utils.tsx +0 -329
package/src/TagGroup.tsx
DELETED
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TagGroup component for solidaria-components
|
|
3
|
-
*
|
|
4
|
-
* Pre-wired headless tag group component that combines aria hooks.
|
|
5
|
-
* Port of react-aria-components/src/TagGroup.tsx
|
|
6
|
-
*
|
|
7
|
-
* A tag group is a focusable list of labels, categories, keywords, filters, or other items,
|
|
8
|
-
* with support for keyboard navigation, selection, and removal.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
type JSX,
|
|
13
|
-
createContext,
|
|
14
|
-
createMemo,
|
|
15
|
-
createSignal,
|
|
16
|
-
splitProps,
|
|
17
|
-
useContext,
|
|
18
|
-
For,
|
|
19
|
-
Show,
|
|
20
|
-
} from 'solid-js';
|
|
21
|
-
import {
|
|
22
|
-
createTagGroup,
|
|
23
|
-
createTag,
|
|
24
|
-
type AriaTagGroupProps,
|
|
25
|
-
} from '@proyecto-viviana/solidaria';
|
|
26
|
-
import {
|
|
27
|
-
createListState,
|
|
28
|
-
type ListState,
|
|
29
|
-
type Key,
|
|
30
|
-
type SelectionMode,
|
|
31
|
-
type SelectionBehavior,
|
|
32
|
-
} from '@proyecto-viviana/solid-stately';
|
|
33
|
-
import {
|
|
34
|
-
type RenderChildren,
|
|
35
|
-
type ClassNameOrFunction,
|
|
36
|
-
type StyleOrFunction,
|
|
37
|
-
type SlotProps,
|
|
38
|
-
useRenderProps,
|
|
39
|
-
filterDOMProps,
|
|
40
|
-
dataAttr,
|
|
41
|
-
} from './utils';
|
|
42
|
-
|
|
43
|
-
// ============================================
|
|
44
|
-
// TYPES
|
|
45
|
-
// ============================================
|
|
46
|
-
|
|
47
|
-
export interface TagGroupRenderProps {
|
|
48
|
-
/** Whether the tag group is disabled. */
|
|
49
|
-
isDisabled: boolean;
|
|
50
|
-
/** Whether the tag list is empty. */
|
|
51
|
-
isEmpty: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface TagGroupProps
|
|
55
|
-
extends Omit<AriaTagGroupProps, 'id'>,
|
|
56
|
-
SlotProps {
|
|
57
|
-
/** The children of the component. */
|
|
58
|
-
children?: JSX.Element;
|
|
59
|
-
/** The CSS className for the element. */
|
|
60
|
-
class?: ClassNameOrFunction<TagGroupRenderProps>;
|
|
61
|
-
/** The inline style for the element. */
|
|
62
|
-
style?: StyleOrFunction<TagGroupRenderProps>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface TagListRenderProps {
|
|
66
|
-
/** Whether the tag list is empty. */
|
|
67
|
-
isEmpty: boolean;
|
|
68
|
-
/** Whether the tag list is focused. */
|
|
69
|
-
isFocused: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface TagListProps<T> extends SlotProps {
|
|
73
|
-
/** The items to display in the tag list. */
|
|
74
|
-
items: T[];
|
|
75
|
-
/** Function to render each item. */
|
|
76
|
-
children: (item: T) => JSX.Element;
|
|
77
|
-
/** The CSS className for the element. */
|
|
78
|
-
class?: ClassNameOrFunction<TagListRenderProps>;
|
|
79
|
-
/** The inline style for the element. */
|
|
80
|
-
style?: StyleOrFunction<TagListRenderProps>;
|
|
81
|
-
/** Content to render when the list is empty. */
|
|
82
|
-
renderEmptyState?: () => JSX.Element;
|
|
83
|
-
/** The selection mode for the tag list. */
|
|
84
|
-
selectionMode?: SelectionMode;
|
|
85
|
-
/** How selection behaves in the collection. */
|
|
86
|
-
selectionBehavior?: SelectionBehavior;
|
|
87
|
-
/** The currently selected keys (controlled). */
|
|
88
|
-
selectedKeys?: Iterable<Key>;
|
|
89
|
-
/** The default selected keys (uncontrolled). */
|
|
90
|
-
defaultSelectedKeys?: Iterable<Key>;
|
|
91
|
-
/** Handler called when selection changes. */
|
|
92
|
-
onSelectionChange?: (keys: 'all' | Set<Key>) => void;
|
|
93
|
-
/** Keys that are disabled. */
|
|
94
|
-
disabledKeys?: Iterable<Key>;
|
|
95
|
-
/** Function to get a unique key from an item. */
|
|
96
|
-
getKey?: (item: T) => Key;
|
|
97
|
-
/** Accessibility label. */
|
|
98
|
-
label?: string;
|
|
99
|
-
/** Custom aria-label. */
|
|
100
|
-
'aria-label'?: string;
|
|
101
|
-
/** Reference to external label element. */
|
|
102
|
-
'aria-labelledby'?: string;
|
|
103
|
-
/** Reference to description element. */
|
|
104
|
-
'aria-describedby'?: string;
|
|
105
|
-
/** Whether the tag list is disabled. */
|
|
106
|
-
isDisabled?: boolean;
|
|
107
|
-
/** Handler called when tags are removed. */
|
|
108
|
-
onRemove?: (keys: Set<Key>) => void;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface TagRenderProps {
|
|
112
|
-
/** Whether the tag is selected. */
|
|
113
|
-
isSelected: boolean;
|
|
114
|
-
/** Whether the tag is disabled. */
|
|
115
|
-
isDisabled: boolean;
|
|
116
|
-
/** Whether the tag is focused. */
|
|
117
|
-
isFocused: boolean;
|
|
118
|
-
/** Whether the tag is pressed. */
|
|
119
|
-
isPressed: boolean;
|
|
120
|
-
/** Whether the tag allows removal. */
|
|
121
|
-
allowsRemoving: boolean;
|
|
122
|
-
/** The selection mode. */
|
|
123
|
-
selectionMode: SelectionMode;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface TagProps extends SlotProps {
|
|
127
|
-
/** A unique key for this tag. */
|
|
128
|
-
id: Key;
|
|
129
|
-
/** Whether the tag is disabled. */
|
|
130
|
-
isDisabled?: boolean;
|
|
131
|
-
/** A text value for accessibility. */
|
|
132
|
-
textValue?: string;
|
|
133
|
-
/** The children of the component. A function may be provided to receive render props. */
|
|
134
|
-
children?: RenderChildren<TagRenderProps>;
|
|
135
|
-
/** The CSS className for the element. */
|
|
136
|
-
class?: ClassNameOrFunction<TagRenderProps>;
|
|
137
|
-
/** The inline style for the element. */
|
|
138
|
-
style?: StyleOrFunction<TagRenderProps>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ============================================
|
|
142
|
-
// CONTEXT
|
|
143
|
-
// ============================================
|
|
144
|
-
|
|
145
|
-
interface TagGroupContextValue {
|
|
146
|
-
state: ListState;
|
|
147
|
-
onRemove?: (keys: Set<Key>) => void;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export const TagGroupContext = createContext<TagGroupContextValue | null>(null);
|
|
151
|
-
export const TagListStateContext = createContext<ListState | null>(null);
|
|
152
|
-
|
|
153
|
-
export function useTagGroupContext(): TagGroupContextValue | null {
|
|
154
|
-
return useContext(TagGroupContext);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ============================================
|
|
158
|
-
// TAG GROUP COMPONENT
|
|
159
|
-
// ============================================
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* A tag group is a focusable list of labels, categories, keywords, filters, or other items,
|
|
163
|
-
* with support for keyboard navigation, selection, and removal.
|
|
164
|
-
*
|
|
165
|
-
* @example
|
|
166
|
-
* ```tsx
|
|
167
|
-
* <TagGroup label="Categories" onRemove={(keys) => removeItems(keys)}>
|
|
168
|
-
* <TagList items={items}>
|
|
169
|
-
* {(item) => <Tag id={item.id}>{item.name}</Tag>}
|
|
170
|
-
* </TagList>
|
|
171
|
-
* </TagGroup>
|
|
172
|
-
* ```
|
|
173
|
-
*/
|
|
174
|
-
export function TagGroup(props: TagGroupProps): JSX.Element {
|
|
175
|
-
const [local] = splitProps(props, [
|
|
176
|
-
'class',
|
|
177
|
-
'style',
|
|
178
|
-
'slot',
|
|
179
|
-
]);
|
|
180
|
-
|
|
181
|
-
// We need TagList to provide the state, so TagGroup just provides context
|
|
182
|
-
return (
|
|
183
|
-
<div
|
|
184
|
-
class={typeof local.class === 'string' ? local.class : 'solidaria-TagGroup'}
|
|
185
|
-
style={typeof local.style === 'object' ? local.style : undefined}
|
|
186
|
-
slot={local.slot}
|
|
187
|
-
>
|
|
188
|
-
{props.children}
|
|
189
|
-
</div>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ============================================
|
|
194
|
-
// TAG LIST COMPONENT
|
|
195
|
-
// ============================================
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* TagList contains the list of tags within a TagGroup.
|
|
199
|
-
*/
|
|
200
|
-
export function TagList<T extends { id?: Key; key?: Key }>(props: TagListProps<T>): JSX.Element {
|
|
201
|
-
const [local] = splitProps(props, [
|
|
202
|
-
'items',
|
|
203
|
-
'class',
|
|
204
|
-
'style',
|
|
205
|
-
'slot',
|
|
206
|
-
'renderEmptyState',
|
|
207
|
-
'selectionMode',
|
|
208
|
-
'selectionBehavior',
|
|
209
|
-
'selectedKeys',
|
|
210
|
-
'defaultSelectedKeys',
|
|
211
|
-
'onSelectionChange',
|
|
212
|
-
'disabledKeys',
|
|
213
|
-
'getKey',
|
|
214
|
-
'label',
|
|
215
|
-
'aria-label',
|
|
216
|
-
'aria-labelledby',
|
|
217
|
-
'aria-describedby',
|
|
218
|
-
'isDisabled',
|
|
219
|
-
'onRemove',
|
|
220
|
-
]);
|
|
221
|
-
|
|
222
|
-
// Create a ref for the grid
|
|
223
|
-
const [gridRef, setGridRef] = createSignal<HTMLDivElement | null>(null);
|
|
224
|
-
|
|
225
|
-
// Default getKey function
|
|
226
|
-
const getKey = (item: T): Key => {
|
|
227
|
-
if (local.getKey) return local.getKey(item);
|
|
228
|
-
if (item.id !== undefined) return item.id;
|
|
229
|
-
if (item.key !== undefined) return item.key;
|
|
230
|
-
return String(item);
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// Create list state
|
|
234
|
-
const state = createListState({
|
|
235
|
-
get items() { return local.items; },
|
|
236
|
-
getKey,
|
|
237
|
-
get selectionMode() { return local.selectionMode ?? 'none'; },
|
|
238
|
-
get selectionBehavior() { return local.selectionBehavior ?? 'toggle'; },
|
|
239
|
-
get selectedKeys() { return local.selectedKeys; },
|
|
240
|
-
get defaultSelectedKeys() { return local.defaultSelectedKeys; },
|
|
241
|
-
get onSelectionChange() { return local.onSelectionChange; },
|
|
242
|
-
get disabledKeys() { return local.disabledKeys; },
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Create tag group accessibility props
|
|
246
|
-
const tagGroupAria = createTagGroup(
|
|
247
|
-
{
|
|
248
|
-
get label() { return local.label; },
|
|
249
|
-
get 'aria-label'() { return local['aria-label']; },
|
|
250
|
-
get 'aria-labelledby'() { return local['aria-labelledby']; },
|
|
251
|
-
get 'aria-describedby'() { return local['aria-describedby']; },
|
|
252
|
-
get isDisabled() { return local.isDisabled; },
|
|
253
|
-
get onRemove() { return local.onRemove; },
|
|
254
|
-
},
|
|
255
|
-
state,
|
|
256
|
-
gridRef
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
// Track focus
|
|
260
|
-
const [isFocused, setIsFocused] = createSignal(false);
|
|
261
|
-
|
|
262
|
-
// Render props values
|
|
263
|
-
const renderValues = createMemo<TagListRenderProps>(() => ({
|
|
264
|
-
isEmpty: local.items.length === 0,
|
|
265
|
-
isFocused: isFocused(),
|
|
266
|
-
}));
|
|
267
|
-
|
|
268
|
-
// Resolve render props
|
|
269
|
-
const renderProps = useRenderProps(
|
|
270
|
-
{
|
|
271
|
-
class: local.class,
|
|
272
|
-
style: local.style,
|
|
273
|
-
defaultClassName: 'solidaria-TagList',
|
|
274
|
-
},
|
|
275
|
-
renderValues
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
// Context value
|
|
279
|
-
const contextValue: TagGroupContextValue = {
|
|
280
|
-
state,
|
|
281
|
-
get onRemove() { return local.onRemove; },
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
return (
|
|
285
|
-
<TagGroupContext.Provider value={contextValue}>
|
|
286
|
-
<TagListStateContext.Provider value={state}>
|
|
287
|
-
<div
|
|
288
|
-
ref={setGridRef}
|
|
289
|
-
{...tagGroupAria.gridProps}
|
|
290
|
-
class={renderProps.class()}
|
|
291
|
-
style={renderProps.style()}
|
|
292
|
-
onFocus={() => setIsFocused(true)}
|
|
293
|
-
onBlur={() => setIsFocused(false)}
|
|
294
|
-
data-empty={dataAttr(local.items.length === 0)}
|
|
295
|
-
data-focused={dataAttr(isFocused())}
|
|
296
|
-
>
|
|
297
|
-
<Show
|
|
298
|
-
when={local.items.length > 0}
|
|
299
|
-
fallback={local.renderEmptyState?.()}
|
|
300
|
-
>
|
|
301
|
-
<For each={local.items}>
|
|
302
|
-
{(item) => props.children(item)}
|
|
303
|
-
</For>
|
|
304
|
-
</Show>
|
|
305
|
-
</div>
|
|
306
|
-
</TagListStateContext.Provider>
|
|
307
|
-
</TagGroupContext.Provider>
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ============================================
|
|
312
|
-
// TAG COMPONENT
|
|
313
|
-
// ============================================
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* A Tag is an individual item within a TagList.
|
|
317
|
-
*/
|
|
318
|
-
export function Tag(props: TagProps): JSX.Element {
|
|
319
|
-
const [local, rest] = splitProps(props, [
|
|
320
|
-
'id',
|
|
321
|
-
'class',
|
|
322
|
-
'style',
|
|
323
|
-
'slot',
|
|
324
|
-
'isDisabled',
|
|
325
|
-
'textValue',
|
|
326
|
-
]);
|
|
327
|
-
|
|
328
|
-
const state = useContext(TagListStateContext);
|
|
329
|
-
|
|
330
|
-
// Create a ref for the tag
|
|
331
|
-
const [tagRef, setTagRef] = createSignal<HTMLDivElement | null>(null);
|
|
332
|
-
|
|
333
|
-
// Create tag accessibility props
|
|
334
|
-
const tagAria = createTag(
|
|
335
|
-
{
|
|
336
|
-
get key() { return local.id; },
|
|
337
|
-
get isDisabled() { return local.isDisabled; },
|
|
338
|
-
get textValue() { return local.textValue; },
|
|
339
|
-
},
|
|
340
|
-
state!,
|
|
341
|
-
tagRef
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
// Render props values
|
|
345
|
-
const renderValues = createMemo<TagRenderProps>(() => ({
|
|
346
|
-
isSelected: tagAria.isSelected,
|
|
347
|
-
isDisabled: tagAria.isDisabled,
|
|
348
|
-
isFocused: tagAria.isFocused,
|
|
349
|
-
isPressed: tagAria.isPressed,
|
|
350
|
-
allowsRemoving: tagAria.allowsRemoving,
|
|
351
|
-
selectionMode: state?.selectionMode() ?? 'none',
|
|
352
|
-
}));
|
|
353
|
-
|
|
354
|
-
// Resolve render props
|
|
355
|
-
const renderProps = useRenderProps(
|
|
356
|
-
{
|
|
357
|
-
children: props.children,
|
|
358
|
-
class: local.class,
|
|
359
|
-
style: local.style,
|
|
360
|
-
defaultClassName: 'solidaria-Tag',
|
|
361
|
-
},
|
|
362
|
-
renderValues
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
// Filter DOM props
|
|
366
|
-
const domProps = createMemo(() => filterDOMProps(rest, { global: true }));
|
|
367
|
-
|
|
368
|
-
return (
|
|
369
|
-
<div
|
|
370
|
-
ref={setTagRef}
|
|
371
|
-
{...domProps()}
|
|
372
|
-
{...tagAria.rowProps}
|
|
373
|
-
class={renderProps.class()}
|
|
374
|
-
style={renderProps.style()}
|
|
375
|
-
data-selected={dataAttr(tagAria.isSelected)}
|
|
376
|
-
data-disabled={dataAttr(tagAria.isDisabled)}
|
|
377
|
-
data-focused={dataAttr(tagAria.isFocused)}
|
|
378
|
-
data-pressed={dataAttr(tagAria.isPressed)}
|
|
379
|
-
data-allows-removing={dataAttr(tagAria.allowsRemoving)}
|
|
380
|
-
>
|
|
381
|
-
<div {...tagAria.gridCellProps} style={{ display: 'contents' }}>
|
|
382
|
-
{renderProps.renderChildren()}
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ============================================
|
|
389
|
-
// TAG REMOVE BUTTON COMPONENT
|
|
390
|
-
// ============================================
|
|
391
|
-
|
|
392
|
-
export interface TagRemoveButtonProps {
|
|
393
|
-
/** The children of the button (usually an X icon). */
|
|
394
|
-
children?: JSX.Element;
|
|
395
|
-
/** The CSS className for the element. */
|
|
396
|
-
class?: string;
|
|
397
|
-
/** The inline style for the element. */
|
|
398
|
-
style?: JSX.CSSProperties;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* TagRemoveButton is the button used to remove a tag.
|
|
403
|
-
* It should be placed inside a Tag component.
|
|
404
|
-
*/
|
|
405
|
-
export function TagRemoveButton(props: TagRemoveButtonProps): JSX.Element {
|
|
406
|
-
// This is a simplified version - in a full implementation,
|
|
407
|
-
// we'd get the remove button props from the Tag context
|
|
408
|
-
return (
|
|
409
|
-
<button
|
|
410
|
-
type="button"
|
|
411
|
-
class={props.class ?? 'solidaria-TagRemoveButton'}
|
|
412
|
-
style={props.style}
|
|
413
|
-
aria-label="Remove"
|
|
414
|
-
>
|
|
415
|
-
{props.children ?? '×'}
|
|
416
|
-
</button>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Re-export types
|
|
421
|
-
export type { Key, SelectionMode, SelectionBehavior };
|
package/src/TextField.tsx
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TextField component for solidaria-components
|
|
3
|
-
*
|
|
4
|
-
* A pre-wired headless text field that combines state + aria hooks.
|
|
5
|
-
* Port of react-aria-components/src/TextField.tsx
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
type JSX,
|
|
10
|
-
createContext,
|
|
11
|
-
useContext,
|
|
12
|
-
createMemo,
|
|
13
|
-
splitProps,
|
|
14
|
-
} from 'solid-js';
|
|
15
|
-
import {
|
|
16
|
-
createTextField,
|
|
17
|
-
createFocusRing,
|
|
18
|
-
createHover,
|
|
19
|
-
type AriaTextFieldProps,
|
|
20
|
-
} from '@proyecto-viviana/solidaria';
|
|
21
|
-
import { createTextFieldState } from '@proyecto-viviana/solid-stately';
|
|
22
|
-
import {
|
|
23
|
-
type RenderChildren,
|
|
24
|
-
type ClassNameOrFunction,
|
|
25
|
-
type StyleOrFunction,
|
|
26
|
-
type SlotProps,
|
|
27
|
-
useRenderProps,
|
|
28
|
-
filterDOMProps,
|
|
29
|
-
} from './utils';
|
|
30
|
-
|
|
31
|
-
// ============================================
|
|
32
|
-
// TYPES
|
|
33
|
-
// ============================================
|
|
34
|
-
|
|
35
|
-
export interface TextFieldRenderProps {
|
|
36
|
-
/** Whether the text field is disabled. */
|
|
37
|
-
isDisabled: boolean;
|
|
38
|
-
/** Whether the value is invalid. */
|
|
39
|
-
isInvalid: boolean;
|
|
40
|
-
/** Whether the text field is read only. */
|
|
41
|
-
isReadOnly: boolean;
|
|
42
|
-
/** Whether the text field is required. */
|
|
43
|
-
isRequired: boolean;
|
|
44
|
-
/** Whether the text field is currently hovered with a mouse. */
|
|
45
|
-
isHovered: boolean;
|
|
46
|
-
/** Whether the text field is focused, either via a mouse or keyboard. */
|
|
47
|
-
isFocused: boolean;
|
|
48
|
-
/** Whether the text field is keyboard focused. */
|
|
49
|
-
isFocusVisible: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface TextFieldProps
|
|
53
|
-
extends Omit<AriaTextFieldProps, 'children'>,
|
|
54
|
-
SlotProps {
|
|
55
|
-
/** The children of the component. A function may be provided to receive render props. */
|
|
56
|
-
children?: RenderChildren<TextFieldRenderProps>;
|
|
57
|
-
/** The CSS className for the element. */
|
|
58
|
-
class?: ClassNameOrFunction<TextFieldRenderProps>;
|
|
59
|
-
/** The inline style for the element. */
|
|
60
|
-
style?: StyleOrFunction<TextFieldRenderProps>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================
|
|
64
|
-
// CONTEXT
|
|
65
|
-
// ============================================
|
|
66
|
-
|
|
67
|
-
export interface TextFieldContextValue {
|
|
68
|
-
labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
|
|
69
|
-
inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
|
|
70
|
-
descriptionProps: JSX.HTMLAttributes<HTMLElement>;
|
|
71
|
-
errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
|
|
72
|
-
isInvalid: boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const TextFieldContext = createContext<TextFieldContextValue | null>(null);
|
|
76
|
-
|
|
77
|
-
// ============================================
|
|
78
|
-
// SUB-COMPONENTS
|
|
79
|
-
// ============================================
|
|
80
|
-
|
|
81
|
-
export interface LabelProps extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
|
|
82
|
-
children?: JSX.Element;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* A label element that automatically wires up to the parent TextField context.
|
|
87
|
-
* This enables the proper htmlFor/id relationship between label and input.
|
|
88
|
-
*/
|
|
89
|
-
export function Label(props: LabelProps): JSX.Element {
|
|
90
|
-
const context = useContext(TextFieldContext);
|
|
91
|
-
|
|
92
|
-
// Merge context labelProps with local props (local props take precedence)
|
|
93
|
-
const mergedProps = () => {
|
|
94
|
-
if (context) {
|
|
95
|
-
const { ref: _ref, ...contextLabelProps } = context.labelProps as Record<string, unknown>;
|
|
96
|
-
return { ...contextLabelProps, ...props };
|
|
97
|
-
}
|
|
98
|
-
return props;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return <label {...mergedProps()}>{props.children}</label>;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'children'> {}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* An input element that automatically wires up to the parent TextField context.
|
|
108
|
-
* This enables focus tracking, validation, and accessibility props to flow from
|
|
109
|
-
* the TextField to the actual input element.
|
|
110
|
-
*/
|
|
111
|
-
export function Input(props: InputProps): JSX.Element {
|
|
112
|
-
const context = useContext(TextFieldContext);
|
|
113
|
-
|
|
114
|
-
// Merge context inputProps with local props (local props take precedence)
|
|
115
|
-
const mergedProps = () => {
|
|
116
|
-
if (context) {
|
|
117
|
-
// Remove ref from context props to avoid conflicts
|
|
118
|
-
const { ref: _ref, ...contextInputProps } = context.inputProps as Record<string, unknown>;
|
|
119
|
-
return { ...contextInputProps, ...props };
|
|
120
|
-
}
|
|
121
|
-
return props;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return <input {...mergedProps()} />;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export interface TextAreaProps extends Omit<JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, 'children'> {}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* A textarea element that automatically wires up to the parent TextField context.
|
|
131
|
-
* This enables focus tracking, validation, and accessibility props to flow from
|
|
132
|
-
* the TextField to the actual textarea element.
|
|
133
|
-
*/
|
|
134
|
-
export function TextArea(props: TextAreaProps): JSX.Element {
|
|
135
|
-
const context = useContext(TextFieldContext);
|
|
136
|
-
|
|
137
|
-
// Merge context inputProps with local props (local props take precedence)
|
|
138
|
-
// Note: TextArea uses inputProps from context since it's an input variant
|
|
139
|
-
const mergedProps = () => {
|
|
140
|
-
if (context) {
|
|
141
|
-
const { ref: _ref, type: _type, ...contextInputProps } = context.inputProps as Record<string, unknown>;
|
|
142
|
-
return { ...contextInputProps, ...props };
|
|
143
|
-
}
|
|
144
|
-
return props;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
return <textarea {...mergedProps()} />;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ============================================
|
|
151
|
-
// COMPONENT
|
|
152
|
-
// ============================================
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* A text field allows a user to enter a plain text value with a keyboard.
|
|
156
|
-
*
|
|
157
|
-
* This is a headless component that provides accessibility and state management.
|
|
158
|
-
* Style it using the render props pattern or data attributes.
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* ```tsx
|
|
162
|
-
* <TextField>
|
|
163
|
-
* {({ isInvalid }) => (
|
|
164
|
-
* <>
|
|
165
|
-
* <Label>Email</Label>
|
|
166
|
-
* <Input class={isInvalid ? 'border-red-500' : 'border-gray-300'} />
|
|
167
|
-
* </>
|
|
168
|
-
* )}
|
|
169
|
-
* </TextField>
|
|
170
|
-
* ```
|
|
171
|
-
*/
|
|
172
|
-
export function TextField(props: TextFieldProps): JSX.Element {
|
|
173
|
-
// Split props
|
|
174
|
-
const [local, ariaProps] = splitProps(props, [
|
|
175
|
-
'children',
|
|
176
|
-
'class',
|
|
177
|
-
'style',
|
|
178
|
-
'slot',
|
|
179
|
-
]);
|
|
180
|
-
|
|
181
|
-
// Create text field state
|
|
182
|
-
// Use getters to ensure props are read lazily inside reactive contexts
|
|
183
|
-
const state = createTextFieldState({
|
|
184
|
-
get value() { return ariaProps.value; },
|
|
185
|
-
get defaultValue() { return ariaProps.defaultValue; },
|
|
186
|
-
get onChange() { return ariaProps.onChange; },
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Create text field aria props
|
|
190
|
-
const textFieldAria = createTextField(() => ({
|
|
191
|
-
...ariaProps,
|
|
192
|
-
value: state.value(),
|
|
193
|
-
onChange: state.setValue,
|
|
194
|
-
}));
|
|
195
|
-
|
|
196
|
-
// Create focus ring
|
|
197
|
-
const { isFocused, isFocusVisible, focusProps } = createFocusRing();
|
|
198
|
-
|
|
199
|
-
// Create hover
|
|
200
|
-
const { isHovered, hoverProps } = createHover({
|
|
201
|
-
get isDisabled() {
|
|
202
|
-
return ariaProps.isDisabled;
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Render props values
|
|
207
|
-
const renderValues = createMemo<TextFieldRenderProps>(() => ({
|
|
208
|
-
isDisabled: ariaProps.isDisabled || false,
|
|
209
|
-
isInvalid: textFieldAria.isInvalid,
|
|
210
|
-
isReadOnly: ariaProps.isReadOnly || false,
|
|
211
|
-
isRequired: ariaProps.isRequired || false,
|
|
212
|
-
isHovered: isHovered(),
|
|
213
|
-
isFocused: isFocused(),
|
|
214
|
-
isFocusVisible: isFocusVisible(),
|
|
215
|
-
}));
|
|
216
|
-
|
|
217
|
-
// Resolve render props
|
|
218
|
-
const renderProps = useRenderProps(
|
|
219
|
-
{
|
|
220
|
-
children: props.children,
|
|
221
|
-
class: local.class,
|
|
222
|
-
style: local.style,
|
|
223
|
-
defaultClassName: 'solidaria-TextField',
|
|
224
|
-
},
|
|
225
|
-
renderValues
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
// Filter DOM props
|
|
229
|
-
const domProps = createMemo(() => {
|
|
230
|
-
const filtered = filterDOMProps(ariaProps, { global: true });
|
|
231
|
-
delete (filtered as Record<string, unknown>).id;
|
|
232
|
-
return filtered;
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Remove ref from spread props to avoid type conflicts
|
|
236
|
-
const cleanHoverProps = () => {
|
|
237
|
-
const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
|
|
238
|
-
return rest;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// Context value for sub-components
|
|
242
|
-
// Note: We create the value object directly (not in a memo) so it's available
|
|
243
|
-
// immediately when children access the context
|
|
244
|
-
const contextValue: TextFieldContextValue = {
|
|
245
|
-
labelProps: textFieldAria.labelProps as JSX.LabelHTMLAttributes<HTMLLabelElement>,
|
|
246
|
-
inputProps: { ...textFieldAria.inputProps, ...focusProps } as JSX.InputHTMLAttributes<HTMLInputElement>,
|
|
247
|
-
descriptionProps: textFieldAria.descriptionProps as JSX.HTMLAttributes<HTMLElement>,
|
|
248
|
-
errorMessageProps: textFieldAria.errorMessageProps as JSX.HTMLAttributes<HTMLElement>,
|
|
249
|
-
isInvalid: textFieldAria.isInvalid,
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
return (
|
|
253
|
-
<TextFieldContext.Provider value={contextValue}>
|
|
254
|
-
<div
|
|
255
|
-
{...domProps()}
|
|
256
|
-
{...cleanHoverProps()}
|
|
257
|
-
class={renderProps.class()}
|
|
258
|
-
style={renderProps.style()}
|
|
259
|
-
data-disabled={ariaProps.isDisabled || undefined}
|
|
260
|
-
data-invalid={textFieldAria.isInvalid || undefined}
|
|
261
|
-
data-readonly={ariaProps.isReadOnly || undefined}
|
|
262
|
-
data-required={ariaProps.isRequired || undefined}
|
|
263
|
-
data-hovered={isHovered() || undefined}
|
|
264
|
-
data-focused={isFocused() || undefined}
|
|
265
|
-
data-focus-visible={isFocusVisible() || undefined}
|
|
266
|
-
>
|
|
267
|
-
{renderProps.renderChildren()}
|
|
268
|
-
</div>
|
|
269
|
-
</TextFieldContext.Provider>
|
|
270
|
-
);
|
|
271
|
-
}
|