@edux-design/comments 0.1.4 → 0.2.0

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/dist/index.mjs CHANGED
@@ -21,7 +21,10 @@ var CommentContext = createContext({
21
21
  resolved: false
22
22
  });
23
23
  var useCommentContext = () => useContext(CommentContext);
24
- var Comment = forwardRef(function Comment2({
24
+ var flattenReplies = (children) => React.Children.toArray(children).flatMap(
25
+ (child) => isValidElement(child) && child.type === React.Fragment ? flattenReplies(child.props.children) : [child]
26
+ );
27
+ function CommentComponent({
25
28
  author = "Author",
26
29
  timestamp,
27
30
  avatar,
@@ -39,10 +42,7 @@ var Comment = forwardRef(function Comment2({
39
42
  }, ref) {
40
43
  const childArray = React.Children.toArray(children);
41
44
  const actionsSlot = childArray.find(
42
- (child) => {
43
- var _a;
44
- return isValidElement(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
45
- }
45
+ (child) => isValidElement(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
46
46
  );
47
47
  const bodyNodes = childArray.filter((child) => child !== actionsSlot);
48
48
  const bodyContent = body ?? bodyNodes;
@@ -50,9 +50,11 @@ var Comment = forwardRef(function Comment2({
50
50
  const isResolved = resolved || variant === "resolved";
51
51
  const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
52
52
  const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
53
- const shouldRenderAvatar = Boolean(avatar || avatarProps || resolvedAvatarName);
53
+ const shouldRenderAvatar = Boolean(
54
+ avatar || avatarProps || resolvedAvatarName
55
+ );
54
56
  const replyItems = useMemo(
55
- () => React.Children.toArray(replies).filter(
57
+ () => flattenReplies(replies).filter(
56
58
  (reply) => reply !== null && reply !== void 0 && reply !== false
57
59
  ),
58
60
  [replies]
@@ -64,49 +66,55 @@ var Comment = forwardRef(function Comment2({
64
66
  setShowAllReplies(!shouldCollapseReplies);
65
67
  }, [shouldCollapseReplies]);
66
68
  const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
67
- return /* @__PURE__ */ jsx(CommentContext.Provider, { value: { editable, onResolve, resolved: isResolved }, children: /* @__PURE__ */ jsxs(
68
- "div",
69
+ return /* @__PURE__ */ jsx(
70
+ CommentContext.Provider,
69
71
  {
70
- ref,
71
- className: cx(
72
- "flex gap-12 rounded-default border p-12",
73
- resolvedVariant,
74
- className
75
- ),
76
- "data-variant": variant,
77
- ...props,
78
- children: [
79
- shouldRenderAvatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx(Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
80
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
81
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
82
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
83
- timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-fg-subtle", children: timestamp })
84
- ] }),
85
- hasBodyContent && !isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
86
- isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
87
- actionsSlot ? /* @__PURE__ */ jsx("div", { className: "mt-8", children: actionsSlot }) : null,
88
- replies && !isResolved ? /* @__PURE__ */ jsxs("div", { className: "mt-10 border-l border-border-minimal pl-12", children: [
89
- shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ jsxs(
90
- "button",
91
- {
92
- type: "button",
93
- className: "mb-8 text-xs font-semibold text-fg-info-base",
94
- onClick: () => setShowAllReplies(true),
95
- "aria-expanded": showAllReplies,
96
- children: [
97
- "Show all ",
98
- replyCount,
99
- " replies"
100
- ]
101
- }
102
- ),
103
- visibleReplies
104
- ] }) : null
105
- ] })
106
- ]
72
+ value: { editable, onResolve, resolved: isResolved },
73
+ children: /* @__PURE__ */ jsxs(
74
+ "div",
75
+ {
76
+ ref,
77
+ className: cx(
78
+ "flex gap-12 rounded-default border p-12",
79
+ resolvedVariant,
80
+ className
81
+ ),
82
+ "data-variant": variant,
83
+ ...props,
84
+ children: [
85
+ shouldRenderAvatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx(Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
86
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
87
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
88
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
89
+ timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-fg-subtle", children: timestamp })
90
+ ] }),
91
+ hasBodyContent && !isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
92
+ isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
93
+ actionsSlot ? /* @__PURE__ */ jsx("div", { className: "mt-8", children: actionsSlot }) : null,
94
+ replies && !isResolved ? /* @__PURE__ */ jsxs("div", { className: "mt-[40px] border-l border-border-minimal pl-[20px]", children: [
95
+ shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ jsxs(
96
+ "button",
97
+ {
98
+ type: "button",
99
+ className: "mb-8 text-xs font-semibold text-fg-info-base",
100
+ onClick: () => setShowAllReplies(true),
101
+ "aria-expanded": showAllReplies,
102
+ children: [
103
+ "Show all ",
104
+ replyCount,
105
+ " replies"
106
+ ]
107
+ }
108
+ ),
109
+ visibleReplies
110
+ ] }) : null
111
+ ] })
112
+ ]
113
+ }
114
+ )
107
115
  }
108
- ) });
109
- });
116
+ );
117
+ }
110
118
  var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ jsx(
111
119
  "div",
112
120
  {
@@ -166,10 +174,13 @@ var CommentResolve = ({
166
174
  }
167
175
  );
168
176
  };
169
- Comment.Actions = CommentActions;
170
- Comment.Reply = CommentReply;
171
- Comment.Edit = CommentEdit;
172
- Comment.Resolve = CommentResolve;
177
+ var Comment = Object.assign(forwardRef(CommentComponent), {
178
+ Actions: CommentActions,
179
+ Reply: CommentReply,
180
+ Edit: CommentEdit,
181
+ Resolve: CommentResolve
182
+ });
183
+ Comment.displayName = "Comment";
173
184
  CommentActions.displayName = "Comment.Actions";
174
185
  CommentReply.displayName = "Comment.Reply";
175
186
  CommentEdit.displayName = "Comment.Edit";
@@ -182,7 +193,9 @@ import React3, {
182
193
  useRef,
183
194
  createContext as createContext2,
184
195
  useContext as useContext2,
185
- useEffect as useEffect2
196
+ useEffect as useEffect2,
197
+ useImperativeHandle,
198
+ useState as useState2
186
199
  } from "react";
187
200
  import { cx as cx2 } from "@edux-design/utils";
188
201
  import { Avatar as Avatar2 } from "@edux-design/avatars";
@@ -198,9 +211,8 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
198
211
  import { Mention } from "@edux-design/mentions";
199
212
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
200
213
  var MentionNodeView = ({ node }) => {
201
- var _a, _b;
202
- const handle = ((_a = node.attrs) == null ? void 0 : _a.handle) ?? "";
203
- const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
214
+ const handle = node.attrs?.handle ?? "";
215
+ const label = node.attrs?.label ?? "";
204
216
  return /* @__PURE__ */ jsx2(
205
217
  NodeViewWrapper,
206
218
  {
@@ -243,6 +255,11 @@ var CommentMentionExtension = Node.create({
243
255
  label
244
256
  ];
245
257
  },
258
+ renderText({ node }) {
259
+ const handle = node.attrs.handle || "";
260
+ const label = node.attrs.label || "";
261
+ return `@${handle || label || "mention"}`;
262
+ },
246
263
  addNodeView() {
247
264
  return ReactNodeViewRenderer(MentionNodeView);
248
265
  }
@@ -260,87 +277,237 @@ var useCommentComposer = () => {
260
277
  }
261
278
  return context;
262
279
  };
263
- var CommentComposer = forwardRef2(function CommentComposer2({
280
+ function readValue(editor) {
281
+ const mentions = [];
282
+ editor.state.doc.descendants((node) => {
283
+ if (node.type.name === "commentMention") {
284
+ mentions.push({
285
+ id: node.attrs.id ?? null,
286
+ handle: node.attrs.handle ?? "",
287
+ label: node.attrs.label ?? ""
288
+ });
289
+ }
290
+ });
291
+ const legacyText = editor.getText();
292
+ const text = editor.getText({
293
+ blockSeparator: "\n",
294
+ textSerializers: {
295
+ commentMention: ({ node }) => {
296
+ const { handle = "", label = "" } = node.attrs;
297
+ return label ? `@${handle} \xB7 ${label}` : `@${handle}`;
298
+ }
299
+ }
300
+ }).replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").replace(/\r?\n/g, " ");
301
+ return {
302
+ text,
303
+ legacyText,
304
+ html: editor.getHTML(),
305
+ json: editor.getJSON(),
306
+ mentions,
307
+ isEmpty: !text.trim()
308
+ };
309
+ }
310
+ function stringContent(value, format) {
311
+ if (format !== "text") return value;
312
+ return {
313
+ type: "doc",
314
+ content: value.split(/\r?\n/).map((line) => ({
315
+ type: "paragraph",
316
+ ...line ? { content: [{ type: "text", text: line }] } : {}
317
+ }))
318
+ };
319
+ }
320
+ function CommentComposerComponent({
264
321
  value,
322
+ valueFormat = "html",
323
+ richValue,
265
324
  onChange,
325
+ onValueChange,
266
326
  onSubmit,
267
327
  onCancel,
328
+ composerRef,
329
+ editorLabel = "Comment",
330
+ editorAttributes: suppliedEditorAttributes,
331
+ autoFocus = false,
332
+ externalSubmit = false,
333
+ submitOnModEnter = false,
268
334
  placeholder = "Write a comment...",
269
335
  rows = 4,
270
336
  disabled = false,
271
337
  isSubmitting = false,
272
338
  submitLabel = "Post",
339
+ submittingLabel = "Posting...",
273
340
  cancelLabel = "Cancel",
274
341
  avatar,
275
342
  avatarProps,
276
343
  actions,
277
344
  className,
278
345
  children,
346
+ onKeyDown,
347
+ onKeyDownCapture,
279
348
  ...props
280
349
  }, ref) {
281
- const hasValue = value === void 0 ? true : Boolean(value && value.trim().length);
282
- const isSubmitDisabled = disabled || isSubmitting || !hasValue;
350
+ const containerRef = useRef(null);
351
+ const formRef = useRef(null);
352
+ const pendingFocusRef = useRef(null);
353
+ const handledShortcutEvents = useRef(/* @__PURE__ */ new WeakSet());
354
+ const callbacks = useRef({ onChange, onValueChange });
355
+ callbacks.current = { onChange, onValueChange };
356
+ const pendingEchoesRef = useRef([]);
357
+ const controlledRef = useRef(false);
358
+ controlledRef.current = value !== void 0 || richValue !== void 0;
359
+ const placeholderRef = useRef(placeholder);
360
+ placeholderRef.current = placeholder;
361
+ const [hasValue, setHasValue] = useState2(Boolean(richValue || value?.trim()));
362
+ const locked = disabled || isSubmitting;
363
+ const editorAttributes = useMemo2(
364
+ () => ({
365
+ ...suppliedEditorAttributes,
366
+ "data-comment-editor": "true",
367
+ role: "textbox",
368
+ "aria-label": editorLabel,
369
+ "aria-multiline": "true",
370
+ "aria-disabled": String(locked),
371
+ class: cx2(
372
+ "w-full bg-bg-base text-fg-base border-2 rounded-md border-border-base",
373
+ "outline-none transition-all duration-300 ease-in-out motion-reduce:transition-none",
374
+ "pl-[20px] py-12 font-normal text-base leading-relaxed overflow-y-auto",
375
+ "focus:shadow-focus focus-visible:shadow-focus",
376
+ locked && "bg-bg-subtle border-border-subtle text-fg-inactive"
377
+ ),
378
+ style: `height: ${Math.max(1, rows) * 30}px`
379
+ }),
380
+ [editorLabel, locked, rows, suppliedEditorAttributes]
381
+ );
382
+ const isSubmitDisabled = locked || !hasValue;
283
383
  const editor = useEditor({
384
+ immediatelyRender: false,
284
385
  extensions: [
285
386
  StarterKit,
286
387
  CommentMentionExtension,
287
- Placeholder.configure({
288
- placeholder
289
- })
388
+ Placeholder.configure({ placeholder: () => placeholderRef.current })
290
389
  ],
291
- content: value ?? "",
292
- editable: !disabled,
293
- editorProps: {
294
- attributes: {
295
- class: cx2(
296
- "w-full bg-white border-2 rounded-md border-border-base",
297
- "outline-none transition-all duration-300 ease-in-out",
298
- "pl-5 py-3 font-normal text-base leading-relaxed",
299
- "h-[120px] overflow-y-auto",
300
- "focus:shadow-focus focus-visible:shadow-focus",
301
- disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
302
- )
303
- }
304
- },
390
+ content: richValue ?? stringContent(value ?? "", valueFormat),
391
+ editable: !locked,
392
+ editorProps: { attributes: editorAttributes },
305
393
  onUpdate: ({ editor: editor2 }) => {
306
- onChange == null ? void 0 : onChange({ target: { value: editor2.getText() } });
394
+ const next = readValue(editor2);
395
+ if (controlledRef.current && (callbacks.current.onChange || callbacks.current.onValueChange)) {
396
+ pendingEchoesRef.current.push(next);
397
+ if (pendingEchoesRef.current.length > 100)
398
+ pendingEchoesRef.current.shift();
399
+ }
400
+ setHasValue(!next.isEmpty);
401
+ callbacks.current.onChange?.({ target: { value: next.legacyText } });
402
+ callbacks.current.onValueChange?.(next);
307
403
  }
308
404
  });
309
- const containerRef = useRef(null);
405
+ useImperativeHandle(ref, () => containerRef.current, []);
406
+ useImperativeHandle(
407
+ composerRef,
408
+ () => ({
409
+ focus: (position = "end") => {
410
+ if (locked) return false;
411
+ if (!editor) {
412
+ pendingFocusRef.current = position;
413
+ return true;
414
+ }
415
+ return editor.commands.focus(position);
416
+ },
417
+ clear: () => !locked && Boolean(editor?.commands.clearContent(true)),
418
+ getValue: () => editor ? readValue(editor) : null,
419
+ submit: () => {
420
+ if (!locked) formRef.current?.requestSubmit();
421
+ }
422
+ }),
423
+ [editor, locked]
424
+ );
310
425
  useEffect2(() => {
311
- if (typeof ref === "function") {
312
- ref(containerRef.current);
313
- } else if (ref) {
314
- ref.current = containerRef.current;
315
- }
316
- }, [ref]);
426
+ if (!editor) return;
427
+ editor.setEditable(!locked, false);
428
+ editor.setOptions({
429
+ editorProps: {
430
+ ...editor.options.editorProps,
431
+ attributes: editorAttributes
432
+ }
433
+ });
434
+ }, [editor, locked, editorAttributes, placeholder]);
317
435
  useEffect2(() => {
318
436
  if (!editor) return;
319
- if (value === "") {
320
- editor.commands.clearContent();
437
+ const next = richValue ?? value;
438
+ if (next === void 0) return;
439
+ const current = readValue(editor);
440
+ const echoIndex = pendingEchoesRef.current.findIndex(
441
+ (snapshot) => richValue !== void 0 ? JSON.stringify(snapshot.json) === JSON.stringify(richValue) : snapshot.text === value || snapshot.legacyText === value
442
+ );
443
+ if (echoIndex !== -1) {
444
+ pendingEchoesRef.current.splice(0, echoIndex + 1);
445
+ setHasValue(!current.isEmpty);
446
+ return;
321
447
  }
322
- }, [editor, value]);
448
+ pendingEchoesRef.current = [];
449
+ const equal = richValue !== void 0 ? JSON.stringify(current.json) === JSON.stringify(richValue) : current.text === value || current.legacyText === value;
450
+ if (!equal)
451
+ editor.commands.setContent(
452
+ richValue ?? stringContent(value, valueFormat),
453
+ { emitUpdate: false }
454
+ );
455
+ setHasValue(!readValue(editor).isEmpty);
456
+ }, [editor, value, richValue, valueFormat]);
457
+ useEffect2(() => {
458
+ if (editor) setHasValue(!readValue(editor).isEmpty);
459
+ }, [editor]);
460
+ useEffect2(() => {
461
+ if (!editor || locked) return;
462
+ if (!autoFocus && pendingFocusRef.current === null) return;
463
+ const position = pendingFocusRef.current ?? "end";
464
+ pendingFocusRef.current = null;
465
+ const frame = requestAnimationFrame(() => {
466
+ if (!editor.isDestroyed && editor.isEditable)
467
+ editor.commands.focus(position);
468
+ });
469
+ return () => cancelAnimationFrame(frame);
470
+ }, [editor, autoFocus, locked]);
323
471
  const contextValue = useMemo2(
324
472
  () => ({
325
473
  editor,
326
474
  placeholder,
475
+ editable: !locked,
327
476
  editorContainerRef: containerRef
328
477
  }),
329
- [editor, placeholder]
478
+ [editor, placeholder, locked]
330
479
  );
331
480
  return /* @__PURE__ */ jsx3(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
332
481
  "form",
333
482
  {
483
+ ...props,
484
+ ref: formRef,
485
+ "data-comment-surface": "composer",
486
+ "aria-busy": isSubmitting || void 0,
334
487
  className: cx2(
335
488
  "relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
336
489
  className
337
490
  ),
491
+ onKeyDown: (event) => {
492
+ if (!handledShortcutEvents.current.has(event.nativeEvent))
493
+ onKeyDown?.(event);
494
+ },
495
+ onKeyDownCapture: (event) => {
496
+ onKeyDownCapture?.(event);
497
+ if (!event.defaultPrevented && submitOnModEnter && !event.nativeEvent.isComposing && (event.ctrlKey || event.metaKey) && event.key === "Enter") {
498
+ handledShortcutEvents.current.add(event.nativeEvent);
499
+ onKeyDown?.(event);
500
+ if (event.defaultPrevented) return;
501
+ event.preventDefault();
502
+ if (!locked && editor && !readValue(editor).isEmpty)
503
+ formRef.current?.requestSubmit();
504
+ }
505
+ },
338
506
  onSubmit: (event) => {
339
507
  event.preventDefault();
340
- if (isSubmitDisabled) return;
341
- onSubmit == null ? void 0 : onSubmit(event);
508
+ if (!locked && editor && !readValue(editor).isEmpty)
509
+ onSubmit?.(event);
342
510
  },
343
- ...props,
344
511
  children: [
345
512
  (avatar || avatarProps) && /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx3(Avatar2, { size: "small", ...avatarProps }) }),
346
513
  /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
@@ -355,11 +522,11 @@ var CommentComposer = forwardRef2(function CommentComposer2({
355
522
  size: "small",
356
523
  isSecondary: true,
357
524
  onClick: onCancel,
358
- disabled: disabled || isSubmitting,
525
+ disabled: locked,
359
526
  children: cancelLabel
360
527
  }
361
528
  ),
362
- /* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? "Posting..." : submitLabel })
529
+ !externalSubmit && /* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? submittingLabel : submitLabel })
363
530
  ] })
364
531
  ] })
365
532
  ] }),
@@ -367,29 +534,38 @@ var CommentComposer = forwardRef2(function CommentComposer2({
367
534
  ]
368
535
  }
369
536
  ) });
370
- });
537
+ }
538
+ var CommentComposer = forwardRef2(CommentComposerComponent);
371
539
 
372
540
  // src/elements/CommentMention.jsx
373
- import React4, { useMemo as useMemo3, useState as useState2, useCallback, useEffect as useEffect3, useRef as useRef2 } from "react";
541
+ import React4, {
542
+ useMemo as useMemo3,
543
+ useState as useState3,
544
+ useCallback,
545
+ useEffect as useEffect3,
546
+ useRef as useRef2
547
+ } from "react";
374
548
  import { Menu, MenuList, MenuItem } from "@edux-design/menus";
375
549
  import { Avatar as Avatar3 } from "@edux-design/avatars";
376
550
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
377
- var CommentMention = ({
378
- peopleList = [],
379
- onQueryChange
380
- }) => {
381
- const { editor, editorContainerRef } = useCommentComposer();
382
- const [menuOpen, setMenuOpen] = useState2(false);
383
- const [query, setQuery] = useState2("");
384
- const [range, setRange] = useState2(null);
385
- const [anchorStyle, setAnchorStyle] = useState2({ left: 0, top: 0 });
551
+ var CommentMention = ({ peopleList = [], onQueryChange }) => {
552
+ const { editor, editorContainerRef, editable = true } = useCommentComposer();
553
+ const [menuOpen, setMenuOpen] = useState3(false);
554
+ const [query, setQuery] = useState3("");
555
+ const [anchorStyle, setAnchorStyle] = useState3({ left: 0, top: 0 });
386
556
  const menuListRef = useRef2(null);
387
557
  const rangeRef = useRef2(null);
388
558
  const lastQueryRef = useRef2("");
389
- const peopleByHandle = useMemo3(
390
- () => new Map(peopleList.map((person) => [person.handle.toLowerCase(), person])),
391
- [peopleList]
392
- );
559
+ const dismissedRangeRef = useRef2(null);
560
+ const focusFrameRef = useRef2(null);
561
+ const focusTimeoutRef = useRef2(null);
562
+ const dismissMenu = useCallback(() => {
563
+ const activeRange = rangeRef.current;
564
+ dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
565
+ setMenuOpen(false);
566
+ if (editor && !editor.isDestroyed && editor.isEditable !== false)
567
+ editor.view.focus();
568
+ }, [editor]);
393
569
  const filteredPeople = useMemo3(() => {
394
570
  if (!query) return peopleList;
395
571
  const lowered = query.toLowerCase();
@@ -398,14 +574,15 @@ var CommentMention = ({
398
574
  );
399
575
  }, [peopleList, query]);
400
576
  const updateMenuState = useCallback(() => {
401
- var _a, _b;
402
- if (!editor) return;
403
- const { from } = editor.state.selection;
577
+ if (!editor || !editable || editor.isEditable === false) {
578
+ setMenuOpen(false);
579
+ rangeRef.current = null;
580
+ return;
581
+ }
404
582
  const { parent, parentOffset, pos } = editor.state.selection.$from;
405
583
  if (!parent.isTextblock) {
406
584
  setMenuOpen(false);
407
585
  setQuery("");
408
- setRange(null);
409
586
  return;
410
587
  }
411
588
  const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
@@ -417,13 +594,12 @@ var CommentMention = ({
417
594
  onQueryChange(handleQuery);
418
595
  }
419
596
  setQuery(handleQuery);
420
- setMenuOpen(true);
421
597
  const fromPos = pos - match[0].length;
422
598
  const nextRange = { from: fromPos, to: pos };
423
- setRange(nextRange);
599
+ setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
424
600
  rangeRef.current = nextRange;
425
601
  const coords = editor.view.coordsAtPos(fromPos);
426
- const containerRect = (_b = (_a = editorContainerRef == null ? void 0 : editorContainerRef.current) == null ? void 0 : _a.getBoundingClientRect) == null ? void 0 : _b.call(_a);
602
+ const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
427
603
  if (containerRect) {
428
604
  setAnchorStyle({
429
605
  left: coords.left - containerRect.left + 12,
@@ -434,16 +610,16 @@ var CommentMention = ({
434
610
  }
435
611
  setMenuOpen(false);
436
612
  setQuery("");
437
- setRange(null);
438
613
  rangeRef.current = null;
439
614
  if (onQueryChange && lastQueryRef.current !== "") {
440
615
  lastQueryRef.current = "";
441
616
  onQueryChange("");
442
617
  }
443
- }, [editor, editorContainerRef, onQueryChange]);
618
+ }, [editor, editorContainerRef, onQueryChange, editable]);
444
619
  const handleSelectPerson = useCallback(
445
620
  (person) => {
446
- if (!editor || !rangeRef.current) return;
621
+ if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
622
+ return;
447
623
  const selectionRange = rangeRef.current;
448
624
  editor.commands.focus();
449
625
  editor.commands.setTextSelection(selectionRange);
@@ -463,21 +639,34 @@ var CommentMention = ({
463
639
  { updateSelection: false }
464
640
  );
465
641
  const nextPos = selectionRange.from + 2;
466
- requestAnimationFrame(() => {
642
+ editor.view.focus();
643
+ cancelAnimationFrame(focusFrameRef.current);
644
+ clearTimeout(focusTimeoutRef.current);
645
+ focusFrameRef.current = requestAnimationFrame(() => {
646
+ if (editor.isDestroyed || editor.isEditable === false) return;
467
647
  editor.commands.setTextSelection(nextPos);
468
648
  editor.view.focus();
469
- setTimeout(() => editor.view.focus(), 0);
649
+ focusTimeoutRef.current = setTimeout(() => {
650
+ if (!editor.isDestroyed && editor.isEditable !== false)
651
+ editor.view.focus();
652
+ }, 0);
470
653
  });
471
654
  setMenuOpen(false);
472
655
  setQuery("");
473
- setRange(null);
474
656
  rangeRef.current = null;
475
657
  if (onQueryChange && lastQueryRef.current !== "") {
476
658
  lastQueryRef.current = "";
477
659
  onQueryChange("");
478
660
  }
479
661
  },
480
- [editor, onQueryChange]
662
+ [editor, onQueryChange, editable]
663
+ );
664
+ useEffect3(
665
+ () => () => {
666
+ cancelAnimationFrame(focusFrameRef.current);
667
+ clearTimeout(focusTimeoutRef.current);
668
+ },
669
+ [editor]
481
670
  );
482
671
  useEffect3(() => {
483
672
  if (!editor) return;
@@ -490,25 +679,30 @@ var CommentMention = ({
490
679
  };
491
680
  }, [editor, updateMenuState]);
492
681
  useEffect3(() => {
493
- var _a;
494
682
  if (!editor) return;
495
683
  const handleKeyDown = (event) => {
496
- var _a2;
497
684
  if (!menuOpen) return;
685
+ if (event.key === "Escape") {
686
+ event.preventDefault();
687
+ event.stopPropagation();
688
+ dismissMenu();
689
+ return;
690
+ }
498
691
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
499
692
  event.preventDefault();
693
+ event.stopPropagation();
500
694
  const list = menuListRef.current;
501
- const items = list == null ? void 0 : list.querySelectorAll("[data-menu-item]");
502
- if (!list || !(items == null ? void 0 : items.length)) return;
695
+ const items = list?.querySelectorAll("[data-menu-item]");
696
+ if (!list || !items?.length) return;
503
697
  list.focus();
504
698
  const direction = event.key === "ArrowDown" ? 1 : -1;
505
699
  const targetIndex = direction === 1 ? 0 : items.length - 1;
506
- (_a2 = items[targetIndex]) == null ? void 0 : _a2.focus();
700
+ items[targetIndex]?.focus();
507
701
  };
508
- const dom = (_a = editor.view) == null ? void 0 : _a.dom;
509
- dom == null ? void 0 : dom.addEventListener("keydown", handleKeyDown);
510
- return () => dom == null ? void 0 : dom.removeEventListener("keydown", handleKeyDown);
511
- }, [editor, menuOpen]);
702
+ const dom = editor.view?.dom;
703
+ dom?.addEventListener("keydown", handleKeyDown, true);
704
+ return () => dom?.removeEventListener("keydown", handleKeyDown, true);
705
+ }, [editor, menuOpen, dismissMenu]);
512
706
  const menuContent = useMemo3(
513
707
  () => /* @__PURE__ */ jsx4(
514
708
  Menu,
@@ -520,6 +714,7 @@ var CommentMention = ({
520
714
  {
521
715
  type: "button",
522
716
  "aria-hidden": true,
717
+ tabIndex: -1,
523
718
  className: "absolute h-1 w-1 opacity-0",
524
719
  style: {
525
720
  left: `${anchorStyle.left}px`,
@@ -539,32 +734,46 @@ var CommentMention = ({
539
734
  collisionPadding: 12,
540
735
  onCloseAutoFocus: (event) => event.preventDefault()
541
736
  },
542
- children: /* @__PURE__ */ jsx4(MenuList, { ref: menuListRef, className: "min-w-[220px]", children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
543
- MenuItem,
737
+ children: /* @__PURE__ */ jsx4(
738
+ MenuList,
544
739
  {
545
- onMouseDown: (event) => {
546
- event.preventDefault();
547
- event.stopPropagation();
548
- handleSelectPerson(person);
740
+ ref: menuListRef,
741
+ className: "min-w-[220px]",
742
+ onKeyDownCapture: (event) => {
743
+ if (event.key === "Escape") {
744
+ event.preventDefault();
745
+ event.stopPropagation();
746
+ dismissMenu();
747
+ }
549
748
  },
550
- onClick: () => handleSelectPerson(person),
551
- onPointerDown: (event) => event.preventDefault(),
552
- children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-8", children: [
553
- /* @__PURE__ */ jsx4(Avatar3, { size: "small", ...person.avatarProps }),
554
- /* @__PURE__ */ jsxs4("span", { className: "flex flex-col", children: [
555
- /* @__PURE__ */ jsx4("span", { className: "text-sm text-fg-base", children: person.name }),
556
- /* @__PURE__ */ jsxs4("span", { className: "text-xs text-fg-subtle", children: [
557
- "@",
558
- person.handle
749
+ children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
750
+ MenuItem,
751
+ {
752
+ onMouseDown: (event) => {
753
+ event.preventDefault();
754
+ event.stopPropagation();
755
+ handleSelectPerson(person);
756
+ },
757
+ onClick: () => handleSelectPerson(person),
758
+ onPointerDown: (event) => event.preventDefault(),
759
+ children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-8", children: [
760
+ /* @__PURE__ */ jsx4(Avatar3, { size: "small", ...person.avatarProps }),
761
+ /* @__PURE__ */ jsxs4("span", { className: "flex flex-col", children: [
762
+ /* @__PURE__ */ jsx4("span", { className: "text-sm text-fg-base", children: person.name }),
763
+ /* @__PURE__ */ jsxs4("span", { className: "text-xs text-fg-subtle", children: [
764
+ "@",
765
+ person.handle
766
+ ] })
767
+ ] })
559
768
  ] })
560
- ] })
561
- ] })
562
- },
563
- person.id
564
- )) })
769
+ },
770
+ person.id
771
+ ))
772
+ }
773
+ )
565
774
  }
566
775
  ),
567
- [filteredPeople, handleSelectPerson, menuOpen]
776
+ [filteredPeople, handleSelectPerson, menuOpen, anchorStyle, dismissMenu]
568
777
  );
569
778
  if (!editor) return null;
570
779
  return menuContent;