Fix: Keep ConfigPage tabs mounted to preserve form state

Previously, the tab content wrapper used 'key={tab}' which caused React to
unmount and remount the entire subtree when switching tabs. This destroyed
all component state, including unsaved form data and pending auto-saves.

Changes:
- Removed 'key={tab}' from the wrapper div
- All tab panels now render at page initialization
- Inactive tabs use CSS 'display: none' to hide without unmounting
- Tabs remain mounted throughout the page lifetime
- Users can now switch tabs without losing form input

Updated ConfigPage.test.tsx to reflect that inactive tabs remain in the DOM
(just hidden with CSS) rather than being removed entirely.

Documentation: Added 'Tab Panels' section to Web-Development.md
explaining the rule and rationale.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-22 21:21:36 +02:00
parent 0f261e31c2
commit 0bfa975222
4 changed files with 34 additions and 26 deletions

View File

@@ -36,7 +36,11 @@ const useStyles = makeStyles({
header: {
marginBottom: tokens.spacingVerticalL,
},
tabContent: {
tabPanel: {
display: "none",
},
tabPanelVisible: {
display: "block",
marginTop: tokens.spacingVerticalL,
animationName: "fadeInUp",
animationDuration: tokens.durationNormal,
@@ -94,16 +98,26 @@ export function ConfigPage(): React.JSX.Element {
<Tab value="regex">Regex Tester</Tab>
</TabList>
<div className={styles.tabContent} key={tab}>
{tab === "jails" && (
<JailsTab
initialJail={(location.state as { jail?: string } | null)?.jail}
/>
)}
{tab === "filters" && <FiltersTab />}
{tab === "actions" && <ActionsTab />}
{tab === "server" && <ServerTab />}
{tab === "regex" && <RegexTesterTab />}
<div className={tab === "jails" ? styles.tabPanelVisible : styles.tabPanel}>
<JailsTab
initialJail={(location.state as { jail?: string } | null)?.jail}
/>
</div>
<div className={tab === "filters" ? styles.tabPanelVisible : styles.tabPanel}>
<FiltersTab />
</div>
<div className={tab === "actions" ? styles.tabPanelVisible : styles.tabPanel}>
<ActionsTab />
</div>
<div className={tab === "server" ? styles.tabPanelVisible : styles.tabPanel}>
<ServerTab />
</div>
<div className={tab === "regex" ? styles.tabPanelVisible : styles.tabPanel}>
<RegexTesterTab />
</div>
</div>
);

View File

@@ -38,7 +38,8 @@ describe("ConfigPage", () => {
renderPage();
fireEvent.click(screen.getByRole("tab", { name: /filters/i }));
expect(screen.getByTestId("filters-tab")).toBeInTheDocument();
expect(screen.queryByTestId("jails-tab")).not.toBeInTheDocument();
// Jails tab remains mounted (not removed from DOM), just hidden with CSS
expect(screen.getByTestId("jails-tab")).toBeInTheDocument();
});
it("switches to Actions tab when Actions tab is clicked", () => {