Browse Source

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.
Malachi Willey 1 month ago
parent
commit
09c089f438

+ 16 - 51
static/app/components/nav/context.tsx

@@ -1,61 +1,26 @@
-import {createContext, useContext, useMemo} from 'react';
-
-import {createNavConfig} from 'sentry/components/nav/config';
-import type {
-  NavConfig,
-  NavItemLayout,
-  NavSidebarItem,
-  NavSubmenuItem,
-} from 'sentry/components/nav/utils';
-import {isNavItemActive, isSubmenuItemActive} from 'sentry/components/nav/utils';
-import {useLocation} from 'sentry/utils/useLocation';
-import useOrganization from 'sentry/utils/useOrganization';
+import {createContext, useContext, useMemo, useState} from 'react';
 
 export interface NavContext {
-  /** Raw config for entire nav items */
-  config: Readonly<NavConfig>;
-  /** Currently active submenu items, if any */
-  submenu?: Readonly<NavItemLayout<NavSubmenuItem>>;
+  secondaryNavEl: HTMLElement | null;
+  setSecondaryNavEl: (el: HTMLElement | null) => void;
 }
 
-const NavContext = createContext<NavContext>({config: {main: []}});
+const NavContext = createContext<NavContext>({
+  secondaryNavEl: null,
+  setSecondaryNavEl: () => {},
+});
 
 export function useNavContext(): NavContext {
-  const navContext = useContext(NavContext);
-  return navContext;
+  return useContext(NavContext);
 }
 
-export function NavContextProvider({children}: any) {
-  const organization = useOrganization();
-  const location = useLocation();
-  /** Raw nav configuration values */
-  const config = useMemo(() => createNavConfig({organization}), [organization]);
-  /**
-   * Active submenu items derived from the nav config and current `location`.
-   * These are returned in a normalized layout format for ease of use.
-   */
-  const submenu = useMemo<NavContext['submenu']>(() => {
-    for (const item of config.main) {
-      if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
-        return normalizeSubmenu(item.submenu);
-      }
-    }
-    if (config.footer) {
-      for (const item of config.footer) {
-        if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
-          return normalizeSubmenu(item.submenu);
-        }
-      }
-    }
-    return undefined;
-  }, [config, location]);
+export function NavContextProvider({children}: {children: React.ReactNode}) {
+  const [secondaryNavEl, setSecondaryNavEl] = useState<HTMLElement | null>(null);
 
-  return <NavContext.Provider value={{config, submenu}}>{children}</NavContext.Provider>;
-}
+  const value = useMemo(
+    () => ({secondaryNavEl, setSecondaryNavEl}),
+    [secondaryNavEl, setSecondaryNavEl]
+  );
 
-const normalizeSubmenu = (submenu: NavSidebarItem['submenu']): NavContext['submenu'] => {
-  if (Array.isArray(submenu)) {
-    return {main: submenu};
-  }
-  return submenu;
-};
+  return <NavContext.Provider value={value}>{children}</NavContext.Provider>;
+}

+ 67 - 0
static/app/components/nav/contextDeprecated.tsx

@@ -0,0 +1,67 @@
+import {createContext, useContext, useMemo} from 'react';
+
+import {createNavConfig} from 'sentry/components/nav/config';
+import type {
+  NavConfig,
+  NavItemLayout,
+  NavSidebarItem,
+  NavSubmenuItem,
+} from 'sentry/components/nav/utils';
+import {isNavItemActive, isSubmenuItemActive} from 'sentry/components/nav/utils';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+
+export interface DeprecatedNavContext {
+  /** Raw config for entire nav items */
+  config: Readonly<NavConfig>;
+  /** Currently active submenu items, if any */
+  submenu?: Readonly<NavItemLayout<NavSubmenuItem>>;
+}
+
+const DeprecatedNavContext = createContext<DeprecatedNavContext>({config: {main: []}});
+
+export function useNavContextDeprecated(): DeprecatedNavContext {
+  const navContext = useContext(DeprecatedNavContext);
+  return navContext;
+}
+
+export function DeprecatedNavContextProvider({children}: {children: React.ReactNode}) {
+  const organization = useOrganization();
+  const location = useLocation();
+  /** Raw nav configuration values */
+  const config = useMemo(() => createNavConfig({organization}), [organization]);
+  /**
+   * Active submenu items derived from the nav config and current `location`.
+   * These are returned in a normalized layout format for ease of use.
+   */
+  const submenu = useMemo<DeprecatedNavContext['submenu']>(() => {
+    for (const item of config.main) {
+      if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
+        return normalizeSubmenu(item.submenu);
+      }
+    }
+    if (config.footer) {
+      for (const item of config.footer) {
+        if (isNavItemActive(item, location) || isSubmenuItemActive(item, location)) {
+          return normalizeSubmenu(item.submenu);
+        }
+      }
+    }
+    return undefined;
+  }, [config, location]);
+
+  return (
+    <DeprecatedNavContext.Provider value={{config, submenu}}>
+      {children}
+    </DeprecatedNavContext.Provider>
+  );
+}
+
+const normalizeSubmenu = (
+  submenu: NavSidebarItem['submenu']
+): DeprecatedNavContext['submenu'] => {
+  if (Array.isArray(submenu)) {
+    return {main: submenu};
+  }
+  return submenu;
+};

