testomatio-editor-blocks 0.4.74 → 0.4.76

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.
@@ -1,5 +1,5 @@
1
1
  import OverType, { type OverType as OverTypeInstance } from "overtype";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import type { ReactNode, ChangeEvent } from "react";
4
4
  import { useComponentsContext } from "@blocknote/react";
5
5
  import { EditLinkMenuItems } from "@blocknote/react";
@@ -63,7 +63,26 @@ const READ_ONLY_ALLOWED_KEYS = new Set([
63
63
 
64
64
  const AUTOCOMPLETE_TRIGGER_KEYS = new Set([" ", "Space"]);
65
65
 
66
- const markdownParser = (OverType as { MarkdownParser?: { parse: (markdown: string) => string } }).MarkdownParser;
66
+ const markdownParser = (
67
+ OverType as {
68
+ MarkdownParser?: {
69
+ parse: (
70
+ markdown: string,
71
+ activeLine?: number,
72
+ showActiveLineRaw?: boolean,
73
+ instanceHighlighter?: unknown,
74
+ isPreviewMode?: boolean,
75
+ ) => string;
76
+ };
77
+ }
78
+ ).MarkdownParser;
79
+
80
+ /**
81
+ * `useLayoutEffect` that degrades to `useEffect` outside the browser so SSR
82
+ * doesn't warn. Static previews and the OverType mount run pre-paint to avoid a
83
+ * blank/jumping frame on the click→edit swap.
84
+ */
85
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
67
86
 
68
87
  function ImageUploadIcon() {
69
88
  return (
@@ -674,6 +693,136 @@ function markdownToPlainText(markdown: string): string {
674
693
  }
675
694
  }
676
695
 
696
+ const IMAGE_SYNTAX = /!\[([^\]]*)\]\(([^)]+)\)/g;
697
+
698
+ /**
699
+ * Render a run of plain text, turning any `![alt](url)` markdown into real
700
+ * `<img>` elements. Returns a string when there are no images (so simple text
701
+ * stays a plain text node), otherwise a keyed array of strings and images.
702
+ */
703
+ function renderTextWithImages(text: string, key: string): ReactNode {
704
+ if (!text.includes("![")) {
705
+ return text;
706
+ }
707
+ const nodes: ReactNode[] = [];
708
+ IMAGE_SYNTAX.lastIndex = 0;
709
+ let last = 0;
710
+ let part = 0;
711
+ let match: RegExpExecArray | null;
712
+ while ((match = IMAGE_SYNTAX.exec(text)) !== null) {
713
+ if (match.index > last) {
714
+ nodes.push(<Fragment key={`${key}-t${part++}`}>{text.slice(last, match.index)}</Fragment>);
715
+ }
716
+ nodes.push(<img key={`${key}-i${part++}`} src={match[2]} alt={match[1] || "Step image"} />);
717
+ last = match.index + match[0].length;
718
+ }
719
+ if (last < text.length) {
720
+ nodes.push(<Fragment key={`${key}-t${part++}`}>{text.slice(last)}</Fragment>);
721
+ }
722
+ return nodes;
723
+ }
724
+
725
+ /**
726
+ * Render a step field's markdown as a faithful, read-only reading view: the same
727
+ * clean text + bold/italic/code/link decorations + inline images the live
728
+ * OverType preview shows, but as plain React children (no editor, no refs, no
729
+ * imperative DOM — BlockNote's node-view renderer doesn't attach refs the way a
730
+ * normal React commit does, so the content must be declarative).
731
+ *
732
+ * Decorations are applied by slicing the plain text at every formatting/link
733
+ * boundary and wrapping each segment in the same `step-preview-*` elements the
734
+ * live editor uses, so all existing CSS applies unchanged.
735
+ */
736
+ function renderStepFieldContent(value: string): ReactNode {
737
+ const { plainText, links, formatting } = stripInlineMarkdown(value);
738
+ if (!plainText) {
739
+ return null;
740
+ }
741
+ if (formatting.length === 0 && links.length === 0) {
742
+ return renderTextWithImages(plainText, "p");
743
+ }
744
+
745
+ const len = plainText.length;
746
+ const points = new Set<number>([0, len]);
747
+ for (const f of formatting) {
748
+ points.add(Math.max(0, f.start));
749
+ points.add(Math.min(len, f.end));
750
+ }
751
+ for (const l of links) {
752
+ points.add(Math.max(0, l.start));
753
+ points.add(Math.min(len, l.end));
754
+ }
755
+ const sorted = [...points].sort((a, b) => a - b);
756
+
757
+ const out: ReactNode[] = [];
758
+ for (let i = 0; i < sorted.length - 1; i++) {
759
+ const a = sorted[i];
760
+ const b = sorted[i + 1];
761
+ if (a >= b) {
762
+ continue;
763
+ }
764
+ const text = plainText.slice(a, b);
765
+ const fmts = new Set(
766
+ formatting.filter((f) => f.start <= a && f.end >= b).map((f) => f.type),
767
+ );
768
+ const link = links.find((l) => l.start <= a && l.end >= b);
769
+
770
+ let node: ReactNode = renderTextWithImages(text, `s${i}`);
771
+ if (fmts.has("code")) {
772
+ node = <code className="step-preview-code">{node}</code>;
773
+ }
774
+ if (fmts.has("italic")) {
775
+ node = <em className="step-preview-italic">{node}</em>;
776
+ }
777
+ if (fmts.has("bold")) {
778
+ node = <strong className="step-preview-bold">{node}</strong>;
779
+ }
780
+ if (link) {
781
+ node = (
782
+ <a className="step-preview-link" href={link.url}>
783
+ {node}
784
+ </a>
785
+ );
786
+ }
787
+ out.push(<Fragment key={i}>{node}</Fragment>);
788
+ }
789
+ return out;
790
+ }
791
+
792
+ /**
793
+ * Lightweight, non-interactive stand-in for {@link StepField}. Mounts no
794
+ * OverType editor, observers, or event handlers — just a styled
795
+ * `.bn-step-editor--preview` box whose markdown is rendered declaratively. Used
796
+ * for every step that isn't currently being edited.
797
+ */
798
+ export function StepFieldPreview({
799
+ value,
800
+ fieldName,
801
+ multiline = true,
802
+ }: {
803
+ value: string;
804
+ fieldName?: string;
805
+ multiline?: boolean;
806
+ }) {
807
+ const content = useMemo(() => renderStepFieldContent(value), [value]);
808
+
809
+ const editorClassName = [
810
+ "bn-step-editor",
811
+ multiline ? "bn-step-editor--multiline" : "",
812
+ "bn-step-editor--preview",
813
+ ]
814
+ .filter(Boolean)
815
+ .join(" ");
816
+
817
+ return (
818
+ <div className="bn-step-field">
819
+ <div className={editorClassName} data-step-field={fieldName}>
820
+ {content}
821
+ </div>
822
+ </div>
823
+ );
824
+ }
825
+
677
826
  export function StepField({
678
827
  label,
679
828
  showLabel = true,
@@ -712,6 +861,10 @@ export function StepField({
712
861
  const autoFocusRef = useRef(false);
713
862
  const pendingFocusRef = useRef(false);
714
863
  const initialValueRef = useRef(value);
864
+ // Read at OverType init so the editor mounts already-collapsed in compact
865
+ // mode (no tall first frame before the compact layout effect runs).
866
+ const compactModeRef = useRef(compactMode);
867
+ compactModeRef.current = compactMode;
715
868
  const onChangeRef = useRef(onChange);
716
869
  const [plainTextValue, setPlainTextValue] = useState(() => markdownToPlainText(value));
717
870
  const [isFocused, setIsFocused] = useState(false);
@@ -799,7 +952,7 @@ export function StepField({
799
952
  onChangeRef.current?.(markdown);
800
953
  }, [pushUndoSnapshot]);
801
954
 
802
- useEffect(() => {
955
+ useIsomorphicLayoutEffect(() => {
803
956
  const container = editorContainerRef.current;
804
957
  if (!container) {
805
958
  return;
@@ -820,11 +973,16 @@ export function StepField({
820
973
  value: plainText,
821
974
  placeholder: resolvedPlaceholder,
822
975
  autoResize: multiline,
823
- minHeight: multiline ? "4rem" : "2.5rem",
976
+ // Seed the compact floor at init so a clicked step paints already
977
+ // collapsed — the compact layout effect below keeps it in sync after.
978
+ minHeight: compactModeRef.current ? "0px" : multiline ? "4rem" : "2.5rem",
824
979
  padding: "0.5rem 0.75rem",
825
980
  fontSize: "0.95rem",
826
981
  onChange: handleEditorChange,
827
982
  });
983
+ if (compactModeRef.current && instance.textarea) {
984
+ instance.textarea.rows = 1;
985
+ }
828
986
 
829
987
  // Monkey-patch updatePreview to add link highlights
830
988
  const originalUpdatePreview = instance.updatePreview.bind(instance);
@@ -976,7 +1134,7 @@ export function StepField({
976
1134
  // so caret and value survive. Driven by the stable compactMode flag (not
977
1135
  // `compact`) so collapsed and expanded share one height — focusing never
978
1136
  // shifts the layout.
979
- useEffect(() => {
1137
+ useIsomorphicLayoutEffect(() => {
980
1138
  const instance = editorInstanceRef.current as
981
1139
  | (OverTypeInstance & {
982
1140
  options?: { minHeight?: string };
@@ -31,6 +31,77 @@ export function serializeMetaFields(fields: MetaField[]): string {
31
31
  return JSON.stringify(fields);
32
32
  }
33
33
 
34
+ type TestEditorLike = {
35
+ document: any[];
36
+ getTextCursorPosition?: () => { block?: { id?: string } };
37
+ insertBlocks: (
38
+ blocks: any[],
39
+ referenceId: string,
40
+ placement: "before" | "after",
41
+ ) => any[];
42
+ };
43
+
44
+ /** Reads the `labels` value from the first suite `testMeta` block, if any. */
45
+ function suiteLabelsFromDocument(document: any[]): string {
46
+ for (const block of document) {
47
+ if (block?.type !== "testMeta") continue;
48
+ if ((block.props as any)?.metaKind !== "suite") continue;
49
+ const fields = parseMetaFields((block.props as any)?.metaFields);
50
+ const labels = fields.find((f) => f.key.trim().toLowerCase() === "labels");
51
+ if (labels) return labels.value;
52
+ }
53
+ return "";
54
+ }
55
+
56
+ /**
57
+ * Insert a new test: a `testMeta` panel (labelled "NEW TEST" until it gets an id)
58
+ * seeded with default metadata — `type: manual`, `priority: normal`, and `labels`
59
+ * copied from the suite block — followed by an empty H2 heading for the test title.
60
+ *
61
+ * Inserts at the text cursor when the editor is focused, otherwise at the end of
62
+ * the document. Returns the title heading's block id (for focusing), or null.
63
+ */
64
+ export function addTestBlock(editor: TestEditorLike): string | null {
65
+ const fields: MetaField[] = [
66
+ { key: "type", value: "manual" },
67
+ { key: "priority", value: "normal" },
68
+ { key: "labels", value: suiteLabelsFromDocument(editor.document) },
69
+ ];
70
+
71
+ const metaBlock = {
72
+ type: "testMeta" as const,
73
+ props: {
74
+ metaKind: "test",
75
+ metaFields: serializeMetaFields(fields),
76
+ metaInline: false,
77
+ },
78
+ children: [],
79
+ };
80
+ const titleHeading = {
81
+ type: "heading" as const,
82
+ props: { level: 2 },
83
+ content: [],
84
+ children: [],
85
+ };
86
+
87
+ // Prefer the text cursor position — BlockNote keeps it in the editor state even
88
+ // after the editor blurs (e.g. when a toolbar button is clicked), so the test
89
+ // lands where the user left the caret. Fall back to the document end only when
90
+ // there is no cursor at all.
91
+ const docs = editor.document;
92
+ let referenceId: string | undefined;
93
+ try {
94
+ referenceId = editor.getTextCursorPosition?.().block?.id;
95
+ } catch {
96
+ referenceId = undefined;
97
+ }
98
+ if (!referenceId) referenceId = docs[docs.length - 1]?.id;
99
+ if (!referenceId) return null;
100
+
101
+ const inserted = editor.insertBlocks([metaBlock, titleHeading], referenceId, "after");
102
+ return inserted?.[1]?.id ?? null;
103
+ }
104
+
34
105
  type AddFieldMenuProps = {
35
106
  kind: "test" | "suite";
36
107
  usedKeys: string[];
@@ -214,7 +285,9 @@ export const testMetaBlock = createReactBlockSpec(
214
285
  draggable={false}
215
286
  >
216
287
  <div className="bn-testmeta__header">
217
- <span className="bn-testmeta__label">{kind.toUpperCase()}</span>
288
+ <span className="bn-testmeta__label">
289
+ {kind === "test" && !idField?.value ? "NEW TEST" : kind.toUpperCase()}
290
+ </span>
218
291
  {idField?.value && <span className="bn-testmeta__id">{idField.value}</span>}
219
292
  {!expanded && (
220
293
  <button
@@ -631,6 +631,30 @@ html.dark .bn-step-editor .overtype-wrapper .overtype-preview a.step-preview-lin
631
631
  vertical-align: 1px;
632
632
  }
633
633
 
634
+ /* Read-only preview parity for compact reading rows: the live compact field
635
+ tightens padding on the OverType layers (above), which the static preview
636
+ doesn't have, so apply the same padding to the flow `--preview` box. */
637
+ .bn-teststep--compact .bn-step-editor--preview {
638
+ padding: 4px 12px;
639
+ }
640
+
641
+ /* Same "Expected" reading badge for the static preview. The static preview is a
642
+ single flow box (not OverType's per-line divs), so the badge goes on its own
643
+ ::before. */
644
+ .bn-teststep--collapsed .bn-step-editor--preview[data-step-field="expected"]::before {
645
+ content: "Expected";
646
+ display: inline-block;
647
+ margin-right: 8px;
648
+ padding: 0 6px;
649
+ border-radius: 4px;
650
+ background: var(--step-bg-light);
651
+ color: var(--step-muted);
652
+ font-size: 11px;
653
+ font-weight: 600;
654
+ line-height: 18px;
655
+ vertical-align: 1px;
656
+ }
657
+
634
658
  .bn-teststep__view-toggle--compact svg {
635
659
  color: var(--step-muted);
636
660
  }
@@ -974,6 +998,19 @@ html.dark .testomatio-editor [data-content-type="heading"] .bn-tag-badge {
974
998
  color: var(--text-muted);
975
999
  }
976
1000
 
1001
+ /*
1002
+ * The "new test" title heading is inserted directly after a testMeta panel, so it
1003
+ * is the block-outer immediately following the one containing a testMeta block.
1004
+ * Override BlockNote's injected "Heading" placeholder just for that heading.
1005
+ * !important beats the dynamically injected placeholder rule.
1006
+ */
1007
+ .bn-block-outer:has(.bn-block-content[data-content-type="testMeta"])
1008
+ + .bn-block-outer
1009
+ .bn-block-content[data-content-type="heading"]
1010
+ .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
1011
+ content: "Enter test title" !important;
1012
+ }
1013
+
977
1014
  .bn-snippet-dropdown {
978
1015
  position: relative;
979
1016
  }
@@ -1432,6 +1469,58 @@ html.dark .bn-step-editor--preview {
1432
1469
  color: #e5e5e5;
1433
1470
  }
1434
1471
 
1472
+ /* Wrapper around a non-edited step's read-only preview. Focusable so Tab enters
1473
+ a step (which mounts its editor); no lingering outline since focus moves to
1474
+ the freshly-mounted field immediately. */
1475
+ .bn-teststep-preview-wrapper {
1476
+ outline: none;
1477
+ }
1478
+
1479
+ /* Inline decorations inside the read-only preview mirror the live OverType
1480
+ preview, which uses the same step-preview-* classes. The live rules are
1481
+ scoped to `.overtype-wrapper .overtype-preview` (which the static preview
1482
+ doesn't render), so the same declarations are repeated here for the flow
1483
+ `--preview` container. */
1484
+ .bn-step-editor--preview a.step-preview-link {
1485
+ color: #4f46e5;
1486
+ text-decoration: underline;
1487
+ pointer-events: none;
1488
+ }
1489
+
1490
+ .bn-step-editor--preview strong.step-preview-bold {
1491
+ -webkit-text-stroke: 0.5px currentColor;
1492
+ font-weight: inherit;
1493
+ color: inherit;
1494
+ }
1495
+
1496
+ .bn-step-editor--preview em.step-preview-italic {
1497
+ font-style: italic;
1498
+ color: inherit;
1499
+ }
1500
+
1501
+ .bn-step-editor--preview code.step-preview-code {
1502
+ background-color: transparent;
1503
+ font-family: inherit;
1504
+ font-size: inherit;
1505
+ color: rgb(146, 64, 14);
1506
+ }
1507
+
1508
+ .bn-step-editor--preview img {
1509
+ display: block;
1510
+ max-width: 100%;
1511
+ border-radius: 0.65rem;
1512
+ margin: 0.5rem 0;
1513
+ pointer-events: none;
1514
+ }
1515
+
1516
+ html.dark .bn-step-editor--preview code.step-preview-code {
1517
+ color: rgba(251, 191, 36, 1);
1518
+ }
1519
+
1520
+ html.dark .bn-step-editor--preview a.step-preview-link {
1521
+ color: rgba(129, 140, 248, 1);
1522
+ }
1523
+
1435
1524
  .bn-step-editor.bn-step-editor--focused {
1436
1525
  outline: none;
1437
1526
  box-shadow: none;
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export {
6
6
  } from "./editor/customSchema";
7
7
  export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
8
8
  export { snippetBlock } from "./editor/blocks/snippet";
9
- export { testMetaBlock } from "./editor/blocks/testMeta";
9
+ export { testMetaBlock, addTestBlock } from "./editor/blocks/testMeta";
10
10
  export {
11
11
  setMetaFieldSuggestions,
12
12
  getMetaFieldSuggestions,
@@ -1,26 +0,0 @@
1
- /**
2
- * Defers mounting of expensive block content until the element is at (or near)
3
- * the viewport. Heavy blocks (e.g. test steps that each spin up an OverType
4
- * editor) render a cheap placeholder first; the real interactive content is
5
- * mounted only once the block scrolls into view. This keeps pasting/loading a
6
- * large document fast — only the visible steps pay the editor-init cost up
7
- * front, the rest are upgraded lazily as the user scrolls.
8
- *
9
- * Returns a ref to attach to the wrapper element and a boolean that flips to
10
- * `true` once (and stays true — we never tear an editor back down).
11
- *
12
- * `activate(focus)` lets the caller upgrade eagerly on interaction. Passing
13
- * `focus: true` (a click/focus on the placeholder) records that the freshly
14
- * mounted content should take focus, so a single click on a preview starts
15
- * editing. Passive activation (hover pre-warm, scroll-into-view) leaves focus
16
- * alone via `shouldFocusOnActivate === false`.
17
- */
18
- export declare function useDeferredMount<T extends HTMLElement>(options?: {
19
- rootMargin?: string;
20
- initiallyActive?: boolean;
21
- }): {
22
- ref: React.RefObject<T | null>;
23
- active: boolean;
24
- activate: (focus?: boolean) => void;
25
- shouldFocusOnActivate: boolean;
26
- };
@@ -1,54 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- /**
3
- * Defers mounting of expensive block content until the element is at (or near)
4
- * the viewport. Heavy blocks (e.g. test steps that each spin up an OverType
5
- * editor) render a cheap placeholder first; the real interactive content is
6
- * mounted only once the block scrolls into view. This keeps pasting/loading a
7
- * large document fast — only the visible steps pay the editor-init cost up
8
- * front, the rest are upgraded lazily as the user scrolls.
9
- *
10
- * Returns a ref to attach to the wrapper element and a boolean that flips to
11
- * `true` once (and stays true — we never tear an editor back down).
12
- *
13
- * `activate(focus)` lets the caller upgrade eagerly on interaction. Passing
14
- * `focus: true` (a click/focus on the placeholder) records that the freshly
15
- * mounted content should take focus, so a single click on a preview starts
16
- * editing. Passive activation (hover pre-warm, scroll-into-view) leaves focus
17
- * alone via `shouldFocusOnActivate === false`.
18
- */
19
- export function useDeferredMount(options = {}) {
20
- const { rootMargin = "300px 0px", initiallyActive = false } = options;
21
- const ref = useRef(null);
22
- const [active, setActive] = useState(initiallyActive);
23
- const activeRef = useRef(active);
24
- activeRef.current = active;
25
- const focusOnActivateRef = useRef(false);
26
- const activate = (focus = false) => {
27
- if (activeRef.current)
28
- return;
29
- if (focus)
30
- focusOnActivateRef.current = true;
31
- setActive(true);
32
- };
33
- useEffect(() => {
34
- if (activeRef.current)
35
- return;
36
- const el = ref.current;
37
- if (!el)
38
- return;
39
- // Environments without IntersectionObserver (or SSR) just mount eagerly.
40
- if (typeof IntersectionObserver === "undefined") {
41
- setActive(true);
42
- return;
43
- }
44
- const observer = new IntersectionObserver((entries) => {
45
- if (entries.some((entry) => entry.isIntersecting)) {
46
- setActive(true);
47
- observer.disconnect();
48
- }
49
- }, { rootMargin });
50
- observer.observe(el);
51
- return () => observer.disconnect();
52
- }, [rootMargin]);
53
- return { ref, active, activate, shouldFocusOnActivate: focusOnActivateRef.current };
54
- }
@@ -1,66 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
-
3
- /**
4
- * Defers mounting of expensive block content until the element is at (or near)
5
- * the viewport. Heavy blocks (e.g. test steps that each spin up an OverType
6
- * editor) render a cheap placeholder first; the real interactive content is
7
- * mounted only once the block scrolls into view. This keeps pasting/loading a
8
- * large document fast — only the visible steps pay the editor-init cost up
9
- * front, the rest are upgraded lazily as the user scrolls.
10
- *
11
- * Returns a ref to attach to the wrapper element and a boolean that flips to
12
- * `true` once (and stays true — we never tear an editor back down).
13
- *
14
- * `activate(focus)` lets the caller upgrade eagerly on interaction. Passing
15
- * `focus: true` (a click/focus on the placeholder) records that the freshly
16
- * mounted content should take focus, so a single click on a preview starts
17
- * editing. Passive activation (hover pre-warm, scroll-into-view) leaves focus
18
- * alone via `shouldFocusOnActivate === false`.
19
- */
20
- export function useDeferredMount<T extends HTMLElement>(
21
- options: { rootMargin?: string; initiallyActive?: boolean } = {},
22
- ): {
23
- ref: React.RefObject<T | null>;
24
- active: boolean;
25
- activate: (focus?: boolean) => void;
26
- shouldFocusOnActivate: boolean;
27
- } {
28
- const { rootMargin = "300px 0px", initiallyActive = false } = options;
29
- const ref = useRef<T>(null);
30
- const [active, setActive] = useState(initiallyActive);
31
- const activeRef = useRef(active);
32
- activeRef.current = active;
33
- const focusOnActivateRef = useRef(false);
34
-
35
- const activate = (focus = false) => {
36
- if (activeRef.current) return;
37
- if (focus) focusOnActivateRef.current = true;
38
- setActive(true);
39
- };
40
-
41
- useEffect(() => {
42
- if (activeRef.current) return;
43
- const el = ref.current;
44
- if (!el) return;
45
-
46
- // Environments without IntersectionObserver (or SSR) just mount eagerly.
47
- if (typeof IntersectionObserver === "undefined") {
48
- setActive(true);
49
- return;
50
- }
51
-
52
- const observer = new IntersectionObserver(
53
- (entries) => {
54
- if (entries.some((entry) => entry.isIntersecting)) {
55
- setActive(true);
56
- observer.disconnect();
57
- }
58
- },
59
- { rootMargin },
60
- );
61
- observer.observe(el);
62
- return () => observer.disconnect();
63
- }, [rootMargin]);
64
-
65
- return { ref, active, activate, shouldFocusOnActivate: focusOnActivateRef.current };
66
- }