mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
fix(tours): keep a multi-day tour's days in sight in the list row
The stage count sat in the row's fixed-height line of badges, so a tour with stages and a recorded track wrapped and lost its days badge. The count now sits on the fold button, a moon with the number at its corner, or beside the row where it does not unfold, and the days come before the measures so the ascent is what gets cut.
This commit is contained in:
@@ -7,9 +7,10 @@ import { FoldButton, TourActivityBadge, TourCautionBadge, TourDaysBadge, TourGra
|
||||
import type { Day } from '../../types'
|
||||
import { endPlaceDrag, startPlaceDrag } from '../Map/markerDrag'
|
||||
import { TourDayRowPills } from './TourDayRowFacts'
|
||||
import { TourStagesBadge } from './stages/StageParts'
|
||||
import { StageCountCorner, TourStagesBadge } from './stages/StageParts'
|
||||
import { useTourDays } from './tourDays'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
const NO_DAYS: Day[] = []
|
||||
|
||||
@@ -73,16 +74,16 @@ function TourRowBody({ tour, nameId, tourDays, dimmed }: {
|
||||
</span>
|
||||
)}
|
||||
{/* One line of whole badges, most important first: one that does not fit wraps onto a
|
||||
second line the fixed height hides, so a narrow sidebar shows two and never half of one. */}
|
||||
second line the fixed height hides, so a narrow sidebar shows two and never half of one.
|
||||
The days come before the measures, so the ascent is what goes, never the days. */}
|
||||
<div
|
||||
data-testid="tour-metrics"
|
||||
className="-mx-0.5 mt-0.5 flex h-6 min-w-0 flex-wrap content-start gap-x-1.5 gap-y-2 overflow-hidden px-0.5 pt-0.5"
|
||||
>
|
||||
<TourCautionBadge tour={tour} />
|
||||
<TourActivityBadge tour={tour} />
|
||||
<TourStagesBadge tour={tour} />
|
||||
<TourDayRowPills tour={tour} compact />
|
||||
<TourDaysBadge tourDays={tourDays} />
|
||||
<TourDayRowPills tour={tour} compact />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,6 +112,24 @@ function TourRowActions({ action, mapToggle, selected }: Pick<TourListRowProps,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* MDT-02: how many stages the tour has, always in sight beside the row: on
|
||||
* the fold button where the row unfolds them, as a mark of its own otherwise.
|
||||
*/
|
||||
function TourRowStages({ tour, fold }: Pick<TourListRowProps, 'tour' | 'fold'>) {
|
||||
const { t } = useTranslation()
|
||||
if (!fold) return <TourStagesBadge tour={tour} />
|
||||
const count = tour.stage_count ?? 0
|
||||
return (
|
||||
// Always in sight, unlike the hover actions: the stages are part of what the row is.
|
||||
<span role="presentation" className="flex-none" onClick={event => event.stopPropagation()}>
|
||||
<FoldButton expanded={fold.expanded} onToggle={fold.onToggle} controls={fold.controls} label={fold.label}
|
||||
corner={count > 0 ? <StageCountCorner count={count} /> : undefined}
|
||||
hint={count > 0 ? t('tours.stages.count', { count }) : undefined} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A tour in a list, laid out like a stop in the day plan: its tile with the
|
||||
* grade, the name, the description below it, and one line of the plan's
|
||||
@@ -154,12 +173,7 @@ export default function TourListRow({ tour,
|
||||
)}
|
||||
<TourRowBody tour={tour} nameId={nameId} tourDays={tourDays} dimmed={!!mapToggle?.hidden} />
|
||||
<TourRowActions action={action} mapToggle={mapToggle} selected={selected} />
|
||||
{fold && (
|
||||
// Always in sight, unlike the hover actions: the stages are part of what the row is.
|
||||
<span role="presentation" className="flex-none" onClick={event => event.stopPropagation()}>
|
||||
<FoldButton expanded={fold.expanded} onToggle={fold.onToggle} controls={fold.controls} label={fold.label} />
|
||||
</span>
|
||||
)}
|
||||
<TourRowStages tour={tour} fold={fold} />
|
||||
</div>
|
||||
{children}
|
||||
</li>
|
||||
|
||||
@@ -538,6 +538,24 @@ describe('ToursSidebar', () => {
|
||||
expect(badge).toHaveAttribute('aria-label', 'Day 1, Day 3: Summit')
|
||||
})
|
||||
|
||||
it('MDT-02: the days come before the measures in the badge line, and a multi-day tour counts its stages beside the row', () => {
|
||||
const days = [{ id: 7, trip_id: 1, day_number: 1, title: null, date: null }] as never[]
|
||||
seedStore(useTripStore, { assignments: { '7': [{ id: 1, place_id: 42 }] } as never })
|
||||
render(<ToursSidebar tripId={1} days={days} tours={[{ ...tour, stage_count: 3 }]} onAssignToDay={vi.fn()} onSelectTour={vi.fn()} />)
|
||||
|
||||
const row = screen.getByRole('option', { name: /Selected ridge walk/ })
|
||||
const metrics = within(row).getByTestId('tour-metrics')
|
||||
const daysBadge = within(metrics).getByTestId('tour-days-badge')
|
||||
const measures = [...metrics.querySelectorAll('[aria-label]')].filter(element => !daysBadge.contains(element))
|
||||
expect(measures.length).toBeGreaterThan(0)
|
||||
for (const measure of measures) {
|
||||
expect(daysBadge.compareDocumentPosition(measure) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
}
|
||||
expect(metrics).not.toHaveTextContent(/stages/)
|
||||
expect(within(row).getByRole('img', { name: '3 stages' })).toBeInTheDocument()
|
||||
expect(metrics).not.toContainElement(within(row).getByTestId('tour-stages-badge'))
|
||||
})
|
||||
|
||||
it('PR1-13: offers the source and routing limit only where the list varies, and narrows by them', () => {
|
||||
render(<ToursSidebar tripId={1} days={[]} tours={[
|
||||
{ ...tour, place_id: 1, name: 'Drawn', origin: 'trek', max_hiking_difficulty: 2 },
|
||||
|
||||
@@ -137,6 +137,12 @@ describe('TourPlannerToursRail', () => {
|
||||
const hutToHut = { ...tour, place_id: 43, name: 'Hut to hut', stage_count: 2 }
|
||||
render(<TourPlannerToursRail planner={planner({ mode: { type: 'new-draft' } })} tours={[tour, hutToHut]} {...railProps} />)
|
||||
expect(screen.getAllByRole('button', { name: 'Show stages' })).toHaveLength(1)
|
||||
// The count sits on the fold button, always in sight, never in the row's line of badges.
|
||||
const unfold = screen.getByRole('button', { name: 'Show stages' })
|
||||
expect(unfold).toHaveAccessibleDescription('2 stages')
|
||||
expect(within(unfold).getByTestId('tour-stages-count')).toHaveTextContent('2')
|
||||
const row = screen.getByRole('option', { name: /Hut to hut/ })
|
||||
expect(within(within(row).getByTestId('tour-metrics')).queryByText(/stages/)).toBeNull()
|
||||
expect(screen.queryByTestId('tour-stage-overview')).toBeNull()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Show stages' }))
|
||||
expect(screen.getByTestId('tour-stage-overview')).toHaveAttribute('id', 'tour-stages-43')
|
||||
|
||||
@@ -199,15 +199,46 @@ export function stageMenuItems(stages: TourStagesController, point: { id: string
|
||||
return items;
|
||||
}
|
||||
|
||||
/** MDT-01: a multi-day tour in a list says how many stages it has, with the moon of its nights. */
|
||||
export function TourStagesBadge({ tour }: { tour: Pick<TourListItem, 'stage_count'> }) {
|
||||
const { t } = useTranslation();
|
||||
if (!tour.stage_count) return null;
|
||||
/**
|
||||
* MDT-02: how many stages a multi-day tour has, as a small moon with the
|
||||
* number beside it, in the corner of what it sits on. Its words are said by
|
||||
* whatever carries it.
|
||||
*/
|
||||
export function StageCountCorner({ count }: { count: number }) {
|
||||
return (
|
||||
<span data-testid="tour-stages-badge" className="inline-flex">
|
||||
<WhiteBadge icon={<Moon size={10} strokeWidth={2.2} className="flex-none text-content-faint" />}>
|
||||
{t('tours.stages.count', { count: tour.stage_count })}
|
||||
</WhiteBadge>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-testid="tour-stages-count"
|
||||
className="pointer-events-none absolute -end-1.5 -top-1 inline-flex h-3.5 items-center gap-px rounded-full bg-accent px-1 font-geist font-semibold tabular-nums leading-none text-accent-text"
|
||||
style={fs(9)}
|
||||
>
|
||||
<Moon size={7} strokeWidth={2.6} />
|
||||
{count}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MDT-01: a multi-day tour in a list says how many stages it has: a moon with
|
||||
* the count at its corner, beside the row rather than in its line of badges,
|
||||
* so it is always in sight and never pushes the days it is on out of it. A row
|
||||
* that unfolds its stages carries the count on its fold button instead.
|
||||
*/
|
||||
export function TourStagesBadge({ tour }: { tour: Pick<TourListItem, 'stage_count'> }) {
|
||||
const { t } = useTranslation();
|
||||
if (!tour.stage_count) return null;
|
||||
const label = t('tours.stages.count', { count: tour.stage_count });
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<span
|
||||
data-testid="tour-stages-badge"
|
||||
role="img"
|
||||
aria-label={label}
|
||||
className="relative grid h-6 w-6 flex-none place-items-center text-content-faint"
|
||||
>
|
||||
<Moon size={13} strokeWidth={2.2} aria-hidden="true" />
|
||||
<StageCountCorner count={tour.stage_count} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ describe('stage views', () => {
|
||||
|
||||
it('FE-TOUR-STAGE-VIEW-003: a multi-day tour in a list says how many stages it has; a day tour says nothing', () => {
|
||||
const { rerender } = render(<TourStagesBadge tour={{ stage_count: 3 }} />);
|
||||
expect(screen.getByTestId('tour-stages-badge')).toHaveTextContent('3 stages');
|
||||
expect(screen.getByRole('img', { name: '3 stages' })).toBe(screen.getByTestId('tour-stages-badge'));
|
||||
expect(screen.getByTestId('tour-stages-count')).toHaveTextContent('3');
|
||||
rerender(<TourStagesBadge tour={{ stage_count: null }} />);
|
||||
expect(screen.queryByTestId('tour-stages-badge')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -274,12 +274,28 @@ export function TourNotice({ tone = 'neutral', icon, children, role }: { tone?:
|
||||
}
|
||||
|
||||
/** The chevron that folds a section away, quiet like the rest of a panel's controls. */
|
||||
export function FoldButton({ expanded, onToggle, controls, label }: { expanded: boolean; onToggle: () => void; controls: string; label: string }) {
|
||||
/**
|
||||
* A chevron that folds what `controls` names. `corner` sits at its top corner
|
||||
* (a count of what unfolds), and `hint` says the same in words: in the tooltip
|
||||
* after the label, and as the button's description for assistive tech.
|
||||
*/
|
||||
export function FoldButton({ expanded, onToggle, controls, label, corner, hint }: {
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
controls: string
|
||||
label: string
|
||||
corner?: ReactNode
|
||||
hint?: string
|
||||
}) {
|
||||
const hintId = useId()
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<Tooltip label={hint ? `${label}: ${hint}` : label}>
|
||||
<button type="button" onClick={onToggle} aria-expanded={expanded} aria-controls={controls} aria-label={label}
|
||||
className="grid h-6 w-6 place-items-center rounded-full text-content-faint transition-colors hover:bg-surface-card hover:text-content focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent">
|
||||
aria-describedby={hint ? hintId : undefined}
|
||||
className="relative grid h-6 w-6 place-items-center rounded-full text-content-faint transition-colors hover:bg-surface-card hover:text-content focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent">
|
||||
<ChevronDown size={14} strokeWidth={2.2} className={`transition-transform ${expanded ? '' : '-rotate-90'}`} />
|
||||
{corner}
|
||||
{hint && <span id={hintId} className="sr-only">{hint}</span>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user