+ 3 - 3
static/app/components/nav/index.tsx

@@ -1,6 +1,6 @@
 import styled from '@emotion/styled';
 
-import {NavContextProvider} from 'sentry/components/nav/context';
+import {DeprecatedNavContextProvider} from 'sentry/components/nav/contextDeprecated';
 import MobileTopbar from 'sentry/components/nav/mobileTopbar';
 import Sidebar from 'sentry/components/nav/sidebar';
 import {useBreakpoints} from 'sentry/utils/metrics/useBreakpoints';
@@ -9,9 +9,9 @@ function Nav() {
   const screen = useBreakpoints();
 
   return (
-    <NavContextProvider>
+    <DeprecatedNavContextProvider>
       <NavContainer>{screen.medium ? <Sidebar /> : <MobileTopbar />}</NavContainer>
-    </NavContextProvider>
+    </DeprecatedNavContextProvider>
   );
 }
 

+ 2 - 2
static/app/components/nav/sidebar.tsx

@@ -7,7 +7,7 @@ import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import InteractionStateLayer from 'sentry/components/interactionStateLayer';
 import Link from 'sentry/components/links/link';
 import {linkStyles} from 'sentry/components/links/styles';
-import {useNavContext} from 'sentry/components/nav/context';
+import {useNavContextDeprecated} from 'sentry/components/nav/contextDeprecated';
 import Submenu from 'sentry/components/nav/submenu';
 import {
   isNavItemActive,
@@ -41,7 +41,7 @@ function Sidebar() {
 export default Sidebar;
 
 export function SidebarItems() {
-  const {config} = useNavContext();
+  const {config} = useNavContextDeprecated();
   return (
     <Fragment>
       <SidebarBody>

+ 2 - 2
static/app/components/nav/submenu.tsx

@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
 import Feature from 'sentry/components/acl/feature';
 import InteractionStateLayer from 'sentry/components/interactionStateLayer';
 import Link from 'sentry/components/links/link';
-import {useNavContext} from 'sentry/components/nav/context';
+import {useNavContextDeprecated} from 'sentry/components/nav/contextDeprecated';
 import type {NavSubmenuItem} from 'sentry/components/nav/utils';
 import {
   isNavItemActive,
@@ -15,7 +15,7 @@ import {space} from 'sentry/styles/space';
 import {useLocation} from 'sentry/utils/useLocation';
 
 function Submenu() {
-  const nav = useNavContext();
+  const nav = useNavContextDeprecated();
   if (!nav.submenu) {
     return null;
   }

+ 13 - 10
static/app/views/organizationLayout/index.tsx

@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
 import Footer from 'sentry/components/footer';
 import HookOrDefault from 'sentry/components/hookOrDefault';
 import Nav from 'sentry/components/nav';
+import {NavContextProvider} from 'sentry/components/nav/context';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import Sidebar from 'sentry/components/sidebar';
 import type {Organization} from 'sentry/types/organization';
@@ -55,16 +56,18 @@ interface LayoutProps extends Props {
 
 function AppLayout({children, organization}: LayoutProps) {
   return (
-    <AppContainer className="app">
-      <Nav />
-      {/* The `#main` selector is used to make the app content `inert` when an overlay is active */}
-      <BodyContainer id="main">
-        {organization && <OrganizationHeader organization={organization} />}
-        {organization && <DevToolInit />}
-        <Body>{children}</Body>
-        <Footer />
-      </BodyContainer>
-    </AppContainer>
+    <NavContextProvider>
+      <AppContainer className="app">
+        <Nav />
+        {/* The `#main` selector is used to make the app content `inert` when an overlay is active */}
+        <BodyContainer id="main">
+          {organization && <OrganizationHeader organization={organization} />}
+          {organization && <DevToolInit />}
+          <Body>{children}</Body>
+          <Footer />
+        </BodyContainer>
+      </AppContainer>
+    </NavContextProvider>
   );
 }