@octanejs/lexical 0.1.9 → 0.1.10

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.
Files changed (34) hide show
  1. package/package.json +5 -5
  2. package/src/LexicalAutoEmbedPlugin.tsrx +31 -6
  3. package/src/LexicalAutoFocusPlugin.tsrx +5 -1
  4. package/src/LexicalAutoLinkPlugin.tsrx +9 -1
  5. package/src/LexicalBlockWithAlignableContents.tsrx +13 -1
  6. package/src/LexicalCharacterLimitPlugin.tsrx +18 -7
  7. package/src/LexicalCheckListPlugin.tsrx +5 -1
  8. package/src/LexicalClearEditorPlugin.tsrx +5 -1
  9. package/src/LexicalClickableLinkPlugin.tsrx +6 -1
  10. package/src/LexicalComposer.tsrx +13 -4
  11. package/src/LexicalContentEditable.tsrx +25 -3
  12. package/src/LexicalDraggableBlockPlugin.tsrx +68 -31
  13. package/src/LexicalEditorRefPlugin.tsrx +7 -1
  14. package/src/LexicalErrorBoundary.tsrx +18 -2
  15. package/src/LexicalHistoryPlugin.tsrx +7 -1
  16. package/src/LexicalHorizontalRuleNode.tsrx +10 -7
  17. package/src/LexicalLinkPlugin.tsrx +7 -1
  18. package/src/LexicalListPlugin.tsrx +17 -1
  19. package/src/LexicalMarkdownShortcutPlugin.tsrx +8 -3
  20. package/src/LexicalNestedComposer.tsrx +21 -3
  21. package/src/LexicalNodeContextMenuPlugin.tsrx +76 -22
  22. package/src/LexicalNodeEventPlugin.tsrx +10 -3
  23. package/src/LexicalNodeMenuPlugin.tsrx +21 -3
  24. package/src/LexicalOnChangePlugin.tsrx +8 -1
  25. package/src/LexicalPlainTextPlugin.tsrx +16 -2
  26. package/src/LexicalRichTextPlugin.tsrx +16 -2
  27. package/src/LexicalSelectionAlwaysOnDisplay.tsrx +5 -1
  28. package/src/LexicalTabIndentationPlugin.tsrx +12 -1
  29. package/src/LexicalTableOfContentsPlugin.tsrx +39 -12
  30. package/src/LexicalTablePlugin.tsrx +34 -1
  31. package/src/LexicalTypeaheadMenuPlugin.tsrx +24 -3
  32. package/src/shared/LexicalContentEditableElement.tsrx +58 -10
  33. package/src/shared/LexicalDecorators.tsrx +28 -5
  34. package/src/shared/LexicalMenu.tsrx +38 -7
@@ -3,15 +3,32 @@
3
3
  // component: `children(entries, editor)` returns the element to render.
4
4
  import { useLexicalComposerContext } from './LexicalComposerContext';
5
5
  import { $isHeadingNode, HeadingNode } from '@lexical/rich-text';
6
+ import type { HeadingTagType } from '@lexical/rich-text';
6
7
  import { $getNextRightPreorderNode } from '@lexical/utils';
7
8
  import { $getNodeByKey, $getRoot, $isElementNode, TextNode } from 'lexical';
9
+ import type { ElementNode, LexicalEditor, LexicalNode, NodeKey } from 'lexical';
8
10
  import { useEffect, useState } from 'octane';
11
+ import type { OctaneNode } from 'octane';
9
12
 
