Extract combo editor and table grid
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s

This commit is contained in:
Shinuwa 2026-08-24 23:01:35 +02:00
parent 9455b31438
commit e512eef232
5 changed files with 360 additions and 217 deletions

View file

@ -140,15 +140,17 @@ test("toolbox module registry and modules expose expected behavior", async () =>
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
const combosModule = await readFile("website/src/features/toolboxes/modules/CombosModule.jsx", "utf8");
const combosEditor = await readFile("website/src/features/toolboxes/modules/CombosEditor.jsx", "utf8");
const combosParts = await readFile("website/src/features/toolboxes/modules/CombosParts.jsx", "utf8");
const combosList = await readFile("website/src/features/toolboxes/modules/CombosList.jsx", "utf8");
const combosBundle = `${combosModule}\n${combosParts}\n${combosList}`;
const combosBundle = `${combosModule}\n${combosEditor}\n${combosParts}\n${combosList}`;
const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8");
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8");
const tableGrid = await readFile("website/src/features/toolboxes/modules/TableGrid.jsx", "utf8");
const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8");
const tableToolbar = await readFile("website/src/features/toolboxes/modules/TableToolbar.jsx", "utf8");
const tableBundle = `${tableModule}\n${tableGridParts}\n${tableToolbar}`;
const tableBundle = `${tableModule}\n${tableGrid}\n${tableGridParts}\n${tableToolbar}`;
const tableUtils = await readFile("website/src/features/toolboxes/modules/tableUtils.js", "utf8");
const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8");
const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8");
@ -258,9 +260,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(combosUtils, /grab/);
assert.match(combosUtils, /protect/);
assert.match(combosUtils, /shoot/);
assert.match(combosModule, /holdMs/);
assert.match(combosBundle, /holdMs/);
assert.match(combosModule, /toggleSelectedInputHold/);
assert.match(combosModule, /combo-hold-field/);
assert.match(combosEditor, /combo-hold-field/);
assert.match(combosParts, /useInlineEdit/);
assert.match(combosModule, /useGroupedReorder/);
assert.match(combosModule, /reorderFeatures/);
@ -316,6 +318,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(tableUtils, /parseTsvBlock/);
assert.match(tableGridParts, /is-move-target/);
assert.match(tableUtils, /isMultiCellSelection/);
assert.match(tableGrid, /export function TableGrid/);
assert.match(tableModule, /handleCellKeyDown/);
assert.match(tableModule, /clearSelection/);
assert.match(tableModule, /event\.key === "Delete"/);

View file

@ -0,0 +1,194 @@
// Rôle : rend le formulaire de création et édition des combos.
import { Icon } from "../../../components/Icon.jsx";
import { ComboInputToken, ComboSequence } from "./CombosParts.jsx";
import {
DEVICE_OPTIONS,
createInput,
getHoldSeconds
} from "./combosUtils.js";
export function CombosEditor({
data,
textContent,
comboCategory,
comboName,
customKey,
keyboardLayout,
paletteColumns,
draftInputs,
cleanDraftInputs,
activeStepIndex,
selectedInput,
selectedDraftInput,
simultaneousMode,
editingComboId,
onDeviceChange,
onKeyboardLayoutChange,
onComboCategoryChange,
onComboNameChange,
onCustomKeyChange,
onCustomKeyAdd,
onComboSave,
onDraftInputAdd,
onPaletteInputDrag,
onDraftInputDrag,
onInputDrop,
onNewStepDrop,
onStepSelect,
onInputSelect,
onSelectedInputHoldToggle,
onSelectedInputHoldChange,
onSimultaneousModeToggle,
onLastInputRemove,
onActiveStepRemove,
onComboEditCancel
}) {
return (
<div className="module-add-panel combos-editor">
<div className="combos-device-row">
<label>
<span>{textContent.deviceLabel || "Périphérique"}</span>
<select value={data.device} onChange={(event) => onDeviceChange(event.target.value)}>
{DEVICE_OPTIONS.map((device) => (
<option key={device.value} value={device.value}>{device.label}</option>
))}
</select>
</label>
{data.device === "keyboardMouse" && (
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
{[
{ value: "azerty", label: "AZERTY" },
{ value: "qwerty", label: "QWERTY" }
].map((layout) => (
<button
key={layout.value}
type="button"
className={keyboardLayout === layout.value ? "active" : ""}
onClick={() => onKeyboardLayoutChange(layout.value)}
aria-pressed={keyboardLayout === layout.value}
>
{layout.label}
</button>
))}
</div>
)}
</div>
<form className="combos-add-form" onSubmit={onComboSave}>
<div className="combos-form-grid">
<label>
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
<input value={comboCategory} onChange={(event) => onComboCategoryChange(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
</label>
<label>
<span>{textContent.comboNameLabel || "Nom"}</span>
<input value={comboName} onChange={(event) => onComboNameChange(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
</label>
</div>
<div className="combos-palette" aria-label={textContent.paletteLabel || "Palette de touches"}>
{paletteColumns.map((columnGroups, columnIndex) => (
<div className="combos-palette-column" key={columnIndex}>
{columnGroups.map((group) => (
<div className={`combos-palette-group ${group.kind === "directions" ? "is-direction-grid" : ""}`} key={group.title}>
<span>{group.title}</span>
<div>
{group.inputs.map((input, inputIndex) => {
if (!input) return <span className="combo-palette-empty-cell" key={`empty-${inputIndex}`} aria-hidden="true" />;
const [kind, value] = input;
return (
<ComboInputToken
key={`${kind}-${value}`}
input={createInput(kind, value)}
device={data.device}
palette
onClick={() => onDraftInputAdd(kind, value)}
onDragStart={(event) => onPaletteInputDrag(event, kind, value)}
/>
);
})}
</div>
</div>
))}
</div>
))}
</div>
{data.device === "keyboardMouse" && (
<div className="inline-form combos-custom-key-form">
<input value={customKey} onChange={(event) => onCustomKeyChange(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
<button type="button" onClick={onCustomKeyAdd}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
</div>
)}
<div className="combo-draft-panel">
<ComboSequence
inputs={draftInputs}
device={data.device}
emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."}
activeStepIndex={activeStepIndex}
selectedInput={selectedInput}
onSelectStep={onStepSelect}
onSelectInput={onInputSelect}
onDropInput={onInputDrop}
onDropNewStep={onNewStepDrop}
onDragInput={onDraftInputDrag}
/>
<div className="combo-draft-actions">
<label className="combo-hold-field">
<span className="combo-hold-controls">
<button
type="button"
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
onClick={onSelectedInputHoldToggle}
disabled={!selectedDraftInput}
aria-pressed={Boolean(selectedDraftInput?.hold || selectedDraftInput?.holdMs)}
>
{textContent.holdToggleLabel || "Hold"}
</button>
<input
type="number"
min="0"
max="99"
step="1"
inputMode="numeric"
value={selectedDraftInput ? getHoldSeconds(selectedDraftInput) || "" : ""}
onChange={(event) => onSelectedInputHoldChange(event.target.value)}
disabled={!selectedDraftInput}
placeholder="0"
aria-label={textContent.holdLabel || "Maintien en secondes"}
/>
</span>
</label>
<button
type="button"
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}
onClick={onSimultaneousModeToggle}
aria-pressed={simultaneousMode}
title={textContent.simultaneousModeTitle || "Ajout simultané"}
aria-label={textContent.simultaneousModeTitle || "Ajout simultané"}
>
<span className={!simultaneousMode ? "active" : ""}>{textContent.successiveModeLabel || "A › B"}</span>
<span className={simultaneousMode ? "active" : ""}>{textContent.simultaneousModeLabel || "A + B"}</span>
</button>
<button type="button" className="notepad-toolbar-button" onClick={onLastInputRemove} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
<Icon name="back" />
</button>
<button type="button" className="notepad-toolbar-button danger" onClick={onActiveStepRemove} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
<Icon name="trash" />
</button>
<div className="combo-save-actions">
{editingComboId && (
<button type="button" className="combo-cancel-button" onClick={onComboEditCancel}>
{textContent.cancelEditButton || "Annuler"}
</button>
)}
<button className="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
{editingComboId ? textContent.saveComboButton || "Enregistrer" : textContent.addComboButton || "Ajouter le combo"}
</button>
</div>
</div>
</div>
</form>
</div>
);
}

View file

@ -1,20 +1,17 @@
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
import { useMemo, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import {
applyGroupedReorderOperation,
getGroupedEntries,
useGroupedReorder
} from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ComboInputToken, ComboSequence } from "./CombosParts.jsx";
import { CombosEditor } from "./CombosEditor.jsx";
import { CombosList } from "./CombosList.jsx";
import {
DEVICE_OPTIONS,
createCombo,
createInput,
getComboCategory,
getHoldSeconds,
getPaletteColumns,
getPaletteGroups,
normalizeCustomKey,
@ -326,155 +323,45 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
return (
<div className="combos-module">
{(editing || editingComboId) && (
<div className="module-add-panel combos-editor">
<div className="combos-device-row">
<label>
<span>{textContent.deviceLabel || "Périphérique"}</span>
<select value={data.device} onChange={(event) => updateDevice(event.target.value)}>
{DEVICE_OPTIONS.map((device) => (
<option key={device.value} value={device.value}>{device.label}</option>
))}
</select>
</label>
{data.device === "keyboardMouse" && (
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
{[
{ value: "azerty", label: "AZERTY" },
{ value: "qwerty", label: "QWERTY" }
].map((layout) => (
<button
key={layout.value}
type="button"
className={keyboardLayout === layout.value ? "active" : ""}
onClick={() => setKeyboardLayout(layout.value)}
aria-pressed={keyboardLayout === layout.value}
>
{layout.label}
</button>
))}
</div>
)}
</div>
<form className="combos-add-form" onSubmit={saveCombo}>
<div className="combos-form-grid">
<label>
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
<input value={comboCategory} onChange={(event) => setComboCategory(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
</label>
<label>
<span>{textContent.comboNameLabel || "Nom"}</span>
<input value={comboName} onChange={(event) => setComboName(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
</label>
</div>
<div className="combos-palette" aria-label={textContent.paletteLabel || "Palette de touches"}>
{paletteColumns.map((columnGroups, columnIndex) => (
<div className="combos-palette-column" key={columnIndex}>
{columnGroups.map((group) => (
<div className={`combos-palette-group ${group.kind === "directions" ? "is-direction-grid" : ""}`} key={group.title}>
<span>{group.title}</span>
<div>
{group.inputs.map((input, inputIndex) => {
if (!input) return <span className="combo-palette-empty-cell" key={`empty-${inputIndex}`} aria-hidden="true" />;
const [kind, value] = input;
return (
<ComboInputToken
key={`${kind}-${value}`}
input={createInput(kind, value)}
device={data.device}
palette
onClick={() => addDraftInput(kind, value)}
onDragStart={(event) => dragPaletteInput(event, kind, value)}
/>
);
})}
</div>
</div>
))}
</div>
))}
</div>
{data.device === "keyboardMouse" && (
<div className="inline-form combos-custom-key-form">
<input value={customKey} onChange={(event) => setCustomKey(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
<button type="button" onClick={addCustomKey}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
</div>
)}
<div className="combo-draft-panel">
<ComboSequence
inputs={draftInputs}
device={data.device}
emptyLabel={textContent.emptyDraft || "Aucune touche sélectionnée."}
<CombosEditor
data={data}
textContent={textContent}
comboCategory={comboCategory}
comboName={comboName}
customKey={customKey}
keyboardLayout={keyboardLayout}
paletteColumns={paletteColumns}
draftInputs={draftInputs}
cleanDraftInputs={cleanDraftInputs}
activeStepIndex={activeStepIndex}
selectedInput={selectedInput}
onSelectStep={selectDraftStep}
onSelectInput={(stepIndex, inputIndex) => {
selectedDraftInput={selectedDraftInput}
simultaneousMode={simultaneousMode}
editingComboId={editingComboId}
onDeviceChange={updateDevice}
onKeyboardLayoutChange={setKeyboardLayout}
onComboCategoryChange={setComboCategory}
onComboNameChange={setComboName}
onCustomKeyChange={setCustomKey}
onCustomKeyAdd={addCustomKey}
onComboSave={saveCombo}
onDraftInputAdd={addDraftInput}
onPaletteInputDrag={dragPaletteInput}
onDraftInputDrag={dragDraftInput}
onInputDrop={dropInputOnStep}
onNewStepDrop={dropInputAsNewStep}
onStepSelect={selectDraftStep}
onInputSelect={(stepIndex, inputIndex) => {
setActiveStepIndex(stepIndex);
setSelectedInput({ stepIndex, inputIndex });
}}
onDropInput={dropInputOnStep}
onDropNewStep={dropInputAsNewStep}
onDragInput={dragDraftInput}
onSelectedInputHoldToggle={toggleSelectedInputHold}
onSelectedInputHoldChange={updateSelectedInputHold}
onSimultaneousModeToggle={() => setSimultaneousMode((enabled) => !enabled)}
onLastInputRemove={removeLastInput}
onActiveStepRemove={removeActiveStep}
onComboEditCancel={cancelComboEdit}
/>
<div className="combo-draft-actions">
<label className="combo-hold-field">
<span className="combo-hold-controls">
<button
type="button"
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
onClick={toggleSelectedInputHold}
disabled={!selectedDraftInput}
aria-pressed={Boolean(selectedDraftInput?.hold || selectedDraftInput?.holdMs)}
>
{textContent.holdToggleLabel || "Hold"}
</button>
<input
type="number"
min="0"
max="99"
step="1"
inputMode="numeric"
value={selectedDraftInput ? getHoldSeconds(selectedDraftInput) || "" : ""}
onChange={(event) => updateSelectedInputHold(event.target.value)}
disabled={!selectedDraftInput}
placeholder="0"
aria-label={textContent.holdLabel || "Maintien en secondes"}
/>
</span>
</label>
<button
type="button"
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}
onClick={() => setSimultaneousMode((enabled) => !enabled)}
aria-pressed={simultaneousMode}
title={textContent.simultaneousModeTitle || "Ajout simultané"}
aria-label={textContent.simultaneousModeTitle || "Ajout simultané"}
>
<span className={!simultaneousMode ? "active" : ""}>{textContent.successiveModeLabel || "A › B"}</span>
<span className={simultaneousMode ? "active" : ""}>{textContent.simultaneousModeLabel || "A + B"}</span>
</button>
<button type="button" className="notepad-toolbar-button" onClick={removeLastInput} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
<Icon name="back" />
</button>
<button type="button" className="notepad-toolbar-button danger" onClick={removeActiveStep} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
<Icon name="trash" />
</button>
<div className="combo-save-actions">
{editingComboId && (
<button type="button" className="combo-cancel-button" onClick={cancelComboEdit}>
{textContent.cancelEditButton || "Annuler"}
</button>
)}
<button className="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
{editingComboId ? textContent.saveComboButton || "Enregistrer" : textContent.addComboButton || "Ajouter le combo"}
</button>
</div>
</div>
</div>
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">

View file

@ -0,0 +1,92 @@
// Rôle : rend la grille du tableau et connecte les cellules aux handlers du module.
import { columnIndexToName, parseCellAddress } from "./tableFormulaEngine.js";
import { HeaderLabelInput, TableRow } from "./TableGridParts.jsx";
export function TableGrid({
data,
textContent,
evaluatedCells,
editingCell,
highlightedCell,
selection,
movePreview,
columnWidths,
rowHeaderWidth,
draggingSelectionRef,
onColumnLabelChange,
onRowLabelChange,
onCellChange,
onFormulaSelectionStart,
onFormulaSelectionExtend,
onStartMoveSelection,
onMoveSelection,
onSelectCell,
onExtendSelection,
onPasteSelection,
onCellKeyDown,
onStartEditing,
onEditingCellClear,
registerInput
}) {
return (
<div className="table-grid-wrap legacy-scrollbar">
<div
className="table-grid"
style={{
"--table-columns": `${rowHeaderWidth}px ${columnWidths.map((width) => `minmax(${width}px, 1fr)`).join(" ")}`,
"--table-min-width": `${rowHeaderWidth + columnWidths.reduce((total, width) => total + width, 0)}px`
}}
role="grid"
aria-label={textContent.gridLabel || "Tableau"}
>
<span className="table-corner" aria-hidden="true" />
{Array.from({ length: data.columns }, (_, columnIndex) => (
<HeaderLabelInput
className="table-header-cell table-header-input"
key={columnIndex}
value={data.columnLabels?.[columnIndex] || columnIndexToName(columnIndex)}
fallback={columnIndexToName(columnIndex)}
onSave={(value) => onColumnLabelChange(columnIndex, value)}
aria-label={`${textContent.columnLabel || "Intitulé de colonne"} ${columnIndexToName(columnIndex)}`}
title={`${columnIndexToName(columnIndex)} - ${textContent.columnFormulaTitle || "Référence conservée dans les formules"}`}
/>
))}
{Array.from({ length: data.rows }, (_, rowIndex) => (
<TableRow
key={rowIndex}
rowIndex={rowIndex}
columns={data.columns}
cells={data.cells}
evaluatedCells={evaluatedCells}
editingCell={editingCell}
highlightedCell={highlightedCell}
textContent={textContent}
rowLabel={data.rowLabels?.[rowIndex] || ""}
selection={selection}
movePreview={movePreview}
onRowLabelChange={onRowLabelChange}
onFocus={(cell) => {
const parsed = parseCellAddress(cell);
if (parsed && !selection) onSelectCell(parsed.rowIndex, parsed.columnIndex);
}}
onBlur={(cell) => {
if (editingCell === cell) onEditingCellClear();
}}
onCellChange={onCellChange}
onFormulaSelectionStart={onFormulaSelectionStart}
onFormulaSelectionExtend={onFormulaSelectionExtend}
onStartMoveSelection={onStartMoveSelection}
onMoveSelection={onMoveSelection}
onSelectCell={onSelectCell}
onExtendSelection={onExtendSelection}
onPasteSelection={onPasteSelection}
draggingSelectionRef={draggingSelectionRef}
onCellKeyDown={onCellKeyDown}
onStartEditing={onStartEditing}
registerInput={registerInput}
/>
))}
</div>
</div>
);
}

View file

@ -2,7 +2,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { cellAddress, columnIndexToName, evaluateTable, parseCellAddress } from "./tableFormulaEngine.js";
import { HeaderLabelInput, TableRow } from "./TableGridParts.jsx";
import { TableGrid } from "./TableGrid.jsx";
import { TableToolbar } from "./TableToolbar.jsx";
import {
DEFAULT_COLUMNS,
@ -410,49 +410,19 @@ export function TableModule({ toolboxId, moduleId, context, textExchangeOpen })
</div>
)}
<TableToolbar rows={data.rows} columns={data.columns} textContent={textContent} onResize={resize} onCopy={copyTsv} />
<div className="table-grid-wrap legacy-scrollbar">
<div
className="table-grid"
style={{
"--table-columns": `${rowHeaderWidth}px ${columnWidths.map((width) => `minmax(${width}px, 1fr)`).join(" ")}`,
"--table-min-width": `${rowHeaderWidth + columnWidths.reduce((total, width) => total + width, 0)}px`
}}
role="grid"
aria-label={textContent.gridLabel || "Tableau"}
>
<span className="table-corner" aria-hidden="true" />
{Array.from({ length: data.columns }, (_, columnIndex) => (
<HeaderLabelInput
className="table-header-cell table-header-input"
key={columnIndex}
value={data.columnLabels?.[columnIndex] || columnIndexToName(columnIndex)}
fallback={columnIndexToName(columnIndex)}
onSave={(value) => setColumnLabel(columnIndex, value)}
aria-label={`${textContent.columnLabel || "Intitulé de colonne"} ${columnIndexToName(columnIndex)}`}
title={`${columnIndexToName(columnIndex)} - ${textContent.columnFormulaTitle || "Référence conservée dans les formules"}`}
/>
))}
{Array.from({ length: data.rows }, (_, rowIndex) => (
<TableRow
key={rowIndex}
rowIndex={rowIndex}
columns={data.columns}
cells={data.cells}
<TableGrid
data={data}
textContent={textContent}
evaluatedCells={evaluatedCells}
editingCell={editingCell}
highlightedCell={highlightedCell}
textContent={textContent}
rowLabel={data.rowLabels?.[rowIndex] || ""}
selection={selection}
movePreview={movePreview}
columnWidths={columnWidths}
rowHeaderWidth={rowHeaderWidth}
draggingSelectionRef={draggingSelectionRef}
onColumnLabelChange={setColumnLabel}
onRowLabelChange={setRowLabel}
onFocus={(cell) => {
const parsed = parseCellAddress(cell);
if (parsed && !selection) selectCell(parsed.rowIndex, parsed.columnIndex);
}}
onBlur={(cell) => {
if (editingCell === cell) setEditingCell("");
}}
onCellChange={setCellValue}
onFormulaSelectionStart={startFormulaSelection}
onFormulaSelectionExtend={extendFormulaSelection}
@ -461,14 +431,11 @@ export function TableModule({ toolboxId, moduleId, context, textExchangeOpen })
onSelectCell={selectCell}
onExtendSelection={extendSelection}
onPasteSelection={pasteSelection}
draggingSelectionRef={draggingSelectionRef}
onCellKeyDown={handleCellKeyDown}
onStartEditing={startEditing}
onEditingCellClear={() => setEditingCell("")}
registerInput={registerInput}
/>
))}
</div>
</div>
</div>
);
}