@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/CHANGELOG.md +24 -0
- package/README.md +20 -0
- package/dist/index.d.mts +163 -48
- package/dist/index.d.ts +163 -48
- package/dist/index.js +424 -156
- package/dist/index.mjs +425 -149
- package/docs/composer.md +65 -0
- package/docs/mention-portals.md +105 -0
- package/package.json +11 -24
- package/src/demos/Comment.stories.jsx +0 -593
- package/src/demos/CommentComposer.stories.jsx +0 -66
- package/src/elements/Comment.jsx +0 -240
- package/src/elements/Comment.test.jsx +0 -151
- package/src/elements/CommentComposer.jsx +0 -184
- package/src/elements/CommentComposer.test.jsx +0 -125
- package/src/elements/CommentMention.jsx +0 -232
- package/src/elements/CommentMention.test.jsx +0 -150
- package/src/elements/CommentMentionExtension.jsx +0 -56
- package/src/index.js +0 -3
- package/tsconfig.json +0 -10
package/docs/composer.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Comment composer
|
|
2
|
+
|
|
3
|
+
`CommentComposer` owns the editor presentation and accessible interactions. The host owns comment transport, authorisation, discussion threads, mention lookup and request status.
|
|
4
|
+
|
|
5
|
+
## Values and mentions
|
|
6
|
+
|
|
7
|
+
The existing `value` string and `onChange({ target: { value } })` contract remains available. Legacy `onChange` continues to use Tiptap `getText()`: mentions are `@handle` (or `@label`, then `@mention` when neither exists). The new snapshot's `text` uses the full visible mention label `@handle · Name`, suitable for Payload's existing transport body. Plain-text echoes of either form preserve mention nodes and the selection.
|
|
8
|
+
|
|
9
|
+
`valueFormat="html"` is the backward-compatible default for string input. Set `valueFormat="text"` for literal application text (so `<em>text</em>` remains literal text), and use `richValue` JSON to restore rich drafts. Treating untrusted strings as HTML should be a deliberate host decision.
|
|
10
|
+
|
|
11
|
+
Use `onValueChange` for a complete editor snapshot:
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<CommentComposer
|
|
15
|
+
value={text}
|
|
16
|
+
onChange={(event) => setText(event.target.value)}
|
|
17
|
+
onValueChange={({ text, html, json, mentions, isEmpty }) =>
|
|
18
|
+
saveDraft({ text, json, mentions })
|
|
19
|
+
}
|
|
20
|
+
/>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The snapshot has normalised `text`, original `legacyText`, serialised `html`, Tiptap `json`, ordered `mentions` (`{ id, handle, label }`; IDs retain their original string/number type or null), and `isEmpty`. Mentions are read from the document model, including immediately after insertion; no DOM selector, MutationObserver or node-view timing workaround is needed. Snapshot `text` replaces NBSP with ordinary space, removes U+200B–U+200D/U+FEFF and replaces each model line break with one space; it does not trim. Visual node-view layout does not add artificial line breaks. Duplicate mentions remain separate occurrences. Snapshot HTML is editor output, not a general-purpose sanitiser for untrusted HTML.
|
|
24
|
+
|
|
25
|
+
Use `richValue` with the snapshot's JSON to restore/drive a rich draft without losing mention identities. If both `richValue` and `value` are provided, `richValue` takes precedence. The composer recognises echoes of the most recent 100 model transactions so normal batched/debounced parent updates do not overwrite newer keystrokes; older updates are treated as explicit controlled replacements. This history is bounded even when an observing host never echoes values. Controlled prop replacements do not emit change callbacks. Editor edits and public `clear()` do. An omitted value is uncontrolled and an initially empty editor disables submission.
|
|
26
|
+
|
|
27
|
+
## Focus and refs
|
|
28
|
+
|
|
29
|
+
The existing forwarded `ref` still points to the editor's wrapping `HTMLDivElement`. Add `composerRef` for a `CommentComposerHandle`:
|
|
30
|
+
|
|
31
|
+
| Method | Behaviour |
|
|
32
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `focus(position = 'end')` | Focus the editor at `start`, `end`, `all` or a numeric document position; return success. |
|
|
34
|
+
| `clear()` | Clear content and emit value callbacks; return success. Controlled hosts should echo the cleared value. |
|
|
35
|
+
| `getValue()` | Return a current `CommentComposerValue`, or `null` before the editor is ready. |
|
|
36
|
+
| `submit()` | Request normal form submission using the same empty/disabled/pending guard as the internal button. |
|
|
37
|
+
|
|
38
|
+
`editorLabel` keeps the published default “Comment”; Payload can pass “Write a comment” and labels the multiline textbox directly. `editorAttributes` forwards editable `id`, `aria-describedby`, `aria-invalid` and `aria-required`, including runtime updates. `autoFocus` is opt-in and schedules focus after editor readiness/on re-enabling, with frame cleanup on unmount. Imperative focus requests made before editor readiness are queued. Disabled/pending composers reject focus, clear and submit operations and prevent editing. `getValue` remains available for reading a pending draft. The form exposes `aria-busy` while `isSubmitting` is true. `submittingLabel` defaults to “Posting...” and supports context-specific pending labels.
|
|
39
|
+
|
|
40
|
+
## External submit
|
|
41
|
+
|
|
42
|
+
```jsx
|
|
43
|
+
const composerRef = useRef(null);
|
|
44
|
+
|
|
45
|
+
<CommentComposer
|
|
46
|
+
id="reply"
|
|
47
|
+
composerRef={composerRef}
|
|
48
|
+
externalSubmit
|
|
49
|
+
submitOnModEnter
|
|
50
|
+
editorLabel="Reply to this thread"
|
|
51
|
+
valueFormat="text"
|
|
52
|
+
value={text}
|
|
53
|
+
onValueChange={(value) => setText(value.text)}
|
|
54
|
+
onSubmit={(event) => sendReply(composerRef.current.getValue())}
|
|
55
|
+
/>
|
|
56
|
+
<Button form="reply" type="submit">Send reply</Button>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`externalSubmit` omits only the built-in submit button. Cancellation and action slots remain usable. Associate a native submit button with the form `id`, or call `composerRef.current.submit()`. All routes preserve `onSubmit`'s form event and call `preventDefault` before handing control to the host. The opt-in `submitOnModEnter` enables Control/Command+Enter without overriding ordinary Enter or composition. The native `onKeyDown` handler is composed once before this shortcut, so either it or `onKeyDownCapture` can prevent the package action.
|
|
60
|
+
|
|
61
|
+
## Layout and theme
|
|
62
|
+
|
|
63
|
+
The default editor remains 120px high; `rows` sets 30px per row. Colour modes use existing semantic tokens; root padding, border widths and editor height are unchanged when themes switch. The editor transition respects reduced motion. The host supplies the panel width; no portal, application focus trap or transport logic is added. Supply React 19 and the package's declared Tiptap 3 peers consistently.
|
|
64
|
+
|
|
65
|
+
Storybook includes basic text submission, mention insertion, external submission/focus, rich drafts, disabled/pending states, narrow layout and light/dark colour modes. Interaction plays exercise the real editor, rapid controlled typing, accessible menu selection/Escape, and host-controlled failure/retry without clearing a failed draft.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Mention menus in dialogs
|
|
2
|
+
|
|
3
|
+
`CommentMention` automatically portals its menu into the nearest connected
|
|
4
|
+
`[role="dialog"]`, `[role="alertdialog"]` or native `<dialog>` containing its
|
|
5
|
+
editor. This keeps menu pointer and keyboard interaction inside an owning modal
|
|
6
|
+
Drawer or Modal. Outside a dialog, the menu uses its ordinary document-body
|
|
7
|
+
portal.
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
<Drawer>
|
|
11
|
+
<DrawerTrigger asChild>
|
|
12
|
+
<Button>Open comments</Button>
|
|
13
|
+
</DrawerTrigger>
|
|
14
|
+
<DrawerContent aria-label="Comments" aria-describedby={undefined}>
|
|
15
|
+
<DrawerHeader>
|
|
16
|
+
<ModalTitle>Comments</ModalTitle>
|
|
17
|
+
</DrawerHeader>
|
|
18
|
+
<DrawerBody>
|
|
19
|
+
<CommentComposer
|
|
20
|
+
value={draft}
|
|
21
|
+
valueFormat="text"
|
|
22
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
23
|
+
onValueChange={setSnapshot}
|
|
24
|
+
>
|
|
25
|
+
<CommentMention peopleList={people} onQueryChange={setQuery} />
|
|
26
|
+
</CommentComposer>
|
|
27
|
+
</DrawerBody>
|
|
28
|
+
</DrawerContent>
|
|
29
|
+
</Drawer>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Import the Drawer components from `@edux-design/drawers`, `Button` from
|
|
33
|
+
`@edux-design/buttons`, and the composer and mention components from
|
|
34
|
+
`@edux-design/comments`. `ModalTitle` from `@edux-design/modals` supplies the shared
|
|
35
|
+
Dialog title in the Drawer header. Modal components have the same automatic
|
|
36
|
+
ownership behaviour.
|
|
37
|
+
|
|
38
|
+
## Optional portal override
|
|
39
|
+
|
|
40
|
+
| `portalContainer` | Behaviour |
|
|
41
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
42
|
+
| Omitted or `undefined` | Find the nearest owning dialog; otherwise use the body portal. |
|
|
43
|
+
| Connected `HTMLElement` in the editor's document | Portal into that element. It takes precedence over automatic ownership. |
|
|
44
|
+
| `null` | Explicitly use the ordinary body portal. |
|
|
45
|
+
| Detached element or element from another document | Ignore the override and use automatic ownership. |
|
|
46
|
+
|
|
47
|
+
Pass the committed element itself, rather than a ref object. A callback ref stored
|
|
48
|
+
in state makes the element available after mount:
|
|
49
|
+
|
|
50
|
+
```jsx
|
|
51
|
+
const [mentionHost, setMentionHost] = React.useState(null);
|
|
52
|
+
|
|
53
|
+
<ModalContent>
|
|
54
|
+
<ModalHeader>
|
|
55
|
+
<ModalTitle>Comments</ModalTitle>
|
|
56
|
+
<ModalDescription>Discuss the current draft.</ModalDescription>
|
|
57
|
+
</ModalHeader>
|
|
58
|
+
<ModalBody>
|
|
59
|
+
<CommentComposer>
|
|
60
|
+
<CommentMention
|
|
61
|
+
peopleList={people}
|
|
62
|
+
portalContainer={mentionHost ?? undefined}
|
|
63
|
+
/>
|
|
64
|
+
</CommentComposer>
|
|
65
|
+
<div ref={setMentionHost} />
|
|
66
|
+
</ModalBody>
|
|
67
|
+
</ModalContent>;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use `?? undefined` when a not-yet-mounted callback ref should retain automatic
|
|
71
|
+
ownership. Passing its initial `null` directly requests the body portal. Keep a
|
|
72
|
+
custom host inside the active modal's interactive surface. A body portal or a host
|
|
73
|
+
outside that modal remains subject to the modal's pointer and focus restrictions.
|
|
74
|
+
|
|
75
|
+
Ownership resolves after refs commit. While a mention query is active, it follows
|
|
76
|
+
editor reparenting, dialog role changes, and removal of an override element.
|
|
77
|
+
Nested dialogs select the nearest owning dialog. Each composer resolves its own
|
|
78
|
+
owner. Tracking stops when the query closes and when the composer unmounts.
|
|
79
|
+
|
|
80
|
+
## Interaction and values
|
|
81
|
+
|
|
82
|
+
Typing `@query` filters the supplied people by handle and calls `onQueryChange`.
|
|
83
|
+
Consumers continue to own asynchronous lookup, permissions and persistence.
|
|
84
|
+
Pointer activation or ArrowDown/ArrowUp followed by Enter inserts one mention,
|
|
85
|
+
closes the menu, and returns focus to the editor. Escape first dismisses an open
|
|
86
|
+
mention menu and returns focus to the editor; a subsequent Escape can close the
|
|
87
|
+
owning dialog according to that dialog's existing policy.
|
|
88
|
+
|
|
89
|
+
The mention menu retains `trapFocus={false}` and
|
|
90
|
+
`disableOutsidePointerEvents={false}`. The owning Drawer or Modal retains its
|
|
91
|
+
normal focus trap and outside-pointer blocking. Disabled or submitting composers
|
|
92
|
+
do not offer editable mention interaction.
|
|
93
|
+
|
|
94
|
+
Portal ownership does not change the composer model or submission contract.
|
|
95
|
+
`onValueChange` and `composerRef.current.getValue()` continue returning `text`,
|
|
96
|
+
`legacyText`, `html`, `json`, `mentions` and `isEmpty`. Existing `onChange`,
|
|
97
|
+
`onSubmit`, `onCancel`, `externalSubmit`, `submitOnModEnter`, editor labels,
|
|
98
|
+
autofocus, avatar/actions/children, and all composer handle methods remain
|
|
99
|
+
available. See [Composer values, focus and external submission](./composer.md).
|
|
100
|
+
|
|
101
|
+
The Storybook **Content / Comment Mention Portals** stories exercise real modal
|
|
102
|
+
Drawers and Modals, pointer and keyboard selection, Escape ordering, nested and
|
|
103
|
+
changing owners, overrides, independent composers, asynchronous people, locked
|
|
104
|
+
states, and a dark narrow Drawer. They use synthetic people and strict
|
|
105
|
+
accessibility checks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edux-design/comments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -8,36 +8,17 @@
|
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
11
12
|
"import": "./dist/index.mjs",
|
|
12
|
-
"require": "./dist/index.js"
|
|
13
|
-
"types": "./dist/index.d.ts"
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"lint": "eslint . --max-warnings 0",
|
|
18
|
-
"build": "tsup src/index.js --format esm,cjs --dts",
|
|
19
|
-
"dev": "tsup src/index.js --watch --format esm,cjs --dts",
|
|
20
|
-
"generate:component": "turbo gen react-component",
|
|
21
|
-
"check-types": "tsc --noEmit"
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"typescript": "^5.9.2",
|
|
25
|
-
"tailwindcss": "^4.1.12",
|
|
26
|
-
"postcss": "^8.5.6",
|
|
27
|
-
"tsup": "^8.5.0",
|
|
28
|
-
"@tailwindcss/cli": "^4.1.12",
|
|
29
|
-
"@tailwindcss/postcss": "^4.1.12",
|
|
30
|
-
"@tiptap/core": "^2.14.0",
|
|
31
|
-
"@tiptap/react": "^2.14.0",
|
|
32
|
-
"@tiptap/starter-kit": "^2.14.0",
|
|
33
|
-
"@tiptap/extension-placeholder": "^2.14.0"
|
|
34
|
-
},
|
|
35
16
|
"dependencies": {
|
|
36
17
|
"@edux-design/avatars": "*",
|
|
37
18
|
"@edux-design/buttons": "*",
|
|
38
19
|
"@edux-design/forms": "*",
|
|
39
20
|
"@edux-design/mentions": "*",
|
|
40
|
-
"@edux-design/menus": "
|
|
21
|
+
"@edux-design/menus": "^1.1.0",
|
|
41
22
|
"@edux-design/utils": "*"
|
|
42
23
|
},
|
|
43
24
|
"peerDependencies": {
|
|
@@ -47,5 +28,11 @@
|
|
|
47
28
|
"@tiptap/react": "^3.15.3",
|
|
48
29
|
"@tiptap/starter-kit": "^3.15.3",
|
|
49
30
|
"@tiptap/extension-placeholder": "^3.15.3"
|
|
50
|
-
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"docs",
|
|
35
|
+
"README.md",
|
|
36
|
+
"CHANGELOG.md"
|
|
37
|
+
]
|
|
51
38
|
}
|