10
- function toEntry(heading) {
13
+ /**
14
+ * A single entry in the table of contents, as a tuple of the heading node's
15
+ * {@link NodeKey}, its text content, and its heading tag (for example `'h1'`).
16
+ */
17
+ export type TableOfContentsEntry = [key: NodeKey, text: string, tag: HeadingTagType];
18
+
19
+ type Props = {
20
+ children: (values: Array<TableOfContentsEntry>, editor: LexicalEditor) => OctaneNode;
21
+ };
22
+
23
+ function toEntry(heading: HeadingNode): TableOfContentsEntry {
11
24
  return [heading.getKey(), heading.getTextContent(), heading.getTag()];
12
25
  }
13
26
 
14
- function $insertHeadingIntoTableOfContents(prevHeading, newHeading, currentTableOfContents) {
27
+ function $insertHeadingIntoTableOfContents(
28
+ prevHeading: HeadingNode | null,
29
+ newHeading: HeadingNode | null,
30
+ currentTableOfContents: Array<TableOfContentsEntry>,
31
+ ): Array<TableOfContentsEntry> {
15
32
  if (newHeading === null) {
16
33
  return currentTableOfContents;
17
34
  }
@@ -40,7 +57,10 @@ function $insertHeadingIntoTableOfContents(prevHeading, newHeading, currentTable
40
57
  return newTableOfContents;
41
58
  }
42
59
 
43
- function $deleteHeadingFromTableOfContents(key, currentTableOfContents) {
60
+ function $deleteHeadingFromTableOfContents(
61
+ key: NodeKey,
62
+ currentTableOfContents: Array<TableOfContentsEntry>,
63
+ ): Array<TableOfContentsEntry> {
44
64
  const newTableOfContents = [];
45
65
  for (const heading of currentTableOfContents) {
46
66
  if (heading[0] !== key) {
@@ -50,7 +70,10 @@ function $deleteHeadingFromTableOfContents(key, currentTableOfContents) {
50
70
  return newTableOfContents;
51
71
  }
52
72
 
53
- function $updateHeadingInTableOfContents(heading, currentTableOfContents) {
73
+ function $updateHeadingInTableOfContents(
74
+ heading: HeadingNode,
75
+ currentTableOfContents: Array<TableOfContentsEntry>,
76
+ ): Array<TableOfContentsEntry> {
54
77
  const newTableOfContents = [];
55
78
  for (const oldHeading of currentTableOfContents) {
56
79
  if (oldHeading[0] === heading.getKey()) {
@@ -62,8 +85,12 @@ function $updateHeadingInTableOfContents(heading, currentTableOfContents) {
62
85
  return newTableOfContents;
63
86
  }
64
87
 
65
- function $updateHeadingPosition(prevHeading, heading, currentTableOfContents) {
66
- const newTableOfContents = [];
88
+ function $updateHeadingPosition(
89
+ prevHeading: HeadingNode | null,
90
+ heading: HeadingNode,
91
+ currentTableOfContents: Array<TableOfContentsEntry>,
92
+ ): Array<TableOfContentsEntry> {
93
+ const newTableOfContents: Array<TableOfContentsEntry> = [];
67
94
  const newEntry = toEntry(heading);
68
95
 
69
96
  if (!prevHeading) {
@@ -82,7 +109,7 @@ function $updateHeadingPosition(prevHeading, heading, currentTableOfContents) {
82
109
  return newTableOfContents;
83
110
  }
84
111
 
85
- function $getPreviousHeading(node) {
112
+ function $getPreviousHeading(node: LexicalNode): HeadingNode | null {
86
113
  let prevHeading = $getNextRightPreorderNode(node);
87
114
  while (prevHeading !== null && !$isHeadingNode(prevHeading)) {
88
115
  prevHeading = $getNextRightPreorderNode(prevHeading);
@@ -90,14 +117,14 @@ function $getPreviousHeading(node) {
90
117
  return prevHeading;
91
118
  }
92
119
 
93
- export function TableOfContentsPlugin(props) {
94
- const [tableOfContents, setTableOfContents] = useState([]);
120
+ export function TableOfContentsPlugin(props: Props) {
121
+ const [tableOfContents, setTableOfContents] = useState<Array<TableOfContentsEntry>>([]);
95
122
  const [editor] = useLexicalComposerContext();
96
123
 
97
124
  useEffect(() => {
98
- let currentTableOfContents = [];
125
+ let currentTableOfContents: Array<TableOfContentsEntry> = [];
99
126
  editor.read('latest', () => {
100
- const updateCurrentTableOfContents = (node) => {
127
+ const updateCurrentTableOfContents = (node: ElementNode) => {
101
128
  for (const child of node.getChildren()) {
102
129
  if ($isHeadingNode(child)) {
103
130
  currentTableOfContents.push([
@@ -120,7 +147,7 @@ export function TableOfContentsPlugin(props) {
120
147
  dirtyElements,
121
148
  }) => {
122
149
  editorState.read(() => {
123
- const updateChildHeadings = (node) => {
150
+ const updateChildHeadings = (node: ElementNode) => {
124
151
  for (const child of node.getChildren()) {
125
152
  if ($isHeadingNode(child)) {
126
153
  const prevHeading = $getPreviousHeading(child);
@@ -12,7 +12,40 @@ import { $fullReconcile } from 'lexical';
12
12
  import { useLexicalComposerContext } from './LexicalComposerContext';
13
13
  import { useEffect, useState } from 'octane';
14
14
 
15
- export function TablePlugin(props) {
15
+ /**
16
+ * Props for the {@link TablePlugin} component.
17
+ */
18
+ export interface TablePluginProps {
19
+ /**
20
+ * When `false` (default `true`), merged cell support (colspan and rowspan)
21
+ * will be disabled and all tables will be forced into a regular grid with
22
+ * 1x1 table cells.
23
+ */
24
+ hasCellMerge?: boolean;
25
+ /**
26
+ * When `false` (default `true`), the background color of TableCellNode will
27
+ * always be removed.
28
+ */
29
+ hasCellBackgroundColor?: boolean;
30
+ /**
31
+ * When `true` (default `true`), the tab key can be used to navigate table
32
+ * cells.
33
+ */
34
+ hasTabHandler?: boolean;
35
+ /**
36
+ * When `true` (default `false`), tables will be wrapped in a `<div>` to
37
+ * enable horizontal scrolling.
38
+ */
39
+ hasHorizontalScroll?: boolean;
40
+ /**
41
+ * When `true` (default `false`), nested tables will be allowed.
42
+ *
43
+ * @experimental Nested tables are not officially supported.
44
+ */
45
+ hasNestedTables?: boolean;
46
+ }
47
+
48
+ export function TablePlugin(props: TablePluginProps) {
16
49
  const [editor] = useLexicalComposerContext();
17
50
  const hasCellMerge =
18
51
  props.hasCellMerge === undefined ? true : props.hasCellMerge;
@@ -11,7 +11,9 @@ import {
11
11
  tryToPositionRange,
12
12
  } from './typeaheadShared';
13
13
  import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW } from 'lexical';
14
+ import type { CommandListenerPriority, TextNode } from 'lexical';
14
15
  import { startTransition, useCallback, useEffect, useState } from 'octane';
16
+ import type { MenuOption, MenuRenderFn, MenuResolution, TriggerFn } from './shared/menuShared';
15
17
 
16
18
  export { useBasicTypeaheadTriggerMatch, PUNCTUATION } from './typeaheadShared';
17
19
  export { MenuOption, SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND } from './shared/menuShared';
@@ -19,7 +21,26 @@ export { useDynamicPositioning } from './shared/useDynamicPositioning';
19
21
  // Upstream re-exports @lexical/utils' getScrollParent from this module.
20
22
  export { getScrollParent } from '@lexical/utils';
21
23
 
22
- export function LexicalTypeaheadMenuPlugin(props) {
24
+ /**
25
+ * Props for the {@link LexicalTypeaheadMenuPlugin} component.
26
+ */
27
+ export type TypeaheadMenuPluginProps<TOption extends MenuOption> = {
28
+ onQueryChange: (matchingString: string | null) => void;
29
+ onSelectOption: (option: TOption, textNodeContainingQuery: | TextNode
30
+ | null, closeMenu: () => void, matchingString: string) => void;
31
+ options: TOption[];
32
+ triggerFn: TriggerFn;
33
+ menuRenderFn?: MenuRenderFn<TOption>;
34
+ onOpen?: (resolution: MenuResolution) => void;
35
+ onClose?: () => void | PromiseLike<void>;
36
+ anchorClassName?: string;
37
+ commandPriority?: CommandListenerPriority;
38
+ parent?: HTMLElement;
39
+ preselectFirstItem?: boolean;
40
+ ignoreEntityBoundary?: boolean;
41
+ };
42
+
43
+ export function LexicalTypeaheadMenuPlugin<TOption extends MenuOption>(props: TypeaheadMenuPluginProps<TOption>) {
23
44
  const options = props.options;
24
45
  const onQueryChange = props.onQueryChange;
25
46
  const onSelectOption = props.onSelectOption;
@@ -35,7 +56,7 @@ export function LexicalTypeaheadMenuPlugin(props) {
35
56
  const ignoreEntityBoundary = props.ignoreEntityBoundary === true;
36
57
 
37
58
  const [editor] = useLexicalComposerContext();
38
- const [resolution, setResolution] = useState(null);
59
+ const [resolution, setResolution] = useState<MenuResolution | null>(null);
39
60
  const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName, parent);
40
61
 
41
62
  const closeTypeahead = useCallback(() => {
@@ -55,7 +76,7 @@ export function LexicalTypeaheadMenuPlugin(props) {
55
76
  }
56
77
  }, [onClose, resolution]);
57
78
 
58
- const openTypeahead = useCallback((res) => {
79
+ const openTypeahead = useCallback((res: MenuResolution) => {
59
80
  setResolution(res);
60
81
  if (onOpen != null && resolution === null) {
61
82
  onOpen(res);
@@ -7,7 +7,42 @@
7
7
  // The full ARIA set + `{...rest}` passthrough mirror @lexical/react exactly:
8
8
  // boolean ARIA values are stringified (octane drops `false` as an attribute), and
9
9
  // unknown props flow through via `rest` (everything not consumed by name).
10
+ import type { LexicalEditor } from 'lexical';
10
11
  import { useCallback, useLayoutEffect, useState } from 'octane';
12
+ import type { Octane } from 'octane/jsx-runtime';
13
+
14
+ type DivAttributes = Octane.HTMLAttributes<HTMLDivElement>;
15
+
16
+ /**
17
+ * Props for the {@link ContentEditableElement} component. In addition to an
18
+ * `editor`, it accepts the standard `<div>` attributes (except `placeholder`),
19
+ * including the hyphenated `aria-*` attributes, which are the preferred way to
20
+ * set ARIA properties. The camelCase `aria*` props (such as `ariaLabel`) are
21
+ * also accepted but are retained only for backwards compatibility.
22
+ */
23
+ export type ContentEditableElementProps = {
24
+ editor: LexicalEditor;
25
+ ariaActiveDescendant?: DivAttributes['aria-activedescendant'];
26
+ ariaAutoComplete?: DivAttributes['aria-autocomplete'];
27
+ ariaControls?: DivAttributes['aria-controls'];
28
+ ariaDescribedBy?: DivAttributes['aria-describedby'];
29
+ ariaErrorMessage?: DivAttributes['aria-errormessage'];
30
+ ariaExpanded?: DivAttributes['aria-expanded'];
31
+ ariaInvalid?: DivAttributes['aria-invalid'];
32
+ ariaLabel?: DivAttributes['aria-label'];
33
+ ariaLabelledBy?: DivAttributes['aria-labelledby'];
34
+ ariaMultiline?: DivAttributes['aria-multiline'];
35
+ ariaOwns?: DivAttributes['aria-owns'];
36
+ ariaRequired?: DivAttributes['aria-required'];
37
+ autoCapitalize?: HTMLDivElement['autocapitalize'];
38
+ 'data-testid'?: string | null | undefined;
39
+ // Octane ref shapes (no forwardRef): callback or ref object.
40
+ ref?: ((el: HTMLDivElement | null) => void) | { current: HTMLDivElement | null };
41
+ // Consumed (excluded from the `rest` passthrough) but never rendered —
42
+ // upstream omits `placeholder` from the accepted div attributes; the octane
43
+ // port receives it via `<ContentEditable {...props}>`'s spread instead.
44
+ placeholder?: unknown;
45
+ } & Omit<Octane.JSX.IntrinsicElements['div'], 'ref' | 'placeholder'>;
11
46
 
12
47
  // Props consumed by name — everything else flows through to the <div> via `rest`.
13
48
  const NAMED_PROPS = new Set([
@@ -37,11 +72,11 @@ const NAMED_PROPS = new Set([
37
72
  'data-testid',
38
73
  ]);
39
74
 
40
- export function ContentEditableElement(props) @{
75
+ export function ContentEditableElement(props: ContentEditableElementProps) @{
41
76
  const editor = props.editor;
42
77
  const [isEditable, setEditable] = useState(editor.isEditable());
43
78
 
44
- const handleRef = useCallback((rootElement) => {
79
+ const handleRef = useCallback((rootElement: HTMLDivElement | null) => {
45
80
  // defaultView is required for a root element. In multi-window setups it may
46
81
  // not exist at certain points.
47
82
  if (rootElement && rootElement.ownerDocument && rootElement.ownerDocument.defaultView) {
@@ -63,7 +98,7 @@ export function ContentEditableElement(props) @{
63
98
  const rest: Record<string, any> = {};
64
99
  for (const k in props) {
65
100
  if (!NAMED_PROPS.has(k)) {
66
- rest[k] = props[k];
101
+ rest[k] = (props as Record<string, unknown>)[k];
67
102
  }
68
103
  }
69
104
  // Route `class` through the spread (setSpread omits an absent/undefined key) so
@@ -73,29 +108,42 @@ export function ContentEditableElement(props) @{
73
108
  rest['class'] = props.className;
74
109
  }
75
110
 
111
+ // Attribute names are authored camelCase (matching upstream and octane's typed
112
+ // JSX contract); the runtime writes the equivalent lowercase DOM attributes
113
+ // (`tabIndex` via ATTRIBUTE_ALIASES, the rest via HTML's case-insensitive
114
+ // setAttribute).
76
115
  <div
77
- ref={props.ref != null ? [props.ref, handleRef] : handleRef}
116
+ ref={props.ref != null
117
+ ? // Octane's runtime accepts an array of refs natively; the checker's
118
+ // merged-ref signature only models the single-ref forms, so the
119
+ // dynamic array branch needs a type-level cast.
120
+ [props.ref, handleRef] as unknown as typeof handleRef
121
+ : handleRef}
78
122
  aria-activedescendant={isEditable ? props.ariaActiveDescendant : undefined}
79
123
  aria-autocomplete={isEditable ? props.ariaAutoComplete : 'none'}
80
124
  aria-controls={isEditable ? props.ariaControls : undefined}
81
125
  aria-describedby={props.ariaDescribedBy}
82
126
  aria-errormessage={props.ariaErrorMessage != null ? props.ariaErrorMessage : undefined}
83
- aria-expanded={isEditable && role === 'combobox' ? String(!!props.ariaExpanded) : undefined}
127
+ aria-expanded={isEditable && role === 'combobox'
128
+ ? String(!!props.ariaExpanded) as 'true' | 'false'
129
+ : undefined}
84
130
  aria-invalid={props.ariaInvalid != null ? props.ariaInvalid : undefined}
85
131
  aria-label={props.ariaLabel}
86
132
  aria-labelledby={props.ariaLabelledBy}
87
133
  aria-multiline={props.ariaMultiline}
88
134
  aria-owns={isEditable ? props.ariaOwns : undefined}
89
135
  aria-readonly={isEditable ? undefined : 'true'}
90
- aria-required={props.ariaRequired != null ? String(props.ariaRequired) : undefined}
91
- autocapitalize={props.autoCapitalize}
92
- contenteditable={isEditable ? 'true' : 'false'}
136
+ aria-required={props.ariaRequired != null
137
+ ? String(props.ariaRequired) as 'true' | 'false'
138
+ : undefined}
139
+ autoCapitalize={props.autoCapitalize}
140
+ contentEditable={isEditable ? 'true' : 'false'}
93
141
  data-testid={props['data-testid']}
94
142
  id={props.id}
95
143
  role={role}
96
- spellcheck={props.spellCheck === false ? 'false' : 'true'}
144
+ spellCheck={props.spellCheck === false ? 'false' : 'true'}
97
145
  style={props.style}
98
- tabindex={props.tabIndex}
146
+ tabIndex={props.tabIndex}
99
147
  {...rest}
100
148
  />
101
149
  }
@@ -8,6 +8,8 @@
8
8
  // element (a createPortal VALUE is inert), so each portal lives inside its own
9
9
  // `DecoratorPortal` (a display:contents wrapper — the content escapes into the
10
10
  // node's element).
11
+ import type { LexicalEditor } from 'lexical';
12
+ import type { OctaneNode } from 'octane';
11
13
  import {
12
14
  useState,
13
15
  useEffect,
@@ -19,18 +21,39 @@ import {
19
21
  Suspense,
20
22
  } from 'octane';
21
23
 
22
- function DecoratorPortalBody(props) {
24
+ type ErrorBoundaryType = (props: {
25
+ children: OctaneNode;
26
+ onError: (error: Error) => void;
27
+ }) => OctaneNode;
28
+
29
+ type DecoratorPortalBodyProps = {
30
+ ErrorBoundary: ErrorBoundaryType;
31
+ onError: (error: Error) => void;
32
+ decorator: OctaneNode;
33
+ };
34
+
35
+ function DecoratorPortalBody(props: DecoratorPortalBodyProps) {
23
36
  return createElement(props.ErrorBoundary, {
24
37
  onError: props.onError,
25
38
  children: createElement(Suspense, { fallback: null, children: props.decorator }),
26
39
  });
27
40
  }
28
41
 
29
- function DecoratorPortal(props) {
42
+ type DecoratorPortalProps = {
43
+ element: HTMLElement;
44
+ bodyProps: DecoratorPortalBodyProps;
45
+ };
46
+
47
+ function DecoratorPortal(props: DecoratorPortalProps) {
30
48
  return createPortal(DecoratorPortalBody, props.element, props.bodyProps);
31
49
  }
32
50
 
33
- export function Decorators(props) {
51
+ type DecoratorsProps = {
52
+ editor: LexicalEditor;
53
+ ErrorBoundary: ErrorBoundaryType;
54
+ };
55
+
56
+ export function Decorators(props: DecoratorsProps) {
34
57
  const editor = props.editor;
35
58
  const ErrorBoundary = props.ErrorBoundary;
36
59
 
@@ -38,7 +61,7 @@ export function Decorators(props) {
38
61
 
39
62
  // Subscribe to decorator changes.
40
63
  useLayoutEffect(() => {
41
- return editor.registerDecoratorListener((nextDecorators) => {
64
+ return editor.registerDecoratorListener((nextDecorators: Record<string, OctaneNode>) => {
42
65
  flushSync(() => {
43
66
  setDecorators(nextDecorators);
44
67
  });
@@ -79,7 +102,7 @@ export function Decorators(props) {
79
102
  element={entry.element}
80
103
  bodyProps={{
81
104
  ErrorBoundary,
82
- onError: (e) => editor._onError(e),
105
+ onError: (e: Error) => editor._onError(e),
83
106
  decorator: entry.decorator,
84
107
  }}
85
108
  />,
@@ -2,11 +2,13 @@
2
2
  // @lexical/react/src/shared/LexicalMenu.tsx. A `.tsrx` component (auto-slotted
3
3
  // hooks). The hooks (useMenuAnchorRef / useDynamicPositioning) + non-component
4
4
  // pieces live in the sibling `.ts` files.
5
+ import type { MenuOption, MenuRef, MenuRenderFn, MenuResolution } from './menuShared';
5
6
  import {
6
7
  $splitNodeContainingQuery,
7
8
  scrollIntoViewIfNeeded,
8
9
  SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND,
9
10
  } from './menuShared';
11
+ import type { CommandListenerPriority, LexicalEditor, TextNode } from 'lexical';
10
12
  import {
11
13
  COMMAND_PRIORITY_LOW,
12
14
  KEY_ARROW_DOWN_COMMAND,
@@ -18,9 +20,17 @@ import {
18
20
  } from 'lexical';
19
21
  import { createPortal, useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'octane';
20
22
 
21
- function MenuItem(props) @{
23
+ type MenuItemProps<TOption extends MenuOption> = {
24
+ index: number;
25
+ isSelected: boolean;
26
+ onClick: () => void;
27
+ onMouseEnter: () => void;
28
+ option: TOption;
29
+ };
30
+
31
+ function MenuItem<TOption extends MenuOption>(props: MenuItemProps<TOption>) @{
22
32
  <li
23
- tabindex={-1}
33
+ tabIndex={-1}
24
34
  class={props.isSelected ? 'item selected' : 'item'}
25
35
  ref={props.option.setRefElement}
26
36
  role="option"
@@ -34,7 +44,14 @@ function MenuItem(props) @{
34
44
  </li>
35
45
  }
36
46
 
37
- function DefaultMenu(props) @{
47
+ type DefaultMenuProps<TOption extends MenuOption> = {
48
+ options: TOption[];
49
+ selectedIndex: number | null;
50
+ selectOptionAndCleanUp: (option: TOption) => void;
51
+ setHighlightedIndex: (index: number) => void;
52
+ };
53
+
54
+ function DefaultMenu<TOption extends MenuOption>(props: DefaultMenuProps<TOption>) @{
38
55
  <div class="typeahead-popover mentions-menu">
39
56
  <ul>{props.options.map(
40
57
  (option, i) => <MenuItem
@@ -52,7 +69,21 @@ function DefaultMenu(props) @{
52
69
  </div>
53
70
  }
54
71
 
55
- export function LexicalMenu(props) {
72
+ type LexicalMenuProps<TOption extends MenuOption> = {
73
+ close: () => void;
74
+ editor: LexicalEditor;
75
+ anchorElementRef: MenuRef;
76
+ resolution: MenuResolution;
77
+ options: TOption[];
78
+ shouldSplitNodeWithQuery?: boolean;
79
+ menuRenderFn?: MenuRenderFn<TOption>;
80
+ onSelectOption: (option: TOption, textNodeContainingQuery: | TextNode
81
+ | null, closeMenu: () => void, matchingString: string) => void;
82
+ commandPriority?: CommandListenerPriority;
83
+ preselectFirstItem?: boolean;
84
+ };
85
+
86
+ export function LexicalMenu<TOption extends MenuOption>(props: LexicalMenuProps<TOption>) {
56
87
  const close = props.close;
57
88
  const editor = props.editor;
58
89
  const anchorElementRef = props.anchorElementRef;
@@ -65,7 +96,7 @@ export function LexicalMenu(props) {
65
96
  props.commandPriority != null ? props.commandPriority : COMMAND_PRIORITY_LOW;
66
97
  const preselectFirstItem = props.preselectFirstItem !== false;
67
98
 
68
- const [rawSelectedIndex, setHighlightedIndex] = useState(null);
99
+ const [rawSelectedIndex, setHighlightedIndex] = useState<number | null>(null);
69
100
  const selectedIndex =
70
101
  rawSelectedIndex !== null ? Math.min(options.length - 1, rawSelectedIndex) : null;
71
102
 
@@ -77,7 +108,7 @@ export function LexicalMenu(props) {
77
108
  }
78
109
  }, [matchingString, preselectFirstItem]);
79
110
 
80
- const selectOptionAndCleanUp = useCallback((selectedEntry) => {
111
+ const selectOptionAndCleanUp = useCallback((selectedEntry: TOption) => {
81
112
  editor.update(() => {
82
113
  const textNodeContainingQuery =
83
114
  resolution.match != null && shouldSplitNodeWithQuery
@@ -92,7 +123,7 @@ export function LexicalMenu(props) {
92
123
  });
93
124
  }, [editor, shouldSplitNodeWithQuery, resolution.match, onSelectOption, close]);
94
125
 
95
- const updateSelectedIndex = useCallback((index) => {
126
+ const updateSelectedIndex = useCallback((index: number) => {
96
127
  const rootElem = editor.getRootElement();
97
128
  if (rootElem !== null) {
98
129
  rootElem.setAttribute('aria-activedescendant', 'typeahead-item-' + index);