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.
This commit is contained in:
Maurice
2026-10-09 15:01:55 +02:00
parent c8d8ddfd3e
commit 3fcfb12749
4 changed files with 63 additions and 27 deletions
@@ -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(
<details className="group">
<FoldSummary>
<span>More</span>
</FoldSummary>
<p>inside</p>
</details>
);
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(
<details className="group">
<FoldSummary>More</FoldSummary>
<p>inside</p>
</details>
);
const details = container.querySelector('details') as HTMLDetailsElement;
expect(details.open).toBe(false);
fireEvent.click(container.querySelector('summary') as HTMLElement);
expect(details.open).toBe(true);
});
});
+21
View File
@@ -0,0 +1,21 @@
import { ChevronRight } from 'lucide-react';
import React from 'react';
/**
* The clickable head of a native <details> fold on the admin settings page: a
* chevron that turns when the fold opens, then whatever the fold calls itself.
* The surrounding <details> needs the `group` class for the chevron to turn.
*/
export default function FoldSummary({ children }: { children: React.ReactNode }): React.ReactElement {
return (
<summary className="flex cursor-pointer list-none items-center gap-2.5 px-3.5 py-2.5 hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--text-primary)] [&::-webkit-details-marker]:hidden">
<ChevronRight
size={15}
strokeWidth={2}
className="flex-none text-content-faint transition-transform group-open:rotate-90"
aria-hidden="true"
/>
{children}
</summary>
);
}
+3 -13
View File
@@ -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 (
<details className="group overflow-hidden rounded-[12px] border border-edge-faint bg-surface-secondary">
<summary
className="flex cursor-pointer list-none items-center gap-2.5 px-3.5 py-2.5 hover:bg-surface-hover
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--text-primary)]
[&::-webkit-details-marker]:hidden"
>
<ChevronRight
size={15}
strokeWidth={2}
className="flex-none text-content-faint transition-transform group-open:rotate-90"
aria-hidden="true"
/>
<FoldSummary>
<span className="min-w-0 flex-1 truncate font-medium text-content" style={fs(13, 'body')}>{title}</span>
<StatusPill>{summary}</StatusPill>
</summary>
</FoldSummary>
<div className="divide-y divide-edge-faint border-t border-edge-faint bg-surface-card">{children}</div>
</details>
)
+4 -14
View File
@@ -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
</div>
<details className="group border-t border-edge-faint">
<summary
className="flex cursor-pointer list-none items-center gap-2.5 px-3.5 py-2.5
hover:bg-surface-hover focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[color:var(--text-primary)]
[&::-webkit-details-marker]:hidden"
>
<ChevronRight
size={15}
strokeWidth={2}
className="flex-none text-content-faint transition-transform group-open:rotate-90"
aria-hidden="true"
/>
<FoldSummary>
<span className="font-medium text-content" style={fs(13, 'body')}>{t('admin.trekApi.more')}</span>
</summary>
</FoldSummary>
<div className="divide-y divide-edge-faint border-t border-edge-faint bg-surface-secondary">
{/* 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