Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,15 @@ main {

/* Road incident card: what it is and where, when, what it does to
* traffic, the source's own words, and a quiet credit line. */
/* The card is the popup's only scroll area, reached only when an opened
* text or a stack of conditions outgrows the screen. */
.omx-incident {
width: 268px;
max-width: 100%;
font-size: 13px;
line-height: 1.4;
}
.omx-incident--stack {
max-height: 60vh;
overflow-y: auto;
font-size: 13px;
line-height: 1.4;
}
.omx-incident__count {
padding-right: 26px;
Expand Down Expand Up @@ -472,11 +472,36 @@ main {
}
.omx-incident__text {
margin-top: 10px;
max-height: 132px;
overflow-y: auto;
white-space: pre-line;
}
.omx-incident__body {
opacity: 0.85;
}
/* A long text shows its first four lines, fading out, until opened. */
.omx-incident__text--long:not(:has(.omx-incident__more[open])) .omx-incident__body {
max-height: calc(4 * 1.4em);
overflow: hidden;
mask-image: linear-gradient(to bottom, #000 55%, transparent);
}
.omx-incident__more summary {
display: inline-block;
margin-top: 2px;
font-weight: 600;
color: var(--omx-brand);
cursor: pointer;
list-style: none;
}
.omx-incident__more summary::-webkit-details-marker {
display: none;
}
.omx-incident__more summary:hover,
.omx-incident__more summary:focus-visible {
text-decoration: underline;
}
.omx-incident__more[open] .omx-incident__show,
.omx-incident__more:not([open]) .omx-incident__hide {
display: none;
}
.omx-incident__text p {
margin: 0;
}
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/features/map-layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ incident displays a localized card: what it is and the road and direction it
affects, how much it affects traffic, until when it holds (or when it starts,
including recurring schedule windows in the source's local timezone), what it
does to traffic (closed roads and lanes, vehicle limits, delays, detours), the
source's own description, and one credit line per source with when it was last
source's own description (a long one shows its first lines and opens in place with
"Show more"), and one credit line per source with when it was last
updated. Overlapping markers are automatically grouped into a single scrollable
popup (ordered most-severe first) so all incidents at the same spot stay reachable, and
linear events (such as segment closures) render as line segments directly on the map.
Expand Down
16 changes: 16 additions & 0 deletions integrations/road-conditions/__tests__/popup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ describe("incident card content", () => {
).not.toHaveProperty("headline");
});

it("prints a short text in full and opens a long one in place, never in a scroll area", () => {
const short = render([situation({ description: text("Erneuerung an der Decke") })]).html;
expect(short).toContain("Erneuerung an der Decke");
expect(short).not.toContain("omx-incident__more");

const long = "Angespannte Verkehrslage zwischen Duisburg-Beeck und Oberhausen-West. ".repeat(4);
const shown = card([situation({ headline: text("A42 | Duisburg"), description: text(long) })]);
expect(shown?.more).toEqual({ show: "panel.showMore", hide: "panel.showLess" });
const html = render([situation({ description: text(long) })]).html;
// The whole text is in the card; the disclosure only lifts its fade.
expect(html).toContain(long.trim());
expect(html).toContain('<details class="omx-incident__more"><summary>');
expect(html).toContain("panel.showMore");
expect(html).toContain("panel.showLess");
});

it("shows the headline, description and road names in the reader's language", () => {
const event = situation({
headline: [
Expand Down
43 changes: 34 additions & 9 deletions integrations/road-conditions/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export interface IncidentCard {
/** The source's own headline, where it says more than the title. */
headline?: string;
description?: string;
/** Present when the source's text runs longer than the card shows at first. */
more?: { show: string; hide: string };
warning?: string;
/** The captured credit of every source shown, as the map credits receive them. */
credits: Attribution[];
Expand Down Expand Up @@ -414,6 +416,10 @@ export function buildIncidentCard(
const formatUpdated = input.formatRelative ?? input.formatDateTime;
const sameText = (a?: string, b?: string) =>
Boolean(a && b && a.toLocaleLowerCase() === b.toLocaleLowerCase());
const shownHeadline =
headline && !sameText(headline, title) && !sameText(headline, description)
? headline
: undefined;
return {
severity,
glyph: markerGlyph(representative),
Expand All @@ -424,10 +430,11 @@ export function buildIncidentCard(
...(events.every(isUnconfirmedCrowd) ? { unconfirmed: translate("report.unconfirmed") } : {}),
when: whenText(groupValidity(events, representative), future, input),
impacts,
...(headline && !sameText(headline, title) && !sameText(headline, description)
? { headline }
: {}),
...(shownHeadline ? { headline: shownHeadline } : {}),
...(description ? { description } : {}),
...(estimatedLines([shownHeadline, description]) > TEXT_LINES_SHOWN + 1
? { more: { show: translate("panel.showMore"), hide: translate("panel.showLess") } }
: {}),
...(input.needsRefresh
? { warning: translate("freshness.needsRefresh") }
: expired
Expand All @@ -440,6 +447,18 @@ export function buildIncidentCard(
};
}

/** The lines of the source's text a card shows before "Show more". */
const TEXT_LINES_SHOWN = 4;
/** Roughly the characters of one line of card text. */
const CHARS_PER_LINE = 38;

/** About how many lines the texts take in a card, each paragraph and line break apart. */
function estimatedLines(texts: readonly (string | undefined)[]): number {
return texts
.flatMap((text) => (text ? text.split("\n") : []))
.reduce((lines, line) => lines + Math.max(1, Math.ceil(line.length / CHARS_PER_LINE)), 0);
}

function glyphSvg(glyph: string): string {
const path = GLYPHS[glyph] ?? GLYPHS.other;
return `<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path fill="currentColor" d="${escapeHtml(path ?? "")}"/></svg>`;
Expand Down Expand Up @@ -478,12 +497,18 @@ export function renderIncidentCard(card: IncidentCard, options: PopupCreditsOpti
.map((line) => `<li>${escapeHtml(line)}</li>`)
.join("")}</ul>`
: "";
const text =
card.headline || card.description
? `<div class="omx-incident__text">${
card.headline ? `<p class="omx-incident__headline">${escapeHtml(card.headline)}</p>` : ""
}${card.description ? `<p>${escapeHtml(card.description)}</p>` : ""}</div>`
: "";
const paragraphs = `${
card.headline ? `<p class="omx-incident__headline">${escapeHtml(card.headline)}</p>` : ""
}${card.description ? `<p>${escapeHtml(card.description)}</p>` : ""}`;
// A long text shows its first lines and opens in place, so the card never
// holds a second scroll area.
const text = !paragraphs
? ""
: card.more
? `<div class="omx-incident__text omx-incident__text--long"><div class="omx-incident__body">${paragraphs}</div><details class="omx-incident__more"><summary><span class="omx-incident__show">${escapeHtml(
card.more.show,
)}</span><span class="omx-incident__hide">${escapeHtml(card.more.hide)}</span></summary></details></div>`
: `<div class="omx-incident__text"><div class="omx-incident__body">${paragraphs}</div></div>`;
const warning = card.warning
? `<div class="omx-incident__warning" role="status">${escapeHtml(card.warning)}</div>`
: "";
Expand Down
4 changes: 3 additions & 1 deletion packages/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,9 @@
},
"panel": {
"conditionsHere": "{count, plural, one {# Meldung hier} other {# Meldungen hier}}",
"updated": "Aktualisiert {time}"
"updated": "Aktualisiert {time}",
"showMore": "Mehr anzeigen",
"showLess": "Weniger anzeigen"
},
"binding": {
"exact": "Exakt",
Expand Down
4 changes: 3 additions & 1 deletion packages/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,9 @@
},
"panel": {
"conditionsHere": "{count, plural, one {# condition here} other {# conditions here}}",
"updated": "Updated {time}"
"updated": "Updated {time}",
"showMore": "Show more",
"showLess": "Show less"
},
"binding": {
"exact": "Exact",
Expand Down
Loading