@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.js CHANGED
@@ -50,7 +50,10 @@ var CommentContext = (0, import_react.createContext)({
50
50
  resolved: false
51
51
  });
52
52
  var useCommentContext = () => (0, import_react.useContext)(CommentContext);
53
- var Comment = (0, import_react.forwardRef)(function Comment2({
53
+ var flattenReplies = (children) => import_react.default.Children.toArray(children).flatMap(
54
+ (child) => (0, import_react.isValidElement)(child) && child.type === import_react.default.Fragment ? flattenReplies(child.props.children) : [child]
55
+ );
56
+ function CommentComponent({
54
57
  author = "Author",
55
58
  timestamp,
56
59
  avatar,
@@ -68,10 +71,7 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
68
71
  }, ref) {
69
72
  const childArray = import_react.default.Children.toArray(children);
70
73
  const actionsSlot = childArray.find(
71
- (child) => {
72
- var _a;
73
- return (0, import_react.isValidElement)(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
74
- }
74
+ (child) => (0, import_react.isValidElement)(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
75
75
  );
76
76
  const bodyNodes = childArray.filter((child) => child !== actionsSlot);
77
77
  const bodyContent = body ?? bodyNodes;
@@ -79,9 +79,11 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
79
79
  const isResolved = resolved || variant === "resolved";
80
80
  const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
81
81
  const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
82
- const shouldRenderAvatar = Boolean(avatar || avatarProps || resolvedAvatarName);
82
+ const shouldRenderAvatar = Boolean(
83
+ avatar || avatarProps || resolvedAvatarName
84
+ );
83
85
  const replyItems = (0, import_react.useMemo)(
84
- () => import_react.default.Children.toArray(replies).filter(
86
+ () => flattenReplies(replies).filter(
85
87
  (reply) => reply !== null && reply !== void 0 && reply !== false
86
88
  ),
87
89
  [replies]
@@ -93,49 +95,55 @@ var Comment = (0, import_react.forwardRef)(function Comment2({
93
95
  setShowAllReplies(!shouldCollapseReplies);
94
96
  }, [shouldCollapseReplies]);
95
97
  const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
96
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CommentContext.Provider, { value: { editable, onResolve, resolved: isResolved }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
97
- "div",
98
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
99
+ CommentContext.Provider,
98
100
  {
99
- ref,
100
- className: (0, import_utils.cx)(
101
- "flex gap-12 rounded-default border p-12",
102
- resolvedVariant,
103
- className
104
- ),
105
- "data-variant": variant,
106
- ...props,
107
- children: [
108
- shouldRenderAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_avatars.Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
109
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
110
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-6", children: [
111
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
112
- timestamp && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-fg-subtle", children: timestamp })
113
- ] }),
114
- hasBodyContent && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
115
- isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
116
- actionsSlot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-8", children: actionsSlot }) : null,
117
- replies && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-10 border-l border-border-minimal pl-12", children: [
118
- shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
119
- "button",
120
- {
121
- type: "button",
122
- className: "mb-8 text-xs font-semibold text-fg-info-base",
123
- onClick: () => setShowAllReplies(true),
124
- "aria-expanded": showAllReplies,
125
- children: [
126
- "Show all ",
127
- replyCount,
128
- " replies"
129
- ]
130
- }
131
- ),
132
- visibleReplies
133
- ] }) : null
134
- ] })
135
- ]
101
+ value: { editable, onResolve, resolved: isResolved },
102
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
103
+ "div",
104
+ {
105
+ ref,
106
+ className: (0, import_utils.cx)(
107
+ "flex gap-12 rounded-default border p-12",
108
+ resolvedVariant,
109
+ className
110
+ ),
111
+ "data-variant": variant,
112
+ ...props,
113
+ children: [
114
+ shouldRenderAvatar && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_avatars.Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-6", children: [
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
118
+ timestamp && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-fg-subtle", children: timestamp })
119
+ ] }),
120
+ hasBodyContent && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
121
+ isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
122
+ actionsSlot ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-8", children: actionsSlot }) : null,
123
+ replies && !isResolved ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-[40px] border-l border-border-minimal pl-[20px]", children: [
124
+ shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ className: "mb-8 text-xs font-semibold text-fg-info-base",
129
+ onClick: () => setShowAllReplies(true),
130
+ "aria-expanded": showAllReplies,
131
+ children: [
132
+ "Show all ",
133
+ replyCount,
134
+ " replies"
135
+ ]
136
+ }
137
+ ),
138
+ visibleReplies
139
+ ] }) : null
140
+ ] })
141
+ ]
142
+ }
143
+ )
136
144
  }
