@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.
@@ -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 function RichTextEditor({ label, isLabelHidden, description, isOptional, isRequired, defaultValue, onChange, placeholder, isReadOnly, isDisabled, status, width, labelTooltip, size: sizeProp, nodes, plugins, hasMarkdownShortcuts, transformers, hasAutoFocus, namespace, xstyle, className, style, ...rest }: RichTextEditorProps): import("react").JSX.Element;
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,EAAoC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAgBxE,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;AAiEjB,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,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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,MAAM,EACN,KAAK,EACL,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,KAAK,EACL,OAAO,EACP,oBAA2B,EAC3B,YAA2B,EAC3B,YAAoB,EACpB,SAA2B,EAC3B,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,mBAAmB,+BAwHrB;yBAjJe,cAAc"}
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
@@ -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.faa200a",
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.faa200a",
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 — 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.',
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 {useEffect, useId, useMemo, useRef, type ReactNode} from 'react';
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 function RichTextEditor({
281
- label,
282
- isLabelHidden = false,
283
- description,
284
- isOptional = false,
285
- isRequired = false,
286
- defaultValue,
287
- onChange,
288
- placeholder,
289
- isReadOnly = false,
290
- isDisabled = false,
291
- status,
292
- width,
293
- labelTooltip,
294
- size: sizeProp,
295
- nodes,
296
- plugins,
297
- hasMarkdownShortcuts = true,
298
- transformers = TRANSFORMERS,
299
- hasAutoFocus = false,
300
- namespace = 'astryx-editor',
301
- xstyle,
302
- className,
303
- style,
304
- ...rest
305
- }: RichTextEditorProps) {
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
@@ -15,6 +15,7 @@
15
15
  export {RichTextEditor} from './RichTextEditor';
16
16
  export type {
17
17
  RichTextEditorProps,
18
+ RichTextEditorRef,
18
19
  RichTextEditorStatus,
19
20
  RichTextEditorStatusType,
20
21
  RichTextEditorSize,
package/src/index.ts CHANGED
@@ -235,6 +235,7 @@ export {
235
235
  export {
236
236
  RichTextEditor,
237
237
  type RichTextEditorProps,
238
+ type RichTextEditorRef,
238
239
  type RichTextEditorStatus,
239
240
  type RichTextEditorStatusType,
240
241
  type RichTextEditorSize,