testomatio-editor-blocks 0.4.82 → 0.4.83
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/package/editor/blocks/step.js +87 -16
- package/package.json +1 -1
- package/src/editor/blocks/step.tsx +117 -44
|
@@ -313,23 +313,62 @@ function TestStepBlock({ block, editor }) {
|
|
|
313
313
|
focusOnMountRef.current = true;
|
|
314
314
|
setEditing(true);
|
|
315
315
|
}, []);
|
|
316
|
-
// Mousedown rather than click so the editor mounts before focus settles, and
|
|
317
|
-
// preventDefault so the browser doesn't move focus to <body> when the preview
|
|
318
|
-
// (the mousedown target) unmounts mid-click — that stray blur would otherwise
|
|
319
|
-
// immediately tear the new editor back down.
|
|
320
|
-
const beginEditingFromPointer = useCallback((event) => {
|
|
321
|
-
event.preventDefault();
|
|
322
|
-
beginEditing();
|
|
323
|
-
}, [beginEditing]);
|
|
324
316
|
const endEditing = useCallback(() => setEditing(false), []);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
317
|
+
// `editing` read from a ref so the single mousedown guard below (attached once)
|
|
318
|
+
// always sees the current state without re-binding.
|
|
319
|
+
const editingRef = useRef(editing);
|
|
320
|
+
editingRef.current = editing;
|
|
321
|
+
// Zero-box anchor (display: contents) used only to locate this block's node-view
|
|
322
|
+
// element; it adds no layout box, so the step renders exactly as before.
|
|
323
|
+
const anchorRef = useRef(null);
|
|
324
|
+
// The step is a ProseMirror atom node (content: "none") whose node-view
|
|
325
|
+
// (`.bn-block-content`) is wider than the visible step, so the empty area around
|
|
326
|
+
// it — and, in the collapsed preview, the whole strip to the right of the text —
|
|
327
|
+
// is inert atom surface. A plain mousedown there makes ProseMirror node-select the
|
|
328
|
+
// block (the stray blue outline) instead of editing it. One native listener on the
|
|
329
|
+
// node-view guards every mousedown in both the preview and editing states before
|
|
330
|
+
// ProseMirror sees it. Native (not onMouseDown) is required because React 18
|
|
331
|
+
// delegates at the app root, so its handlers run after ProseMirror's own.
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
var _a;
|
|
334
|
+
const blockEl = (_a = anchorRef.current) === null || _a === void 0 ? void 0 : _a.closest(".bn-block-content");
|
|
335
|
+
if (!blockEl) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const handleMouseDown = (event) => {
|
|
339
|
+
const target = event.target;
|
|
340
|
+
// The real editor surface, form controls, and popovers/dialogs need the
|
|
341
|
+
// native mousedown (caret placement, typing, focus) to behave normally.
|
|
342
|
+
if (target === null || target === void 0 ? void 0 : target.closest('.overtype-container, textarea, input, [role="dialog"], .bn-popover-content, .bn-form-popover')) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Buttons and links (toolbar, view toggle, action bar, autocomplete
|
|
346
|
+
// suggestions): preventDefault keeps the focused field from blurring — a blur
|
|
347
|
+
// node-selects the atom and can trip the focusout teardown on re-render — but
|
|
348
|
+
// we don't stopPropagation, so their own click/mousedown handlers still run.
|
|
349
|
+
if (target === null || target === void 0 ? void 0 : target.closest("button, a[href]")) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// Everything else — field labels, the timeline, the header, surrounding
|
|
354
|
+
// padding, and the empty node-view strip — is inert atom chrome. Swallow the
|
|
355
|
+
// mousedown so ProseMirror can't node-select the block; in the collapsed
|
|
356
|
+
// preview a click there means "edit this step", so begin editing.
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
event.stopPropagation();
|
|
359
|
+
if (!editingRef.current) {
|
|
360
|
+
beginEditing();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
blockEl.addEventListener("mousedown", handleMouseDown);
|
|
364
|
+
return () => blockEl.removeEventListener("mousedown", handleMouseDown);
|
|
365
|
+
}, [beginEditing]);
|
|
366
|
+
return (_jsx("div", { ref: anchorRef, style: { display: "contents" }, children: editing ? (
|
|
367
|
+
// Empty steps mounted eagerly (freshly inserted) auto-focus their title. A
|
|
368
|
+
// preview upgraded by a click focuses its field too, so a single click starts
|
|
369
|
+
// editing. The editor tears back down to a preview when focus leaves the step
|
|
370
|
+
// (see TestStepContent's blur handling).
|
|
371
|
+
_jsx(TestStepContent, { block: block, editor: editor, stepNumber: stepNumber, viewMode: viewMode, autoFocusEnabled: isEmptyStep, focusOnMount: focusOnMountRef.current, onEditEnd: endEditing })) : (_jsx("div", { className: "bn-teststep-preview-wrapper", tabIndex: 0, onFocusCapture: beginEditing, children: _jsx(TestStepPreview, { blockId: block.id, stepNumber: stepNumber, viewMode: viewMode, stepTitle: block.props.stepTitle || "", stepData: block.props.stepData || "", expectedResult: block.props.expectedResult || "" }) })) }));
|
|
333
372
|
}
|
|
334
373
|
function TestStepContent({ block, editor, stepNumber, viewMode, autoFocusEnabled = false, focusOnMount = false, onEditEnd, }) {
|
|
335
374
|
// When a preview is upgraded by a click, focus its primary field once on
|
|
@@ -347,6 +386,12 @@ function TestStepContent({ block, editor, stepNumber, viewMode, autoFocusEnabled
|
|
|
347
386
|
const uploadImage = useStepImageUpload();
|
|
348
387
|
const containerRef = useRef(null);
|
|
349
388
|
const [forceVertical, setForceVertical] = useState(false);
|
|
389
|
+
// Set for a moment while a view-mode toggle is in flight. Switching into or
|
|
390
|
+
// out of the horizontal layout swaps the whole subtree, so the OverType editor
|
|
391
|
+
// unmounts and its blur would otherwise trip the focusout teardown below and
|
|
392
|
+
// collapse the step to a preview. The flag tells the teardown to re-focus the
|
|
393
|
+
// freshly mounted editor instead of ending the edit.
|
|
394
|
+
const viewTransitionRef = useRef(false);
|
|
350
395
|
useEffect(() => {
|
|
351
396
|
var _a;
|
|
352
397
|
const el = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
|
|
@@ -382,12 +427,24 @@ function TestStepContent({ block, editor, stepNumber, viewMode, autoFocusEnabled
|
|
|
382
427
|
// popover that portals to <body>, e.g. the link editor) has settled
|
|
383
428
|
// before we decide whether editing has really ended.
|
|
384
429
|
requestAnimationFrame(() => {
|
|
430
|
+
var _a;
|
|
385
431
|
const active = document.activeElement;
|
|
386
432
|
if (active &&
|
|
387
433
|
(root.contains(active) ||
|
|
388
434
|
active.closest(".bn-popover-content, .bn-form-popover, [role='dialog']"))) {
|
|
389
435
|
return;
|
|
390
436
|
}
|
|
437
|
+
// A view-mode toggle remounted the editor; the blur isn't the user
|
|
438
|
+
// leaving the step. Keep editing and move focus into the freshly
|
|
439
|
+
// mounted layout instead of collapsing to a preview.
|
|
440
|
+
if (viewTransitionRef.current) {
|
|
441
|
+
viewTransitionRef.current = false;
|
|
442
|
+
const live = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelector("textarea");
|
|
443
|
+
if (live) {
|
|
444
|
+
live.focus();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
391
448
|
onEditEnd();
|
|
392
449
|
});
|
|
393
450
|
};
|
|
@@ -502,6 +559,20 @@ function TestStepContent({ block, editor, stepNumber, viewMode, autoFocusEnabled
|
|
|
502
559
|
else {
|
|
503
560
|
next = "vertical";
|
|
504
561
|
}
|
|
562
|
+
// Only switching into or out of the horizontal layout swaps the subtree and
|
|
563
|
+
// remounts the editor; vertical↔compact reuse the same editor instance and
|
|
564
|
+
// never blur. Flag just the remounting transitions so the focusout teardown
|
|
565
|
+
// re-focuses the new editor instead of collapsing — and so a genuine
|
|
566
|
+
// click-away after a non-remounting toggle still tears down normally. Cleared
|
|
567
|
+
// on a timer as a safety net in case the expected blur never arrives.
|
|
568
|
+
if (viewMode === "horizontal" || next === "horizontal") {
|
|
569
|
+
viewTransitionRef.current = true;
|
|
570
|
+
if (typeof window !== "undefined") {
|
|
571
|
+
window.setTimeout(() => {
|
|
572
|
+
viewTransitionRef.current = false;
|
|
573
|
+
}, 300);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
505
576
|
writeStepViewMode(next);
|
|
506
577
|
// The shared useStepViewMode hook (in every step, including this one)
|
|
507
578
|
// listens for this event and re-reads the mode, so we don't track it
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createReactBlockSpec, useEditorChange } from "@blocknote/react";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { StepField, StepFieldPreview } from "./stepField";
|
|
4
4
|
import { StepHorizontalView } from "./stepHorizontalView";
|
|
5
5
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
@@ -375,53 +375,95 @@ function TestStepBlock({ block, editor }: { block: any; editor: any }) {
|
|
|
375
375
|
setEditing(true);
|
|
376
376
|
}, []);
|
|
377
377
|
|
|
378
|
-
// Mousedown rather than click so the editor mounts before focus settles, and
|
|
379
|
-
// preventDefault so the browser doesn't move focus to <body> when the preview
|
|
380
|
-
// (the mousedown target) unmounts mid-click — that stray blur would otherwise
|
|
381
|
-
// immediately tear the new editor back down.
|
|
382
|
-
const beginEditingFromPointer = useCallback(
|
|
383
|
-
(event: ReactMouseEvent) => {
|
|
384
|
-
event.preventDefault();
|
|
385
|
-
beginEditing();
|
|
386
|
-
},
|
|
387
|
-
[beginEditing],
|
|
388
|
-
);
|
|
389
|
-
|
|
390
378
|
const endEditing = useCallback(() => setEditing(false), []);
|
|
391
379
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
380
|
+
// `editing` read from a ref so the single mousedown guard below (attached once)
|
|
381
|
+
// always sees the current state without re-binding.
|
|
382
|
+
const editingRef = useRef(editing);
|
|
383
|
+
editingRef.current = editing;
|
|
384
|
+
|
|
385
|
+
// Zero-box anchor (display: contents) used only to locate this block's node-view
|
|
386
|
+
// element; it adds no layout box, so the step renders exactly as before.
|
|
387
|
+
const anchorRef = useRef<HTMLDivElement>(null);
|
|
388
|
+
|
|
389
|
+
// The step is a ProseMirror atom node (content: "none") whose node-view
|
|
390
|
+
// (`.bn-block-content`) is wider than the visible step, so the empty area around
|
|
391
|
+
// it — and, in the collapsed preview, the whole strip to the right of the text —
|
|
392
|
+
// is inert atom surface. A plain mousedown there makes ProseMirror node-select the
|
|
393
|
+
// block (the stray blue outline) instead of editing it. One native listener on the
|
|
394
|
+
// node-view guards every mousedown in both the preview and editing states before
|
|
395
|
+
// ProseMirror sees it. Native (not onMouseDown) is required because React 18
|
|
396
|
+
// delegates at the app root, so its handlers run after ProseMirror's own.
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
const blockEl = anchorRef.current?.closest(".bn-block-content") as HTMLElement | null;
|
|
399
|
+
if (!blockEl) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
403
|
+
const target = event.target as HTMLElement | null;
|
|
404
|
+
// The real editor surface, form controls, and popovers/dialogs need the
|
|
405
|
+
// native mousedown (caret placement, typing, focus) to behave normally.
|
|
406
|
+
if (
|
|
407
|
+
target?.closest(
|
|
408
|
+
'.overtype-container, textarea, input, [role="dialog"], .bn-popover-content, .bn-form-popover',
|
|
409
|
+
)
|
|
410
|
+
) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Buttons and links (toolbar, view toggle, action bar, autocomplete
|
|
414
|
+
// suggestions): preventDefault keeps the focused field from blurring — a blur
|
|
415
|
+
// node-selects the atom and can trip the focusout teardown on re-render — but
|
|
416
|
+
// we don't stopPropagation, so their own click/mousedown handlers still run.
|
|
417
|
+
if (target?.closest("button, a[href]")) {
|
|
418
|
+
event.preventDefault();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// Everything else — field labels, the timeline, the header, surrounding
|
|
422
|
+
// padding, and the empty node-view strip — is inert atom chrome. Swallow the
|
|
423
|
+
// mousedown so ProseMirror can't node-select the block; in the collapsed
|
|
424
|
+
// preview a click there means "edit this step", so begin editing.
|
|
425
|
+
event.preventDefault();
|
|
426
|
+
event.stopPropagation();
|
|
427
|
+
if (!editingRef.current) {
|
|
428
|
+
beginEditing();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
blockEl.addEventListener("mousedown", handleMouseDown);
|
|
432
|
+
return () => blockEl.removeEventListener("mousedown", handleMouseDown);
|
|
433
|
+
}, [beginEditing]);
|
|
409
434
|
|
|
410
435
|
return (
|
|
411
|
-
<div
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
436
|
+
<div ref={anchorRef} style={{ display: "contents" }}>
|
|
437
|
+
{editing ? (
|
|
438
|
+
// Empty steps mounted eagerly (freshly inserted) auto-focus their title. A
|
|
439
|
+
// preview upgraded by a click focuses its field too, so a single click starts
|
|
440
|
+
// editing. The editor tears back down to a preview when focus leaves the step
|
|
441
|
+
// (see TestStepContent's blur handling).
|
|
442
|
+
<TestStepContent
|
|
443
|
+
block={block}
|
|
444
|
+
editor={editor}
|
|
445
|
+
stepNumber={stepNumber}
|
|
446
|
+
viewMode={viewMode}
|
|
447
|
+
autoFocusEnabled={isEmptyStep}
|
|
448
|
+
focusOnMount={focusOnMountRef.current}
|
|
449
|
+
onEditEnd={endEditing}
|
|
450
|
+
/>
|
|
451
|
+
) : (
|
|
452
|
+
<div
|
|
453
|
+
className="bn-teststep-preview-wrapper"
|
|
454
|
+
tabIndex={0}
|
|
455
|
+
onFocusCapture={beginEditing}
|
|
456
|
+
>
|
|
457
|
+
<TestStepPreview
|
|
458
|
+
blockId={block.id}
|
|
459
|
+
stepNumber={stepNumber}
|
|
460
|
+
viewMode={viewMode}
|
|
461
|
+
stepTitle={(block.props.stepTitle as string) || ""}
|
|
462
|
+
stepData={(block.props.stepData as string) || ""}
|
|
463
|
+
expectedResult={(block.props.expectedResult as string) || ""}
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
425
467
|
</div>
|
|
426
468
|
);
|
|
427
469
|
}
|
|
@@ -460,6 +502,12 @@ function TestStepContent({
|
|
|
460
502
|
const uploadImage = useStepImageUpload();
|
|
461
503
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
462
504
|
const [forceVertical, setForceVertical] = useState(false);
|
|
505
|
+
// Set for a moment while a view-mode toggle is in flight. Switching into or
|
|
506
|
+
// out of the horizontal layout swaps the whole subtree, so the OverType editor
|
|
507
|
+
// unmounts and its blur would otherwise trip the focusout teardown below and
|
|
508
|
+
// collapse the step to a preview. The flag tells the teardown to re-focus the
|
|
509
|
+
// freshly mounted editor instead of ending the edit.
|
|
510
|
+
const viewTransitionRef = useRef(false);
|
|
463
511
|
|
|
464
512
|
useEffect(() => {
|
|
465
513
|
const el = containerRef.current?.parentElement;
|
|
@@ -504,6 +552,17 @@ function TestStepContent({
|
|
|
504
552
|
) {
|
|
505
553
|
return;
|
|
506
554
|
}
|
|
555
|
+
// A view-mode toggle remounted the editor; the blur isn't the user
|
|
556
|
+
// leaving the step. Keep editing and move focus into the freshly
|
|
557
|
+
// mounted layout instead of collapsing to a preview.
|
|
558
|
+
if (viewTransitionRef.current) {
|
|
559
|
+
viewTransitionRef.current = false;
|
|
560
|
+
const live = containerRef.current?.querySelector("textarea");
|
|
561
|
+
if (live) {
|
|
562
|
+
(live as HTMLTextAreaElement).focus();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
507
566
|
onEditEnd();
|
|
508
567
|
});
|
|
509
568
|
};
|
|
@@ -642,6 +701,20 @@ function TestStepContent({
|
|
|
642
701
|
} else {
|
|
643
702
|
next = "vertical";
|
|
644
703
|
}
|
|
704
|
+
// Only switching into or out of the horizontal layout swaps the subtree and
|
|
705
|
+
// remounts the editor; vertical↔compact reuse the same editor instance and
|
|
706
|
+
// never blur. Flag just the remounting transitions so the focusout teardown
|
|
707
|
+
// re-focuses the new editor instead of collapsing — and so a genuine
|
|
708
|
+
// click-away after a non-remounting toggle still tears down normally. Cleared
|
|
709
|
+
// on a timer as a safety net in case the expected blur never arrives.
|
|
710
|
+
if (viewMode === "horizontal" || next === "horizontal") {
|
|
711
|
+
viewTransitionRef.current = true;
|
|
712
|
+
if (typeof window !== "undefined") {
|
|
713
|
+
window.setTimeout(() => {
|
|
714
|
+
viewTransitionRef.current = false;
|
|
715
|
+
}, 300);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
645
718
|
writeStepViewMode(next);
|
|
646
719
|
// The shared useStepViewMode hook (in every step, including this one)
|
|
647
720
|
// listens for this event and re-reads the mode, so we don't track it
|