@cdx-ui/data-grid 0.3.10 → 0.3.12

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 (82) hide show
  1. package/README.md +3 -1
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/DataGrid.js +232 -62
  4. package/lib/commonjs/DataGrid.js.map +1 -1
  5. package/lib/commonjs/DataGridFilterPanel.js +420 -0
  6. package/lib/commonjs/DataGridFilterPanel.js.map +1 -0
  7. package/lib/commonjs/adapter/columnTypes.js +256 -0
  8. package/lib/commonjs/adapter/columnTypes.js.map +1 -0
  9. package/lib/commonjs/adapter/columns.js +132 -4
  10. package/lib/commonjs/adapter/columns.js.map +1 -1
  11. package/lib/commonjs/adapter/filterOperators.js +422 -0
  12. package/lib/commonjs/adapter/filterOperators.js.map +1 -0
  13. package/lib/commonjs/adapter/pinning.js +101 -0
  14. package/lib/commonjs/adapter/pinning.js.map +1 -0
  15. package/lib/commonjs/index.js +26 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/numberCells.js +280 -0
  18. package/lib/commonjs/numberCells.js.map +1 -0
  19. package/lib/commonjs/styles.js +61 -1
  20. package/lib/commonjs/styles.js.map +1 -1
  21. package/lib/commonjs/types.js +29 -1
  22. package/lib/commonjs/types.js.map +1 -1
  23. package/lib/commonjs/useDataGridTable.js +98 -13
  24. package/lib/commonjs/useDataGridTable.js.map +1 -1
  25. package/lib/module/DataGrid.js +237 -67
  26. package/lib/module/DataGrid.js.map +1 -1
  27. package/lib/module/DataGridFilterPanel.js +414 -0
  28. package/lib/module/DataGridFilterPanel.js.map +1 -0
  29. package/lib/module/adapter/columnTypes.js +240 -0
  30. package/lib/module/adapter/columnTypes.js.map +1 -0
  31. package/lib/module/adapter/columns.js +129 -4
  32. package/lib/module/adapter/columns.js.map +1 -1
  33. package/lib/module/adapter/filterOperators.js +406 -0
  34. package/lib/module/adapter/filterOperators.js.map +1 -0
  35. package/lib/module/adapter/pinning.js +90 -0
  36. package/lib/module/adapter/pinning.js.map +1 -0
  37. package/lib/module/index.js +2 -0
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/numberCells.js +275 -0
  40. package/lib/module/numberCells.js.map +1 -0
  41. package/lib/module/styles.js +60 -0
  42. package/lib/module/styles.js.map +1 -1
  43. package/lib/module/types.js +28 -0
  44. package/lib/module/types.js.map +1 -1
  45. package/lib/module/useDataGridTable.js +99 -14
  46. package/lib/module/useDataGridTable.js.map +1 -1
  47. package/lib/typescript/DataGrid.d.ts.map +1 -1
  48. package/lib/typescript/DataGridFilterPanel.d.ts +17 -0
  49. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
  50. package/lib/typescript/adapter/columnTypes.d.ts +32 -0
  51. package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
  52. package/lib/typescript/adapter/columns.d.ts +14 -5
  53. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  54. package/lib/typescript/adapter/filterOperators.d.ts +43 -0
  55. package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
  56. package/lib/typescript/adapter/pinning.d.ts +25 -0
  57. package/lib/typescript/adapter/pinning.d.ts.map +1 -0
  58. package/lib/typescript/index.d.ts +4 -1
  59. package/lib/typescript/index.d.ts.map +1 -1
  60. package/lib/typescript/numberCells.d.ts +46 -0
  61. package/lib/typescript/numberCells.d.ts.map +1 -0
  62. package/lib/typescript/styles.d.ts +21 -0
  63. package/lib/typescript/styles.d.ts.map +1 -1
  64. package/lib/typescript/types.d.ts +86 -7
  65. package/lib/typescript/types.d.ts.map +1 -1
  66. package/lib/typescript/useDataGridTable.d.ts +18 -4
  67. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  68. package/package.json +5 -5
  69. package/src/DataGrid.tsx +377 -67
  70. package/src/DataGridFilterPanel.tsx +477 -0
  71. package/src/adapter/columnTypes.ts +327 -0
  72. package/src/adapter/columns.test.ts +75 -1
  73. package/src/adapter/columns.ts +178 -3
  74. package/src/adapter/filterOperators.test.ts +292 -0
  75. package/src/adapter/filterOperators.ts +524 -0
  76. package/src/adapter/pinning.ts +125 -0
  77. package/src/index.ts +15 -0
  78. package/src/numberCells.tsx +379 -0
  79. package/src/styles.ts +75 -0
  80. package/src/types.ts +114 -7
  81. package/src/useDataGridTable.test.ts +69 -0
  82. package/src/useDataGridTable.ts +127 -8
