Integrate circuit list navigation and AMEV cell picker
Preserve the ID fallback and compact editor controls. Add dialog keyboard focus, Tab containment and Escape handling; document navigation and picker behavior. Validation: 418 tests, API/web builds, scripts and Next typechecks, isolated AMEV dialog checks and diff checks passed. Full GUI retest waived by the user based on the colleague's prior testing; the keyboard follow-up was checked in isolation.
This commit is contained in:
commit
641513c2ef
6 changed files with 215 additions and 17 deletions
|
|
@ -164,6 +164,9 @@ Keyboard behavior:
|
||||||
- Escape cancels
|
- Escape cancels
|
||||||
- Tab / Shift+Tab navigates editable cells
|
- Tab / Shift+Tab navigates editable cells
|
||||||
- Arrow keys navigate cells when not editing
|
- Arrow keys navigate cells when not editing
|
||||||
|
- Alt+ArrowDown on a GZF cell opens the AMEV table (`GzfPickerModal`); the
|
||||||
|
selected GZF cell also shows a small table-icon button. The picked value is
|
||||||
|
committed like a typed edit.
|
||||||
|
|
||||||
Support Ctrl+Plus and Ctrl+Shift+Plus for insertion.
|
Support Ctrl+Plus and Ctrl+Shift+Plus for insertion.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
# Circuit List Editor Interactions
|
# Circuit List Editor Interactions
|
||||||
|
|
||||||
|
The back-arrow link beside the page title returns to the project's distribution
|
||||||
|
board overview without changing circuit-list data.
|
||||||
|
|
||||||
## Editing Model
|
## Editing Model
|
||||||
|
|
||||||
Inline cells are static text by default. A cell enters edit mode by:
|
Inline cells are static text by default. A cell enters edit mode by:
|
||||||
|
|
@ -44,6 +47,7 @@ Selection, keyboard movement, and editability checks run against this normalized
|
||||||
- arrow keys: move selected cell in grid when not editing
|
- arrow keys: move selected cell in grid when not editing
|
||||||
- type-to-edit: printable keys open edit mode and replace current display value with typed character
|
- type-to-edit: printable keys open edit mode and replace current display value with typed character
|
||||||
- `Ctrl+Plus` / `Ctrl+Shift+Plus`: insert below the selected circuit or device context
|
- `Ctrl+Plus` / `Ctrl+Shift+Plus`: insert below the selected circuit or device context
|
||||||
|
- `Alt+ArrowDown` on a GZF cell (selected or editing): open the AMEV simultaneity-factor table
|
||||||
- `Delete`: delete the selected circuit or device context after confirmation
|
- `Delete`: delete the selected circuit or device context after confirmation
|
||||||
|
|
||||||
For insertion, a circuit-level cell creates a reserve circuit directly after the selected circuit. A device-level cell creates a manual device row directly after the selected device. On the virtual `-frei-` row, or without a valid row selection, the shortcut creates a circuit at the end of the active section. Insertions use command history and never renumber existing equipment identifiers.
|
For insertion, a circuit-level cell creates a reserve circuit directly after the selected circuit. A device-level cell creates a manual device row directly after the selected device. On the virtual `-frei-` row, or without a valid row selection, the shortcut creates a circuit at the end of the active section. Insertions use command history and never renumber existing equipment identifiers.
|
||||||
|
|
@ -52,6 +56,23 @@ For deletion, a circuit-level cell targets the complete circuit and a device-lev
|
||||||
|
|
||||||
Creating or deleting a device row updates the row and the circuit reserve state in one SQLite transaction.
|
Creating or deleting a device row updates the row and the circuit reserve state in one SQLite transaction.
|
||||||
|
|
||||||
|
## AMEV Simultaneity Factor
|
||||||
|
|
||||||
|
The selected GZF cell shows a small table-icon button; it and `Alt+ArrowDown` open
|
||||||
|
the shared `GzfPickerModal` (AMEV Planungshilfe, Tabelle 3), which the project
|
||||||
|
device dialog uses as well. "Übernehmen" commits the mean value of the chosen
|
||||||
|
consumer group through the same path as a typed edit (`commitEdit` with an
|
||||||
|
explicit target cell), so it is one undoable command and works on device rows,
|
||||||
|
compact circuits, reserve circuits and the `-frei-` row. A running inline edit
|
||||||
|
of the cell is discarded when the picker opens. The button only appears on the
|
||||||
|
selected cell; the grid keeps its static-text cells.
|
||||||
|
|
||||||
|
The dialog takes keyboard focus, keeps Tab/Shift+Tab within its actions and
|
||||||
|
closes on Escape without writing a value. Closing returns focus to the grid;
|
||||||
|
only "Übernehmen" saves the selected mean value. Opening the picker is disabled
|
||||||
|
while a write or history operation is running. Its cell button stays outside
|
||||||
|
the Tab order; `Alt+ArrowDown` provides keyboard access from the selected cell.
|
||||||
|
|
||||||
## Equipment Identifier Validation
|
## Equipment Identifier Validation
|
||||||
|
|
||||||
Equipment identifiers are compared case-insensitively after trimming whitespace. A conflicting edit is highlighted before submission and cannot be committed. Existing conflicts are highlighted in the grid. Validation errors remain dismissible above the editor instead of replacing the complete workspace.
|
Equipment identifiers are compared case-insensitively after trimming whitespace. A conflicting edit is highlighted before submission and cannot be committed. Existing conflicts are highlighted in the grid. Validation errors remain dismissible above the editor instead of replacing the complete workspace.
|
||||||
|
|
|
||||||
|
|
@ -1324,6 +1324,38 @@ a.kpi:hover {
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Picker trigger on the selected GZF cell. Positioned over the cell so the
|
||||||
|
column width does not change while the selection moves. */
|
||||||
|
.tree-grid td:has(> .cell-picker-trigger) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-grid .cell-picker-trigger {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0.2rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.15rem;
|
||||||
|
height: 1.15rem;
|
||||||
|
border: 1px solid var(--accent-blue-strong-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .tree-grid .cell-picker-trigger {
|
||||||
|
color: var(--color-accent-pale);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-grid .cell-picker-trigger:hover {
|
||||||
|
background: var(--accent-blue-soft-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.tree-grid .section-title {
|
.tree-grid .section-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { CircuitTreeEditor } from "../../../../../../frontend/components/circuit-tree-editor";
|
import { CircuitTreeEditor } from "../../../../../../frontend/components/circuit-tree-editor";
|
||||||
|
|
@ -10,10 +11,20 @@ export default function CircuitTreeEditPage() {
|
||||||
return (
|
return (
|
||||||
<main className="container-fluid px-3 px-xxl-4 py-4">
|
<main className="container-fluid px-3 px-xxl-4 py-4">
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
<div className="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<Link
|
||||||
|
aria-label="Zurück zur Verteilerübersicht"
|
||||||
|
className="btn btn-outline-secondary btn-sm"
|
||||||
|
href={`/projects/${params.projectId}`}
|
||||||
|
title="Zurück zur Verteilerübersicht"
|
||||||
|
>
|
||||||
|
<ArrowLeft aria-hidden="true" size={18} />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="h4 mb-1">Stromkreisliste</h1>
|
<h1 className="h4 mb-1">Stromkreisliste</h1>
|
||||||
<p className="text-secondary mb-0">Stromkreise und zugeordnete Geräte bearbeiten.</p>
|
<p className="text-secondary mb-0">Stromkreise und zugeordnete Geräte bearbeiten.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<Link href={`/projects/${params.projectId}`} className="btn btn-outline-secondary btn-sm">
|
<Link href={`/projects/${params.projectId}`} className="btn btn-outline-secondary btn-sm">
|
||||||
Zum Projekt
|
Zum Projekt
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
Plus,
|
Plus,
|
||||||
Redo2,
|
Redo2,
|
||||||
ShieldPlus,
|
ShieldPlus,
|
||||||
|
TableProperties,
|
||||||
Trash2,
|
Trash2,
|
||||||
Undo2,
|
Undo2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -159,6 +160,7 @@ import type {
|
||||||
CircuitSnapshot,
|
CircuitSnapshot,
|
||||||
} from "../../domain/models/circuit-structure-project-command.model";
|
} from "../../domain/models/circuit-structure-project-command.model";
|
||||||
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
||||||
|
import { GzfPickerModal } from "./gzf-picker-modal";
|
||||||
import { CircuitGroupModal } from "./circuit-group-modal";
|
import { CircuitGroupModal } from "./circuit-group-modal";
|
||||||
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
||||||
import {
|
import {
|
||||||
|
|
@ -318,6 +320,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
useState<StructureComponentEditorIntent | null>(null);
|
useState<StructureComponentEditorIntent | null>(null);
|
||||||
const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] =
|
const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] =
|
||||||
useState<CircuitGroupEditorIntent | null>(null);
|
useState<CircuitGroupEditorIntent | null>(null);
|
||||||
|
// GZF cell the AMEV picker writes to; null while the picker is closed.
|
||||||
|
const [gzfPickerCell, setGzfPickerCell] = useState<SelectedCell | null>(null);
|
||||||
const [protectionEditorCircuit, setProtectionEditorCircuit] =
|
const [protectionEditorCircuit, setProtectionEditorCircuit] =
|
||||||
useState<CircuitTreeCircuitDto | null>(null);
|
useState<CircuitTreeCircuitDto | null>(null);
|
||||||
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
const [projectDevices, setProjectDevices] = useState<ProjectDeviceDto[]>([]);
|
||||||
|
|
@ -1871,22 +1875,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commits the active editor input through command history so edits remain undoable.
|
// Commits the active editor input through command history so edits remain undoable.
|
||||||
|
// `target` commits a value for a cell without an open inline editor
|
||||||
|
// (AMEV GZF picker); the regular edit path passes no target.
|
||||||
async function commitEdit(
|
async function commitEdit(
|
||||||
direction: SaveDirection = "stay",
|
direction: SaveDirection = "stay",
|
||||||
draftOverride?: string
|
draftOverride?: string,
|
||||||
|
target?: SelectedCell
|
||||||
) {
|
) {
|
||||||
if (!editingCell) {
|
const session = target ?? editingCell;
|
||||||
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = findRow(editingCell.rowKey);
|
const row = findRow(session.rowKey);
|
||||||
const cell = row?.cells.find((entry) => entry.cellKey === editingCell.cellKey);
|
const cell = row?.cells.find((entry) => entry.cellKey === session.cellKey);
|
||||||
if (!row || !cell || !cell.editable) {
|
if (!row || !cell || !cell.editable) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const draft = draftOverride ?? editingCell.draft;
|
const draft = draftOverride ?? editingCell?.draft ?? "";
|
||||||
if (
|
if (
|
||||||
editingCell.cellKey === "equipmentIdentifier" &&
|
session.cellKey === "equipmentIdentifier" &&
|
||||||
row.circuit &&
|
row.circuit &&
|
||||||
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, draft)
|
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, draft)
|
||||||
) {
|
) {
|
||||||
|
|
@ -1894,7 +1902,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
let targetSelection: SelectedCell = { rowKey: session.rowKey, cellKey: session.cellKey };
|
||||||
const nextSelectionIntent = () => {
|
const nextSelectionIntent = () => {
|
||||||
let selected = targetSelection;
|
let selected = targetSelection;
|
||||||
if (direction !== "stay") {
|
if (direction !== "stay") {
|
||||||
|
|
@ -1917,7 +1925,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
const selection = await createFromPlaceholder(
|
const selection = await createFromPlaceholder(
|
||||||
row.sectionId,
|
row.sectionId,
|
||||||
editingCell.cellKey,
|
session.cellKey,
|
||||||
draft
|
draft
|
||||||
);
|
);
|
||||||
targetSelection = selection;
|
targetSelection = selection;
|
||||||
|
|
@ -1934,7 +1942,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
const command: HistoryCommand = {
|
const command: HistoryCommand = {
|
||||||
label: "Stromkreisfeld bearbeiten",
|
label: "Stromkreisfeld bearbeiten",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
await patchCircuit(row.circuit!.id, editingCell.cellKey, next);
|
await patchCircuit(row.circuit!.id, session.cellKey, next);
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1948,7 +1956,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
const command: HistoryCommand = {
|
const command: HistoryCommand = {
|
||||||
label: "Gerätefeld bearbeiten",
|
label: "Gerätefeld bearbeiten",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
await patchDeviceRow(row.device!.id, editingCell.cellKey, next);
|
await patchDeviceRow(row.device!.id, session.cellKey, next);
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1963,7 +1971,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
label: "Gerätezeile im Reservestromkreis erstellen",
|
label: "Gerätezeile im Reservestromkreis erstellen",
|
||||||
redo: async () => {
|
redo: async () => {
|
||||||
const editValues = createValuesFromEditPatch(
|
const editValues = createValuesFromEditPatch(
|
||||||
buildDeviceRowEditPatch(editingCell.cellKey, next)
|
buildDeviceRowEditPatch(session.cellKey, next)
|
||||||
);
|
);
|
||||||
const snapshot = createDeviceRowSnapshot(
|
const snapshot = createDeviceRowSnapshot(
|
||||||
row.circuit!.id,
|
row.circuit!.id,
|
||||||
|
|
@ -1983,7 +1991,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
snapshot,
|
snapshot,
|
||||||
"Gerätezeile im Reservestromkreis erstellen"
|
"Gerätezeile im Reservestromkreis erstellen"
|
||||||
);
|
);
|
||||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: session.cellKey };
|
||||||
return nextSelectionIntent();
|
return nextSelectionIntent();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1992,6 +2000,45 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGzfPickerCell(target: SelectedCell | null): target is SelectedCell {
|
||||||
|
return Boolean(
|
||||||
|
target &&
|
||||||
|
target.cellKey === "simultaneityFactor" &&
|
||||||
|
findCell(target.rowKey, target.cellKey)?.editable
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens the AMEV simultaneity-factor table for one GZF cell. A running
|
||||||
|
// inline edit is discarded because the picked value replaces it.
|
||||||
|
function openGzfPicker(target: SelectedCell) {
|
||||||
|
if (isSaving || historyBusy || !isGzfPickerCell(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEditingCell(null);
|
||||||
|
setSelectedCell(target);
|
||||||
|
setGzfPickerCell(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeGzfPicker() {
|
||||||
|
setGzfPickerCell(null);
|
||||||
|
focusGridWithoutScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commits the picked mean value like a typed edit (undoable command).
|
||||||
|
function handleGzfPicked(value: number) {
|
||||||
|
const target = gzfPickerCell;
|
||||||
|
setGzfPickerCell(null);
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// parseNumeric accepts only the German decimal comma.
|
||||||
|
const draft = value.toLocaleString("de-DE", {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
useGrouping: false,
|
||||||
|
});
|
||||||
|
void commitEdit("stay", draft, target);
|
||||||
|
}
|
||||||
|
|
||||||
// Cancels current edit session and restores grid keyboard focus.
|
// Cancels current edit session and restores grid keyboard focus.
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
if (!editingCell) {
|
if (!editingCell) {
|
||||||
|
|
@ -2935,6 +2982,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
// - printable key starts overwrite edit (type-to-edit)
|
// - printable key starts overwrite edit (type-to-edit)
|
||||||
// - Arrow/Tab move selectedCell when not editing
|
// - Arrow/Tab move selectedCell when not editing
|
||||||
function handleContainerKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
function handleContainerKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
||||||
|
// The picker modal sits outside the grid; keep grid keys from acting behind it.
|
||||||
|
if (gzfPickerCell) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeGzfPicker();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (editingCell) {
|
if (editingCell) {
|
||||||
if (event.key === "Tab") {
|
if (event.key === "Tab") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -3002,6 +3057,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
moveHorizontal(-1);
|
moveHorizontal(-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.altKey && event.key === "ArrowDown" && isGzfPickerCell(selectedCell)) {
|
||||||
|
event.preventDefault();
|
||||||
|
openGzfPicker(selectedCell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === "ArrowDown") {
|
if (event.key === "ArrowDown") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
moveVertical(1);
|
moveVertical(1);
|
||||||
|
|
@ -3169,6 +3229,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
onSave={handleSaveCircuitGroup}
|
onSave={handleSaveCircuitGroup}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{gzfPickerCell ? (
|
||||||
|
<GzfPickerModal onClose={closeGzfPicker} onSelect={handleGzfPicked} />
|
||||||
|
) : null}
|
||||||
{protectionEditorCircuit ? (
|
{protectionEditorCircuit ? (
|
||||||
<CircuitProtectionModal
|
<CircuitProtectionModal
|
||||||
equipmentIdentifier={
|
equipmentIdentifier={
|
||||||
|
|
@ -4487,11 +4550,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
} else if (event.key === "Tab") {
|
} else if (event.key === "Tab") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void commitEdit(event.shiftKey ? "prev" : "next");
|
void commitEdit(event.shiftKey ? "prev" : "next");
|
||||||
|
} else if (
|
||||||
|
event.altKey &&
|
||||||
|
event.key === "ArrowDown" &&
|
||||||
|
column.key === "simultaneityFactor"
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
openGzfPicker({ rowKey: row.rowKey, cellKey: column.key });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const active = document.activeElement as HTMLElement | null;
|
const active = document.activeElement as HTMLElement | null;
|
||||||
|
if (active?.closest('[role="dialog"][aria-modal="true"]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!active || !containerRef.current?.contains(active)) {
|
if (!active || !containerRef.current?.contains(active)) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
focusGridWithoutScroll();
|
focusGridWithoutScroll();
|
||||||
|
|
@ -4499,6 +4572,27 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : column.key === "simultaneityFactor" &&
|
||||||
|
cell.editable &&
|
||||||
|
isSelected ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
aria-label="Gleichzeitigkeitsfaktor nach AMEV wählen"
|
||||||
|
className="cell-picker-trigger"
|
||||||
|
disabled={isSaving || historyBusy}
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
openGzfPicker({ rowKey: row.rowKey, cellKey: column.key });
|
||||||
|
}}
|
||||||
|
onDoubleClick={(event) => event.stopPropagation()}
|
||||||
|
title="Gleichzeitigkeitsfaktor nach AMEV wählen (Alt+↓)"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<TableProperties aria-hidden="true" size={13} />
|
||||||
|
</button>
|
||||||
|
{formatCellValue(cell.value, column.key)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
formatCellValue(cell.value, column.key)
|
formatCellValue(cell.value, column.key)
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useId, useRef } from "react";
|
||||||
import { AMEV_GZF_TABLE } from "../../shared/constants/amev-gzf";
|
import { AMEV_GZF_TABLE } from "../../shared/constants/amev-gzf";
|
||||||
|
|
||||||
interface GzfPickerModalProps {
|
interface GzfPickerModalProps {
|
||||||
|
|
@ -12,14 +13,50 @@ function formatFactor(value: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GzfPickerModal({ onClose, onSelect }: GzfPickerModalProps) {
|
export function GzfPickerModal({ onClose, onSelect }: GzfPickerModalProps) {
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
const titleId = useId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Wait for the inline editor's blur callback before taking keyboard focus.
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
dialogRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div aria-modal="true" className="modal fade show d-block" role="dialog" tabIndex={-1}>
|
<div
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
aria-modal="true"
|
||||||
|
className="modal fade show d-block"
|
||||||
|
ref={dialogRef}
|
||||||
|
role="dialog"
|
||||||
|
tabIndex={-1}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
} else if (event.key === "Tab") {
|
||||||
|
const buttons = dialogRef.current?.querySelectorAll<HTMLButtonElement>("button:not(:disabled)");
|
||||||
|
const first = buttons?.[0];
|
||||||
|
const last = buttons?.[buttons.length - 1];
|
||||||
|
if (event.shiftKey && (document.activeElement === first || document.activeElement === dialogRef.current)) {
|
||||||
|
event.preventDefault();
|
||||||
|
last?.focus();
|
||||||
|
} else if (!event.shiftKey && document.activeElement === last) {
|
||||||
|
event.preventDefault();
|
||||||
|
first?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="modal-dialog modal-lg modal-dialog-centered">
|
<div className="modal-dialog modal-lg modal-dialog-centered">
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="modal-title fs-5">Gleichzeitigkeitsfaktor nach AMEV</h2>
|
<h2 className="modal-title fs-5" id={titleId}>Gleichzeitigkeitsfaktor nach AMEV</h2>
|
||||||
<p className="text-secondary small mb-0">
|
<p className="text-secondary small mb-0">
|
||||||
Tabelle 3 – Gleichzeitigkeitsfaktoren für Verbrauchergruppen
|
Tabelle 3 – Gleichzeitigkeitsfaktoren für Verbrauchergruppen
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue