testomatio-editor-blocks 0.4.66 → 0.4.67
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.d.ts +5 -0
- package/package/editor/blocks/step.js +273 -213
- package/package/editor/blocks/stepHorizontalView.d.ts +1 -0
- package/package/editor/blocks/stepHorizontalView.js +2 -2
- package/package/editor/blocks/useDeferredMount.d.ts +26 -0
- package/package/editor/blocks/useDeferredMount.js +54 -0
- package/package/editor/createMarkdownPasteHandler.js +56 -9
- package/package/styles.css +14 -0
- package/package.json +1 -1
- package/src/App.tsx +33 -13
- package/src/editor/blocks/step.tsx +198 -47
- package/src/editor/blocks/stepHorizontalView.tsx +3 -0
- package/src/editor/blocks/stepNumber.test.ts +39 -0
- package/src/editor/blocks/useDeferredMount.ts +66 -0
- package/src/editor/createMarkdownPasteHandler.test.ts +126 -0
- package/src/editor/createMarkdownPasteHandler.ts +60 -8
- package/src/editor/renderingPerf.test.ts +59 -0
- package/src/editor/styles.css +14 -0
|
@@ -34,6 +34,11 @@ export declare function addSnippetBlock(editor: {
|
|
|
34
34
|
document: any[];
|
|
35
35
|
insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
|
|
36
36
|
}): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* A test step's 1-based position within its group: count back over preceding
|
|
39
|
+
* steps (blank lines don't break the run) until a non-step block.
|
|
40
|
+
*/
|
|
41
|
+
export declare function computeStepNumber(allBlocks: any[], blockId: string): number;
|
|
37
42
|
export declare const stepBlock: {
|
|
38
43
|
config: {
|
|
39
44
|
readonly type: "testStep";
|
|
@@ -3,6 +3,7 @@ import { createReactBlockSpec, useEditorChange } from "@blocknote/react";
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { StepField } from "./stepField";
|
|
5
5
|
import { StepHorizontalView } from "./stepHorizontalView";
|
|
6
|
+
import { useDeferredMount } from "./useDeferredMount";
|
|
6
7
|
import { useStepImageUpload } from "../stepImageUpload";
|
|
7
8
|
const EXPECTED_COLLAPSED_KEY = "bn-expected-collapsed";
|
|
8
9
|
const VIEW_MODE_KEY = "bn-step-view-mode";
|
|
@@ -207,6 +208,277 @@ export function addSnippetBlock(editor) {
|
|
|
207
208
|
const inserted = editor.insertBlocks([stepsHeading, emptySnippet], lastBlock.id, "after");
|
|
208
209
|
return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
209
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* A test step's 1-based position within its group: count back over preceding
|
|
213
|
+
* steps (blank lines don't break the run) until a non-step block.
|
|
214
|
+
*/
|
|
215
|
+
export function computeStepNumber(allBlocks, blockId) {
|
|
216
|
+
const blockIndex = allBlocks.findIndex((b) => b.id === blockId);
|
|
217
|
+
if (blockIndex < 0)
|
|
218
|
+
return 1;
|
|
219
|
+
let count = 1;
|
|
220
|
+
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
221
|
+
const b = allBlocks[i];
|
|
222
|
+
if (b.type === "testStep") {
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
else if (isEmptyParagraph(b)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return count;
|
|
233
|
+
}
|
|
234
|
+
/** Strip the most common inline markdown markers for a readable static preview. */
|
|
235
|
+
function stripMarkdownForPreview(text) {
|
|
236
|
+
return text
|
|
237
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, "")
|
|
238
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
239
|
+
.replace(/(\*\*|__|\*|_|~~|`)/g, "")
|
|
240
|
+
.replace(/<\/?[^>]+>/g, "")
|
|
241
|
+
.trim();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Cheap static stand-in shown before a step's interactive editor is mounted.
|
|
245
|
+
* Mirrors the real step's structure/typography so the document height stays
|
|
246
|
+
* stable and so it reads correctly during the brief window before upgrade.
|
|
247
|
+
*/
|
|
248
|
+
function TestStepPreview({ blockId, stepNumber, stepTitle, stepData, expectedResult, }) {
|
|
249
|
+
const titleText = stripMarkdownForPreview(stepTitle);
|
|
250
|
+
const dataText = stripMarkdownForPreview(stepData);
|
|
251
|
+
const expectedText = stripMarkdownForPreview(expectedResult);
|
|
252
|
+
return (_jsxs("div", { className: "bn-teststep", "data-block-id": blockId, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsx("div", { className: "bn-teststep__header", children: _jsx("span", { className: "bn-teststep__title", children: "Step" }) }), _jsx("div", { className: "bn-step-field", children: _jsx("div", { className: "bn-step-editor bn-step-editor--multiline bn-step-editor--preview", children: titleText || " " }) }), dataText ? (_jsx("div", { className: "bn-step-field", children: _jsx("div", { className: "bn-step-editor bn-step-editor--multiline bn-step-editor--preview", children: dataText }) })) : null, expectedText ? (_jsx("div", { className: "bn-step-field", children: _jsx("div", { className: "bn-step-editor bn-step-editor--multiline bn-step-editor--preview", children: expectedText }) })) : null] })] }));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Wrapper that defers mounting the (expensive) interactive step editor until
|
|
256
|
+
* the block scrolls into view. Off-screen steps render {@link TestStepPreview}
|
|
257
|
+
* instead, which is what keeps pasting/loading a large test document fast.
|
|
258
|
+
*
|
|
259
|
+
* The step number is tracked here and pushed down as a prop. We subscribe to
|
|
260
|
+
* editor changes but bail out of re-rendering when the number is unchanged, so
|
|
261
|
+
* ordinary text edits don't re-render every step in the document.
|
|
262
|
+
*/
|
|
263
|
+
function TestStepBlock({ block, editor }) {
|
|
264
|
+
// An empty step is almost always a freshly-inserted one that needs to focus
|
|
265
|
+
// its title immediately, so mount its real editor eagerly. Steps with content
|
|
266
|
+
// (e.g. from a large paste) can safely start as a cheap preview.
|
|
267
|
+
const isEmptyStep = !(block.props.stepTitle || "") &&
|
|
268
|
+
!(block.props.stepData || "") &&
|
|
269
|
+
!(block.props.expectedResult || "");
|
|
270
|
+
const { ref, active, activate, shouldFocusOnActivate } = useDeferredMount({
|
|
271
|
+
initiallyActive: isEmptyStep,
|
|
272
|
+
});
|
|
273
|
+
const [stepNumber, setStepNumber] = useState(() => computeStepNumber(editor.document, block.id));
|
|
274
|
+
useEditorChange(() => {
|
|
275
|
+
// Recompute on change, but bail out of the state update (and therefore the
|
|
276
|
+
// re-render) when the number is unchanged. This is the key win: ordinary
|
|
277
|
+
// text edits leave every step's number untouched, so they don't re-render
|
|
278
|
+
// the whole step list.
|
|
279
|
+
const next = computeStepNumber(editor.document, block.id);
|
|
280
|
+
setStepNumber((prev) => (prev === next ? prev : next));
|
|
281
|
+
}, editor);
|
|
282
|
+
if (active) {
|
|
283
|
+
// Empty steps mounted eagerly (freshly inserted) auto-focus their title.
|
|
284
|
+
// A preview upgraded by a click focuses its field too, so a single click
|
|
285
|
+
// starts editing. Steps upgraded passively (scroll-into-view, hover
|
|
286
|
+
// pre-warm) must never steal focus.
|
|
287
|
+
return (_jsx(TestStepContent, { block: block, editor: editor, stepNumber: stepNumber, autoFocusEnabled: isEmptyStep, focusOnMount: shouldFocusOnActivate }));
|
|
288
|
+
}
|
|
289
|
+
return (_jsx("div", { ref: ref, onMouseDownCapture: () => activate(true), onFocusCapture: () => activate(true), children: _jsx(TestStepPreview, { blockId: block.id, stepNumber: stepNumber, stepTitle: block.props.stepTitle || "", stepData: block.props.stepData || "", expectedResult: block.props.expectedResult || "" }) }));
|
|
290
|
+
}
|
|
291
|
+
function TestStepContent({ block, editor, stepNumber, autoFocusEnabled = false, focusOnMount = false, }) {
|
|
292
|
+
// When a preview is upgraded by a click, focus its primary field once on
|
|
293
|
+
// mount so a single click starts editing (caret at end).
|
|
294
|
+
const mountFocusSignal = focusOnMount ? 1 : 0;
|
|
295
|
+
const stepTitle = block.props.stepTitle || "";
|
|
296
|
+
const stepData = block.props.stepData || "";
|
|
297
|
+
const expectedResult = block.props.expectedResult || "";
|
|
298
|
+
const expectedHasContent = expectedResult.trim().length > 0;
|
|
299
|
+
/* storedExpectedCollapsed removed — currently unused */
|
|
300
|
+
const dataHasContent = stepData.trim().length > 0;
|
|
301
|
+
const [isExpectedVisible, setIsExpectedVisible] = useState(expectedHasContent);
|
|
302
|
+
const [isDataVisible, setIsDataVisible] = useState(dataHasContent);
|
|
303
|
+
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
304
|
+
const uploadImage = useStepImageUpload();
|
|
305
|
+
const [viewMode, setViewMode] = useState(() => readStepViewMode());
|
|
306
|
+
const containerRef = useRef(null);
|
|
307
|
+
const [forceVertical, setForceVertical] = useState(false);
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
var _a;
|
|
310
|
+
const el = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
|
|
311
|
+
if (!el)
|
|
312
|
+
return;
|
|
313
|
+
const observer = new ResizeObserver((entries) => {
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
setForceVertical(entry.contentRect.width < FORCE_VERTICAL_WIDTH);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
observer.observe(el);
|
|
319
|
+
return () => observer.disconnect();
|
|
320
|
+
}, []);
|
|
321
|
+
const effectiveVertical = forceVertical || viewMode === "vertical";
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (typeof window === "undefined") {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const handleStorage = (event) => {
|
|
327
|
+
if (event.key === VIEW_MODE_KEY) {
|
|
328
|
+
setViewMode(readStepViewMode());
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const handleLocal = () => {
|
|
332
|
+
setViewMode(readStepViewMode());
|
|
333
|
+
};
|
|
334
|
+
window.addEventListener("storage", handleStorage);
|
|
335
|
+
window.addEventListener("bn-step-view-mode", handleLocal);
|
|
336
|
+
return () => {
|
|
337
|
+
window.removeEventListener("storage", handleStorage);
|
|
338
|
+
window.removeEventListener("bn-step-view-mode", handleLocal);
|
|
339
|
+
};
|
|
340
|
+
}, []);
|
|
341
|
+
const combinedStepValue = useMemo(() => {
|
|
342
|
+
if (!stepData) {
|
|
343
|
+
return stepTitle;
|
|
344
|
+
}
|
|
345
|
+
return stepTitle ? `${stepTitle}\n${stepData}` : stepData;
|
|
346
|
+
}, [stepData, stepTitle]);
|
|
347
|
+
const handleCombinedStepChange = useCallback((next) => {
|
|
348
|
+
if (next === combinedStepValue) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const [nextTitle = "", ...rest] = next.split("\n");
|
|
352
|
+
const nextData = rest.join("\n");
|
|
353
|
+
editor.updateBlock(block.id, {
|
|
354
|
+
props: {
|
|
355
|
+
stepTitle: nextTitle,
|
|
356
|
+
stepData: nextData,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}, [block.id, combinedStepValue, editor]);
|
|
360
|
+
const handleStepTitleChange = useCallback((next) => {
|
|
361
|
+
if (next === stepTitle) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
editor.updateBlock(block.id, {
|
|
365
|
+
props: {
|
|
366
|
+
stepTitle: next,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}, [editor, block.id, stepTitle]);
|
|
370
|
+
const handleStepDataChange = useCallback((next) => {
|
|
371
|
+
if (next === stepData) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
editor.updateBlock(block.id, {
|
|
375
|
+
props: {
|
|
376
|
+
stepData: next,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
}, [editor, block.id, stepData]);
|
|
380
|
+
const handleShowData = useCallback(() => {
|
|
381
|
+
setIsDataVisible(true);
|
|
382
|
+
setShouldFocusDataField(true);
|
|
383
|
+
}, []);
|
|
384
|
+
const handleHideData = useCallback(() => {
|
|
385
|
+
setIsDataVisible(false);
|
|
386
|
+
editor.updateBlock(block.id, { props: { stepData: "" } });
|
|
387
|
+
}, [editor, block.id]);
|
|
388
|
+
const handleExpectedChange = useCallback((next) => {
|
|
389
|
+
if (next === expectedResult) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
editor.updateBlock(block.id, {
|
|
393
|
+
props: {
|
|
394
|
+
expectedResult: next,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}, [editor, block.id, expectedResult]);
|
|
398
|
+
const handleInsertNextStep = useCallback(() => {
|
|
399
|
+
var _a;
|
|
400
|
+
const allBlocks = editor.document;
|
|
401
|
+
const idx = allBlocks.findIndex((b) => b.id === block.id);
|
|
402
|
+
const next = idx >= 0 ? allBlocks[idx + 1] : null;
|
|
403
|
+
if (next && isEmptyParagraph(next)) {
|
|
404
|
+
editor.removeBlocks([next.id]);
|
|
405
|
+
}
|
|
406
|
+
const currentListStyle = (_a = block.props.listStyle) !== null && _a !== void 0 ? _a : "bullet";
|
|
407
|
+
editor.insertBlocks([
|
|
408
|
+
{
|
|
409
|
+
type: "testStep",
|
|
410
|
+
props: {
|
|
411
|
+
stepTitle: "",
|
|
412
|
+
stepData: "",
|
|
413
|
+
expectedResult: "",
|
|
414
|
+
listStyle: currentListStyle,
|
|
415
|
+
},
|
|
416
|
+
children: [],
|
|
417
|
+
},
|
|
418
|
+
], block.id, "after");
|
|
419
|
+
}, [editor, block.id, block.props]);
|
|
420
|
+
const handleFieldFocus = useCallback(() => {
|
|
421
|
+
var _a, _b, _c;
|
|
422
|
+
const selection = editor.getSelection();
|
|
423
|
+
const blocks = (_a = selection === null || selection === void 0 ? void 0 : selection.blocks) !== null && _a !== void 0 ? _a : [];
|
|
424
|
+
const firstId = (_b = blocks[0]) === null || _b === void 0 ? void 0 : _b.id;
|
|
425
|
+
const lastId = (_c = blocks[blocks.length - 1]) === null || _c === void 0 ? void 0 : _c.id;
|
|
426
|
+
if (firstId === block.id && lastId === block.id) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
editor.setSelection(block.id, block.id);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
//
|
|
434
|
+
}
|
|
435
|
+
}, [editor, block.id]);
|
|
436
|
+
const handleToggleView = useCallback(() => {
|
|
437
|
+
const next = viewMode === "horizontal" ? "vertical" : "horizontal";
|
|
438
|
+
writeStepViewMode(next);
|
|
439
|
+
setViewMode(next);
|
|
440
|
+
if (typeof window !== "undefined") {
|
|
441
|
+
window.dispatchEvent(new Event("bn-step-view-mode"));
|
|
442
|
+
}
|
|
443
|
+
}, [viewMode]);
|
|
444
|
+
const [dataFocusSignal] = useState(0);
|
|
445
|
+
const [expectedFocusSignal, setExpectedFocusSignal] = useState(0);
|
|
446
|
+
const handleShowExpected = useCallback(() => {
|
|
447
|
+
setIsExpectedVisible(true);
|
|
448
|
+
setExpectedFocusSignal((value) => value + 1);
|
|
449
|
+
writeExpectedCollapsedPreference(false);
|
|
450
|
+
}, []);
|
|
451
|
+
const handleHideExpected = useCallback(() => {
|
|
452
|
+
setIsExpectedVisible(false);
|
|
453
|
+
writeExpectedCollapsedPreference(true);
|
|
454
|
+
editor.updateBlock(block.id, { props: { expectedResult: "" } });
|
|
455
|
+
}, [editor, block.id]);
|
|
456
|
+
const viewToggleButton = (_jsx("button", { type: "button", className: `bn-teststep__view-toggle${!effectiveVertical ? " bn-teststep__view-toggle--horizontal" : ""}${forceVertical ? " bn-teststep__view-toggle--disabled" : ""}`, "data-tooltip": forceVertical ? "Not enough space for horizontal view" : "Switch step view", "aria-label": forceVertical ? "Not enough space for horizontal view" : "Switch step view", onClick: forceVertical ? undefined : handleToggleView, "aria-disabled": forceVertical, tabIndex: -1, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) }));
|
|
457
|
+
if (!effectiveVertical) {
|
|
458
|
+
return (_jsx(StepHorizontalView, { ref: containerRef, blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: viewToggleButton, focusSignal: mountFocusSignal }));
|
|
459
|
+
}
|
|
460
|
+
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggleButton] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: autoFocusEnabled && stepTitle.length === 0, focusSignal: mountFocusSignal, multiline: true, disableNewlines: true, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
461
|
+
if (!uploadImage) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
setIsDataVisible(true);
|
|
465
|
+
setShouldFocusDataField(true);
|
|
466
|
+
try {
|
|
467
|
+
const result = await uploadImage(file);
|
|
468
|
+
if (result === null || result === void 0 ? void 0 : result.url) {
|
|
469
|
+
const nextValue = stepData.trim().length > 0 ? `${stepData}\n` : ``;
|
|
470
|
+
editor.updateBlock(block.id, {
|
|
471
|
+
props: {
|
|
472
|
+
stepData: nextValue,
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
console.error("Failed to upload image to Step Data", error);
|
|
479
|
+
}
|
|
480
|
+
} }), isDataVisible ? (_jsx(StepField, { label: "Step data", placeholder: STEP_DATA_PLACEHOLDER, labelAction: _jsx("button", { type: "button", className: "bn-step-field__dismiss", "data-tooltip": "Hide step data", onClick: handleHideData, "aria-label": "Hide step data", children: "\u00D7" }), value: stepData, onChange: handleStepDataChange, autoFocus: shouldFocusDataField, focusSignal: dataFocusSignal, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : null, isExpectedVisible ? (_jsx(StepField, { label: "Expected result", placeholder: EXPECTED_RESULT_PLACEHOLDER, labelAction: _jsx("button", { type: "button", className: "bn-step-field__dismiss", "data-tooltip": "Hide expected result", onClick: handleHideExpected, tabIndex: -1, "aria-label": "Hide expected result", children: "\u00D7" }), value: expectedResult, onChange: handleExpectedChange, multiline: true, focusSignal: expectedFocusSignal, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : null, _jsxs("div", { className: "bn-step-actions", children: [_jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: handleInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }), !isDataVisible && (_jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: handleShowData, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8.666 7.333H12.666V8.667H8.666V12.667H7.332V8.667H3.332V7.333H7.332V3.333H8.666V7.333Z", fill: "currentColor" }) }), "Step data"] })), !isExpectedVisible && (_jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: handleShowExpected, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8.666 7.333H12.666V8.667H8.666V12.667H7.332V8.667H3.332V7.333H7.332V3.333H8.666V7.333Z", fill: "currentColor" }) }), "Expected result"] }))] })] })] }));
|
|
481
|
+
}
|
|
210
482
|
export const stepBlock = createReactBlockSpec({
|
|
211
483
|
type: "testStep",
|
|
212
484
|
content: "none",
|
|
@@ -225,217 +497,5 @@ export const stepBlock = createReactBlockSpec({
|
|
|
225
497
|
},
|
|
226
498
|
},
|
|
227
499
|
}, {
|
|
228
|
-
render: ({ block, editor }) => {
|
|
229
|
-
const stepTitle = block.props.stepTitle || "";
|
|
230
|
-
const stepData = block.props.stepData || "";
|
|
231
|
-
const expectedResult = block.props.expectedResult || "";
|
|
232
|
-
const expectedHasContent = expectedResult.trim().length > 0;
|
|
233
|
-
/* storedExpectedCollapsed removed — currently unused */
|
|
234
|
-
const dataHasContent = stepData.trim().length > 0;
|
|
235
|
-
const [isExpectedVisible, setIsExpectedVisible] = useState(expectedHasContent);
|
|
236
|
-
const [isDataVisible, setIsDataVisible] = useState(dataHasContent);
|
|
237
|
-
const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
|
|
238
|
-
const [documentVersion, setDocumentVersion] = useState(0);
|
|
239
|
-
const uploadImage = useStepImageUpload();
|
|
240
|
-
const [viewMode, setViewMode] = useState(() => readStepViewMode());
|
|
241
|
-
const containerRef = useRef(null);
|
|
242
|
-
const [forceVertical, setForceVertical] = useState(false);
|
|
243
|
-
useEffect(() => {
|
|
244
|
-
var _a;
|
|
245
|
-
const el = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement;
|
|
246
|
-
if (!el)
|
|
247
|
-
return;
|
|
248
|
-
const observer = new ResizeObserver((entries) => {
|
|
249
|
-
for (const entry of entries) {
|
|
250
|
-
setForceVertical(entry.contentRect.width < FORCE_VERTICAL_WIDTH);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
observer.observe(el);
|
|
254
|
-
return () => observer.disconnect();
|
|
255
|
-
}, []);
|
|
256
|
-
const effectiveVertical = forceVertical || viewMode === "vertical";
|
|
257
|
-
// Calculate step number based on position in document
|
|
258
|
-
const stepNumber = useMemo(() => {
|
|
259
|
-
const allBlocks = editor.document;
|
|
260
|
-
const blockIndex = allBlocks.findIndex((b) => b.id === block.id);
|
|
261
|
-
if (blockIndex < 0)
|
|
262
|
-
return 1;
|
|
263
|
-
let count = 1;
|
|
264
|
-
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
265
|
-
const b = allBlocks[i];
|
|
266
|
-
if (b.type === "testStep") {
|
|
267
|
-
count++;
|
|
268
|
-
}
|
|
269
|
-
else if (isEmptyParagraph(b)) {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return count;
|
|
277
|
-
}, [block.id, documentVersion, editor.document]);
|
|
278
|
-
useEditorChange(() => {
|
|
279
|
-
setDocumentVersion((version) => version + 1);
|
|
280
|
-
}, editor);
|
|
281
|
-
useEffect(() => {
|
|
282
|
-
if (typeof window === "undefined") {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const handleStorage = (event) => {
|
|
286
|
-
if (event.key === VIEW_MODE_KEY) {
|
|
287
|
-
setViewMode(readStepViewMode());
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
const handleLocal = () => {
|
|
291
|
-
setViewMode(readStepViewMode());
|
|
292
|
-
};
|
|
293
|
-
window.addEventListener("storage", handleStorage);
|
|
294
|
-
window.addEventListener("bn-step-view-mode", handleLocal);
|
|
295
|
-
return () => {
|
|
296
|
-
window.removeEventListener("storage", handleStorage);
|
|
297
|
-
window.removeEventListener("bn-step-view-mode", handleLocal);
|
|
298
|
-
};
|
|
299
|
-
}, []);
|
|
300
|
-
const combinedStepValue = useMemo(() => {
|
|
301
|
-
if (!stepData) {
|
|
302
|
-
return stepTitle;
|
|
303
|
-
}
|
|
304
|
-
return stepTitle ? `${stepTitle}\n${stepData}` : stepData;
|
|
305
|
-
}, [stepData, stepTitle]);
|
|
306
|
-
const handleCombinedStepChange = useCallback((next) => {
|
|
307
|
-
if (next === combinedStepValue) {
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
const [nextTitle = "", ...rest] = next.split("\n");
|
|
311
|
-
const nextData = rest.join("\n");
|
|
312
|
-
editor.updateBlock(block.id, {
|
|
313
|
-
props: {
|
|
314
|
-
stepTitle: nextTitle,
|
|
315
|
-
stepData: nextData,
|
|
316
|
-
},
|
|
317
|
-
});
|
|
318
|
-
}, [block.id, combinedStepValue, editor]);
|
|
319
|
-
const handleStepTitleChange = useCallback((next) => {
|
|
320
|
-
if (next === stepTitle) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
editor.updateBlock(block.id, {
|
|
324
|
-
props: {
|
|
325
|
-
stepTitle: next,
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
}, [editor, block.id, stepTitle]);
|
|
329
|
-
const handleStepDataChange = useCallback((next) => {
|
|
330
|
-
if (next === stepData) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
editor.updateBlock(block.id, {
|
|
334
|
-
props: {
|
|
335
|
-
stepData: next,
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
}, [editor, block.id, stepData]);
|
|
339
|
-
const handleShowData = useCallback(() => {
|
|
340
|
-
setIsDataVisible(true);
|
|
341
|
-
setShouldFocusDataField(true);
|
|
342
|
-
}, []);
|
|
343
|
-
const handleHideData = useCallback(() => {
|
|
344
|
-
setIsDataVisible(false);
|
|
345
|
-
editor.updateBlock(block.id, { props: { stepData: "" } });
|
|
346
|
-
}, [editor, block.id]);
|
|
347
|
-
const handleExpectedChange = useCallback((next) => {
|
|
348
|
-
if (next === expectedResult) {
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
editor.updateBlock(block.id, {
|
|
352
|
-
props: {
|
|
353
|
-
expectedResult: next,
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
}, [editor, block.id, expectedResult]);
|
|
357
|
-
const handleInsertNextStep = useCallback(() => {
|
|
358
|
-
var _a;
|
|
359
|
-
const allBlocks = editor.document;
|
|
360
|
-
const idx = allBlocks.findIndex((b) => b.id === block.id);
|
|
361
|
-
const next = idx >= 0 ? allBlocks[idx + 1] : null;
|
|
362
|
-
if (next && isEmptyParagraph(next)) {
|
|
363
|
-
editor.removeBlocks([next.id]);
|
|
364
|
-
}
|
|
365
|
-
const currentListStyle = (_a = block.props.listStyle) !== null && _a !== void 0 ? _a : "bullet";
|
|
366
|
-
editor.insertBlocks([
|
|
367
|
-
{
|
|
368
|
-
type: "testStep",
|
|
369
|
-
props: {
|
|
370
|
-
stepTitle: "",
|
|
371
|
-
stepData: "",
|
|
372
|
-
expectedResult: "",
|
|
373
|
-
listStyle: currentListStyle,
|
|
374
|
-
},
|
|
375
|
-
children: [],
|
|
376
|
-
},
|
|
377
|
-
], block.id, "after");
|
|
378
|
-
}, [editor, block.id, block.props]);
|
|
379
|
-
const handleFieldFocus = useCallback(() => {
|
|
380
|
-
var _a, _b, _c;
|
|
381
|
-
const selection = editor.getSelection();
|
|
382
|
-
const blocks = (_a = selection === null || selection === void 0 ? void 0 : selection.blocks) !== null && _a !== void 0 ? _a : [];
|
|
383
|
-
const firstId = (_b = blocks[0]) === null || _b === void 0 ? void 0 : _b.id;
|
|
384
|
-
const lastId = (_c = blocks[blocks.length - 1]) === null || _c === void 0 ? void 0 : _c.id;
|
|
385
|
-
if (firstId === block.id && lastId === block.id) {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
editor.setSelection(block.id, block.id);
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
//
|
|
393
|
-
}
|
|
394
|
-
}, [editor, block.id]);
|
|
395
|
-
const handleToggleView = useCallback(() => {
|
|
396
|
-
const next = viewMode === "horizontal" ? "vertical" : "horizontal";
|
|
397
|
-
writeStepViewMode(next);
|
|
398
|
-
setViewMode(next);
|
|
399
|
-
if (typeof window !== "undefined") {
|
|
400
|
-
window.dispatchEvent(new Event("bn-step-view-mode"));
|
|
401
|
-
}
|
|
402
|
-
}, [viewMode]);
|
|
403
|
-
const [dataFocusSignal] = useState(0);
|
|
404
|
-
const [expectedFocusSignal, setExpectedFocusSignal] = useState(0);
|
|
405
|
-
const handleShowExpected = useCallback(() => {
|
|
406
|
-
setIsExpectedVisible(true);
|
|
407
|
-
setExpectedFocusSignal((value) => value + 1);
|
|
408
|
-
writeExpectedCollapsedPreference(false);
|
|
409
|
-
}, []);
|
|
410
|
-
const handleHideExpected = useCallback(() => {
|
|
411
|
-
setIsExpectedVisible(false);
|
|
412
|
-
writeExpectedCollapsedPreference(true);
|
|
413
|
-
editor.updateBlock(block.id, { props: { expectedResult: "" } });
|
|
414
|
-
}, [editor, block.id]);
|
|
415
|
-
const viewToggleButton = (_jsx("button", { type: "button", className: `bn-teststep__view-toggle${!effectiveVertical ? " bn-teststep__view-toggle--horizontal" : ""}${forceVertical ? " bn-teststep__view-toggle--disabled" : ""}`, "data-tooltip": forceVertical ? "Not enough space for horizontal view" : "Switch step view", "aria-label": forceVertical ? "Not enough space for horizontal view" : "Switch step view", onClick: forceVertical ? undefined : handleToggleView, "aria-disabled": forceVertical, tabIndex: -1, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) }));
|
|
416
|
-
if (!effectiveVertical) {
|
|
417
|
-
return (_jsx(StepHorizontalView, { ref: containerRef, blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: viewToggleButton }));
|
|
418
|
-
}
|
|
419
|
-
return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, ref: containerRef, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__header", children: [_jsx("span", { className: "bn-teststep__title", children: "Step" }), viewToggleButton] }), _jsx(StepField, { label: "Step", showLabel: false, value: stepTitle, placeholder: STEP_TITLE_PLACEHOLDER, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, multiline: true, disableNewlines: true, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
|
|
420
|
-
if (!uploadImage) {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
setIsDataVisible(true);
|
|
424
|
-
setShouldFocusDataField(true);
|
|
425
|
-
try {
|
|
426
|
-
const result = await uploadImage(file);
|
|
427
|
-
if (result === null || result === void 0 ? void 0 : result.url) {
|
|
428
|
-
const nextValue = stepData.trim().length > 0 ? `${stepData}\n` : ``;
|
|
429
|
-
editor.updateBlock(block.id, {
|
|
430
|
-
props: {
|
|
431
|
-
stepData: nextValue,
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
catch (error) {
|
|
437
|
-
console.error("Failed to upload image to Step Data", error);
|
|
438
|
-
}
|
|
439
|
-
} }), isDataVisible ? (_jsx(StepField, { label: "Step data", placeholder: STEP_DATA_PLACEHOLDER, labelAction: _jsx("button", { type: "button", className: "bn-step-field__dismiss", "data-tooltip": "Hide step data", onClick: handleHideData, "aria-label": "Hide step data", children: "\u00D7" }), value: stepData, onChange: handleStepDataChange, autoFocus: shouldFocusDataField, focusSignal: dataFocusSignal, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : null, isExpectedVisible ? (_jsx(StepField, { label: "Expected result", placeholder: EXPECTED_RESULT_PLACEHOLDER, labelAction: _jsx("button", { type: "button", className: "bn-step-field__dismiss", "data-tooltip": "Hide expected result", onClick: handleHideExpected, tabIndex: -1, "aria-label": "Hide expected result", children: "\u00D7" }), value: expectedResult, onChange: handleExpectedChange, multiline: true, focusSignal: expectedFocusSignal, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : null, _jsxs("div", { className: "bn-step-actions", children: [_jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: handleInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }), !isDataVisible && (_jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: handleShowData, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8.666 7.333H12.666V8.667H8.666V12.667H7.332V8.667H3.332V7.333H7.332V3.333H8.666V7.333Z", fill: "currentColor" }) }), "Step data"] })), !isExpectedVisible && (_jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: handleShowExpected, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M8.666 7.333H12.666V8.667H8.666V12.667H7.332V8.667H3.332V7.333H7.332V3.333H8.666V7.333Z", fill: "currentColor" }) }), "Expected result"] }))] })] })] }));
|
|
440
|
-
},
|
|
500
|
+
render: ({ block, editor }) => (_jsx(TestStepBlock, { block: block, editor: editor })),
|
|
441
501
|
});
|
|
@@ -9,6 +9,7 @@ type StepHorizontalViewProps = {
|
|
|
9
9
|
onInsertNextStep: () => void;
|
|
10
10
|
onFieldFocus: () => void;
|
|
11
11
|
viewToggle?: ReactNode;
|
|
12
|
+
focusSignal?: number;
|
|
12
13
|
};
|
|
13
14
|
export declare const StepHorizontalView: import("react").ForwardRefExoticComponent<StepHorizontalViewProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
14
15
|
export {};
|
|
@@ -3,6 +3,6 @@ import { forwardRef } from "react";
|
|
|
3
3
|
import { StepField } from "./stepField";
|
|
4
4
|
const STEP_PLACEHOLDER = "Enter step name";
|
|
5
5
|
const EXPECTED_RESULT_PLACEHOLDER = "Enter expected result";
|
|
6
|
-
export const StepHorizontalView = forwardRef(function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, }, ref) {
|
|
7
|
-
return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, ref: ref, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Step", value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true }) }), _jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Expected result", labelAction: viewToggle, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus }) })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
|
|
6
|
+
export const StepHorizontalView = forwardRef(function StepHorizontalView({ blockId, stepNumber, stepValue, expectedResult, onStepChange, onExpectedChange, onInsertNextStep, onFieldFocus, viewToggle, focusSignal, }, ref) {
|
|
7
|
+
return (_jsxs("div", { className: "bn-teststep bn-teststep--horizontal", "data-block-id": blockId, ref: ref, children: [_jsxs("div", { className: "bn-teststep__timeline", children: [_jsx("span", { className: "bn-teststep__number", children: stepNumber }), _jsx("div", { className: "bn-teststep__line" })] }), _jsxs("div", { className: "bn-teststep__content", children: [_jsxs("div", { className: "bn-teststep__horizontal-fields", children: [_jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Step", value: stepValue, onChange: onStepChange, placeholder: STEP_PLACEHOLDER, focusSignal: focusSignal, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: onFieldFocus, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true }) }), _jsx("div", { className: "bn-teststep__horizontal-col", children: _jsx(StepField, { label: "Expected result", labelAction: viewToggle, value: expectedResult, onChange: onExpectedChange, placeholder: EXPECTED_RESULT_PLACEHOLDER, multiline: true, enableAutocomplete: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: onFieldFocus }) })] }), _jsx("div", { className: "bn-step-actions", children: _jsxs("button", { type: "button", className: "bn-step-action-btn", onClick: onInsertNextStep, children: [_jsx("svg", { className: "bn-step-action-btn__icon", width: "16", height: "16", viewBox: "0 0 13.334 13.334", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M6.667 0a6.667 6.667 0 1 1 0 13.334A6.667 6.667 0 0 1 6.667 0Zm0 1.334a5.333 5.333 0 1 0 0 10.666 5.333 5.333 0 0 0 0-10.666ZM7.334 3.334V6H10v1.334H7.334V10H6V7.334H3.334V6H6V3.334h1.334Z", fill: "currentColor" }) }), "Add new step"] }) })] })] }));
|
|
8
8
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
}
|