@edux-design/comments 0.1.4 → 0.3.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,45 +554,108 @@ 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
- var import_react6 = __toESM(require("react"));
561
+ var import_react7 = __toESM(require("react"));
396
562
  var import_menus = require("@edux-design/menus");
397
563
  var import_avatars3 = require("@edux-design/avatars");
564
+
565
+ // src/elements/useMentionPortalContainer.js
566
+ var import_react6 = require("react");
567
+ function resolveMentionPortalContainer(anchor, override) {
568
+ const document = anchor?.ownerDocument;
569
+ if (!document || !anchor.isConnected) return void 0;
570
+ if (override === null) return void 0;
571
+ if (override?.isConnected && override.ownerDocument === document)
572
+ return override;
573
+ const dialog = anchor.closest(
574
+ '[role="dialog"], [role="alertdialog"], dialog'
575
+ );
576
+ return dialog?.isConnected ? dialog : void 0;
577
+ }
578
+ function useMentionPortalContainer(anchorRef, override, active) {
579
+ const [container, setContainer] = (0, import_react6.useState)(void 0);
580
+ (0, import_react6.useLayoutEffect)(() => {
581
+ let disposed = false;
582
+ let observer;
583
+ let observedDocument;
584
+ const refresh = () => {
585
+ if (disposed) return;
586
+ const anchor = anchorRef?.current;
587
+ const next = resolveMentionPortalContainer(anchor, override);
588
+ setContainer((previous) => previous === next ? previous : next);
589
+ const document = anchor?.ownerDocument;
590
+ if (!active || observedDocument === document) return;
591
+ observer?.disconnect();
592
+ observedDocument = document;
593
+ const MutationObserver = document?.defaultView?.MutationObserver;
594
+ if (MutationObserver && document.documentElement) {
595
+ observer = new MutationObserver(refresh);
596
+ observer.observe(document.documentElement, {
597
+ childList: true,
598
+ subtree: true,
599
+ attributes: true,
600
+ attributeFilter: ["role"]
601
+ });
602
+ }
603
+ };
604
+ refresh();
605
+ return () => {
606
+ disposed = true;
607
+ observer?.disconnect();
608
+ };
609
+ }, [anchorRef, override, active]);
610
+ return container;
611
+ }
612
+
613
+ // src/elements/CommentMention.jsx
398
614
  var import_jsx_runtime4 = require("react/jsx-runtime");
399
615
  var CommentMention = ({
400
616
  peopleList = [],
401
- onQueryChange
617
+ onQueryChange,
618
+ portalContainer
402
619
  }) => {
403
- const { editor, editorContainerRef } = useCommentComposer();
404
- const [menuOpen, setMenuOpen] = (0, import_react6.useState)(false);
405
- const [query, setQuery] = (0, import_react6.useState)("");
406
- const [range, setRange] = (0, import_react6.useState)(null);
407
- const [anchorStyle, setAnchorStyle] = (0, import_react6.useState)({ left: 0, top: 0 });
408
- const menuListRef = (0, import_react6.useRef)(null);
409
- const rangeRef = (0, import_react6.useRef)(null);
410
- 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]
620
+ const { editor, editorContainerRef, editable = true } = useCommentComposer();
621
+ const [menuOpen, setMenuOpen] = (0, import_react7.useState)(false);
622
+ const mentionPortalContainer = useMentionPortalContainer(
623
+ editorContainerRef,
624
+ portalContainer,
625
+ menuOpen
414
626
  );
