Skip to content

feat: one-row phone header with a Meny, new-tab links and a one-line strip - #69

Merged
malinfossum merged 8 commits into
mainfrom
feat/phone-header
Oct 12, 2026
Merged

malinfossum merged 8 commits into
mainfrom
feat/phone-header

Conversation

@malinfossum

@malinfossum malinfossum commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

What changed

#63 gave the phone a header of three rows (brand, pickers, links) and pinned the quick exit to the top right corner, with 10rem of room reserved for it in the header and the acute strip. At 320 px that was too tall, and the reserved room squeezed the strip to one number per row. This replaces it.

  • One sticky row at every width. Brand, Meny and "Forlat siden" sit in one row. From 1024 px the links and pickers are back in the row and the Meny is hidden. Nothing uses position: fixed any more, and the 10rem of reserved room is gone.
  • The Meny is a native <details> with three groups: the links (Alle tjenester, Helsenorge, NAV), Språk and Tema. The Språk and Tema groups carry visible labels, and every row shares one text edge and one check edge. Choosing a link closes it and puts focus back on the trigger. A theme row keeps focus and announces "Tema: Mørkt". It sits in its own "Hovedmeny" navigation, and only one of the two navigations is shown at any width, so a screen reader finds one landmark.
  • One source, two layouts. The links, the language rows and the theme rows are shared components, so the header and the Meny cannot drift apart.
  • Helsenorge and NAV open in a new tab, with rel="noopener noreferrer" and hidden text "(ekstern side, åpnes i ny fane)" / "(external site, opens in a new tab)". The quick exit is unchanged.
  • 320 px fallback. The brand word shows from 360 px and the word "Meny" from 768 px. Below that the row keeps the brand mark and an icon-only trigger, so it stays one row with no sideways scroll.
  • One scroll-padding-top (5rem) for the sticky header, so a focused element or an anchor never lands under it.
  • The acute strip is one line at every width. An icon and the number for each service (flame 110, shield 112, ambulance 113, stethoscope 116 117), then "Alle →" / "All →" below 768 px and the full "Alle nødtjenester" / "All emergency services" from 768 px. Each icon and number form one 44 px tel link. The label is not shown but starts the link's name ("Brann 110", "Fire 110"), and the last link is always named by the full term, so the short visible word is the start of its name. The labels start with a capital (Brann, Politi, Ambulanse, Legevakt / Fire, Police, Ambulance, Out-of-hours service). The strip is 44 px tall on a phone, down from 156 px for the 2 x 2 version I tested first. At 320 px the line uses 251 of 273 px (measured with a desktop scrollbar), so with the browser text size at 200 % the links wrap to two or three rows instead of overlapping or scrolling sideways. With a mouse, each number shows its label as a tooltip below it on hover and keyboard focus. The tooltip repeats the link's name, so it is hidden from screen readers, and Escape closes it (WCAG 1.4.13). It only appears where the pointer can hover ((hover: hover) and (pointer: fine)), so a tap on a phone just dials.

Red proofs

Every new or changed check failed once with the code broken on purpose, then passed after I restored it.

Unit tests, new-tab links and shared rows

Break Red result
target="_blank" removed from the external links target attribute missing
rel="noopener noreferrer" cut to rel="noopener" noreferrer missing
header.external back to the old text (en and nb) no link named "... (external site, opens in a new tab)" / "(ekstern side, åpnes i ny fane)"
Alle tjenester pointed at / href assertion failed

Unit tests, the Meny

Break Red result
focusOnOpen: "active" first row not focused
Theme row closes the Meny Meny closed after a theme row
Meny given its own theme state instead of the shared one desktop picker did not show "Tema: Mørkt"
announce(...) removed no status text after a theme row
onFollow only sets open = false focus not back on the trigger
<nav> replaced by a <div> no "Hovedmeny" landmark
axe hide of the Meny's nav removed 16 axe tests failed on landmark-unique
aria-labelledby removed from either group no group named "Språk" / "Tema"
.menu-word span removed summary-name axe violation
Meny links rendered as nav-link arrow keys did not move focus
onFollow removed from the Meny Meny stayed open after a link
page param kept on a language row URL kept &page=2
localStorage.setItem removed language not stored
Class renamed to header-meny prerender test found no Meny
lang removed from the language rows prerendered rows had no lang
Each scoped query reverted (shell, race, language picker, theme picker, header) "Found multiple elements"
One targeted break per remaining assertion in headerMenu.test.tsx (trigger name, hrefs, target, rel, aria-current, lang, group names, arrow wrap, theme applied, row pressed, landmark name, focus) each failed on its own assertion line

