@astryxdesign/lab 0.1.9-canary.faa200a → 0.1.9-canary.fdcae1d
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/RichTextEditor/RichTextEditor.d.ts +29 -5
- package/dist/RichTextEditor/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor/RichTextEditor.js +87 -6
- package/dist/RichTextEditor/index.d.ts +1 -1
- package/dist/RichTextEditor/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/RichTextEditor/RichTextEditor.doc.mjs +6 -1
- package/src/RichTextEditor/RichTextEditor.test.tsx +125 -3
- package/src/RichTextEditor/RichTextEditor.tsx +151 -29
- package/src/RichTextEditor/index.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -33,6 +33,29 @@ export interface RichTextEditorStatus {
|
|
|
33
33
|
/** Optional message to display below the editor. */
|
|
34
34
|
message?: string;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Imperative handle exposed via `ref`. Lets callers focus, clear, and read the
|
|
38
|
+
* editor without wiring a custom plugin. Available after mount.
|
|
39
|
+
*/
|
|
40
|
+
export interface RichTextEditorRef {
|
|
41
|
+
/**
|
|
42
|
+
* Move focus into the editor's editable surface. No-op when the editor is
|
|
43
|
+
* read-only or disabled.
|
|
44
|
+
*/
|
|
45
|
+
focus: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Remove all content, resetting the editor to a single empty paragraph.
|
|
48
|
+
* No-op when the editor is read-only or disabled.
|
|
49
|
+
*/
|
|
50
|
+
clear: () => void;
|
|
51
|
+
/** Read the current `EditorState`. Serialize with `.toJSON()` to persist. */
|
|
52
|
+
getEditorState: () => EditorState;
|
|
53
|
+
/**
|
|
54
|
+
* Access the underlying `LexicalEditor` instance for advanced use cases
|
|
55
|
+
* (custom commands, listeners, node transforms).
|
|
56
|
+
*/
|
|
57
|
+
getEditor: () => LexicalEditor;
|
|
58
|
+
}
|
|
36
59
|
export interface RichTextEditorProps extends Omit<BaseProps, 'onChange' | 'defaultValue'> {
|
|
37
60
|
/** Label text for the editor (always rendered for accessibility). */
|
|
38
61
|
label: string;
|
|
@@ -151,16 +174,17 @@ export interface RichTextEditorProps extends Omit<BaseProps, 'onChange' | 'defau
|
|
|
151
174
|
*
|
|
152
175
|
* @example
|
|
153
176
|
* ```
|
|
154
|
-
* import {RichTextEditor} from '@astryxdesign/lab';
|
|
177
|
+
* import {RichTextEditor, type RichTextEditorRef} from '@astryxdesign/lab';
|
|
178
|
+
*
|
|
179
|
+
* const ref = useRef<RichTextEditorRef>(null);
|
|
155
180
|
* <RichTextEditor
|
|
181
|
+
* ref={ref}
|
|
156
182
|
* label="Notes"
|
|
157
183
|
* placeholder="Write something..."
|
|
158
184
|
* onChange={state => save(JSON.stringify(state.toJSON()))}
|
|
159
185
|
* />
|
|
186
|
+
* // Later: ref.current?.focus(); ref.current?.clear();
|
|
160
187
|
* ```
|
|
161
188
|
*/
|
|
162
|
-
export declare
|
|
163
|
-
export declare namespace RichTextEditor {
|
|
164
|
-
var displayName: string;
|
|
165
|
-
}
|
|
189
|
+
export declare const RichTextEditor: import("react").ForwardRefExoticComponent<RichTextEditorProps & import("react").RefAttributes<RichTextEditorRef>>;
|
|
166
190
|
//# sourceMappingURL=RichTextEditor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAOL,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAgBf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAkBxD,OAAO,EAAe,KAAK,WAAW,EAAC,MAAM,mBAAmB,CAAC;AACjE,YAAY,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAKnD,OAAO,KAAK,EACV,WAAW,EACX,KAAK,EACL,aAAa,EACb,WAAW,EAEZ,MAAM,SAAS,CAAC;AAmGjB,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvE,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,6EAA6E;IAC7E,cAAc,EAAE,MAAM,WAAW,CAAC;IAClC;;;OAGG;IACH,SAAS,EAAE,MAAM,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAC/C,SAAS,EACT,UAAU,GAAG,cAAc,CAC5B;IACC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACrE,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC1C,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,cAAc,mHAwJzB,CAAC"}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Component Specification Protocol. `lexical` and `@lexical/*` are OPTIONAL peer
|
|
24
24
|
* dependencies — install them to use this component.
|
|
25
25
|
*/
|
|
26
|
-
import { useEffect, useId, useMemo, useRef } from 'react';
|
|
26
|
+
import { useEffect, useId, useImperativeHandle, useMemo, useRef, forwardRef } from 'react';
|
|
27
27
|
import * as stylex from '@stylexjs/stylex';
|
|
28
28
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
29
29
|
import { colorVars, spacingVars, typographyVars, typeScaleVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
@@ -48,6 +48,37 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
|
48
48
|
import { LinkNode, AutoLinkNode } from '@lexical/link';
|
|
49
49
|
import { CodeNode, CodeHighlightNode } from '@lexical/code';
|
|
50
50
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
51
|
+
/**
|
|
52
|
+
* Serialized state for an empty editor: a root containing a single empty
|
|
53
|
+
* paragraph. Used by the imperative `clear()` handle.
|
|
54
|
+
*
|
|
55
|
+
* We deliberately reset the editor via `editor.setEditorState(...)` rather than
|
|
56
|
+
* an `editor.update(() => { $getRoot()... })` callback, because `$getRoot` /
|
|
57
|
+
* `$createParagraphNode` are *runtime value* imports from the top-level
|
|
58
|
+
* `lexical` package. In the sandbox's Next build a top-level `lexical` value
|
|
59
|
+
* import forces Babel to transpile lexical's raw `src/*.ts` (which uses
|
|
60
|
+
* `declare` class fields) and fails the build. `parseEditorState` /
|
|
61
|
+
* `setEditorState` are methods on the editor instance, so no top-level
|
|
62
|
+
* `lexical` value import is needed. Setting a fresh state still notifies update
|
|
63
|
+
* listeners, so `onChange` fires.
|
|
64
|
+
*/
|
|
65
|
+
const EMPTY_EDITOR_STATE_JSON = JSON.stringify({
|
|
66
|
+
root: {
|
|
67
|
+
children: [{
|
|
68
|
+
children: [],
|
|
69
|
+
direction: null,
|
|
70
|
+
format: '',
|
|
71
|
+
indent: 0,
|
|
72
|
+
type: 'paragraph',
|
|
73
|
+
version: 1
|
|
74
|
+
}],
|
|
75
|
+
direction: null,
|
|
76
|
+
format: '',
|
|
77
|
+
indent: 0,
|
|
78
|
+
type: 'root',
|
|
79
|
+
version: 1
|
|
80
|
+
}
|
|
81
|
+
});
|
|
51
82
|
const styles = {
|
|
52
83
|
wrapper: {
|
|
53
84
|
kXwgrk: "xdt5ytf",
|
|
@@ -76,6 +107,12 @@ const sizeStyles = {
|
|
|
76
107
|
|
|
77
108
|
/** The default OSS node set registered with the editor. */
|
|
78
109
|
const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode, CodeNode, CodeHighlightNode];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Imperative handle exposed via `ref`. Lets callers focus, clear, and read the
|
|
113
|
+
* editor without wiring a custom plugin. Available after mount.
|
|
114
|
+
*/
|
|
115
|
+
|
|
79
116
|
/**
|
|
80
117
|
* A WYSIWYG rich-text editor built on Lexical, styled with Astryx design
|
|
81
118
|
* tokens. Experimental — ships from `@astryxdesign/lab` (canary). `lexical` and
|
|
@@ -88,15 +125,19 @@ const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode,
|
|
|
88
125
|
*
|
|
89
126
|
* @example
|
|
90
127
|
* ```
|
|
91
|
-
* import {RichTextEditor} from '@astryxdesign/lab';
|
|
128
|
+
* import {RichTextEditor, type RichTextEditorRef} from '@astryxdesign/lab';
|
|
129
|
+
*
|
|
130
|
+
* const ref = useRef<RichTextEditorRef>(null);
|
|
92
131
|
* <RichTextEditor
|
|
132
|
+
* ref={ref}
|
|
93
133
|
* label="Notes"
|
|
94
134
|
* placeholder="Write something..."
|
|
95
135
|
* onChange={state => save(JSON.stringify(state.toJSON()))}
|
|
96
136
|
* />
|
|
137
|
+
* // Later: ref.current?.focus(); ref.current?.clear();
|
|
97
138
|
* ```
|
|
98
139
|
*/
|
|
99
|
-
export function RichTextEditor({
|
|
140
|
+
export const RichTextEditor = /*#__PURE__*/forwardRef(function RichTextEditor({
|
|
100
141
|
label,
|
|
101
142
|
isLabelHidden = false,
|
|
102
143
|
description,
|
|
@@ -121,7 +162,7 @@ export function RichTextEditor({
|
|
|
121
162
|
className,
|
|
122
163
|
style,
|
|
123
164
|
...rest
|
|
124
|
-
}) {
|
|
165
|
+
}, ref) {
|
|
125
166
|
const size = useSize(sizeProp, 'md');
|
|
126
167
|
const id = useId();
|
|
127
168
|
const descriptionID = useId();
|
|
@@ -201,12 +242,15 @@ export function RichTextEditor({
|
|
|
201
242
|
onChange: onChange,
|
|
202
243
|
ignoreHistoryMergeTagChange: true,
|
|
203
244
|
ignoreSelectionChange: true
|
|
204
|
-
}), plugins
|
|
245
|
+
}), plugins, /*#__PURE__*/_jsx(EditorRefBridge, {
|
|
246
|
+
editorRef: ref,
|
|
247
|
+
editable: editable
|
|
248
|
+
})]
|
|
205
249
|
})
|
|
206
250
|
})
|
|
207
251
|
})
|
|
208
252
|
});
|
|
209
|
-
}
|
|
253
|
+
});
|
|
210
254
|
RichTextEditor.displayName = 'RichTextEditor';
|
|
211
255
|
|
|
212
256
|
/**
|
|
@@ -221,6 +265,43 @@ function AutoFocusOnMount() {
|
|
|
221
265
|
return null;
|
|
222
266
|
}
|
|
223
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Wires the imperative `RichTextEditorRef` handle. Split into its own plugin so
|
|
270
|
+
* it runs inside the composer context and can reach the `LexicalEditor` via
|
|
271
|
+
* `useLexicalComposerContext()`. Renders nothing.
|
|
272
|
+
*
|
|
273
|
+
* `focus()` and `clear()` are gated on `editable` so a read-only or disabled
|
|
274
|
+
* editor cannot be mutated or focused through the imperative handle — matching
|
|
275
|
+
* the behaviour of the editable surface itself.
|
|
276
|
+
*/
|
|
277
|
+
function EditorRefBridge({
|
|
278
|
+
editorRef,
|
|
279
|
+
editable
|
|
280
|
+
}) {
|
|
281
|
+
const [editor] = useLexicalComposerContext();
|
|
282
|
+
useImperativeHandle(editorRef, () => ({
|
|
283
|
+
focus: () => {
|
|
284
|
+
if (!editable) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
editor.focus();
|
|
288
|
+
},
|
|
289
|
+
clear: () => {
|
|
290
|
+
if (!editable) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Reset to a single empty paragraph via a fresh EditorState. Uses the
|
|
294
|
+
// editor instance's own parse/set methods so we avoid a top-level
|
|
295
|
+
// `lexical` value import (see EMPTY_EDITOR_STATE_JSON). This still
|
|
296
|
+
// notifies update listeners, so `onChange` fires.
|
|
297
|
+
editor.setEditorState(editor.parseEditorState(EMPTY_EDITOR_STATE_JSON));
|
|
298
|
+
},
|
|
299
|
+
getEditorState: () => editor.getEditorState(),
|
|
300
|
+
getEditor: () => editor
|
|
301
|
+
}), [editor, editable]);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
224
305
|
/**
|
|
225
306
|
* Renders the Lexical ContentEditable. Split out so the placeholder
|
|
226
307
|
* discriminated union (`placeholder` + `aria-placeholder` present together, or
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* SYNC: When modified, update this header and the editor component doc files.
|
|
9
9
|
*/
|
|
10
10
|
export { RichTextEditor } from './RichTextEditor';
|
|
11
|
-
export type { RichTextEditorProps, RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize, Transformer, } from './RichTextEditor';
|
|
11
|
+
export type { RichTextEditorProps, RichTextEditorRef, RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize, Transformer, } from './RichTextEditor';
|
|
12
12
|
export { RichTextView } from './RichTextView';
|
|
13
13
|
export type { RichTextViewProps } from './RichTextView';
|
|
14
14
|
export { sharedEditorTheme } from './editorTheme';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,5 +27,5 @@ export { SankeyChart, type SankeyChartProps, SankeyLink, type SankeyLinkProps, S
|
|
|
27
27
|
export * from './Stepper';
|
|
28
28
|
export * from './CircularProgress';
|
|
29
29
|
export { LogStream, type LogStreamProps, type LogEntry, type LogStreamLevel, } from './LogStream';
|
|
30
|
-
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorStatus, type RichTextEditorStatusType, type RichTextEditorSize, type Transformer, RichTextView, type RichTextViewProps, sharedEditorTheme, } from './RichTextEditor';
|
|
30
|
+
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorRef, type RichTextEditorStatus, type RichTextEditorStatusType, type RichTextEditorSize, type Transformer, RichTextView, type RichTextViewProps, sharedEditorTheme, } from './RichTextEditor';
|
|
31
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.1.9-canary.
|
|
3
|
+
"version": "0.1.9-canary.fdcae1d",
|
|
4
4
|
"description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@stylexjs/stylex": ">=0.10.0",
|
|
48
|
-
"@astryxdesign/core": "0.1.9-canary.
|
|
48
|
+
"@astryxdesign/core": "0.1.9-canary.fdcae1d",
|
|
49
49
|
"react": ">=19.0.0",
|
|
50
50
|
"react-dom": ">=19.0.0",
|
|
51
51
|
"lexical": "^0.46.0",
|
|
@@ -100,7 +100,7 @@ export const docs = {
|
|
|
100
100
|
name: 'transformers',
|
|
101
101
|
type: 'ReadonlyArray<Transformer>',
|
|
102
102
|
description:
|
|
103
|
-
'Markdown transformers
|
|
103
|
+
'Markdown transformers: the single source of truth for markdown behaviour. Defaults to the standard @lexical/markdown TRANSFORMERS. In Lexical the same array drives all three markdown operations (shortcut typing, markdown->state import, state->markdown export); this prop wires shortcut typing today and is the intended input for the serialization APIs added in later phases. Pass a custom array to support additional node types (e.g. transformers layered in via the nodes extension point) consistently across all three. Shortcut typing is only applied when hasMarkdownShortcuts is true.',
|
|
104
104
|
default: 'TRANSFORMERS',
|
|
105
105
|
},
|
|
106
106
|
{
|
|
@@ -144,6 +144,11 @@ export const docs = {
|
|
|
144
144
|
description:
|
|
145
145
|
'Register custom node types via the nodes prop on BOTH the editor and the RichTextView so serialized content round-trips.',
|
|
146
146
|
},
|
|
147
|
+
{
|
|
148
|
+
guidance: true,
|
|
149
|
+
description:
|
|
150
|
+
'Use a ref (RichTextEditorRef) to imperatively focus(), clear(), read the state via getEditorState(), or reach the LexicalEditor via getEditor(). The handle is available after mount. focus() and clear() are no-ops when the editor is read-only or disabled, and clear() resets to a single empty paragraph.',
|
|
151
|
+
},
|
|
147
152
|
{
|
|
148
153
|
guidance: false,
|
|
149
154
|
description:
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {render, screen, waitFor} from '@testing-library/react';
|
|
14
|
-
import {useEffect} from 'react';
|
|
14
|
+
import {createRef, useEffect} from 'react';
|
|
15
15
|
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
16
|
-
import type {LexicalEditor} from 'lexical';
|
|
16
|
+
import type {EditorState, LexicalEditor} from 'lexical';
|
|
17
17
|
import {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';
|
|
18
18
|
import {HeadingNode} from '@lexical/rich-text';
|
|
19
19
|
import {
|
|
20
20
|
TRANSFORMERS,
|
|
21
21
|
$convertFromMarkdownString,
|
|
22
22
|
} from '@lexical/markdown';
|
|
23
|
-
import {RichTextEditor} from './RichTextEditor';
|
|
23
|
+
import {RichTextEditor, type RichTextEditorRef} from './RichTextEditor';
|
|
24
24
|
import {RichTextView} from './RichTextView';
|
|
25
25
|
|
|
26
26
|
// Small plugin that captures the editor instance so tests can drive real
|
|
@@ -333,6 +333,128 @@ describe('RichTextEditor', () => {
|
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
|
+
|
|
337
|
+
it('exposes an imperative ref handle after mount', () => {
|
|
338
|
+
const ref = createRef<RichTextEditorRef>();
|
|
339
|
+
render(<RichTextEditor ref={ref} label="Notes" />);
|
|
340
|
+
expect(ref.current).not.toBeNull();
|
|
341
|
+
expect(typeof ref.current?.focus).toBe('function');
|
|
342
|
+
expect(typeof ref.current?.clear).toBe('function');
|
|
343
|
+
expect(typeof ref.current?.getEditorState).toBe('function');
|
|
344
|
+
expect(typeof ref.current?.getEditor).toBe('function');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('ref.focus() runs without throwing and targets the editable surface', () => {
|
|
348
|
+
const ref = createRef<RichTextEditorRef>();
|
|
349
|
+
render(<RichTextEditor ref={ref} label="Notes" />);
|
|
350
|
+
// Lexical dispatches focus via its editor command; jsdom does not always
|
|
351
|
+
// reflect programmatic contenteditable focus onto document.activeElement,
|
|
352
|
+
// so assert the call is wired and the root element is reachable rather
|
|
353
|
+
// than asserting jsdom focus state.
|
|
354
|
+
expect(() => ref.current?.focus()).not.toThrow();
|
|
355
|
+
const root = ref.current?.getEditor().getRootElement();
|
|
356
|
+
expect(root).toBe(screen.getByRole('textbox'));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('ref.getEditorState() returns the current EditorState', () => {
|
|
360
|
+
const ref = createRef<RichTextEditorRef>();
|
|
361
|
+
render(
|
|
362
|
+
<RichTextEditor ref={ref} label="Notes" defaultValue={HELLO_STATE} />,
|
|
363
|
+
);
|
|
364
|
+
const state = ref.current?.getEditorState();
|
|
365
|
+
expect(state).toBeDefined();
|
|
366
|
+
const text = state?.read(() => $getRoot().getTextContent());
|
|
367
|
+
expect(text).toBe('Hello world');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('ref.getEditor() returns the underlying LexicalEditor', () => {
|
|
371
|
+
const ref = createRef<RichTextEditorRef>();
|
|
372
|
+
render(<RichTextEditor ref={ref} label="Notes" />);
|
|
373
|
+
const editor = ref.current?.getEditor();
|
|
374
|
+
expect(editor).toBeDefined();
|
|
375
|
+
expect(typeof editor?.update).toBe('function');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('ref.clear() resets the editor to a single empty paragraph', async () => {
|
|
379
|
+
const ref = createRef<RichTextEditorRef>();
|
|
380
|
+
render(
|
|
381
|
+
<RichTextEditor ref={ref} label="Notes" defaultValue={HELLO_STATE} />,
|
|
382
|
+
);
|
|
383
|
+
ref.current?.clear();
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
const state: EditorState | undefined = ref.current?.getEditorState();
|
|
386
|
+
const text = state?.read(() => $getRoot().getTextContent());
|
|
387
|
+
expect(text).toBe('');
|
|
388
|
+
});
|
|
389
|
+
// The root should hold exactly one (empty) paragraph, not zero children —
|
|
390
|
+
// a bare root breaks selection/typing.
|
|
391
|
+
const state = ref.current?.getEditorState();
|
|
392
|
+
const childCount = state?.read(() => $getRoot().getChildrenSize());
|
|
393
|
+
expect(childCount).toBe(1);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('ref.clear() fires onChange', async () => {
|
|
397
|
+
const ref = createRef<RichTextEditorRef>();
|
|
398
|
+
const onChange = vi.fn();
|
|
399
|
+
render(
|
|
400
|
+
<RichTextEditor
|
|
401
|
+
ref={ref}
|
|
402
|
+
label="Notes"
|
|
403
|
+
defaultValue={HELLO_STATE}
|
|
404
|
+
onChange={onChange}
|
|
405
|
+
/>,
|
|
406
|
+
);
|
|
407
|
+
// Wait for the editor to mount and the initial state to settle so any
|
|
408
|
+
// seed-time onChange has already fired before we assert on clear().
|
|
409
|
+
await waitFor(() => expect(ref.current).not.toBeNull());
|
|
410
|
+
onChange.mockClear();
|
|
411
|
+
ref.current?.clear();
|
|
412
|
+
await waitFor(() => expect(onChange).toHaveBeenCalled());
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('ref.clear() and ref.focus() are no-ops when isReadOnly', async () => {
|
|
416
|
+
const ref = createRef<RichTextEditorRef>();
|
|
417
|
+
const onChange = vi.fn();
|
|
418
|
+
render(
|
|
419
|
+
<RichTextEditor
|
|
420
|
+
ref={ref}
|
|
421
|
+
label="Notes"
|
|
422
|
+
defaultValue={HELLO_STATE}
|
|
423
|
+
onChange={onChange}
|
|
424
|
+
isReadOnly
|
|
425
|
+
/>,
|
|
426
|
+
);
|
|
427
|
+
onChange.mockClear();
|
|
428
|
+
ref.current?.focus();
|
|
429
|
+
ref.current?.clear();
|
|
430
|
+
// Give any (unexpected) async update a chance to flush before asserting.
|
|
431
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
432
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
433
|
+
const state = ref.current?.getEditorState();
|
|
434
|
+
const text = state?.read(() => $getRoot().getTextContent());
|
|
435
|
+
expect(text).toBe('Hello world');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('ref.clear() is a no-op when isDisabled', async () => {
|
|
439
|
+
const ref = createRef<RichTextEditorRef>();
|
|
440
|
+
const onChange = vi.fn();
|
|
441
|
+
render(
|
|
442
|
+
<RichTextEditor
|
|
443
|
+
ref={ref}
|
|
444
|
+
label="Notes"
|
|
445
|
+
defaultValue={HELLO_STATE}
|
|
446
|
+
onChange={onChange}
|
|
447
|
+
isDisabled
|
|
448
|
+
/>,
|
|
449
|
+
);
|
|
450
|
+
onChange.mockClear();
|
|
451
|
+
ref.current?.clear();
|
|
452
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
453
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
454
|
+
const state = ref.current?.getEditorState();
|
|
455
|
+
const text = state?.read(() => $getRoot().getTextContent());
|
|
456
|
+
expect(text).toBe('Hello world');
|
|
457
|
+
});
|
|
336
458
|
});
|
|
337
459
|
|
|
338
460
|
describe('RichTextView', () => {
|
|
@@ -24,7 +24,16 @@
|
|
|
24
24
|
* dependencies — install them to use this component.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
useEffect,
|
|
29
|
+
useId,
|
|
30
|
+
useImperativeHandle,
|
|
31
|
+
useMemo,
|
|
32
|
+
useRef,
|
|
33
|
+
forwardRef,
|
|
34
|
+
type ReactNode,
|
|
35
|
+
type Ref,
|
|
36
|
+
} from 'react';
|
|
28
37
|
import * as stylex from '@stylexjs/stylex';
|
|
29
38
|
import {
|
|
30
39
|
colorVars,
|
|
@@ -73,6 +82,40 @@ import type {
|
|
|
73
82
|
EditorThemeClasses,
|
|
74
83
|
} from 'lexical';
|
|
75
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Serialized state for an empty editor: a root containing a single empty
|
|
87
|
+
* paragraph. Used by the imperative `clear()` handle.
|
|
88
|
+
*
|
|
89
|
+
* We deliberately reset the editor via `editor.setEditorState(...)` rather than
|
|
90
|
+
* an `editor.update(() => { $getRoot()... })` callback, because `$getRoot` /
|
|
91
|
+
* `$createParagraphNode` are *runtime value* imports from the top-level
|
|
92
|
+
* `lexical` package. In the sandbox's Next build a top-level `lexical` value
|
|
93
|
+
* import forces Babel to transpile lexical's raw `src/*.ts` (which uses
|
|
94
|
+
* `declare` class fields) and fails the build. `parseEditorState` /
|
|
95
|
+
* `setEditorState` are methods on the editor instance, so no top-level
|
|
96
|
+
* `lexical` value import is needed. Setting a fresh state still notifies update
|
|
97
|
+
* listeners, so `onChange` fires.
|
|
98
|
+
*/
|
|
99
|
+
const EMPTY_EDITOR_STATE_JSON = JSON.stringify({
|
|
100
|
+
root: {
|
|
101
|
+
children: [
|
|
102
|
+
{
|
|
103
|
+
children: [],
|
|
104
|
+
direction: null,
|
|
105
|
+
format: '',
|
|
106
|
+
indent: 0,
|
|
107
|
+
type: 'paragraph',
|
|
108
|
+
version: 1,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
direction: null,
|
|
112
|
+
format: '',
|
|
113
|
+
indent: 0,
|
|
114
|
+
type: 'root',
|
|
115
|
+
version: 1,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
76
119
|
const styles = stylex.create({
|
|
77
120
|
wrapper: {
|
|
78
121
|
flexDirection: 'column',
|
|
@@ -147,6 +190,30 @@ export interface RichTextEditorStatus {
|
|
|
147
190
|
message?: string;
|
|
148
191
|
}
|
|
149
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Imperative handle exposed via `ref`. Lets callers focus, clear, and read the
|
|
195
|
+
* editor without wiring a custom plugin. Available after mount.
|
|
196
|
+
*/
|
|
197
|
+
export interface RichTextEditorRef {
|
|
198
|
+
/**
|
|
199
|
+
* Move focus into the editor's editable surface. No-op when the editor is
|
|
200
|
+
* read-only or disabled.
|
|
201
|
+
*/
|
|
202
|
+
focus: () => void;
|
|
203
|
+
/**
|
|
204
|
+
* Remove all content, resetting the editor to a single empty paragraph.
|
|
205
|
+
* No-op when the editor is read-only or disabled.
|
|
206
|
+
*/
|
|
207
|
+
clear: () => void;
|
|
208
|
+
/** Read the current `EditorState`. Serialize with `.toJSON()` to persist. */
|
|
209
|
+
getEditorState: () => EditorState;
|
|
210
|
+
/**
|
|
211
|
+
* Access the underlying `LexicalEditor` instance for advanced use cases
|
|
212
|
+
* (custom commands, listeners, node transforms).
|
|
213
|
+
*/
|
|
214
|
+
getEditor: () => LexicalEditor;
|
|
215
|
+
}
|
|
216
|
+
|
|
150
217
|
export interface RichTextEditorProps extends Omit<
|
|
151
218
|
BaseProps,
|
|
152
219
|
'onChange' | 'defaultValue'
|
|
@@ -269,40 +336,50 @@ export interface RichTextEditorProps extends Omit<
|
|
|
269
336
|
*
|
|
270
337
|
* @example
|
|
271
338
|
* ```
|
|
272
|
-
* import {RichTextEditor} from '@astryxdesign/lab';
|
|
339
|
+
* import {RichTextEditor, type RichTextEditorRef} from '@astryxdesign/lab';
|
|
340
|
+
*
|
|
341
|
+
* const ref = useRef<RichTextEditorRef>(null);
|
|
273
342
|
* <RichTextEditor
|
|
343
|
+
* ref={ref}
|
|
274
344
|
* label="Notes"
|
|
275
345
|
* placeholder="Write something..."
|
|
276
346
|
* onChange={state => save(JSON.stringify(state.toJSON()))}
|
|
277
347
|
* />
|
|
348
|
+
* // Later: ref.current?.focus(); ref.current?.clear();
|
|
278
349
|
* ```
|
|
279
350
|
*/
|
|
280
|
-
export
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
351
|
+
export const RichTextEditor = forwardRef<
|
|
352
|
+
RichTextEditorRef,
|
|
353
|
+
RichTextEditorProps
|
|
354
|
+
>(function RichTextEditor(
|
|
355
|
+
{
|
|
356
|
+
label,
|
|
357
|
+
isLabelHidden = false,
|
|
358
|
+
description,
|
|
359
|
+
isOptional = false,
|
|
360
|
+
isRequired = false,
|
|
361
|
+
defaultValue,
|
|
362
|
+
onChange,
|
|
363
|
+
placeholder,
|
|
364
|
+
isReadOnly = false,
|
|
365
|
+
isDisabled = false,
|
|
366
|
+
status,
|
|
367
|
+
width,
|
|
368
|
+
labelTooltip,
|
|
369
|
+
size: sizeProp,
|
|
370
|
+
nodes,
|
|
371
|
+
plugins,
|
|
372
|
+
hasMarkdownShortcuts = true,
|
|
373
|
+
transformers = TRANSFORMERS,
|
|
374
|
+
hasAutoFocus = false,
|
|
375
|
+
namespace = 'astryx-editor',
|
|
376
|
+
xstyle,
|
|
377
|
+
className,
|
|
378
|
+
style,
|
|
379
|
+
...rest
|
|
380
|
+
}: RichTextEditorProps,
|
|
381
|
+
ref: Ref<RichTextEditorRef>,
|
|
382
|
+
) {
|
|
306
383
|
const size = useSize(sizeProp, 'md');
|
|
307
384
|
const id = useId();
|
|
308
385
|
const descriptionID = useId();
|
|
@@ -417,12 +494,13 @@ export function RichTextEditor({
|
|
|
417
494
|
/>
|
|
418
495
|
)}
|
|
419
496
|
{plugins}
|
|
497
|
+
<EditorRefBridge editorRef={ref} editable={editable} />
|
|
420
498
|
</div>
|
|
421
499
|
</LexicalComposer>
|
|
422
500
|
</div>
|
|
423
501
|
</Field>
|
|
424
502
|
);
|
|
425
|
-
}
|
|
503
|
+
});
|
|
426
504
|
|
|
427
505
|
RichTextEditor.displayName = 'RichTextEditor';
|
|
428
506
|
|
|
@@ -438,6 +516,50 @@ function AutoFocusOnMount(): null {
|
|
|
438
516
|
return null;
|
|
439
517
|
}
|
|
440
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Wires the imperative `RichTextEditorRef` handle. Split into its own plugin so
|
|
521
|
+
* it runs inside the composer context and can reach the `LexicalEditor` via
|
|
522
|
+
* `useLexicalComposerContext()`. Renders nothing.
|
|
523
|
+
*
|
|
524
|
+
* `focus()` and `clear()` are gated on `editable` so a read-only or disabled
|
|
525
|
+
* editor cannot be mutated or focused through the imperative handle — matching
|
|
526
|
+
* the behaviour of the editable surface itself.
|
|
527
|
+
*/
|
|
528
|
+
function EditorRefBridge({
|
|
529
|
+
editorRef,
|
|
530
|
+
editable,
|
|
531
|
+
}: {
|
|
532
|
+
editorRef: Ref<RichTextEditorRef>;
|
|
533
|
+
editable: boolean;
|
|
534
|
+
}): null {
|
|
535
|
+
const [editor] = useLexicalComposerContext();
|
|
536
|
+
useImperativeHandle(
|
|
537
|
+
editorRef,
|
|
538
|
+
() => ({
|
|
539
|
+
focus: () => {
|
|
540
|
+
if (!editable) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
editor.focus();
|
|
544
|
+
},
|
|
545
|
+
clear: () => {
|
|
546
|
+
if (!editable) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Reset to a single empty paragraph via a fresh EditorState. Uses the
|
|
550
|
+
// editor instance's own parse/set methods so we avoid a top-level
|
|
551
|
+
// `lexical` value import (see EMPTY_EDITOR_STATE_JSON). This still
|
|
552
|
+
// notifies update listeners, so `onChange` fires.
|
|
553
|
+
editor.setEditorState(editor.parseEditorState(EMPTY_EDITOR_STATE_JSON));
|
|
554
|
+
},
|
|
555
|
+
getEditorState: () => editor.getEditorState(),
|
|
556
|
+
getEditor: () => editor,
|
|
557
|
+
}),
|
|
558
|
+
[editor, editable],
|
|
559
|
+
);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
441
563
|
/**
|
|
442
564
|
* Renders the Lexical ContentEditable. Split out so the placeholder
|
|
443
565
|
* discriminated union (`placeholder` + `aria-placeholder` present together, or
|