137
- ) });
138
- });
145
+ );
146
+ }
139
147
  var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
140
148
  "div",
141
149
  {
@@ -195,10 +203,13 @@ var CommentResolve = ({
195
203
  }
196
204
  );
197
205
  };
198
- Comment.Actions = CommentActions;
199
- Comment.Reply = CommentReply;
200
- Comment.Edit = CommentEdit;
201
- Comment.Resolve = CommentResolve;
206
+ var Comment = Object.assign((0, import_react.forwardRef)(CommentComponent), {
207
+ Actions: CommentActions,
208
+ Reply: CommentReply,
209
+ Edit: CommentEdit,
210
+ Resolve: CommentResolve
211
+ });
212
+ Comment.displayName = "Comment";
202
213
  CommentActions.displayName = "Comment.Actions";
203
214
  CommentReply.displayName = "Comment.Reply";
204
215
  CommentEdit.displayName = "Comment.Edit";
@@ -220,9 +231,8 @@ var import_react3 = require("@tiptap/react");
220
231
  var import_mentions = require("@edux-design/mentions");
221
232
  var import_jsx_runtime2 = require("react/jsx-runtime");
222
233
  var MentionNodeView = ({ node }) => {
223
- var _a, _b;
224
- const handle = ((_a = node.attrs) == null ? void 0 : _a.handle) ?? "";
225
- const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
234
+ const handle = node.attrs?.handle ?? "";
235
+ const label = node.attrs?.label ?? "";
226
236
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
227
237
  import_react3.NodeViewWrapper,
228
238
  {
@@ -265,6 +275,11 @@ var CommentMentionExtension = import_core.Node.create({
265
275
  label
266
276
  ];
267
277
  },
278
+ renderText({ node }) {
279
+ const handle = node.attrs.handle || "";
280
+ const label = node.attrs.label || "";
281
+ return `@${handle || label || "mention"}`;
282
+ },
268
283
  addNodeView() {
269
284
  return (0, import_react3.ReactNodeViewRenderer)(MentionNodeView);
270
285
  }
@@ -282,87 +297,237 @@ var useCommentComposer = () => {
282
297
  }
283
298
  return context;
284
299
  };
285
- var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
300
+ function readValue(editor) {
301
+ const mentions = [];
302
+ editor.state.doc.descendants((node) => {
303
+ if (node.type.name === "commentMention") {
304
+ mentions.push({
305
+ id: node.attrs.id ?? null,
306
+ handle: node.attrs.handle ?? "",
307
+ label: node.attrs.label ?? ""
308
+ });
309
+ }
310
+ });
311
+ const legacyText = editor.getText();
312
+ const text = editor.getText({
313
+ blockSeparator: "\n",
314
+ textSerializers: {
315
+ commentMention: ({ node }) => {
316
+ const { handle = "", label = "" } = node.attrs;
317
+ return label ? `@${handle} \xB7 ${label}` : `@${handle}`;
318
+ }
319
+ }
320
+ }).replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").replace(/\r?\n/g, " ");
321
+ return {
322
+ text,
323
+ legacyText,
324
+ html: editor.getHTML(),
325
+ json: editor.getJSON(),
326
+ mentions,
327
+ isEmpty: !text.trim()
328
+ };
329
+ }
330
+ function stringContent(value, format) {
331
+ if (format !== "text") return value;
332
+ return {
333
+ type: "doc",
334
+ content: value.split(/\r?\n/).map((line) => ({
335
+ type: "paragraph",
336
+ ...line ? { content: [{ type: "text", text: line }] } : {}
337
+ }))
338
+ };
339
+ }
340
+ function CommentComposerComponent({
286
341
  value,
342
+ valueFormat = "html",
343
+ richValue,
287
344
  onChange,
345
+ onValueChange,
288
346
  onSubmit,
289
347
  onCancel,
348
+ composerRef,
349
+ editorLabel = "Comment",
350
+ editorAttributes: suppliedEditorAttributes,
351
+ autoFocus = false,
352
+ externalSubmit = false,
353
+ submitOnModEnter = false,
290
354
  placeholder = "Write a comment...",
291
355
  rows = 4,
292
356
  disabled = false,
293
357
  isSubmitting = false,
294
358
  submitLabel = "Post",
359
+ submittingLabel = "Posting...",
295
360
  cancelLabel = "Cancel",
296
361
  avatar,
297
362
  avatarProps,
298
363
  actions,
299
364
  className,
300
365
  children,
366
+ onKeyDown,
367
+ onKeyDownCapture,
301
368
  ...props
302
369
  }, ref) {
303
- const hasValue = value === void 0 ? true : Boolean(value && value.trim().length);
304
- const isSubmitDisabled = disabled || isSubmitting || !hasValue;
370
+ const containerRef = (0, import_react4.useRef)(null);
371
+ const formRef = (0, import_react4.useRef)(null);
372
+ const pendingFocusRef = (0, import_react4.useRef)(null);
373
+ const handledShortcutEvents = (0, import_react4.useRef)(/* @__PURE__ */ new WeakSet());
374
+ const callbacks = (0, import_react4.useRef)({ onChange, onValueChange });
375
+ callbacks.current = { onChange, onValueChange };
376
+ const pendingEchoesRef = (0, import_react4.useRef)([]);
377
+ const controlledRef = (0, import_react4.useRef)(false);
378
+ controlledRef.current = value !== void 0 || richValue !== void 0;
379
+ const placeholderRef = (0, import_react4.useRef)(placeholder);
380
+ placeholderRef.current = placeholder;
381
+ const [hasValue, setHasValue] = (0, import_react4.useState)(Boolean(richValue || value?.trim()));
382
+ const locked = disabled || isSubmitting;
383
+ const editorAttributes = (0, import_react4.useMemo)(
384
+ () => ({
385
+ ...suppliedEditorAttributes,
386
+ "data-comment-editor": "true",
387
+ role: "textbox",
388
+ "aria-label": editorLabel,
389
+ "aria-multiline": "true",
390
+ "aria-disabled": String(locked),
391
+ class: (0, import_utils2.cx)(
392
+ "w-full bg-bg-base text-fg-base border-2 rounded-md border-border-base",
393
+ "outline-none transition-all duration-300 ease-in-out motion-reduce:transition-none",
394
+ "pl-[20px] py-12 font-normal text-base leading-relaxed overflow-y-auto",
395
+ "focus:shadow-focus focus-visible:shadow-focus",
396
+ locked && "bg-bg-subtle border-border-subtle text-fg-inactive"
397
+ ),
398
+ style: `height: ${Math.max(1, rows) * 30}px`
399
+ }),
400
+ [editorLabel, locked, rows, suppliedEditorAttributes]
401
+ );
402
+ const isSubmitDisabled = locked || !hasValue;
305
403
  const editor = (0, import_react5.useEditor)({
404
+ immediatelyRender: false,
306
405
  extensions: [
307
406
  import_starter_kit.default,
308
407
  CommentMentionExtension,
309
- import_extension_placeholder.default.configure({
310
- placeholder
311
- })
408
+ import_extension_placeholder.default.configure({ placeholder: () => placeholderRef.current })
312
409
  ],
313
- content: value ?? "",
314
- editable: !disabled,
315
- editorProps: {
316
- attributes: {
317
- class: (0, import_utils2.cx)(
318
- "w-full bg-white border-2 rounded-md border-border-base",
319
- "outline-none transition-all duration-300 ease-in-out",
320
- "pl-5 py-3 font-normal text-base leading-relaxed",
321
- "h-[120px] overflow-y-auto",
322
- "focus:shadow-focus focus-visible:shadow-focus",
323
- disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
324
- )
325
- }
326
- },
410
+ content: richValue ?? stringContent(value ?? "", valueFormat),
411
+ editable: !locked,
412
+ editorProps: { attributes: editorAttributes },
327
413
  onUpdate: ({ editor: editor2 }) => {
328
- onChange == null ? void 0 : onChange({ target: { value: editor2.getText() } });
414
+ const next = readValue(editor2);
415
+ if (controlledRef.current && (callbacks.current.onChange || callbacks.current.onValueChange)) {
416
+ pendingEchoesRef.current.push(next);
417
+ if (pendingEchoesRef.current.length > 100)
418
+ pendingEchoesRef.current.shift();
419
+ }
420
+ setHasValue(!next.isEmpty);
421
+ callbacks.current.onChange?.({ target: { value: next.legacyText } });
422
+ callbacks.current.onValueChange?.(next);
329
423
  }
330
424
  });
331
- const containerRef = (0, import_react4.useRef)(null);
425
+ (0, import_react4.useImperativeHandle)(ref, () => containerRef.current, []);
426
+ (0, import_react4.useImperativeHandle)(
427
+ composerRef,
428
+ () => ({
429
+ focus: (position = "end") => {
430
+ if (locked) return false;
431
+ if (!editor) {
432
+ pendingFocusRef.current = position;
433
+ return true;
434
+ }
435
+ return editor.commands.focus(position);
436
+ },
437
+ clear: () => !locked && Boolean(editor?.commands.clearContent(true)),
438
+ getValue: () => editor ? readValue(editor) : null,
439
+ submit: () => {
440
+ if (!locked) formRef.current?.requestSubmit();
441
+ }
442
+ }),
443
+ [editor, locked]
444
+ );
332
445
  (0, import_react4.useEffect)(() => {
333
- if (typeof ref === "function") {
334
- ref(containerRef.current);
335
- } else if (ref) {
336
- ref.current = containerRef.current;
337
- }
338
- }, [ref]);
446
+ if (!editor) return;
447
+ editor.setEditable(!locked, false);
448
+ editor.setOptions({
449
+ editorProps: {
450
+ ...editor.options.editorProps,
451
+ attributes: editorAttributes
452
+ }
453
+ });
454
+ }, [editor, locked, editorAttributes, placeholder]);
339
455
  (0, import_react4.useEffect)(() => {
340
456
  if (!editor) return;
341
- if (value === "") {
342
- editor.commands.clearContent();
457
+ const next = richValue ?? value;
458
+ if (next === void 0) return;
459
+ const current = readValue(editor);
460
+ const echoIndex = pendingEchoesRef.current.findIndex(
461
+ (snapshot) => richValue !== void 0 ? JSON.stringify(snapshot.json) === JSON.stringify(richValue) : snapshot.text === value || snapshot.legacyText === value
462
+ );
463
+ if (echoIndex !== -1) {
464
+ pendingEchoesRef.current.splice(0, echoIndex + 1);
465
+ setHasValue(!current.isEmpty);
466
+ return;
343
467
  }
344
- }, [editor, value]);
468
+ pendingEchoesRef.current = [];
469
+ const equal = richValue !== void 0 ? JSON.stringify(current.json) === JSON.stringify(richValue) : current.text === value || current.legacyText === value;
470
+ if (!equal)
471
+ editor.commands.setContent(
472
+ richValue ?? stringContent(value, valueFormat),
473
+ { emitUpdate: false }
474
+ );
475
+ setHasValue(!readValue(editor).isEmpty);
476
+ }, [editor, value, richValue, valueFormat]);
477
+ (0, import_react4.useEffect)(() => {
478
+ if (editor) setHasValue(!readValue(editor).isEmpty);
479
+ }, [editor]);
480
+ (0, import_react4.useEffect)(() => {
481
+ if (!editor || locked) return;
482
+ if (!autoFocus && pendingFocusRef.current === null) return;
483
+ const position = pendingFocusRef.current ?? "end";
484
+ pendingFocusRef.current = null;
485
+ const frame = requestAnimationFrame(() => {
486
+ if (!editor.isDestroyed && editor.isEditable)
487
+ editor.commands.focus(position);
488
+ });
489
+ return () => cancelAnimationFrame(frame);
490
+ }, [editor, autoFocus, locked]);
345
491
  const contextValue = (0, import_react4.useMemo)(
346
492
  () => ({
347
493
  editor,
348
494
  placeholder,
495
+ editable: !locked,
349
496
  editorContainerRef: containerRef
350
497
  }),
351
- [editor, placeholder]
498
+ [editor, placeholder, locked]
352
499
  );
