mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(tours): description on its own line, fewer and whole badges in the tour list
The tour list row puts the description between the name and the badges. The badges keep the three that matter most (time, then a paddle tour's check bench and distance, or a hike's distance and climbing) and only those that fit whole, so a narrow sidebar shows two instead of cutting one off. A check bench without obstacle data shows no pill in the list or the day plan; the checklist still names the gap.
This commit is contained in:
@@ -86,9 +86,14 @@ describe('PAD-09 the check bench on a tour row', () => {
|
||||
expect(pill.getAttribute('aria-label')).toContain('A checklist is no safety or legal clearance.');
|
||||
});
|
||||
|
||||
it('PCD-002: missing obstacle data reads as such in the pill, never as a count', () => {
|
||||
it('PCD-002: an area without obstacle data shows no pill at all, never a count', () => {
|
||||
render(<TourDayRowPills tour={{ ...kayak, paddle_check: paddleBench({ data: 'none', open: 3 }) }} />);
|
||||
expect(screen.getByTestId('plan-tour-paddle-check')).toHaveTextContent('No data');
|
||||
expect(screen.queryByTestId('plan-tour-paddle-check')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('PCD-002b: data that could not be read still shows, without a count', () => {
|
||||
render(<TourDayRowPills tour={{ ...kayak, paddle_check: paddleBench({ data: 'unavailable', open: 3 }) }} />);
|
||||
expect(screen.getByTestId('plan-tour-paddle-check')).not.toHaveTextContent('3');
|
||||
});
|
||||
|
||||
it.each([false, true])('PCD-003: the day plan facts name the bench as one more fact (mobile=%s)', (mobile) => {
|
||||
@@ -111,3 +116,31 @@ describe('PAD-09 the check bench on a tour row', () => {
|
||||
expect(screen.queryByTestId('plan-tour-paddle-check')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the compact facts of a tour list row', () => {
|
||||
const testIds = () =>
|
||||
[...document.querySelectorAll('[data-testid^="plan-tour"], [data-testid="trip-plan-tour-time"]')].map((el) =>
|
||||
el.getAttribute('data-testid')
|
||||
);
|
||||
const timed = { ...tour, distance: 4.2, elevation_gain: 478, planned_duration_minutes: 50 };
|
||||
|
||||
it('PCD-005: a hike keeps time, distance and climbing, in that order', () => {
|
||||
render(<TourDayRowPills tour={timed} compact />);
|
||||
expect(testIds()).toEqual(['trip-plan-tour-time', 'plan-tour-distance', 'plan-tour-ascent']);
|
||||
});
|
||||
|
||||
it('PCD-006: a paddle tour puts its check bench second and leaves out the climbing', () => {
|
||||
render(<TourDayRowPills tour={{ ...timed, tour_type: 'kayak', paddle_check: paddleBench() }} compact />);
|
||||
expect(testIds()).toEqual(['trip-plan-tour-time', 'plan-tour-paddle-check', 'plan-tour-distance']);
|
||||
});
|
||||
|
||||
it('PCD-007: the day plan still shows every fact', () => {
|
||||
render(<TourDayRowPills tour={{ ...timed, tour_type: 'kayak', paddle_check: paddleBench() }} />);
|
||||
expect(testIds()).toEqual([
|
||||
'trip-plan-tour-time',
|
||||
'plan-tour-distance',
|
||||
'plan-tour-ascent',
|
||||
'plan-tour-paddle-check',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,18 +141,25 @@ export function TourDayRowPill({
|
||||
);
|
||||
}
|
||||
|
||||
/** A paddle tour's check bench as one fact (PAD-09), or null for a hike or a tour listed without one. */
|
||||
/**
|
||||
* A paddle tour's check bench as one fact (PAD-09), or null for a hike, a tour
|
||||
* listed without one, or an area without obstacle data: a row has no room for
|
||||
* a badge that says nothing, and the checklist itself still names the gap.
|
||||
*/
|
||||
function paddleCheckOf(tour: TourListItem, t: Parameters<typeof paddleCheckFact>[1]) {
|
||||
return tour.tour_type === 'kayak' && tour.paddle_check ? paddleCheckFact(tour.paddle_check, t) : null;
|
||||
const check = tour.tour_type === 'kayak' ? tour.paddle_check : null;
|
||||
return check && check.data !== 'none' ? paddleCheckFact(check, t) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tour's facts on its day-plan row, in the badges every other stop uses: the
|
||||
* time it takes (the planned total where there is one, otherwise the walking
|
||||
* time, the parts in the tooltip), how far, how much climbing and, for a paddle
|
||||
* tour, its check bench.
|
||||
* tour, its check bench. `compact` (the tour list) keeps the three that matter
|
||||
* most, in that order: the time, then a paddle tour's check bench and its
|
||||
* distance, or a hike's distance and climbing.
|
||||
*/
|
||||
export function TourDayRowPills({ tour }: { tour: TourListItem }) {
|
||||
export function TourDayRowPills({ tour, compact = false }: { tour: TourListItem; compact?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const unit = useSettingsStore((state) => state.settings.distance_unit) || 'metric';
|
||||
const times = tourPlannedTimes(tour);
|
||||
@@ -169,35 +176,37 @@ export function TourDayRowPills({ tour }: { tour: TourListItem }) {
|
||||
: null,
|
||||
].filter((part): part is string => part !== null);
|
||||
const check = paddleCheckOf(tour, t);
|
||||
return (
|
||||
<>
|
||||
{shownMinutes != null && (
|
||||
<TourDayRowPill
|
||||
icon={Clock}
|
||||
value={formatPlannedTourDuration(shownMinutes)}
|
||||
label={timeParts.join(', ')}
|
||||
testId="trip-plan-tour-time"
|
||||
/>
|
||||
)}
|
||||
{tour.distance != null && (
|
||||
<TourDayRowPill
|
||||
icon={Ruler}
|
||||
value={formatDistance(tour.distance, unit)}
|
||||
label={`${t('tours.detail.distance')}: ${formatDistance(tour.distance, unit)}`}
|
||||
testId="plan-tour-distance"
|
||||
/>
|
||||
)}
|
||||
{tour.elevation_gain != null && (
|
||||
<TourDayRowPill
|
||||
icon={ArrowUp}
|
||||
value={formatElevation(tour.elevation_gain, unit)}
|
||||
label={`${t('tours.detail.ascent')}: ${formatElevation(tour.elevation_gain, unit)}`}
|
||||
testId="plan-tour-ascent"
|
||||
/>
|
||||
)}
|
||||
{check && <TourDayRowPill {...check} testId="plan-tour-paddle-check" />}
|
||||
</>
|
||||
const time = shownMinutes != null && (
|
||||
<TourDayRowPill
|
||||
key="time"
|
||||
icon={Clock}
|
||||
value={formatPlannedTourDuration(shownMinutes)}
|
||||
label={timeParts.join(', ')}
|
||||
testId="trip-plan-tour-time"
|
||||
/>
|
||||
);
|
||||
const distance = tour.distance != null && (
|
||||
<TourDayRowPill
|
||||
key="distance"
|
||||
icon={Ruler}
|
||||
value={formatDistance(tour.distance, unit)}
|
||||
label={`${t('tours.detail.distance')}: ${formatDistance(tour.distance, unit)}`}
|
||||
testId="plan-tour-distance"
|
||||
/>
|
||||
);
|
||||
const ascent = tour.elevation_gain != null && (
|
||||
<TourDayRowPill
|
||||
key="ascent"
|
||||
icon={ArrowUp}
|
||||
value={formatElevation(tour.elevation_gain, unit)}
|
||||
label={`${t('tours.detail.ascent')}: ${formatElevation(tour.elevation_gain, unit)}`}
|
||||
testId="plan-tour-ascent"
|
||||
/>
|
||||
);
|
||||
const bench = check && <TourDayRowPill key="check" {...check} testId="plan-tour-paddle-check" />;
|
||||
if (!compact) return <>{[time, distance, ascent, bench]}</>;
|
||||
// A paddle tour's climbing is the river's fall, nothing to plan by; its check bench is.
|
||||
return <>{tour.tour_type === 'kayak' ? [time, bench, distance] : [time, distance, ascent]}</>;
|
||||
}
|
||||
|
||||
export default function TourDayRowFacts({ tour, mobile = false }: { tour: TourListItem; mobile?: boolean }) {
|
||||
|
||||
@@ -47,7 +47,7 @@ function MapToggleButton({ toggle }: { toggle: NonNullable<TourListRowProps['map
|
||||
)
|
||||
}
|
||||
|
||||
/** The tile, the name and one line of badges with the description trailing after them. */
|
||||
/** The tile, the name, the description on a line of its own and one line of badges. */
|
||||
function TourRowBody({ tour, nameId, tourDays, dimmed }: {
|
||||
tour: TourListItem
|
||||
nameId: string
|
||||
@@ -60,24 +60,24 @@ function TourRowBody({ tour, nameId, tourDays, dimmed }: {
|
||||
<div className="min-w-0 flex-1">
|
||||
<span id={nameId} className="block min-w-0 truncate font-semibold leading-tight text-content"
|
||||
style={fs(13, 'body')}>{tour.name}</span>
|
||||
{/* One line like a plan row's: the badges keep their place, the description gives way,
|
||||
and what does not fit fades out instead of ending at a hard edge. */}
|
||||
{tour.description && (
|
||||
<span
|
||||
data-testid="tour-description-preview"
|
||||
className="mt-0.5 block min-w-0 truncate text-content-faint"
|
||||
style={fs(10.5)}
|
||||
>
|
||||
{tour.description}
|
||||
</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. */}
|
||||
<div
|
||||
data-testid="tour-metrics"
|
||||
className="trek-fade-end mt-0.5 flex min-w-0 flex-nowrap items-center gap-1.5 overflow-hidden"
|
||||
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} />
|
||||
<TourDayRowPills tour={tour} />
|
||||
<TourDayRowPills tour={tour} compact />
|
||||
<TourDaysBadge tourDays={tourDays} />
|
||||
{tour.description && (
|
||||
<span
|
||||
data-testid="tour-description-preview"
|
||||
className="min-w-0 flex-1 truncate text-content-faint"
|
||||
style={fs(10.5)}
|
||||
>
|
||||
{tour.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,9 +108,9 @@ function TourRowActions({ action, mapToggle, selected }: Pick<TourListRowProps,
|
||||
|
||||
/**
|
||||
* A tour in a list, laid out like a stop in the day plan: its tile with the
|
||||
* grade, the name, and one line of the plan's white badges (time, distance,
|
||||
* ascent, the days it is on, a caution) with the description trailing after
|
||||
* them. The day and delete actions sit on the right.
|
||||
* grade, the name, the description below it, and one line of the plan's
|
||||
* white badges (time, distance, ascent, the days it is on, a caution). The day
|
||||
* and delete actions sit on the right.
|
||||
*/
|
||||
export default function TourListRow({ tour,
|
||||
days = NO_DAYS,
|
||||
|
||||
@@ -75,15 +75,18 @@ describe('ToursSidebar', () => {
|
||||
expect(row).toHaveClass('bg-surface-selected')
|
||||
})
|
||||
|
||||
it('RS-01: trails the description after the badges on one line, as a plan row does', () => {
|
||||
it('RS-01: puts the description on its own line between the name and the badges', () => {
|
||||
const listed = { ...tour, description: 'A quiet path above the lake', website: 'https://www.komoot.com/tour/42', planned_duration_minutes: 95 }
|
||||
render(<ToursSidebar tripId={1} days={[]} tours={[listed]} onAssignToDay={vi.fn()} onSelectTour={vi.fn()} />)
|
||||
const row = screen.getByRole('option', { name: /Selected ridge walk/i })
|
||||
const line = within(row).getByTestId('tour-metrics')
|
||||
expect(line).toHaveClass('flex-nowrap', 'overflow-hidden')
|
||||
expect(line).toHaveClass('flex-wrap', 'h-6', 'overflow-hidden')
|
||||
expect(line).toHaveTextContent('1 h 35 min')
|
||||
expect(within(line).getByTestId('tour-description-preview')).toHaveTextContent('A quiet path above the lake')
|
||||
expect(within(line).getByTestId('tour-description-preview')).toHaveClass('truncate')
|
||||
const description = within(row).getByTestId('tour-description-preview')
|
||||
expect(line).not.toContainElement(description)
|
||||
expect(description).toHaveTextContent('A quiet path above the lake')
|
||||
expect(description).toHaveClass('truncate')
|
||||
expect(description.compareDocumentPosition(line) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
// The information link belongs to the detail, not the list row.
|
||||
expect(row).not.toHaveTextContent('Komoot')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user