Skip to content

Commit 7e0ffd7

Browse files
bloveclaude
andcommitted
chore(website): delete the superseded nav exports and dead drawer CSS
The rebuilt navbar left three kinds of dead weight behind. `NavDesktop.tsx` still exported `links` (the old flat link array) and `trackNavLink` (the old per-label analytics helper). Both were kept alive only for the mobile drawer, which now reads `NAV_TRIGGERS` and tracks through `trackNavItem`. Nothing outside the file referenced either. The dead CSS in `chrome.css` falls into two groups, and the split matters when reading this diff: Orphaned by this branch's drill-in rewrite (Task 8) — `.nav-mtabs`, `.nav-mtab`, `.nav-mtab[data-active]`, `.nav-mobile-site-link`. Already dead before this branch started, leftovers from an older drawer — `.nav-msubtabs-wrap`, `.nav-msubtabs`, `.nav-msubtab`, `.nav-msubtab[data-active]`, `.nav-mobile-content-list`, `.nav-mobile-item`, `.nav-mobile-item[data-active]`, `.nav-mobile-item--strong`, `.nav-mobile-demo-link`, `.nav-mobile-section-toggle`, `.nav-mobile-chevron`, `.nav-mobile-chevron[data-open]`, `.nav-mobile-search`, `.nav-mobile-search:focus-visible`. Removing them is in scope — same drawer's leftovers — but they are not fallout from the redesign. Every class was re-verified unreferenced across `src/`, `e2e/`, the rest of `src/styles/`, `src/app/global.css` and the style contracts before deletion; none is built dynamically, and the drawer's live classes (`.nav-mobile-list`, `-overlay`, `-row`, `-back`, `-panel`, `-group`, `-github-link`, `-cta`, `-dialog-close`) are untouched. Finally, drill-in focus is now symmetric. Pushing a level focuses its "Back to menu" row through an explicit ref; popping fell through to `focusable()[0]`, which is not a designed destination. Both pop paths — the Back button and Escape — now route through `popToRoot`, which records the originating trigger id so the per-level focus effect can land on that row once root has re-rendered. The restore reads a ref map rather than the focus-trap query on purpose: jsdom's multi-clause `querySelectorAll` groups by clause instead of returning document order, so a test written against `focusable()[0]` would assert the Pricing anchor where a browser gives the row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 821ca08 commit 7e0ffd7

4 files changed

Lines changed: 64 additions & 179 deletions

File tree

apps/website/src/components/shared/Nav.spec.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,32 @@ describe('Docs mobile navigation', () => {
343343
);
344344
});
345345

