@dxos/react-ui-editor 0.4.3-main.dda98d3 → 0.4.3-main.e5bbcda

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.
@@ -23,7 +23,6 @@ import {
23
23
  TextHSix,
24
24
  TextItalic,
25
25
  Quotes,
26
- CaretDown,
27
26
  } from '@phosphor-icons/react';
28
27
  import { createContext } from '@radix-ui/react-context';
29
28
  import React, { type PropsWithChildren } from 'react';
@@ -65,7 +64,7 @@ export type ActionType =
65
64
  | 'link'
66
65
  | 'list-bullet'
67
66
  | 'list-ordered'
68
- | 'list-tasks'
67
+ | 'list-task'
69
68
  | 'mention'
70
69
  | 'prompt'
71
70
  | 'strikethrough'
@@ -99,12 +98,17 @@ const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) =>
99
98
  );
100
99
  };
101
100
 
102
- type ToolbarButtonProps = ToolbarToggleGroupItemProps & {
101
+ const buttonStyles = 'min-bs-0 p-2';
102
+ const iconStyles = getSize(5);
103
+
104
+ type ButtonProps = {
105
+ type: ActionType;
103
106
  Icon: Icon;
107
+ getState: (state: Formatting) => boolean;
108
+ disabled?: (state: Formatting) => boolean;
104
109
  };
105
110
 
106
- const buttonStyles = 'min-bs-0 p-2';
107
- const iconStyles = getSize(4);
111
+ type ToolbarButtonProps = ToolbarToggleGroupItemProps & { Icon: Icon };
108
112
 
109
113
  const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
110
114
  return (
@@ -118,7 +122,7 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
118
122
  const ToolbarSeparator = () => <div role='separator' className='grow' />;
119
123
 
120
124
  const MarkdownHeading = () => {
121
- const { t } = useTranslation(translationKey);
125
+ // const { t } = useTranslation(translationKey);
122
126
  const { onAction, state } = useToolbarContext('MarkdownFormatting');
123
127
  const blockType = state ? state.blockType : 'paragraph';
124
128
  const header = blockType && /heading(\d)/.exec(blockType);
@@ -130,21 +134,30 @@ const MarkdownHeading = () => {
130
134
  value={value ?? '0'}
131
135
  onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
132
136
  >
133
- <Select.Trigger asChild>
134
- <NaturalToolbar.Button variant='ghost' classNames={buttonStyles}>
135
- <span className='sr-only'>{t('heading label')}</span>
136
- <HeadingIcon className={iconStyles} />
137
- <CaretDown className={getSize(2)} weight='bold' />
138
- </NaturalToolbar.Button>
139
- </Select.Trigger>
137
+ <Select.TriggerButton>
138
+ <HeadingIcon className={iconStyles} />
139
+ {/* TODO(burdon): Incorrectly positioned. */}
140
+ {/* <NaturalToolbar.Button variant='ghost' classNames={buttonStyles}> */}
141
+ {/* <span className='sr-only'>{t('heading label')}</span> */}
142
+ {/* <HeadingIcon className={iconStyles} /> */}
143
+ {/* <CaretDown className={getSize(2)} weight='bold' /> */}
144
+ {/* </NaturalToolbar.Button> */}
145
+ </Select.TriggerButton>
140
146
  <Select.Portal>
141
147
  <Select.Content>
142
148
  <Select.ScrollUpButton />
143
149
  <Select.Viewport>
144
- <Select.Option value='0'>Paragraph</Select.Option>
150
+ <Select.Option value='0'>
151
+ <Paragraph className={iconStyles} />
152
+ </Select.Option>
145
153
  {[1, 2, 3, 4, 5, 6].map((level) => (
146
154
  <Select.Option key={level} value={String(level)}>
147
- Heading {level}
155
+ {level === 1 && <TextHOne className={iconStyles} />}
156
+ {level === 2 && <TextHTwo className={iconStyles} />}
157
+ {level === 3 && <TextHThree className={iconStyles} />}
158
+ {level === 4 && <TextHFour className={iconStyles} />}
159
+ {level === 5 && <TextHFive className={iconStyles} />}
160
+ {level === 6 && <TextHSix className={iconStyles} />}
148
161
  </Select.Option>
149
162
  ))}
150
163
  </Select.Viewport>
@@ -156,14 +169,12 @@ const MarkdownHeading = () => {
156
169
  );
157
170
  };
158
171
 
159
- const markdownStyles = [
160
- { key: 'strong' as const, Icon: TextB },
161
- { key: 'emphasis' as const, Icon: TextItalic },
162
- { key: 'strikethrough' as const, Icon: TextStrikethrough },
163
- { key: 'code' as const, Icon: Code },
164
- { key: 'link' as const, Icon: Link },
165
- // <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
166
- // <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
172
+ const markdownStyles: ButtonProps[] = [
173
+ { type: 'strong', Icon: TextB, getState: (state) => state.strong },
174
+ { type: 'emphasis', Icon: TextItalic, getState: (state) => state.emphasis },
175
+ { type: 'strikethrough', Icon: TextStrikethrough, getState: (state) => state.strikethrough },
176
+ { type: 'code', Icon: Code, getState: (state) => state.code },
177
+ { type: 'link', Icon: Link, getState: (state) => state.link },
167
178
  ];
168
179
 
169
180
  const MarkdownStyles = () => {
@@ -173,27 +184,27 @@ const MarkdownStyles = () => {
173
184
  return (
174
185
  <NaturalToolbar.ToggleGroup
175
186
  type='multiple'
176
- value={markdownStyles.filter(({ key }) => !!state?.[key]).map(({ key }) => key)}
187
+ value={markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)}
177
188
  >
178
- {markdownStyles.map(({ key, Icon }) => (
189
+ {markdownStyles.map(({ type, getState, Icon }) => (
179
190
  <ToolbarButton
180
- key={key}
181
- value={key}
191
+ key={type}
192
+ value={type}
182
193
  Icon={Icon}
183
194
  disabled={state?.blockType === 'codeblock'}
184
- onClick={() => onAction?.({ type: key, data: state ? !state[key] : null })}
195
+ onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
185
196
  >
186
- {t(`${key} label`)}
197
+ {t(`${type} label`)}
187
198
  </ToolbarButton>
188
199
  ))}
189
200
  </NaturalToolbar.ToggleGroup>
190
201
  );
191
202
  };
192
203
 
193
- const markdownLists = [
194
- { key: 'bullet' as const, Icon: ListBullets, actionType: 'list-bullet' as const },
195
- { key: 'ordered' as const, Icon: ListNumbers, actionType: 'list-ordered' as const },
196
- { key: 'task' as const, Icon: ListChecks, actionType: 'list-tasks' as const },
204
+ const markdownLists: ButtonProps[] = [
205
+ { type: 'list-bullet', Icon: ListBullets, getState: (state) => state.listStyle === 'bullet' },
206
+ { type: 'list-ordered', Icon: ListNumbers, getState: (state) => state.listStyle === 'ordered' },
207
+ { type: 'list-task', Icon: ListChecks, getState: (state) => state.listStyle === 'task' },
197
208
  ];
198
209
 
199
210
  const MarkdownLists = () => {
@@ -201,62 +212,58 @@ const MarkdownLists = () => {
201
212
  const { t } = useTranslation(translationKey);
202
213
  return (
203
214
  <NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ?? undefined}>
204
- {markdownLists.map(({ key, Icon, actionType }) => (
215
+ {markdownLists.map(({ type, getState, Icon }) => (
205
216
  <ToolbarButton
206
- key={key}
207
- value={key}
217
+ key={type}
218
+ value={type}
208
219
  Icon={Icon}
209
- onClick={() => onAction?.({ type: actionType, data: state ? state.listStyle !== key : null })}
220
+ onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
210
221
  >
211
- {t(`${key} label`)}
222
+ {t(`${type} label`)}
212
223
  </ToolbarButton>
213
224
  ))}
214
225
  </NaturalToolbar.ToggleGroup>
215
226
  );
216
227
  };
217
228
 
218
- const markdownBlocks = [
229
+ const markdownBlocks: ButtonProps[] = [
219
230
  {
220
- key: 'blockquote' as const,
231
+ type: 'blockquote',
221
232
  Icon: Quotes,
222
- pressed: (state: Formatting | null) => state?.blockQuote,
223
- action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
224
- disabled: (state: Formatting | null) => false,
233
+ getState: (state) => state.blockQuote,
225
234
  },
226
235
  {
227
- key: 'codeblock' as const,
236
+ type: 'codeblock',
228
237
  Icon: CodeBlock,
229
- pressed: (state: Formatting | null) => state?.blockType === 'codeblock',
230
- action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
231
- disabled: (state: Formatting | null) => !state?.blankLine,
238
+ getState: (state) => state.blockQuote,
239
+ disabled: (state) => !state.blankLine,
240
+ },
241
+ {
242
+ type: 'table',
243
+ Icon: Table,
244
+ getState: (state) => state.blockType === 'tablecell',
245
+ disabled: (state) => !state.blankLine,
232
246
  },
233
247
  ];
234
248
 
235
249
  const MarkdownBlocks = () => {
236
250
  const { onAction, state } = useToolbarContext('MarkdownStyles');
237
251
  const { t } = useTranslation(translationKey);
252
+ const value = markdownBlocks.find(({ getState }) => state && getState(state));
238
253
  return (
239
- <>
240
- <NaturalToolbar.ToggleGroup
241
- type='multiple'
242
- value={markdownBlocks.filter(({ pressed }) => pressed(state)).map(({ key }) => key)}
243
- >
244
- {markdownBlocks.map(({ key, Icon, action }) => (
245
- <ToolbarButton key={key} value={key} Icon={Icon} onClick={() => onAction?.(action(state))}>
246
- {t(`${key} label`)}
247
- </ToolbarButton>
248
- ))}
249
- </NaturalToolbar.ToggleGroup>
250
- <NaturalToolbar.Button
251
- variant='ghost'
252
- disabled={!state?.blankLine}
253
- onClick={() => onAction?.({ type: 'table' })}
254
- classNames={buttonStyles}
255
- >
256
- <Table className={iconStyles} />
257
- <span className='sr-only'>{t('table label')}</span>
258
- </NaturalToolbar.Button>
259
- </>
254
+ <NaturalToolbar.ToggleGroup type='single' value={value?.type}>
255
+ {markdownBlocks.map(({ type, disabled, getState, Icon }) => (
256
+ <ToolbarButton
257
+ key={type}
258
+ value={type}
259
+ Icon={Icon}
260
+ disabled={!state || disabled?.(state)}
261
+ onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
262
+ >
263
+ {t(`${type} label`)}
264
+ </ToolbarButton>
265
+ ))}
266
+ </NaturalToolbar.ToggleGroup>
260
267
  );
261
268
  };
262
269
 
@@ -273,7 +280,12 @@ const MarkdownExtended = () => {
273
280
  const { onAction } = useToolbarContext('MarkdownStyles');
274
281
  const { t } = useTranslation(translationKey);
275
282
  return (
276
- <NaturalToolbar.Button variant='ghost' onClick={() => onAction?.({ type: 'comment' })} classNames={buttonStyles}>
283
+ <NaturalToolbar.Button
284
+ variant='ghost'
285
+ data-testid='editor.toolbar.comment'
286
+ onClick={() => onAction?.({ type: 'comment' })}
287
+ classNames={buttonStyles}
288
+ >
277
289
  <ChatText className={iconStyles} />
278
290
  <span className='sr-only'>{t('comment label')}</span>
279
291
  </NaturalToolbar.Button>
@@ -36,8 +36,11 @@ const styles = EditorView.baseTheme({
36
36
  },
37
37
  });
38
38
 
39
- const commentMark = Decoration.mark({ class: 'cm-comment' });
40
- const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
39
+ const commentMark = Decoration.mark({ class: 'cm-comment', attributes: { 'data-testid': 'cm-comment' } });
40
+ const commentCurrentMark = Decoration.mark({
41
+ class: 'cm-comment-current',
42
+ attributes: { 'data-testid': 'cm-comment' },
43
+ });
41
44
 
42
45
  type CommentState = {
43
46
  comment: Comment;
@@ -370,7 +370,7 @@ describe('removeBlockquote', () => {
370
370
  });
371
371
 
372
372
  describe('addCodeblock', () => {
373
- testCommand('add a code block to a blank line', '{}', addCodeblock, '```{}\n\n```');
373
+ testCommand('add a code block to a blank line', '{}', addCodeblock, '```{}\n```');
374
374
 
375
375
  testCommand(
376
376
  'can turn a paragraph into a code block',
@@ -356,7 +356,6 @@ const snippets = {
356
356
  [
357
357
  //
358
358
  '```#{}',
359
- '#{}',
360
359
  '```',
361
360
  ].join('\n'),
362
361
  ),
@@ -515,6 +514,7 @@ export const addLink: StateCommand = ({ state, dispatch }) => {
515
514
  // Lists
516
515
  //
517
516
 
517
+ // TODO(burdon): Cursor is positioned incorrectly.
518
518
  export const addList =
519
519
  (type: List): StateCommand =>
520
520
  ({ state, dispatch }) => {
@@ -576,18 +576,21 @@ export const addList =
576
576
  // Insert a new list item if the selection is empty.
577
577
  const { from, to } = state.doc.lineAt(state.selection.main.anchor);
578
578
  if (from === to) {
579
+ const insert = type === List.Bullet ? '- ' : type === List.Ordered ? '1. ' : '- [ ] ';
579
580
  dispatch(
580
581
  state.update({
581
582
  changes: [
582
583
  {
583
584
  from,
584
- insert: type === List.Bullet ? '- ' : type === List.Ordered ? '1. ' : '- [ ] ',
585
+ insert,
585
586
  },
586
587
  ],
588
+ selection: { anchor: from + insert.length },
587
589
  userEvent: 'format.list.add',
588
590
  scrollIntoView: true,
589
591
  }),
590
592
  );
593
+
591
594
  return true;
592
595
  }
593
596
 
@@ -54,7 +54,7 @@ export const useActionHandler = (view?: EditorView | null): ToolbarProps['onActi
54
54
 
55
55
  case 'list-ordered':
56
56
  case 'list-bullet':
57
- case 'list-tasks':
57
+ case 'list-task':
58
58
  listType =
59
59
  action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
60
60
  (action.data === false
@@ -12,9 +12,9 @@ export default [
12
12
  'emphasis label': 'Emphasis',
13
13
  'strikethrough label': 'Strike-through',
14
14
  'code label': 'Code',
15
- 'bullet label': 'Bullet list',
16
- 'ordered label': 'Numbered list',
17
- 'task label': 'Task list',
15
+ 'list-bullet label': 'Bullet list',
16
+ 'list-ordered label': 'Numbered list',
17
+ 'list-task label': 'Task list',
18
18
  'blockquote label': 'Block quote',
19
19
  'codeblock label': 'Code block',
20
20
  'comment label': 'Create comment',