@dxos/react-ui-table 0.8.4-main.c85a9c8dae → 0.8.4-main.d05539e30a

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 (119) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/{chunk-DSR2MRZB.mjs → chunk-4URYOFDL.mjs} +10 -21
  3. package/dist/lib/browser/chunk-4URYOFDL.mjs.map +7 -0
  4. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  5. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-PTHMYAAM.mjs → chunk-YDXPC7HA.mjs} +16 -72
  7. package/dist/lib/browser/chunk-YDXPC7HA.mjs.map +7 -0
  8. package/dist/lib/browser/index.mjs +207 -251
  9. package/dist/lib/browser/index.mjs.map +4 -4
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/browser/testing/index.mjs +8 -6
  12. package/dist/lib/browser/testing/index.mjs.map +3 -3
  13. package/dist/lib/browser/translations.mjs +36 -0
  14. package/dist/lib/browser/translations.mjs.map +7 -0
  15. package/dist/lib/browser/types/index.mjs +2 -1
  16. package/dist/lib/node-esm/{chunk-GI72MQSG.mjs → chunk-2AN3QQ3D.mjs} +16 -72
  17. package/dist/lib/node-esm/chunk-2AN3QQ3D.mjs.map +7 -0
  18. package/dist/lib/node-esm/{chunk-HTZJSULI.mjs → chunk-B2SZYAEC.mjs} +10 -21
  19. package/dist/lib/node-esm/chunk-B2SZYAEC.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  21. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  22. package/dist/lib/node-esm/index.mjs +207 -251
  23. package/dist/lib/node-esm/index.mjs.map +4 -4
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/testing/index.mjs +8 -6
  26. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  27. package/dist/lib/node-esm/translations.mjs +37 -0
  28. package/dist/lib/node-esm/translations.mjs.map +7 -0
  29. package/dist/lib/node-esm/types/index.mjs +2 -1
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/DynamicTable.d.ts +1 -2
  33. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +19 -21
  35. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/Relations.stories.d.ts +82 -82
  37. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  39. package/dist/types/src/components/Table/Table.d.ts +20 -21
  40. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  41. package/dist/types/src/components/Table/Table.stories.d.ts +84 -84
  42. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/TableContent.d.ts +18 -0
  44. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  45. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  46. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  47. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  48. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  49. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  50. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +22 -24
  51. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/index.d.ts +0 -1
  53. package/dist/types/src/components/index.d.ts.map +1 -1
  54. package/dist/types/src/components/testing.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  58. package/dist/types/src/index.d.ts +0 -2
  59. package/dist/types/src/index.d.ts.map +1 -1
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +1 -1
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +4 -5
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  67. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
  68. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  69. package/dist/types/src/translations.d.ts +19 -21
  70. package/dist/types/src/translations.d.ts.map +1 -1
  71. package/dist/types/src/types/Table.d.ts +1 -7
  72. package/dist/types/src/types/Table.d.ts.map +1 -1
  73. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  74. package/dist/types/src/util/query-ast.d.ts.map +1 -1
  75. package/dist/types/src/util/schema.d.ts.map +1 -1
  76. package/dist/types/src/util/signals.d.ts.map +1 -1
  77. package/dist/types/src/util/sort.d.ts.map +1 -1
  78. package/dist/types/src/util/table-buttons.d.ts +20 -20
  79. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  80. package/dist/types/src/util/table-controls.d.ts +4 -4
  81. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  82. package/dist/types/src/util/tag.d.ts.map +1 -1
  83. package/dist/types/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +51 -60
  85. package/src/components/Table/ColumnActionsMenu.tsx +7 -6
  86. package/src/components/Table/DynamicTable.stories.tsx +15 -15
  87. package/src/components/Table/DynamicTable.tsx +6 -15
  88. package/src/components/Table/Relations.stories.tsx +72 -32
  89. package/src/components/Table/RowActionsMenu.tsx +3 -2
  90. package/src/components/Table/Table.stories.tsx +28 -31
  91. package/src/components/Table/Table.tsx +37 -427
  92. package/src/components/Table/TableContent.tsx +425 -0
  93. package/src/components/{TableToolbar → Table}/TableToolbar.tsx +34 -41
  94. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -2
  95. package/src/components/TableCellEditor/FormCellEditor.tsx +3 -2
  96. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +10 -11
  97. package/src/components/TableCellEditor/TableCellEditor.tsx +10 -8
  98. package/src/components/index.ts +0 -1
  99. package/src/components/testing.ts +10 -10
  100. package/src/index.ts +0 -4
  101. package/src/model/table-model.test.ts +1 -2
  102. package/src/model/table-model.ts +10 -11
  103. package/src/model/table-presentation.test.ts +1 -2
  104. package/src/model/table-presentation.ts +3 -5
  105. package/src/playwright/smoke.spec.ts +3 -3
  106. package/src/testing/useTestTableModel.ts +5 -5
  107. package/src/translations.ts +19 -21
  108. package/src/types/Table.ts +4 -21
  109. package/src/util/dynamic-table.test.ts +3 -3
  110. package/src/util/dynamic-table.ts +5 -6
  111. package/dist/lib/browser/chunk-DSR2MRZB.mjs.map +0 -7
  112. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +0 -7
  113. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +0 -7
  114. package/dist/lib/node-esm/chunk-HTZJSULI.mjs.map +0 -7
  115. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  116. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  117. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  118. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  119. package/src/components/TableToolbar/index.ts +0 -5
