@object-ui/plugin-grid 0.5.0 → 3.0.0

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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +51 -6
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +97 -0
  4. package/dist/index.js +994 -584
  5. package/dist/index.umd.cjs +3 -3
  6. package/dist/packages/plugin-grid/src/InlineEditing.d.ts +28 -0
  7. package/dist/packages/plugin-grid/src/ListColumnExtensions.test.d.ts +0 -0
  8. package/dist/packages/plugin-grid/src/ListColumnSchema.test.d.ts +1 -0
  9. package/dist/packages/plugin-grid/src/ObjectGrid.EdgeCases.stories.d.ts +25 -0
  10. package/dist/packages/plugin-grid/src/ObjectGrid.d.ts +7 -1
  11. package/dist/packages/plugin-grid/src/ObjectGrid.stories.d.ts +33 -0
  12. package/dist/packages/plugin-grid/src/__tests__/InlineEditing.test.d.ts +0 -0
  13. package/dist/packages/plugin-grid/src/__tests__/VirtualGrid.test.d.ts +0 -0
  14. package/dist/packages/plugin-grid/src/__tests__/accessibility.test.d.ts +0 -0
  15. package/dist/packages/plugin-grid/src/__tests__/performance-benchmark.test.d.ts +0 -0
  16. package/dist/packages/plugin-grid/src/__tests__/view-states.test.d.ts +0 -0
  17. package/dist/packages/plugin-grid/src/index.d.ts +5 -0
  18. package/dist/packages/plugin-grid/src/useGroupedData.d.ts +30 -0
  19. package/dist/packages/plugin-grid/src/useRowColor.d.ts +8 -0
  20. package/package.json +11 -10
  21. package/src/InlineEditing.tsx +235 -0
  22. package/src/ListColumnExtensions.test.tsx +374 -0
  23. package/src/ListColumnSchema.test.ts +88 -0
  24. package/src/ObjectGrid.EdgeCases.stories.tsx +147 -0
  25. package/src/ObjectGrid.msw.test.tsx +24 -1
  26. package/src/ObjectGrid.stories.tsx +139 -0
  27. package/src/ObjectGrid.tsx +409 -113
  28. package/src/__tests__/InlineEditing.test.tsx +360 -0
  29. package/src/__tests__/VirtualGrid.test.tsx +438 -0
  30. package/src/__tests__/accessibility.test.tsx +254 -0
  31. package/src/__tests__/performance-benchmark.test.tsx +182 -0
  32. package/src/__tests__/view-states.test.tsx +203 -0
  33. package/src/index.tsx +17 -2
  34. package/src/useGroupedData.ts +122 -0
  35. package/src/useRowColor.ts +74 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * InlineEditing Component Tests
