@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/Dialog.tsx
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog component for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* A headless dialog component that combines ARIA hooks.
|
|
5
|
+
* Port of react-aria-components Dialog.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type JSX,
|
|
10
|
+
createContext,
|
|
11
|
+
createMemo,
|
|
12
|
+
createUniqueId,
|
|
13
|
+
splitProps,
|
|
14
|
+
useContext,
|
|
15
|
+
Switch,
|
|
16
|
+
Match,
|
|
17
|
+
} from 'solid-js'
|
|
18
|
+
import {
|
|
19
|
+
createDialog,
|
|
20
|
+
createOverlayTrigger,
|
|
21
|
+
type AriaDialogProps,
|
|
22
|
+
} from '@proyecto-viviana/solidaria'
|
|
23
|
+
import { createOverlayTriggerState } from '@proyecto-viviana/solid-stately'
|
|
24
|
+
import { DialogTriggerContext, useOverlayTriggerState } from './contexts'
|
|
25
|
+
import {
|
|
26
|
+
type RenderChildren,
|
|
27
|
+
type ClassNameOrFunction,
|
|
28
|
+
type StyleOrFunction,
|
|
29
|
+
type SlotProps,
|
|
30
|
+
useRenderProps,
|
|
31
|
+
filterDOMProps,
|
|
32
|
+
} from './utils'
|
|
33
|
+
|
|
34
|
+
// ============================================
|
|
35
|
+
// TYPES
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
export interface DialogRenderProps {
|
|
39
|
+
/** Function to close the dialog */
|
|
40
|
+
close: () => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DialogProps extends Omit<AriaDialogProps, 'class' | 'style'>, SlotProps {
|
|
44
|
+
/** The children of the component - can be JSX or render function. */
|
|
45
|
+
children?: RenderChildren<DialogRenderProps>
|
|
46
|
+
/** The CSS className for the element. */
|
|
47
|
+
class?: ClassNameOrFunction<DialogRenderProps>
|
|
48
|
+
/** The inline style for the element. */
|
|
49
|
+
style?: StyleOrFunction<DialogRenderProps>
|
|
50
|
+
/** Callback when dialog should close */
|
|
51
|
+
onClose?: () => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DialogTriggerProps {
|
|
55
|
+
/** The children - should include a trigger and modal/popover content. */
|
|
56
|
+
children: JSX.Element
|
|
57
|
+
/** Whether the dialog is open (controlled). */
|
|
58
|
+
isOpen?: boolean
|
|
59
|
+
/** Whether the dialog is open by default (uncontrolled). */
|
|
60
|
+
defaultOpen?: boolean
|
|
61
|
+
/** Callback when open state changes. */
|
|
62
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================
|
|
66
|
+
// CONTEXTS
|
|
67
|
+
// ============================================
|
|
68
|
+
|
|
69
|
+
interface DialogContextValue {
|
|
70
|
+
close: () => void
|
|
71
|
+
titleId?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const DialogContext = createContext<DialogContextValue | null>(null)
|
|
75
|
+
|
|
76
|
+
// Re-export DialogTriggerContext from shared contexts (also imported above for local use)
|
|
77
|
+
export { DialogTriggerContext, useDialogTrigger } from './contexts'
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// DIALOG TRIGGER COMPONENT
|
|
81
|
+
// ============================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A DialogTrigger opens a dialog when a trigger element is pressed.
|
|
85
|
+
* Children should include a trigger element (e.g. Button) and the dialog content.
|
|
86
|
+
*/
|
|
87
|
+
export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
|
|
88
|
+
const [local] = splitProps(props, ['isOpen', 'defaultOpen', 'onOpenChange'])
|
|
89
|
+
|
|
90
|
+
// Create overlay trigger state
|
|
91
|
+
const state = createOverlayTriggerState({
|
|
92
|
+
get isOpen() {
|
|
93
|
+
return local.isOpen
|
|
94
|
+
},
|
|
95
|
+
get defaultOpen() {
|
|
96
|
+
return local.defaultOpen
|
|
97
|
+
},
|
|
98
|
+
onOpenChange: local.onOpenChange,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Ref for the trigger element
|
|
102
|
+
let triggerRef: HTMLElement | null = null
|
|
103
|
+
const triggerId = createUniqueId()
|
|
104
|
+
|
|
105
|
+
// Create overlay trigger props (used via context, not directly applied)
|
|
106
|
+
createOverlayTrigger(
|
|
107
|
+
{ type: 'dialog' },
|
|
108
|
+
state,
|
|
109
|
+
() => triggerRef
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Context value - memoized to avoid unnecessary re-renders
|
|
113
|
+
const contextValue = createMemo(() => ({
|
|
114
|
+
state,
|
|
115
|
+
triggerRef: () => triggerRef,
|
|
116
|
+
triggerId,
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
// In SolidJS, we simply render children directly within the provider
|
|
120
|
+
// The children will have access to the context
|
|
121
|
+
return (
|
|
122
|
+
<DialogTriggerContext.Provider value={contextValue()}>
|
|
123
|
+
{props.children}
|
|
124
|
+
</DialogTriggerContext.Provider>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================
|
|
129
|
+
// DIALOG COMPONENT
|
|
130
|
+
// ============================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A dialog is an overlay shown above other content in an application.
|
|
134
|
+
*/
|
|
135
|
+
export function Dialog(props: DialogProps): JSX.Element {
|
|
136
|
+
const [local, ariaProps, rest] = splitProps(
|
|
137
|
+
props,
|
|
138
|
+
['class', 'style', 'slot', 'onClose'],
|
|
139
|
+
['role', 'aria-label', 'aria-labelledby', 'aria-describedby']
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
let dialogRef!: HTMLDivElement
|
|
143
|
+
|
|
144
|
+
// Get trigger context for aria-labelledby fallback
|
|
145
|
+
const triggerContext = useContext(DialogTriggerContext)
|
|
146
|
+
|
|
147
|
+
// createDialog returns dialogProps AND titleProps (with the id for the Heading)
|
|
148
|
+
const { dialogProps, titleProps } = createDialog(
|
|
149
|
+
{
|
|
150
|
+
get role() {
|
|
151
|
+
return ariaProps.role
|
|
152
|
+
},
|
|
153
|
+
get 'aria-label'() {
|
|
154
|
+
return ariaProps['aria-label']
|
|
155
|
+
},
|
|
156
|
+
get 'aria-labelledby'() {
|
|
157
|
+
// Use provided labelledby, or fall back to trigger id if no title
|
|
158
|
+
return ariaProps['aria-labelledby'] ?? triggerContext?.triggerId
|
|
159
|
+
},
|
|
160
|
+
get 'aria-describedby'() {
|
|
161
|
+
return ariaProps['aria-describedby']
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
() => dialogRef
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// Get titleId from titleProps - this links Dialog's aria-labelledby to Heading's id
|
|
168
|
+
const titleId = () => titleProps()?.id
|
|
169
|
+
|
|
170
|
+
// Get close function from OverlayTriggerState context or onClose prop
|
|
171
|
+
const overlayState = useOverlayTriggerState()
|
|
172
|
+
|
|
173
|
+
const close = () => {
|
|
174
|
+
local.onClose?.()
|
|
175
|
+
overlayState?.close()
|
|
176
|
+
triggerContext?.state.close()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Render props values
|
|
180
|
+
const renderValues = createMemo<DialogRenderProps>(() => ({
|
|
181
|
+
close,
|
|
182
|
+
}))
|
|
183
|
+
|
|
184
|
+
// Resolve render props
|
|
185
|
+
const renderProps = useRenderProps(
|
|
186
|
+
{
|
|
187
|
+
children: props.children,
|
|
188
|
+
class: local.class,
|
|
189
|
+
style: local.style,
|
|
190
|
+
defaultClassName: 'solidaria-Dialog',
|
|
191
|
+
},
|
|
192
|
+
renderValues
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
// Filter DOM props
|
|
196
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }))
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<DialogContext.Provider value={{ close, titleId: titleId() }}>
|
|
200
|
+
<div
|
|
201
|
+
{...dialogProps()}
|
|
202
|
+
{...domProps()}
|
|
203
|
+
ref={dialogRef}
|
|
204
|
+
class={renderProps.class()}
|
|
205
|
+
style={renderProps.style()}
|
|
206
|
+
>
|
|
207
|
+
{renderProps.renderChildren()}
|
|
208
|
+
</div>
|
|
209
|
+
</DialogContext.Provider>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// HEADING COMPONENT
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
export interface HeadingProps {
|
|
218
|
+
/** The children of the heading. */
|
|
219
|
+
children: JSX.Element
|
|
220
|
+
/** The CSS className. */
|
|
221
|
+
class?: string
|
|
222
|
+
/** The heading level (1-6). Defaults to 2. */
|
|
223
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6
|
|
224
|
+
/** The slot to render into. */
|
|
225
|
+
slot?: string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Heading element for dialog title.
|
|
230
|
+
* When rendered inside a Dialog, automatically gets the titleProps.
|
|
231
|
+
*/
|
|
232
|
+
export function Heading(props: HeadingProps): JSX.Element {
|
|
233
|
+
const dialogContext = useContext(DialogContext)
|
|
234
|
+
const level = () => props.level ?? 2
|
|
235
|
+
const id = () => dialogContext?.titleId
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Switch>
|
|
239
|
+
<Match when={level() === 1}>
|
|
240
|
+
<h1 id={id()} class={props.class}>{props.children}</h1>
|
|
241
|
+
</Match>
|
|
242
|
+
<Match when={level() === 2}>
|
|
243
|
+
<h2 id={id()} class={props.class}>{props.children}</h2>
|
|
244
|
+
</Match>
|
|
245
|
+
<Match when={level() === 3}>
|
|
246
|
+
<h3 id={id()} class={props.class}>{props.children}</h3>
|
|
247
|
+
</Match>
|
|
248
|
+
<Match when={level() === 4}>
|
|
249
|
+
<h4 id={id()} class={props.class}>{props.children}</h4>
|
|
250
|
+
</Match>
|
|
251
|
+
<Match when={level() === 5}>
|
|
252
|
+
<h5 id={id()} class={props.class}>{props.children}</h5>
|
|
253
|
+
</Match>
|
|
254
|
+
<Match when={level() === 6}>
|
|
255
|
+
<h6 id={id()} class={props.class}>{props.children}</h6>
|
|
256
|
+
</Match>
|
|
257
|
+
</Switch>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Keep backward compatibility
|
|
262
|
+
export { Heading as DialogHeading }
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disclosure and Accordion components for solidaria-components
|
|
3
|
+
*
|
|
4
|
+
* Disclosure is a widget that can be toggled to show or hide content.
|
|
5
|
+
* Accordion (DisclosureGroup) manages multiple disclosures with optional single-expand.
|
|
6
|
+
*
|
|
7
|
+
* Port of react-aria-components Disclosure.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type JSX,
|
|
12
|
+
createContext,
|
|
13
|
+
createMemo,
|
|
14
|
+
createSignal,
|
|
15
|
+
splitProps,
|
|
16
|
+
useContext,
|
|
17
|
+
} from 'solid-js';
|
|
18
|
+
import {
|
|
19
|
+
createDisclosureState,
|
|
20
|
+
createDisclosureGroupState,
|
|
21
|
+
type DisclosureState,
|
|
22
|
+
type DisclosureGroupState,
|
|
23
|
+
type DisclosureStateProps,
|
|
24
|
+
type DisclosureGroupStateProps,
|
|
25
|
+
} from '@proyecto-viviana/solid-stately';
|
|
26
|
+
import {
|
|
27
|
+
createDisclosure,
|
|
28
|
+
createDisclosureGroup,
|
|
29
|
+
} from '@proyecto-viviana/solidaria';
|
|
30
|
+
import {
|
|
31
|
+
type RenderChildren,
|
|
32
|
+
type ClassNameOrFunction,
|
|
33
|
+
type StyleOrFunction,
|
|
34
|
+
useRenderProps,
|
|
35
|
+
filterDOMProps,
|
|
36
|
+
dataAttr,
|
|
37
|
+
} from './utils';
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// TYPES
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
export interface DisclosureRenderProps {
|
|
44
|
+
/** Whether the disclosure is expanded. */
|
|
45
|
+
isExpanded: boolean;
|
|
46
|
+
/** Whether the disclosure is disabled. */
|
|
47
|
+
isDisabled: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DisclosureGroupRenderProps {
|
|
51
|
+
/** Whether all items are disabled. */
|
|
52
|
+
isDisabled: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DisclosureProps extends DisclosureStateProps {
|
|
56
|
+
/** The children of the component. */
|
|
57
|
+
children?: JSX.Element;
|
|
58
|
+
/** The CSS className for the element. */
|
|
59
|
+
class?: ClassNameOrFunction<DisclosureRenderProps>;
|
|
60
|
+
/** The inline style for the element. */
|
|
61
|
+
style?: StyleOrFunction<DisclosureRenderProps>;
|
|
62
|
+
/** Whether the disclosure is disabled. */
|
|
63
|
+
isDisabled?: boolean;
|
|
64
|
+
/** A unique identifier for the disclosure (used in groups). */
|
|
65
|
+
id?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DisclosureGroupProps extends DisclosureGroupStateProps {
|
|
69
|
+
/** The children of the component. */
|
|
70
|
+
children?: JSX.Element;
|
|
71
|
+
/** The CSS className for the element. */
|
|
72
|
+
class?: ClassNameOrFunction<DisclosureGroupRenderProps>;
|
|
73
|
+
/** The inline style for the element. */
|
|
74
|
+
style?: StyleOrFunction<DisclosureGroupRenderProps>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DisclosureTriggerProps {
|
|
78
|
+
/** The children of the trigger. */
|
|
79
|
+
children?: JSX.Element;
|
|
80
|
+
/** The CSS className for the element. */
|
|
81
|
+
class?: string;
|
|
82
|
+
/** The inline style for the element. */
|
|
83
|
+
style?: JSX.CSSProperties;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DisclosurePanelProps {
|
|
87
|
+
/** The children of the panel. */
|
|
88
|
+
children?: RenderChildren<DisclosureRenderProps>;
|
|
89
|
+
/** The CSS className for the element. */
|
|
90
|
+
class?: ClassNameOrFunction<DisclosureRenderProps>;
|
|
91
|
+
/** The inline style for the element. */
|
|
92
|
+
style?: StyleOrFunction<DisclosureRenderProps>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// CONTEXT
|
|
97
|
+
// ============================================
|
|
98
|
+
|
|
99
|
+
interface DisclosureContextValue {
|
|
100
|
+
state: DisclosureState;
|
|
101
|
+
isDisabled: () => boolean;
|
|
102
|
+
/** The disclosure ARIA result object - access .buttonProps and .panelProps as getters */
|
|
103
|
+
disclosureAria: {
|
|
104
|
+
readonly buttonProps: JSX.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
105
|
+
readonly panelProps: JSX.HTMLAttributes<HTMLElement>;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const DisclosureContext = createContext<DisclosureContextValue | null>(null);
|
|
110
|
+
|
|
111
|
+
export function useDisclosureContext(): DisclosureContextValue | null {
|
|
112
|
+
return useContext(DisclosureContext);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface DisclosureGroupContextValue {
|
|
116
|
+
state: DisclosureGroupState;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const DisclosureGroupContext = createContext<DisclosureGroupContextValue | null>(null);
|
|
120
|
+
|
|
121
|
+
export function useDisclosureGroupContext(): DisclosureGroupContextValue | null {
|
|
122
|
+
return useContext(DisclosureGroupContext);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// DISCLOSURE GROUP (Accordion)
|
|
127
|
+
// ============================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* DisclosureGroup manages a group of Disclosure components.
|
|
131
|
+
* Use this to create an accordion where only one item can be expanded at a time.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```tsx
|
|
135
|
+
* <DisclosureGroup>
|
|
136
|
+
* <Disclosure id="item1">
|
|
137
|
+
* <DisclosureTrigger>Item 1</DisclosureTrigger>
|
|
138
|
+
* <DisclosurePanel>Content 1</DisclosurePanel>
|
|
139
|
+
* </Disclosure>
|
|
140
|
+
* <Disclosure id="item2">
|
|
141
|
+
* <DisclosureTrigger>Item 2</DisclosureTrigger>
|
|
142
|
+
* <DisclosurePanel>Content 2</DisclosurePanel>
|
|
143
|
+
* </Disclosure>
|
|
144
|
+
* </DisclosureGroup>
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function DisclosureGroup(props: DisclosureGroupProps): JSX.Element {
|
|
148
|
+
// IMPORTANT: Don't destructure or access props.children early!
|
|
149
|
+
// In SolidJS, children are lazily evaluated. Accessing them before
|
|
150
|
+
// the context provider renders causes them to evaluate outside the context.
|
|
151
|
+
// See: https://github.com/solidjs/solid/issues/182
|
|
152
|
+
const [local, rest] = splitProps(props, [
|
|
153
|
+
'class',
|
|
154
|
+
'style',
|
|
155
|
+
'allowsMultipleExpanded',
|
|
156
|
+
'isDisabled',
|
|
157
|
+
'expandedKeys',
|
|
158
|
+
'defaultExpandedKeys',
|
|
159
|
+
'onExpandedChange',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// Create group state
|
|
163
|
+
const state = createDisclosureGroupState({
|
|
164
|
+
allowsMultipleExpanded: local.allowsMultipleExpanded,
|
|
165
|
+
isDisabled: local.isDisabled,
|
|
166
|
+
expandedKeys: local.expandedKeys,
|
|
167
|
+
defaultExpandedKeys: local.defaultExpandedKeys,
|
|
168
|
+
onExpandedChange: local.onExpandedChange,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Create group accessibility props
|
|
172
|
+
const { groupProps } = createDisclosureGroup(
|
|
173
|
+
{ isDisabled: local.isDisabled },
|
|
174
|
+
state
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Render props values
|
|
178
|
+
const renderValues = createMemo<DisclosureGroupRenderProps>(() => ({
|
|
179
|
+
isDisabled: state.isDisabled,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
// Resolve render props - don't pass children, we'll render props.children directly
|
|
183
|
+
const renderProps = useRenderProps(
|
|
184
|
+
{
|
|
185
|
+
class: local.class,
|
|
186
|
+
style: local.style,
|
|
187
|
+
defaultClassName: 'solidaria-DisclosureGroup',
|
|
188
|
+
},
|
|
189
|
+
renderValues
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Filter DOM props
|
|
193
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
|
|
194
|
+
|
|
195
|
+
// Context value
|
|
196
|
+
const contextValue: DisclosureGroupContextValue = { state };
|
|
197
|
+
|
|
198
|
+
// Extract ref from groupProps to avoid type conflicts
|
|
199
|
+
const { ref: _ref, ...cleanGroupProps } = groupProps as Record<string, unknown>;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<DisclosureGroupContext.Provider value={contextValue}>
|
|
203
|
+
<div
|
|
204
|
+
{...domProps()}
|
|
205
|
+
{...cleanGroupProps}
|
|
206
|
+
class={renderProps.class()}
|
|
207
|
+
style={renderProps.style()}
|
|
208
|
+
data-disabled={dataAttr(state.isDisabled)}
|
|
209
|
+
>
|
|
210
|
+
{props.children}
|
|
211
|
+
</div>
|
|
212
|
+
</DisclosureGroupContext.Provider>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================
|
|
217
|
+
// DISCLOSURE
|
|
218
|
+
// ============================================
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Disclosure is a widget that can be toggled to show or hide content.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```tsx
|
|
225
|
+
* <Disclosure>
|
|
226
|
+
* <DisclosureTrigger>Show more</DisclosureTrigger>
|
|
227
|
+
* <DisclosurePanel>Hidden content here...</DisclosurePanel>
|
|
228
|
+
* </Disclosure>
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
export function Disclosure(props: DisclosureProps): JSX.Element {
|
|
232
|
+
// IMPORTANT: Don't destructure or access props.children early!
|
|
233
|
+
// In SolidJS, children are lazily evaluated. Accessing them before
|
|
234
|
+
// the context provider renders causes them to evaluate outside the context.
|
|
235
|
+
// See: https://github.com/solidjs/solid/issues/182
|
|
236
|
+
const [local, rest] = splitProps(props, [
|
|
237
|
+
'class',
|
|
238
|
+
'style',
|
|
239
|
+
'isDisabled',
|
|
240
|
+
'isExpanded',
|
|
241
|
+
'defaultExpanded',
|
|
242
|
+
'onExpandedChange',
|
|
243
|
+
'id',
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
// Check if we're inside a DisclosureGroup
|
|
247
|
+
const groupContext = useDisclosureGroupContext();
|
|
248
|
+
|
|
249
|
+
// Create disclosure state
|
|
250
|
+
// If in a group, sync with group state
|
|
251
|
+
const state = createDisclosureState(() => {
|
|
252
|
+
const id = local.id;
|
|
253
|
+
if (groupContext && id) {
|
|
254
|
+
return {
|
|
255
|
+
isExpanded: groupContext.state.isExpanded(id),
|
|
256
|
+
onExpandedChange: (expanded: boolean) => {
|
|
257
|
+
if (expanded !== groupContext.state.isExpanded(id)) {
|
|
258
|
+
groupContext.state.toggleKey(id);
|
|
259
|
+
}
|
|
260
|
+
local.onExpandedChange?.(expanded);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
isExpanded: local.isExpanded,
|
|
266
|
+
defaultExpanded: local.defaultExpanded,
|
|
267
|
+
onExpandedChange: local.onExpandedChange,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Panel ref as a signal so the createEffect in createDisclosure can track it
|
|
272
|
+
const [panelRef, setPanelRefSignal] = createSignal<HTMLElement | null>(null);
|
|
273
|
+
|
|
274
|
+
// Determine if disabled (used in multiple places)
|
|
275
|
+
const isDisabled = () => local.isDisabled || groupContext?.state.isDisabled || false;
|
|
276
|
+
|
|
277
|
+
// Create disclosure accessibility props
|
|
278
|
+
// Pass props as accessor function for reactivity
|
|
279
|
+
// IMPORTANT: Don't destructure! The getters must be called fresh each render
|
|
280
|
+
const disclosureAria = createDisclosure(
|
|
281
|
+
() => ({ isDisabled: isDisabled() }),
|
|
282
|
+
state,
|
|
283
|
+
panelRef // Pass the accessor directly
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Render props values
|
|
287
|
+
const renderValues = createMemo<DisclosureRenderProps>(() => ({
|
|
288
|
+
isExpanded: state.isExpanded(),
|
|
289
|
+
isDisabled: isDisabled(),
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
// Resolve render props - don't pass children, we'll render props.children directly
|
|
293
|
+
const renderProps = useRenderProps(
|
|
294
|
+
{
|
|
295
|
+
class: local.class,
|
|
296
|
+
style: local.style,
|
|
297
|
+
defaultClassName: 'solidaria-Disclosure',
|
|
298
|
+
},
|
|
299
|
+
renderValues
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Filter DOM props
|
|
303
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
|
|
304
|
+
|
|
305
|
+
// Context value - pass the disclosureAria object with getters intact
|
|
306
|
+
const contextValue: DisclosureContextValue = {
|
|
307
|
+
state,
|
|
308
|
+
isDisabled, // Pass the accessor function, not the value
|
|
309
|
+
disclosureAria,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Setter for panel ref
|
|
313
|
+
const setPanelRef = (el: HTMLElement | null) => {
|
|
314
|
+
setPanelRefSignal(el);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<DisclosureContext.Provider value={contextValue}>
|
|
319
|
+
<DisclosurePanelRefContext.Provider value={setPanelRef}>
|
|
320
|
+
<div
|
|
321
|
+
{...domProps()}
|
|
322
|
+
class={renderProps.class()}
|
|
323
|
+
style={renderProps.style()}
|
|
324
|
+
data-expanded={dataAttr(state.isExpanded())}
|
|
325
|
+
data-disabled={dataAttr(isDisabled())}
|
|
326
|
+
>
|
|
327
|
+
{props.children}
|
|
328
|
+
</div>
|
|
329
|
+
</DisclosurePanelRefContext.Provider>
|
|
330
|
+
</DisclosureContext.Provider>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Internal context to pass panel ref setter
|
|
335
|
+
const DisclosurePanelRefContext = createContext<((el: HTMLElement | null) => void) | null>(null);
|
|
336
|
+
|
|
337
|
+
// ============================================
|
|
338
|
+
// DISCLOSURE TRIGGER
|
|
339
|
+
// ============================================
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* DisclosureTrigger is the button that toggles the disclosure.
|
|
343
|
+
* Pattern matches SelectTrigger for consistency.
|
|
344
|
+
*/
|
|
345
|
+
export function DisclosureTrigger(props: DisclosureTriggerProps): JSX.Element {
|
|
346
|
+
// Get context - now safe because parent uses lazy children evaluation
|
|
347
|
+
const context = useContext(DisclosureContext);
|
|
348
|
+
if (!context) {
|
|
349
|
+
throw new Error('DisclosureTrigger must be used within a Disclosure');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { state, disclosureAria, isDisabled } = context;
|
|
353
|
+
|
|
354
|
+
// Reactive accessors
|
|
355
|
+
const isExpanded = () => state.isExpanded();
|
|
356
|
+
|
|
357
|
+
// Get buttonProps from the getter each time - this ensures reactivity
|
|
358
|
+
// IMPORTANT: Call the getter fresh each render to get updated aria-expanded, etc.
|
|
359
|
+
const getButtonProps = () => {
|
|
360
|
+
const { ref: _ref, ...rest } = disclosureAria.buttonProps as Record<string, unknown>;
|
|
361
|
+
return rest;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<button
|
|
366
|
+
{...getButtonProps()}
|
|
367
|
+
type="button"
|
|
368
|
+
class={props.class}
|
|
369
|
+
style={props.style}
|
|
370
|
+
data-expanded={dataAttr(isExpanded())}
|
|
371
|
+
data-disabled={dataAttr(isDisabled())}
|
|
372
|
+
>
|
|
373
|
+
{props.children}
|
|
374
|
+
</button>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============================================
|
|
379
|
+
// DISCLOSURE PANEL
|
|
380
|
+
// ============================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* DisclosurePanel contains the content that is shown/hidden.
|
|
384
|
+
*/
|
|
385
|
+
export function DisclosurePanel(props: DisclosurePanelProps): JSX.Element {
|
|
386
|
+
// Get context - now safe because parent uses lazy children evaluation
|
|
387
|
+
const context = useContext(DisclosureContext);
|
|
388
|
+
const panelRefSetter = useContext(DisclosurePanelRefContext);
|
|
389
|
+
|
|
390
|
+
const [local, rest] = splitProps(props, ['class', 'style']);
|
|
391
|
+
|
|
392
|
+
// Reactive accessors
|
|
393
|
+
const isExpanded = () => context?.state.isExpanded() ?? false;
|
|
394
|
+
const isDisabled = () => context?.isDisabled() ?? false;
|
|
395
|
+
|
|
396
|
+
// Render props values
|
|
397
|
+
const renderValues = createMemo<DisclosureRenderProps>(() => ({
|
|
398
|
+
isExpanded: isExpanded(),
|
|
399
|
+
isDisabled: isDisabled(),
|
|
400
|
+
}));
|
|
401
|
+
|
|
402
|
+
// Resolve render props
|
|
403
|
+
const renderProps = useRenderProps(
|
|
404
|
+
{
|
|
405
|
+
children: props.children,
|
|
406
|
+
class: local.class,
|
|
407
|
+
style: local.style,
|
|
408
|
+
defaultClassName: 'solidaria-DisclosurePanel',
|
|
409
|
+
},
|
|
410
|
+
renderValues
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Filter DOM props
|
|
414
|
+
const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
|
|
415
|
+
|
|
416
|
+
// Get panelProps from the getter each time - this ensures reactivity
|
|
417
|
+
// IMPORTANT: Call the getter fresh each render to get updated hidden attribute, etc.
|
|
418
|
+
const getPanelProps = () => {
|
|
419
|
+
if (!context) return { id: undefined, role: 'region', 'aria-labelledby': undefined, hidden: true };
|
|
420
|
+
const { ref: _ref, ...rest } = context.disclosureAria.panelProps as Record<string, unknown>;
|
|
421
|
+
return rest;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div
|
|
426
|
+
{...domProps()}
|
|
427
|
+
{...getPanelProps()}
|
|
428
|
+
ref={(el) => panelRefSetter?.(el)}
|
|
429
|
+
class={renderProps.class()}
|
|
430
|
+
style={renderProps.style()}
|
|
431
|
+
data-expanded={dataAttr(isExpanded())}
|
|
432
|
+
>
|
|
433
|
+
{renderProps.renderChildren()}
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Re-export state types for convenience
|
|
439
|
+
export type { DisclosureState, DisclosureGroupState };
|