@dxos/react-ui-editor 0.4.6-main.3500f01 → 0.4.6-main.6d26f8f

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.
@@ -3,13 +3,13 @@
3
3
  //
4
4
 
5
5
  import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
- import { EditorView, scrollPastEnd } from '@codemirror/view';
6
+ import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import defaultsDeep from 'lodash.defaultsdeep';
9
9
  import React, {
10
10
  type ComponentProps,
11
- type KeyboardEventHandler,
12
11
  forwardRef,
12
+ type KeyboardEventHandler,
13
13
  useCallback,
14
14
  useEffect,
15
15
  useImperativeHandle,
@@ -52,8 +52,6 @@ export type TextEditorProps = {
52
52
  model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
53
53
  readonly?: boolean; // TODO(burdon): Move into model.
54
54
  autoFocus?: boolean;
55
- scrollPastEnd?: boolean;
56
- moveToEndOfLine?: boolean;
57
55
  lineWrapping?: boolean;
58
56
  scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
59
57
  selection?: { anchor: number; head?: number };
@@ -70,19 +68,7 @@ export type TextEditorProps = {
70
68
  // TODO(burdon): Replace with useTextEditor.
71
69
  export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
72
70
  (
73
- {
74
- model,
75
- readonly,
76
- autoFocus,
77
- scrollTo = EditorView.scrollIntoView(0),
78
- moveToEndOfLine,
79
- selection,
80
- theme,
81
- slots = defaultSlots,
82
- extensions = [],
83
- scrollPastEnd: _scrollPastEnd,
84
- debug,
85
- },
71
+ { model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
86
72
  forwardedRef,
87
73
  ) => {
88
74
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
@@ -97,7 +83,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
97
83
 
98
84
  // Set focus.
99
85
  useEffect(() => {
100
- if (autoFocus) {
86
+ if (autoFocus && !view?.hasFocus) {
87
+ if (view?.state.selection.main?.from === 0) {
88
+ // Start at end of line.
89
+ const { to } = view.state.doc.lineAt(0);
90
+ view?.dispatch({ selection: { anchor: to } });
91
+ }
101
92
  view?.focus();
102
93
  }
103
94
  }, [view, autoFocus]);
@@ -123,15 +114,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
123
114
  }),
124
115
 
125
116
  // Theme.
126
- // TODO(burdon): Make base theme configurable.
117
+ // TODO(burdon): Make configurable.
127
118
  EditorView.baseTheme(defaultTheme),
128
- theme && EditorView.theme(theme),
119
+ EditorView.theme(theme ?? {}),
129
120
  EditorView.darkTheme.of(themeMode === 'dark'),
130
- slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
131
- slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
132
-
133
- // NOTE: Assumes default line height.
134
- _scrollPastEnd && scrollPastEnd(),
121
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
122
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
135
123
 
136
124
  // Focus.
137
125
  EditorView.updateListener.of((update) => {
@@ -159,7 +147,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
159
147
  // EditorView
160
148
  // https://codemirror.net/docs/ref/#view.EditorViewConfig
161
149
  //
162
- const view = new EditorView({
150
+ const newView = new EditorView({
163
151
  state,
164
152
  parent: rootRef.current,
165
153
  scrollTo,
@@ -173,12 +161,8 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
173
161
  },
174
162
  });
175
163
 
176
- setView(view);
177
-
178
- if (moveToEndOfLine) {
179
- const { to } = view.state.doc.lineAt(0);
180
- view?.dispatch({ selection: { anchor: to } });
181
- }
164
+ view?.destroy();
165
+ setView(newView);
182
166
 
183
167
  // Remove tabster attribute (rely on custom keymap).
184
168
  if (state.facet(editorMode).noTabster) {
@@ -186,7 +170,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
186
170
  }
187
171
 
188
172
  return () => {
189
- view?.destroy();
173
+ newView?.destroy();
190
174
  setView(null);
191
175
  };
192
176
  // TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
@@ -231,7 +215,6 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
231
215
  () => [createBasicBundle({ themeMode, placeholder, lineWrapping }), ...(_extensions ?? [])],
232
216
  [themeMode, placeholder, lineWrapping, _extensions],
233
217
  );
234
-
235
218
  return (
236
219
  <BaseTextEditor
237
220
  ref={forwardedRef}
@@ -253,7 +236,6 @@ export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
253
236
  () => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
254
237
  [themeMode, placeholder, _extensions],
255
238
  );
256
-
257
239
  return (
258
240
  <BaseTextEditor
259
241
  ref={forwardedRef}
@@ -272,7 +254,7 @@ export const defaultSlots: TextEditorSlots = {
272
254
  className: focusRing,
273
255
  },
274
256
  editor: {
275
- className: 'bs-full',
257
+ className: 'min-bs-full',
276
258
  },
277
259
  };
278
260
 
@@ -6,7 +6,7 @@ import '@dxosTheme';
6
6
 
7
7
  import React, { useMemo, useState } from 'react';
8
8
 
9
- import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
9
+ import { Toolbar as NaturalToolbar, Select, useThemeContext } from '@dxos/react-ui';
10
10
  import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
11
11
  import { withTheme } from '@dxos/storybook-utils';
12
12
 
@@ -22,6 +22,7 @@ import {
22
22
  useFormattingState,
23
23
  } from '../../extensions';
24
24
  import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
25
+ import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
25
26
  import { markdownTheme } from '../../themes';
26
27
  import translations from '../../translations';
27
28
  import { Toolbar } from '../Toolbar';
@@ -51,7 +52,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
51
52
  themeMode,
52
53
  theme: markdownTheme,
53
54
  slots: {
54
- editor: { className: 'p-2' },
55
+ content: { className: '!p-4' },
55
56
  },
56
57
  }),
57
58
  // TODO(burdon): Move lineWrapping.
@@ -69,16 +70,16 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
69
70
 
70
71
  const handleAction = useActionHandler(view);
71
72
 
72
- // TODO(marijn): This doesn't update the state on view changes.
73
+ // TODO(marijn) This doesn't update the state on view changes.
73
74
  // Also not sure if view is even guaranteed to exist at this point.
74
75
  return (
75
- <div role='none' className={mx('fixed inset-0 flex flex-col')}>
76
+ <div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
76
77
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
77
78
  <Toolbar.Markdown />
78
79
  <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
79
80
  </Toolbar.Root>
80
- <div role='none' className='grow overflow-hidden'>
81
- <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
81
+ <div role='none' className='overflow-y-auto'>
82
+ <div role='textbox' className={mx(textBlockWidth, attentionSurface, editorFillLayoutEditor)} ref={parentRef} />
82
83
  </div>
83
84
  </div>
84
85
  );
@@ -118,11 +119,7 @@ export default {
118
119
  title: 'react-ui-editor/useTextEditor',
119
120
  component: TextEditor,
120
121
  decorators: [withTheme],
121
- render: (args: StoryProps) => (
122
- <Tooltip.Provider>
123
- <Story {...args} />
124
- </Tooltip.Provider>
125
- ),
122
+ render: (args: StoryProps) => <Story {...args} />,
126
123
  parameters: { translations, layout: 'fullscreen' },
127
124
  };
128
125
 
@@ -9,7 +9,6 @@ import React, { type FC, useMemo, useState } from 'react';
9
9
  import { TextObject } from '@dxos/echo-schema';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { faker } from '@dxos/random';
12
- import { Tooltip } from '@dxos/react-ui';
13
12
  import { mx, textBlockWidth } from '@dxos/react-ui-theme';
14
13
  import { withTheme } from '@dxos/storybook-utils';
15
14
 
@@ -24,6 +23,7 @@ import {
24
23
  useFormattingState,
25
24
  } from '../../extensions';
26
25
  import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
26
+ import { editorFillLayoutEditor, editorFillLayoutRoot, editorWithToolbarLayout } from '../../styles';
27
27
  import translations from '../../translations';
28
28
  import { MarkdownEditor } from '../TextEditor';
29
29
 
@@ -61,7 +61,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
61
61
  }
62
62
 
63
63
  return (
64
- <div role='none' className={mx('fixed inset-0 flex flex-col')}>
64
+ <div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
65
65
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
66
66
  <Toolbar.Markdown />
67
67
  <Toolbar.Separator />
@@ -72,8 +72,9 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
72
72
  model={model}
73
73
  extensions={extensions}
74
74
  slots={{
75
- root: { className: mx(textBlockWidth) },
76
- editor: { className: 'p-2' },
75
+ root: { className: mx(textBlockWidth, editorFillLayoutRoot) },
76
+ editor: { className: editorFillLayoutEditor },
77
+ content: { className: '!pli-2' },
77
78
  }}
78
79
  />
79
80
  </div>
@@ -83,11 +84,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
83
84
  export default {
84
85
  title: 'react-ui-editor/Toolbar',
85
86
  component: Toolbar,
86
- render: (args: any) => (
87
- <Tooltip.Provider>
88
- <Story {...args} />
89
- </Tooltip.Provider>
90
- ),
87
+ render: (args: any) => <Story {...args} />,
91
88
  decorators: [withTheme],
92
89
  parameters: { translations, layout: 'fullscreen' },
93
90
  };
@@ -25,7 +25,7 @@ import {
25
25
  Quotes,
26
26
  } from '@phosphor-icons/react';
27
27
  import { createContext } from '@radix-ui/react-context';
28
- import React, { type PropsWithChildren, useRef, useState } from 'react';
28
+ import React, { type PropsWithChildren } from 'react';
29
29
 
30
30
  import {
31
31
  DensityProvider,
@@ -141,31 +141,11 @@ const MarkdownHeading = () => {
141
141
  const header = blockType && /heading(\d)/.exec(blockType);
142
142
  const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : undefined;
143
143
  const HeadingIcon = HeadingIcons[value ?? '0'];
144
- const suppressNextTooltip = useRef<boolean>(false);
145
- const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
146
- const [selectOpen, setSelectOpen] = useState<boolean>(false);
147
144
  return (
148
- <Tooltip.Root
149
- open={tooltipOpen}
150
- onOpenChange={(nextOpen) => {
151
- if (nextOpen && suppressNextTooltip.current) {
152
- suppressNextTooltip.current = false;
153
- return setTooltipOpen(false);
154
- } else {
155
- return setTooltipOpen(nextOpen);
156
- }
157
- }}
158
- >
145
+ <Tooltip.Root>
159
146
  <Select.Root
160
147
  disabled={value === null}
161
148
  value={value ?? '0'}
162
- open={selectOpen}
163
- onOpenChange={(nextOpen: boolean) => {
164
- if (!nextOpen) {
165
- suppressNextTooltip.current = true;
166
- }
167
- return setSelectOpen(nextOpen);
168
- }}
169
149
  onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
170
150
  >
171
151
  <Tooltip.Trigger asChild>
@@ -16,7 +16,7 @@ import { log } from '@dxos/log';
16
16
  import { type ThemeMode } from '@dxos/react-ui';
17
17
  import { isNotFalsy } from '@dxos/util';
18
18
 
19
- import { type ThemeStyles } from '../styles';
19
+ import { type ThemeStyles } from '..//styles';
20
20
  import { defaultTheme } from '../themes';
21
21
  import { logChanges } from '../util';
22
22
 
@@ -26,7 +26,6 @@ export type UseTextEditorOptions = {
26
26
  debug?: boolean;
27
27
  } & EditorStateConfig;
28
28
 
29
- // TODO(burdon): Return tuple?
30
29
  export type UseTextEditor = {
31
30
  parentRef: RefObject<HTMLDivElement>;
32
31
  view?: EditorView;
@@ -88,7 +87,8 @@ export const useTextEditor = ({
88
87
  return () => {
89
88
  view?.destroy();
90
89
  };
91
- }, [doc, extensions]);
90
+ // TODO(wittjosiah): Does `parentRef` ever change? Only `.current` changes?
91
+ }, [parentRef, extensions]);
92
92
 
93
93
  useEffect(() => {
94
94
  if (view) {
package/src/index.ts CHANGED
@@ -12,7 +12,13 @@ export { Doc, YText, YXmlFragment } from '@dxos/text-model';
12
12
  export * from './components';
13
13
  export * from './extensions';
14
14
  export * from './hooks';
15
- export { getToken, editorWithToolbarLayout, editorFillLayoutRoot, editorFillLayoutEditor } from './styles';
15
+ export {
16
+ getToken,
17
+ editorWithToolbarLayout,
18
+ editorFillLayoutEditor,
19
+ editorFillLayoutRoot,
20
+ editorHalfViewportOverscrollContent,
21
+ } from './styles';
16
22
  export * from './themes';
17
23
  export * from './util';
18
24
  export { translations };
@@ -5,5 +5,8 @@
5
5
  export const editorWithToolbarLayout =
6
6
  'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
7
7
 
8
- export const editorFillLayoutRoot = 'bs-full relative';
9
- export const editorFillLayoutEditor = '!absolute inset-0';
8
+ export const editorFillLayoutRoot = 'bs-full overflow-hidden grid';
9
+ export const editorFillLayoutEditor =
10
+ 'bs-full overflow-hidden [&>.cm-scroller]:bs-full [&>.cm-scroller]:overflow-y-auto';
11
+
12
+ export const editorHalfViewportOverscrollContent = 'after:block after:min-bs-[50dvh] after:pointer-events-none';
@@ -50,18 +50,14 @@ export const defaultTheme: ThemeStyles = {
50
50
  outline: 'none',
51
51
  },
52
52
 
53
- // NOTE: See https://codemirror.net/docs/guide (DOM Structure).
54
53
  '.cm-scroller': {
55
- // TODO(burdon): Reconcile with docs: https://codemirror.net/docs/guide
56
- // Inside of that is the scroller element. If the editor has its own scrollbar, this one should be styled with overflow: auto. But it doesn't have to—the editor also supports growing to accomodate its content, or growing up to a certain max-height and then scrolling.
57
- overflowY: 'auto',
54
+ // overflow: 'visible',
58
55
  fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
59
- lineHeight: 1.5,
56
+ lineHeight: 1.4,
60
57
  },
61
58
 
62
59
  '.cm-content': {
63
- // TODO(burdon): Is it possible to remove the padding value from CM's default theme?
64
- // padding: 'unset',
60
+ padding: 0,
65
61
  // NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
66
62
  fontSize: '16px',
67
63
  },