@dxos/react-ui-grid 0.8.3 → 0.8.4-main.1068cf700f

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/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-grid",
3
- "version": "0.8.3",
3
+ "version": "0.8.4-main.1068cf700f",
4
4
  "description": "React component which manages a `dx-grid` Lit web component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
- "sideEffects": true,
13
+ "sideEffects": false,
10
14
  "type": "module",
11
15
  "exports": {
12
16
  ".": {
17
+ "source": "./src/index.ts",
13
18
  "types": "./dist/types/src/index.d.ts",
14
19
  "browser": "./dist/lib/browser/index.mjs"
15
20
  }
@@ -23,35 +28,35 @@
23
28
  "src"
24
29
  ],
25
30
  "dependencies": {
26
- "@codemirror/autocomplete": "^6.18.1",
27
- "@codemirror/state": "^6.4.1",
28
- "@codemirror/view": "^6.34.1",
29
- "@lit/react": "^1.0.5",
30
- "@preact-signals/safe-react": "^0.9.0",
31
+ "@codemirror/autocomplete": "^6.19.0",
32
+ "@codemirror/state": "^6.5.2",
33
+ "@codemirror/view": "^6.38.4",
34
+ "@lit/react": "^1.0.8",
31
35
  "@radix-ui/react-context": "1.1.1",
32
36
  "@radix-ui/react-popper": "1.2.2",
33
37
  "@radix-ui/react-use-controllable-state": "1.1.0",
34
- "@dxos/lit-grid": "0.8.3",
35
- "@dxos/util": "0.8.3",
36
- "@dxos/react-ui-editor": "0.8.3"
38
+ "@dxos/lit-grid": "0.8.4-main.1068cf700f",
39
+ "@dxos/react-ui-editor": "0.8.4-main.1068cf700f",
40
+ "@dxos/ui-editor": "0.8.4-main.1068cf700f",
41
+ "@dxos/util": "0.8.4-main.1068cf700f"
37
42
  },
38
43
  "devDependencies": {
39
- "@types/react": "~18.2.0",
40
- "@types/react-dom": "~18.2.0",
41
- "react": "~18.2.0",
42
- "react-dom": "~18.2.0",
43
- "vite": "5.4.7",
44
- "@dxos/random": "0.8.3",
45
- "@dxos/react-ui": "0.8.3",
46
- "@dxos/react-ui-searchlist": "0.8.3",
47
- "@dxos/react-ui-theme": "0.8.3",
48
- "@dxos/storybook-utils": "0.8.3"
44
+ "@types/react": "~19.2.7",
45
+ "@types/react-dom": "~19.2.3",
46
+ "react": "~19.2.3",
47
+ "react-dom": "~19.2.3",
48
+ "vite": "7.1.9",
49
+ "@dxos/random": "0.8.4-main.1068cf700f",
50
+ "@dxos/react-ui": "0.8.4-main.1068cf700f",
51
+ "@dxos/react-ui-searchlist": "0.8.4-main.1068cf700f",
52
+ "@dxos/storybook-utils": "0.8.4-main.1068cf700f",
53
+ "@dxos/ui-theme": "0.8.4-main.1068cf700f"
49
54
  },
50
55
  "peerDependencies": {
51
- "react": "~18.2.0",
52
- "react-dom": "~18.2.0",
53
- "@dxos/react-ui": "0.8.3",
54
- "@dxos/react-ui-theme": "0.8.3"
56
+ "react": "~19.2.3",
57
+ "react-dom": "~19.2.3",
58
+ "@dxos/react-ui": "0.8.4-main.1068cf700f",
59
+ "@dxos/ui-theme": "0.8.4-main.1068cf700f"
55
60
  },