3
+ *
4
+ * Tests for the grid inline editing component covering save/cancel,
5
+ * validation feedback, keyboard navigation, and display modes.
6
+ */
7
+
8
+ import { describe, it, expect, vi } from 'vitest';
9
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
10
+ import userEvent from '@testing-library/user-event';
11
+ import '@testing-library/jest-dom';
12
+ import { InlineEditing } from '../InlineEditing';
13
+
14
+ // Mock lucide-react icons
15
+ vi.mock('lucide-react', () => ({
16
+ Check: (props: any) => <svg data-testid="icon-check" {...props} />,
17
+ X: (props: any) => <svg data-testid="icon-x" {...props} />,
18
+ }));
19
+
20
+ // Mock cn utility to pass through classes
21
+ vi.mock('@object-ui/components', () => ({
22
+ cn: (...args: any[]) => args.filter(Boolean).join(' '),
23
+ }));
24
+
25
+ describe('InlineEditing', () => {
26
+ describe('Display mode', () => {
27
+ it('renders the value in display mode by default', () => {
28
+ render(<InlineEditing value="Hello" onSave={vi.fn()} />);
29
+
30
+ expect(screen.getByText('Hello')).toBeInTheDocument();
31
+ // Should not show an input
32
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
33
+ });
34
+
35
+ it('shows placeholder when value is empty', () => {
36
+ render(<InlineEditing value="" onSave={vi.fn()} placeholder="Enter text" />);
37
+
38
+ expect(screen.getByText('Enter text')).toBeInTheDocument();
39
+ });
40
+
41
+ it('shows default placeholder when value is null', () => {
42
+ render(<InlineEditing value={null} onSave={vi.fn()} />);
43
+
44
+ expect(screen.getByText('Click to edit')).toBeInTheDocument();
45
+ });
46
+
47
+ it('has data-slot="inline-editing" on root', () => {
48
+ const { container } = render(<InlineEditing value="test" onSave={vi.fn()} />);
49
+ expect(container.querySelector('[data-slot="inline-editing"]')).toBeInTheDocument();
50
+ });
51
+
52
+ it('applies custom className', () => {
53
+ const { container } = render(
54
+ <InlineEditing value="test" onSave={vi.fn()} className="custom-class" />,
55
+ );
56
+ const root = container.querySelector('[data-slot="inline-editing"]');
57
+ expect(root?.className).toContain('custom-class');
58
+ });
59
+ });
60
+
61
+ describe('Entering edit mode', () => {
62
+ it('enters edit mode on click', async () => {
63
+ const user = userEvent.setup();
64
+ render(<InlineEditing value="Hello" onSave={vi.fn()} />);
65
+
66
+ await user.click(screen.getByRole('button'));
67
+
68
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
69
+ expect((screen.getByRole('textbox') as HTMLInputElement).value).toBe('Hello');
70
+ });
71
+
72
+ it('enters edit mode on Enter key press', () => {
73
+ render(<InlineEditing value="Hello" onSave={vi.fn()} />);
74
+
75
+ fireEvent.keyDown(screen.getByRole('button'), { key: 'Enter' });
76
+
77
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
78
+ });
79
+
80
+ it('enters edit mode on Space key press', () => {
81
+ render(<InlineEditing value="Hello" onSave={vi.fn()} />);
82
+
83
+ fireEvent.keyDown(screen.getByRole('button'), { key: ' ' });
84
+
85
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
86
+ });
87
+
88
+ it('starts in edit mode when editing prop is true', () => {
89
+ render(<InlineEditing value="Hello" onSave={vi.fn()} editing={true} />);
90
+
91
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
92
+ });
93
+
94
+ it('does not enter edit mode when disabled', async () => {
95
+ const user = userEvent.setup();
96
+ render(<InlineEditing value="Hello" onSave={vi.fn()} disabled />);
97
+
98
+ await user.click(screen.getByText('Hello'));
99
+
100
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
101
+ });
102
+ });
103
+
104
+ describe('Save', () => {
105
+ it('saves on Enter key press', async () => {
106
+ const user = userEvent.setup();
107
+ const onSave = vi.fn();
108
+ render(<InlineEditing value="Hello" onSave={onSave} editing={true} />);
109
+
110
+ const input = screen.getByRole('textbox');
111
+ await user.clear(input);
112
+ await user.type(input, 'World');
113
+ await user.keyboard('{Enter}');
114
+
115
+ await waitFor(() => {
116
+ expect(onSave).toHaveBeenCalledWith('World');
117
+ });
118
+ });
119
+
120
+ it('saves on save button click', async () => {
121
+ const user = userEvent.setup();
122
+ const onSave = vi.fn();
123
+ render(<InlineEditing value="Hello" onSave={onSave} editing={true} />);
124
+
125
+ const input = screen.getByRole('textbox');
126
+ await user.clear(input);
127
+ await user.type(input, 'Updated');
128
+
129
+ await user.click(screen.getByLabelText('Save'));
130
+
131
+ await waitFor(() => {
132
+ expect(onSave).toHaveBeenCalledWith('Updated');
133
+ });
134
+ });
135
+
136
+ it('coerces value to number when type is number', async () => {
137
+ const user = userEvent.setup();
138
+ const onSave = vi.fn();
139
+ render(
140
+ <InlineEditing value={42} onSave={onSave} type="number" editing={true} />,
141
+ );
142
+
143
+ const input = screen.getByRole('spinbutton');
144
+ await user.clear(input);
145
+ await user.type(input, '99');
146
+ await user.click(screen.getByLabelText('Save'));
147
+
148
+ await waitFor(() => {
149
+ expect(onSave).toHaveBeenCalledWith(99);
150
+ });
151
+ });
152
+
153
+ it('exits edit mode after successful save', async () => {
154
+ const user = userEvent.setup();
155
+ const onSave = vi.fn();
156
+ render(<InlineEditing value="Hello" onSave={onSave} editing={true} />);
157
+
158
+ await user.click(screen.getByLabelText('Save'));
159
+
160
+ await waitFor(() => {
161
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
162
+ });
163
+ });
164
+ });
165
+
166
+ describe('Cancel', () => {
167
+ it('cancels on Escape key press', async () => {
168
+ const user = userEvent.setup();
169
+ const onCancel = vi.fn();
170
+ render(
171
+ <InlineEditing value="Hello" onSave={vi.fn()} onCancel={onCancel} editing={true} />,
172
+ );
173
+
174
+ const input = screen.getByRole('textbox');
175
+ await user.type(input, ' extra');
176
+ await user.keyboard('{Escape}');
177
+
178
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
179
+ expect(onCancel).toHaveBeenCalled();
180
+ });
181
+
182
+ it('cancels on cancel button click', async () => {
183
+ const user = userEvent.setup();
184
+ const onCancel = vi.fn();
185
+ render(
186
+ <InlineEditing value="Hello" onSave={vi.fn()} onCancel={onCancel} editing={true} />,
187
+ );
188
+
189
+ await user.click(screen.getByLabelText('Cancel'));
190
+
191
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
192
+ expect(onCancel).toHaveBeenCalled();
193
+ });
194
+
195
+ it('restores original value after cancel', async () => {
196
+ const user = userEvent.setup();
197
+ render(
198
+ <InlineEditing value="Original" onSave={vi.fn()} editing={true} />,
199
+ );
200
+
201
+ const input = screen.getByRole('textbox');
202
+ await user.clear(input);
203
+ await user.type(input, 'Changed');
204
+ await user.keyboard('{Escape}');
205
+
206
+ // Back to display mode, showing original value
207
+ expect(screen.getByText('Original')).toBeInTheDocument();
208
+ });
209
+ });
210
+
211
+ describe('Validation', () => {
212
+ it('shows validation error and prevents save', async () => {
213
+ const user = userEvent.setup();
214
+ const onSave = vi.fn();
215
+ const validate = (val: any) =>
216
+ String(val).trim() === '' ? 'Required field' : undefined;
217
+
218
+ render(
219
+ <InlineEditing
220
+ value="Hello"
221
+ onSave={onSave}
222
+ validate={validate}
223
+ editing={true}
224
+ />,
225
+ );
226
+
227
+ const input = screen.getByRole('textbox');
228
+ await user.clear(input);
229
+ await user.click(screen.getByLabelText('Save'));
230
+
231
+ expect(screen.getByRole('alert')).toHaveTextContent('Required field');
232
+ expect(onSave).not.toHaveBeenCalled();
233
+ // Should still be in edit mode
234
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
235
+ });
236
+
237
+ it('clears validation error on input change', async () => {
238
+ const user = userEvent.setup();
239
+ const validate = (val: any) =>
240
+ String(val).trim() === '' ? 'Required field' : undefined;
241
+
242
+ render(
243
+ <InlineEditing
244
+ value="Hello"
245
+ onSave={vi.fn()}
246
+ validate={validate}
247
+ editing={true}
248
+ />,
249
+ );
250
+
251
+ const input = screen.getByRole('textbox');
252
+ await user.clear(input);
253
+ await user.click(screen.getByLabelText('Save'));
254
+
255
+ expect(screen.getByRole('alert')).toBeInTheDocument();
256
+
257
+ // Start typing to clear error
258
+ await user.type(input, 'A');
259
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
260
+ });
261
+
262
+ it('shows error returned from onSave', async () => {
263
+ const user = userEvent.setup();
264
+ const onSave = vi.fn().mockReturnValue('Server error');
265
+
266
+ render(
267
+ <InlineEditing value="Hello" onSave={onSave} editing={true} />,
268
+ );
269
+
270
+ await user.click(screen.getByLabelText('Save'));
271
+
272
+ await waitFor(() => {
273
+ expect(screen.getByRole('alert')).toHaveTextContent('Server error');
274
+ });
275
+ });
276
+
277
+ it('marks input as aria-invalid when error is present', async () => {
278
+ const user = userEvent.setup();
279
+ const validate = () => 'Error';
280
+
281
+ render(
282
+ <InlineEditing value="" onSave={vi.fn()} validate={validate} editing={true} />,
283
+ );
284
+
285
+ await user.click(screen.getByLabelText('Save'));
286
+
287
+ expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
288
+ });
289
+ });
290
+
291
+ describe('Async save', () => {
292
+ it('handles async onSave', async () => {
293
+ const user = userEvent.setup();
294
+ const onSave = vi.fn().mockResolvedValue(undefined);
295
+
296
+ render(<InlineEditing value="Hello" onSave={onSave} editing={true} />);
297
+
298
+ await user.click(screen.getByLabelText('Save'));
299
+
300
+ await waitFor(() => {
301
+ expect(onSave).toHaveBeenCalledWith('Hello');
302
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
303
+ });
304
+ });
305
+
306
+ it('shows error when async onSave throws', async () => {
307
+ const user = userEvent.setup();
308
+ const onSave = vi.fn().mockRejectedValue(new Error('Network error'));
309
+
310
+ render(<InlineEditing value="Hello" onSave={onSave} editing={true} />);
311
+
312
+ await user.click(screen.getByLabelText('Save'));
313
+
314
+ await waitFor(() => {
315
+ expect(screen.getByRole('alert')).toHaveTextContent('Network error');
316
+ });
317
+
318
+ // Should still be in edit mode
319
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
320
+ });
321
+ });
322
+
323
+ describe('Input types', () => {
324
+ it('renders text input by default', () => {
325
+ render(<InlineEditing value="text" onSave={vi.fn()} editing={true} />);
326
+ expect(screen.getByRole('textbox')).toHaveAttribute('type', 'text');
327
+ });
328
+
329
+ it('renders number input when type is number', () => {
330
+ render(<InlineEditing value={42} onSave={vi.fn()} type="number" editing={true} />);
331
+ expect(screen.getByRole('spinbutton')).toHaveAttribute('type', 'number');
332
+ });
333
+
334
+ it('renders email input when type is email', () => {
335
+ render(<InlineEditing value="a@b.com" onSave={vi.fn()} type="email" editing={true} />);
336
+ const input = screen.getByDisplayValue('a@b.com');
337
+ expect(input).toHaveAttribute('type', 'email');
338
+ });
339
+ });
340
+
341
+ describe('data-slot attributes', () => {
342
+ it('has correct data-slot attributes in display mode', () => {
343
+ const { container } = render(<InlineEditing value="test" onSave={vi.fn()} />);
344
+
345
+ expect(container.querySelector('[data-slot="inline-editing"]')).toBeInTheDocument();
346
+ expect(container.querySelector('[data-slot="inline-editing-display"]')).toBeInTheDocument();
347
+ });
348
+
349
+ it('has correct data-slot attributes in edit mode', () => {
350
+ const { container } = render(
351
+ <InlineEditing value="test" onSave={vi.fn()} editing={true} />,
352
+ );
353
+
354
+ expect(container.querySelector('[data-slot="inline-editing"]')).toBeInTheDocument();
355
+ expect(container.querySelector('[data-slot="inline-editing-input"]')).toBeInTheDocument();
356
+ expect(container.querySelector('[data-slot="inline-editing-save"]')).toBeInTheDocument();
357
+ expect(container.querySelector('[data-slot="inline-editing-cancel"]')).toBeInTheDocument();
358
+ });
359
+ });
360
+ });