@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,268 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { Breadcrumbs, useBreadcrumbs, type BreadcrumbItem } from '../Breadcrumbs';
|
|
4
|
+
|
|
5
|
+
// Test component for useBreadcrumbs hook
|
|
6
|
+
const BreadcrumbHookTest = () => {
|
|
7
|
+
const { breadcrumbs, addBreadcrumb, removeBreadcrumb, setBreadcrumbs, clearBreadcrumbs } = useBreadcrumbs();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<div data-testid="breadcrumb-count">{breadcrumbs.length}</div>
|
|
12
|
+
<button onClick={() => addBreadcrumb({ label: 'Test', href: '/test' })}>
|
|
13
|
+
Add Breadcrumb
|
|
14
|
+
</button>
|
|
15
|
+
<button onClick={() => removeBreadcrumb(0)}>
|
|
16
|
+
Remove First
|
|
17
|
+
</button>
|
|
18
|
+
<button onClick={() => setBreadcrumbs([{ label: 'Set', href: '/set' }])}>
|
|
19
|
+
Set Breadcrumbs
|
|
20
|
+
</button>
|
|
21
|
+
<button onClick={clearBreadcrumbs}>
|
|
22
|
+
Clear All
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Sample breadcrumb data
|
|
29
|
+
const sampleBreadcrumbs: BreadcrumbItem[] = [
|
|
30
|
+
{ label: 'Home', href: '/' },
|
|
31
|
+
{ label: 'Products', href: '/products' },
|
|
32
|
+
{ label: 'Electronics', href: '/products/electronics', current: true },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
describe('Breadcrumbs', () => {
|
|
36
|
+
it('renders breadcrumb items correctly', () => {
|
|
37
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} />);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText('Products')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('Electronics')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders with proper ARIA attributes', () => {
|
|
45
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} />);
|
|
46
|
+
|
|
47
|
+
const nav = screen.getByRole('navigation');
|
|
48
|
+
expect(nav).toHaveAttribute('aria-label', 'Breadcrumb navigation');
|
|
49
|
+
|
|
50
|
+
// Electronics has current: true, so it should be wrapped in a span with aria-current="page"
|
|
51
|
+
const currentItemText = screen.getByText('Electronics');
|
|
52
|
+
const currentItem = currentItemText.parentElement;
|
|
53
|
+
expect(currentItem?.tagName).toBe('SPAN');
|
|
54
|
+
expect(currentItem).toHaveAttribute('aria-current', 'page');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders custom separator', () => {
|
|
58
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} separator=">" />);
|
|
59
|
+
|
|
60
|
+
const separators = screen.getAllByText('>');
|
|
61
|
+
expect(separators).toHaveLength(2); // Should have 2 separators for 3 items
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('applies custom className', () => {
|
|
65
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} className="custom-breadcrumbs" />);
|
|
66
|
+
|
|
67
|
+
const nav = screen.getByRole('navigation');
|
|
68
|
+
expect(nav).toHaveClass('breadcrumbs', 'custom-breadcrumbs');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles navigation clicks with onNavigate', () => {
|
|
72
|
+
const onNavigate = jest.fn();
|
|
73
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} onNavigate={onNavigate} />);
|
|
74
|
+
|
|
75
|
+
const homeLink = screen.getByText('Home');
|
|
76
|
+
fireEvent.click(homeLink);
|
|
77
|
+
|
|
78
|
+
expect(onNavigate).toHaveBeenCalledWith(
|
|
79
|
+
sampleBreadcrumbs[0],
|
|
80
|
+
0
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles keyboard navigation', () => {
|
|
85
|
+
const onNavigate = jest.fn();
|
|
86
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} onNavigate={onNavigate} />);
|
|
87
|
+
|
|
88
|
+
const homeLink = screen.getByText('Home');
|
|
89
|
+
|
|
90
|
+
// Test Enter key
|
|
91
|
+
fireEvent.keyDown(homeLink, { key: 'Enter' });
|
|
92
|
+
expect(onNavigate).toHaveBeenCalledWith(sampleBreadcrumbs[0], 0);
|
|
93
|
+
|
|
94
|
+
// Test Space key
|
|
95
|
+
fireEvent.keyDown(homeLink, { key: ' ' });
|
|
96
|
+
expect(onNavigate).toHaveBeenCalledTimes(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('truncates items when maxItems is set', () => {
|
|
100
|
+
const manyItems: BreadcrumbItem[] = [
|
|
101
|
+
{ label: 'Home', href: '/' },
|
|
102
|
+
{ label: 'Level1', href: '/level1' },
|
|
103
|
+
{ label: 'Level2', href: '/level2' },
|
|
104
|
+
{ label: 'Level3', href: '/level3' },
|
|
105
|
+
{ label: 'Level4', href: '/level4' },
|
|
106
|
+
{ label: 'Current', href: '/current', current: true },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
render(<Breadcrumbs items={manyItems} maxItems={4} />);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText('...')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('Current')).toBeInTheDocument();
|
|
114
|
+
|
|
115
|
+
// Should not show middle items
|
|
116
|
+
expect(screen.queryByText('Level2')).not.toBeInTheDocument();
|
|
117
|
+
expect(screen.queryByText('Level3')).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('hides root item when showRoot is false', () => {
|
|
121
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} showRoot={false} />);
|
|
122
|
+
|
|
123
|
+
expect(screen.queryByText('Home')).not.toBeInTheDocument();
|
|
124
|
+
expect(screen.getByText('Products')).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByText('Electronics')).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('renders with icons when provided', () => {
|
|
129
|
+
const itemsWithIcons: BreadcrumbItem[] = [
|
|
130
|
+
{
|
|
131
|
+
label: 'Home',
|
|
132
|
+
href: '/',
|
|
133
|
+
icon: <span data-testid="home-icon">🏠</span>
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: 'Products',
|
|
137
|
+
href: '/products',
|
|
138
|
+
icon: <span data-testid="products-icon">📦</span>
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
render(<Breadcrumbs items={itemsWithIcons} />);
|
|
143
|
+
|
|
144
|
+
expect(screen.getByTestId('home-icon')).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByTestId('products-icon')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('does not render when only one item or less', () => {
|
|
149
|
+
const { container } = render(<Breadcrumbs items={[{ label: 'Home', href: '/' }]} />);
|
|
150
|
+
expect(container.firstChild).toBeNull();
|
|
151
|
+
|
|
152
|
+
const { container: emptyContainer } = render(<Breadcrumbs items={[]} />);
|
|
153
|
+
expect(emptyContainer.firstChild).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders non-clickable items correctly', () => {
|
|
157
|
+
const nonClickableItems: BreadcrumbItem[] = [
|
|
158
|
+
{ label: 'Home', href: '/' }, // Clickable: has href, not current
|
|
159
|
+
{ label: 'Current', current: true }, // Non-clickable: current is true
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
render(<Breadcrumbs items={nonClickableItems} />);
|
|
163
|
+
|
|
164
|
+
// Home should be clickable - find the parent element (the <a> tag)
|
|
165
|
+
const homeElement = screen.getByText('Home').parentElement;
|
|
166
|
+
expect(homeElement?.tagName).toBe('A');
|
|
167
|
+
|
|
168
|
+
// Current should not be clickable - find the parent element (should be <span>)
|
|
169
|
+
const currentElement = screen.getByText('Current').parentElement;
|
|
170
|
+
expect(currentElement?.tagName).toBe('SPAN');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('prevents navigation for ellipsis items', () => {
|
|
174
|
+
const onNavigate = jest.fn();
|
|
175
|
+
const manyItems: BreadcrumbItem[] = [
|
|
176
|
+
{ label: 'Home', href: '/' },
|
|
177
|
+
{ label: 'Level1', href: '/level1' },
|
|
178
|
+
{ label: 'Level2', href: '/level2' },
|
|
179
|
+
{ label: 'Level3', href: '/level3' },
|
|
180
|
+
{ label: 'Current', href: '/current', current: true },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
render(<Breadcrumbs items={manyItems} maxItems={3} onNavigate={onNavigate} />);
|
|
184
|
+
|
|
185
|
+
const ellipsis = screen.getByText('...');
|
|
186
|
+
fireEvent.click(ellipsis);
|
|
187
|
+
|
|
188
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('handles default link behavior when no onNavigate is provided', () => {
|
|
192
|
+
// Create a mock for window.location.href
|
|
193
|
+
const originalLocation = window.location;
|
|
194
|
+
delete (window as any).location;
|
|
195
|
+
window.location = { ...originalLocation, href: '' } as any;
|
|
196
|
+
|
|
197
|
+
const itemsWithHref: BreadcrumbItem[] = [
|
|
198
|
+
{ label: 'Home', href: '/' },
|
|
199
|
+
{ label: 'Current', href: '/current' }, // Need at least 2 items for breadcrumbs to render
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
render(<Breadcrumbs items={itemsWithHref} />);
|
|
203
|
+
|
|
204
|
+
const homeLink = screen.getByText('Home');
|
|
205
|
+
fireEvent.keyDown(homeLink, { key: 'Enter' });
|
|
206
|
+
|
|
207
|
+
// Should attempt to navigate using href
|
|
208
|
+
expect(window.location.href).toBe('/');
|
|
209
|
+
|
|
210
|
+
// Restore original location
|
|
211
|
+
window.location = originalLocation;
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('useBreadcrumbs hook', () => {
|
|
216
|
+
it('provides initial empty breadcrumbs', () => {
|
|
217
|
+
render(<BreadcrumbHookTest />);
|
|
218
|
+
|
|
219
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('0');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('allows adding breadcrumbs', () => {
|
|
223
|
+
render(<BreadcrumbHookTest />);
|
|
224
|
+
|
|
225
|
+
const addButton = screen.getByText('Add Breadcrumb');
|
|
226
|
+
fireEvent.click(addButton);
|
|
227
|
+
|
|
228
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('1');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('allows removing breadcrumbs', () => {
|
|
232
|
+
render(<BreadcrumbHookTest />);
|
|
233
|
+
|
|
234
|
+
// Add a breadcrumb first
|
|
235
|
+
const addButton = screen.getByText('Add Breadcrumb');
|
|
236
|
+
fireEvent.click(addButton);
|
|
237
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('1');
|
|
238
|
+
|
|
239
|
+
// Remove it
|
|
240
|
+
const removeButton = screen.getByText('Remove First');
|
|
241
|
+
fireEvent.click(removeButton);
|
|
242
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('0');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('allows setting breadcrumbs', () => {
|
|
246
|
+
render(<BreadcrumbHookTest />);
|
|
247
|
+
|
|
248
|
+
const setButton = screen.getByText('Set Breadcrumbs');
|
|
249
|
+
fireEvent.click(setButton);
|
|
250
|
+
|
|
251
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('1');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('allows clearing all breadcrumbs', () => {
|
|
255
|
+
render(<BreadcrumbHookTest />);
|
|
256
|
+
|
|
257
|
+
// Add multiple breadcrumbs
|
|
258
|
+
const addButton = screen.getByText('Add Breadcrumb');
|
|
259
|
+
fireEvent.click(addButton);
|
|
260
|
+
fireEvent.click(addButton);
|
|
261
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('2');
|
|
262
|
+
|
|
263
|
+
// Clear all
|
|
264
|
+
const clearButton = screen.getByText('Clear All');
|
|
265
|
+
fireEvent.click(clearButton);
|
|
266
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('0');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { ErrorBoundary, withErrorBoundary } from '../ErrorBoundary';
|
|
4
|
+
|
|
5
|
+
// Test component that throws an error
|
|
6
|
+
const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
|
|
7
|
+
if (shouldThrow) {
|
|
8
|
+
throw new Error('Test error');
|
|
9
|
+
}
|
|
10
|
+
return <div>No error</div>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Mock console.error to avoid noise in tests
|
|
14
|
+
const originalError = console.error;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
console.error = jest.fn();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
console.error = originalError;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('ErrorBoundary', () => {
|
|
24
|
+
it('renders children when there is no error', () => {
|
|
25
|
+
render(
|
|
26
|
+
<ErrorBoundary>
|
|
27
|
+
<div>Test content</div>
|
|
28
|
+
</ErrorBoundary>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(screen.getByText('Test content')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders error UI when child component throws', () => {
|
|
35
|
+
render(
|
|
36
|
+
<ErrorBoundary>
|
|
37
|
+
<ThrowError shouldThrow={true} />
|
|
38
|
+
</ErrorBoundary>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText(/An unexpected error occurred/)).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByRole('button', { name: 'Try Again' })).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByRole('button', { name: 'Refresh Page' })).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders custom fallback UI when provided', () => {
|
|
48
|
+
const customFallback = <div>Custom error message</div>;
|
|
49
|
+
|
|
50
|
+
render(
|
|
51
|
+
<ErrorBoundary fallback={customFallback}>
|
|
52
|
+
<ThrowError shouldThrow={true} />
|
|
53
|
+
</ErrorBoundary>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText('Custom error message')).toBeInTheDocument();
|
|
57
|
+
expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('calls onError callback when error occurs', () => {
|
|
61
|
+
const onError = jest.fn();
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<ErrorBoundary onError={onError}>
|
|
65
|
+
<ThrowError shouldThrow={true} />
|
|
66
|
+
</ErrorBoundary>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(onError).toHaveBeenCalledWith(
|
|
71
|
+
expect.any(Error),
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
componentStack: expect.any(String)
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('shows error details in development mode', () => {
|
|
79
|
+
const originalEnv = process.env.NODE_ENV;
|
|
80
|
+
process.env.NODE_ENV = 'development';
|
|
81
|
+
|
|
82
|
+
render(
|
|
83
|
+
<ErrorBoundary>
|
|
84
|
+
<ThrowError shouldThrow={true} />
|
|
85
|
+
</ErrorBoundary>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByText(/Error Details \(Development Mode\)/)).toBeInTheDocument();
|
|
89
|
+
|
|
90
|
+
// Cleanup
|
|
91
|
+
process.env.NODE_ENV = originalEnv;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('shows error details when showErrorDetails is true', () => {
|
|
95
|
+
render(
|
|
96
|
+
<ErrorBoundary showErrorDetails={true}>
|
|
97
|
+
<ThrowError shouldThrow={true} />
|
|
98
|
+
</ErrorBoundary>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText(/Error Details \(Development Mode\)/)).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('resets error state when retry button is clicked', () => {
|
|
105
|
+
const TestComponent = () => {
|
|
106
|
+
const [shouldThrow, setShouldThrow] = React.useState(true);
|
|
107
|
+
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
// After a delay, stop throwing errors
|
|
110
|
+
const timer = setTimeout(() => setShouldThrow(false), 100);
|
|
111
|
+
return () => clearTimeout(timer);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
return <ThrowError shouldThrow={shouldThrow} />;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
render(
|
|
118
|
+
<ErrorBoundary>
|
|
119
|
+
<TestComponent />
|
|
120
|
+
</ErrorBoundary>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Error UI should be shown
|
|
124
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
125
|
+
|
|
126
|
+
// Click retry
|
|
127
|
+
fireEvent.click(screen.getByRole('button', { name: 'Try Again' }));
|
|
128
|
+
|
|
129
|
+
// Should attempt to render children again
|
|
130
|
+
// Note: This test is limited because the component will likely throw again
|
|
131
|
+
// In a real scenario, you'd fix the underlying issue before retrying
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('withErrorBoundary HOC', () => {
|
|
136
|
+
it('wraps component with ErrorBoundary', () => {
|
|
137
|
+
const TestComponent = () => <div>Test content</div>;
|
|
138
|
+
const WrappedComponent = withErrorBoundary(TestComponent);
|
|
139
|
+
|
|
140
|
+
render(<WrappedComponent />);
|
|
141
|
+
|
|
142
|
+
expect(screen.getByText('Test content')).toBeInTheDocument();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('passes errorBoundary props to ErrorBoundary', () => {
|
|
146
|
+
const onError = jest.fn();
|
|
147
|
+
const TestComponent = () => <ThrowError shouldThrow={true} />;
|
|
148
|
+
const WrappedComponent = withErrorBoundary(TestComponent, { onError });
|
|
149
|
+
|
|
150
|
+
render(<WrappedComponent />);
|
|
151
|
+
|
|
152
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('sets correct displayName', () => {
|
|
156
|
+
const TestComponent = () => <div>Test</div>;
|
|
157
|
+
TestComponent.displayName = 'TestComponent';
|
|
158
|
+
|
|
159
|
+
const WrappedComponent = withErrorBoundary(TestComponent);
|
|
160
|
+
|
|
161
|
+
expect(WrappedComponent.displayName).toBe('withErrorBoundary(TestComponent)');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -46,7 +46,7 @@ type FormBlockViewProps = ModelProps<FormBlockModel> & {
|
|
|
46
46
|
/**
|
|
47
47
|
* Form content
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
children?: React.ReactNode;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Footer content (links, additional text, etc.)
|
|
@@ -99,7 +99,7 @@ function FormBlockView({
|
|
|
99
99
|
title,
|
|
100
100
|
description,
|
|
101
101
|
coverImage,
|
|
102
|
-
form,
|
|
102
|
+
children: form,
|
|
103
103
|
footer,
|
|
104
104
|
status,
|
|
105
105
|
message,
|
package/src/components/index.ts
CHANGED
|
@@ -19,6 +19,9 @@ export * from './Scaffold';
|
|
|
19
19
|
export * from './ResponsiveMenu';
|
|
20
20
|
export * from './QwickApp';
|
|
21
21
|
export * from './AccessibilityChecker';
|
|
22
|
+
export * from './ErrorBoundary';
|
|
23
|
+
export * from './AccessibilityProvider';
|
|
24
|
+
export * from './Breadcrumbs';
|
|
22
25
|
// DataDrivenSafeSpan functionality is now integrated into SafeSpan
|
|
23
26
|
|
|
24
27
|
export { default as Logo } from './Logo';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwitchInputField - Switch toggle component with data binding support
|
|
3
|
+
*
|
|
4
|
+
* Provides a standardized switch field with:
|
|
5
|
+
* - Consistent Material-UI styling
|
|
6
|
+
* - Data binding capabilities
|
|
7
|
+
* - Label and helper text support
|
|
8
|
+
* - Focus and error handling
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import {
|
|
15
|
+
FormControl,
|
|
16
|
+
FormControlLabel,
|
|
17
|
+
FormHelperText,
|
|
18
|
+
Paper,
|
|
19
|
+
Switch,
|
|
20
|
+
Typography
|
|
21
|
+
} from '@mui/material';
|
|
22
|
+
import type { WithDataBinding, ModelProps } from '@qwickapps/schema';
|
|
23
|
+
import { QWICKAPP_COMPONENT, useBaseProps } from '../../hooks';
|
|
24
|
+
import { useDataBinding } from '../../hooks';
|
|
25
|
+
import SwitchInputFieldModel from '../../schemas/SwitchInputFieldSchema';
|
|
26
|
+
|
|
27
|
+
type SwitchInputFieldViewProps = ModelProps<SwitchInputFieldModel> & {
|
|
28
|
+
/** Change handler */
|
|
29
|
+
onChange?: (checked: boolean) => void;
|
|
30
|
+
/** Focus handler */
|
|
31
|
+
onFocus?: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface SwitchInputFieldProps extends SwitchInputFieldViewProps, WithDataBinding {}
|
|
35
|
+
|
|
36
|
+
// View component - handles the actual rendering
|
|
37
|
+
function SwitchInputFieldView({
|
|
38
|
+
label,
|
|
39
|
+
checked = false,
|
|
40
|
+
onChange,
|
|
41
|
+
onFocus,
|
|
42
|
+
required = false,
|
|
43
|
+
disabled = false,
|
|
44
|
+
error,
|
|
45
|
+
helperText,
|
|
46
|
+
size = 'medium',
|
|
47
|
+
color = 'primary',
|
|
48
|
+
...restProps
|
|
49
|
+
}: SwitchInputFieldViewProps) {
|
|
50
|
+
const { styleProps, htmlProps } = useBaseProps(restProps);
|
|
51
|
+
|
|
52
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
53
|
+
if (onChange) {
|
|
54
|
+
onChange(event.target.checked);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<FormControl
|
|
60
|
+
{...htmlProps}
|
|
61
|
+
{...styleProps}
|
|
62
|
+
error={!!error}
|
|
63
|
+
required={required}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
sx={{
|
|
66
|
+
display: 'block',
|
|
67
|
+
...styleProps.sx
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<FormControlLabel
|
|
71
|
+
control={
|
|
72
|
+
<Switch
|
|
73
|
+
checked={checked}
|
|
74
|
+
onChange={handleChange}
|
|
75
|
+
onFocus={onFocus}
|
|
76
|
+
size={size}
|
|
77
|
+
color={color}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
/>
|
|
80
|
+
}
|
|
81
|
+
label={label}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
/>
|
|
84
|
+
{(error || helperText) && (
|
|
85
|
+
<FormHelperText>
|
|
86
|
+
{error || helperText}
|
|
87
|
+
</FormHelperText>
|
|
88
|
+
)}
|
|
89
|
+
</FormControl>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* SwitchInputField component with data binding support
|
|
95
|
+
* Supports both traditional props and dataSource-driven rendering
|
|
96
|
+
*/
|
|
97
|
+
function SwitchInputField(props: SwitchInputFieldProps) {
|
|
98
|
+
const { dataSource, bindingOptions, ...restProps } = props;
|
|
99
|
+
|
|
100
|
+
// If no dataSource, use traditional props
|
|
101
|
+
if (!dataSource) {
|
|
102
|
+
return <SwitchInputFieldView {...restProps} />;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Use data binding
|
|
106
|
+
const bindingResult = useDataBinding<SwitchInputFieldModel>(
|
|
107
|
+
dataSource,
|
|
108
|
+
restProps as Partial<SwitchInputFieldModel>,
|
|
109
|
+
SwitchInputFieldModel.getSchema(),
|
|
110
|
+
{ cache: true, cacheTTL: 300000, strict: false, ...bindingOptions }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Check if we're still loading data using the metadata properties
|
|
114
|
+
const bindingLoading = bindingResult.$loading;
|
|
115
|
+
|
|
116
|
+
// Extract all the actual switch properties (excluding metadata)
|
|
117
|
+
const { dataSource: _source, $loading, $error, $dataSource, $cached, cached, ...switchInputFieldProps } = bindingResult;
|
|
118
|
+
const error = bindingResult.$error;
|
|
119
|
+
|
|
120
|
+
// Show loading state while fetching data
|
|
121
|
+
if (bindingLoading) {
|
|
122
|
+
return (
|
|
123
|
+
<Paper
|
|
124
|
+
variant="outlined"
|
|
125
|
+
sx={{
|
|
126
|
+
p: 2,
|
|
127
|
+
textAlign: 'center'
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<Typography variant="body2">Loading SwitchInputField...</Typography>
|
|
131
|
+
<Typography variant="caption" color="text.secondary">
|
|
132
|
+
Loading switch field configuration from data source...
|
|
133
|
+
</Typography>
|
|
134
|
+
</Paper>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (error) {
|
|
139
|
+
console.error('Error loading switch input field:', error);
|
|
140
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
141
|
+
return (
|
|
142
|
+
<Paper
|
|
143
|
+
variant="outlined"
|
|
144
|
+
sx={{
|
|
145
|
+
p: 2,
|
|
146
|
+
textAlign: 'center',
|
|
147
|
+
borderColor: 'error.main'
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<Typography variant="body2" color="error">
|
|
151
|
+
Error loading switch field: {error.message}
|
|
152
|
+
</Typography>
|
|
153
|
+
</Paper>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return <SwitchInputFieldView {...switchInputFieldProps} />;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Mark as QwickApp component
|
|
163
|
+
(SwitchInputField as any)[QWICKAPP_COMPONENT] = true;
|
|
164
|
+
|
|
165
|
+
export default SwitchInputField;
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
export { default as ChoiceInputField } from './ChoiceInputField';
|
|
8
8
|
export { default as HtmlInputField } from './HtmlInputField';
|
|
9
9
|
export { default as SelectInputField } from './SelectInputField';
|
|
10
|
+
export { default as SwitchInputField } from './SwitchInputField';
|
|
10
11
|
export { default as TextField } from './TextField';
|
|
11
12
|
export { default as TextInputField } from './TextInputField';
|
|
12
13
|
|
|
13
14
|
export type { ChoiceInputFieldProps } from './ChoiceInputField';
|
|
14
15
|
export type { HtmlInputFieldProps } from './HtmlInputField';
|
|
15
16
|
export type { SelectInputFieldProps, SelectOption } from './SelectInputField';
|
|
17
|
+
export type { SwitchInputFieldProps } from './SwitchInputField';
|
|
16
18
|
export type { TextFieldProps } from './TextField';
|
|
17
19
|
export type { TextInputFieldProps } from './TextInputField';
|