-
Notifications
You must be signed in to change notification settings - Fork 652
fix issues with navlist static to interactive subnav state #7511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@primer/react": patch | ||
| --- | ||
|
|
||
| Fix NavList parent item flicker during static-to-interactive transitions when navigating between current sub-items in a SubNav. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import type {Meta} from '@storybook/react-vite' | ||
| import React, {useEffect} from 'react' | ||
| import {renderToStaticMarkup} from 'react-dom/server' | ||
| import {PageLayout} from '../PageLayout' | ||
| import {NavList} from './NavList' | ||
| import {ArrowRightIcon, ArrowLeftIcon, BookIcon, FileDirectoryIcon} from '@primer/octicons-react' | ||
|
|
@@ -69,3 +71,87 @@ export const WithGroupTitleAndHeading = () => ( | |
| <PageLayout.Content></PageLayout.Content> | ||
| </PageLayout> | ||
| ) | ||
|
|
||
| type NavigationItem = 'item1' | 'subitem1' | 'subitem2' | 'item3' | ||
|
|
||
| const NAVIGATION_FALLBACK_ITEM: NavigationItem = 'subitem1' | ||
|
|
||
| function getNavigationItemFromHash(): NavigationItem { | ||
| const hash = window.location.hash.replace('#', '') | ||
| if (hash === 'item1' || hash === 'subitem1' || hash === 'subitem2' || hash === 'item3') { | ||
| return hash | ||
| } | ||
| return NAVIGATION_FALLBACK_ITEM | ||
| } | ||
|
|
||
| function StaticTransitionNav({currentItem}: {currentItem: NavigationItem}) { | ||
| return ( | ||
| <PageLayout> | ||
| <PageLayout.Pane position="start"> | ||
| <NavList> | ||
| <NavList.Item href="#item1" aria-current={currentItem === 'item1' ? 'page' : 'false'}> | ||
| Item 1 | ||
| </NavList.Item> | ||
| <NavList.Item> | ||
| Item 2 | ||
| <NavList.SubNav> | ||
| <NavList.Item href="#subitem1" aria-current={currentItem === 'subitem1' ? 'page' : 'false'}> | ||
| Sub item 1 | ||
| </NavList.Item> | ||
| <NavList.Item href="#subitem2" aria-current={currentItem === 'subitem2' ? 'page' : 'false'}> | ||
| Sub item 2 | ||
| </NavList.Item> | ||
| </NavList.SubNav> | ||
| </NavList.Item> | ||
| <NavList.Item href="#item3" aria-current={currentItem === 'item3' ? 'page' : 'false'}> | ||
| Item 3 | ||
| </NavList.Item> | ||
| </NavList> | ||
| </PageLayout.Pane> | ||
| <PageLayout.Content></PageLayout.Content> | ||
| </PageLayout> | ||
| ) | ||
| } | ||
|
|
||
| export const SubNavStaticToInteractiveTransition = () => { | ||
| const [currentItem, setCurrentItem] = React.useState<NavigationItem>(() => getNavigationItemFromHash()) | ||
| const [renderMode, setRenderMode] = React.useState<'static' | 'interactive'>('static') | ||
|
|
||
| const staticMarkup = React.useMemo(() => { | ||
| return renderToStaticMarkup(<StaticTransitionNav currentItem={currentItem} />) | ||
| }, [currentItem]) | ||
|
Comment on lines
+120
to
+122
|
||
|
|
||
| useEffect(() => { | ||
| const abortController = new AbortController() | ||
| let transitionTimer: number | null = null | ||
|
|
||
| const showStaticThenInteractive = () => { | ||
| setCurrentItem(getNavigationItemFromHash()) | ||
| setRenderMode('static') | ||
|
|
||
| if (transitionTimer) { | ||
| window.clearTimeout(transitionTimer) | ||
| } | ||
|
|
||
| // Keep static markup visible long enough to make the collapsed frame obvious. | ||
| transitionTimer = window.setTimeout(() => { | ||
| setRenderMode('interactive') | ||
| }, 180) | ||
| } | ||
|
|
||
| showStaticThenInteractive() | ||
| window.addEventListener('hashchange', showStaticThenInteractive, {signal: abortController.signal}) | ||
|
|
||
| return () => { | ||
| abortController.abort() | ||
| if (transitionTimer) { | ||
| window.clearTimeout(transitionTimer) | ||
| } | ||
| } | ||
| }, []) | ||
|
|
||
| if (renderMode === 'static') return <div dangerouslySetInnerHTML={{__html: staticMarkup}} /> | ||
| return <StaticTransitionNav currentItem={currentItem} /> | ||
| } | ||
|
|
||
| SubNavStaticToInteractiveTransition.storyName = 'SubNav static-to-interactive transition' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import {describe, it, expect, vi} from 'vitest' | ||
| import {render, fireEvent, act} from '@testing-library/react' | ||
| import React from 'react' | ||
| import {renderToStaticMarkup} from 'react-dom/server' | ||
| import {NavList} from './NavList' | ||
| import {ReactRouterLikeLink} from '../Pagination/mocks/ReactRouterLink' | ||
| import {implementsClassName} from '../utils/testing' | ||
|
|
@@ -148,6 +149,11 @@ describe('NavList.Item with NavList.SubNav', () => { | |
| expect(queryByRole('list', {name: 'Item 2'})).toBeNull() | ||
| }) | ||
|
|
||
| it('renders parent item expanded on initial static render when SubNav contains the current item', () => { | ||
| const markup = renderToStaticMarkup(<NavListWithCurrentSubNav />) | ||
| expect(markup).toContain('aria-expanded="true"') | ||
| }) | ||
|
Comment on lines
+152
to
+155
|
||
|
|
||
| it('hides SubNav by default if SubNav does not contain the current item', () => { | ||
| const {queryByRole} = render(<NavListWithSubNav />) | ||
| const subNav = queryByRole('list', {name: 'Item 2'}) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing
renderToStaticMarkupfromreact-dom/serverin a Storybook story (browser bundle) can break Vite/Storybook builds becausereact-dom/serveris Node-oriented. Prefer switching this import toreact-dom/server.browser(if available in your React version), or restructure the story to avoid bundling the server renderer into the client (e.g., generate the static markup outside the browser bundle / via a build-time step).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the .dev.stories after a maintainer has tested the changes locally. It's just to showcase the bug