diff --git a/client/src/components/Tours/TourListRow.tsx b/client/src/components/Tours/TourListRow.tsx
index 89f0de651..a8c60af48 100644
--- a/client/src/components/Tours/TourListRow.tsx
+++ b/client/src/components/Tours/TourListRow.tsx
@@ -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 }: {
)}
{/* 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. */}
-
-
+
@@ -111,6 +112,24 @@ function TourRowActions({ action, mapToggle, selected }: Pick) {
+ const { t } = useTranslation()
+ if (!fold) return
+ const count = tour.stage_count ?? 0
+ return (
+ // Always in sight, unlike the hover actions: the stages are part of what the row is.
+ event.stopPropagation()}>
+ 0 ? : undefined}
+ hint={count > 0 ? t('tours.stages.count', { count }) : undefined} />
+
+ )
+}
+
/**
* 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,
)}
- {fold && (
- // Always in sight, unlike the hover actions: the stages are part of what the row is.
- event.stopPropagation()}>
-
-
- )}
+
{children}
diff --git a/client/src/components/Tours/ToursSidebar.test.tsx b/client/src/components/Tours/ToursSidebar.test.tsx
index e99fed879..e0da3372b 100644
--- a/client/src/components/Tours/ToursSidebar.test.tsx
+++ b/client/src/components/Tours/ToursSidebar.test.tsx
@@ -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()
+
+ 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( {
const hutToHut = { ...tour, place_id: 43, name: 'Hut to hut', stage_count: 2 }
render()
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')
diff --git a/client/src/components/Tours/stages/StageParts.tsx b/client/src/components/Tours/stages/StageParts.tsx
index 2d96242f9..74397bce4 100644
--- a/client/src/components/Tours/stages/StageParts.tsx
+++ b/client/src/components/Tours/stages/StageParts.tsx
@@ -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 }) {
- 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 (
-
- }>
- {t('tours.stages.count', { count: tour.stage_count })}
-
+
+
+ {count}
);
}
+
+/**
+ * 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 }) {
+ const { t } = useTranslation();
+ if (!tour.stage_count) return null;
+ const label = t('tours.stages.count', { count: tour.stage_count });
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/Tours/stages/stageViews.test.tsx b/client/src/components/Tours/stages/stageViews.test.tsx
index 7516bebae..f36873fe9 100644
--- a/client/src/components/Tours/stages/stageViews.test.tsx
+++ b/client/src/components/Tours/stages/stageViews.test.tsx
@@ -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();
- 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();
expect(screen.queryByTestId('tour-stages-badge')).toBeNull();
});
diff --git a/client/src/components/Tours/tourParts.tsx b/client/src/components/Tours/tourParts.tsx
index 4d412d0e7..9d1006883 100644
--- a/client/src/components/Tours/tourParts.tsx
+++ b/client/src/components/Tours/tourParts.tsx
@@ -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 (
-
+
)