Skip to content
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

Stacked Nav #77665

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open

Stacked Nav #77665

wants to merge 22 commits into from

Conversation

natemoo-re
Copy link
Member

@natemoo-re natemoo-re commented Sep 17, 2024

Update side navigation to new Stacked UI, behind the navigation-sidebar-v2 flag.

Rather than the approach taken in #76868, this PR implements the new navigation as an entirely new set of components in the components/nav/ directory. This new experience is rendered from the static/app/views/organizationLayout/index.tsx file. The intention is that the existing components/sidebar/ directory would be removed when this feature flag reaches GA.

Todo

  • Basic Tests
  • Basic Mobile nav

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Sep 17, 2024
Copy link

codecov bot commented Sep 17, 2024

Bundle Report

Changes will increase total bundle size by 25.42kB (0.09%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
app-webpack-bundle-array-push 29.27MB 25.42kB (0.09%) ⬆️

};
}

export function useNavItems(): NavItemsResult {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useNavItems hook acts as a single source of truth for the entire nav, including nested navigation and feature flag checks.

Comment on lines 37 to +41
// oganization is loaded before rendering children. Organization may not be
// loaded yet when this first renders.
const organization = useOrganization({allowNull: true});
const hasNavigationV2 = organization?.features.includes('navigation-sidebar-v2');
const App = hasNavigationV2 ? AppLayout : LegacyAppLayout;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to watch out for: this note calls out that "Organization may not be loaded yet when this first renders".

For this experience to exist behind a feature flag, the organization has to be loaded. This could potentially cause some layout thrashing once the org loads.

Comment on lines +58 to +66
<AppContainer className="app">
<Nav />
<BodyContainer>
{organization && <OrganizationHeader organization={organization} />}
{organization && <DevToolInit />}
<Body>{children}</Body>
<Footer />
</BodyContainer>
</AppContainer>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notable: adding a new wrapper for the main content allows us to remove the position: fixed CSS for the sidebar. This means that we no longer have to hardcode sidebar width and manually adjust padding for the content offset.

Copy link

codecov bot commented Sep 17, 2024

Codecov Report

Attention: Patch coverage is 52.68293% with 97 lines in your changes missing coverage. Please review.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
static/app/components/nav/mobile.tsx 6.45% 29 Missing ⚠️
static/app/components/nav/utils.tsx 53.44% 19 Missing and 8 partials ⚠️
static/app/components/nav/submenu.tsx 26.31% 14 Missing ⚠️
static/app/components/nav/useIndicator.tsx 38.09% 13 Missing ⚠️
static/app/components/nav/index.tsx 66.66% 5 Missing and 1 partial ⚠️
static/app/views/organizationLayout/index.tsx 55.55% 3 Missing and 1 partial ⚠️
static/app/views/issueList/utils.tsx 71.42% 1 Missing and 1 partial ⚠️
static/app/components/nav/config.tsx 94.44% 0 Missing and 1 partial ⚠️
static/app/components/nav/sidebar.tsx 95.83% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #77665      +/-   ##
==========================================
- Coverage   78.12%   78.10%   -0.02%     
==========================================
  Files        6996     7003       +7     
  Lines      310224   310429     +205     
  Branches    50762    50796      +34     
==========================================
+ Hits       242351   242461     +110     
- Misses      56142    56226      +84     
- Partials    11731    11742      +11     

Comment on lines +55 to +76
const [view, setView] = useState<'closed' | 'primary' | 'secondary'>('closed');
const handleClick = useCallback(() => {
setView(value => (value === 'closed' ? 'primary' : 'closed'));
}, [setView]);

const location = useLocation();

useEffect(() => {
setView('closed');
}, [location.pathname, setView]);

useEffect(() => {
const body = document.body;
const app = document.querySelector('.app > :not(nav)');
if (view !== 'closed') {
app?.setAttribute('inert', '');
body.style.setProperty('overflow', 'hidden');
} else {
app?.removeAttribute('inert');
body.style.removeProperty('overflow');
}
}, [view]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is generally preferable to use useLayoutEffect when doing such changes, useEffect might result in subtle flickering

You can get rid of this last useEffect if you call the body attrib logic inside the handler, it's generally preferred to do that as useEffects are annoying to follow

onViewChange = useCallback(view => { 
  setView(view)
  updateNavStyleAttributes(view) // can be defined outside of the component
},[setView])

useEffect...
   onViewChange('closed')
[onViewChange]

Can we use a dedicated query selector and target the element directly (it would make it a bit more robust than having to rely on the dom selector). Right now if the selector fails, then logic will silently fail, but I think we want to log this as the nav should always be present?

Comment on lines +16 to +31
const handlePointerMove = (e: PointerEvent<HTMLUListElement>) => {
if (!(e.target instanceof Element)) return;
const a = e.target.closest('a');
const list = e.target.closest('ul');
if (!(a && list)) {
hover.current = null;
animation.current = {duration: 0.01};
return;
}
if (hover.current === a) return;
animation.current = {type: 'spring', mass: 0.1, damping: 10, stiffness: 150};
hover.current = a;
const newY = a?.getBoundingClientRect()?.y ?? 0;
const offset = list.getBoundingClientRect()?.y ?? 0;
setY(newY - offset);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be a performance problem (not sure to what extend), but we are triggering style and layout by calling getBoundingClientRect on both elements as well as using state to store the y variable. Since this will fire on pointer move, it will create a ton of updates

Is there a way you can do this without using state and calling bounding rects or using setState to track y coordinate?

static/app/components/nav/utils.tsx Outdated Show resolved Hide resolved
static/app/components/nav/utils.tsx Outdated Show resolved Hide resolved
static/app/components/nav/utils.tsx Outdated Show resolved Hide resolved

mockUseLocation.mockReturnValue({...LocationFixture()});
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
afterEach(() => ConfigStore.reset())

I think you can also reset the store

Comment on lines +30 to +39
export interface NavItemsResult {
primary: {
body: SidebarItem[];
footer: SidebarItem[];
};
secondary: {
body: SubmenuItem[];
footer: SubmenuItem[];
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export interface NavItemsResult {
primary: {
body: SidebarItem[];
footer: SidebarItem[];
};
secondary: {
body: SubmenuItem[];
footer: SubmenuItem[];
};
}
export interface NavItemsResult {
primary: {
body: ReadonlyArray<SidebarItem>;
footer: ReadonlyArray<SidebarItem>;
};
secondary: {
body: ReadonlyArray<SidebarItem>;
footer: ReadonlyArray<SidebarItem>;
};
}

I think you want to make sure these are not mutated in any component as it would impact the rendering


interface NavItem {
label: string;
check?: FeatureCheck;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment about reusing the feature check signature if possible

indicatorProps: HTMLAttributes<HTMLSpanElement>;
}

export function useIndicator() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function useIndicator() {
export function useNavIndicator() {

@@ -33,20 +37,59 @@ function OrganizationLayout({children}: Props) {
// oganization is loaded before rendering children. Organization may not be
// loaded yet when this first renders.
const organization = useOrganization({allowNull: true});
const hasNavigationV2 = organization?.features.includes('navigation-sidebar-v2');
const App = hasNavigationV2 ? AppLayout : LegacyAppLayout;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for not using V2 to name the layout as well :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Frontend Automatically applied to PRs that change frontend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants