@qwickapps/react-framework 1.3.2 → 1.3.4
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/README.md +326 -0
- package/dist/components/AccessibilityProvider.d.ts +64 -0
- package/dist/components/AccessibilityProvider.d.ts.map +1 -0
- package/dist/components/Breadcrumbs.d.ts +39 -0
- package/dist/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +39 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/QwickApp.d.ts.map +1 -1
- package/dist/components/forms/FormBlock.d.ts +1 -1
- package/dist/components/forms/FormBlock.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/input/SwitchInputField.d.ts +28 -0
- package/dist/components/input/SwitchInputField.d.ts.map +1 -0
- package/dist/components/input/index.d.ts +2 -0
- package/dist/components/input/index.d.ts.map +1 -1
- package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts +34 -0
- package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -0
- package/dist/components/layout/CollapsibleLayout/index.d.ts +9 -0
- package/dist/components/layout/CollapsibleLayout/index.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/index.bundled.css +12 -0
- package/dist/index.esm.js +1678 -25
- package/dist/index.js +1689 -21
- package/dist/schemas/CollapsibleLayoutSchema.d.ts +31 -0
- package/dist/schemas/CollapsibleLayoutSchema.d.ts.map +1 -0
- package/dist/schemas/SwitchInputFieldSchema.d.ts +18 -0
- package/dist/schemas/SwitchInputFieldSchema.d.ts.map +1 -0
- package/dist/types/CollapsibleLayout.d.ts +142 -0
- package/dist/types/CollapsibleLayout.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AccessibilityProvider.tsx +466 -0
- package/src/components/Breadcrumbs.tsx +223 -0
- package/src/components/ErrorBoundary.tsx +216 -0
- package/src/components/QwickApp.tsx +17 -11
- package/src/components/__tests__/AccessibilityProvider.test.tsx +330 -0
- package/src/components/__tests__/Breadcrumbs.test.tsx +268 -0
- package/src/components/__tests__/ErrorBoundary.test.tsx +163 -0
- package/src/components/forms/FormBlock.tsx +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/input/SwitchInputField.tsx +165 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +554 -0
- package/src/components/layout/CollapsibleLayout/__tests__/CollapsibleLayout.test.tsx +1469 -0
- package/src/components/layout/CollapsibleLayout/index.tsx +17 -0
- package/src/components/layout/index.ts +4 -1
- package/src/components/pages/FormPage.tsx +1 -1
- package/src/schemas/CollapsibleLayoutSchema.ts +276 -0
- package/src/schemas/SwitchInputFieldSchema.ts +99 -0
- package/src/stories/AccessibilityProvider.stories.tsx +284 -0
- package/src/stories/Breadcrumbs.stories.tsx +304 -0
- package/src/stories/CollapsibleLayout.stories.tsx +1566 -0
- package/src/stories/ErrorBoundary.stories.tsx +159 -0
- package/src/types/CollapsibleLayout.ts +231 -0
- package/src/types/index.ts +1 -0
- package/dist/schemas/Builders.d.ts +0 -7
- package/dist/schemas/Builders.d.ts.map +0 -1
- package/dist/schemas/types.d.ts +0 -7
- package/dist/schemas/types.d.ts.map +0 -1
- package/dist/types/DataBinding.d.ts +0 -7
- package/dist/types/DataBinding.d.ts.map +0 -1
- package/dist/types/DataProvider.d.ts +0 -7
- package/dist/types/DataProvider.d.ts.map +0 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CollapsibleLayout Component - Advanced expandable/collapsible container
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Controlled and uncontrolled state management
|
|
6
|
+
* - Multiple animation styles (fade, slide, scale)
|
|
7
|
+
* - Customizable trigger areas (button, header, both)
|
|
8
|
+
* - localStorage persistence
|
|
9
|
+
* - Full accessibility support
|
|
10
|
+
* - Material-UI integration with themes
|
|
11
|
+
* - Multiple visual variants
|
|
12
|
+
*
|
|
13
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
|
17
|
+
import {
|
|
18
|
+
Box,
|
|
19
|
+
Typography,
|
|
20
|
+
IconButton,
|
|
21
|
+
Collapse,
|
|
22
|
+
Divider,
|
|
23
|
+
Paper,
|
|
24
|
+
Stack,
|
|
25
|
+
useTheme,
|
|
26
|
+
SxProps,
|
|
27
|
+
Theme,
|
|
28
|
+
} from '@mui/material';
|
|
29
|
+
import {
|
|
30
|
+
ExpandMore as ExpandMoreIcon,
|
|
31
|
+
ExpandLess as ExpandLessIcon,
|
|
32
|
+
Visibility as VisibilityIcon,
|
|
33
|
+
VisibilityOff as VisibilityOffIcon,
|
|
34
|
+
} from '@mui/icons-material';
|
|
35
|
+
import { WithDataBinding } from '@qwickapps/schema';
|
|
36
|
+
import { QWICKAPP_COMPONENT, useBaseProps, useDataBinding } from '../../../hooks';
|
|
37
|
+
import CollapsibleLayoutModel from '../../../schemas/CollapsibleLayoutSchema';
|
|
38
|
+
import {
|
|
39
|
+
CollapsibleLayoutViewProps,
|
|
40
|
+
CollapsibleLayoutProps,
|
|
41
|
+
UseCollapsibleLayoutState,
|
|
42
|
+
spacingConfigs,
|
|
43
|
+
animationConfigs,
|
|
44
|
+
defaultCollapsibleLayoutProps,
|
|
45
|
+
} from '../../../types/CollapsibleLayout';
|
|
46
|
+
import Html from '../../Html';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Custom hook for managing collapsible state - simplified approach
|
|
50
|
+
*/
|
|
51
|
+
function useCollapsibleState(
|
|
52
|
+
controlled: boolean,
|
|
53
|
+
collapsedProp?: boolean,
|
|
54
|
+
defaultCollapsedProp?: boolean,
|
|
55
|
+
onToggleProp?: (collapsed: boolean) => void,
|
|
56
|
+
persistState?: boolean,
|
|
57
|
+
storageKey?: string
|
|
58
|
+
): UseCollapsibleLayoutState {
|
|
59
|
+
const id = useId();
|
|
60
|
+
const finalStorageKey = storageKey || `collapsible-${id}`;
|
|
61
|
+
|
|
62
|
+
// For controlled components, use the prop; for uncontrolled, use internal state
|
|
63
|
+
const [internalCollapsed, setInternalCollapsed] = useState<boolean>(() => {
|
|
64
|
+
if (controlled) {
|
|
65
|
+
return collapsedProp ?? false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try localStorage first if persistence is enabled
|
|
69
|
+
if (persistState && typeof window !== 'undefined') {
|
|
70
|
+
const stored = localStorage.getItem(finalStorageKey);
|
|
71
|
+
if (stored !== null) {
|
|
72
|
+
return JSON.parse(stored);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return defaultCollapsedProp ?? false;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Sync with controlled prop changes
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (controlled && collapsedProp !== undefined) {
|
|
82
|
+
setInternalCollapsed(collapsedProp);
|
|
83
|
+
}
|
|
84
|
+
}, [controlled, collapsedProp]);
|
|
85
|
+
|
|
86
|
+
// Persist to localStorage for uncontrolled components
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!controlled && persistState && typeof window !== 'undefined') {
|
|
89
|
+
localStorage.setItem(finalStorageKey, JSON.stringify(internalCollapsed));
|
|
90
|
+
}
|
|
91
|
+
}, [controlled, internalCollapsed, persistState, finalStorageKey]);
|
|
92
|
+
|
|
93
|
+
const toggle = useCallback(() => {
|
|
94
|
+
const currentState = controlled ? (collapsedProp ?? false) : internalCollapsed;
|
|
95
|
+
const newCollapsed = !currentState;
|
|
96
|
+
|
|
97
|
+
// Always update internal state for visual updates
|
|
98
|
+
setInternalCollapsed(newCollapsed);
|
|
99
|
+
|
|
100
|
+
// Call callback to notify parent
|
|
101
|
+
onToggleProp?.(newCollapsed);
|
|
102
|
+
}, [controlled, collapsedProp, internalCollapsed, onToggleProp]);
|
|
103
|
+
|
|
104
|
+
const setCollapsed = useCallback((collapsed: boolean) => {
|
|
105
|
+
setInternalCollapsed(collapsed);
|
|
106
|
+
onToggleProp?.(collapsed);
|
|
107
|
+
}, [onToggleProp]);
|
|
108
|
+
|
|
109
|
+
// Return the appropriate state
|
|
110
|
+
const finalCollapsed = controlled ? (collapsedProp ?? false) : internalCollapsed;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
collapsed: finalCollapsed,
|
|
114
|
+
toggle,
|
|
115
|
+
setCollapsed,
|
|
116
|
+
isControlled: controlled,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Core CollapsibleLayout View component
|
|
122
|
+
*/
|
|
123
|
+
function CollapsibleLayoutView({
|
|
124
|
+
// State props
|
|
125
|
+
collapsed: collapsedProp,
|
|
126
|
+
defaultCollapsed = false,
|
|
127
|
+
onToggle,
|
|
128
|
+
|
|
129
|
+
// Content props
|
|
130
|
+
title,
|
|
131
|
+
subtitle,
|
|
132
|
+
leadIcon,
|
|
133
|
+
headerActions,
|
|
134
|
+
collapsedView,
|
|
135
|
+
children,
|
|
136
|
+
footerView,
|
|
137
|
+
|
|
138
|
+
// Behavior props
|
|
139
|
+
triggerArea = 'both',
|
|
140
|
+
animationStyle = 'slide',
|
|
141
|
+
persistState = false,
|
|
142
|
+
storageKey,
|
|
143
|
+
animationDuration = 300,
|
|
144
|
+
disableAnimations = false,
|
|
145
|
+
|
|
146
|
+
// Icons
|
|
147
|
+
collapsedIcon,
|
|
148
|
+
expandedIcon,
|
|
149
|
+
|
|
150
|
+
// Layout props
|
|
151
|
+
showDivider = true,
|
|
152
|
+
variant = 'default',
|
|
153
|
+
headerSpacing = 'comfortable',
|
|
154
|
+
contentSpacing = 'comfortable',
|
|
155
|
+
|
|
156
|
+
// Accessibility
|
|
157
|
+
toggleAriaLabel = 'Toggle content visibility',
|
|
158
|
+
'aria-describedby': ariaDescribedBy,
|
|
159
|
+
contentAriaProps = {},
|
|
160
|
+
|
|
161
|
+
// CSS classes
|
|
162
|
+
containerClassName,
|
|
163
|
+
headerClassName,
|
|
164
|
+
contentClassName,
|
|
165
|
+
footerClassName,
|
|
166
|
+
|
|
167
|
+
...restProps
|
|
168
|
+
}: CollapsibleLayoutViewProps) {
|
|
169
|
+
const theme = useTheme();
|
|
170
|
+
const { styleProps, htmlProps, restProps: otherProps } = useBaseProps(restProps);
|
|
171
|
+
|
|
172
|
+
// Mark as QwickApp component
|
|
173
|
+
(CollapsibleLayoutView as any)[QWICKAPP_COMPONENT] = true;
|
|
174
|
+
|
|
175
|
+
// Determine controlled vs uncontrolled usage
|
|
176
|
+
const controlled = collapsedProp !== undefined;
|
|
177
|
+
|
|
178
|
+
// State management
|
|
179
|
+
const { collapsed, toggle, setCollapsed } = useCollapsibleState(
|
|
180
|
+
controlled,
|
|
181
|
+
collapsedProp,
|
|
182
|
+
defaultCollapsed,
|
|
183
|
+
onToggle,
|
|
184
|
+
persistState,
|
|
185
|
+
storageKey
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Get spacing configurations
|
|
189
|
+
const headerSpacingConfig = spacingConfigs[headerSpacing];
|
|
190
|
+
const contentSpacingConfig = spacingConfigs[contentSpacing];
|
|
191
|
+
|
|
192
|
+
// Get animation configuration
|
|
193
|
+
const animationConfig = animationConfigs[animationStyle];
|
|
194
|
+
|
|
195
|
+
// Generate unique IDs for accessibility
|
|
196
|
+
const headerId = useId();
|
|
197
|
+
const contentId = useId();
|
|
198
|
+
|
|
199
|
+
// Handle click events
|
|
200
|
+
const handleHeaderClick = useCallback((event: React.MouseEvent) => {
|
|
201
|
+
if (triggerArea === 'header' || triggerArea === 'both') {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
toggle();
|
|
204
|
+
}
|
|
205
|
+
}, [triggerArea, toggle]);
|
|
206
|
+
|
|
207
|
+
const handleButtonClick = useCallback((event: React.MouseEvent) => {
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
toggle();
|
|
210
|
+
}, [toggle]);
|
|
211
|
+
|
|
212
|
+
// Render icons
|
|
213
|
+
const renderIcon = useCallback((icon: React.ReactNode | string | undefined, defaultIcon: React.ReactNode) => {
|
|
214
|
+
if (React.isValidElement(icon)) {
|
|
215
|
+
return icon;
|
|
216
|
+
}
|
|
217
|
+
if (typeof icon === 'string') {
|
|
218
|
+
return <Html>{icon}</Html>;
|
|
219
|
+
}
|
|
220
|
+
return defaultIcon;
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
const toggleIcon = renderIcon(
|
|
224
|
+
collapsed ? collapsedIcon : expandedIcon,
|
|
225
|
+
collapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Container styles based on variant
|
|
229
|
+
const containerSx: SxProps<Theme> = useMemo(() => {
|
|
230
|
+
const baseSx: SxProps<Theme> = {
|
|
231
|
+
width: '100%',
|
|
232
|
+
position: 'relative',
|
|
233
|
+
...styleProps.sx,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
switch (variant) {
|
|
237
|
+
case 'outlined':
|
|
238
|
+
return {
|
|
239
|
+
...baseSx,
|
|
240
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
241
|
+
borderRadius: 1,
|
|
242
|
+
backgroundColor: 'var(--theme-surface)',
|
|
243
|
+
};
|
|
244
|
+
case 'elevated':
|
|
245
|
+
return {
|
|
246
|
+
...baseSx,
|
|
247
|
+
boxShadow: theme.shadows[2],
|
|
248
|
+
borderRadius: 1,
|
|
249
|
+
backgroundColor: theme.palette.background.paper,
|
|
250
|
+
};
|
|
251
|
+
case 'filled':
|
|
252
|
+
return {
|
|
253
|
+
...baseSx,
|
|
254
|
+
backgroundColor: 'var(--theme-surface-variant)',
|
|
255
|
+
borderRadius: 1,
|
|
256
|
+
};
|
|
257
|
+
default:
|
|
258
|
+
return baseSx;
|
|
259
|
+
}
|
|
260
|
+
}, [variant, theme, styleProps.sx]);
|
|
261
|
+
|
|
262
|
+
// Header styles
|
|
263
|
+
const headerSx: SxProps<Theme> = useMemo(() => ({
|
|
264
|
+
...headerSpacingConfig,
|
|
265
|
+
cursor: (triggerArea === 'header' || triggerArea === 'both') ? 'pointer' : 'default',
|
|
266
|
+
userSelect: 'none',
|
|
267
|
+
display: 'flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
justifyContent: 'space-between',
|
|
270
|
+
'&:hover': (triggerArea === 'header' || triggerArea === 'both') ? {
|
|
271
|
+
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
|
272
|
+
} : {},
|
|
273
|
+
}), [headerSpacingConfig, triggerArea]);
|
|
274
|
+
|
|
275
|
+
// Content styles
|
|
276
|
+
const contentSx: SxProps<Theme> = useMemo(() => ({
|
|
277
|
+
...contentSpacingConfig,
|
|
278
|
+
}), [contentSpacingConfig]);
|
|
279
|
+
|
|
280
|
+
// Animation props for Collapse component
|
|
281
|
+
const collapseProps = useMemo(() => {
|
|
282
|
+
if (disableAnimations) {
|
|
283
|
+
return { timeout: 0 };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const baseProps = {
|
|
287
|
+
timeout: animationDuration,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
switch (animationStyle) {
|
|
291
|
+
case 'fade':
|
|
292
|
+
return {
|
|
293
|
+
...baseProps,
|
|
294
|
+
sx: {
|
|
295
|
+
'& .MuiCollapse-wrapper': {
|
|
296
|
+
opacity: collapsed ? 0 : 1,
|
|
297
|
+
transition: `opacity ${animationDuration}ms ${animationConfig.easing}`,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
case 'scale':
|
|
302
|
+
return {
|
|
303
|
+
...baseProps,
|
|
304
|
+
sx: {
|
|
305
|
+
'& .MuiCollapse-wrapper': {
|
|
306
|
+
transform: collapsed ? 'scale(0.95)' : 'scale(1)',
|
|
307
|
+
opacity: collapsed ? 0 : 1,
|
|
308
|
+
transition: `all ${animationDuration}ms ${animationConfig.easing}`,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
default: // slide
|
|
313
|
+
return baseProps;
|
|
314
|
+
}
|
|
315
|
+
}, [disableAnimations, animationDuration, animationStyle, animationConfig, collapsed]);
|
|
316
|
+
|
|
317
|
+
// Render content based on type
|
|
318
|
+
const renderContent = useCallback((content: React.ReactNode | string | undefined) => {
|
|
319
|
+
if (!content) return null;
|
|
320
|
+
|
|
321
|
+
if (typeof content === 'string') {
|
|
322
|
+
return <Html>{content}</Html>;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return content;
|
|
326
|
+
}, []);
|
|
327
|
+
|
|
328
|
+
// Container content that will be wrapped
|
|
329
|
+
const containerContent = (
|
|
330
|
+
<>
|
|
331
|
+
{/* Header Section */}
|
|
332
|
+
{(title || subtitle || leadIcon || headerActions || (triggerArea === 'button' || triggerArea === 'both')) && (
|
|
333
|
+
<>
|
|
334
|
+
<Box
|
|
335
|
+
id={headerId}
|
|
336
|
+
className={headerClassName}
|
|
337
|
+
sx={headerSx}
|
|
338
|
+
onClick={handleHeaderClick}
|
|
339
|
+
role={triggerArea === 'header' || triggerArea === 'both' ? 'button' : undefined}
|
|
340
|
+
tabIndex={triggerArea === 'header' || triggerArea === 'both' ? 0 : undefined}
|
|
341
|
+
aria-expanded={!collapsed}
|
|
342
|
+
aria-controls={contentId}
|
|
343
|
+
aria-describedby={ariaDescribedBy}
|
|
344
|
+
onKeyDown={(e) => {
|
|
345
|
+
if ((triggerArea === 'header' || triggerArea === 'both') && (e.key === 'Enter' || e.key === ' ')) {
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
toggle();
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
{/* Left section: Lead icon, title, subtitle */}
|
|
352
|
+
<Stack direction="row" spacing={2} alignItems="center" sx={{ minWidth: 0, flex: 1 }}>
|
|
353
|
+
{leadIcon && (
|
|
354
|
+
<Box sx={{ flexShrink: 0 }}>
|
|
355
|
+
{renderContent(leadIcon)}
|
|
356
|
+
</Box>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{(title || subtitle) && (
|
|
360
|
+
<Box sx={{ minWidth: 0, flex: 1 }}>
|
|
361
|
+
{title && (
|
|
362
|
+
<Typography
|
|
363
|
+
variant="h6"
|
|
364
|
+
component="h3"
|
|
365
|
+
sx={{
|
|
366
|
+
fontWeight: 600,
|
|
367
|
+
lineHeight: 1.2,
|
|
368
|
+
...(subtitle && { mb: 0.5 })
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
{title}
|
|
372
|
+
</Typography>
|
|
373
|
+
)}
|
|
374
|
+
{subtitle && (
|
|
375
|
+
<Typography
|
|
376
|
+
variant="body2"
|
|
377
|
+
color="text.secondary"
|
|
378
|
+
sx={{ lineHeight: 1.3 }}
|
|
379
|
+
>
|
|
380
|
+
{subtitle}
|
|
381
|
+
</Typography>
|
|
382
|
+
)}
|
|
383
|
+
</Box>
|
|
384
|
+
)}
|
|
385
|
+
</Stack>
|
|
386
|
+
|
|
387
|
+
{/* Right section: Header actions and toggle button */}
|
|
388
|
+
<Stack direction="row" spacing={1} alignItems="center" sx={{ flexShrink: 0 }}>
|
|
389
|
+
{headerActions && (
|
|
390
|
+
<Box>
|
|
391
|
+
{renderContent(headerActions)}
|
|
392
|
+
</Box>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
<IconButton
|
|
396
|
+
onClick={handleButtonClick}
|
|
397
|
+
size="small"
|
|
398
|
+
aria-label={toggleAriaLabel}
|
|
399
|
+
aria-expanded={!collapsed}
|
|
400
|
+
aria-controls={contentId}
|
|
401
|
+
>
|
|
402
|
+
{toggleIcon}
|
|
403
|
+
</IconButton>
|
|
404
|
+
</Stack>
|
|
405
|
+
</Box>
|
|
406
|
+
|
|
407
|
+
{showDivider && <Divider />}
|
|
408
|
+
</>
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
{/* Collapsed View (shown when collapsed) */}
|
|
412
|
+
{collapsed && collapsedView && (
|
|
413
|
+
<>
|
|
414
|
+
<Box
|
|
415
|
+
className={contentClassName}
|
|
416
|
+
sx={contentSx}
|
|
417
|
+
>
|
|
418
|
+
{renderContent(collapsedView)}
|
|
419
|
+
</Box>
|
|
420
|
+
{showDivider && footerView && <Divider />}
|
|
421
|
+
</>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{/* Expanded Content (shown when not collapsed) */}
|
|
425
|
+
<Collapse
|
|
426
|
+
in={!collapsed}
|
|
427
|
+
{...collapseProps}
|
|
428
|
+
>
|
|
429
|
+
<Box
|
|
430
|
+
id={contentId}
|
|
431
|
+
className={contentClassName}
|
|
432
|
+
sx={contentSx}
|
|
433
|
+
role="region"
|
|
434
|
+
aria-labelledby={title ? headerId : undefined}
|
|
435
|
+
{...contentAriaProps}
|
|
436
|
+
>
|
|
437
|
+
{renderContent(children)}
|
|
438
|
+
</Box>
|
|
439
|
+
{showDivider && footerView && <Divider />}
|
|
440
|
+
</Collapse>
|
|
441
|
+
|
|
442
|
+
{/* Footer Section (always visible) */}
|
|
443
|
+
{footerView && (
|
|
444
|
+
<Box
|
|
445
|
+
className={footerClassName}
|
|
446
|
+
sx={contentSx}
|
|
447
|
+
>
|
|
448
|
+
{renderContent(footerView)}
|
|
449
|
+
</Box>
|
|
450
|
+
)}
|
|
451
|
+
</>
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Return appropriate container based on variant
|
|
455
|
+
if (variant === 'outlined') {
|
|
456
|
+
return (
|
|
457
|
+
<Paper
|
|
458
|
+
variant="outlined"
|
|
459
|
+
elevation={0}
|
|
460
|
+
{...htmlProps}
|
|
461
|
+
{...otherProps}
|
|
462
|
+
className={containerClassName}
|
|
463
|
+
sx={containerSx}
|
|
464
|
+
>
|
|
465
|
+
{containerContent}
|
|
466
|
+
</Paper>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (variant === 'elevated') {
|
|
471
|
+
return (
|
|
472
|
+
<Paper
|
|
473
|
+
elevation={2}
|
|
474
|
+
{...htmlProps}
|
|
475
|
+
{...otherProps}
|
|
476
|
+
className={containerClassName}
|
|
477
|
+
sx={containerSx}
|
|
478
|
+
>
|
|
479
|
+
{containerContent}
|
|
480
|
+
</Paper>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Default variant (default, filled)
|
|
485
|
+
return (
|
|
486
|
+
<Box
|
|
487
|
+
{...htmlProps}
|
|
488
|
+
{...otherProps}
|
|
489
|
+
className={containerClassName}
|
|
490
|
+
sx={containerSx}
|
|
491
|
+
>
|
|
492
|
+
{containerContent}
|
|
493
|
+
</Box>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Main CollapsibleLayout component with data binding support
|
|
499
|
+
*/
|
|
500
|
+
function CollapsibleLayout(props: CollapsibleLayoutProps) {
|
|
501
|
+
const { dataSource, bindingOptions, ...restProps } = props;
|
|
502
|
+
|
|
503
|
+
// If no dataSource, use traditional props
|
|
504
|
+
if (!dataSource) {
|
|
505
|
+
return <CollapsibleLayoutView {...restProps} />;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Use data binding
|
|
509
|
+
const { dataSource: _source, loading, error, cached, ...collapsibleProps } = useDataBinding<CollapsibleLayoutModel>(
|
|
510
|
+
dataSource,
|
|
511
|
+
restProps as Partial<CollapsibleLayoutModel>,
|
|
512
|
+
CollapsibleLayoutModel.getSchema(),
|
|
513
|
+
{ cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Show loading state
|
|
517
|
+
if (loading) {
|
|
518
|
+
return (
|
|
519
|
+
<CollapsibleLayoutView
|
|
520
|
+
{...restProps}
|
|
521
|
+
title="Loading..."
|
|
522
|
+
variant="default"
|
|
523
|
+
headerSpacing="comfortable"
|
|
524
|
+
contentSpacing="comfortable"
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (error) {
|
|
530
|
+
console.error('Error loading collapsible layout:', error);
|
|
531
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
532
|
+
return (
|
|
533
|
+
<CollapsibleLayoutView
|
|
534
|
+
{...restProps}
|
|
535
|
+
title="Error Loading Layout"
|
|
536
|
+
subtitle={error.message}
|
|
537
|
+
variant="outlined"
|
|
538
|
+
headerSpacing="comfortable"
|
|
539
|
+
contentSpacing="comfortable"
|
|
540
|
+
/>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return <CollapsibleLayoutView {...collapsibleProps} />;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Set default props
|
|
550
|
+
CollapsibleLayout.defaultProps = defaultCollapsibleLayoutProps;
|
|
551
|
+
|
|
552
|
+
export default CollapsibleLayout;
|
|
553
|
+
export { CollapsibleLayout, CollapsibleLayoutView, useCollapsibleState };
|
|
554
|
+
export type { CollapsibleLayoutProps, CollapsibleLayoutViewProps, UseCollapsibleLayoutState };
|