56
61
  "publishConfig": {
57
62
  "access": "public"
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React, { useState } from 'react';
7
+
8
+ import { withTheme } from '@dxos/react-ui/testing';
9
+
10
+ import { CellEditor, type CellEditorProps, type EditorKeyEvent, editorKeys } from './CellEditor';
11
+
12
+ const DefaultStory = (props: CellEditorProps) => {
13
+ const [value, setValue] = useState(props.value || 'Edit me');
14
+ const [lastAction, setLastAction] = useState<string>('');
15
+
16
+ const handleBlur = (newValue?: string) => {
17
+ if (newValue !== undefined) {
18
+ setValue(newValue);
19
+ setLastAction(`Blur: ${newValue}`);
20
+ }
21
+ };
22
+
23
+ const handleKeyEvent = (newValue: string | undefined, event: EditorKeyEvent) => {
24
+ if (newValue !== undefined) {
25
+ setValue(newValue);
26
+ setLastAction(`Key: ${event.key}${event.shift ? ' + Shift' : ''}, Value: ${newValue}`);
27
+ } else {
28
+ setLastAction(`Key: ${event.key}${event.shift ? ' + Shift' : ''}, Cancelled`);
29
+ }
30
+ };
31
+
32
+ // Create an extension with editor keys
33
+ const extensions = props.extensions || [
34
+ editorKeys({
35
+ onClose: handleKeyEvent,
36
+ onNav: (value, event) => {
37
+ setLastAction(`Navigation: ${event.key}, Value: ${value}`);
38
+ },
39
+ }),
40
+ ];
41
+
42
+ return (
43
+ <div className='flex flex-col gap-4 p-4'>
44
+ <div className='text-sm'>
45
+ Current value: <span className='font-mono'>{value}</span>
46
+ </div>
47
+ <div className='text-sm'>
48
+ Last action: <span className='font-mono'>{lastAction}</span>
49
+ </div>
50
+ <div className='relative border border-separator bs-[100px] is-[300px]'>
51
+ <CellEditor
52
+ value={value}
53
+ extensions={extensions}
54
+ autoFocus={props.autoFocus}
55
+ onBlur={handleBlur}
56
+ box={{
57
+ insetInlineStart: 10,
58
+ insetBlockStart: 10,
59
+ inlineSize: 280,
60
+ blockSize: 30,
61
+ }}
62
+ gridId='demo-grid'
63
+ />
64
+ </div>
65
+ </div>
66
+ );
67
+ };
68
+
69
+ const meta = {
70
+ title: 'ui/react-ui-grid/CellEditor',
71
+ component: CellEditor,
72
+ render: DefaultStory,
73
+ decorators: [withTheme()],
74
+ parameters: {
75
+ layout: 'centered',
76
+ },
77
+ } satisfies Meta<typeof CellEditor>;
78
+
79
+ export default meta;
80
+
81
+ type Story = StoryObj<typeof meta>;
82
+
83
+ export const Default: Story = {
84
+ args: {
85
+ value: 'Edit me',
86
+ autoFocus: true,
87
+ },
88
+ };
@@ -8,13 +8,14 @@ import { EditorView, keymap } from '@codemirror/view';
8
8
  import React, { type KeyboardEvent } from 'react';
9
9
 
10
10
  import { useThemeContext } from '@dxos/react-ui';
11
+ import { type UseTextEditorProps, useTextEditor } from '@dxos/react-ui-editor';
11
12
  import {
12
- type UseTextEditorProps,
13
+ type ThemeExtensionsOptions,
13
14
  createBasicExtensions,
14
15
  createThemeExtensions,
15
- preventNewline,
16
- useTextEditor,
17
- } from '@dxos/react-ui-editor';
16
+ filterChars,
17
+ } from '@dxos/ui-editor';
18
+ import { mx } from '@dxos/ui-theme';
18
19
 
19
20
  import { type GridEditBox } from '../Grid';
20
21
 
@@ -114,12 +115,14 @@ export const editorKeys = ({ onNav, onClose }: EditorKeysProps): Extension => {
114
115
 
115
116
  export type CellEditorProps = {
116
117
  value?: string;
117
- extension?: Extension;
118
+ extensions?: Extension;
118
119
  box?: GridEditBox;
119
120
  gridId?: string;
120
- } & Pick<UseTextEditorProps, 'autoFocus'> & { onBlur?: EditorBlurHandler };
121
+ onBlur?: EditorBlurHandler;
122
+ } & Pick<UseTextEditorProps, 'autoFocus'> &
123
+ Pick<ThemeExtensionsOptions, 'slots'>;
121
124
 
