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

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 } from '@codemirror/view';
6
+ import { EditorView, scrollPastEnd } 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
- forwardRef,
12
11
  type KeyboardEventHandler,
12
+ forwardRef,
13
13
  useCallback,
14
14
  useEffect,
15
15
  useImperativeHandle,
@@ -52,6 +52,8 @@ 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;
55
57
  lineWrapping?: boolean;
56
58
  scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
57
59
  selection?: { anchor: number; head?: number };
@@ -68,7 +70,19 @@ export type TextEditorProps = {
68
70
  // TODO(burdon): Replace with useTextEditor.
69
71
  export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
70
72
  (
71
- { model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
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
+ },
72
86
  forwardedRef,
73
87
  ) => {
74
88
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
@@ -83,12 +97,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
83
97
 
84
98
  // Set focus.
85
99
  useEffect(() => {
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
- }
100
+ if (autoFocus) {
92
101
  view?.focus();
93
102
  }
94
103
  }, [view, autoFocus]);
@@ -114,12 +123,15 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
114
123
  }),
115
124
 
116
125
  // Theme.
117
- // TODO(burdon): Make configurable.
126
+ // TODO(burdon): Make base theme configurable.
118
127
  EditorView.baseTheme(defaultTheme),
119
- EditorView.theme(theme ?? {}),
128
+ theme && EditorView.theme(theme),
120
129
  EditorView.darkTheme.of(themeMode === 'dark'),
121
- EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
122
- EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
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(),
123
135
 
124
136
  // Focus.
125
137
  EditorView.updateListener.of((update) => {
@@ -147,7 +159,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
147
159
  // EditorView
148
160
  // https://codemirror.net/docs/ref/#view.EditorViewConfig
149
161
  //
150
- const newView = new EditorView({
162
+ const view = new EditorView({
151
163
  state,
152
164
  parent: rootRef.current,
153
165
  scrollTo,
@@ -161,8 +173,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
161
173
  },
162
174
  });
163
175
 
164
- view?.destroy();
165
- setView(newView);
176
+ setView(view);
177
+
178
+ if (moveToEndOfLine) {
179
+ const { to } = view.state.doc.lineAt(0);
180
+ view?.dispatch({ selection: { anchor: to } });
181
+ }
166
182
 
167
183
  // Remove tabster attribute (rely on custom keymap).
168
184
  if (state.facet(editorMode).noTabster) {
@@ -170,7 +186,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
170
186
  }
171
187
 
172
188
  return () => {
173
- newView?.destroy();
189
+ view?.destroy();
174
190
  setView(null);
175
191
  };
176
192
  // TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
@@ -215,6 +231,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
215
231
  () => [createBasicBundle({ themeMode, placeholder, lineWrapping }), ...(_extensions ?? [])],
216
232
  [themeMode, placeholder, lineWrapping, _extensions],
217
233
  );
234
+
218
235
  return (
219
236
  <BaseTextEditor
220
237
  ref={forwardedRef}
@@ -236,6 +253,7 @@ export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
236
253
  () => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
237
254
  [themeMode, placeholder, _extensions],
238
255
  );
256
+
239
257
  return (
240
258
  <BaseTextEditor
241
259
  ref={forwardedRef}
@@ -254,7 +272,7 @@ export const defaultSlots: TextEditorSlots = {
254
272
  className: focusRing,
255
273
  },
256
274
  editor: {
257
- className: 'min-bs-full',
275
+ className: 'bs-full',
258
276
  },
259
277
  };
260
278
 
@@ -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 } from '@dxos/react-ui';
9
+ import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } 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,7 +22,6 @@ import {
22
22
  useFormattingState,
23
23
  } from '../../extensions';
24
24
  import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
25
- import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
26
25
  import { markdownTheme } from '../../themes';
27
26
  import translations from '../../translations';
28
27
  import { Toolbar } from '../Toolbar';
@@ -52,7 +51,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
52
51
  themeMode,
53
52
  theme: markdownTheme,
54
53
  slots: {
55
- content: { className: '!p-4' },
54
+ editor: { className: 'p-2' },
56
55
  },
57
56
  }),
58
57
  // TODO(burdon): Move lineWrapping.
@@ -70,16 +69,16 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
70
69
 
71
70
  const handleAction = useActionHandler(view);
72
71
 
73
- // TODO(marijn) This doesn't update the state on view changes.
72
+ // TODO(marijn): This doesn't update the state on view changes.
74
73
  // Also not sure if view is even guaranteed to exist at this point.
75
74
  return (
76
- <div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
75
+ <div role='none' className={mx('fixed inset-0 flex flex-col')}>
77
76
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
78
77
  <Toolbar.Markdown />
79
78
  <EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
80
79
  </Toolbar.Root>
81
- <div role='none' className='overflow-y-auto'>
82
- <div role='textbox' className={mx(textBlockWidth, attentionSurface, editorFillLayoutEditor)} ref={parentRef} />
80
+ <div role='none' className='grow overflow-hidden'>
81
+ <div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
83
82
  </div>
84
83
  </div>
85
84
  );