@@ -0,0 +1,477 @@
1
+ import { useRef, type RefObject } from 'react';
2
+ import { View } from 'react-native';
3
+ import { useDismissOverlay, useEscapeKey } from '@cdx-ui/primitives';
4
+ import {
5
+ Button,
6
+ DateField,
7
+ DatePicker,
8
+ IconButton,
9
+ Input,
10
+ Select,
11
+ Text,
12
+ type DatePickerProps,
13
+ } from '@cdx-ui/components';
14
+ import { Add, Close, Delete } from '@cdx-ui/icons';
15
+ import {
16
+ filterOperatorNeedsValue,
17
+ filterValueToInputText,
18
+ getDefaultFilterOperator,
19
+ getFilterOperatorLabel,
20
+ getGridFilterOperators,
21
+ headerFilterValueFromText,
22
+ } from './adapter/filterOperators';
23
+ import {
24
+ dataGridFilterPanelColumnVariants,
25
+ dataGridFilterPanelFieldVariants,
26
+ dataGridFilterPanelFooterVariants,
27
+ dataGridFilterPanelLogicVariants,
28
+ dataGridFilterPanelOperatorVariants,
29
+ dataGridFilterPanelRowVariants,
30
+ dataGridFilterPanelValueVariants,
31
+ dataGridFilterPanelVariants,
32
+ } from './styles';
33
+ import type {
34
+ GridColDef,
35
+ GridFilterItem,
36
+ GridFilterModel,
37
+ GridFilterOperator,
38
+ GridLogicOperator,
39
+ GridValidRowModel,
40
+ } from './types';
41
+
42
+ const LOGIC_OPERATORS: readonly { value: GridLogicOperator; label: string }[] = [
43
+ { value: 'and', label: 'And' },
44
+ { value: 'or', label: 'Or' },
45
+ ];
46
+
47
+ const BOOLEAN_FILTER_VALUES: readonly { value: 'any' | 'true' | 'false'; label: string }[] = [
48
+ { value: 'any', label: 'any' },
49
+ { value: 'true', label: 'true' },
50
+ { value: 'false', label: 'false' },
51
+ ];
52
+
53
+ function booleanSelectValue(value: unknown): 'any' | 'true' | 'false' {
54
+ if (value === true || value === 'true') {
55
+ return 'true';
56
+ }
57
+ if (value === false || value === 'false') {
58
+ return 'false';
59
+ }
60
+ return 'any';
61
+ }
62
+
63
+ function booleanFilterItem(
64
+ field: string,
65
+ operator: GridFilterOperator,
66
+ next: string,
67
+ ): GridFilterItem {
68
+ if (next === 'true') {
69
+ return { field, operator, value: true };
70
+ }
71
+ if (next === 'false') {
72
+ return { field, operator, value: false };
73
+ }
74
+ return { field, operator };
75
+ }
76
+
77
+ function logicOperatorLabel(operator: GridLogicOperator): string {
78
+ return operator === 'or' ? 'Or' : 'And';
79
+ }
80
+
81
+ export function filterPlaceholder<R extends GridValidRowModel>(
82
+ col: GridColDef<R> | undefined,
83
+ operator: GridFilterOperator,
84
+ ): string {
85
+ if (operator === 'isAnyOf') {
86
+ return 'a, b';
87
+ }
88
+ switch (col?.type) {
89
+ case 'number':
90
+ return '0';
91
+ case 'date':
92
+ case 'dateTime':
93
+ return 'YYYY-MM-DD';
94
+ case 'boolean':
95
+ return 'true';
96
+ default:
97
+ return 'Filter value';
98
+ }
99
+ }
100
+
101
+ function isDateColumn<R extends GridValidRowModel>(col: GridColDef<R> | undefined): boolean {
102
+ return col?.type === 'date' || col?.type === 'dateTime';
103
+ }
104
+
105
+ function padDatePart(value: number): string {
106
+ return String(value).padStart(2, '0');
107
+ }
108
+
109
+ /** `DatePicker` speaks `YYYY-MM-DD`. The filter model keeps a local `Date`. */
110
+ function dateValueToPickerValue(value: unknown): DatePickerProps['value'] {
111
+ let wire: string | undefined;
112
+ if (value instanceof Date && Number.isFinite(value.getTime())) {
113
+ wire = `${String(value.getFullYear())}-${padDatePart(value.getMonth() + 1)}-${padDatePart(value.getDate())}`;
114
+ } else if (typeof value === 'string' && pickerValueToDate(value.slice(0, 10))) {
115
+ wire = value.slice(0, 10);
116
+ }
117
+ return wire as DatePickerProps['value'];
118
+ }
119
+
120
+ function pickerValueToDate(value: string): Date | undefined {
121
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
122
+ if (!match) {
123
+ return undefined;
124
+ }
125
+ const year = Number(match[1]);
126
+ const month = Number(match[2]);
127
+ const day = Number(match[3]);
128
+ const date = new Date(year, month - 1, day);
129
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
130
+ ? date
131
+ : undefined;
132
+ }
133
+
134
+ function FilterDateValue({
135
+ value,
136
+ accessibilityLabel,
137
+ onChange,
138
+ }: {
139
+ value: unknown;
140
+ accessibilityLabel: string;
141
+ onChange: (date: Date | undefined) => void;
142
+ }) {
143
+ return (
144
+ <DatePicker
145
+ value={dateValueToPickerValue(value)}
146
+ onValueChange={(next) => onChange(next === undefined ? undefined : pickerValueToDate(next))}
147
+ className="w-full gap-0"
148
+ >
149
+ <DateField.Group size="small" fullWidth>
150
+ <DateField.Input />
151
+ <DateField.Suffix>
152
+ <DatePicker.Trigger>
153
+ <DatePicker.TriggerIndicator />
154
+ </DatePicker.Trigger>
155
+ </DateField.Suffix>
156
+ </DateField.Group>
157
+ <DatePicker.Popover>
158
+ <DatePicker.Calendar aria-label={accessibilityLabel} />
159
+ </DatePicker.Popover>
160
+ </DatePicker>
161
+ );
162
+ }
163
+
164
+ export function createDefaultFilterItem<R extends GridValidRowModel>(
165
+ columns: readonly GridColDef<R>[],
166
+ ): GridFilterItem | undefined {
167
+ if (columns.length === 0) {
168
+ return undefined;
169
+ }
170
+ const col = columns[0];
171
+ return { field: col.field, operator: getDefaultFilterOperator(col.type) };
172
+ }
173
+
174
+ function resolveItemOperator<R extends GridValidRowModel>(
175
+ col: GridColDef<R> | undefined,
176
+ item: GridFilterItem,
177
+ ): GridFilterOperator {
178
+ const operators = getGridFilterOperators(col?.type);
179
+ return item.operator !== undefined && operators.includes(item.operator)
180
+ ? item.operator
181
+ : getDefaultFilterOperator(col?.type);
182
+ }
183
+
184
+ function itemWithValueText<R extends GridValidRowModel>(
185
+ col: GridColDef<R> | undefined,
186
+ field: string,
187
+ operator: GridFilterOperator,
188
+ text: string,
189
+ ): GridFilterItem {
190
+ if (!filterOperatorNeedsValue(operator)) {
191
+ return { field, operator };
192
+ }
193
+ return {
194
+ field,
195
+ operator,
196
+ value: text.trim().length > 0 ? headerFilterValueFromText(col, operator, text) : undefined,
197
+ };
198
+ }
199
+
200
+ function FilterPanelRow<R extends GridValidRowModel>({
201
+ index,
202
+ item,
203
+ columns,
204
+ logicOperator,
205
+ onLogicOperatorChange,
206
+ onChange,
207
+ onRemove,
208
+ }: {
209
+ index: number;
210
+ item: GridFilterItem;
211
+ columns: readonly GridColDef<R>[];
212
+ logicOperator: GridLogicOperator;
213
+ onLogicOperatorChange: (operator: GridLogicOperator) => void;
214
+ onChange: (item: GridFilterItem) => void;
215
+ onRemove: () => void;
216
+ }) {
217
+ const col = columns.find((candidate) => candidate.field === item.field);
218
+ const operator = resolveItemOperator(col, item);
219
+ const operators = getGridFilterOperators(col?.type);
220
+ const valueText = filterValueToInputText(item.value);
221
+ const rowLabel = `filter ${(index + 1).toString()}`;
222
+
223
+ return (
224
+ <View className={dataGridFilterPanelRowVariants()} accessibilityRole="none">
225
+ <IconButton
226
+ as={Close}
227
+ variant="ghost"
228
+ color="neutral"
229
+ size="small"
230
+ iconClassName="size-4"
231
+ accessibilityLabel={`Remove ${rowLabel}`}
232
+ onPress={onRemove}
233
+ />
234
+ <View className={dataGridFilterPanelLogicVariants()}>
235
+ {index === 1 ? (
236
+ <Select
237
+ items={[...LOGIC_OPERATORS]}
238
+ value={logicOperator}
239
+ onValueChange={(next) => {
240
+ const nextOperator = LOGIC_OPERATORS.find((candidate) => candidate.value === next);
241
+ if (nextOperator) {
242
+ onLogicOperatorChange(nextOperator.value);
243
+ }
244
+ }}
245
+ size="small"
246
+ className="w-full"
247
+ accessibilityLabel="Logic operator"
248
+ >
249
+ <Select.Trigger>
250
+ <Select.Value />
251
+ </Select.Trigger>
252
+ <Select.Content>
253
+ {LOGIC_OPERATORS.map((candidate) => (
254
+ <Select.Item key={candidate.value} value={candidate.value}>
255
+ <Select.ItemLabel>{candidate.label}</Select.ItemLabel>
256
+ </Select.Item>
257
+ ))}
258
+ </Select.Content>
259
+ </Select>
260
+ ) : index > 1 ? (
261
+ <Text className="px-3 text-sm text-content-secondary">
262
+ {logicOperatorLabel(logicOperator)}
263
+ </Text>
264
+ ) : null}
265
+ </View>
266
+ <View className={dataGridFilterPanelFieldVariants({ size: 'column' })}>
267
+ <Text className="text-xs text-content-secondary">Columns</Text>
268
+ <Select
269
+ items={columns.map((candidate) => ({
270
+ value: candidate.field,
271
+ label: candidate.headerName ?? candidate.field,
272
+ }))}
273
+ value={item.field}
274
+ onValueChange={(next) => {
275
+ const nextCol = columns.find((candidate) => candidate.field === next);
276
+ if (nextCol && nextCol.field !== item.field) {
277
+ onChange({ field: nextCol.field, operator: getDefaultFilterOperator(nextCol.type) });
278
+ }
279
+ }}
280
+ size="small"
281
+ className={dataGridFilterPanelColumnVariants()}
282
+ accessibilityLabel={`Column for ${rowLabel}`}
283
+ >
284
+ <Select.Trigger>
285
+ <Select.Value />
286
+ </Select.Trigger>
287
+ <Select.Content>
288
+ {columns.map((candidate) => (
289
+ <Select.Item key={candidate.field} value={candidate.field}>
290
+ <Select.ItemLabel>{candidate.headerName ?? candidate.field}</Select.ItemLabel>
291
+ </Select.Item>
292
+ ))}
293
+ </Select.Content>
294
+ </Select>
295
+ </View>
296
+ <View className={dataGridFilterPanelFieldVariants({ size: 'operator' })}>
297
+ <Text className="text-xs text-content-secondary">Operator</Text>
298
+ <Select
299
+ items={operators.map((candidate) => ({
300
+ value: candidate,
301
+ label: getFilterOperatorLabel(candidate),
302
+ }))}
303
+ value={operator}
304
+ onValueChange={(next) => {
305
+ const nextOperator = operators.find((candidate) => candidate === next);
306
+ if (nextOperator === undefined) {
307
+ return;
308
+ }
309
+ if (!filterOperatorNeedsValue(nextOperator)) {
310
+ onChange({ field: item.field, operator: nextOperator });
311
+ } else if (isDateColumn(col)) {
312
+ onChange({ field: item.field, operator: nextOperator, value: item.value });
313
+ } else if (col?.type === 'boolean') {
314
+ onChange(booleanFilterItem(item.field, nextOperator, booleanSelectValue(item.value)));
315
+ } else {
316
+ onChange(itemWithValueText(col, item.field, nextOperator, valueText));
317
+ }
318
+ }}
319
+ size="small"
320
+ className={dataGridFilterPanelOperatorVariants()}
321
+ accessibilityLabel={`Operator for ${rowLabel}`}
322
+ >
323
+ <Select.Trigger>
324
+ <Select.Value />
325
+ </Select.Trigger>
326
+ <Select.Content>
327
+ {operators.map((candidate) => (
328
+ <Select.Item key={candidate} value={candidate}>
329
+ <Select.ItemLabel>{getFilterOperatorLabel(candidate)}</Select.ItemLabel>
330
+ </Select.Item>
331
+ ))}
332
+ </Select.Content>
333
+ </Select>
334
+ </View>
335
+ <View className={dataGridFilterPanelFieldVariants({ size: 'value' })}>
336
+ {filterOperatorNeedsValue(operator) ? (
337
+ <>
338
+ <Text className="text-xs text-content-secondary">Value</Text>
339
+ {isDateColumn(col) ? (
340
+ <FilterDateValue
341
+ value={item.value}
342
+ accessibilityLabel={`Value for ${rowLabel}`}
343
+ onChange={(date) =>
344
+ onChange(
345
+ date === undefined
346
+ ? { field: item.field, operator }
347
+ : { field: item.field, operator, value: date },
348
+ )
349
+ }
350
+ />
351
+ ) : col?.type === 'boolean' ? (
352
+ <Select
353
+ items={[...BOOLEAN_FILTER_VALUES]}
354
+ value={booleanSelectValue(item.value)}
355
+ onValueChange={(next) => onChange(booleanFilterItem(item.field, operator, next))}
356
+ size="small"
357
+ className={dataGridFilterPanelValueVariants()}
358
+ accessibilityLabel={`Value for ${rowLabel}`}
359
+ >
360
+ <Select.Trigger>
361
+ <Select.Value />
362
+ </Select.Trigger>
363
+ <Select.Content>
364
+ {BOOLEAN_FILTER_VALUES.map((candidate) => (
365
+ <Select.Item key={candidate.value} value={candidate.value}>
366
+ <Select.ItemLabel>{candidate.label}</Select.ItemLabel>
367
+ </Select.Item>
368
+ ))}
369
+ </Select.Content>
370
+ </Select>
371
+ ) : (
372
+ <Input size="small" className={dataGridFilterPanelValueVariants()}>
373
+ <Input.Field
374
+ value={valueText}
375
+ onChangeText={(text) =>
376
+ onChange(itemWithValueText(col, item.field, operator, text))
377
+ }
378
+ placeholder={filterPlaceholder(col, operator)}
379
+ accessibilityLabel={`Value for ${rowLabel}`}
380
+ />
381
+ </Input>
382
+ )}
383
+ </>
384
+ ) : null}
385
+ </View>
386
+ </View>
387
+ );
388
+ }
389
+
390
+ export interface DataGridFilterPanelProps<R extends GridValidRowModel> {
391
+ columns: readonly GridColDef<R>[];
392
+ filterModel: GridFilterModel;
393
+ onFilterModelChange: (model: GridFilterModel) => void;
394
+ logicOperator: GridLogicOperator;
395
+ onLogicOperatorChange: (operator: GridLogicOperator) => void;
396
+ onClose: () => void;
397
+ /** The control that toggles the panel; presses on it are not treated as outside presses. */
398
+ triggerRef?: RefObject<View | null>;
399
+ }
400
+
401
+ export function DataGridFilterPanel<R extends GridValidRowModel>({
402
+ columns,
403
+ filterModel,
404
+ onFilterModelChange,
405
+ logicOperator,
406
+ onLogicOperatorChange,
407
+ onClose,
408
+ triggerRef,
409
+ }: DataGridFilterPanelProps<R>) {
410
+ const panelRef = useRef<View>(null);
411
+ useDismissOverlay(true, panelRef, triggerRef ? [triggerRef] : [], onClose);
412
+ useEscapeKey(true, onClose);
413
+
414
+ const defaultItem = createDefaultFilterItem(columns);
415
+ const items = filterModel.length > 0 ? filterModel : defaultItem ? [defaultItem] : [];
416
+
417
+ const replaceItem = (index: number, next: GridFilterItem) => {
418
+ onFilterModelChange(items.map((item, itemIndex) => (itemIndex === index ? next : item)));
419
+ };
420
+ const removeItem = (index: number) => {
421
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
422
+ onFilterModelChange(next);
423
+ if (next.length === 0) {
424
+ onClose();
425
+ }
426
+ };
427
+
428
+ return (
429
+ <View
430
+ ref={panelRef}
431
+ className={dataGridFilterPanelVariants()}
432
+ accessibilityLabel="Filters"
433
+ testID="data-grid-filter-panel"
434
+ >
435
+ {items.map((item, index) => (
436
+ <FilterPanelRow
437
+ key={index}
438
+ index={index}
439
+ item={item}
440
+ columns={columns}
441
+ logicOperator={logicOperator}
442
+ onLogicOperatorChange={onLogicOperatorChange}
443
+ onChange={(next) => replaceItem(index, next)}
444
+ onRemove={() => removeItem(index)}
445
+ />
446
+ ))}
447
+ <View className={dataGridFilterPanelFooterVariants()}>
448
+ <Button
449
+ variant="ghost"
450
+ color="action"
451
+ size="small"
452
+ isDisabled={defaultItem === undefined}
453
+ onPress={() => {
454
+ if (defaultItem) {
455
+ onFilterModelChange([...items, defaultItem]);
456
+ }
457
+ }}
458
+ >
459
+ <Button.Icon as={Add} />
460
+ <Button.Label>Add filter</Button.Label>
461
+ </Button>
462
+ <Button
463
+ variant="ghost"
464
+ color="action"
465
+ size="small"
466
+ onPress={() => {
467
+ onFilterModelChange([]);
468
+ onClose();
469
+ }}
470
+ >
471
+ <Button.Icon as={Delete} />
472
+ <Button.Label>Remove all</Button.Label>
473
+ </Button>
474
+ </View>
475
+ </View>
476
+ );
477
+ }