mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user