Skip to content

Commit eb70f2a

Browse files
authored
ref(nav): Create new NavContextProvider that wraps the entire application (#83615)
The first step towards making this component more reusable is creating a new context that will be available anywhere in the app.
1 parent be7514c commit eb70f2a

File tree

6 files changed

+103
-68
lines changed

6 files changed

+103
-68
lines changed

static/app/components/nav/context.tsx

+16-51
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,26 @@
1-
import {createContext, useContext, useMemo} from 'react';
2-
3-
import {createNavConfig} from 'sentry/components/nav/config';
4-
import type {
5-
NavConfig,
6-
NavItemLayout,
7-
NavSidebarItem,
8-
NavSubmenuItem,
9-
} from 'sentry/components/nav/utils';
10-
import {isNavItemActive, isSubmenuItemActive} from 'sentry/components/nav/utils';
11-
import {useLocation} from 'sentry/utils/useLocation';
12-
import useOrganization from 'sentry/utils/useOrganization';
1+
import {createContext, useContext, useMemo, useState} from 'react';
132

143
export interface NavContext {
15-
/** Raw config for entire nav items */
16-
config: Readonly<NavConfig>;
17-
/** Currently active submenu items, if any */
18-
submenu?: Readonly<NavItemLayout<NavSubmenuItem>>;
4+
secondaryNavEl: HTMLElement | null;
5+
setSecondaryNavEl: (el: HTMLElement | null) => void;
196
}
207

21-
const NavContext = createContext<NavContext>({config: {main: []}});
8+
const NavContext = createContext<NavContext>({
9+
secondaryNavEl: null,
10+
setSecondaryNavEl: () => {},
11+
});
2212

2313
export function useNavContext(): NavContext {
24-
const navContext = useContext(NavContext);
25-
return navContext;
14+
return useContext(NavContext);
2615
}
2716

28-
export function NavContextProvider({children}: any) {
29-
const organization = useOrganization();
30-
const location = useLocation();
31-
/** Raw nav configuration values */
32-
const config = useMemo(() => createNavConfig({organization}), [organization]);
33-
/**
34-
* Active submenu items derived from the nav config and current `location`.
35-
* These are returned in a normalized layout format for ease of use.
36-
*/
37-
const submenu = useMemo<NavContext['submenu']>(() => {
38-
for (const item of config.main) {
39-
if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
40-
return normalizeSubmenu(item.submenu);
41-
}
42-
}
43-
if (config.footer) {
44-
for (const item of config.footer) {
45-
if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
46-
return normalizeSubmenu(item.submenu);
47-
}
48-
}
49-
}
50-
return undefined;
51-
}, [config, location]);
17+
export function NavContextProvider({children}: {children: React.ReactNode}) {
18+
const [secondaryNavEl, setSecondaryNavEl] = useState<HTMLElement | null>(null);
5219

53-
return <NavContext.Provider value={{config, submenu}}>{children}</NavContext.Provider>;
54-
}
20+
const value = useMemo(
21+
() => ({secondaryNavEl, setSecondaryNavEl}),
22+
[secondaryNavEl, setSecondaryNavEl]
23+
);
5524

56-
const normalizeSubmenu = (submenu: NavSidebarItem['submenu']): NavContext['submenu'] => {
57-
if (Array.isArray(submenu)) {
58-
return {main: submenu};
59-
}
60-
return submenu;
61-
};
25+
return <NavContext.Provider value={value}>{children}</NavContext.Provider>;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {createContext, useContext, useMemo} from 'react';
2+
3+
import {createNavConfig} from 'sentry/components/nav/config';
4+
import type {
5+
NavConfig,
6+
NavItemLayout,
7+
NavSidebarItem,
8+
NavSubmenuItem,
9+
} from 'sentry/components/nav/utils';
10+
import {isNavItemActive, isSubmenuItemActive} from 'sentry/components/nav/utils';
11+
import {useLocation} from 'sentry/utils/useLocation';
12+
import useOrganization from 'sentry/utils/useOrganization';
13+
14+
export interface DeprecatedNavContext {
15+
/** Raw config for entire nav items */
16+
config: Readonly<NavConfig>;
17+
/** Currently active submenu items, if any */
18+
submenu?: Readonly<NavItemLayout<NavSubmenuItem>>;
19+
}
20+
21+
const DeprecatedNavContext = createContext<DeprecatedNavContext>({config: {main: []}});
22+
23+
export function useNavContextDeprecated(): DeprecatedNavContext {
24+
const navContext = useContext(DeprecatedNavContext);
25+
return navContext;
26+
}
27+
28+
export function DeprecatedNavContextProvider({children}: {children: React.ReactNode}) {
29+
const organization = useOrganization();
30+
const location = useLocation();
31+
/** Raw nav configuration values */
32+
const config = useMemo(() => createNavConfig({organization}), [organization]);
33+
/**
34+
* Active submenu items derived from the nav config and current `location`.
35+
* These are returned in a normalized layout format for ease of use.
36+
*/
37+
const submenu = useMemo<DeprecatedNavContext['submenu']>(() => {
38+
for (const item of config.main) {
39+
if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
40+
return normalizeSubmenu(item.submenu);
41+
}
42+
}
43+
if (config.footer) {
44+
for (const item of config.footer) {
45+
if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
46+
return normalizeSubmenu(item.submenu);
47+
}
48+
}
49+
}
50+
return undefined;
51+
}, [config, location]);
52+
53+
return (
54+
<DeprecatedNavContext.Provider value={{config, submenu}}>
55+
{children}
56+
</DeprecatedNavContext.Provider>
57+
);
58+
}
59+
60+
const normalizeSubmenu = (
61+
submenu: NavSidebarItem['submenu']
62+
): DeprecatedNavContext['submenu'] => {
63+
if (Array.isArray(submenu)) {
64+
return {main: submenu};
65+
}
66+
return submenu;
67+
};