@@ -6,25 +6,25 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback } from 'react';
8
8
 
9
- import { Annotation, type Database, Format, Obj, type QueryAST, Ref, Type } from '@dxos/echo';
10
- import { View } from '@dxos/echo';
9
+ import { Annotation, type Database, Format, Obj, type QueryAST, Ref, Type, View } from '@dxos/echo';
11
10
  import { type Mutable, PropertyMetaAnnotationId } from '@dxos/echo/internal';
12
11
  import { invariant } from '@dxos/invariant';
13
- import { faker } from '@dxos/random';
12
+ import { random } from '@dxos/random';
14
13
  import { PublicKey } from '@dxos/react-client';
15
14
  import { withClientProvider } from '@dxos/react-client/testing';
16
15
  import { Panel, ScrollArea } from '@dxos/react-ui';
16
+ import { ViewEditor } from '@dxos/react-ui-form';
17
+ import { translations as formTranslations } from '@dxos/react-ui-form/translations';
18
+ import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
17
19
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
18
- import { ViewEditor, translations as formTranslations } from '@dxos/react-ui-form';
19
- import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
20
20
  import { ViewModel, getSchemaFromPropertyDefinitions, getTypenameFromQuery } from '@dxos/schema';
21
21
  import { TestSchema, createObjectFactory } from '@dxos/schema/testing';
22
22
  import { withRegistry } from '@dxos/storybook-utils';
23
23
 
24
+ import { translations } from '#translations';
25
+
24
26
  import { useTestTableModel } from '../../testing';
25
- import { translations } from '../../translations';
26
27
  import { Table } from '../../types';
27
-
28
28
  import { Table as TableComponent } from './Table';
29
29
 
