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
43 changes: 20 additions & 23 deletions frontend/e2e/header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ async function boxOf(locator: ReturnType<Page['locator']>) {
return box
}

// Whether every part shares the logo's row.
async function expectOneRow(page: Page) {
const { home, report, theme } = header(page)
const homeBox = await boxOf(home)
for (const box of [await boxOf(report), await boxOf(theme)]) {
expect(box.y).toBeLessThan(homeBox.y + homeBox.height)
expect(box.y + box.height).toBeGreaterThan(homeBox.y)
}
}

// The layout comes from CSS media queries, so it is checked in a real
// browser at the widths where it changes.
for (const colorScheme of ['light', 'dark'] as const) {
test(`puts the report link and theme switch on one row at 320 px (${colorScheme})`, async ({
test(`fits the header on one row at 320 px (${colorScheme})`, async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 640 })
Expand All @@ -28,23 +38,15 @@ for (const colorScheme of ['light', 'dark'] as const) {
await page.goto('./')
const { home, report, theme } = header(page)

const homeBox = await boxOf(home)
const reportBox = await boxOf(report)
const themeBox = await boxOf(theme)

// The logo has the first row, the link and the switch share the second
// at the same height, and the rows line up at the left edge.
expect(reportBox.y).toBeGreaterThan(homeBox.y + homeBox.height)
expect(reportBox.y).toBe(themeBox.y)
expect(reportBox.height).toBe(themeBox.height)
expect(reportBox.x).toBe(homeBox.x)
expect(reportBox.x + reportBox.width).toBeLessThan(themeBox.x)
expect(themeBox.x + themeBox.width).toBeLessThanOrEqual(320)
await expectOneRow(page)
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(320)
// Only "Report" shows, but the name is still the whole action.
await expect(report).toHaveAccessibleName('Report a sighting')
const hidden = await boxOf(report.locator('.banner__submit-more'))
expect(hidden.width).toBeLessThanOrEqual(1)

// The Tab order follows the rows.
await home.focus()
await page.keyboard.press('Tab')
await expect(report).toBeFocused()
Expand All @@ -53,18 +55,13 @@ for (const colorScheme of ['light', 'dark'] as const) {
})
}

// Just above the two-row layout, the smaller logo keeps the header from
// wrapping on its own.
for (const width of [481, 520]) {
// 480 px is the widest with the short link, and 481 to 520 px the range
// where the smaller logo keeps the full link from wrapping.
for (const width of [375, 480, 481, 520]) {
test(`keeps the header on one row at ${width} px`, async ({ page }) => {
await page.setViewportSize({ width, height: 640 })
await mockGraphql(page)
await page.goto('./')
const { home, report, theme } = header(page)

const homeBox = await boxOf(home)
for (const box of [await boxOf(report), await boxOf(theme)]) {
expect(box.y).toBeLessThan(homeBox.y + homeBox.height)
}
await expectOneRow(page)
})
}
95 changes: 56 additions & 39 deletions frontend/src/components/Banner.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@
}

/* The full-size logo, the link and the switch need about 490 px, so the
smaller logo starts above the 480 px where the header wraps. Otherwise
it would wrap on its own, with the switch between the two rows. */
smaller logo starts above the 480 px where the link is shortened.
Otherwise the header would wrap on its own. */
@media (max-width: 520px) {
.banner__title {
gap: 8px;
Expand All @@ -111,41 +111,10 @@
}
}

/* Too narrow for one row, so the logo gets the first row, and the link
and the switch share the second, in the same order as the Tab order.
The link fills the space next to the switch, so both rows line up with
the page's edges, and is as tall as the switch. */
@media (max-width: 480px) {
.banner__content {
flex-wrap: nowrap;
align-items: flex-end;
gap: 12px;
}

.banner__nav {
flex-direction: column;
align-items: flex-start;
gap: 12px;
min-width: 0;
}

.banner__submit {
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
min-height: 42px;
box-sizing: border-box;
}

.banner__theme {
flex: 0 0 auto;
}
}

/* A pill with a moon on the left and a sun on the right. The thumb slides
behind the icon of the mode in use. */
.banner__theme {
--theme-icon: 34px;
position: relative;
display: inline-flex;
border: 1px solid var(--border);
Expand All @@ -165,23 +134,23 @@
position: absolute;
top: 3px;
left: 3px;
width: 34px;
height: 34px;
width: var(--theme-icon);
height: var(--theme-icon);
border-radius: 11px;
background: var(--accent);
box-shadow: var(--shadow);
transition: transform 0.2s;
}

.banner__theme[aria-checked='false'] .banner__theme-thumb {
transform: translateX(34px);
transform: translateX(var(--theme-icon));
}

.banner__theme-icon {
/* Above the thumb, which comes first in the markup. */
position: relative;
width: 34px;
height: 34px;
width: var(--theme-icon);
height: var(--theme-icon);
padding: 8px;
box-sizing: border-box;
fill: none;
Expand Down Expand Up @@ -216,3 +185,51 @@
transition: none;
}
}

/* At phone widths the full header needs about 450 px, so it would wrap
onto two rows. Shortening the link to "Report" and making the logo and
the switch a little smaller fits it on one row from 320 px. The rest of
the link's text stays for screen readers, so its name is still "Report
a sighting" and still starts with the word that shows. The header can
still wrap, so a wider font or larger text never pushes it off screen. */
@media (max-width: 480px) {
.banner__content,
.banner__nav {
gap: 8px;
}

.banner__title {
gap: 6px;
font-size: 1.15rem;
}

.banner__logo {
width: 30px;
height: 27px;
}

.banner__submit {
padding: 8px 10px;
}

.banner__submit-more {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}

/* 28 px stays above the 24 px minimum for touch targets. */
.banner__theme {
--theme-icon: 28px;
}

.banner__theme-icon {
padding: 6px;
}
}
5 changes: 4 additions & 1 deletion frontend/src/components/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ function Banner() {
<span className="banner__ufo">UFO</span>und it
</span>
</Link>
{/* Phones show only "Report" (Banner.css), but screen readers
always read the whole name. The space is outside the hidden
part, since some name calculations drop it from there. */}
<Link className="banner__submit" to="/submit">
Report a sighting
Report <span className="banner__submit-more">a sighting</span>
</Link>
</nav>
{/* A switch, so screen readers say "Dark mode, switch, on" while
Expand Down