@@ -119,7 +118,11 @@ export default {
119
118
  title: 'react-ui-editor/useTextEditor',
120
119
  component: TextEditor,
121
120
  decorators: [withTheme],
122
- render: (args: StoryProps) => <Story {...args} />,
121
+ render: (args: StoryProps) => (
122
+ <Tooltip.Provider>
123
+ <Story {...args} />
124
+ </Tooltip.Provider>
125
+ ),
123
126
  parameters: { translations, layout: 'fullscreen' },
124
127
  };
125
128
 
@@ -9,6 +9,7 @@ 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';
12
13
  import { mx, textBlockWidth } from '@dxos/react-ui-theme';
13
14
  import { withTheme } from '@dxos/storybook-utils';
14
15
 
@@ -23,7 +24,6 @@ import {
23
24
  useFormattingState,
24
25
  } from '../../extensions';
25
26
  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', editorWithToolbarLayout)}>
64
+ <div role='none' className={mx('fixed inset-0 flex flex-col')}>
65
65
  <Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
66
66
  <Toolbar.Markdown />
67
67
  <Toolbar.Separator />
@@ -72,9 +72,8 @@ 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, editorFillLayoutRoot) },
76
- editor: { className: editorFillLayoutEditor },
77
- content: { className: '!pli-2' },
75
+ root: { className: mx(textBlockWidth) },
76
+ editor: { className: 'p-2' },
78
77
  }}
79
78
  />
80
79
  </div>
@@ -84,7 +83,11 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
84
83
  export default {
85
84
  title: 'react-ui-editor/Toolbar',
86
85
  component: Toolbar,
87
- render: (args: any) => <Story {...args} />,
86
+ render: (args: any) => (
87
+ <Tooltip.Provider>
88
+ <Story {...args} />
89
+ </Tooltip.Provider>
90
+ ),
88
91
  decorators: [withTheme],
89
92
  parameters: { translations, layout: 'fullscreen' },
90
93
  };
@@ -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 } from 'react';
28
+ import React, { type PropsWithChildren, useRef, useState } from 'react';
29
29
 
30
30
  import {
31
31
  DensityProvider,
@@ -141,11 +141,31 @@ 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);
144
147
  return (
145
- <Tooltip.Root>
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
+ >
146
159
  <Select.Root
147
160
  disabled={value === null}
148
161
  value={value ?? '0'}
162
+ open={selectOpen}
163
+ onOpenChange={(nextOpen: boolean) => {
164
+ if (!nextOpen) {
165
+ suppressNextTooltip.current = true;
166
+ }
167
+ return setSelectOpen(nextOpen);
168
+ }}
149
169
  onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
150
170
  >
151
171
  <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,6 +26,7 @@ export type UseTextEditorOptions = {
26
26
  debug?: boolean;
27
27
  } & EditorStateConfig;
28
28
 
29
+ // TODO(burdon): Return tuple?
29
30
  export type UseTextEditor = {
30
31
  parentRef: RefObject<HTMLDivElement>;
31
32
  view?: EditorView;
@@ -87,8 +88,7 @@ export const useTextEditor = ({
87
88
  return () => {
88
89
  view?.destroy();
89
90
  };
90
- // TODO(wittjosiah): Does `parentRef` ever change? Only `.current` changes?
91
- }, [parentRef, extensions]);
91
+ }, [doc, extensions]);
92
92
 
93
93
  useEffect(() => {
94
94
  if (view) {
package/src/index.ts CHANGED
@@ -12,13 +12,7 @@ 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 {
16
- getToken,
17
- editorWithToolbarLayout,
18
- editorFillLayoutEditor,
19
- editorFillLayoutRoot,
20
- editorHalfViewportOverscrollContent,
21
- } from './styles';
15
+ export { getToken, editorWithToolbarLayout, editorFillLayoutRoot, editorFillLayoutEditor } from './styles';
22
16
  export * from './themes';
23
17
  export * from './util';
24
18
  export { translations };
@@ -5,8 +5,5 @@
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 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';
8
+ export const editorFillLayoutRoot = 'bs-full relative';
9
+ export const editorFillLayoutEditor = '!absolute inset-0';
@@ -50,14 +50,18 @@ export const defaultTheme: ThemeStyles = {
50
50
  outline: 'none',
51
51
  },
52
52
 
53
+ // NOTE: See https://codemirror.net/docs/guide (DOM Structure).
53
54
  '.cm-scroller': {
54
- // overflow: 'visible',
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',
55
58
  fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
56
- lineHeight: 1.4,
59
+ lineHeight: 1.5,
57
60
  },
58
61
 
59
62
  '.cm-content': {
60
- padding: 0,
63
+ // TODO(burdon): Is it possible to remove the padding value from CM's default theme?
64
+ // padding: 'unset',
61
65
  // NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
62
66
  fontSize: '16px',
63
67
  },