@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d
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/README.md +0 -2
- package/commenting.css +1152 -350
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/{ui/avatar.js → canvas/canvas-events.js} +14 -22
- package/dist-cjs/canvas/canvas-events.js.map +7 -0
- package/dist-cjs/canvas/cluster-badge.js +121 -0
- package/dist-cjs/canvas/cluster-badge.js.map +7 -0
- package/dist-cjs/canvas/cluster-fade.js +85 -0
- package/dist-cjs/canvas/cluster-fade.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +53 -3
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/cluster-model.js +252 -0
- package/dist-cjs/canvas/cluster-model.js.map +7 -0
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-mutations.js +152 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +160 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +9 -6
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js +19 -9
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +63 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +17 -37
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +151 -989
- package/dist-cjs/canvas/comments-overlay.js.map +3 -3
- package/dist-cjs/canvas/comments-sidebar.js +49 -41
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/context.js +17 -0
- package/dist-cjs/canvas/context.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +28 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/license.js.map +1 -1
- package/dist-cjs/canvas/mobile-placement.js +91 -0
- package/dist-cjs/canvas/mobile-placement.js.map +7 -0
- package/dist-cjs/canvas/options.js +57 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pending-composer.js +134 -0
- package/dist-cjs/canvas/pending-composer.js.map +7 -0
- package/dist-cjs/canvas/pin-stacking.js +81 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- package/dist-cjs/canvas/region-box.js +113 -0
- package/dist-cjs/canvas/region-box.js.map +7 -0
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +28 -14
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +304 -0
- package/dist-cjs/canvas/thread-pin.js.map +7 -0
- package/dist-cjs/canvas/thread-preview.js +177 -0
- package/dist-cjs/canvas/thread-preview.js.map +7 -0
- package/dist-cjs/canvas/thread-stack.js +199 -0
- package/dist-cjs/canvas/thread-stack.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +82 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +450 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +6 -6
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/replay.js +89 -22
- package/dist-cjs/clustering/replay.js.map +3 -3
- package/dist-cjs/clustering/runtime.js +126 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +904 -353
- package/dist-cjs/index.js +52 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +10 -6
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +15 -8
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +124 -39
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +11 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +9 -7
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +39 -40
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +3 -2
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/emoji-picker.js +54 -0
- package/dist-cjs/ui/emoji-picker.js.map +7 -0
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/format-time.js +16 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +115 -0
- package/dist-cjs/ui/icons.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +71 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +58 -6
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -7
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
- package/dist-cjs/ui/reply-count.js.map +7 -0
- package/dist-cjs/ui/send-button.js +13 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
- package/dist-cjs/ui/visual-viewport.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
- package/dist-esm/canvas/canvas-events.mjs +15 -0
- package/dist-esm/canvas/canvas-events.mjs.map +7 -0
- package/dist-esm/canvas/cluster-badge.mjs +106 -0
- package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
- package/dist-esm/canvas/cluster-fade.mjs +65 -0
- package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +54 -4
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/cluster-model.mjs +236 -0
- package/dist-esm/canvas/cluster-model.mjs.map +7 -0
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-mutations.mjs +132 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +145 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +10 -7
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs +19 -9
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +66 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +157 -1010
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +53 -46
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/context.mjs +1 -0
- package/dist-esm/canvas/context.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +35 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/license.mjs.map +1 -1
- package/dist-esm/canvas/mobile-placement.mjs +71 -0
- package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +59 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pending-composer.mjs +126 -0
- package/dist-esm/canvas/pending-composer.mjs.map +7 -0
- package/dist-esm/canvas/pin-stacking.mjs +61 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- package/dist-esm/canvas/region-box.mjs +93 -0
- package/dist-esm/canvas/region-box.mjs.map +7 -0
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +29 -19
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +310 -0
- package/dist-esm/canvas/thread-pin.mjs.map +7 -0
- package/dist-esm/canvas/thread-preview.mjs +163 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +189 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +86 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +461 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +6 -6
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/replay.mjs +89 -22
- package/dist-esm/clustering/replay.mjs.map +3 -3
- package/dist-esm/clustering/runtime.mjs +126 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +904 -353
- package/dist-esm/index.mjs +102 -43
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +11 -7
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +15 -8
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +135 -39
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +11 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +9 -7
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +51 -50
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +3 -2
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/emoji-picker.mjs +34 -0
- package/dist-esm/ui/emoji-picker.mjs.map +7 -0
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +16 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +95 -0
- package/dist-esm/ui/icons.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +59 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +59 -7
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -8
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +9 -0
- package/dist-esm/ui/reply-count.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +13 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/visual-viewport.mjs +12 -0
- package/dist-esm/ui/visual-viewport.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +191 -0
- package/src/canvas/canvas-events.ts +18 -0
- package/src/canvas/canvas.css +464 -55
- package/src/canvas/cluster-badge.tsx +133 -0
- package/src/canvas/cluster-fade.ts +102 -0
- package/src/canvas/cluster-input.test.ts +178 -36
- package/src/canvas/cluster-input.ts +95 -9
- package/src/canvas/cluster-model.ts +378 -0
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +452 -0
- package/src/canvas/comment-mutations.ts +299 -0
- package/src/canvas/comment-reactions.test.ts +154 -0
- package/src/canvas/comment-reactions.tsx +246 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +17 -13
- package/src/canvas/comment-store.ts +68 -33
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +103 -25
- package/src/canvas/comments-filter-menu.tsx +20 -36
- package/src/canvas/comments-overflow-menu.tsx +17 -19
- package/src/canvas/comments-overlay.tsx +252 -1369
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +112 -73
- package/src/canvas/comments-visibility-toggle.tsx +25 -0
- package/src/canvas/context.ts +48 -0
- package/src/canvas/hooks.test.ts +94 -0
- package/src/canvas/hooks.ts +66 -11
- package/src/canvas/license.ts +1 -1
- package/src/canvas/mobile-placement.ts +115 -0
- package/src/canvas/options.test.ts +176 -4
- package/src/canvas/options.ts +276 -18
- package/src/canvas/pending-composer.tsx +157 -0
- package/src/canvas/pin-stacking.test.ts +224 -0
- package/src/canvas/pin-stacking.ts +103 -0
- package/src/canvas/region-box.tsx +124 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +88 -39
- package/src/canvas/thread-pin.tsx +418 -0
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +294 -0
- package/src/canvas/thread-stack.tsx +242 -0
- package/src/canvas/thread-state.test.ts +260 -0
- package/src/canvas/thread-state.ts +171 -37
- package/src/canvas/thread-view.test.ts +72 -0
- package/src/canvas/thread-view.tsx +614 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +17 -8
- package/src/clustering/replay.test.ts +0 -7
- package/src/clustering/replay.ts +131 -32
- package/src/clustering/runtime.test.ts +267 -13
- package/src/clustering/runtime.ts +176 -19
- package/src/clustering/schedule.test.ts +0 -6
- package/src/clustering/screen-offsets.test.ts +171 -0
- package/src/clustering/types.ts +19 -9
- package/src/index.ts +91 -42
- package/src/ui/byline.tsx +21 -9
- package/src/ui/comment-card.tsx +20 -10
- package/src/ui/comment-composer.tsx +178 -56
- package/src/ui/comment-pin.tsx +13 -2
- package/src/ui/comment-thread.tsx +13 -6
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +82 -47
- package/src/ui/comments.css +577 -294
- package/src/ui/count-badge.tsx +9 -2
- package/src/ui/emoji-picker.test.ts +25 -0
- package/src/ui/emoji-picker.tsx +76 -0
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.test.ts +69 -0
- package/src/ui/format-time.ts +27 -2
- package/src/ui/icons.tsx +116 -0
- package/src/ui/reaction-picker.tsx +102 -0
- package/src/ui/reaction.tsx +125 -6
- package/src/ui/reactions.tsx +69 -10
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +13 -3
- package/src/ui/visual-viewport.test.ts +36 -0
- package/src/ui/visual-viewport.ts +27 -0
- package/dist-cjs/canvas/region-options.js +0 -49
- package/dist-cjs/canvas/region-options.js.map +0 -7
- package/dist-cjs/ui/avatar.js.map +0 -7
- package/dist-cjs/ui/comment-mention.js +0 -42
- package/dist-cjs/ui/comment-mention.js.map +0 -7
- package/dist-cjs/ui/comment-text.js.map +0 -7
- package/dist-cjs/ui/mention-list.js +0 -70
- package/dist-cjs/ui/mention-list.js.map +0 -7
- package/dist-cjs/ui/mention-suggestion.js +0 -206
- package/dist-cjs/ui/mention-suggestion.js.map +0 -7
- package/dist-cjs/ui/mention.js.map +0 -7
- package/dist-cjs/ui/render-markdown.js +0 -63
- package/dist-cjs/ui/render-markdown.js.map +0 -7
- package/dist-esm/canvas/region-options.mjs +0 -29
- package/dist-esm/canvas/region-options.mjs.map +0 -7
- package/dist-esm/ui/avatar.mjs +0 -23
- package/dist-esm/ui/avatar.mjs.map +0 -7
- package/dist-esm/ui/comment-mention.mjs +0 -22
- package/dist-esm/ui/comment-mention.mjs.map +0 -7
- package/dist-esm/ui/comment-text.mjs +0 -9
- package/dist-esm/ui/comment-text.mjs.map +0 -7
- package/dist-esm/ui/mention-list.mjs +0 -50
- package/dist-esm/ui/mention-list.mjs.map +0 -7
- package/dist-esm/ui/mention-suggestion.mjs +0 -186
- package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
- package/dist-esm/ui/mention.mjs +0 -11
- package/dist-esm/ui/mention.mjs.map +0 -7
- package/dist-esm/ui/render-markdown.mjs +0 -45
- package/dist-esm/ui/render-markdown.mjs.map +0 -7
- package/src/canvas/region-options.ts +0 -57
- package/src/ui/avatar.tsx +0 -31
- package/src/ui/comment-mention.ts +0 -47
- package/src/ui/comment-text.tsx +0 -12
- package/src/ui/mention-list.tsx +0 -106
- package/src/ui/mention-suggestion.test.ts +0 -18
- package/src/ui/mention-suggestion.tsx +0 -285
- package/src/ui/mention.tsx +0 -9
- package/src/ui/render-markdown.tsx +0 -72
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSONContent } from '@tiptap/core'
|
|
2
2
|
import { EditorContent, useEditor } from '@tiptap/react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Avatar,
|
|
5
|
+
type CommentAuthor,
|
|
6
|
+
createMentionExtension,
|
|
7
|
+
createMentionSuggestion,
|
|
8
|
+
isMentionPickerOpen,
|
|
9
|
+
MentionMember,
|
|
10
|
+
} from '@tldraw/mentions'
|
|
11
|
+
import {
|
|
12
|
+
type MouseEvent as ReactMouseEvent,
|
|
13
|
+
ReactNode,
|
|
14
|
+
useEffect,
|
|
15
|
+
useLayoutEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react'
|
|
4
20
|
import { isEqual, TLRichText, useMaybeEditor } from 'tldraw'
|
|
5
|
-
import { Avatar } from './avatar'
|
|
6
21
|
import { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'
|
|
7
|
-
import { commentMention } from './comment-mention'
|
|
8
|
-
import { MentionMember } from './mention-list'
|
|
9
|
-
import { createMentionSuggestion, isMentionPickerOpen } from './mention-suggestion'
|
|
10
22
|
import { SendButton } from './send-button'
|
|
11
23
|
|
|
12
24
|
/** @public */
|
|
13
25
|
export interface CommentComposerProps {
|
|
14
|
-
author:
|
|
26
|
+
author: CommentAuthor
|
|
15
27
|
placeholder: string
|
|
16
28
|
/** Controlled rich-text value. Omit for the presentational (display-only) composer. */
|
|
17
29
|
value?: TLRichText
|
|
@@ -19,6 +31,9 @@ export interface CommentComposerProps {
|
|
|
19
31
|
/** Called on Send click or Enter. When set, the composer is interactive. */
|
|
20
32
|
onSubmit?(): void
|
|
21
33
|
sendLabel?: string
|
|
34
|
+
/** Called when Up is pressed in an empty composer — e.g. to start editing the comment above,
|
|
35
|
+
* the way chat apps edit your last message. With content in the field, Up moves the cursor. */
|
|
36
|
+
onArrowUpWhenEmpty?(): void
|
|
22
37
|
disabled?: boolean
|
|
23
38
|
autoFocus?: boolean
|
|
24
39
|
/** The leading element before the field. Defaults to the author's avatar. */
|
|
@@ -43,6 +58,7 @@ export function CommentComposer({
|
|
|
43
58
|
onChange,
|
|
44
59
|
onSubmit,
|
|
45
60
|
sendLabel = 'Send',
|
|
61
|
+
onArrowUpWhenEmpty,
|
|
46
62
|
disabled,
|
|
47
63
|
autoFocus,
|
|
48
64
|
leading,
|
|
@@ -50,16 +66,16 @@ export function CommentComposer({
|
|
|
50
66
|
renderMentionSuggestion,
|
|
51
67
|
}: CommentComposerProps) {
|
|
52
68
|
const interactive = !!onChange || !!onSubmit
|
|
53
|
-
//
|
|
54
|
-
// when the composer is used outside a tldraw editor (e.g. an isolated demo).
|
|
69
|
+
// Lets the mention popup track the camera. Null outside a tldraw editor.
|
|
55
70
|
const tlEditor = useMaybeEditor()
|
|
56
71
|
|
|
57
|
-
//
|
|
58
|
-
// change identity between renders.
|
|
72
|
+
// Read through refs so the editor isn't recreated when callback identity changes.
|
|
59
73
|
const onChangeRef = useRef(onChange)
|
|
60
74
|
onChangeRef.current = onChange
|
|
61
75
|
const onSubmitRef = useRef(onSubmit)
|
|
62
76
|
onSubmitRef.current = onSubmit
|
|
77
|
+
const onArrowUpWhenEmptyRef = useRef(onArrowUpWhenEmpty)
|
|
78
|
+
onArrowUpWhenEmptyRef.current = onArrowUpWhenEmpty
|
|
63
79
|
const disabledRef = useRef(disabled)
|
|
64
80
|
disabledRef.current = disabled
|
|
65
81
|
const getMentionSuggestionsRef = useRef(getMentionSuggestions)
|
|
@@ -68,50 +84,80 @@ export function CommentComposer({
|
|
|
68
84
|
renderMentionSuggestionRef.current = renderMentionSuggestion
|
|
69
85
|
|
|
70
86
|
const [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))
|
|
87
|
+
// Whether the field has grown to two rows: input across the full width, send button below.
|
|
88
|
+
// Flips when the (single-line) content width reaches the space left beside the send button.
|
|
89
|
+
const [expanded, setExpanded] = useState(false)
|
|
90
|
+
const expandedRef = useRef(expanded)
|
|
91
|
+
expandedRef.current = expanded
|
|
92
|
+
const inputWrapRef = useRef<HTMLDivElement>(null)
|
|
93
|
+
const mirrorRef = useRef<HTMLDivElement>(null)
|
|
71
94
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
// Measure whether the content still fits on one line beside the send button, against a hidden
|
|
96
|
+
// nowrap clone so marks and mention chips measure at their true width. The gap between the
|
|
97
|
+
// expand and collapse thresholds is a dead zone, so the layout can't flap at the boundary.
|
|
98
|
+
const remeasure = () => {
|
|
99
|
+
const wrap = inputWrapRef.current
|
|
100
|
+
const mirror = mirrorRef.current
|
|
101
|
+
const editor = editorRef.current
|
|
102
|
+
if (!wrap || !mirror || !editor) return
|
|
103
|
+
// Empty always fits — and measuring before TipTap has laid out reads zero widths, flashing
|
|
104
|
+
// the expanded layout for a frame on mount.
|
|
105
|
+
if (editor.isEmpty) {
|
|
106
|
+
if (expandedRef.current) setExpanded(false)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (wrap.clientWidth === 0) return
|
|
110
|
+
const doc = editor.state.doc
|
|
111
|
+
const multiBlock =
|
|
112
|
+
doc.childCount > 1 || (doc.firstChild !== null && doc.firstChild.type.name !== 'paragraph')
|
|
113
|
+
if (multiBlock) {
|
|
114
|
+
if (!expandedRef.current) setExpanded(true)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const field = wrap.parentElement
|
|
118
|
+
const send = field ? field.querySelector<HTMLElement>('.tlui-cmt-send') : null
|
|
119
|
+
const input = wrap.querySelector('.tlui-cmt-input')
|
|
120
|
+
if (!send || !input) return
|
|
121
|
+
mirror.innerHTML = input.innerHTML
|
|
122
|
+
const textWidth = mirror.offsetWidth
|
|
123
|
+
// Measured against the wrap's *content* box: text wraps at clientWidth minus padding, so
|
|
124
|
+
// without subtracting it the input grows a line before expansion fires and then snaps.
|
|
125
|
+
const wrapStyle = getComputedStyle(wrap)
|
|
126
|
+
const wrapPadX = parseFloat(wrapStyle.paddingLeft) + parseFloat(wrapStyle.paddingRight)
|
|
127
|
+
const collapsedAvailable =
|
|
128
|
+
wrap.clientWidth - wrapPadX - (expandedRef.current ? send.offsetWidth + 6 : 0)
|
|
129
|
+
if (!expandedRef.current && textWidth > collapsedAvailable - 8) {
|
|
130
|
+
setExpanded(true)
|
|
131
|
+
} else if (expandedRef.current && textWidth < collapsedAvailable - 24) {
|
|
132
|
+
setExpanded(false)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const remeasureRef = useRef(remeasure)
|
|
136
|
+
remeasureRef.current = remeasure
|
|
137
|
+
|
|
138
|
+
// Reachable from `handleKeyDown`, which is created before `useEditor` returns.
|
|
139
|
+
const editorRef = useRef<ReturnType<typeof useEditor>>(null)
|
|
140
|
+
|
|
141
|
+
// `commands.enter()` re-dispatches through `handleKeyDown`; without this guard our own handler
|
|
142
|
+
// would catch the synthetic Enter and submit.
|
|
143
|
+
const replayingEnter = useRef(false)
|
|
94
144
|
|
|
95
|
-
// The suggestion plugin is built once
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// later. Whether mentions (and a custom picker row) are wired at all is fixed at mount; only the
|
|
99
|
-
// callbacks themselves are live.
|
|
145
|
+
// The suggestion plugin is built once and runs outside React, so it reads the mention callbacks
|
|
146
|
+
// through refs — otherwise it queries the roster present at mount forever, never seeing a member
|
|
147
|
+
// who joins later. Whether mentions are wired at all is fixed at mount; the callbacks are live.
|
|
100
148
|
const mentionsEnabled = !!getMentionSuggestions
|
|
101
149
|
const hasCustomRow = !!renderMentionSuggestion
|
|
102
150
|
const extensions = useMemo(() => {
|
|
103
|
-
const list = [...commentTipTapExtensions
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// picker itself only turns on when the host provides a resolver; otherwise the node is present
|
|
107
|
-
// but its trigger is disabled.
|
|
151
|
+
const list = [...commentTipTapExtensions]
|
|
152
|
+
// The mention node is always registered, or ProseMirror strips existing mentions when an
|
|
153
|
+
// edited body loads. Only the `@` trigger is gated on the host providing a resolver.
|
|
108
154
|
if (mentionsEnabled) {
|
|
109
155
|
const resolveSuggestions = (query: string) => getMentionSuggestionsRef.current?.(query) ?? []
|
|
110
156
|
const renderRow = hasCustomRow
|
|
111
157
|
? (member: MentionMember) => renderMentionSuggestionRef.current?.(member)
|
|
112
158
|
: undefined
|
|
113
159
|
list.push(
|
|
114
|
-
|
|
160
|
+
createMentionExtension({
|
|
115
161
|
suggestion: createMentionSuggestion(resolveSuggestions, {
|
|
116
162
|
renderMember: renderRow,
|
|
117
163
|
editor: tlEditor,
|
|
@@ -119,29 +165,76 @@ export function CommentComposer({
|
|
|
119
165
|
})
|
|
120
166
|
)
|
|
121
167
|
} else {
|
|
122
|
-
list.push(
|
|
168
|
+
list.push(createMentionExtension({ suggestion: { char: '@', allow: () => false } }))
|
|
123
169
|
}
|
|
124
170
|
return list
|
|
125
|
-
}, [
|
|
171
|
+
}, [mentionsEnabled, hasCustomRow, tlEditor])
|
|
126
172
|
|
|
127
173
|
const editor = useEditor(
|
|
128
174
|
{
|
|
129
175
|
extensions,
|
|
130
176
|
content: (value ?? EMPTY_COMMENT) as JSONContent,
|
|
131
177
|
editable: interactive,
|
|
132
|
-
// tldraw
|
|
133
|
-
//
|
|
134
|
-
// mirrors RichTextArea's setup.
|
|
178
|
+
// tldraw ships its own TextDirection extension, so TipTap's core one would warn about a
|
|
179
|
+
// duplicate. Mirrors RichTextArea's setup.
|
|
135
180
|
enableCoreExtensions: { textDirection: false },
|
|
136
181
|
textDirection: 'auto',
|
|
137
|
-
editorProps: {
|
|
182
|
+
editorProps: {
|
|
183
|
+
attributes: {
|
|
184
|
+
class: 'tlui-cmt-input',
|
|
185
|
+
'aria-label': placeholder,
|
|
186
|
+
role: 'textbox',
|
|
187
|
+
'aria-multiline': 'true',
|
|
188
|
+
},
|
|
189
|
+
// Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter — an
|
|
190
|
+
// `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.
|
|
191
|
+
handleKeyDown: (_view, event) => {
|
|
192
|
+
// Let the keymaps handle the synthetic Enter we replay for a Shift+Enter newline.
|
|
193
|
+
if (replayingEnter.current) return false
|
|
194
|
+
// Up in an empty composer hands off to the host. With content it stays cursor
|
|
195
|
+
// movement, and with the picker open it navigates the roster.
|
|
196
|
+
if (event.key === 'ArrowUp' && !event.isComposing) {
|
|
197
|
+
if (
|
|
198
|
+
onArrowUpWhenEmptyRef.current &&
|
|
199
|
+
editorRef.current?.isEmpty &&
|
|
200
|
+
!isMentionPickerOpen()
|
|
201
|
+
) {
|
|
202
|
+
onArrowUpWhenEmptyRef.current()
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
if (event.key !== 'Enter' || event.isComposing) return false
|
|
208
|
+
// While the @-mention picker is open, Enter selects the highlighted member — defer.
|
|
209
|
+
if (isMentionPickerOpen()) return false
|
|
210
|
+
// Shift+Enter inserts a new line by replaying a plain Enter through the keymaps, reusing
|
|
211
|
+
// the editor's list-aware handling. tldraw doesn't do soft breaks.
|
|
212
|
+
if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
213
|
+
// An empty field would open the comment with a stray leading blank line.
|
|
214
|
+
if (editorRef.current?.isEmpty) return true
|
|
215
|
+
replayingEnter.current = true
|
|
216
|
+
try {
|
|
217
|
+
editorRef.current?.commands.enter()
|
|
218
|
+
} finally {
|
|
219
|
+
replayingEnter.current = false
|
|
220
|
+
}
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
// Enter, Cmd+Enter, and Ctrl+Enter submit.
|
|
224
|
+
if (!disabledRef.current) onSubmitRef.current?.()
|
|
225
|
+
return true
|
|
226
|
+
},
|
|
227
|
+
},
|
|
138
228
|
onUpdate: ({ editor }) => {
|
|
139
229
|
setIsEmpty(editor.isEmpty)
|
|
230
|
+
// Same-value state sets don't re-render, and the DOM is already updated here.
|
|
231
|
+
remeasureRef.current()
|
|
140
232
|
onChangeRef.current?.(editor.getJSON() as TLRichText)
|
|
141
233
|
},
|
|
142
234
|
},
|
|
143
|
-
[interactive]
|
|
235
|
+
[interactive, placeholder]
|
|
144
236
|
)
|
|
237
|
+
editorRef.current = editor
|
|
145
238
|
|
|
146
239
|
// Sync controlled resets (e.g. the parent clearing to EMPTY_COMMENT after posting) into the
|
|
147
240
|
// editor without echoing back the value the editor itself just emitted.
|
|
@@ -152,6 +245,12 @@ export function CommentComposer({
|
|
|
152
245
|
setIsEmpty(editor.isEmpty)
|
|
153
246
|
}, [editor, value])
|
|
154
247
|
|
|
248
|
+
// Measure once the editor's content is in the DOM, so a composer that mounts pre-filled (the
|
|
249
|
+
// edit-in-place composer) starts in the right layout.
|
|
250
|
+
useLayoutEffect(() => {
|
|
251
|
+
remeasureRef.current()
|
|
252
|
+
}, [editor, value])
|
|
253
|
+
|
|
155
254
|
// Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a
|
|
156
255
|
// canvas pointer event whose default focus handling would otherwise steal it back.
|
|
157
256
|
useEffect(() => {
|
|
@@ -160,17 +259,40 @@ export function CommentComposer({
|
|
|
160
259
|
return () => cancelAnimationFrame(raf)
|
|
161
260
|
}, [autoFocus, editor])
|
|
162
261
|
|
|
262
|
+
// The whole field behaves like the text input: clicking its empty area (the padding, or the
|
|
263
|
+
// space beside/below a short line) focuses the editor rather than only the text glyphs being
|
|
264
|
+
// clickable. The input (caret placement) and the send button keep their own click handling.
|
|
265
|
+
const focusEditorFromField = (e: ReactMouseEvent<HTMLDivElement>) => {
|
|
266
|
+
const target = e.target as HTMLElement
|
|
267
|
+
if (target.closest('.tlui-cmt-input') || target.closest('.tlui-cmt-send')) return
|
|
268
|
+
e.preventDefault()
|
|
269
|
+
editor?.commands.focus('end')
|
|
270
|
+
}
|
|
271
|
+
|
|
163
272
|
return (
|
|
164
|
-
<div className="cmt-composer">
|
|
165
|
-
{leading ?? <Avatar
|
|
166
|
-
<div
|
|
167
|
-
|
|
273
|
+
<div className="tlui-cmt-composer">
|
|
274
|
+
{leading ?? <Avatar author={author} />}
|
|
275
|
+
<div
|
|
276
|
+
className={[
|
|
277
|
+
'tlui-cmt-composer__field',
|
|
278
|
+
interactive && expanded && 'tlui-cmt-composer__field--expanded',
|
|
279
|
+
]
|
|
280
|
+
.filter(Boolean)
|
|
281
|
+
.join(' ')}
|
|
282
|
+
onMouseDown={interactive ? focusEditorFromField : undefined}
|
|
283
|
+
>
|
|
284
|
+
<div className="tlui-cmt-composer__input-wrap" ref={inputWrapRef}>
|
|
168
285
|
<EditorContent editor={editor} />
|
|
169
286
|
{isEmpty && (
|
|
170
|
-
<div className="cmt-input__placeholder" aria-hidden="true">
|
|
287
|
+
<div className="tlui-cmt-input__placeholder" aria-hidden="true">
|
|
171
288
|
{placeholder}
|
|
172
289
|
</div>
|
|
173
290
|
)}
|
|
291
|
+
<div
|
|
292
|
+
className="tlui-cmt-composer__mirror tlui-cmt-input"
|
|
293
|
+
ref={mirrorRef}
|
|
294
|
+
aria-hidden="true"
|
|
295
|
+
/>
|
|
174
296
|
</div>
|
|
175
297
|
{interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}
|
|
176
298
|
</div>
|
package/src/ui/comment-pin.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react'
|
|
2
|
+
import { PinCheckIcon } from './icons'
|
|
2
3
|
|
|
3
4
|
/** @public */
|
|
4
5
|
export interface CommentPinProps {
|
|
@@ -10,12 +11,22 @@ export interface CommentPinProps {
|
|
|
10
11
|
open?: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/* Decorative — the pin's accessible name lives on whatever wraps it. */
|
|
15
|
+
const resolvedCheck = <PinCheckIcon />
|
|
16
|
+
|
|
13
17
|
/** A canvas comment marker: shows its `children` (or a check when resolved). Purely
|
|
14
18
|
* presentational — it reflects open/resolved state via CSS; wrap it to make it clickable.
|
|
15
19
|
* @public @react */
|
|
16
20
|
export function CommentPin({ children, resolved, open }: CommentPinProps) {
|
|
17
|
-
const className = [
|
|
21
|
+
const className = [
|
|
22
|
+
// `tlui-cmt-marker` carries the resting shadow and the hover lift — the same treatment a
|
|
23
|
+
// count badge wears, so the two markers behave alike.
|
|
24
|
+
'tlui-cmt-marker',
|
|
25
|
+
'tlui-cmt-pin',
|
|
26
|
+
resolved && 'tlui-cmt-pin--resolved',
|
|
27
|
+
open && 'tlui-cmt-marker--open',
|
|
28
|
+
]
|
|
18
29
|
.filter(Boolean)
|
|
19
30
|
.join(' ')
|
|
20
|
-
return <div className={className}>{resolved ?
|
|
31
|
+
return <div className={className}>{resolved ? resolvedCheck : children}</div>
|
|
21
32
|
}
|
|
@@ -14,6 +14,9 @@ export interface CommentThreadProps {
|
|
|
14
14
|
resolvedBanner?: ReactNode
|
|
15
15
|
/** Reply composer props. Omit for a read-only thread (no composer). */
|
|
16
16
|
composer?: CommentComposerProps
|
|
17
|
+
/** Shown at the bottom of the thread (e.g. a sign-in prompt when there's no composer). When
|
|
18
|
+
* both are set, the footer renders after the composer. */
|
|
19
|
+
footer?: ReactNode
|
|
17
20
|
/** Override how each comment renders. Defaults to `<CommentCard>`. */
|
|
18
21
|
renderComment?(comment: CommentCardProps, index: number): ReactNode
|
|
19
22
|
}
|
|
@@ -30,20 +33,23 @@ export function CommentThread({
|
|
|
30
33
|
headerActions,
|
|
31
34
|
resolvedBanner,
|
|
32
35
|
composer,
|
|
36
|
+
footer,
|
|
33
37
|
renderComment,
|
|
34
38
|
}: CommentThreadProps) {
|
|
35
39
|
return (
|
|
36
|
-
<div className="cmt-thread">
|
|
40
|
+
<div className="tlui-cmt-thread">
|
|
37
41
|
{(header !== undefined || headerActions !== undefined) && (
|
|
38
|
-
<div className="cmt-thread__header">
|
|
39
|
-
<span className="cmt-thread__title">{header}</span>
|
|
42
|
+
<div className="tlui-cmt-thread__header">
|
|
43
|
+
{header !== undefined && <span className="tlui-cmt-thread__title">{header}</span>}
|
|
40
44
|
{headerActions !== undefined && (
|
|
41
|
-
<div className="cmt-thread__actions">{headerActions}</div>
|
|
45
|
+
<div className="tlui-cmt-thread__actions">{headerActions}</div>
|
|
42
46
|
)}
|
|
43
47
|
</div>
|
|
44
48
|
)}
|
|
45
|
-
{resolvedBanner !== undefined &&
|
|
46
|
-
|
|
49
|
+
{resolvedBanner !== undefined && (
|
|
50
|
+
<div className="tlui-cmt-thread__resolved">{resolvedBanner}</div>
|
|
51
|
+
)}
|
|
52
|
+
<div className="tlui-cmt-thread__list">
|
|
47
53
|
{comments.map((comment, i) => (
|
|
48
54
|
<div key={i}>
|
|
49
55
|
{renderComment ? renderComment(comment, i) : <CommentCard {...comment} />}
|
|
@@ -51,6 +57,7 @@ export function CommentThread({
|
|
|
51
57
|
))}
|
|
52
58
|
</div>
|
|
53
59
|
{composer !== undefined && <CommentComposer {...composer} />}
|
|
60
|
+
{footer !== undefined && <div className="tlui-cmt-thread__footer">{footer}</div>}
|
|
54
61
|
</div>
|
|
55
62
|
)
|
|
56
63
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MouseEvent } from 'react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { isOpenInNewTabClick } from './comments-list'
|
|
4
|
+
|
|
5
|
+
function click(overrides: Partial<MouseEvent> = {}): MouseEvent {
|
|
6
|
+
return {
|
|
7
|
+
metaKey: false,
|
|
8
|
+
ctrlKey: false,
|
|
9
|
+
shiftKey: false,
|
|
10
|
+
altKey: false,
|
|
11
|
+
button: 0,
|
|
12
|
+
...overrides,
|
|
13
|
+
} as MouseEvent
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('isOpenInNewTabClick', () => {
|
|
17
|
+
it('treats a plain left click as an in-place select', () => {
|
|
18
|
+
expect(isOpenInNewTabClick(click())).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it.each(['metaKey', 'ctrlKey', 'shiftKey', 'altKey'] as const)(
|
|
22
|
+
'leaves a %s click to the browser',
|
|
23
|
+
(modifier) => {
|
|
24
|
+
expect(isOpenInNewTabClick(click({ [modifier]: true }))).toBe(true)
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
it('leaves non-primary-button clicks to the browser', () => {
|
|
29
|
+
expect(isOpenInNewTabClick(click({ button: 1 }))).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
})
|
package/src/ui/comments-list.tsx
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Avatar, type CommentAuthor } from '@tldraw/mentions'
|
|
2
|
+
import { Fragment, MouseEvent, ReactNode } from 'react'
|
|
3
|
+
import { useTranslation } from 'tldraw'
|
|
3
4
|
import { Byline } from './byline'
|
|
5
|
+
import { CheckIcon } from './icons'
|
|
6
|
+
import { Reactions, type ReactionSummary } from './reactions'
|
|
7
|
+
import { replyCountLabel } from './reply-count'
|
|
4
8
|
|
|
5
9
|
/** @public */
|
|
6
10
|
export interface CommentListItemProps {
|
|
7
11
|
id: string
|
|
8
|
-
author:
|
|
12
|
+
author: CommentAuthor
|
|
9
13
|
/** A short preview of the thread — e.g. the first comment's body. */
|
|
10
14
|
preview: ReactNode
|
|
11
15
|
/** ISO datetime of the thread's first comment. */
|
|
@@ -17,6 +21,28 @@ export interface CommentListItemProps {
|
|
|
17
21
|
count?: number
|
|
18
22
|
/** Whether this thread is the open one. */
|
|
19
23
|
selected?: boolean
|
|
24
|
+
/** Tallied reactions for the row (a thread's, or a single comment's when the row is one
|
|
25
|
+
* comment), shown as inert pills under the preview. Omit to hide. */
|
|
26
|
+
reactions?: ReactionSummary[]
|
|
27
|
+
/**
|
|
28
|
+
* Link target for the item. When set, the row renders as an anchor so browser affordances
|
|
29
|
+
* (ctrl/cmd-click, middle-click) open it in a new tab; a plain click still calls `onSelect`.
|
|
30
|
+
*/
|
|
31
|
+
href?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What a row is rendered with: the item, plus the list-level wiring it needs to be interactive.
|
|
36
|
+
* A custom row gets the same props the default `<CommentListItem>` does, so it can wrap the
|
|
37
|
+
* default rather than reimplement it.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export interface CommentListItemRenderProps extends CommentListItemProps {
|
|
42
|
+
/** Label for a resolved thread's marker on its row. */
|
|
43
|
+
resolvedLabel?: string
|
|
44
|
+
/** Called with the thread id when the row is chosen. */
|
|
45
|
+
onSelect?(id: string): void
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
/** @public */
|
|
@@ -32,8 +58,12 @@ export interface CommentsListProps {
|
|
|
32
58
|
empty?: ReactNode
|
|
33
59
|
/** Label for a resolved thread's marker on its row. Defaults to "Resolved". */
|
|
34
60
|
resolvedLabel?: string
|
|
35
|
-
/**
|
|
36
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Override how each item renders. Defaults to `<CommentListItem>`, which is exported — so a
|
|
63
|
+
* row that only adds something can spread these props into it rather than start over. The list
|
|
64
|
+
* supplies the key, so a custom row doesn't need one.
|
|
65
|
+
*/
|
|
66
|
+
renderItem?(props: CommentListItemRenderProps): ReactNode
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
/**
|
|
@@ -52,36 +82,36 @@ export function CommentsList({
|
|
|
52
82
|
renderItem,
|
|
53
83
|
}: CommentsListProps) {
|
|
54
84
|
return (
|
|
55
|
-
<div className="cmt-list">
|
|
85
|
+
<div className="tlui-cmt-list">
|
|
56
86
|
{(header !== undefined || headerAction) && (
|
|
57
|
-
<div className="cmt-list__header">
|
|
58
|
-
{header !== undefined && <span className="cmt-list__header-title">{header}</span>}
|
|
87
|
+
<div className="tlui-cmt-list__header">
|
|
88
|
+
{header !== undefined && <span className="tlui-cmt-list__header-title">{header}</span>}
|
|
59
89
|
{headerAction}
|
|
60
90
|
</div>
|
|
61
91
|
)}
|
|
62
92
|
{items.length === 0 ? (
|
|
63
|
-
<div className="cmt-list__empty">{empty}</div>
|
|
93
|
+
<div className="tlui-cmt-list__empty">{empty}</div>
|
|
64
94
|
) : (
|
|
65
|
-
<div className="cmt-list__items">
|
|
66
|
-
{items.map((item) =>
|
|
67
|
-
|
|
68
|
-
|
|
95
|
+
<div className="tlui-cmt-list__items">
|
|
96
|
+
{items.map((item) => {
|
|
97
|
+
const props: CommentListItemRenderProps = { ...item, resolvedLabel, onSelect }
|
|
98
|
+
// A custom row is wrapped rather than keyed directly: it's the consumer's element,
|
|
99
|
+
// and requiring them to remember a key is the kind of thing that only shows up as a
|
|
100
|
+
// console warning in someone else's app.
|
|
101
|
+
return renderItem ? (
|
|
102
|
+
<Fragment key={item.id}>{renderItem(props)}</Fragment>
|
|
69
103
|
) : (
|
|
70
|
-
<CommentListItem
|
|
71
|
-
key={item.id}
|
|
72
|
-
{...item}
|
|
73
|
-
resolvedLabel={resolvedLabel}
|
|
74
|
-
onSelect={onSelect}
|
|
75
|
-
/>
|
|
104
|
+
<CommentListItem key={item.id} {...props} />
|
|
76
105
|
)
|
|
77
|
-
)}
|
|
106
|
+
})}
|
|
78
107
|
</div>
|
|
79
108
|
)}
|
|
80
109
|
</div>
|
|
81
110
|
)
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
|
|
113
|
+
/** One thread's row in a {@link CommentsList}. @public @react */
|
|
114
|
+
export function CommentListItem({
|
|
85
115
|
id,
|
|
86
116
|
author,
|
|
87
117
|
preview,
|
|
@@ -90,50 +120,55 @@ function CommentListItem({
|
|
|
90
120
|
page,
|
|
91
121
|
count,
|
|
92
122
|
selected,
|
|
123
|
+
reactions,
|
|
124
|
+
href,
|
|
93
125
|
resolvedLabel = 'Resolved',
|
|
94
126
|
onSelect,
|
|
95
|
-
}:
|
|
96
|
-
const
|
|
127
|
+
}: CommentListItemRenderProps) {
|
|
128
|
+
const msg = useTranslation()
|
|
129
|
+
const handleClick = (e: MouseEvent) => {
|
|
130
|
+
if (href && isOpenInNewTabClick(e)) return
|
|
131
|
+
e.preventDefault()
|
|
97
132
|
if (onSelect) onSelect(id)
|
|
98
133
|
}
|
|
134
|
+
const Tag = href ? 'a' : 'button'
|
|
135
|
+
// `count` is the thread's total comments; a reply is every comment after the opening one.
|
|
136
|
+
const replies = count !== undefined ? replyCountLabel(msg, count - 1) : null
|
|
99
137
|
return (
|
|
100
|
-
<
|
|
101
|
-
type
|
|
102
|
-
className={
|
|
138
|
+
<Tag
|
|
139
|
+
{...(href ? { href } : { type: 'button' })}
|
|
140
|
+
className={
|
|
141
|
+
selected ? 'tlui-cmt-list__item tlui-cmt-list__item--selected' : 'tlui-cmt-list__item'
|
|
142
|
+
}
|
|
103
143
|
data-resolved={resolved || undefined}
|
|
104
144
|
onClick={handleClick}
|
|
105
145
|
>
|
|
106
|
-
<Avatar
|
|
107
|
-
<div className="cmt-list__item-body">
|
|
146
|
+
<Avatar author={author} />
|
|
147
|
+
<div className="tlui-cmt-list__item-body">
|
|
108
148
|
<Byline author={author} date={date} />
|
|
109
|
-
<div className="cmt-list__item-preview">{preview}</div>
|
|
110
|
-
{
|
|
111
|
-
|
|
149
|
+
<div className="tlui-cmt-list__item-preview">{preview}</div>
|
|
150
|
+
{reactions && <Reactions reactions={reactions} canReact={false} enableHoverList={false} />}
|
|
151
|
+
{(resolved || page !== undefined || replies) && (
|
|
152
|
+
<div className="tlui-cmt-list__item-meta">
|
|
112
153
|
{resolved && (
|
|
113
|
-
<span className="cmt-list__item-resolved">
|
|
154
|
+
<span className="tlui-cmt-list__item-resolved">
|
|
114
155
|
<CheckIcon />
|
|
115
156
|
{resolvedLabel}
|
|
116
157
|
</span>
|
|
117
158
|
)}
|
|
118
|
-
{page !== undefined && <span className="cmt-list__item-page">{page}</span>}
|
|
159
|
+
{page !== undefined && <span className="tlui-cmt-list__item-page">{page}</span>}
|
|
160
|
+
{replies && <span className="tlui-cmt-list__item-replies">{replies}</span>}
|
|
119
161
|
</div>
|
|
120
162
|
)}
|
|
121
163
|
</div>
|
|
122
|
-
|
|
123
|
-
</button>
|
|
164
|
+
</Tag>
|
|
124
165
|
)
|
|
125
166
|
}
|
|
126
167
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
strokeWidth="1.5"
|
|
134
|
-
strokeLinecap="round"
|
|
135
|
-
strokeLinejoin="round"
|
|
136
|
-
/>
|
|
137
|
-
</svg>
|
|
138
|
-
)
|
|
168
|
+
/**
|
|
169
|
+
* Whether a click should be left to the browser's link handling (new tab, new window, or download).
|
|
170
|
+
* @public
|
|
171
|
+
*/
|
|
172
|
+
export function isOpenInNewTabClick(e: MouseEvent) {
|
|
173
|
+
return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0
|
|
139
174
|
}
|