Playwright, the header (320 to 1920)

Break Red result
.brand-word { display: none } removed two rows at 320 in English
.menu-nav { display: none } removed at 1024 Meny visible at 1024
scroll-padding-top set to 0 9 of 12 focus cases failed on Shift+Tab
.menu-list .picker-check rule removed check marks not on the right edge
flex-wrap removed from the row sideways scroll at 200 % text
.btn-secondary 40 px control height 40, expected 44
.quick-exit shifted 6 px bottom edge off by 3
Row given min-width at 330 px and below sideways scroll at 320
.brand-word given a 14 px left margin from 360 px the English header wrapped to two rows at 360 px (light, dark and system)
Header made static exit scrolled out of view (y -2874)
Exit translated 1000 px down exit bottom 1052, expected <= 950
Exit min-height 40 px exit height 40
.header-tools or .header-nav not hidden at 1023 still visible
Hidden .header-nav laid out but empty two "Hovedmeny" landmarks
1024 display rules removed (nav, tools) not visible at 1024
.header-menu hidden instead of .menu-nav at 1024 landmark count 2
Edge shift in usePicker disabled list left edge -17 at 320
.menu-list pushed 200 px right list right edge 430, expected <= 367
Escape branch disabled Meny stayed open
Keyboard test, one break per assertion (first row, wrap, theme applied, stays open, aria-pressed, Escape, trigger focus) each failed
Link points to /om; Meny not closed after a link URL and open assertions failed
Current-row padding, code margin, missing check icon text lefts, code rights and check count assertions failed

Not shown red. Two things could not be. First, the .menu-nav { display: flex } safeguard: with today's markup the nav is 44 px tall as display: block too, so no break makes a check fail. Second, the Playwright assertion "Mørkt keeps focus after Enter" stays green with row.focus() removed, because Chromium keeps focus on the button after Enter by itself. The row.focus() is there for Safari clicks. The unit test covers that code path and goes red with its own break ("row keeps focus", focus moved to the summary instead).

The acute strip

Break Red result
aria-label removed from the tel links unit test: no link named "Brann 110"
Full "Alle nødtjenester" shown below 768 px too one-line check failed at 320 px (nb, en), 360 px (en) and 375 px (en)
Full term shown from 640 px instead of 768 px text check failed at 767 px (nb, en)
Tooltip z-index removed all 6 hover checks failed: the sticky header was drawn over the tooltip
Escape rule (data-tips="hidden") disabled all 6 hover checks failed: the tooltip stayed open after Escape
Hover media query dropped, so tooltips show everywhere the touch-phone check failed
flex-wrap removed from the strip the 200 % check failed in 6 of 8 cases (320, 360 and 375 px, both languages). 767 px passes, since the line fits there even at 200 %

Checks

npx biome ci . clean (one existing info about the schema version), 336 unit tests, npm run build:site prerendered 210 pages, 119 Playwright tests (Playwright 1.64.0, Chromium 1248).

Manual, before merge

  • NVDA pass over the Meny at a narrow window. D finds one "Hovedmeny" navigation landmark. The trigger reads "Meny" and collapsed or expanded. The group labels "Språk" and "Tema" are read on entering each group. A theme row reads "trykket", and after Enter it reads "Tema: Mørkt". "Alle tjenester" on /sok leaves NVDA on "Meny". Escape returns to "Meny".
  • Look at the strip at 320 and 375 px in both languages, also with the browser text size at 200 %.

After merge, open https://varde.pages.dev on a phone and try the Meny, the exit and the strip in both languages.

The 2 x 2 grid used two fixed columns, so a one-word label overflowed its cell at a larger text size and printed over the next number. The columns now drop to one when two will not fit, and stay 2 x 2 at default size. Also checks 360 px in the header row rule, retitles the 200 % header test and removes a stale skip-link comment.
@malinfossum malinfossum changed the title feat: one-row phone header with a Meny, new-tab links and a 2 x 2 strip feat: one-row phone header with a Meny, new-tab links and a one-line strip Oct 12, 2026
@malinfossum
malinfossum merged commit b4da282 into main Oct 12, 2026
14 checks passed
@malinfossum
malinfossum deleted the feat/phone-header branch October 12, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant