Repository navigation
Conversation
…y stay mounted A hidden region takes no space, goes inert and keeps its state. Focus inside it moves to main. The region's edge and the content's change in space play as one motion through the shared edge reveal, which measures both before the commit and plays both on one clock after it. On a phone the drawer and its toggle follow 'sideNav', and a drawer with nothing to show reads as closed without writing the app's open state. The section variant's side nav divider goes with the hidden side nav. MobileNav moves focus to main when the button that opened it is hidden. Records: AppShell FR19-FR21, a Public concepts row and verification rows, and the MobileNav FR6 amendment. Stories: Focus Mode and the section variant. Co-authored-by: AK <144495202+AKnassa@users.noreply.github.com> Co-authored-by: Humberto Virtudes <humberto.virtudes@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
|
/approve-spec 2733c69 |
There was a problem hiding this comment.
Changes requested. hiddenRegions works as described on desktop, where both regions slide out with the content on one clock and go inert, but four phone paths don't yet match the AppShell records.
- On a phone, hiding the nav that fills an open drawer removes the menu button instead of hiding it (
MobileNavTogglerenders nothing once mobile navigation is disabled), so MobileNav's new hidden-opener branch never runs and focus ends in the closed drawer or on the page body instead of main (FR20). - On a phone with a top nav, hiding the side nav unmounts its drawer copy and hiding the top nav swaps the whole drawer subtree, so the side nav's state resets after a hide and show (FR19).
- With a top nav that has no drawer items of its own, hiding the side nav removes the drawer while
useAppShellMobile()still reports it open and enabled, so an app's ownMobileNavTogglestays expanded for a drawer that is gone (FR19). - Hiding only the top nav on a phone leaves the visible side-nav top bar without a banner landmark, because the bar checks
headerContent == nullwhile the hidden header still exists (FR3). - The changeset's "keep their state" and "MobileNav moves focus to main when the button that opened it is hidden" don't hold inside AppShell on a phone until the points above are fixed.
A phone-width AppShell test for each path (drawer open, then hide) would pin the fixes. The inline comments point at the lines involved.
[Automated review]
…focus to main - A hidden side nav stays mounted, hidden and inert, inside the top nav's drawer, and the drawer no longer swaps its subtree when the top nav hides, so the side nav keeps its state on a phone. - TopNav reports whether it has drawer items of its own, so a drawer with only a hidden side nav reads as closed and disabled. - When the drawer closes because it has nothing left to show, its menu button is gone, and MobileNav moves focus to this shell's main. - With only the top nav hidden, the side nav's bar keeps the banner landmark. Co-authored-by: AK <144495202+AKnassa@users.noreply.github.com> Co-authored-by: Humberto Virtudes <humberto.virtudes@gmail.com>
|
/approve-spec ab61fbc |
There was a problem hiding this comment.
Changes requested. The phone gaps from the last round are fixed: a hidden side nav keeps its drawer state, an empty drawer reads closed, focus reaches this shell's main, and the banner landmark stays. Four things still need work:
- With a TopNav that has only a heading and no side nav, a shell that passes no
hiddenRegionsnow treats its drawer as empty, so amobileNav.contentdrawer and the app's ownMobileNavTogglestop rendering anduseAppShellMobile()reports navigation disabled, against AppShell FR11 and FR12 (countcontentas drawer content and apply the empty-drawer test only while a region is hidden). - TopNav reports its items to AppShell from a layout effect into state that starts at
true, so the first render, and the server HTML, show a menu toggle for an empty drawer that goes only after hydration; derive whether the top nav has drawer items during render instead. - With only the top nav hidden on a phone, the drawer is still headed by the hidden top nav's heading because
TopNavpassesheader={heading}whatever its hidden state, while FR19 keeps a hidden top nav out of the drawer. - Miss: hiding both navs also drops a
mobileNav.contentdrawer that still has content to show (FR12), which the first fix covers.
CI has not run on this head yet. The inline comments point at the lines involved.
[Automated review]
…idden A shell that hid nothing, with a heading-only TopNav and the app's own drawer content, lost its drawer and toggle. The empty-drawer rule now applies only while a listed region is hidden, and counts the app's own drawer content. Whether the top nav has drawer items of its own is read from its props during render, with no Effect or state, so the server HTML matches. A hidden top nav gives the drawer no heading. A test matrix pins the drawer of a shell that hides nothing; it passes on main too. Co-authored-by: AK <144495202+AKnassa@users.noreply.github.com> Co-authored-by: Humberto Virtudes <humberto.virtudes@gmail.com>
|
/approve-spec b8ddd89 |
There was a problem hiding this comment.
Changes requested. hiddenRegions works as described on desktop and leaves shells that hide nothing unchanged, but four things still need work:
- When an app wraps a heading-only TopNav in its own component and hides only the side nav,
topNavHasDrawerItemscounts the wrapper as having items, so on a phone the menu button stays and opens an empty drawer whileuseAppShellMobile()reports it open and enabled, against AppShell FR19. - Miss: when a region hides while the phone drawer is open, the drawer closes as FR20 says, but revealing the region reopens it and moves focus into it because AppShell keeps its stored open state, against FR20's "Revealing a region does not move focus"; clear AppShell's own open state when the drawer empties and say in the record what a controlled drawer does on reveal.
- Miss: TopNav FR5 and FR6 still say the drawer is headed by
heading, shows a divider whenever items and side nav content are both present, and keeps the toggle while AppShell passes side nav content, which no longer holds for hidden regions, so add a clause deferring to AppShell FR19 and give the MobileNav FR6 amendment the removed-opener case the code handles. - Miss: the changeset, the prop's JSDoc and both doc descriptions say the phone drawer and its toggle follow
'sideNav', but with a top nav that has items hiding the side nav keeps the toggle and with only a top nav hiding'topNav'removes it, so describe it as the drawer leaving out each hidden region and its toggle going when nothing is left.
CI has not run on this head yet. The inline comments point at the lines involved.
[Automated review]
A TopNav wrapped in the app's own component is not one AppShell can read, so with only the side nav hidden its menu button stayed and opened an empty drawer. TopNav now shows its menu button and opens its drawer, during render, only for its own items unless hidden and the side nav unless hidden. A drawer with nothing visible closes, and focus goes to main. FR19 states what AppShell reports for a slot that is not a TopNav element. Co-authored-by: AK <144495202+AKnassa@users.noreply.github.com> Co-authored-by: Humberto Virtudes <humberto.virtudes@gmail.com>
|
/approve-spec b5f3bab |
There was a problem hiding this comment.
Changes requested. The wrapped TopNav menu button from the last round is fixed, and shells that hide nothing still render exactly as on main, but three things still need work:
- When a region hides while the phone drawer is open, revealing it reopens the drawer and moves focus into it because AppShell keeps its stored open state (
AppShell.tsx:727-728), against FR20's "Revealing a region does not move focus", and with a heading-only TopNav wrapped in the app's own component the same state leavesuseAppShellMobile()reporting the drawer open (an app's ownMobileNavTogglereads expanded) while TopNav holds it closed, against FR19; clear the stored state when the drawer empties and say in FR19 and FR20 what a controlled drawer and a top nav slot AppShell cannot read report. - TopNav FR5 and FR6 still say the drawer is headed by
heading, shows a divider whenever items and side nav content are both present, and keeps the toggle while AppShell passes side nav content, which no longer holds for hidden regions, so add a clause deferring to AppShell FR19 and give the MobileNav FR6 amendment the removed-opener case the code handles. - The changeset, the prop's JSDoc and both doc descriptions say the phone drawer and its toggle follow
'sideNav', but with a top nav that has items hiding the side nav keeps the toggle and with only a top nav hiding'topNav'removes it, so describe it as the drawer leaving out each hidden region and its toggle going when nothing is left.
CI has not run on this head yet. The inline comments point at the lines involved.
[Automated review]
…d revealing it does not reopen it The drawer's stored open state survived a hide, so revealing the region reopened the drawer and moved focus into it. A region that hides while the drawer is open now holds the drawer shut until the app opens it again; the app's own open state is not written, and useAppShellMobile() reports the drawer closed meanwhile. Records: AppShell FR19 and FR20, TopNav FR5 and FR6, and MobileNav FR6 state the hidden-region cases. The changeset and docs say the drawer leaves out each hidden region. Co-authored-by: AK <144495202+AKnassa@users.noreply.github.com> Co-authored-by: Humberto Virtudes <humberto.virtudes@gmail.com>
|
/approve-spec cc51d1c |
There was a problem hiding this comment.
Approved pending CI: CI has not run on this head yet.
The three changes requested last round are in: revealing a hidden region now keeps the phone drawer closed until the menu button opens it again, TopNav FR5/FR6 and MobileNav FR6 state the hidden-region cases, and the changeset and docs describe the drawer as it ships. Shells that don't pass hiddenRegions still render exactly as on main.
Two optional follow-ups, both inline:
- Miss: a standalone
MobileNavwhose hidden opener sits beside a plain<main>(notabindex) leaves focus on the page body, because that<main>can't take focus, so FR6's "focus moves to main" holds only inside AppShell or for a focusable main. - The new drawer-hold comment in
AppShell.tsxcould be split into short, active sentences.
[Automated review]
| shellFocus?.main() ?? | ||
| opener.ownerDocument.querySelector<HTMLElement>('[role="main"], main') | ||
| )?.focus(); |
There was a problem hiding this comment.
Miss, non-blocking: outside AppShell this focuses the document's first main. A plain <main> without tabindex is not focusable, so focus() does nothing and focus ends on the page body, not main as the amended FR6 says ("a connected but hidden opener counts as gone, and focus moves to main"). Inside AppShell main has tabIndex={-1}, so that path works. Scoping the FR6 sentence to a main that can take focus, or giving main tabindex="-1" for the move, closes the gap.
| // A region that hides while the drawer is open closes the drawer, and the | ||
| // drawer stays shut until the app opens it again, so revealing the region | ||
| // never reopens it. The app's own open state is not written. The hold is | ||
| // adjusted during render from what this render sees. |
There was a problem hiding this comment.
Non-blocking (comment style): the first sentence runs 30 words over several topics, and "The hold is adjusted during render" is passive. Suggested:
// A region that hides while the drawer is open closes the drawer.
// The drawer stays shut until the app opens it again. Revealing the
// region does not reopen it. Hiding never writes the app's open state.
Supersedes #10276 (SideNav
collapsedWidth/collapsedVariant), and builds the hide-and-keep part of #3890 by AKnassa and of the Focus Mode spec in #8410 by humbertovirtudes on AppShell. Both are credited as co-authors and in the changeset. Owner decision, Nov 6 2026: hidden regions live on the shell, as one controlled list, so the shell can play the nav's slide and the content's reflow as one motion.User impact
An app can now hide its top nav, its side nav, or both, for example for a focus mode, and bring them back exactly as they were:
hiddenRegionsare unchanged.Public delta
AppShellProps.hiddenRegions?: ReadonlyArray<'topNav' | 'sideNav'>. Exactly those two values; the banner and content can't be hidden. At runtime an unknown entry is ignored with a development warning. No other API change.Change
AppShell.tsx:mobileNav.isOpenor callsonOpenChange.mobileNav.content. A shell that hides nothing keeps its drawer exactly as before.useAppShellMobile()keeps reporting the drawer as enabled there.MobileNav.tsx: an opener that is still connected but hidden (inert) counts as gone, and focus moves to main. Inside AppShell, a menu button removed because the drawer has nothing left to show counts as gone too, and focus moves to this shell's main (an internal context, not the document's first main).spec:AST-002FR4 admission note and the Implementation note), and the Compatibility line.Core/AppShell"Focus Mode" and "Hidden Side Nav Section"; docs;[feat]changeset.Test plan
main's AppShell, 6 of the 8 newhiddenRegionscases fail; all pass here. The 2 that already pass onmainare "focus in content untouched" and "state kept", whichmainsatisfies trivially because nothing hides. The MobileNav hidden-opener case is new.hiddenRegionsomitted or empty × a heading-only top nav, a top nav with items, app drawer content, and no navigation. It checks the drawer's enabled state and toggle count, and passes onmainand here, so the hidden-state logic can't change the default shell.onOpenChangenever called); a wrapped heading-only top nav with only the side nav hidden shows no menu button; the drawer keeps the hidden side nav's state; a drawer with nothing left reads as closed and its toggle goes; focus reaches main when the drawer closes because its side nav hid; and the banner landmark stays when only the top nav hides.tscis clean, andpnpm check:knowledgeandpnpm check:changesetspass. Lint andtscwere re-run on this head.Writing rubric results
R1 narration: none; FR19–FR21, DEC-2 and the MobileNav FR6 amendment state the behavior in the present tense.
R2 research: none in the records.
R3 evidence: FR19–FR21 cite the owner decision and
spec:AST-002,spec:AST-105and MobileNav; verification rows name tests and stories, with no pull request.R4 rejected alternatives: the Does-not-own list states scope, not history.
R5 change references: none.
R6 open questions: none added.