415
- const filteredPeople = (0, import_react6.useMemo)(() => {
627
+ const [query, setQuery] = (0, import_react7.useState)("");
628
+ const [anchorStyle, setAnchorStyle] = (0, import_react7.useState)({ left: 0, top: 0 });
629
+ const menuListRef = (0, import_react7.useRef)(null);
630
+ const rangeRef = (0, import_react7.useRef)(null);
631
+ const lastQueryRef = (0, import_react7.useRef)("");
632
+ const dismissedRangeRef = (0, import_react7.useRef)(null);
633
+ const focusFrameRef = (0, import_react7.useRef)(null);
634
+ const focusTimeoutRef = (0, import_react7.useRef)(null);
635
+ const dismissMenu = (0, import_react7.useCallback)(() => {
636
+ const activeRange = rangeRef.current;
637
+ dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
638
+ setMenuOpen(false);
639
+ if (editor && !editor.isDestroyed && editor.isEditable !== false)
640
+ editor.view.focus();
641
+ }, [editor]);
642
+ const filteredPeople = (0, import_react7.useMemo)(() => {
416
643
  if (!query) return peopleList;
417
644
  const lowered = query.toLowerCase();
418
645
  return peopleList.filter(
419
646
  (person) => person.handle.toLowerCase().startsWith(lowered)
420
647
  );
421
648
  }, [peopleList, query]);
422
- const updateMenuState = (0, import_react6.useCallback)(() => {
423
- var _a, _b;
424
- if (!editor) return;
425
- const { from } = editor.state.selection;
649
+ const updateMenuState = (0, import_react7.useCallback)(() => {
650
+ if (!editor || !editable || editor.isEditable === false) {
651
+ setMenuOpen(false);
652
+ rangeRef.current = null;
653
+ return;
654
+ }
426
655
  const { parent, parentOffset, pos } = editor.state.selection.$from;
427
656
  if (!parent.isTextblock) {
428
657
  setMenuOpen(false);
429
658
  setQuery("");
430
- setRange(null);
431
659
  return;
432
660
  }
433
661
  const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
@@ -439,13 +667,12 @@ var CommentMention = ({
439
667
  onQueryChange(handleQuery);
440
668
  }
441
669
  setQuery(handleQuery);
442
- setMenuOpen(true);
443
670
  const fromPos = pos - match[0].length;
444
671
  const nextRange = { from: fromPos, to: pos };
445
- setRange(nextRange);
672
+ setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
446
673
  rangeRef.current = nextRange;
447
674
  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);
675
+ const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
449
676
  if (containerRect) {
450
677
  setAnchorStyle({
451
678
  left: coords.left - containerRect.left + 12,
@@ -456,16 +683,16 @@ var CommentMention = ({
456
683
  }
457
684
  setMenuOpen(false);
458
685
  setQuery("");
459
- setRange(null);
460
686
  rangeRef.current = null;
461
687
  if (onQueryChange && lastQueryRef.current !== "") {
462
688
  lastQueryRef.current = "";
463
689
  onQueryChange("");
464
690
  }
465
- }, [editor, editorContainerRef, onQueryChange]);
466
- const handleSelectPerson = (0, import_react6.useCallback)(
691
+ }, [editor, editorContainerRef, onQueryChange, editable]);
692
+ const handleSelectPerson = (0, import_react7.useCallback)(
467
693
  (person) => {
468
- if (!editor || !rangeRef.current) return;
694
+ if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
695
+ return;
469
696
  const selectionRange = rangeRef.current;
470
697
  editor.commands.focus();
471
698
  editor.commands.setTextSelection(selectionRange);
@@ -485,23 +712,36 @@ var CommentMention = ({
485
712
  { updateSelection: false }
486
713
  );
487
714
  const nextPos = selectionRange.from + 2;
488
- requestAnimationFrame(() => {
715
+ editor.view.focus();
716
+ cancelAnimationFrame(focusFrameRef.current);
717
+ clearTimeout(focusTimeoutRef.current);
718
+ focusFrameRef.current = requestAnimationFrame(() => {
719
+ if (editor.isDestroyed || editor.isEditable === false) return;
489
720
  editor.commands.setTextSelection(nextPos);
490
721
  editor.view.focus();
491
- setTimeout(() => editor.view.focus(), 0);
722
+ focusTimeoutRef.current = setTimeout(() => {
723
+ if (!editor.isDestroyed && editor.isEditable !== false)
724
+ editor.view.focus();
725
+ }, 0);
492
726
  });
493
727
  setMenuOpen(false);
494
728
  setQuery("");
495
- setRange(null);
496
729
  rangeRef.current = null;
497
730
  if (onQueryChange && lastQueryRef.current !== "") {
498
731
  lastQueryRef.current = "";
499
732
  onQueryChange("");
500
733
  }
501
734
  },
502
- [editor, onQueryChange]
735
+ [editor, onQueryChange, editable]
503
736
  );
504
- (0, import_react6.useEffect)(() => {
737
+ (0, import_react7.useEffect)(
738
+ () => () => {
739
+ cancelAnimationFrame(focusFrameRef.current);
740
+ clearTimeout(focusTimeoutRef.current);
741
+ },
742
+ [editor]
743
+ );
744
+ (0, import_react7.useEffect)(() => {
505
745
  if (!editor) return;
506
746
  updateMenuState();
507
747
  editor.on("transaction", updateMenuState);
@@ -511,27 +751,32 @@ var CommentMention = ({
511
751
  editor.off("selectionUpdate", updateMenuState);
512
752
  };
513
753
  }, [editor, updateMenuState]);
514
- (0, import_react6.useEffect)(() => {
515
- var _a;
754
+ (0, import_react7.useEffect)(() => {
516
755
  if (!editor) return;
517
756
  const handleKeyDown = (event) => {
518
- var _a2;
519
757
  if (!menuOpen) return;
758
+ if (event.key === "Escape") {
759
+ event.preventDefault();
760
+ event.stopPropagation();
761
+ dismissMenu();
762
+ return;
763
+ }
520
764
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
521
765
  event.preventDefault();
766
+ event.stopPropagation();
522
767
  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;
768
+ const items = list?.querySelectorAll("[data-menu-item]");
769
+ if (!list || !items?.length) return;
525
770
  list.focus();
526
771
  const direction = event.key === "ArrowDown" ? 1 : -1;
527
772
  const targetIndex = direction === 1 ? 0 : items.length - 1;
528
- (_a2 = items[targetIndex]) == null ? void 0 : _a2.focus();
773
+ items[targetIndex]?.focus();
529
774
  };
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]);
534
- const menuContent = (0, import_react6.useMemo)(
775
+ const dom = editor.view?.dom;
776
+ dom?.addEventListener("keydown", handleKeyDown, true);
777
+ return () => dom?.removeEventListener("keydown", handleKeyDown, true);
778
+ }, [editor, menuOpen, dismissMenu]);
779
+ const menuContent = (0, import_react7.useMemo)(
535
780
  () => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
536
781
  import_menus.Menu,
537
782
  {
@@ -542,6 +787,7 @@ var CommentMention = ({
542
787
  {
543
788
  type: "button",
544
789
  "aria-hidden": true,
790
+ tabIndex: -1,
545
791
  className: "absolute h-1 w-1 opacity-0",
546
792
  style: {
547
793
  left: `${anchorStyle.left}px`,
@@ -559,34 +805,56 @@ var CommentMention = ({
559
805
  sideOffset: 4,
560
806
  avoidCollisions: true,
561
807
  collisionPadding: 12,
808
+ container: mentionPortalContainer,
562
809
  onCloseAutoFocus: (event) => event.preventDefault()
563
810
  },
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,
811
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
812
+ import_menus.MenuList,
566
813
  {
567
- onMouseDown: (event) => {
568
- event.preventDefault();
569
- event.stopPropagation();
570
- handleSelectPerson(person);
814
+ ref: menuListRef,
815
+ className: "min-w-[220px]",
816
+ onKeyDownCapture: (event) => {
817
+ if (event.key === "Escape") {
818
+ event.preventDefault();
819
+ event.stopPropagation();
820
+ dismissMenu();
821
+ }
571
822
  },
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
823
+ children: filteredPeople.map((person) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
824
+ import_menus.MenuItem,
825
+ {
826
+ onMouseDown: (event) => {
827
+ event.preventDefault();
828
+ event.stopPropagation();
829
+ handleSelectPerson(person);
830
+ },
831
+ onClick: () => handleSelectPerson(person),
832
+ onPointerDown: (event) => event.preventDefault(),
833
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center gap-8", children: [
834
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_avatars3.Avatar, { size: "small", ...person.avatarProps }),
835
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex flex-col", children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm text-fg-base", children: person.name }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "text-xs text-fg-subtle", children: [
838
+ "@",
839
+ person.handle
840
+ ] })
841
+ ] })
581
842
  ] })
582
- ] })
583
- ] })
584
- },
585
- person.id
586
- )) })
843
+ },
844
+ person.id
845
+ ))
846
+ }
847
+ )
587
848
  }
588
849
  ),
589
- [filteredPeople, handleSelectPerson, menuOpen]
850
+ [
851
+ filteredPeople,
852
+ handleSelectPerson,
853
+ menuOpen,
854
+ anchorStyle,
855
+ dismissMenu,
856
+ mentionPortalContainer
857
+ ]
590
858
  );
591
859
  if (!editor) return null;
592
860
  return menuContent;