346+
it('returns focus to the trigger row a popped level came from', () => {
347+
pathnameRef.current = '/';
348+
render(<Nav />);
349+
fireEvent.click(screen.getByRole('button', { name: 'Open menu' }));
350+
const dialog = screen.getByRole('dialog', { name: 'Mobile navigation' });
351+
352+
// Both pop paths, one after the other, against the same drawer: the
353+
// "Back to menu" button and Escape must land in the same place, and that
354+
// place is the row the reader pushed from — not whatever the focus trap's
355+
// query happens to list first. (jsdom's multi-clause querySelectorAll
356+
// groups by clause instead of returning document order, so an assertion
357+
// written against `focusable()[0]` would encode the wrong element and
358+
// pass while a real browser did something else.)
359+
fireEvent.click(within(dialog).getByRole('button', { name: 'Libraries' }));
360+
fireEvent.click(within(dialog).getByRole('button', { name: 'Back to menu' }));
361+
expect(document.activeElement).toBe(
362+
within(dialog).getByRole('button', { name: 'Libraries' }),
363+
);
364+
365+
fireEvent.click(within(dialog).getByRole('button', { name: 'Libraries' }));
366+
fireEvent.keyDown(document, { key: 'Escape' });
367+
expect(document.activeElement).toBe(
368+
within(dialog).getByRole('button', { name: 'Libraries' }),
369+
);
370+
});
371+
346372
it('tags mobile panel analytics with the trigger it came from', () => {
347373
pathnameRef.current = '/';
348374
render(<Nav />);

apps/website/src/components/shared/NavDesktop.tsx

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,6 @@ import { NAV_TRIGGERS, type NavItem, type NavPanel } from './nav-config';
1717
const OPEN_DELAY_MS = 100;
1818
const CLOSE_DELAY_MS = 150;
1919

20-
export const links = [
21-
{ label: 'Pilot to Prod', href: '/pilot-to-prod', external: false },
22-
{ label: 'Docs', href: '/docs', external: false },
23-
{ label: 'Pricing', href: '/pricing', external: false },
24-
];
25-
26-
export function trackNavLink(
27-
label: string,
28-
href: string,
29-
external: boolean,
30-
surface: 'nav' | 'mobile_nav'
31-
) {
32-
const slug = label
33-
.toLowerCase()
34-
.replace(/[^a-z0-9]+/g, '_')
35-
.replace(/^_|_$/g, '');
36-
const ctaId: `nav_${string}` | `mobile_nav_${string}` =
37-
surface === 'nav' ? `nav_${slug}` : `mobile_nav_${slug}`;
38-
if (external) {
39-
trackExternalLinkClick(href, { surface, cta_id: ctaId, cta_text: label });
40-
return;
41-
}
42-
trackCtaClick({
43-
surface,
44-
destination_url: href,
45-
cta_id: ctaId,
46-
cta_text: label,
47-
});
48-
}
49-
5020
export function trackNavItem(item: NavItem, surface: 'nav' | 'mobile_nav') {
5121
const ctaId: `nav_${string}` | `mobile_nav_${string}` =
5222
surface === 'nav' ? `nav_${item.ctaId}` : `mobile_nav_${item.ctaId}`;

apps/website/src/components/shared/NavMobile.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ export function NavMobile({
109109
const mobileTriggerRef = useRef<HTMLButtonElement>(null);
110110
const mobileDialogRef = useRef<HTMLDivElement>(null);
111111
const mobileBackRef = useRef<HTMLButtonElement>(null);
112+
/** The root trigger rows, so a pop can land back on the one it came from. */
113+
const rootRowRefs = useRef(new Map<string, HTMLButtonElement>());
114+
/**
115+
* Which root row a pending pop should focus. Read by the per-level focus
116+
* effect below: the row does not exist while the panel level is mounted,
117+
* so the restore has to wait until root has rendered.
118+
*/
119+
const pendingRootFocusRef = useRef<string | null>(null);
112120
const restoreMobileFocusRef = useRef(false);
113121
const pendingMobileSearchRef = useRef(false);
114122
const cancelScheduledMobileRestoreRef = useRef<(() => void) | null>(null);
@@ -126,6 +134,16 @@ export function NavMobile({
126134
initialLevel(isDocsPage)
127135
);
128136

137+
/**
138+
* Pop back to the root list, remembering the trigger row the reader came
139+
* from. Pushing lands on the level's "Back to menu" row; popping mirrors it
140+
* by landing back on that trigger, so the two moves are symmetric.
141+
*/
142+
const popToRoot = useCallback((fromId: string) => {
143+
pendingRootFocusRef.current = fromId;
144+
setLevel(rootLevel);
145+
}, []);
146+
129147
useEffect(() => {
130148
if (open) setLevel(initialLevel(isDocsPage));
131149
}, [isDocsPage, open]);
@@ -202,9 +220,19 @@ export function NavMobile({
202220
'a[href], button:not(:disabled), [tabindex]:not([tabindex="-1"])'
203221
) ?? []
204222
);
205-
// A pushed level leads with its back row; land there so the way out is
206-
// the first thing the keyboard reaches.
207-
(mobileBackRef.current ?? focusable()[0])?.focus();
223+
// A pop returns to the trigger row it came from — an explicit ref, never
224+
// `focusable()[0]`: jsdom's multi-clause querySelectorAll groups by clause
225+
// rather than returning document order, so index 0 there is not the
226+
// element a browser would hand back.
227+
const pendingRootFocus = pendingRootFocusRef.current;
228+
if (pendingRootFocus !== null) {
229+
pendingRootFocusRef.current = null;
230+
rootRowRefs.current.get(pendingRootFocus)?.focus();
231+
} else {
232+
// A pushed level leads with its back row; land there so the way out is
233+
// the first thing the keyboard reaches.
234+
(mobileBackRef.current ?? focusable()[0])?.focus();
235+
}
208236
const onKeyDown = (event: KeyboardEvent) => {
209237
if (event.key === 'Escape') {
210238
event.preventDefault();
@@ -219,7 +247,7 @@ export function NavMobile({
219247
// sole trigger' runs on a docs route and depends on this falling
220248
// through to closeMobileMenu() at the opening level.
221249
if (level.kind === 'panel' && !sameLevel(level, initialLevel(isDocsPage))) {
222-
setLevel(rootLevel);
250+
popToRoot(level.id);
223251
return;
224252
}
225253
closeMobileMenu();
@@ -240,7 +268,7 @@ export function NavMobile({
240268
};
241269
document.addEventListener('keydown', onKeyDown);
242270
return () => document.removeEventListener('keydown', onKeyDown);
243-
}, [closeMobileMenu, isDocsPage, level, open]);
271+
}, [closeMobileMenu, isDocsPage, level, open, popToRoot]);
244272

245273
// The docs level hosts the live docs tree, but only on a docs route — there
246274
// is no docs context anywhere else, so the marketing panel stands in.
@@ -291,7 +319,7 @@ export function NavMobile({
291319
ref={mobileBackRef}
292320
type="button"
293321
className="nav-mobile-back"
294-
onClick={() => setLevel(rootLevel)}
322+
onClick={() => popToRoot(level.id)}
295323
>
296324
<ChevronLeft size={16} strokeWidth={2} aria-hidden="true" />
297325
Back to menu
@@ -322,6 +350,10 @@ export function NavMobile({
322350
<button
323351
key={trigger.id}
324352
type="button"
353+
ref={(node) => {
354+
if (node) rootRowRefs.current.set(trigger.id, node);
355+
else rootRowRefs.current.delete(trigger.id);
356+
}}
325357
className="nav-mobile-row"
326358
onClick={() => setLevel({ kind: 'panel', id: trigger.id })}
327359
>

apps/website/src/styles/chrome.css

Lines changed: 0 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -234,139 +234,11 @@
234234
.nav-mobile-dialog-close:focus-visible {
235235
background: var(--color-surface-dim);
236236
}
237-
.nav-mtabs {
238-
display: flex;
239-
gap: 6px;
240-
padding: 4px;
241-
background: rgba(0, 0, 0, 0.03);
242-
border-radius: 10px;
243-
}
244-
.nav-mtab {
245-
flex: 1;
246-
text-align: center;
247-
padding: 10px 0;
248-
font-size: 15px;
249-
font-weight: 500;
250-
font-family: var(--font-sans);
251-
background: transparent;
252-
color: var(--color-text-muted);
253-
border: none;
254-
border-radius: 8px;
255-
cursor: pointer;
256-
min-height: 44px;
257-
}
258-
.nav-mtab[data-active] {
259-
background: var(--color-accent-surface);
260-
color: var(--color-accent);
261-
}
262-
.nav-msubtabs-wrap {
263-
display: flex;
264-
flex-direction: column;
265-
gap: 8px;
266-
}
267237
.nav-mobile-list {
268238
display: flex;
269239
flex-direction: column;
270240
gap: 2px;
271241
}
272-
.nav-mobile-content-list {
273-
display: flex;
274-
flex-direction: column;
275-
gap: 4px;
276-
}
277-
.nav-mobile-item {
278-
display: block;
279-
padding: 12px 14px;
280-
border-radius: 8px;
281-
font-size: 16px;
282-
line-height: 24px;
283-
min-height: 44px;
284-
text-decoration: none;
285-
font-family: var(--font-sans);
286-
color: var(--color-text-secondary);
287-
background: transparent;
288-
}
289-
.nav-mobile-item[data-active] {
290-
color: var(--color-accent);
291-
background: var(--color-accent-surface);
292-
}
293-
.nav-mobile-item--strong {
294-
font-weight: 600;
295-
}
296-
.nav-msubtabs {
297-
display: flex;
298-
gap: 4px;
299-
padding: 3px;
300-
background: rgba(0, 0, 0, 0.03);
301-
border-radius: 8px;
302-
}
303-
.nav-msubtab {
304-
flex: 1;
305-
text-align: center;
306-
padding: 8px 0;
307-
font-family: var(--font-mono);
308-
font-size: 0.75rem;
309-
font-weight: 600;
310-
background: transparent;
311-
color: var(--color-text-muted);
312-
border: none;
313-
border-radius: 6px;
314-
cursor: pointer;
315-
min-height: 36px;
316-
}
317-
.nav-msubtab[data-active] {
318-
background: var(--color-accent-surface);
319-
color: var(--color-accent);
320-
}
321-
.nav-mobile-demo-link {
322-
display: flex;
323-
align-items: center;
324-
justify-content: space-between;
325-
padding: 12px 14px;
326-
border-radius: 8px;
327-
min-height: 44px;
328-
color: var(--color-accent);
329-
background: var(--color-accent-surface);
330-
text-decoration: none;
331-
font-family: var(--font-sans);
332-
font-weight: 600;
333-
}
334-
.nav-mobile-section-toggle {
335-
width: 100%;
336-
text-align: left;
337-
display: flex;
338-
align-items: center;
339-
justify-content: space-between;
340-
background: none;
341-
border: none;
342-
cursor: pointer;
343-
padding: 12px 14px;
344-
min-height: 48px;
345-
border-radius: 8px;
346-
font-family: var(--font-sans);
347-
font-size: 16px;
348-
line-height: 24px;
349-
color: var(--color-text-primary);
350-
}
351-
.nav-mobile-chevron {
352-
transition: transform 0.25s ease;
353-
flex-shrink: 0;
354-
transform: rotate(0);
355-
}
356-
.nav-mobile-chevron[data-open] {
357-
transform: rotate(180deg);
358-
}
359-
.nav-mobile-site-link {
360-
display: block;
361-
padding: 14px 14px;
362-
border-radius: 8px;
363-
font-size: 16px;
364-
line-height: 24px;
365-
min-height: 48px;
366-
color: var(--color-text-secondary);
367-
text-decoration: none;
368-
font-family: var(--font-sans);
369-
}
370242
.nav-mobile-github-link {
371243
display: flex;
372244
align-items: center;
@@ -505,21 +377,6 @@
505377
font-family: var(--font-sans);
506378
}
507379

508-
/* Mobile docs-search entry (polish arc PR 3) — button reset to match the
509-
* .nav-mobile-item link styling it shares. */
510-
.nav-mobile-search {
511-
width: 100%;
512-
text-align: left;
513-
background: none;
514-
border: none;
515-
cursor: pointer;
516-
color: var(--color-text-secondary);
517-
}
518-
.nav-mobile-search:focus-visible {
519-
outline: none;
520-
box-shadow: var(--shadow-focus);
521-
}
522-
523380
/* Nav panels
524381
*
525382
* No borders and no dividers anywhere: the hovered item separates itself with a

0 commit comments

Comments
 (0)