@proyecto-viviana/solidaria-components 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Color.d.ts +2 -6
- package/dist/Color.d.ts.map +1 -1
- package/dist/ComboBox.d.ts +3 -3
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/GridList.d.ts +2 -2
- package/dist/GridList.d.ts.map +1 -1
- package/dist/ListBox.d.ts +5 -5
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/Menu.d.ts +3 -3
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Select.d.ts +3 -3
- package/dist/Select.d.ts.map +1 -1
- package/dist/Table.d.ts +2 -2
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +1 -1
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/index.js +56 -56
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +56 -56
- package/dist/index.ssr.js.map +2 -2
- package/package.json +10 -8
- package/src/Autocomplete.tsx +174 -0
- package/src/Breadcrumbs.tsx +264 -0
- package/src/Button.tsx +238 -0
- package/src/Calendar.tsx +471 -0
- package/src/Checkbox.tsx +387 -0
- package/src/Color.tsx +1370 -0
- package/src/ComboBox.tsx +824 -0
- package/src/DateField.tsx +337 -0
- package/src/DatePicker.tsx +367 -0
- package/src/Dialog.tsx +262 -0
- package/src/Disclosure.tsx +439 -0
- package/src/GridList.tsx +511 -0
- package/src/Landmark.tsx +203 -0
- package/src/Link.tsx +201 -0
- package/src/ListBox.tsx +346 -0
- package/src/Menu.tsx +544 -0
- package/src/Meter.tsx +157 -0
- package/src/Modal.tsx +433 -0
- package/src/NumberField.tsx +542 -0
- package/src/Popover.tsx +540 -0
- package/src/ProgressBar.tsx +162 -0
- package/src/RadioGroup.tsx +356 -0
- package/src/RangeCalendar.tsx +462 -0
- package/src/SearchField.tsx +479 -0
- package/src/Select.tsx +734 -0
- package/src/Separator.tsx +130 -0
- package/src/Slider.tsx +500 -0
- package/src/Switch.tsx +213 -0
- package/src/Table.tsx +857 -0
- package/src/Tabs.tsx +552 -0
- package/src/TagGroup.tsx +421 -0
- package/src/TextField.tsx +271 -0
- package/src/TimeField.tsx +455 -0
- package/src/Toast.tsx +503 -0
- package/src/Toolbar.tsx +160 -0
- package/src/Tooltip.tsx +423 -0
- package/src/Tree.tsx +551 -0
- package/src/VisuallyHidden.tsx +60 -0
- package/src/contexts.ts +74 -0
- package/src/index.ts +620 -0
- package/src/utils.tsx +329 -0
- package/dist/index.jsx +0 -9056
- package/dist/index.jsx.map +0 -7
package/src/Tree.tsx
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* A pre-wired headless tree that combines state + aria hooks.
|
|
5
|
+
* Based on react-aria-components/src/Tree.tsx
|
|
6
|
+
*
|
|
7
|
+
* Tree displays hierarchical data with expandable/collapsible nodes,
|
|
8
|
+
* supporting keyboard navigation and selection.
|
|
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
|
+
createTree,
|
|
23
|
+
createTreeItem,
|
|
24
|
+
createTreeSelectionCheckbox,
|
|
25
|
+
createFocusRing,
|
|
26
|
+
createHover,
|
|
27
|
+
type AriaTreeProps,
|
|
28
|
+
} from '@proyecto-viviana/solidaria';
|
|
29
|
+
import {
|
|
30
|
+
createTreeState,
|
|
31
|
+
createTreeCollection,
|
|
32
|
+
type TreeState,
|
|
33
|
+
type TreeCollection,
|
|
34
|
+
type TreeNode,
|
|
35
|
+
type TreeItemData,
|
|
36
|
+
type Key,
|
|
37
|
+
} from '@proyecto-viviana/solid-stately';
|
|
38
|
+
import {
|
|
39
|
+
type RenderChildren,
|
|
40
|
+
type ClassNameOrFunction,
|
|
41
|
+
type StyleOrFunction,
|
|
42
|
+
type SlotProps,
|
|
43
|
+
useRenderProps,
|
|
44
|
+
filterDOMProps,
|
|
45
|
+
} from './utils';
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// TYPES
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
export interface TreeRenderProps {
|
|
52
|
+
/** Whether the tree has focus. */
|
|
53
|
+
isFocused: boolean;
|
|
54
|
+
/** Whether the tree has keyboard focus. */
|
|
55
|
+
isFocusVisible: boolean;
|
|
56
|
+
/** Whether the tree is disabled. */
|
|
57
|
+
isDisabled: boolean;
|
|
58
|
+
/** Whether the tree is empty. */
|
|
59
|
+
isEmpty: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TreeProps<T extends object> extends Omit<AriaTreeProps, 'children'>, SlotProps {
|
|
63
|
+
/** The hierarchical items to render in the tree. */
|
|
64
|
+
items: TreeItemData<T>[];
|
|
65
|
+
/** The selection mode. */
|
|
66
|
+
selectionMode?: 'none' | 'single' | 'multiple';
|
|
67
|
+
/** Keys of disabled items. */
|
|
68
|
+
disabledKeys?: Iterable<Key>;
|
|
69
|
+
/** Currently selected keys (controlled). */
|
|
70
|
+
selectedKeys?: 'all' | Iterable<Key>;
|
|
71
|
+
/** Default selected keys (uncontrolled). */
|
|
72
|
+
defaultSelectedKeys?: 'all' | Iterable<Key>;
|
|
73
|
+
/** Handler called when selection changes. */
|
|
74
|
+
onSelectionChange?: (keys: 'all' | Set<Key>) => void;
|
|
75
|
+
/** Currently expanded keys (controlled). */
|
|
76
|
+
expandedKeys?: Iterable<Key>;
|
|
77
|
+
/** Default expanded keys (uncontrolled). */
|
|
78
|
+
defaultExpandedKeys?: Iterable<Key>;
|
|
79
|
+
/** Handler called when expansion changes. */
|
|
80
|
+
onExpandedChange?: (keys: Set<Key>) => void;
|
|
81
|
+
/** The children of the component. A function provided to render each item. */
|
|
82
|
+
children: (item: TreeItemData<T>, state: TreeRenderItemState) => JSX.Element;
|
|
83
|
+
/** The CSS className for the element. */
|
|
84
|
+
class?: ClassNameOrFunction<TreeRenderProps>;
|
|
85
|
+
/** The inline style for the element. */
|
|
86
|
+
style?: StyleOrFunction<TreeRenderProps>;
|
|
87
|
+
/** A function to render when the tree is empty. */
|
|
88
|
+
renderEmptyState?: () => JSX.Element;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TreeRenderItemState {
|
|
92
|
+
/** Whether the item is expanded. */
|
|
93
|
+
isExpanded: boolean;
|
|
94
|
+
/** Whether the item is expandable (has children). */
|
|
95
|
+
isExpandable: boolean;
|
|
96
|
+
/** The nesting level (0 = root). */
|
|
97
|
+
level: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface TreeItemRenderProps {
|
|
101
|
+
/** Whether the item is selected. */
|
|
102
|
+
isSelected: boolean;
|
|
103
|
+
/** Whether the item is focused. */
|
|
104
|
+
isFocused: boolean;
|
|
105
|
+
/** Whether the item has keyboard focus. */
|
|
106
|
+
isFocusVisible: boolean;
|
|
107
|
+
/** Whether the item is pressed. */
|
|
108
|
+
isPressed: boolean;
|
|
109
|
+
/** Whether the item is hovered. */
|
|
110
|
+
isHovered: boolean;
|
|
111
|
+
/** Whether the item is disabled. */
|
|
112
|
+
isDisabled: boolean;
|
|
113
|
+
/** Whether the item is expanded. */
|
|
114
|
+
isExpanded: boolean;
|
|
115
|
+
/** Whether the item is expandable (has children). */
|
|
116
|
+
isExpandable: boolean;
|
|
117
|
+
/** The nesting level (0 = root). */
|
|
118
|
+
level: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TreeItemProps<T extends object> extends SlotProps {
|
|
122
|
+
/** The unique key for the item. */
|
|
123
|
+
id: Key;
|
|
124
|
+
/** The item value. */
|
|
125
|
+
item?: TreeItemData<T>;
|
|
126
|
+
/** The children of the item. A function may be provided to receive render props. */
|
|
127
|
+
children?: RenderChildren<TreeItemRenderProps>;
|
|
128
|
+
/** The CSS className for the element. */
|
|
129
|
+
class?: ClassNameOrFunction<TreeItemRenderProps>;
|
|
130
|
+
/** The inline style for the element. */
|
|
131
|
+
style?: StyleOrFunction<TreeItemRenderProps>;
|
|
132
|
+
/** The text value of the item (for accessibility). */
|
|
133
|
+
textValue?: string;
|
|
134
|
+
/** Handler called when the item is activated. */
|
|
135
|
+
onAction?: () => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TreeExpandButtonProps {
|
|
139
|
+
/** CSS class name for the button. */
|
|
140
|
+
class?: string;
|
|
141
|
+
/** CSS style for the button. */
|
|
142
|
+
style?: JSX.CSSProperties;
|
|
143
|
+
/** Children to render inside the button. */
|
|
144
|
+
children?: JSX.Element | ((props: { isExpanded: boolean }) => JSX.Element);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================
|
|
148
|
+
// CONTEXT
|
|
149
|
+
// ============================================
|
|
150
|
+
|
|
151
|
+
interface TreeContextValue<T extends object> {
|
|
152
|
+
state: TreeState<T, TreeCollection<T>>;
|
|
153
|
+
collection: TreeCollection<T>;
|
|
154
|
+
isDisabled: boolean;
|
|
155
|
+
renderItem: (item: TreeItemData<T>, state: TreeRenderItemState) => JSX.Element;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface TreeItemContextValue<T extends object> {
|
|
159
|
+
node: TreeNode<T>;
|
|
160
|
+
isExpanded: boolean;
|
|
161
|
+
isExpandable: boolean;
|
|
162
|
+
level: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const TreeContext = createContext<TreeContextValue<object> | null>(null);
|
|
166
|
+
export const TreeStateContext = createContext<TreeState<object, TreeCollection<object>> | null>(null);
|
|
167
|
+
export const TreeItemContext = createContext<TreeItemContextValue<object> | null>(null);
|
|
168
|
+
|
|
169
|
+
// ============================================
|
|
170
|
+
// COMPONENTS
|
|
171
|
+
// ============================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A tree displays hierarchical data with expandable/collapsible nodes,
|
|
175
|
+
* supporting keyboard navigation and selection.
|
|
176
|
+
*/
|
|
177
|
+
export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
|
|
178
|
+
const [local, stateProps, ariaProps] = splitProps(
|
|
179
|
+
props,
|
|
180
|
+
['class', 'style', 'slot', 'renderEmptyState'],
|
|
181
|
+
[
|
|
182
|
+
'items',
|
|
183
|
+
'disabledKeys',
|
|
184
|
+
'selectionMode',
|
|
185
|
+
'selectedKeys',
|
|
186
|
+
'defaultSelectedKeys',
|
|
187
|
+
'onSelectionChange',
|
|
188
|
+
'expandedKeys',
|
|
189
|
+
'defaultExpandedKeys',
|
|
190
|
+
'onExpandedChange',
|
|
191
|
+
]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Create ref signal
|
|
195
|
+
const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
|
|
196
|
+
|
|
197
|
+
// Create tree state
|
|
198
|
+
const state = createTreeState<T, TreeCollection<T>>(() => ({
|
|
199
|
+
collectionFactory: (expandedKeys) =>
|
|
200
|
+
createTreeCollection(stateProps.items, expandedKeys) as TreeCollection<T>,
|
|
201
|
+
disabledKeys: stateProps.disabledKeys,
|
|
202
|
+
selectionMode: stateProps.selectionMode,
|
|
203
|
+
selectedKeys: stateProps.selectedKeys,
|
|
204
|
+
defaultSelectedKeys: stateProps.defaultSelectedKeys,
|
|
205
|
+
onSelectionChange: stateProps.onSelectionChange,
|
|
206
|
+
expandedKeys: stateProps.expandedKeys,
|
|
207
|
+
defaultExpandedKeys: stateProps.defaultExpandedKeys,
|
|
208
|
+
onExpandedChange: stateProps.onExpandedChange,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// Create tree aria props
|
|
212
|
+
const { treeProps } = createTree<T, TreeCollection<T>>(
|
|
213
|
+
() => ({
|
|
214
|
+
id: ariaProps.id,
|
|
215
|
+
'aria-label': ariaProps['aria-label'],
|
|
216
|
+
'aria-labelledby': ariaProps['aria-labelledby'],
|
|
217
|
+
'aria-describedby': ariaProps['aria-describedby'],
|
|
218
|
+
isVirtualized: ariaProps.isVirtualized,
|
|
219
|
+
onAction: ariaProps.onAction,
|
|
220
|
+
isDisabled: ariaProps.isDisabled,
|
|
221
|
+
}),
|
|
222
|
+
() => state,
|
|
223
|
+
ref
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Create focus ring
|
|
227
|
+
const { isFocused, isFocusVisible, focusProps } = createFocusRing();
|
|
228
|
+
|
|
229
|
+
// Render props values
|
|
230
|
+
const renderValues = createMemo<TreeRenderProps>(() => ({
|
|
231
|
+
isFocused: state.isFocused || isFocused(),
|
|
232
|
+
isFocusVisible: isFocusVisible(),
|
|
233
|
+
isDisabled: ariaProps.isDisabled ?? false,
|
|
234
|
+
isEmpty: stateProps.items.length === 0,
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
// Resolve render props
|
|
238
|
+
const renderProps = useRenderProps(
|
|
239
|
+
{
|
|
240
|
+
class: local.class,
|
|
241
|
+
style: local.style,
|
|
242
|
+
defaultClassName: 'solidaria-Tree',
|
|
243
|
+
},
|
|
244
|
+
renderValues
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Filter DOM props
|
|
248
|
+
const domProps = createMemo(() => {
|
|
249
|
+
const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
|
|
250
|
+
return filtered;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Remove ref from spread props
|
|
254
|
+
const cleanTreeProps = () => {
|
|
255
|
+
const { ref: _ref1, ...rest } = treeProps as Record<string, unknown>;
|
|
256
|
+
return rest;
|
|
257
|
+
};
|
|
258
|
+
const cleanFocusProps = () => {
|
|
259
|
+
const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
|
|
260
|
+
return rest;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const isEmpty = () => stateProps.items.length === 0;
|
|
264
|
+
|
|
265
|
+
const contextValue = createMemo<TreeContextValue<T>>(() => ({
|
|
266
|
+
state,
|
|
267
|
+
collection: state.collection,
|
|
268
|
+
isDisabled: ariaProps.isDisabled ?? false,
|
|
269
|
+
renderItem: props.children,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Render visible rows (flat list based on expansion state)
|
|
273
|
+
const visibleRows = createMemo(() => state.collection.rows);
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<TreeContext.Provider value={contextValue() as unknown as TreeContextValue<object>}>
|
|
277
|
+
<TreeStateContext.Provider value={state as unknown as TreeState<object, TreeCollection<object>>}>
|
|
278
|
+
<div
|
|
279
|
+
ref={setRef}
|
|
280
|
+
{...domProps()}
|
|
281
|
+
{...cleanTreeProps()}
|
|
282
|
+
{...cleanFocusProps()}
|
|
283
|
+
class={renderProps.class()}
|
|
284
|
+
style={renderProps.style()}
|
|
285
|
+
data-focused={state.isFocused || undefined}
|
|
286
|
+
data-focus-visible={isFocusVisible() || undefined}
|
|
287
|
+
data-disabled={ariaProps.isDisabled || undefined}
|
|
288
|
+
data-empty={isEmpty() || undefined}
|
|
289
|
+
>
|
|
290
|
+
{isEmpty() && local.renderEmptyState ? (
|
|
291
|
+
local.renderEmptyState()
|
|
292
|
+
) : (
|
|
293
|
+
<For each={visibleRows()}>
|
|
294
|
+
{(node) => {
|
|
295
|
+
// Find the original item data to pass to render function
|
|
296
|
+
const itemData: TreeItemData<T> = {
|
|
297
|
+
key: node.key,
|
|
298
|
+
value: node.value as T,
|
|
299
|
+
textValue: node.textValue,
|
|
300
|
+
children: node.hasChildNodes
|
|
301
|
+
? node.childNodes.map((child) => ({
|
|
302
|
+
key: child.key,
|
|
303
|
+
value: child.value as T,
|
|
304
|
+
textValue: child.textValue,
|
|
305
|
+
}))
|
|
306
|
+
: undefined,
|
|
307
|
+
};
|
|
308
|
+
const itemState: TreeRenderItemState = {
|
|
309
|
+
isExpanded: node.isExpanded ?? false,
|
|
310
|
+
isExpandable: node.isExpandable ?? false,
|
|
311
|
+
level: node.level,
|
|
312
|
+
};
|
|
313
|
+
return props.children(itemData, itemState);
|
|
314
|
+
}}
|
|
315
|
+
</For>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
</TreeStateContext.Provider>
|
|
319
|
+
</TreeContext.Provider>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* An item in a tree.
|
|
325
|
+
*/
|
|
326
|
+
export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element {
|
|
327
|
+
const [local] = splitProps(props, [
|
|
328
|
+
'class',
|
|
329
|
+
'style',
|
|
330
|
+
'slot',
|
|
331
|
+
'id',
|
|
332
|
+
'item',
|
|
333
|
+
'textValue',
|
|
334
|
+
'onAction',
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
// Get state from context
|
|
338
|
+
const context = useContext(TreeStateContext);
|
|
339
|
+
if (!context) {
|
|
340
|
+
throw new Error('TreeItem must be used within a Tree');
|
|
341
|
+
}
|
|
342
|
+
const state = context as TreeState<T, TreeCollection<T>>;
|
|
343
|
+
|
|
344
|
+
// Create ref signal
|
|
345
|
+
const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
|
|
346
|
+
|
|
347
|
+
// Find the item node
|
|
348
|
+
const itemNode = createMemo(() => {
|
|
349
|
+
const node = state.collection.getItem(local.id);
|
|
350
|
+
if (!node) {
|
|
351
|
+
// Create a simple node for the item
|
|
352
|
+
return {
|
|
353
|
+
type: 'item' as const,
|
|
354
|
+
key: local.id,
|
|
355
|
+
value: local.item?.value ?? null,
|
|
356
|
+
textValue: local.textValue ?? String(local.id),
|
|
357
|
+
level: 0,
|
|
358
|
+
index: 0,
|
|
359
|
+
hasChildNodes: false,
|
|
360
|
+
childNodes: [],
|
|
361
|
+
isExpandable: false,
|
|
362
|
+
isExpanded: false,
|
|
363
|
+
} as TreeNode<T>;
|
|
364
|
+
}
|
|
365
|
+
return node;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Create item aria props
|
|
369
|
+
const {
|
|
370
|
+
rowProps,
|
|
371
|
+
gridCellProps,
|
|
372
|
+
expandButtonProps: _expandButtonProps,
|
|
373
|
+
isSelected,
|
|
374
|
+
isDisabled,
|
|
375
|
+
isPressed,
|
|
376
|
+
isExpanded,
|
|
377
|
+
isExpandable,
|
|
378
|
+
level,
|
|
379
|
+
} = createTreeItem<T, TreeCollection<T>>(
|
|
380
|
+
() => ({
|
|
381
|
+
node: itemNode(),
|
|
382
|
+
onAction: local.onAction,
|
|
383
|
+
}),
|
|
384
|
+
() => state,
|
|
385
|
+
ref
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Create hover
|
|
389
|
+
const { isHovered, hoverProps } = createHover({
|
|
390
|
+
get isDisabled() {
|
|
391
|
+
return isDisabled;
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Create focus ring
|
|
396
|
+
const { isFocusVisible, focusProps } = createFocusRing();
|
|
397
|
+
|
|
398
|
+
// Check if focused
|
|
399
|
+
const isFocused = createMemo(() => state.focusedKey === local.id);
|
|
400
|
+
|
|
401
|
+
// Render props values
|
|
402
|
+
const renderValues = createMemo<TreeItemRenderProps>(() => ({
|
|
403
|
+
isSelected,
|
|
404
|
+
isFocused: isFocused(),
|
|
405
|
+
isFocusVisible: isFocusVisible() && isFocused(),
|
|
406
|
+
isPressed,
|
|
407
|
+
isHovered: isHovered(),
|
|
408
|
+
isDisabled,
|
|
409
|
+
isExpanded,
|
|
410
|
+
isExpandable,
|
|
411
|
+
level,
|
|
412
|
+
}));
|
|
413
|
+
|
|
414
|
+
// Resolve render props
|
|
415
|
+
const renderProps = useRenderProps(
|
|
416
|
+
{
|
|
417
|
+
children: props.children,
|
|
418
|
+
class: local.class,
|
|
419
|
+
style: local.style,
|
|
420
|
+
defaultClassName: 'solidaria-Tree-item',
|
|
421
|
+
},
|
|
422
|
+
renderValues
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Remove ref from spread props
|
|
426
|
+
const cleanRowProps = () => {
|
|
427
|
+
const { ref: _ref1, ...rest } = rowProps as Record<string, unknown>;
|
|
428
|
+
return rest;
|
|
429
|
+
};
|
|
430
|
+
const cleanHoverProps = () => {
|
|
431
|
+
const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
|
|
432
|
+
return rest;
|
|
433
|
+
};
|
|
434
|
+
const cleanFocusProps = () => {
|
|
435
|
+
const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
|
|
436
|
+
return rest;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Item context for nested components
|
|
440
|
+
const itemContextValue = createMemo<TreeItemContextValue<T>>(() => ({
|
|
441
|
+
node: itemNode(),
|
|
442
|
+
isExpanded,
|
|
443
|
+
isExpandable,
|
|
444
|
+
level,
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<TreeItemContext.Provider value={itemContextValue() as TreeItemContextValue<object>}>
|
|
449
|
+
<div
|
|
450
|
+
ref={setRef}
|
|
451
|
+
{...cleanRowProps()}
|
|
452
|
+
{...cleanHoverProps()}
|
|
453
|
+
{...cleanFocusProps()}
|
|
454
|
+
class={renderProps.class()}
|
|
455
|
+
style={renderProps.style()}
|
|
456
|
+
data-selected={isSelected || undefined}
|
|
457
|
+
data-focused={isFocused() || undefined}
|
|
458
|
+
data-focus-visible={(isFocusVisible() && isFocused()) || undefined}
|
|
459
|
+
data-pressed={isPressed || undefined}
|
|
460
|
+
data-hovered={isHovered() || undefined}
|
|
461
|
+
data-disabled={isDisabled || undefined}
|
|
462
|
+
data-expanded={isExpanded || undefined}
|
|
463
|
+
data-expandable={isExpandable || undefined}
|
|
464
|
+
data-level={level}
|
|
465
|
+
>
|
|
466
|
+
<div {...gridCellProps} class="solidaria-Tree-item-content">
|
|
467
|
+
{renderProps.renderChildren()}
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</TreeItemContext.Provider>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A button to expand/collapse a tree item.
|
|
476
|
+
*/
|
|
477
|
+
export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
|
|
478
|
+
// Get item context
|
|
479
|
+
const itemContext = useContext(TreeItemContext);
|
|
480
|
+
if (!itemContext) {
|
|
481
|
+
throw new Error('TreeExpandButton must be used within a Tree');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Get state context
|
|
485
|
+
const stateContext = useContext(TreeStateContext);
|
|
486
|
+
if (!stateContext) {
|
|
487
|
+
throw new Error('TreeExpandButton must be used within a Tree');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const state = stateContext as TreeState<object, TreeCollection<object>>;
|
|
491
|
+
|
|
492
|
+
// Create expand button props
|
|
493
|
+
const { expandButtonProps } = createTreeItem(
|
|
494
|
+
() => ({ node: itemContext.node }),
|
|
495
|
+
() => state,
|
|
496
|
+
() => null
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// Remove ref and add custom handling
|
|
500
|
+
const cleanExpandProps = () => {
|
|
501
|
+
const { ref: _ref, ...rest } = expandButtonProps as Record<string, unknown>;
|
|
502
|
+
return rest;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const isExpanded = createMemo(() => state.isExpanded(itemContext.node.key));
|
|
506
|
+
|
|
507
|
+
// Render children
|
|
508
|
+
const renderChildren = () => {
|
|
509
|
+
if (typeof props.children === 'function') {
|
|
510
|
+
return props.children({ isExpanded: isExpanded() });
|
|
511
|
+
}
|
|
512
|
+
return props.children;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<Show when={itemContext.isExpandable}>
|
|
517
|
+
<button
|
|
518
|
+
{...cleanExpandProps()}
|
|
519
|
+
class={props.class ?? 'solidaria-Tree-expand-button'}
|
|
520
|
+
style={props.style}
|
|
521
|
+
data-expanded={isExpanded() || undefined}
|
|
522
|
+
>
|
|
523
|
+
{renderChildren()}
|
|
524
|
+
</button>
|
|
525
|
+
</Show>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A checkbox for item selection in a tree.
|
|
531
|
+
*/
|
|
532
|
+
export function TreeSelectionCheckbox(props: { itemKey: Key }): JSX.Element {
|
|
533
|
+
const context = useContext(TreeStateContext);
|
|
534
|
+
if (!context) {
|
|
535
|
+
throw new Error('TreeSelectionCheckbox must be used within a Tree');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const state = context as TreeState<object, TreeCollection<object>>;
|
|
539
|
+
|
|
540
|
+
const { checkboxProps } = createTreeSelectionCheckbox<object, TreeCollection<object>>(
|
|
541
|
+
() => ({ key: props.itemKey }),
|
|
542
|
+
() => state
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
return <input {...checkboxProps} class="solidaria-Tree-checkbox" />;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Attach static properties
|
|
549
|
+
Tree.Item = TreeItem;
|
|
550
|
+
Tree.ExpandButton = TreeExpandButton;
|
|
551
|
+
Tree.SelectionCheckbox = TreeSelectionCheckbox;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VisuallyHidden component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* Hides content visually but keeps it accessible to screen readers.
|
|
5
|
+
* Port of react-aria's VisuallyHidden.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, type ParentProps, splitProps } from 'solid-js';
|
|
9
|
+
import { Dynamic } from 'solid-js/web';
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// TYPES
|
|
13
|
+
// ============================================
|
|
14
|
+
|
|
15
|
+
export interface VisuallyHiddenProps extends ParentProps {
|
|
16
|
+
/** The element type to render. @default 'span' */
|
|
17
|
+
elementType?: keyof JSX.IntrinsicElements;
|
|
18
|
+
/** Whether the element should be focusable when focused. */
|
|
19
|
+
isFocusable?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================
|
|
23
|
+
// STYLES
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
const visuallyHiddenStyles: JSX.CSSProperties = {
|
|
27
|
+
border: '0',
|
|
28
|
+
clip: 'rect(0 0 0 0)',
|
|
29
|
+
'clip-path': 'inset(50%)',
|
|
30
|
+
height: '1px',
|
|
31
|
+
margin: '-1px',
|
|
32
|
+
overflow: 'hidden',
|
|
33
|
+
padding: '0',
|
|
34
|
+
position: 'absolute',
|
|
35
|
+
width: '1px',
|
|
36
|
+
'white-space': 'nowrap',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// COMPONENT
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* VisuallyHidden hides its children visually, while keeping content visible to screen readers.
|
|
45
|
+
*/
|
|
46
|
+
export function VisuallyHidden(props: VisuallyHiddenProps): JSX.Element {
|
|
47
|
+
const [local, others] = splitProps(props, ['elementType', 'isFocusable']);
|
|
48
|
+
|
|
49
|
+
const elementType = () => local.elementType ?? 'span';
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Dynamic
|
|
53
|
+
component={elementType()}
|
|
54
|
+
style={visuallyHiddenStyles}
|
|
55
|
+
{...others}
|
|
56
|
+
>
|
|
57
|
+
{props.children}
|
|
58
|
+
</Dynamic>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/contexts.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contexts for overlay components.
|
|
3
|
+
*
|
|
4
|
+
* These are separated to avoid circular dependencies between
|
|
5
|
+
* Dialog, Modal, Popover, and Button components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createContext, useContext } from 'solid-js'
|
|
9
|
+
import type { OverlayTriggerState as StatelyOverlayTriggerState } from '@proyecto-viviana/solid-stately'
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// OVERLAY TRIGGER STATE CONTEXT
|
|
13
|
+
// ============================================
|
|
14
|
+
|
|
15
|
+
export interface OverlayTriggerState {
|
|
16
|
+
isOpen: boolean
|
|
17
|
+
open: () => void
|
|
18
|
+
close: () => void
|
|
19
|
+
toggle: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const OverlayTriggerStateContext = createContext<OverlayTriggerState | null>(null)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook to access the overlay trigger state from context.
|
|
26
|
+
*/
|
|
27
|
+
export function useOverlayTriggerState(): OverlayTriggerState | null {
|
|
28
|
+
return useContext(OverlayTriggerStateContext)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================
|
|
32
|
+
// DIALOG TRIGGER CONTEXT
|
|
33
|
+
// ============================================
|
|
34
|
+
|
|
35
|
+
export interface DialogTriggerContextValue {
|
|
36
|
+
state: StatelyOverlayTriggerState
|
|
37
|
+
triggerRef: () => HTMLElement | null
|
|
38
|
+
triggerId: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DialogTriggerContext = createContext<DialogTriggerContextValue | null>(null)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hook to access the dialog trigger state from context.
|
|
45
|
+
*/
|
|
46
|
+
export function useDialogTrigger(): DialogTriggerContextValue | null {
|
|
47
|
+
return useContext(DialogTriggerContext)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================
|
|
51
|
+
// POPOVER TRIGGER CONTEXT
|
|
52
|
+
// ============================================
|
|
53
|
+
|
|
54
|
+
export interface PopoverTriggerContextValue {
|
|
55
|
+
state: {
|
|
56
|
+
isOpen: () => boolean
|
|
57
|
+
open: () => void
|
|
58
|
+
close: () => void
|
|
59
|
+
toggle: () => void
|
|
60
|
+
}
|
|
61
|
+
triggerRef: () => HTMLElement | null
|
|
62
|
+
setTriggerRef: (el: HTMLElement | null) => void
|
|
63
|
+
triggerId: string
|
|
64
|
+
trigger: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const PopoverTriggerContext = createContext<PopoverTriggerContextValue | null>(null)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to access the popover trigger state from context.
|
|
71
|
+
*/
|
|
72
|
+
export function usePopoverTrigger(): PopoverTriggerContextValue | null {
|
|
73
|
+
return useContext(PopoverTriggerContext)
|
|
74
|
+
}
|