static/app/components/nav/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styled from '@emotion/styled';
22

3-
import {NavContextProvider} from 'sentry/components/nav/context';
3+
import {DeprecatedNavContextProvider} from 'sentry/components/nav/contextDeprecated';
44
import MobileTopbar from 'sentry/components/nav/mobileTopbar';
55
import Sidebar from 'sentry/components/nav/sidebar';
66
import {useBreakpoints} from 'sentry/utils/metrics/useBreakpoints';
@@ -9,9 +9,9 @@ function Nav() {
99
const screen = useBreakpoints();
1010

1111
return (
12-
<NavContextProvider>
12+
<DeprecatedNavContextProvider>
1313
<NavContainer>{screen.medium ? <Sidebar /> : <MobileTopbar />}</NavContainer>
14-
</NavContextProvider>
14+
</DeprecatedNavContextProvider>
1515
);
1616
}
1717

static/app/components/nav/sidebar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {DropdownMenu} from 'sentry/components/dropdownMenu';
77
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
88
import Link from 'sentry/components/links/link';
99
import {linkStyles} from 'sentry/components/links/styles';
10-
import {useNavContext} from 'sentry/components/nav/context';
10+
import {useNavContextDeprecated} from 'sentry/components/nav/contextDeprecated';
1111
import Submenu from 'sentry/components/nav/submenu';
1212
import {
1313
isNavItemActive,
@@ -41,7 +41,7 @@ function Sidebar() {
4141
export default Sidebar;
4242

4343
export function SidebarItems() {
44-
const {config} = useNavContext();
44+
const {config} = useNavContextDeprecated();
4545
return (
4646
<Fragment>
4747
<SidebarBody>

static/app/components/nav/submenu.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
44
import Feature from 'sentry/components/acl/feature';
55
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
66
import Link from 'sentry/components/links/link';
7-
import {useNavContext} from 'sentry/components/nav/context';
7+
import {useNavContextDeprecated} from 'sentry/components/nav/contextDeprecated';
88
import type {NavSubmenuItem} from 'sentry/components/nav/utils';
99
import {
1010
isNavItemActive,
@@ -15,7 +15,7 @@ import {space} from 'sentry/styles/space';
1515
import {useLocation} from 'sentry/utils/useLocation';
1616

1717
function Submenu() {
18-
const nav = useNavContext();
18+
const nav = useNavContextDeprecated();
1919
if (!nav.submenu) {
2020
return null;
2121
}

static/app/views/organizationLayout/index.tsx

+13-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
33
import Footer from 'sentry/components/footer';
44
import HookOrDefault from 'sentry/components/hookOrDefault';
55
import Nav from 'sentry/components/nav';
6+
import {NavContextProvider} from 'sentry/components/nav/context';
67
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
78
import Sidebar from 'sentry/components/sidebar';
89
import type {Organization} from 'sentry/types/organization';
@@ -55,16 +56,18 @@ interface LayoutProps extends Props {
5556

5657
function AppLayout({children, organization}: LayoutProps) {
5758
return (
58-
<AppContainer className="app">
59-
<Nav />
60-
{/* The `#main` selector is used to make the app content `inert` when an overlay is active */}
61-
<BodyContainer id="main">
62-
{organization && <OrganizationHeader organization={organization} />}
63-
{organization && <DevToolInit />}
64-
<Body>{children}</Body>
65-
<Footer />
66-
</BodyContainer>
67-
</AppContainer>
59+
<NavContextProvider>
60+
<AppContainer className="app">
61+
<Nav />
62+
{/* The `#main` selector is used to make the app content `inert` when an overlay is active */}
63+
<BodyContainer id="main">
64+
{organization && <OrganizationHeader organization={organization} />}
65+
{organization && <DevToolInit />}
66+
<Body>{children}</Body>
67+
<Footer />
68+
</BodyContainer>
69+
</AppContainer>
70+
</NavContextProvider>
6871
);
6972
}
7073

0 commit comments

Comments
 (0)