30
30
  const Example = Schema.Struct({
@@ -52,7 +52,7 @@ const Example = Schema.Struct({
52
52
  title: 'Parent',
53
53
  }),
54
54
  }).pipe(
55
- Type.object({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
55
+ Type.object({ typename: `com.example.type.${PublicKey.random().truncate()}`, version: '0.1.0' }),
56
56
  Annotation.LabelAnnotation.set(['name']),
57
57
  );
58
58
  interface Example extends Schema.Schema.Type<typeof Example> {}
@@ -74,8 +74,8 @@ const StoryViewEditor = ({
74
74
  invariant(Type.isMutable(schema));
75
75
  schema.updateTypename(getTypenameFromQuery(newQuery));
76
76
  invariant(view);
77
- Obj.change(view, (v) => {
78
- v.query.ast = newQuery as Mutable<typeof newQuery>;
77
+ Obj.update(view, (view) => {
78
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
79
79
  });
80
80
  },
81
81
  [schema, view],
@@ -119,7 +119,7 @@ const DefaultStory = () => {
119
119
 
120
120
  return (
121
121
  <div className='grow grid grid-cols-[1fr_350px]'>
122
- <TableComponent.Root>
122
+ <TableComponent.Root ref={tableRef}>
123
123
  <Panel.Root>
124
124
  <Panel.Toolbar asChild>
125
125
  <TableComponent.Toolbar
@@ -129,8 +129,7 @@ const DefaultStory = () => {
129
129
  />
130
130
  </Panel.Toolbar>
131
131
  <Panel.Content asChild>
132
- <TableComponent.Main
133
- ref={tableRef}
132
+ <TableComponent.Content
134
133
  schema={schema}
135
134
  model={model}
136
135
  presentation={presentation}
@@ -143,16 +142,14 @@ const DefaultStory = () => {
143
142
  <ScrollArea.Root orientation='vertical' classNames='border-l border-separator'>
144
143
  <ScrollArea.Viewport>
145
144
  <StoryViewEditor view={table.view.target} schema={schema} db={db} handleDeleteColumn={handleDeleteColumn} />
146
- <SyntaxHighlighter language='json' className='text-xs'>
147
- {JSON.stringify({ view: table.view.target, schema }, null, 2)}
148
- </SyntaxHighlighter>
145
+ <JsonHighlighter data={{ view: table.view.target, schema }} classNames='text-xs' />
149
146
  </ScrollArea.Viewport>
150
147
  </ScrollArea.Root>
151
148
  </div>
152
149
  );
153
150
  };
154
151
 
155
- type StoryProps = { rows?: number };
152
+ type DefaultStoryProps = { rows?: number };
156
153
 
157
154
  //
158
155
  // Story definitions.
@@ -175,10 +172,10 @@ const meta = {
175
172
  const [schema] = await space.db.schemaRegistry.register([Example]);
176
173
  const { view, jsonSchema } = await ViewModel.makeFromDatabase({ db: space.db, typename: schema.typename });
177
174
  const table = Table.make({ view, jsonSchema });
178
- Obj.change(view, (v) => {
179
- v.projection.fields = [
180
- v.projection.fields.find((field) => field.path === 'name')!,
181
- ...v.projection.fields.filter((field) => field.path !== 'name'),
175
+ Obj.update(view, (view) => {
176
+ view.projection.fields = [
177
+ view.projection.fields.find((field) => field.path === 'name')!,
178
+ ...view.projection.fields.filter((field) => field.path !== 'name'),
182
179
  ];
183
180
  });
184
181
 
@@ -187,9 +184,9 @@ const meta = {
187
184
  Array.from({ length: 10 }).map(() => {
188
185
  return space.db.add(
189
186
  Obj.make(schema, {
190
- name: faker.lorem.sentence(),
191
- status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
192
- description: faker.lorem.paragraph(),
187
+ name: random.lorem.sentence(),
188
+ status: random.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
189
+ description: random.lorem.paragraph(),
193
190
  }),
194
191
  );
195
192
  });
@@ -226,7 +223,7 @@ export const StaticSchema: StoryObj = {
226
223
  const table = Table.make({ view, jsonSchema });
227
224
  space.db.add(table);
228
225
 
229
- const factory = createObjectFactory(space.db, faker as any);
226
+ const factory = createObjectFactory(space.db, random as any);
230
227
  await factory([
231
228
  { type: TestSchema.Person, count: 10 },
232
229
  // { type: TestSchema.Organization, count: 1 },
@@ -252,7 +249,7 @@ const ContactWithArrayOfEmails = Schema.Struct({
252
249
  ),
253
250
  }).pipe(
254
251
  Type.object({
255
- typename: 'dxos.org/type/ContactWithArrayOfEmails',
252
+ typename: 'org.dxos.type.contactWithArrayOfEmails',
256
253
  version: '0.1.0',
257
254
  }),
258
255
  );
@@ -272,7 +269,7 @@ export const ArrayOfObjects: StoryObj = {
272
269
  const table = Table.make({ view, jsonSchema });
273
270
  space.db.add(table);
274
271
 
275
- const factory = createObjectFactory(space.db, faker as any);
272
+ const factory = createObjectFactory(space.db, random as any);
276
273
  await factory([
277
274
  // { type: TestSchema.Person, count: 10 },
278
275
  // { type: TestSchema.Organization, count: 1 },
@@ -287,7 +284,7 @@ export const ArrayOfObjects: StoryObj = {
287
284
  },
288
285
  };
289
286
 
290
- export const Tags: Meta<StoryProps> = {
287
+ export const Tags: Meta<DefaultStoryProps> = {
291
288
  title: 'ui/react-ui-table/Table',
292
289
  render: DefaultStory,
293
290
  decorators: [
@@ -297,7 +294,7 @@ export const Tags: Meta<StoryProps> = {
297
294
  createSpace: true,
298
295
  onCreateSpace: async ({ space }) => {
299
296
  // Configure schema.
300
- const typename = 'example.com/SingleSelect';
297
+ const typename = 'com.example.type.single-select';
301
298
  const selectOptions = [
302
299
  { id: 'one', title: 'One', color: 'emerald' },
303
300
  { id: 'two', title: 'Two', color: 'blue' },
@@ -331,8 +328,8 @@ export const Tags: Meta<StoryProps> = {
331
328
  Array.from({ length: 10 }).map(() => {
332
329
  return space.db.add(
333
330
  Obj.make(storedSchema, {
334
- single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
335
- multiple: faker.helpers.randomSubset(selectOptionIds),
331
+ single: random.helpers.arrayElement([...selectOptionIds, undefined]),
332
+ multiple: random.helpers.randomSubset(selectOptionIds),
336
333
  }),
337
334
  );
338
335
  });
@@ -2,458 +2,64 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
- import * as String from 'effect/String';
7
- import React, {
8
- type JSX,
9
- type MouseEvent,
10
- type PropsWithChildren,
11
- type Ref,
12
- type WheelEvent,
13
- forwardRef,
14
- useCallback,
15
- useContext,
16
- useEffect,
17
- useImperativeHandle,
18
- useMemo,
19
- useState,
20
- } from 'react';
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import { createContext } from '@radix-ui/react-context';
7
+ import React, { type PropsWithChildren, type RefObject, forwardRef, useImperativeHandle, useRef } from 'react';
21
8
 
22
- import { type Type } from '@dxos/echo';
23
- import { log } from '@dxos/log';
24
- import { useAttention } from '@dxos/react-ui-attention';
25
- import {
26
- type DxGridAxisMeta,
27
- type DxGridElement,
28
- type DxGridPlane,
29
- type DxGridPlaneRange,
30
- type DxGridPosition,
31
- Grid,
32
- type GridContentProps,
33
- closestCell,
34
- gridSeparatorBlockEnd,
35
- gridSeparatorInlineEnd,
36
- } from '@dxos/react-ui-grid';
37
- import { DxEditRequest } from '@dxos/react-ui-grid';
38
- import { mx, useComposableProps } from '@dxos/ui-theme';
39
- import { type ComposableProps } from '@dxos/ui-types';
9
+ import { type DxGridAxisMeta, type DxGridPosition } from '@dxos/react-ui-grid';
40
10
 
41
- import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
42
- import { tableButtons, tableControls } from '../../util';
43
- import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
44
- import { TableToolbar } from '../TableToolbar';
45
-
46
- import { ColumnActionsMenu } from './ColumnActionsMenu';
47
- import { ColumnSettings } from './ColumnSettings';
48
- import { RowActionsMenu } from './RowActionsMenu';
11
+ import { type InsertRowResult } from '../../model';
12
+ import { TableContent } from './TableContent';
13
+ import { TableToolbar } from './TableToolbar';
49
14
 
50
15
  const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
51
16
  const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
52
17
  const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
53
18
 
54
19
  //
55
- // Table.Root
20
+ // Context
56
21
  //
57
22
 
58
- export type TableRootProps = PropsWithChildren;
59
-
60
- const TableRoot = ({ children }: TableRootProps) => {
61
- return <>{children}</>;
23
+ type TableContextValue = {
24
+ /** Mutable ref populated by Content so Root can expose the controller. */
25
+ controllerRef: RefObject<TableController>;
62
26
  };
63
27
 
28
+ const [TableContextProvider, useTableContext] = createContext<TableContextValue>('Table');
29
+
64
30
  //
65
- // Table.Main
31
+ // Controller
66
32
  //
67
33
 
68
- const emptyRowsAtom = Atom.make<unknown[]>([]);
69
- const emptyCellUpdateAtom = Atom.make<number>(0);
70
-
71
- export type TableController = {
34
+ type TableController = {
72
35
  update?: (cell?: DxGridPosition) => void;
73
36
  handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
74
37
  };
75
38
 
76
- export type TableMainProps<T extends Type.AnyEntity = Type.AnyEntity> = ComposableProps<{
77
- schema?: T;
78
- model?: TableModel;
79
- presentation?: TablePresentation;
80
- // TODO(burdon): Factor out attention.
81
- ignoreAttention?: boolean;
82
- onCreate?: OnCreateHandler;
83
- onRowClick?: (row: any) => void;
84
- testId?: string;
85
- }>;
86
-
87
- const TableMainInner = <T extends Type.AnyEntity = Type.AnyEntity>(
88
- { schema, model, presentation, ignoreAttention, onCreate, onRowClick, testId, ...props }: TableMainProps<T>,
89
- forwardedRef: Ref<TableController>,
90
- ) => {
91
- const registry = useContext(RegistryContext);
92
- const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
93
- const { hasAttention } = useAttention(model?.id ?? 'table');
94
- const modals = useMemo(() => new ModalController(registry), [registry]);
95
- const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
96
- // Subscribe to rows atom to trigger re-render when rows change.
97
- const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
98
- // Subscribe to cell update counter to trigger re-render when individual cells change.
99
- const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
100
- // Derive column count from columnMeta (reactive) with fallback.
101
- const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
102
-
103
- const draftRowCount = model?.getDraftRowCount() ?? 0;
104
-
105
- const frozen = useMemo(() => {
106
- const noActionColumn =
107
- model?.features.dataEditable === false &&
108
- model?.features.schemaEditable === false &&
109
- model.rowActions.length === 0;
110
-
111
- return {
112
- frozenRowsStart: 1,
113
- frozenRowsEnd: Math.max(1, draftRowCount),
114
- frozenColsStart: model?.features.selection.enabled ? 1 : 0,
115
- frozenColsEnd: noActionColumn ? 0 : 1,
116
- };
117
- }, [model, draftRowCount]);
118
-
119
- const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
120
- (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
121
- [presentation],
122
- );
123
-
124
- useEffect(() => {
125
- if (!dxGrid || !presentation) {
126
- return;
127
- }
128
-
129
- dxGrid.getCells = getCells;
130
- }, [dxGrid, presentation, getCells]);
131
-
132
- // Trigger grid update when rows or cell values change.
133
- useEffect(() => {
134
- if (!dxGrid) {
135
- return;
136
- }
137
-
138
- dxGrid.updateCells(true);
139
- dxGrid.requestUpdate();
140
- }, [dxGrid, rows, cellUpdateCounter]);
141
-
142
- const handleInsertRowResult = useCallback(
143
- (insertResult?: InsertRowResult) => {
144
- if (insertResult === 'draft') {
145
- requestAnimationFrame(() => {
146
- dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
147
- dxGrid?.refocus();
148
- });
149
- } else {
150
- requestAnimationFrame(() => {
151
- dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
152
- dxGrid?.refocus();
153
- });
154
- }
155
- },
156
- [model, dxGrid],
157
- );
158
-
159
- const handleSave = useCallback(() => {
160
- dxGrid?.updateCells(true);
161
- dxGrid?.requestUpdate();
162
- }, [dxGrid]);
163
-
164
- const handleSaveDraftRow = useCallback(
165
- (rowIndex = 0) => {
166
- if (model && dxGrid) {
167
- const didCommitSuccessfully = model.commitDraftRow(rowIndex);
168
- if (didCommitSuccessfully) {
169
- requestAnimationFrame(() => {
170
- dxGrid.scrollToEndRow();
171
- });
172
- }
173
- }
174
- },
175
- [model, dxGrid],
176
- );
177
-
178
- const handleGridClick = useCallback(
179
- (event: MouseEvent) => {
180
- const cell = closestCell(event.target as HTMLElement);
181
- if (cell) {
182
- const { row: rowIndex, plane } = cell;
183
- if (onRowClick) {
184
- if (plane === 'grid') {
185
- const row = model?.getRowAt(rowIndex);
186
- row && onRowClick(row);
187
- } else {
188
- onRowClick(cell);
189
- }
190
- }
191
-
192
- if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
193
- model.selection.toggleSelectionForRowIndex(rowIndex);
194
- }
195
- }
196
-
197
- if (!modals) {
198
- return;
199
- }
200
-
201
- const buttonSelector = Object.values(tableButtons)
202
- .map((button) => `button[${button.attr}]`)
203
- .join(',');
204
-
205
- const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
206
- if (tableButtonElement) {
207
- const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
208
- if (!button) {
209
- return;
210
- }
211
-
212
- modals.setTrigger(tableButtonElement);
213
- const data = button.getData(tableButtonElement);
214
- switch (data.type) {
215
- case 'rowMenu': {
216
- modals.showRowMenu(data.rowIndex);
217
- break;
218
- }
219
- case 'columnSettings': {
220
- modals.showColumnMenu(data.fieldId);
221
- break;
222
- }
223
- case 'newColumn': {
224
- modals.showColumnCreator();
225
- break;
226
- }
227
- case 'sort': {
228
- model?.toggleSort(data.fieldId);
229
- break;
230
- }
231
- case 'saveDraftRow': {
232
- handleSaveDraftRow();
233
- break;
234
- }
235
- }
236
- return;
237
- }
238
-
239
- const controlSelector = Object.values(tableControls)
240
- .map((control) => `input[${control.attr}]`)
241
- .join(',');
242
-
243
- const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
244
- if (controlElement) {
245
- const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
246
- if (!control) {
247
- return;
248
- }
249
-
250
- const data = control.getData(controlElement);
251
- switch (data.type) {
252
- case 'checkbox': {
253
- if (data.header) {
254
- model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
255
- } else {
256
- model?.selection.toggleSelectionForRowIndex(data.rowIndex);
257
- }
258
- break;
259
- }
260
- case 'switch': {
261
- if (model) {
262
- model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
263
- }
264
- break;
265
- }
266
- }
267
- }
268
- },
269
- [model, modals, dxGrid],
270
- );
271
-
272
- const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
273
- (increment, delta, cell) => {
274
- if (dxGrid && model) {
275
- if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
276
- handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
277
- } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
278
- handleSaveDraftRow(cell.row);
279
- handleInsertRowResult(model.insertRow());
280
- } else {
281
- dxGrid.refocus(increment, delta);
282
- }
283
- }
284
- },
285
- [dxGrid, model],
286
- );
287
-
288
- const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
289
- (event) => {
290
- if (!model?.features.dataEditable) {
291
- return;
292
- }
293
-
294
- const cell = closestCell(event.target);
295
- if (!model || !cell) {
296
- return;
297
- }
39
+ //
40
+ // Root
41
+ //
298
42
 
299
- // Handle Meta+C (Copy) and Meta+V (Paste) commands
300
- if (event.metaKey || event.ctrlKey) {
301
- switch (event.key) {
302
- case 'c': {
303
- // Copy focused cell's text content to clipboard
304
- try {
305
- const cellData = model.getCellData(cell);
306
- const textContent = cellData?.toString() ?? '';
307
- void navigator.clipboard.writeText(textContent);
308
- event.preventDefault();
309
- } catch (error) {
310
- log.catch(error);
311
- }
312
- break;
313
- }
314
- case 'v': {
315
- // Paste clipboard content to focused cell
316
- event.preventDefault();
317
- void navigator.clipboard.readText().then((clipboardText) => {
318
- try {
319
- // Attempt to set the cell's content to clipboard content
320
- model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
321
- handleSave();
322
- } catch {
323
- // If validation fails, emit a DxEditRequest event with initialContent from clipboard
324
- // TODO(thure): Should `dx-grid` expose a method like this?
325
- const cellElement = (event.target as HTMLElement).closest(
326
- '[data-dx-grid-action="cell"]',
327
- ) as HTMLElement;
328
- if (cellElement) {
329
- const rect = cellElement.getBoundingClientRect();
330
- const editRequest = new DxEditRequest({
331
- cellIndex: `${cell.plane},${cell.col},${cell.row}`,
332
- cellBox: {
333
- insetInlineStart: rect.left,
334
- insetBlockStart: rect.top,
335
- inlineSize: rect.width,
336
- blockSize: rect.height,
337
- },
338
- cellElement,
339
- initialContent: clipboardText,
340
- });
341
- cellElement.dispatchEvent(editRequest);
342
- }
343
- }
344
- });
345
- break;
346
- }
347
- }
348
- }
43
+ const TABLE_ROOT_NAME = 'Table.Root';
349
44
 
350
- switch (event.key) {
351
- case 'Backspace':
352
- case 'Delete': {
353
- try {
354
- model.setCellData(cell, undefined);
355
- event.preventDefault();
356
- handleSave();
357
- } catch {
358
- // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
359
- }
360
- break;
361
- }
362
- }
363
- },
364
- [model, dxGrid, handleSave],
365
- );
45
+ type TableRootProps = PropsWithChildren;
366
46
 
367
- const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
368
- (event) => {
369
- if (event.axis === 'col') {
370
- const columnIndex = parseInt(event.index, 10);
371
- model?.setColumnWidth(columnIndex, event.size);
372
- }
373
- },
374
- [model],
375
- );
47
+ const TableRoot = forwardRef<TableController, TableRootProps>(({ children }, forwardedRef) => {
48
+ const controllerRef = useRef<TableController>({});
376
49
 
377
- const handleWheel = useCallback(
378
- (event: WheelEvent) => {
379
- if (!ignoreAttention && !hasAttention) {
380
- event.stopPropagation();
381
- }
382
- },
383
- [hasAttention, ignoreAttention],
50
+ useImperativeHandle(
51
+ forwardedRef,
52
+ () => ({
53
+ update: (cell) => controllerRef.current?.update?.(cell),
54
+ handleInsertRowResult: (result) => controllerRef.current?.handleInsertRowResult?.(result),
55
+ }),
56
+ [],
384
57
  );
385
58
 
386
- const handleNewColumn = useCallback(() => {
387
- const columns = model?.getColumnCount();
388
- if (dxGrid && columns) {
389
- dxGrid.scrollToColumn(columns - 1);
390
- }
391
- }, [model, dxGrid]);
59
+ return <TableContextProvider controllerRef={controllerRef}>{children}</TableContextProvider>;
60
+ });
392
61
 
393
- /**
394
- * Provides an external controller that can be called to repaint the table.
395
- */
396
- useImperativeHandle<TableController, TableController>(forwardedRef, () => {
397
- if (!dxGrid || !presentation) {
398
- return {};
399
- }
400
-
401
- return {
402
- update: (cell) => {
403
- if (cell) {
404
- dxGrid.updateIfWithinBounds(cell, true);
405
- } else {
406
- dxGrid.updateCells(true);
407
- dxGrid.requestUpdate();
408
- }
409
- },
410
- handleInsertRowResult,
411
- };
412
- }, [dxGrid, model, presentation]);
413
-
414
- if (!model || !modals) {
415
- return <span role='none' className='dx-attention-surface' />;
416
- }
417
-
418
- const { className, ...rest } = useComposableProps(props);
419
- return (
420
- <div {...rest} role='none' className={mx('dx-container relative', className)}>
421
- <Grid.Root id={model.id ?? 'table-grid'}>
422
- <TableValueEditor<T>
423
- model={model}
424
- modals={modals}
425
- schema={schema}
426
- onFocus={handleFocus}
427
- onSave={handleSave}
428
- onCreate={onCreate}
429
- />
430
- <Grid.Content
431
- className={mx('[--dx-grid-base:var(--base-surface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
432
- frozen={frozen}
433
- columns={columnMeta}
434
- columnDefault={columnDefault}
435
- rowDefault={rowDefault}
436
- limitRows={rows.length}
437
- limitColumns={columnCount}
438
- overscroll='trap'
439
- onAxisResize={handleAxisResize}
440
- onClick={handleGridClick}
441
- onKeyDownCapture={handleKeyDown}
442
- onWheelCapture={handleWheel}
443
- {...(testId && { 'data-testid': testId })}
444
- ref={setDxGrid}
445
- />
446
- <RowActionsMenu model={model} modals={modals} />
447
- <ColumnActionsMenu model={model} modals={modals} />
448
- <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
449
- </Grid.Root>
450
- </div>
451
- );
452
- };
453
-
454
- const TableMain = forwardRef(TableMainInner) as <T extends Type.AnyEntity = Type.AnyEntity>(
455
- props: TableMainProps<T> & { ref?: Ref<TableController> },
456
- ) => JSX.Element;
62
+ TableRoot.displayName = TABLE_ROOT_NAME;
457
63
 
458
64
  //
459
65
  // Table
@@ -461,6 +67,10 @@ const TableMain = forwardRef(TableMainInner) as <T extends Type.AnyEntity = Type
461
67
 
462
68
  export const Table = {
463
69
  Root: TableRoot,
464
- Main: TableMain,
465
70
  Toolbar: TableToolbar,
71
+ Content: TableContent,
466
72
  };
73
+
74
+ export type { TableController, TableRootProps };
75
+
76
+ export { useTableContext };