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
29 changes: 27 additions & 2 deletions docs/design/tpr-ui-kit.html
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,24 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0 24px;
font-size: 12px;
}
.ribbon .condensed.cols > span {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.ribbon .condensed.cols .n {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ribbon .condensed.cols .y {
flex-shrink: 0;
font-variant-numeric: tabular-nums;
color: rgb(255 255 255 / 0.8);
}
.ribbon .axes {
background: rgb(255 255 255 / 0.95);
Expand Down Expand Up @@ -4235,8 +4252,16 @@ <h4>Detail page ribbon — scrolled state</h4>
>
<div class="ribbon">
<div class="condensed cols">
<span>IEA NZE</span><span>ACE ATS</span
><span>NGFS Net Zero</span>
<span
><span class="n">IEA: NZE</span
><span class="y">2024</span></span
><span
><span class="n">IEA: NZE</span
><span class="y">2025</span></span
><span
><span class="n">NGFS: Net Zero 2050</span
><span class="y">2024</span></span
>
</div>
<div
class="axes"
Expand Down
79 changes: 63 additions & 16 deletions src/components/ComparisonScopeHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { PathwayMetadataType } from "../types";
const iea = {
id: "IEA-APS-2024",
name: { full: "Announced Pledges Scenario", short: "APS" },
publication: { publisher: { short: "IEA", full: "IEA Long" } },
publication: { publisher: { short: "IEA", full: "IEA Long" }, year: 2024 },
sectors: [{ name: "Power" }, { name: "Steel" }, { name: "Buildings" }],
geography: {
global: true,
Expand All @@ -31,7 +31,7 @@ const iea = {
const ace = {
id: "ACE-ATS-2024",
name: { full: "ASEAN Target Scenario", short: "ATS" },
publication: { publisher: { short: "ACE", full: "ACE Long" } },
publication: { publisher: { short: "ACE", full: "ACE Long" }, year: 2024 },
sectors: [{ name: "Power" }, { name: "Buildings" }],
geography: {
regions: { "South East Asia": ["ID", "TH", "VN"] },
Expand Down Expand Up @@ -156,20 +156,24 @@ describe("ComparisonScopeHeader", () => {
it("gives every column its own control, named for its pathway", () => {
renderHeader();

expect(columnTrigger("APS")).toBeInTheDocument();
expect(columnTrigger("ATS")).toBeInTheDocument();
expect(columnTrigger("IEA: APS (2024)")).toBeInTheDocument();
expect(columnTrigger("ACE: ATS (2024)")).toBeInTheDocument();
});

it("shows each column's own current geography", () => {
renderHeader();

expect(columnTrigger("APS")).toHaveTextContent("Southeast Asia");
expect(columnTrigger("ATS")).toHaveTextContent("South East Asia");
expect(columnTrigger("IEA: APS (2024)")).toHaveTextContent(
"Southeast Asia",
);
expect(columnTrigger("ACE: ATS (2024)")).toHaveTextContent(
"South East Asia",
);
});

it("offers only the geographies that column's publisher declares", async () => {
const { user } = renderHeader();
await user.click(columnTrigger("ATS"));
await user.click(columnTrigger("ACE: ATS (2024)"));

const names = screen
.getAllByRole("option")
Expand All @@ -183,7 +187,7 @@ describe("ComparisonScopeHeader", () => {
// The column carries the provenance, so "(IEA)" would be pure noise —
// and across the loadable pathways it disambiguates nothing anyway.
const { user } = renderHeader();
await user.click(columnTrigger("APS"));
await user.click(columnTrigger("IEA: APS (2024)"));

const names = screen
.getAllByRole("option")
Expand All @@ -194,7 +198,7 @@ describe("ComparisonScopeHeader", () => {

it("changes only the column that was used", async () => {
const { onGeographyChange, user } = renderHeader();
await user.click(columnTrigger("APS"));
await user.click(columnTrigger("IEA: APS (2024)"));
await user.click(screen.getByRole("option", { name: "Global" }));

expect(onGeographyChange).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -223,15 +227,15 @@ describe("ComparisonScopeHeader", () => {
selectedGeographies: { [iea.id]: "Global", bare: null },
});

expect(columnTrigger("BARE")).toBeDisabled();
expect(columnTrigger("ACE: BARE (2024)")).toBeDisabled();
});
});

describe("availability", () => {
it("shades the geographies this tool cannot plot", async () => {
// 13 of IEA's 15 declared geographies carry no timeseries rows.
const { user } = renderHeader({ available: ["Southeast Asia"] });
await user.click(columnTrigger("APS"));
await user.click(columnTrigger("IEA: APS (2024)"));

const region = screen.getByRole("option", { name: "Southeast Asia" });
expect(region.querySelector(".bg-transparent")).toBeNull();
Expand All @@ -242,7 +246,7 @@ describe("ComparisonScopeHeader", () => {

it("leads with what can be plotted", async () => {
const { user } = renderHeader({ available: ["Southeast Asia"] });
await user.click(columnTrigger("APS"));
await user.click(columnTrigger("IEA: APS (2024)"));

expect(screen.getAllByRole("option")[0]).toHaveAccessibleName(
"Southeast Asia",
Expand Down Expand Up @@ -302,24 +306,67 @@ describe("ComparisonScopeHeader", () => {
it("stays collapsed until the cards scroll away", () => {
renderHeader();

const bar = screen.getByText("APS").closest("div[aria-hidden='true']");
const bar = screen
.getByText("IEA: APS")
.closest("div[aria-hidden='true']");
expect(bar).toHaveClass("h-0");
});

it("names each column once the cards scroll away", () => {
renderHeader();
scrollPast();

const bar = screen.getByText("APS").closest("div[aria-hidden='true']");
const bar = screen
.getByText("IEA: APS")
.closest("div[aria-hidden='true']");
expect(bar).toHaveClass("h-8");
expect(screen.getByText("ATS")).toBeInTheDocument();
expect(screen.getByText("ACE: ATS")).toBeInTheDocument();
});

it("holds no focusables, so nothing hides in a collapsed bar", () => {
renderHeader();

const bar = screen.getByText("APS").closest("div[aria-hidden='true']");
const bar = screen
.getByText("IEA: APS")
.closest("div[aria-hidden='true']");
expect(bar?.querySelectorAll("button, a, input")).toHaveLength(0);
});

it("tells apart columns that share a short pathway name", () => {
// Two editions of one pathway, and another publisher's same short name.
const nze2024 = {
...iea,
id: "IEA-NZE-2024",
name: { full: "Net Zero Emissions by 2050", short: "NZE" },
} as unknown as PathwayMetadataType;
const nze2025: PathwayMetadataType = {
...nze2024,
id: "IEA-NZE-2025",
publication: { ...nze2024.publication, year: 2025 },
};
const otherNze = {
...ace,
id: "ACE-NZE-2024",
name: { full: "Net Zero", short: "NZE" },
} as unknown as PathwayMetadataType;

renderHeader({
pathways: [nze2024, nze2025, otherNze],
selectedGeographies: {},
});
scrollPast();

const bar = screen
.getByText("ACE: NZE")
.closest("div[aria-hidden='true']") as HTMLElement;
expect(within(bar).getAllByText("IEA: NZE")).toHaveLength(2);
expect(within(bar).getByText("2025")).toBeInTheDocument();
expect(within(bar).getAllByText("2024")).toHaveLength(2);

// The geography selects are named apart too, year included.
expect(columnTrigger("IEA: NZE (2024)")).toBeInTheDocument();
expect(columnTrigger("IEA: NZE (2025)")).toBeInTheDocument();
expect(columnTrigger("ACE: NZE (2024)")).toBeInTheDocument();
});
});
});
30 changes: 23 additions & 7 deletions src/components/ComparisonScopeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,17 @@ interface ComparisonScopeHeaderProps {
divergence: ColumnGeographyDivergence;
}

/**
* "Publisher: Pathway", so two columns with the same short pathway name (two
* editions of IEA NZE, or two publishers' "Reference") stay distinguishable.
* Both the condensed bar and the geography selects' accessible names add the
* publication year, which separates two editions of the same pathway.
* `||` rather than `??`: some publishers have an empty short name.
*/
const columnLabel = (pathway: PathwayMetadataType): string =>
pathway.name.short || pathway.name.full;
`${pathway.publication.publisher.short || pathway.publication.publisher.full}: ${
pathway.name.short || pathway.name.full
}`;

/**
* The comparison page's sticky scope bar: one shared sector axis, one geography
Expand Down Expand Up @@ -83,9 +92,11 @@ export const ComparisonScopeHeader: React.FC<ComparisonScopeHeaderProps> = ({

<div className="sticky top-(--env-banner-height) z-20">
{/*
Condensed column names. aria-hidden because it restates the pathway
cards above, which stay in the accessibility tree whether or not they
are on screen — and it holds no focusables, so nothing is lost.
Condensed column names, with the publication year on the right; the
year never truncates, so the name gives way first. aria-hidden
because it restates the pathway cards above, which stay in the
accessibility tree whether or not they are on screen — and it holds
no focusables, so nothing is lost.
*/}
<div
aria-hidden="true"
Expand All @@ -100,9 +111,14 @@ export const ComparisonScopeHeader: React.FC<ComparisonScopeHeaderProps> = ({
{pathways.map((pathway) => (
<span
key={pathway.id}
className="text-xs font-semibold truncate"
className="flex items-baseline justify-between gap-2 min-w-0 text-xs"
>
{columnLabel(pathway)}
<span className="font-semibold truncate">
{columnLabel(pathway)}
</span>
<span className="shrink-0 tabular-nums text-white/80">
{pathway.publication.year}
</span>
</span>
))}
</div>
Expand Down Expand Up @@ -158,7 +174,7 @@ export const ComparisonScopeHeader: React.FC<ComparisonScopeHeaderProps> = ({
{pathways.map((pathway) => (
<ColumnGeographySelect
key={pathway.id}
pathwayName={columnLabel(pathway)}
pathwayName={`${columnLabel(pathway)} (${pathway.publication.year})`}
options={geographyOptions[pathway.id] ?? []}
selected={selectedGeographies[pathway.id] ?? null}
onSelect={(token) => onGeographyChange(pathway.id, token)}
Expand Down
18 changes: 9 additions & 9 deletions src/pages/ComparisonPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ describe("ComparisonPage — shared scope", () => {

await screen.findByText("PubA: Comparison Pathway A", undefined, WAIT);

expect(columnTrigger("A")).toHaveTextContent("Europe");
expect(columnTrigger("B")).toHaveTextContent("Germany");
expect(columnTrigger("PubA: A (2024)")).toHaveTextContent("Europe");
expect(columnTrigger("PubB: B (2023)")).toHaveTextContent("Germany");
expect(
screen
.getAllByRole("button")
Expand All @@ -301,8 +301,8 @@ describe("ComparisonPage — shared scope", () => {

await screen.findByText("PubA: Comparison Pathway A", undefined, WAIT);

expect(columnTrigger("A")).toHaveTextContent("Global");
expect(columnTrigger("B")).toHaveTextContent("Germany");
expect(columnTrigger("PubA: A (2024)")).toHaveTextContent("Global");
expect(columnTrigger("PubB: B (2023)")).toHaveTextContent("Germany");
});

it("ignores a token the column does not declare", async () => {
Expand All @@ -314,8 +314,8 @@ describe("ComparisonPage — shared scope", () => {

await screen.findByText("PubA: Comparison Pathway A", undefined, WAIT);

expect(columnTrigger("A")).toHaveTextContent("Global");
expect(columnTrigger("B")).toHaveTextContent("Germany");
expect(columnTrigger("PubA: A (2024)")).toHaveTextContent("Global");
expect(columnTrigger("PubB: B (2023)")).toHaveTextContent("Germany");
});

it("warns that the columns are not showing the same geography", async () => {
Expand Down Expand Up @@ -353,11 +353,11 @@ describe("ComparisonPage — shared scope", () => {
await screen.findByText("PubA: Comparison Pathway A", undefined, WAIT);

const user = userEvent.setup();
await user.click(columnTrigger("A"));
await user.click(columnTrigger("PubA: A (2024)"));
await user.click(screen.getByRole("option", { name: "Europe" }));

expect(columnTrigger("A")).toHaveTextContent("Europe");
expect(columnTrigger("B")).toHaveTextContent("Germany");
expect(columnTrigger("PubA: A (2024)")).toHaveTextContent("Europe");
expect(columnTrigger("PubB: B (2023)")).toHaveTextContent("Germany");
});
});

Expand Down
Loading