122
- export const CellEditor = ({ value, extension, autoFocus, onBlur, box, gridId }: CellEditorProps) => {
125
+ export const CellEditor = ({ value, extensions, box, gridId, autoFocus, slots, onBlur }: CellEditorProps) => {
123
126
  const { themeMode } = useThemeContext();
124
127
  const { parentRef } = useTextEditor(() => {
125
128
  return {
@@ -127,38 +130,49 @@ export const CellEditor = ({ value, extension, autoFocus, onBlur, box, gridId }:
127
130
  initialValue: value,
128
131
  selection: { anchor: value?.length ?? 0 },
129
132
  extensions: [
130
- extension ?? [],
131
- preventNewline,
133
+ extensions ?? [],
134
+ filterChars(/[\n\r]+/),
132
135
  EditorView.focusChangeEffect.of((state, focusing) => {
133
136
  if (!focusing) {
134
137
  onBlur?.(state.doc.toString());
135
138
  }
136
139
  return null;
137
140
  }),
138
- createBasicExtensions({ lineWrapping: false }),
141
+ createBasicExtensions({ lineWrapping: true }),
139
142
  createThemeExtensions({
140
143
  themeMode,
141
144
  slots: {
142
145
  editor: {
143
- className: '[&>.cm-scroller]:scrollbar-none tabular-nums',
146
+ className: mx(
147
+ '!min-is-full !is-min !max-is-[--dx-grid-cell-editor-max-inline-size] !min-bs-full !max-bs-[--dx-grid-cell-editor-max-block-size]',
148
+ slots?.editor?.className,
149
+ ),
150
+ },
151
+ scroll: {
152
+ className: mx(
153
+ '!overflow-x-hidden !plb-[max(0,calc(var(--dx-grid-cell-editor-padding-block)-1px))] !pie-0 !pis-[--dx-grid-cell-editor-padding-inline]',
154
+ slots?.scroll?.className,
155
+ ),
144
156
  },
145
157
  content: {
146
- className:
147
- '!border !border-transparent !pli-[var(--dx-grid-cell-padding-inline)] !plb-[var(--dx-grid-cell-padding-block)]',
158
+ className: mx('!break-normal', slots?.content?.className),
148
159
  },
149
160
  },
150
161
  }),
151
162
  ],
152
163
  };
153
- }, [extension, autoFocus, value, onBlur]);
164
+ }, [extensions, autoFocus, value, onBlur, themeMode, slots]);
154
165
 
155
166
  return (
156
167
  <div
157
168
  data-testid='grid.cell-editor'
158
169
  ref={parentRef}
159
- className='absolute z-[1]'
170
+ className='absolute z-[1] dx-grid__cell-editor'
160
171
  style={{
161
- ...box,
172
+ insetInlineStart: box?.insetInlineStart ?? '0px',
173
+ insetBlockStart: box?.insetBlockStart ?? '0px',
174
+ minInlineSize: box?.inlineSize ?? '180px',
175
+ minBlockSize: box?.blockSize ?? '30px',
162
176
  ...{ '--dx-gridCellWidth': `${box?.inlineSize ?? 200}px` },
163
177
  }}
164
178
  {...(gridId && { 'data-grid': gridId })}
@@ -4,15 +4,18 @@
4
4
 
5
5
  import React, { useCallback } from 'react';
6
6
 
7
+ import { type DxGridCellIndex, type GridScopedProps, useGridContext } from '../Grid';
8
+
7
9
  import { CellEditor, type CellEditorProps } from './CellEditor';
8
- import { type GridScopedProps, useGridContext, type DxGridCellIndex } from '../Grid';
9
10
 
10
11
  export type GridCellEditorProps = GridScopedProps<
11
- Pick<CellEditorProps, 'extension' | 'onBlur'> & { getCellContent: (index: DxGridCellIndex) => string | undefined }
12
+ Pick<CellEditorProps, 'extensions' | 'onBlur' | 'slots'> & {
13
+ getCellContent: (index: DxGridCellIndex) => string | undefined;
14
+ }
12
15
  >;
13
16
 
14
- export const GridCellEditor = ({ extension, getCellContent, onBlur, __gridScope }: GridCellEditorProps) => {
15
- const { id, editing, setEditing, editBox } = useGridContext('GridSheetCellEditor', __gridScope);
17
+ export const GridCellEditor = ({ extensions, getCellContent, onBlur, slots, __gridScope }: GridCellEditorProps) => {
18
+ const { id, editing, setEditing, editBox } = useGridContext('GridCellEditor', __gridScope);
16
19
 
17
20
  const handleBlur = useCallback(
18
21
  (value?: string) => {
@@ -28,8 +31,9 @@ export const GridCellEditor = ({ extension, getCellContent, onBlur, __gridScope
28
31
  autoFocus
29
32
  box={editBox}
30
33
  onBlur={handleBlur}
31
- extension={extension}
34
+ extensions={extensions}
32
35
  gridId={id}
36
+ slots={slots}
33
37
  />
34
38
  ) : null;
35
39
  };
@@ -2,18 +2,18 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type Meta, type StoryObj } from '@storybook/react';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import React, { type MouseEvent, type MutableRefObject, useCallback, useRef, useState } from 'react';
9
7
 
10
8
  import { defaultRowSize } from '@dxos/lit-grid';
9
+ import { type DxGridPlaneCells } from '@dxos/lit-grid';
11
10
  import { faker } from '@dxos/random';
12
11
  import { DropdownMenu } from '@dxos/react-ui';
13
- import { PopoverCombobox, type PopoverComboboxRootProps } from '@dxos/react-ui-searchlist';
14
- import { withTheme } from '@dxos/storybook-utils';
12
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
13
+ import { toPlaneCellIndex } from '@dxos/react-ui-grid';
14
+ import { Combobox, type ComboboxRootProps, useSearchListResults } from '@dxos/react-ui-searchlist';
15
15
 
16
- import { Grid, type GridEditing, type GridContentProps, type GridRootProps } from './Grid';
16
+ import { Grid, type GridContentProps, type GridEditing, type GridRootProps } from './Grid';
17
17
 
18
18
  const storybookItems = faker.helpers.uniqueArray(faker.commerce.productName, 16);
19
19
 
@@ -33,7 +33,7 @@ const GridStory = ({ initialCells, ...props }: GridStoryProps) => {
33
33
  // Multiselect
34
34
  const [popoverOpen, setPopoverOpen] = useState(false);
35
35
  const [multiSelectValue, setInternalMultiselectValue] = useState('');
36
- const setMultiselectValue = useCallback<NonNullable<PopoverComboboxRootProps['onValueChange']>>((nextValue) => {
36
+ const setMultiselectValue = useCallback<NonNullable<ComboboxRootProps['onValueChange']>>((nextValue) => {
37
37
  setInternalMultiselectValue(nextValue);
38
38
  setCells((cells) => {
39
39
  // TODO(burdon): How can we get the cell address to update?
@@ -64,7 +64,7 @@ const GridStory = ({ initialCells, ...props }: GridStoryProps) => {
64
64
  }, []);
65
65
 
66
66
  return (
67
- <div role='none' className='fixed inset-0 grid'>
67
+ <div role='none' className='contents'>
68
68
  <Grid.Root id='story' editing={editing} onEditingChange={handleEditingChange}>
69
69
  {/* TODO(burdon): Why is this property not just "cells" or "values" */}
70
70
  <Grid.Content {...props} initialCells={cells} onClick={handleClick} />
@@ -80,38 +80,52 @@ const GridStory = ({ initialCells, ...props }: GridStoryProps) => {
80
80
  </DropdownMenu.Root>
81
81
 
82
82
  {/* Multiselect */}
83
- <PopoverCombobox.Root
83
+ <Combobox.Root
84
84
  open={popoverOpen}
85
85
  onOpenChange={setPopoverOpen}
86
86
  value={multiSelectValue}
87
87
  onValueChange={setMultiselectValue}
88
88
  >
89
- <PopoverCombobox.VirtualTrigger virtualRef={triggerRef} />
90
- <PopoverCombobox.Content filter={(value, search) => (value.includes(search) ? 1 : 0)}>
91
- <PopoverCombobox.Input placeholder='Search...' />
92
- <PopoverCombobox.List>
93
- {storybookItems.map((value) => (
94
- <PopoverCombobox.Item key={value}>{value}</PopoverCombobox.Item>
95
- ))}
96
- </PopoverCombobox.List>
97
- <PopoverCombobox.Arrow />
98
- </PopoverCombobox.Content>
99
- </PopoverCombobox.Root>
89
+ <Combobox.VirtualTrigger virtualRef={triggerRef} />
90
+ <ComboboxContentWithFiltering />
91
+ </Combobox.Root>
100
92
  </div>
101
93
  );
102
94
  };
103
95
 
104
- const meta: Meta<GridStoryProps> = {
96
+ const ComboboxContentWithFiltering = () => {
97
+ const { results, handleSearch } = useSearchListResults({
98
+ items: storybookItems,
99
+ });
100
+
101
+ return (
102
+ <Combobox.Content onSearch={handleSearch}>
103
+ <Combobox.Input placeholder='Search...' />
104
+ <Combobox.List>
105
+ {results.map((value) => (
106
+ <Combobox.Item key={value} value={value} label={value} />
107
+ ))}
108
+ </Combobox.List>
109
+ <Combobox.Arrow />
110
+ </Combobox.Content>
111
+ );
112
+ };
113
+
114
+ const meta = {
105
115
  title: 'ui/react-ui-grid/Grid',
106
116
  component: GridStory,
107
- decorators: [withTheme],
108
- parameters: { layout: 'fullscreen' },
109
- };
117
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
118
+ parameters: {
119
+ layout: 'fullscreen',
120
+ },
121
+ } satisfies Meta<typeof GridStory>;
110
122
 
111
123
  export default meta;
112
124
 
113
125
  type Story = StoryObj<typeof meta>;
114
126
 
127
+ export const Default: Story = {};
128
+
115
129
  export const Basic: Story = {
116
130
  args: {
117
131
  id: 'story',
@@ -158,26 +172,47 @@ export const Basic: Story = {
158
172
  },
159
173
  };
160
174
 
161
- // TODO(burdon): How to make single-column?
162
- export const SingleColumn: Story = {
175
+ const cellSize = 40;
176
+
177
+ // TODO(burdon): Calendar.
178
+ export const Calendar: Story = {
163
179
  args: {
164
180
  id: 'story',
165
- limitColumns: 1,
181
+ limitColumns: 7,
166
182
  columnDefault: {
167
183
  grid: {
168
- size: 180,
184
+ size: cellSize,
185
+ resizeable: false,
169
186
  },
170
187
  },
171
188
  rowDefault: {
172
189
  grid: {
173
- size: defaultRowSize,
190
+ size: cellSize,
174
191
  resizeable: false,
175
192
  },
176
193
  },
177
- columns: {
178
- grid: {
179
- 0: { size: 200 },
180
- },
194
+ getCells: (range, plane) => {
195
+ const cells: DxGridPlaneCells = {};
196
+ if (plane === 'grid') {
197
+ for (let col = range.start.col; col <= range.end.col; col++) {
198
+ for (let row = range.start.row; row <= range.end.row; row++) {
199
+ // TODO(burdon): Formatting changes when cell is selected.
200
+ cells[toPlaneCellIndex({ col, row })] = {
201
+ readonly: true,
202
+ accessoryHtml: '<div class="flex bs-full is-full justify-center items-center overflow-hidden">0</div>',
203
+ className: '',
204
+ };
205
+ }
206
+ }
207
+ }
208
+ return cells;
181
209
  },
182
210
  },
211
+ render: (args) => (
212
+ <div className='bs-full flex justify-center'>
213
+ <div className='bs-full is-[288px] border-x border-separator'>
214
+ <GridStory {...args} />
215
+ </div>
216
+ </div>
217
+ ),
183
218
  };
package/src/Grid/Grid.tsx CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
5
  import '@dxos/lit-grid/dx-grid.pcss';
6
6
 
7
- import { createComponent, type EventName } from '@lit/react';
8
- import { createContextScope, type Scope } from '@radix-ui/react-context';
7
+ import { type EventName, createComponent } from '@lit/react';
8
+ import { type Scope, createContextScope } from '@radix-ui/react-context';
9
9
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
10
10
  import React, {
11
11
  type ComponentProps,
12
- forwardRef,
13
12
  type PropsWithChildren,
13
+ forwardRef,
14
14
  useCallback,
15
15
  useEffect,
16
16
  useState,
@@ -63,7 +63,9 @@ const [createGridContext, createGridScope] = createContextScope(GRID_NAME, []);
63
63
  const [GridProvider, useGridContext] = createGridContext<GridContextValue>(GRID_NAME);
64
64
 
65
65
  type GridRootProps = PropsWithChildren<
66
- { id: string } & Partial<{
66
+ {
67
+ id: string;
68
+ } & Partial<{
67
69
  editing: GridEditing;
68
70
  defaultEditing: GridEditing;
69
71
  onEditingChange: (nextEditing: GridEditing) => void;
@@ -71,12 +73,12 @@ type GridRootProps = PropsWithChildren<
71
73
  >;
72
74
 
73
75
  const GridRoot = ({
76
+ __gridScope,
77
+ children,
74
78
  id,
75
79
  editing: propsEditing,
76
80
  defaultEditing,
77
81
  onEditingChange,
78
- children,
79
- __gridScope,
80
82
  }: GridScopedProps<GridRootProps>) => {
81
83
  const [editing = null, setEditing] = useControllableState({
82
84
  prop: propsEditing,
@@ -102,13 +104,13 @@ const GridRoot = ({
102
104
 
103
105
  GridRoot.displayName = GRID_NAME;
104
106
 
107
+ const GRID_CONTENT_NAME = 'GridContent';
108
+
105
109
  type GridContentProps = Omit<ComponentProps<typeof DxGrid>, 'onEdit'> & {
106
110
  getCells?: NaturalDxGrid['getCells'];
107
111
  activeRefs?: string;
108
112
  };
109
113
 
110
- const GRID_CONTENT_NAME = 'GridContent';
111
-
112
114
  const GridContent = forwardRef<NaturalDxGrid, GridScopedProps<GridContentProps>>((props, forwardedRef) => {
113
115
  const { id, editing, setEditBox, setEditing } = useGridContext(GRID_CONTENT_NAME, props.__gridScope);
114
116
  const [dxGrid, setDxGridInternal] = useState<NaturalDxGrid | null>(null);
@@ -166,7 +168,7 @@ export const Grid = {
166
168
  Content: GridContent,
167
169
  };
168
170
 
169
- export { GridRoot, GridContent, useGridContext, createGridScope, gridSeparatorInlineEnd, gridSeparatorBlockEnd };
171
+ export { GridRoot, GridContent, createGridScope, gridSeparatorInlineEnd, gridSeparatorBlockEnd, useGridContext };
170
172
 
171
173
  export type { GridRootProps, GridContentProps, GridEditing, GridEditBox, GridScopedProps, DxGridElement };
172
174
 
@@ -178,6 +180,7 @@ export {
178
180
  toPlaneCellIndex,
179
181
  parseCellIndex,
180
182
  cellQuery,
183
+ DxEditRequest,
181
184
  } from '@dxos/lit-grid';
182
185
 
183
186
  export type {