353
500
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
354
501
  "form",
355
502
  {
503
+ ...props,
504
+ ref: formRef,
505
+ "data-comment-surface": "composer",
506
+ "aria-busy": isSubmitting || void 0,
356
507
  className: (0, import_utils2.cx)(
357
508
  "relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
358
509
  className
359
510
  ),
511
+ onKeyDown: (event) => {
512
+ if (!handledShortcutEvents.current.has(event.nativeEvent))
513
+ onKeyDown?.(event);
514
+ },
515
+ onKeyDownCapture: (event) => {
516
+ onKeyDownCapture?.(event);
517
+ if (!event.defaultPrevented && submitOnModEnter && !event.nativeEvent.isComposing && (event.ctrlKey || event.metaKey) && event.key === "Enter") {
518
+ handledShortcutEvents.current.add(event.nativeEvent);
519
+ onKeyDown?.(event);
520
+ if (event.defaultPrevented) return;
521
+ event.preventDefault();
522
+ if (!locked && editor && !readValue(editor).isEmpty)
523
+ formRef.current?.requestSubmit();
524
+ }
525
+ },
360
526
  onSubmit: (event) => {
361
527
  event.preventDefault();
362
- if (isSubmitDisabled) return;
363
- onSubmit == null ? void 0 : onSubmit(event);
528
+ if (!locked && editor && !readValue(editor).isEmpty)
529
+ onSubmit?.(event);
364
530
  },
365
- ...props,
366
531
  children: [
367
532
  (avatar || avatarProps) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_avatars2.Avatar, { size: "small", ...avatarProps }) }),
