From 3fcfb12749d12e2d3130357b8d33036b10516ec0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 15:01:55 +0200 Subject: [PATCH] refactor(admin): share the fold head of the Google options and the TREK API card Both folds on the settings page opened with the same summary line and turning chevron. FoldSummary now draws it and each fold passes its own label; the Google fold keeps its status pill. --- client/src/pages/admin/FoldSummary.test.tsx | 35 +++++++++++++++++++++ client/src/pages/admin/FoldSummary.tsx | 21 +++++++++++++ client/src/pages/admin/GoogleOptions.tsx | 16 ++-------- client/src/pages/admin/TrekApiCard.tsx | 18 +++-------- 4 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 client/src/pages/admin/FoldSummary.test.tsx create mode 100644 client/src/pages/admin/FoldSummary.tsx diff --git a/client/src/pages/admin/FoldSummary.test.tsx b/client/src/pages/admin/FoldSummary.test.tsx new file mode 100644 index 000000000..319f9be12 --- /dev/null +++ b/client/src/pages/admin/FoldSummary.test.tsx @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { fireEvent, render, screen } from '../../../tests/helpers/render'; +import FoldSummary from './FoldSummary'; + +describe('FoldSummary', () => { + it('heads the fold with a turning chevron and its own label', () => { + const { container } = render( +
+ + More + +

inside

+
+ ); + const summary = container.querySelector('summary') as HTMLElement; + expect(summary).toContainElement(screen.getByText('More')); + const chevron = summary.querySelector('svg') as SVGElement; + expect(chevron).toHaveAttribute('aria-hidden', 'true'); + expect(chevron.getAttribute('class')).toContain('group-open:rotate-90'); + expect(summary.firstElementChild).toBe(chevron); + }); + + it('opens and closes the fold it heads', () => { + const { container } = render( +
+ More +

inside

+
+ ); + const details = container.querySelector('details') as HTMLDetailsElement; + expect(details.open).toBe(false); + fireEvent.click(container.querySelector('summary') as HTMLElement); + expect(details.open).toBe(true); + }); +}); diff --git a/client/src/pages/admin/FoldSummary.tsx b/client/src/pages/admin/FoldSummary.tsx new file mode 100644 index 000000000..6a8c10f76 --- /dev/null +++ b/client/src/pages/admin/FoldSummary.tsx @@ -0,0 +1,21 @@ +import { ChevronRight } from 'lucide-react'; +import React from 'react'; + +/** + * The clickable head of a native
fold on the admin settings page: a + * chevron that turns when the fold opens, then whatever the fold calls itself. + * The surrounding
needs the `group` class for the chevron to turn. + */ +export default function FoldSummary({ children }: { children: React.ReactNode }): React.ReactElement { + return ( + + + ); +} diff --git a/client/src/pages/admin/GoogleOptions.tsx b/client/src/pages/admin/GoogleOptions.tsx index 9fd1b039b..359227d46 100644 --- a/client/src/pages/admin/GoogleOptions.tsx +++ b/client/src/pages/admin/GoogleOptions.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { ChevronRight } from 'lucide-react' import { fs } from '../../components/shared/DialogShell' import { StatusPill } from '../../components/Settings/settingsKit' +import FoldSummary from './FoldSummary' interface GoogleOptionsProps { /** "Google options" — the heading of the fold. */ @@ -27,20 +27,10 @@ interface GoogleOptionsProps { export default function GoogleOptions({ title, summary, children }: GoogleOptionsProps): React.ReactElement { return (
- - +
{children}
) diff --git a/client/src/pages/admin/TrekApiCard.tsx b/client/src/pages/admin/TrekApiCard.tsx index 35669e4f2..5722e2bac 100644 --- a/client/src/pages/admin/TrekApiCard.tsx +++ b/client/src/pages/admin/TrekApiCard.tsx @@ -1,8 +1,9 @@ import React from 'react' -import { Check, ChevronRight, Library, X } from 'lucide-react' +import { Check, Library, X } from 'lucide-react' import TrekMark from '../../components/shared/TrekMark' import { fs } from '../../components/shared/DialogShell' import { StatusPill } from '../../components/Settings/settingsKit' +import FoldSummary from './FoldSummary' import type { TranslationFn } from '../../types' import { TREK_API_SOURCES as SOURCES, trekApiFacts, trekApiFields } from '../../components/Admin/trekApiModel' @@ -72,20 +73,9 @@ export default function TrekApiCard({ t }: TrekApiCardProps): React.ReactElement
- - +
{/* The fields as chips rather than a paragraph. A list of what you get is something you scan, not something you read, and a sentence