368
533
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "min-w-0 flex-1", children: [
@@ -377,11 +542,11 @@ var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
377
542
  size: "small",
378
543
  isSecondary: true,
379
544
  onClick: onCancel,
380
- disabled: disabled || isSubmitting,
545
+ disabled: locked,
381
546
  children: cancelLabel
382
547
  }
383
548
  ),
384
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_buttons.Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? "Posting..." : submitLabel })
549
+ !externalSubmit && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_buttons.Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? submittingLabel : submitLabel })
385
550
  ] })
386
551
  ] })
387
552
  ] }),
@@ -389,29 +554,32 @@ var CommentComposer = (0, import_react4.forwardRef)(function CommentComposer2({
389
554
  ]
390
555
  }
391
556
  ) });
392
- });
557
+ }
558
+ var CommentComposer = (0, import_react4.forwardRef)(CommentComposerComponent);
393
559
 
394
560
  // src/elements/CommentMention.jsx
395
561
  var import_react6 = __toESM(require("react"));
396
562
  var import_menus = require("@edux-design/menus");
397
563
  var import_avatars3 = require("@edux-design/avatars");
398
564
  var import_jsx_runtime4 = require("react/jsx-runtime");
399
- var CommentMention = ({
400
- peopleList = [],
401
- onQueryChange
402
- }) => {
403
- const { editor, editorContainerRef } = useCommentComposer();
565
+ var CommentMention = ({ peopleList = [], onQueryChange }) => {
566
+ const { editor, editorContainerRef, editable = true } = useCommentComposer();
404
567
  const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
405
568
  const [query, setQuery] = (0, import_react6.useState)("");
406
- const [range, setRange] = (0, import_react6.useState)(null);
407
569
  const [anchorStyle, setAnchorStyle] = (0, import_react6.useState)({ left: 0, top: 0 });
408
570
  const menuListRef = (0, import_react6.useRef)(null);
409
571
  const rangeRef = (0, import_react6.useRef)(null);
410
572
  const lastQueryRef = (0, import_react6.useRef)("");
411
- const peopleByHandle = (0, import_react6.useMemo)(
412
- () => new Map(peopleList.map((person) => [person.handle.toLowerCase(), person])),
413
- [peopleList]
414
- );
573
+ const dismissedRangeRef = (0, import_react6.useRef)(null);
574
+ const focusFrameRef = (0, import_react6.useRef)(null);
575
+ const focusTimeoutRef = (0, import_react6.useRef)(null);
576
+ const dismissMenu = (0, import_react6.useCallback)(() => {
577
+ const activeRange = rangeRef.current;
578
+ dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
579
+ setMenuOpen(false);
580
+ if (editor && !editor.isDestroyed && editor.isEditable !== false)
581
+ editor.view.focus();
582
+ }, [editor]);
415
583
  const filteredPeople = (0, import_react6.useMemo)(() => {
416
584
  if (!query) return peopleList;
417
585
  const lowered = query.toLowerCase();
@@ -420,14 +588,15 @@ var CommentMention = ({
420
588
  );
421
589
  }, [peopleList, query]);
422
590
  const updateMenuState = (0, import_react6.useCallback)(() => {
423
- var _a, _b;
424
- if (!editor) return;
425
- const { from } = editor.state.selection;
591
+ if (!editor || !editable || editor.isEditable === false) {
592
+ setMenuOpen(false);
593
+ rangeRef.current = null;
594
+ return;
595
+ }
426
596
  const { parent, parentOffset, pos } = editor.state.selection.$from;
427
597
  if (!parent.isTextblock) {
428
598
  setMenuOpen(false);
429
599
  setQuery("");
430
- setRange(null);
431
600
  return;
432
601
  }
433
602
  const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
@@ -439,13 +608,12 @@ var CommentMention = ({
439
608
  onQueryChange(handleQuery);
440
609
  }
441
610
  setQuery(handleQuery);
442
- setMenuOpen(true);
443
611
  const fromPos = pos - match[0].length;
444
612
  const nextRange = { from: fromPos, to: pos };
445
- setRange(nextRange);
613
+ setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
446
614
  rangeRef.current = nextRange;
447
615
  const coords = editor.view.coordsAtPos(fromPos);
448
- const containerRect = (_b = (_a = editorContainerRef == null ? void 0 : editorContainerRef.current) == null ? void 0 : _a.getBoundingClientRect) == null ? void 0 : _b.call(_a);
616
+ const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
449
617
  if (containerRect) {
450
618
  setAnchorStyle({
451
619
  left: coords.left - containerRect.left + 12,
@@ -456,16 +624,16 @@ var CommentMention = ({
456
624
  }
457
625
  setMenuOpen(false);
458
626
  setQuery("");
459
- setRange(null);
460
627
  rangeRef.current = null;
461
628
  if (onQueryChange && lastQueryRef.current !== "") {
462
629
  lastQueryRef.current = "";
463
630
  onQueryChange("");
464
631
  }
465
- }, [editor, editorContainerRef, onQueryChange]);
632
+ }, [editor, editorContainerRef, onQueryChange, editable]);
466
633
  const handleSelectPerson = (0, import_react6.useCallback)(
467
634
  (person) => {
468
- if (!editor || !rangeRef.current) return;
635
+ if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
636
+ return;
469
637
  const selectionRange = rangeRef.current;
470
638
  editor.commands.focus();
471
639
  editor.commands.setTextSelection(selectionRange);
@@ -485,21 +653,34 @@ var CommentMention = ({
485
653
  { updateSelection: false }
486
654
  );
487
655
  const nextPos = selectionRange.from + 2;
488
- requestAnimationFrame(() => {
656
+ editor.view.focus();
657
+ cancelAnimationFrame(focusFrameRef.current);
658
+ clearTimeout(focusTimeoutRef.current);
659
+ focusFrameRef.current = requestAnimationFrame(() => {
660
+ if (editor.isDestroyed || editor.isEditable === false) return;
489
661
  editor.commands.setTextSelection(nextPos);
490
662
  editor.view.focus();
491
- setTimeout(() => editor.view.focus(), 0);
663
+ focusTimeoutRef.current = setTimeout(() => {
664
+ if (!editor.isDestroyed && editor.isEditable !== false)
665
+ editor.view.focus();
666
+ }, 0);
492
667
  });
493
668
  setMenuOpen(false);
494
669
  setQuery("");
495
- setRange(null);
496
670
  rangeRef.current = null;
497
671
  if (onQueryChange && lastQueryRef.current !== "") {
498
672
  lastQueryRef.current = "";
499
673
  onQueryChange("");
500
674
  }
501
675
  },
502
- [editor, onQueryChange]
676
+ [editor, onQueryChange, editable]
677
+ );
678
+ (0, import_react6.useEffect)(
679
+ () => () => {
680
+ cancelAnimationFrame(focusFrameRef.current);
681
+ clearTimeout(focusTimeoutRef.current);
682
+ },
683
+ [editor]
503
684
  );
504
685
  (0, import_react6.useEffect)(() => {
505
686
  if (!editor) return;
@@ -512,25 +693,30 @@ var CommentMention = ({
512
693
  };
513
694
  }, [editor, updateMenuState]);
514
695
  (0, import_react6.useEffect)(() => {
515
- var _a;
516
696
  if (!editor) return;
517
697
  const handleKeyDown = (event) => {
518
- var _a2;
519
698
  if (!menuOpen) return;
699
+ if (event.key === "Escape") {
700
+ event.preventDefault();
701
+ event.stopPropagation();
702
+ dismissMenu();
703
+ return;
704
+ }
520
705
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
521
706
  event.preventDefault();
707
+ event.stopPropagation();
522
708
  const list = menuListRef.current;
523
- const items = list == null ? void 0 : list.querySelectorAll("[data-menu-item]");
524
- if (!list || !(items == null ? void 0 : items.length)) return;
709
+ const items = list?.querySelectorAll("[data-menu-item]");
710
+ if (!list || !items?.length) return;
525
711
  list.focus();
526
712
  const direction = event.key === "ArrowDown" ? 1 : -1;
527
713
  const targetIndex = direction === 1 ? 0 : items.length - 1;
528
- (_a2 = items[targetIndex]) == null ? void 0 : _a2.focus();
714
+ items[targetIndex]?.focus();
529
715
  };
530
- const dom = (_a = editor.view) == null ? void 0 : _a.dom;
531
- dom == null ? void 0 : dom.addEventListener("keydown", handleKeyDown);
532
- return () => dom == null ? void 0 : dom.removeEventListener("keydown", handleKeyDown);
533
- }, [editor, menuOpen]);
716
+ const dom = editor.view?.dom;
717
+ dom?.addEventListener("keydown", handleKeyDown, true);
718
+ return () => dom?.removeEventListener("keydown", handleKeyDown, true);
719
+ }, [editor, menuOpen, dismissMenu]);
534
720
  const menuContent = (0, import_react6.useMemo)(
535
721
  () => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
536
722
  import_menus.Menu,
@@ -542,6 +728,7 @@ var CommentMention = ({
542
728
  {
543
729
  type: "button",
544
730
  "aria-hidden": true,
731
+ tabIndex: -1,
545
732
  className: "absolute h-1 w-1 opacity-0",
546
733
  style: {
547
734
  left: `${anchorStyle.left}px`,
@@ -561,32 +748,46 @@ var CommentMention = ({
561
748
  collisionPadding: 12,
562
749
  onCloseAutoFocus: (event) => event.preventDefault()
563
750
  },
564
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_menus.MenuList, { ref: menuListRef, className: "min-w-[220px]", children: filteredPeople.map((person) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
565
- import_menus.MenuItem,
751
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
752
+ import_menus.MenuList,
566
753
  {
567
- onMouseDown: (event) => {
568
- event.preventDefault();
569
- event.stopPropagation();
570
- handleSelectPerson(person);
754
+ ref: menuListRef,
755
+ className: "min-w-[220px]",
756
+ onKeyDownCapture: (event) => {
757
+ if (event.key === "Escape") {
758
+ event.preventDefault();
759
+ event.stopPropagation();
760
+ dismissMenu();
761
+ }
571
762
  },
572
- onClick: () => handleSelectPerson(person),
573
- onPointerDown: (event) => event.preventDefault(),
574
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-8", children: [
575
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_avatars3.Avatar, { size: "small", ...person.avatarProps }),
576
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-col", children: [
577
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-fg-base", children: person.name }),
578
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-xs text-fg-subtle", children: [
579
- "@",
580
- person.handle
763
+ children: filteredPeople.map((person) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
764
+ import_menus.MenuItem,
765
+ {
766
+ onMouseDown: (event) => {
767
+ event.preventDefault();
768
+ event.stopPropagation();
769
+ handleSelectPerson(person);
770
+ },
771
+ onClick: () => handleSelectPerson(person),
772
+ onPointerDown: (event) => event.preventDefault(),
773
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-8", children: [
774
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_avatars3.Avatar, { size: "small", ...person.avatarProps }),
775
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-col", children: [
776
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-fg-base", children: person.name }),
777
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-xs text-fg-subtle", children: [
778
+ "@",
779
+ person.handle
780
+ ] })
781
+ ] })
581
782
  ] })
582
- ] })
583
- ] })
584
- },
585
- person.id
586
- )) })
783
+ },
784
+ person.id
785
+ ))
786
+ }
787
+ )
587
788
  }
588
789
  ),
589
- [filteredPeople, handleSelectPerson, menuOpen]
790
+ [filteredPeople, handleSelectPerson, menuOpen, anchorStyle, dismissMenu]
590
791
  );
591
792
  if (!editor) return null;
592
793
  return menuContent;