From 425ac254b8df8cd1c536b11fe0ba902d0967747d Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 5 Mar 2024 10:15:54 -0500 Subject: [PATCH 01/19] wip --- static/app/components/sidebar/index.tsx | 240 +++++++++--------- .../components/sidebar/sidebarAccordion.tsx | 51 +++- .../components/sidebar/sidebarSmallScreen.tsx | 41 +++ 3 files changed, 214 insertions(+), 118 deletions(-) create mode 100644 static/app/components/sidebar/sidebarSmallScreen.tsx diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 62ad29e93ea8da..65a9aa6584bab9 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -11,6 +11,7 @@ import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext' import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig'; import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar'; import ReplaysOnboardingSidebar from 'sentry/components/replaysOnboarding/sidebar'; +import SidebarSmallScreen from 'sentry/components/sidebar/sidebarSmallScreen'; import {isDone} from 'sentry/components/sidebar/utils'; import { IconChevron, @@ -57,7 +58,7 @@ import Broadcasts from './broadcasts'; import SidebarHelp from './help'; import OnboardingStatus from './onboardingStatus'; import ServiceIncidents from './serviceIncidents'; -import {SidebarAccordion} from './sidebarAccordion'; +import {ExpandedContextProvider, SidebarAccordion} from './sidebarAccordion'; import SidebarDropdown from './sidebarDropdown'; import SidebarItem from './sidebarItem'; import type {SidebarOrientation} from './types'; @@ -509,137 +510,144 @@ function Sidebar() { return ( - - - - - {showSuperuserWarning() && !isExcludedOrg() && ( - - )} - - - - {hasOrganization && ( - - - {issues} - {projects} - - - - {performance} - {starfish} - {profiling} - {ddm} - {replays} - {feedback} - {monitors} - {alerts} - - - - {discover2} - {dashboards} - {releases} - {userFeedback} - - - - {stats} - {settings} - - - )} - - - - {hasOrganization && ( - - togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)} - hidePanel={() => hidePanel('performance-sidequest')} - {...sidebarItemProps} - /> - togglePanel(SidebarPanelKey.FEEDBACK_ONBOARDING)} - hidePanel={hidePanel} - {...sidebarItemProps} - /> - togglePanel(SidebarPanelKey.REPLAYS_ONBOARDING)} - hidePanel={hidePanel} - {...sidebarItemProps} - /> - togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)} - hidePanel={hidePanel} - {...sidebarItemProps} - /> - togglePanel(SidebarPanelKey.METRICS_ONBOARDING)} - hidePanel={hidePanel} - {...sidebarItemProps} - /> - - + + + + + + {showSuperuserWarning() && !isExcludedOrg() && ( + + )} + + + + {hasOrganization && ( + + + {issues} + {projects} + + + + {performance} + {starfish} + {profiling} + {ddm} + {replays} + {feedback} + {monitors} + {alerts} + + + + {discover2} + {dashboards} + {releases} + {userFeedback} + + + + {stats} + {settings} + + + )} + + + + {hasOrganization && ( + + togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)} + onShowPanel={() => togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)} + hidePanel={() => hidePanel('performance-sidequest')} + {...sidebarItemProps} + /> + togglePanel(SidebarPanelKey.FEEDBACK_ONBOARDING)} hidePanel={hidePanel} {...sidebarItemProps} /> - - - - {HookStore.get('sidebar:bottom-items').length > 0 && - HookStore.get('sidebar:bottom-items')[0]({ - orientation, - collapsed, - hasPanel, - organization, - })} - togglePanel(SidebarPanelKey.REPLAYS_ONBOARDING)} hidePanel={hidePanel} - organization={organization} + {...sidebarItemProps} /> - togglePanel(SidebarPanelKey.BROADCASTS)} + onShowPanel={() => togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)} hidePanel={hidePanel} - organization={organization} + {...sidebarItemProps} /> - togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)} + onShowPanel={() => togglePanel(SidebarPanelKey.METRICS_ONBOARDING)} hidePanel={hidePanel} + {...sidebarItemProps} /> - + + togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)} + hidePanel={hidePanel} + {...sidebarItemProps} + /> + - {!horizontal && ( - } - label={collapsed ? t('Expand') : t('Collapse')} - onClick={toggleCollapse} + {HookStore.get('sidebar:bottom-items').length > 0 && + HookStore.get('sidebar:bottom-items')[0]({ + orientation, + collapsed, + hasPanel, + organization, + })} + + togglePanel(SidebarPanelKey.BROADCASTS)} + hidePanel={hidePanel} + organization={organization} + /> + togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)} + hidePanel={hidePanel} /> - )} - - )} + + {!horizontal && ( + + + } + label={collapsed ? t('Expand') : t('Collapse')} + onClick={toggleCollapse} + /> + + )} + + )} + ); } diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 7d7eb39e6c7bc8..fdd8f32be2697b 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -1,4 +1,12 @@ -import {Children, isValidElement, useCallback} from 'react'; +import { + Children, + createContext, + isValidElement, + useCallback, + useContext, + useState, +} from 'react'; +import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; @@ -6,6 +14,7 @@ import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; +import useMedia from 'sentry/utils/useMedia'; import type {SidebarItemProps} from './sidebarItem'; import SidebarItem, {isItemActive} from './sidebarItem'; @@ -14,8 +23,28 @@ type SidebarAccordionProps = SidebarItemProps & { children?: React.ReactNode; }; +export const ExpandedContext = createContext<{ + items: React.ReactNode; + setItems: (items: React.ReactNode) => void; +}>({items: null, setItems: (_: React.ReactNode) => {}}); + +export function ExpandedContextProvider(props) { + const [items, setItems] = useState(null); + + return ( + + {props.children} + + ); +} + function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { + const {items, setItems} = useContext(ExpandedContext); + const theme = useTheme(); + const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); + const {id, collapsed: sidebarCollapsed} = itemProps; + const [expanded, setExpanded] = useLocalStorageState( `sidebar-accordion-${id}:expanded`, true @@ -44,6 +73,23 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { [expanded, setExpanded] ); + const handleMainItemClick = ( + _: string, + e: React.MouseEvent + ) => { + if (!horizontal && !sidebarCollapsed) { + setItems(null); + return; + } + + e.preventDefault(); + if (items === children) { + setItems(null); + } else { + setItems(children); + } + }; + return ( @@ -53,6 +99,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { id={mainItemId} aria-expanded={expanded} aria-owns={contentId} + onClick={handleMainItemClick} trailingItems={ - {expanded && ( + {expanded && !horizontal && !sidebarCollapsed && ( {children} diff --git a/static/app/components/sidebar/sidebarSmallScreen.tsx b/static/app/components/sidebar/sidebarSmallScreen.tsx new file mode 100644 index 00000000000000..87d9fee1a8d041 --- /dev/null +++ b/static/app/components/sidebar/sidebarSmallScreen.tsx @@ -0,0 +1,41 @@ +import {useContext} from 'react'; +import {useTheme} from '@emotion/react'; + +import {ExpandedContext} from 'sentry/components/sidebar/sidebarAccordion'; +import useMedia from 'sentry/utils/useMedia'; + +type Props = { + sidebarCollapsed: boolean; +}; + +function SidebarSmallScreen({sidebarCollapsed}: Props) { + const {items, setItems} = useContext(ExpandedContext); + const theme = useTheme(); + const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); + + const shouldResetExpandedId = !horizontal && !sidebarCollapsed; + + if (shouldResetExpandedId) { + setItems(null); + return null; + } + + if (!items) { + return null; + } + + return ( +
+ {items} +
+ ); +} + +export default SidebarSmallScreen; From 7d39207e0ab844912dceab871f1641f921519c81 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 13 Mar 2024 16:44:14 -0400 Subject: [PATCH 02/19] wip --- static/app/components/sidebar/index.tsx | 26 +++++--- .../components/sidebar/sidebarAccordion.tsx | 26 +++++--- static/app/components/sidebar/sidebarItem.tsx | 61 ++++++++++++++++--- .../components/sidebar/sidebarSmallScreen.tsx | 27 ++++++-- 4 files changed, 110 insertions(+), 30 deletions(-) diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 65a9aa6584bab9..299ca6ad269fbc 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -181,6 +181,15 @@ function Sidebar() { organization, }; + const isSmallSidebar = horizontal || collapsed; + + const nestedSidebarItemProps = { + ...sidebarItemProps, + isNested: true, + isSmall: isSmallSidebar, + orientation: 'left' as SidebarOrientation, + }; + const sidebarAnchor = isDemoWalkthrough() ? ( {t('Projects')} @@ -248,10 +257,11 @@ function Sidebar() { label={{t('Performance')}} to={`/organizations/${organization.slug}/performance/`} id="performance" + exact={!isSmallSidebar} > {t('Queries')} @@ -266,7 +276,7 @@ function Sidebar() { {t('HTTP')}} to={`/organizations/${organization.slug}/performance/http/`} id="performance-http" @@ -275,7 +285,7 @@ function Sidebar() { {t('Web Vitals')} @@ -288,7 +298,7 @@ function Sidebar() { {t('Resources')}} to={`/organizations/${organization.slug}/performance/browser/resources`} id="performance-browser-resources" @@ -345,10 +355,10 @@ function Sidebar() { label={{t('Starfish')}} to={`/organizations/${organization.slug}/starfish/`} id="starfish" - exact + exact={!isSmallSidebar} > {t('Interactions')}} to={`/organizations/${organization.slug}/performance/browser/interactions`} id="performance-browser-interactions" diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index fdd8f32be2697b..32cba85cacaf02 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -4,6 +4,7 @@ import { isValidElement, useCallback, useContext, + useRef, useState, } from 'react'; import {useTheme} from '@emotion/react'; @@ -26,20 +27,32 @@ type SidebarAccordionProps = SidebarItemProps & { export const ExpandedContext = createContext<{ items: React.ReactNode; setItems: (items: React.ReactNode) => void; -}>({items: null, setItems: (_: React.ReactNode) => {}}); + setTitle: (title: React.ReactNode) => void; + title: React.ReactNode; +}>({ + items: null, + setItems: (_: React.ReactNode) => {}, + setTitle: (_: React.ReactNode) => '', + title: '', +}); export function ExpandedContextProvider(props) { const [items, setItems] = useState(null); + const title = useRef(''); + + const setTitle = (newTitle: React.ReactNode) => { + title.current = newTitle; + }; return ( - + {props.children} ); } function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { - const {items, setItems} = useContext(ExpandedContext); + const {items, setItems, setTitle} = useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); @@ -77,7 +90,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { _: string, e: React.MouseEvent ) => { - if (!horizontal && !sidebarCollapsed) { + if ((!horizontal && !sidebarCollapsed) || !children) { setItems(null); return; } @@ -86,6 +99,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { if (items === children) { setItems(null); } else { + setTitle(itemProps.label); setItems(children); } }; @@ -204,8 +218,4 @@ const SidebarAccordionSubitemsWrap = styled('div')` display: flex; flex-direction: column; gap: 1px; - - @media (max-width: ${p => p.theme.breakpoints.medium}) { - flex-direction: row; - } `; diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 2768a89ce10773..ed49002ed56d09 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -83,6 +83,11 @@ export type SidebarItemProps = { * Additional badge letting users know a tab is in beta. */ isBeta?: boolean; + /** + * Is this item nested within another item + */ + isNested?: boolean; + /** * Specify the variant for the badge. */ @@ -91,6 +96,10 @@ export type SidebarItemProps = { * An optional prefix that can be used to reset the "new" indicator */ isNewSeenKeySuffix?: string; + /** + * Is the sidebar collapsed or in mobile view + */ + isSmall?: boolean; onClick?: (id: string, e: React.MouseEvent) => void; search?: string; to?: string; @@ -125,6 +134,8 @@ function SidebarItem({ onClick, trailingItems, variant, + isNested, + isSmall = false, ...props }: SidebarItemProps) { const router = useRouter(); @@ -178,9 +189,12 @@ function SidebarItem({ [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey] ); + const isInFloatingSidebar = isNested && isSmall; + const isInCollapsedState = !isInFloatingSidebar && collapsed; + return ( {label} {badges} @@ -191,6 +205,7 @@ function SidebarItem({ - + {icon} - {!collapsed && !isTop && ( + {!isInCollapsedState && !isTop && ( {label} @@ -208,21 +223,21 @@ function SidebarItem({ )} - {collapsed && showIsNew && ( + {isInCollapsedState && showIsNew && ( )} - {collapsed && isBeta && ( + {isInCollapsedState && isBeta && ( )} - {collapsed && isAlpha && ( + {isInCollapsedState && isAlpha && ( )} {badge !== undefined && badge > 0 && ( - {badge} + {badge} )} {trailingItems} @@ -269,10 +284,38 @@ export function isItemActive( export default SidebarItem; -const getActiveStyle = ({active, theme}: {active?: string; theme?: Theme}) => { +const getActiveStyle = ({ + active, + theme, + isSmall, +}: { + active?: string; + isSmall?: boolean; + theme?: Theme; +}) => { if (!active) { return ''; } + if (isSmall) { + return css` + color: ${theme?.green300}; + + &:active, + &:focus, + &:hover { + color: ${theme?.gray500}; + } + + &:before { + background-color: ${theme?.active}; + top: 0; + left: -15px; + width: 5px; + height: 30px; + border-radius: 0 3px 3px 0; + } + `; + } return css` color: ${theme?.white}; @@ -328,7 +371,7 @@ const StyledSidebarItem = styled(Link, { &:hover, &:focus-visible { - color: ${p => p.theme.white}; + color: ${p => (p.isSmall ? p.theme.gray500 : p.theme.white)}; } &:focus { diff --git a/static/app/components/sidebar/sidebarSmallScreen.tsx b/static/app/components/sidebar/sidebarSmallScreen.tsx index 87d9fee1a8d041..7523b1943b2cba 100644 --- a/static/app/components/sidebar/sidebarSmallScreen.tsx +++ b/static/app/components/sidebar/sidebarSmallScreen.tsx @@ -1,20 +1,27 @@ -import {useContext} from 'react'; +import {useContext, useRef} from 'react'; import {useTheme} from '@emotion/react'; +import styled from '@emotion/styled'; import {ExpandedContext} from 'sentry/components/sidebar/sidebarAccordion'; +import {space} from 'sentry/styles/space'; import useMedia from 'sentry/utils/useMedia'; +import useOnClickOutside from 'sentry/utils/useOnClickOutside'; type Props = { sidebarCollapsed: boolean; }; function SidebarSmallScreen({sidebarCollapsed}: Props) { - const {items, setItems} = useContext(ExpandedContext); + const {items, setItems, title} = useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); - + const panelRef = useRef(null); const shouldResetExpandedId = !horizontal && !sidebarCollapsed; + useOnClickOutside(panelRef, () => { + setItems(null); + }); + if (shouldResetExpandedId) { setItems(null); return null; @@ -28,14 +35,24 @@ function SidebarSmallScreen({sidebarCollapsed}: Props) {
+ {title} {items}
); } +const SidebarItemLabel = styled('span')` + color: ${p => p.theme.gray300}; + font-size: ${p => p.theme.fontSizeMedium}; + white-space: nowrap; +`; + export default SidebarSmallScreen; From a8d9145be3b094e53db35e63742918108db149ec Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 22 Mar 2024 15:33:48 -0400 Subject: [PATCH 03/19] wip --- static/app/components/sidebar/index.tsx | 2 - .../components/sidebar/sidebarAccordion.tsx | 63 +++++++++++-------- static/app/components/sidebar/sidebarItem.tsx | 7 ++- .../components/sidebar/sidebarSmallScreen.tsx | 58 ----------------- 4 files changed, 43 insertions(+), 87 deletions(-) delete mode 100644 static/app/components/sidebar/sidebarSmallScreen.tsx diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 299ca6ad269fbc..1c3a2282eea589 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -11,7 +11,6 @@ import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext' import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig'; import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar'; import ReplaysOnboardingSidebar from 'sentry/components/replaysOnboarding/sidebar'; -import SidebarSmallScreen from 'sentry/components/sidebar/sidebarSmallScreen'; import {isDone} from 'sentry/components/sidebar/utils'; import { IconChevron, @@ -521,7 +520,6 @@ function Sidebar() { return ( - void; - setTitle: (title: React.ReactNode) => void; - title: React.ReactNode; + openMainItemId: string | null; + setOpenMainItem: (mainItemId: string | null) => void; }>({ - items: null, - setItems: (_: React.ReactNode) => {}, - setTitle: (_: React.ReactNode) => '', - title: '', + openMainItemId: null, + setOpenMainItem: () => {}, }); export function ExpandedContextProvider(props) { - const [items, setItems] = useState(null); - const title = useRef(''); - - const setTitle = (newTitle: React.ReactNode) => { - title.current = newTitle; - }; + const [openMainItemId, setOpenMainItem] = useState(null); return ( - + {props.children} ); } function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { - const {items, setItems, setTitle} = useContext(ExpandedContext); + const {id, collapsed: sidebarCollapsed} = itemProps; + const accoridonRef = useRef(null); + const {openMainItemId, setOpenMainItem} = useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); - - const {id, collapsed: sidebarCollapsed} = itemProps; - const [expanded, setExpanded] = useLocalStorageState( `sidebar-accordion-${id}:expanded`, true @@ -65,6 +55,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { const mainItemId = `sidebar-accordion-${id}-item`; const contentId = `sidebar-accordion-${id}-content`; + const isOpenInFloatingSidebar = openMainItemId === mainItemId; const isActive = isItemActive(itemProps); @@ -91,21 +82,20 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { e: React.MouseEvent ) => { if ((!horizontal && !sidebarCollapsed) || !children) { - setItems(null); + setOpenMainItem(null); return; } e.preventDefault(); - if (items === children) { - setItems(null); + if (isOpenInFloatingSidebar) { + setOpenMainItem(null); } else { - setTitle(itemProps.label); - setItems(children); + setOpenMainItem(mainItemId); } }; return ( - + )} + {isOpenInFloatingSidebar && (horizontal || sidebarCollapsed) && ( +
+ {itemProps.label} + {children} +
+ )}
); } @@ -176,6 +183,12 @@ function findChildElementsInTree( return found; } +const SidebarItemLabel = styled('span')` + color: ${p => p.theme.gray300}; + font-size: ${p => p.theme.fontSizeMedium}; + white-space: nowrap; +`; + const SidebarAccordionWrapper = styled('div')` display: flex; flex-direction: column; diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index ed49002ed56d09..7d02759b61aefe 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -1,4 +1,4 @@ -import {Fragment, isValidElement, useCallback, useMemo} from 'react'; +import {Fragment, isValidElement, useCallback, useContext, useMemo} from 'react'; import isPropValid from '@emotion/is-prop-valid'; import type {Theme} from '@emotion/react'; import {css} from '@emotion/react'; @@ -10,6 +10,7 @@ import HookOrDefault from 'sentry/components/hookOrDefault'; import InteractionStateLayer from 'sentry/components/interactionStateLayer'; import Link from 'sentry/components/links/link'; import {Flex} from 'sentry/components/profiling/flex'; +import {ExpandedContext} from 'sentry/components/sidebar/sidebarAccordion'; import TextOverflow from 'sentry/components/textOverflow'; import {Tooltip} from 'sentry/components/tooltip'; import {space} from 'sentry/styles/space'; @@ -138,6 +139,7 @@ function SidebarItem({ isSmall = false, ...props }: SidebarItemProps) { + const {setOpenMainItem} = useContext(ExpandedContext); const router = useRouter(); // label might be wrapped in a guideAnchor let labelString = label; @@ -181,12 +183,13 @@ function SidebarItem({ const handleItemClick = useCallback( (event: React.MouseEvent) => { + setOpenMainItem(null); !(to || href) && event.preventDefault(); recordAnalytics(); onClick?.(id, event); showIsNew && localStorage.setItem(isNewSeenKey, 'true'); }, - [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey] + [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setOpenMainItem] ); const isInFloatingSidebar = isNested && isSmall; diff --git a/static/app/components/sidebar/sidebarSmallScreen.tsx b/static/app/components/sidebar/sidebarSmallScreen.tsx deleted file mode 100644 index 7523b1943b2cba..00000000000000 --- a/static/app/components/sidebar/sidebarSmallScreen.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import {useContext, useRef} from 'react'; -import {useTheme} from '@emotion/react'; -import styled from '@emotion/styled'; - -import {ExpandedContext} from 'sentry/components/sidebar/sidebarAccordion'; -import {space} from 'sentry/styles/space'; -import useMedia from 'sentry/utils/useMedia'; -import useOnClickOutside from 'sentry/utils/useOnClickOutside'; - -type Props = { - sidebarCollapsed: boolean; -}; - -function SidebarSmallScreen({sidebarCollapsed}: Props) { - const {items, setItems, title} = useContext(ExpandedContext); - const theme = useTheme(); - const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); - const panelRef = useRef(null); - const shouldResetExpandedId = !horizontal && !sidebarCollapsed; - - useOnClickOutside(panelRef, () => { - setItems(null); - }); - - if (shouldResetExpandedId) { - setItems(null); - return null; - } - - if (!items) { - return null; - } - - return ( -
- {title} - {items} -
- ); -} - -const SidebarItemLabel = styled('span')` - color: ${p => p.theme.gray300}; - font-size: ${p => p.theme.fontSizeMedium}; - white-space: nowrap; -`; - -export default SidebarSmallScreen; From 11c8d5115955696a6e6af2680b1e3e8a40f6a1b0 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Mon, 25 Mar 2024 13:05:25 -0400 Subject: [PATCH 04/19] add animation --- static/app/components/sidebar/index.tsx | 8 ++-- .../components/sidebar/sidebarAccordion.tsx | 48 +++++++++++++------ static/app/components/sidebar/sidebarItem.tsx | 26 +++++----- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 1c3a2282eea589..e439f3d579b6d4 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -180,12 +180,12 @@ function Sidebar() { organization, }; - const isSmallSidebar = horizontal || collapsed; + const isFloatingSidebar = horizontal || collapsed; const nestedSidebarItemProps = { ...sidebarItemProps, isNested: true, - isSmall: isSmallSidebar, + isFloatingSidebar, orientation: 'left' as SidebarOrientation, }; @@ -256,7 +256,7 @@ function Sidebar() { label={{t('Performance')}} to={`/organizations/${organization.slug}/performance/`} id="performance" - exact={!isSmallSidebar} + exact={!isFloatingSidebar} > {t('Starfish')}} to={`/organizations/${organization.slug}/starfish/`} id="starfish" - exact={!isSmallSidebar} + exact={!isFloatingSidebar} > )} {isOpenInFloatingSidebar && (horizontal || sidebarCollapsed) && ( -
+ {itemProps.label} {children} -
+ )}
); @@ -183,12 +172,41 @@ function findChildElementsInTree( return found; } -const SidebarItemLabel = styled('span')` +const SidebarItemLabel = styled('div')` color: ${p => p.theme.gray300}; - font-size: ${p => p.theme.fontSizeMedium}; + padding: ${space(1)} 0 ${space(1)} 18px; + font-size: ${p => p.theme.fontSizeLarge}; white-space: nowrap; `; +const FloatingSidebar = styled('div')<{ + accordionRef: React.RefObject; + horizontal: boolean; +}>` + position: absolute; + width: ${p => (p.horizontal ? '100%' : '200px')}; + padding: ${space(2)}; + top: ${p => + p.horizontal + ? p.theme.sidebar.mobileHeight + : p.accordionRef.current?.getBoundingClientRect().top}; + left: ${p => + p.horizontal ? 0 : `calc(${p.theme.sidebar.collapsedWidth} + ${space(1)})`}; + background-color: white; + + animation: fadeIn 0.3s ease-in-out; + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + ${p => !p.horizontal && `box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);`} +`; + const SidebarAccordionWrapper = styled('div')` display: flex; flex-direction: column; diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 7d02759b61aefe..594307b98a2611 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -84,11 +84,15 @@ export type SidebarItemProps = { * Additional badge letting users know a tab is in beta. */ isBeta?: boolean; + /** + * Is the sidebar collapsed or in mobile view + */ + isFloatingSidebar?: boolean; + /** * Is this item nested within another item */ isNested?: boolean; - /** * Specify the variant for the badge. */ @@ -97,10 +101,6 @@ export type SidebarItemProps = { * An optional prefix that can be used to reset the "new" indicator */ isNewSeenKeySuffix?: string; - /** - * Is the sidebar collapsed or in mobile view - */ - isSmall?: boolean; onClick?: (id: string, e: React.MouseEvent) => void; search?: string; to?: string; @@ -136,7 +136,7 @@ function SidebarItem({ trailingItems, variant, isNested, - isSmall = false, + isFloatingSidebar = false, ...props }: SidebarItemProps) { const {setOpenMainItem} = useContext(ExpandedContext); @@ -192,7 +192,7 @@ function SidebarItem({ [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setOpenMainItem] ); - const isInFloatingSidebar = isNested && isSmall; + const isInFloatingSidebar = isNested && isFloatingSidebar; const isInCollapsedState = !isInFloatingSidebar && collapsed; return ( @@ -208,7 +208,7 @@ function SidebarItem({ { if (!active) { return ''; } - if (isSmall) { + if (isFloatingSidebar) { return css` - color: ${theme?.green300}; + color: ${theme?.gray500}; &:active, &:focus, @@ -374,7 +374,7 @@ const StyledSidebarItem = styled(Link, { &:hover, &:focus-visible { - color: ${p => (p.isSmall ? p.theme.gray500 : p.theme.white)}; + color: ${p => (p.isFloatingSidebar ? p.theme.gray500 : p.theme.white)}; } &:focus { From dfcee616095b50dea4fec0106964bea18ddd01d4 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Mon, 25 Mar 2024 14:27:05 -0400 Subject: [PATCH 05/19] fix title menu thing --- .../components/sidebar/sidebarAccordion.tsx | 29 +++++++++++++++++-- static/app/components/sidebar/sidebarItem.tsx | 4 ++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 18b529914f2243..7eef9efc429216 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -2,6 +2,7 @@ import { Children, createContext, isValidElement, + type MouseEventHandler, useCallback, useContext, useRef, @@ -16,6 +17,8 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; import useMedia from 'sentry/utils/useMedia'; +import useOnClickOutside from 'sentry/utils/useOnClickOutside'; +import useRouter from 'sentry/utils/useRouter'; import type {SidebarItemProps} from './sidebarItem'; import SidebarItem, {isItemActive} from './sidebarItem'; @@ -44,14 +47,20 @@ export function ExpandedContextProvider(props) { function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { const {id, collapsed: sidebarCollapsed} = itemProps; + const accoridonRef = useRef(null); + const floatingSidebarRef = useRef(null); const {openMainItemId, setOpenMainItem} = useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); + const router = useRouter(); const [expanded, setExpanded] = useLocalStorageState( `sidebar-accordion-${id}:expanded`, true ); + useOnClickOutside(floatingSidebarRef, () => { + setOpenMainItem(null); + }); const mainItemId = `sidebar-accordion-${id}-item`; const contentId = `sidebar-accordion-${id}-content`; @@ -94,6 +103,13 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { } }; + const handleTitleClick: MouseEventHandler = () => { + if (itemProps.to) { + router.push(itemProps.to); + setOpenMainItem(null); + } + }; + return ( @@ -128,8 +144,14 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { )} {isOpenInFloatingSidebar && (horizontal || sidebarCollapsed) && ( - - {itemProps.label} + + + {itemProps.label} + {children} )} @@ -177,6 +199,9 @@ const SidebarItemLabel = styled('div')` padding: ${space(1)} 0 ${space(1)} 18px; font-size: ${p => p.theme.fontSizeLarge}; white-space: nowrap; + &:hover { + cursor: pointer; + } `; const FloatingSidebar = styled('div')<{ diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 594307b98a2611..734ccbd994eae0 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -281,7 +281,9 @@ export function isItemActive( (item?.label === 'Alerts' && location.pathname.includes('/alerts/') && !location.pathname.startsWith('/settings/')) || - (item?.label === 'Releases' && location.pathname.includes('/release-thresholds/')) + (item?.label === 'Releases' && location.pathname.includes('/release-thresholds/')) || + (item?.label === 'Performance' && location.pathname.includes('/performance/')) || + (item?.label === 'Starfish' && location.pathname.includes('/starfish/')) ); } From f2bfe9548e129d3f84727d1e0d346f73cf06bca7 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Mon, 25 Mar 2024 14:28:38 -0400 Subject: [PATCH 06/19] add box shadow in mobile --- static/app/components/sidebar/sidebarAccordion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 7eef9efc429216..528eb0009fb5fb 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -229,7 +229,7 @@ const FloatingSidebar = styled('div')<{ } } - ${p => !p.horizontal && `box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);`} + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); `; const SidebarAccordionWrapper = styled('div')` From f8c96cc090f722df20ef105ccf1bb5518d5ba99d Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 5 Apr 2024 16:33:05 -0400 Subject: [PATCH 07/19] update api --- .../sidebar/expandedContextProvider.tsx | 32 +++++++++++ static/app/components/sidebar/index.tsx | 34 ++++++------ .../components/sidebar/sidebarAccordion.tsx | 53 ++++++++++--------- static/app/components/sidebar/sidebarItem.tsx | 25 ++++----- 4 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 static/app/components/sidebar/expandedContextProvider.tsx diff --git a/static/app/components/sidebar/expandedContextProvider.tsx b/static/app/components/sidebar/expandedContextProvider.tsx new file mode 100644 index 00000000000000..9a163a9a0081f7 --- /dev/null +++ b/static/app/components/sidebar/expandedContextProvider.tsx @@ -0,0 +1,32 @@ +import {createContext, useState} from 'react'; +import {useTheme} from '@emotion/react'; + +import PreferencesStore from 'sentry/stores/preferencesStore'; +import {useLegacyStore} from 'sentry/stores/useLegacyStore'; +import useMedia from 'sentry/utils/useMedia'; + +export const ExpandedContext = createContext<{ + openMainItemId: string | null; + setOpenMainItem: (mainItemId: string | null) => void; + shouldAccordionFloat: boolean; +}>({ + openMainItemId: null, + setOpenMainItem: () => {}, + shouldAccordionFloat: false, +}); + +export function ExpandedContextProvider(props) { + const [openMainItemId, setOpenMainItem] = useState(null); + const theme = useTheme(); + const preferences = useLegacyStore(PreferencesStore); + const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); + const shouldAccordionFloat = horizontal || !!preferences.collapsed; + + return ( + + {props.children} + + ); +} diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 4fbdaf80a17cf8..e9335f2202ca19 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -11,6 +11,10 @@ import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext' import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig'; import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar'; import ReplaysOnboardingSidebar from 'sentry/components/replaysOnboarding/sidebar'; +import { + ExpandedContext, + ExpandedContextProvider, +} from 'sentry/components/sidebar/expandedContextProvider'; import {isDone} from 'sentry/components/sidebar/utils'; import { IconChevron, @@ -59,7 +63,7 @@ import Broadcasts from './broadcasts'; import SidebarHelp from './help'; import OnboardingStatus from './onboardingStatus'; import ServiceIncidents from './serviceIncidents'; -import {ExpandedContextProvider, SidebarAccordion} from './sidebarAccordion'; +import {SidebarAccordion} from './sidebarAccordion'; import SidebarDropdown from './sidebarDropdown'; import SidebarItem from './sidebarItem'; import type {SidebarOrientation} from './types'; @@ -115,6 +119,7 @@ function Sidebar() { const preferences = useLegacyStore(PreferencesStore); const activePanel = useLegacyStore(SidebarPanelStore); const organization = useOrganization({allowNull: true}); + const {shouldAccordionFloat} = useContext(ExpandedContext); const collapsed = !!preferences.collapsed; const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); @@ -182,15 +187,6 @@ function Sidebar() { organization, }; - const isFloatingSidebar = horizontal || collapsed; - - const nestedSidebarItemProps = { - ...sidebarItemProps, - isNested: true, - isFloatingSidebar, - orientation: 'left' as SidebarOrientation, - }; - const sidebarAnchor = isDemoWalkthrough() ? ( {t('Projects')} @@ -258,11 +254,11 @@ function Sidebar() { label={{t('Performance')}} to={`/organizations/${organization.slug}/performance/`} id="performance" - exact={!isFloatingSidebar} + exact={!shouldAccordionFloat} > {t('Queries')} @@ -277,7 +273,7 @@ function Sidebar() { {t('HTTP')}} to={`/organizations/${organization.slug}/performance/http/`} id="performance-http" @@ -287,7 +283,7 @@ function Sidebar() {
{t('Web Vitals')} @@ -300,7 +296,7 @@ function Sidebar() { {t('Resources')}} to={`/organizations/${organization.slug}/performance/browser/resources`} id="performance-browser-resources" @@ -357,10 +353,10 @@ function Sidebar() { label={{t('Starfish')}} to={`/organizations/${organization.slug}/starfish/`} id="starfish" - exact={!isFloatingSidebar} + exact={!shouldAccordionFloat} > {t('Interactions')}} to={`/organizations/${organization.slug}/performance/browser/interactions`} id="performance-browser-interactions" diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 528eb0009fb5fb..de43571e0208a2 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -1,17 +1,19 @@ import { Children, - createContext, + cloneElement, isValidElement, type MouseEventHandler, + type ReactElement, + type ReactNode, useCallback, useContext, useRef, - useState, } from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; +import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -27,28 +29,10 @@ type SidebarAccordionProps = SidebarItemProps & { children?: React.ReactNode; }; -export const ExpandedContext = createContext<{ - openMainItemId: string | null; - setOpenMainItem: (mainItemId: string | null) => void; -}>({ - openMainItemId: null, - setOpenMainItem: () => {}, -}); - -export function ExpandedContextProvider(props) { - const [openMainItemId, setOpenMainItem] = useState(null); - - return ( - - {props.children} - - ); -} - function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { const {id, collapsed: sidebarCollapsed} = itemProps; - const accoridonRef = useRef(null); + const accordionRef = useRef(null); const floatingSidebarRef = useRef(null); const {openMainItemId, setOpenMainItem} = useContext(ExpandedContext); const theme = useTheme(); @@ -78,6 +62,8 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { return false; }); + const childrenWithProps = renderChildrenWithProps(children); + const handleExpandAccordionClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); @@ -111,7 +97,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { }; return ( - + {expanded && !horizontal && !sidebarCollapsed && ( - {children} + {childrenWithProps} )} {isOpenInFloatingSidebar && (horizontal || sidebarCollapsed) && ( {itemProps.label} - {children} + {childrenWithProps} )} @@ -161,6 +147,23 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { export {SidebarAccordion}; +const renderChildrenWithProps = (children: ReactNode): ReactNode => { + const propsToAdd: Partial = { + isNested: true, + }; + + return Children.map(children, child => { + if (!isValidElement(child)) { + return child; + } + // Clone child with common prop and recursively render its children + return cloneElement(child as ReactElement, { + ...propsToAdd, + children: renderChildrenWithProps((child as ReactElement).props.children), + }); + }); +}; + function findChildElementsInTree( children: React.ReactNode, componentName: string, diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 734ccbd994eae0..1dfc80f0aaabe9 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -10,7 +10,7 @@ import HookOrDefault from 'sentry/components/hookOrDefault'; import InteractionStateLayer from 'sentry/components/interactionStateLayer'; import Link from 'sentry/components/links/link'; import {Flex} from 'sentry/components/profiling/flex'; -import {ExpandedContext} from 'sentry/components/sidebar/sidebarAccordion'; +import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider'; import TextOverflow from 'sentry/components/textOverflow'; import {Tooltip} from 'sentry/components/tooltip'; import {space} from 'sentry/styles/space'; @@ -84,15 +84,11 @@ export type SidebarItemProps = { * Additional badge letting users know a tab is in beta. */ isBeta?: boolean; - /** - * Is the sidebar collapsed or in mobile view - */ - isFloatingSidebar?: boolean; - /** * Is this item nested within another item */ isNested?: boolean; + /** * Specify the variant for the badge. */ @@ -136,10 +132,9 @@ function SidebarItem({ trailingItems, variant, isNested, - isFloatingSidebar = false, ...props }: SidebarItemProps) { - const {setOpenMainItem} = useContext(ExpandedContext); + const {setOpenMainItem, shouldAccordionFloat} = useContext(ExpandedContext); const router = useRouter(); // label might be wrapped in a guideAnchor let labelString = label; @@ -151,7 +146,7 @@ function SidebarItem({ !hasPanel && router && isItemActive({to, label: labelString}, exact); const isActive = defined(active) ? active : isActiveRouter; - const isTop = orientation === 'top'; + const isTop = orientation === 'top' && !isNested; const placement = isTop ? 'bottom' : 'right'; const seenSuffix = isNewSeenKeySuffix ?? ''; @@ -192,7 +187,7 @@ function SidebarItem({ [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setOpenMainItem] ); - const isInFloatingSidebar = isNested && isFloatingSidebar; + const isInFloatingSidebar = isNested && shouldAccordionFloat; const isInCollapsedState = !isInFloatingSidebar && collapsed; return ( @@ -208,7 +203,7 @@ function SidebarItem({ { if (!active) { return ''; } - if (isFloatingSidebar) { + if (isInFloatingAccordion) { return css` color: ${theme?.gray500}; @@ -376,7 +371,7 @@ const StyledSidebarItem = styled(Link, { &:hover, &:focus-visible { - color: ${p => (p.isFloatingSidebar ? p.theme.gray500 : p.theme.white)}; + color: ${p => (p.isInFloatingAccordion ? p.theme.gray500 : p.theme.white)}; } &:focus { From 2939bbcb0e12ecf69d2233897ab43e8288c07bdd Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 5 Apr 2024 16:39:19 -0400 Subject: [PATCH 08/19] code review --- .../components/sidebar/expandedContextProvider.tsx | 13 +++++++------ static/app/components/sidebar/sidebarAccordion.tsx | 14 +++++++------- static/app/components/sidebar/sidebarItem.tsx | 6 +++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/static/app/components/sidebar/expandedContextProvider.tsx b/static/app/components/sidebar/expandedContextProvider.tsx index 9a163a9a0081f7..45a63055f0be80 100644 --- a/static/app/components/sidebar/expandedContextProvider.tsx +++ b/static/app/components/sidebar/expandedContextProvider.tsx @@ -6,17 +6,18 @@ import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import useMedia from 'sentry/utils/useMedia'; export const ExpandedContext = createContext<{ - openMainItemId: string | null; - setOpenMainItem: (mainItemId: string | null) => void; + expandedItemId: string | null; + setExpandedItemId: (mainItemId: string | null) => void; shouldAccordionFloat: boolean; }>({ - openMainItemId: null, - setOpenMainItem: () => {}, + expandedItemId: null, + setExpandedItemId: () => {}, shouldAccordionFloat: false, }); +// Provides the expanded context to the sidebar accordion when it's in the floating state only (collapsed sidebar or on mobile view) export function ExpandedContextProvider(props) { - const [openMainItemId, setOpenMainItem] = useState(null); + const [expandedItemId, setExpandedItemId] = useState(null); const theme = useTheme(); const preferences = useLegacyStore(PreferencesStore); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); @@ -24,7 +25,7 @@ export function ExpandedContextProvider(props) { return ( {props.children} diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index de43571e0208a2..678e3d6fb0de94 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -34,7 +34,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { const accordionRef = useRef(null); const floatingSidebarRef = useRef(null); - const {openMainItemId, setOpenMainItem} = useContext(ExpandedContext); + const {expandedItemId, setExpandedItemId} = useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); const router = useRouter(); @@ -43,12 +43,12 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { true ); useOnClickOutside(floatingSidebarRef, () => { - setOpenMainItem(null); + setExpandedItemId(null); }); const mainItemId = `sidebar-accordion-${id}-item`; const contentId = `sidebar-accordion-${id}-content`; - const isOpenInFloatingSidebar = openMainItemId === mainItemId; + const isOpenInFloatingSidebar = expandedItemId === mainItemId; const isActive = isItemActive(itemProps); @@ -77,22 +77,22 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { e: React.MouseEvent ) => { if ((!horizontal && !sidebarCollapsed) || !children) { - setOpenMainItem(null); + setExpandedItemId(null); return; } e.preventDefault(); if (isOpenInFloatingSidebar) { - setOpenMainItem(null); + setExpandedItemId(null); } else { - setOpenMainItem(mainItemId); + setExpandedItemId(mainItemId); } }; const handleTitleClick: MouseEventHandler = () => { if (itemProps.to) { router.push(itemProps.to); - setOpenMainItem(null); + setExpandedItemId(null); } }; diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 1dfc80f0aaabe9..b297ff258cae16 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -134,7 +134,7 @@ function SidebarItem({ isNested, ...props }: SidebarItemProps) { - const {setOpenMainItem, shouldAccordionFloat} = useContext(ExpandedContext); + const {setExpandedItemId, shouldAccordionFloat} = useContext(ExpandedContext); const router = useRouter(); // label might be wrapped in a guideAnchor let labelString = label; @@ -178,13 +178,13 @@ function SidebarItem({ const handleItemClick = useCallback( (event: React.MouseEvent) => { - setOpenMainItem(null); + setExpandedItemId(null); !(to || href) && event.preventDefault(); recordAnalytics(); onClick?.(id, event); showIsNew && localStorage.setItem(isNewSeenKey, 'true'); }, - [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setOpenMainItem] + [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setExpandedItemId] ); const isInFloatingSidebar = isNested && shouldAccordionFloat; From eab43d86fa1ccaa8a4c791059c033d15ab00c7dd Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Mon, 8 Apr 2024 12:01:31 -0400 Subject: [PATCH 09/19] fix button retriggers annimation --- .../components/sidebar/sidebarAccordion.tsx | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 678e3d6fb0de94..aacc879baedef7 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -33,7 +33,8 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { const {id, collapsed: sidebarCollapsed} = itemProps; const accordionRef = useRef(null); - const floatingSidebarRef = useRef(null); + const mainItemRef = useRef(null); + const floatingAccordionRef = useRef(null); const {expandedItemId, setExpandedItemId} = useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); @@ -42,7 +43,11 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { `sidebar-accordion-${id}:expanded`, true ); - useOnClickOutside(floatingSidebarRef, () => { + + useOnClickOutside(floatingAccordionRef, e => { + if (mainItemRef?.current?.contains(e.target as Node)) { + return; + } setExpandedItemId(null); }); @@ -99,30 +104,32 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { return ( - - - - } - /> +
+ + + + } + /> +
{expanded && !horizontal && !sidebarCollapsed && ( @@ -130,16 +137,16 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { )} {isOpenInFloatingSidebar && (horizontal || sidebarCollapsed) && ( - {itemProps.label} {childrenWithProps} - + )}
); @@ -207,7 +214,7 @@ const SidebarItemLabel = styled('div')` } `; -const FloatingSidebar = styled('div')<{ +const FloatingAccordion = styled('div')<{ accordionRef: React.RefObject; horizontal: boolean; }>` From 06480fbd1bc1fa95b121fbd053e9bf492ad37995 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Mon, 8 Apr 2024 16:26:35 -0400 Subject: [PATCH 10/19] add basic tests --- static/app/components/sidebar/index.spec.tsx | 31 +++++++++++++++++++ .../components/sidebar/sidebarAccordion.tsx | 3 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/static/app/components/sidebar/index.spec.tsx b/static/app/components/sidebar/index.spec.tsx index 84bf3e39e94663..71f74e0e4d28f3 100644 --- a/static/app/components/sidebar/index.spec.tsx +++ b/static/app/components/sidebar/index.spec.tsx @@ -14,6 +14,13 @@ import type {Organization, SentryServiceStatus} from 'sentry/types'; jest.mock('sentry/actionCreators/serviceIncidents'); +const sidebarAccordionFeatures = [ + 'performance-view', + 'performance-database-view', + 'performance-cache-view', + 'performance-http', +]; + describe('Sidebar', function () { const {organization, routerContext} = initializeOrg(); const broadcast = BroadcastFixture(); @@ -267,4 +274,28 @@ describe('Sidebar', function () { await userEvent.click(screen.getByTestId('sidebar-collapse')); expect(await screen.findByText(organization.name)).toBeInTheDocument(); }); + + describe('when the accordion is used', () => { + const renderSidebarWithFeatures = () => { + renderSidebar({ + organization: { + ...organization, + features: [...organization.features, ...sidebarAccordionFeatures], + }, + }); + }; + + it('should not render floating accordion when expanded', async () => { + renderSidebarWithFeatures(); + await userEvent.click(screen.getByTestId('sidebar-accordion-performance-item')); + expect(screen.queryByTestId('floating-accordion')).not.toBeInTheDocument(); + }); + + it('should render floating accordion when collapsed', async () => { + renderSidebarWithFeatures(); + await userEvent.click(screen.getByTestId('sidebar-collapse')); + await userEvent.click(screen.getByTestId('sidebar-accordion-performance-item')); + expect(await screen.findByTestId('floating-accordion')).toBeInTheDocument(); + }); + }); }); diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index aacc879baedef7..5922b545d0ff69 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -109,6 +109,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { {...itemProps} active={isActive && !hasActiveChildren} id={mainItemId} + data-test-id={mainItemId} aria-expanded={expanded} aria-owns={contentId} onClick={handleMainItemClick} @@ -141,6 +142,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { accordionRef={accordionRef} horizontal={horizontal} ref={floatingAccordionRef} + data-test-id="floating-accordion" > {itemProps.label} @@ -163,7 +165,6 @@ const renderChildrenWithProps = (children: ReactNode): ReactNode => { if (!isValidElement(child)) { return child; } - // Clone child with common prop and recursively render its children return cloneElement(child as ReactElement, { ...propsToAdd, children: renderChildrenWithProps((child as ReactElement).props.children), From b86af1599208b7dc97bb5b047ce9990db3517e14 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:04:47 +0000 Subject: [PATCH 11/19] :hammer_and_wrench: apply pre-commit fixes --- static/app/components/sidebar/sidebarAccordion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 4843f244d6c2d2..cb15e9984a9e7d 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -122,7 +122,7 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { aria-label={expanded ? t('Collapse') : t('Expand')} sidebarCollapsed={sidebarCollapsed} > - + } /> From 7572f1ecdd1bf46b56705aa3456704da5de20f2b Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 12 Apr 2024 12:15:44 -0400 Subject: [PATCH 12/19] update --- static/app/components/sidebar/index.tsx | 38 ++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index f32a74865bcaf7..51f79f3cab4c1a 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -658,13 +658,33 @@ function Sidebar() { - } - label={collapsed ? t('Expand') : t('Collapse')} - onClick={toggleCollapse} + {HookStore.get('sidebar:bottom-items').length > 0 && + HookStore.get('sidebar:bottom-items')[0]({ + orientation, + collapsed, + hasPanel, + organization, + })} + + togglePanel(SidebarPanelKey.BROADCASTS)} + hidePanel={hidePanel} + organization={organization} + /> + togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)} + hidePanel={hidePanel} /> @@ -674,9 +694,7 @@ function Sidebar() { id="collapse" data-test-id="sidebar-collapse" {...sidebarItemProps} - icon={ - - } + icon={} label={collapsed ? t('Expand') : t('Collapse')} onClick={toggleCollapse} /> From d4ab2c1194f650a1b894b773bcf9d6a9b46506aa Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:17:06 +0000 Subject: [PATCH 13/19] :hammer_and_wrench: apply eslint style fixes --- static/app/components/sidebar/sidebarAccordion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index cb15e9984a9e7d..02ea5150cd62c5 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -13,8 +13,8 @@ import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; -import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider'; import {Chevron} from 'sentry/components/chevron'; +import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; From aecf9be553801e00b69dd352e20f05a6d52bc339 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 16 Apr 2024 15:24:17 -0400 Subject: [PATCH 14/19] update to new mocks --- .../components/sidebar/sidebarAccordion.tsx | 37 ++++++------- static/app/components/sidebar/sidebarItem.tsx | 54 ++++++++++++------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 02ea5150cd62c5..a22f4f0251ede1 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -2,7 +2,6 @@ import { Children, cloneElement, isValidElement, - type MouseEventHandler, type ReactElement, type ReactNode, useCallback, @@ -35,7 +34,8 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { const accordionRef = useRef(null); const mainItemRef = useRef(null); const floatingAccordionRef = useRef(null); - const {expandedItemId, setExpandedItemId} = useContext(ExpandedContext); + const {expandedItemId, setExpandedItemId, shouldAccordionFloat} = + useContext(ExpandedContext); const theme = useTheme(); const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); const router = useRouter(); @@ -94,20 +94,28 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { } }; - const handleTitleClick: MouseEventHandler = () => { + const handleTitleClick: ( + id: string, + e: React.MouseEvent + ) => void = () => { if (itemProps.to) { router.push(itemProps.to); setExpandedItemId(null); } }; + let isMainItemActive = isActive && !hasActiveChildren; + if (shouldAccordionFloat) { + isMainItemActive = isActive || hasActiveChildren; + } + return (
- - {itemProps.label} - + {childrenWithProps} )} @@ -201,23 +212,13 @@ function findChildElementsInTree( return found; } -const SidebarItemLabel = styled('div')` - color: ${p => p.theme.gray300}; - padding: ${space(1)} 0 ${space(1)} 18px; - font-size: ${p => p.theme.fontSizeLarge}; - white-space: nowrap; - &:hover { - cursor: pointer; - } -`; - const FloatingAccordion = styled('div')<{ accordionRef: React.RefObject; horizontal: boolean; }>` position: absolute; width: ${p => (p.horizontal ? '100%' : '200px')}; - padding: ${space(2)}; + padding: ${space(1.5)}; top: ${p => p.horizontal ? p.theme.sidebar.mobileHeight diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 184803bf51d1bf..329bbf2fcfe8e0 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -84,6 +84,11 @@ export type SidebarItemProps = { * Additional badge letting users know a tab is in beta. */ isBeta?: boolean; + /** + * Is main item in a floating accordion + */ + isMainItem?: boolean; + /** * Is this item nested within another item */ @@ -132,6 +137,7 @@ function SidebarItem({ trailingItems, variant, isNested, + isMainItem, ...props }: SidebarItemProps) { const {setExpandedItemId, shouldAccordionFloat} = useContext(ExpandedContext); @@ -145,8 +151,10 @@ function SidebarItem({ const isActiveRouter = !hasPanel && router && isItemActive({to, label: labelString}, exact); + const isInFloatingAccordion = (isNested || isMainItem) && shouldAccordionFloat; + const isActive = defined(active) ? active : isActiveRouter; - const isTop = orientation === 'top' && !isNested; + const isTop = orientation === 'top' && !isInFloatingAccordion; const placement = isTop ? 'bottom' : 'right'; const seenSuffix = isNewSeenKeySuffix ?? ''; @@ -187,8 +195,7 @@ function SidebarItem({ [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setExpandedItemId] ); - const isInFloatingSidebar = isNested && shouldAccordionFloat; - const isInCollapsedState = !isInFloatingSidebar && collapsed; + const isInCollapsedState = !isInFloatingAccordion && collapsed; return ( - {icon} + {!isInFloatingAccordion && {icon}} {!isInCollapsedState && !isTop && ( - + {label} {badges} @@ -298,22 +308,13 @@ const getActiveStyle = ({ } if (isInFloatingAccordion) { return css` - color: ${theme?.gray500}; + background-color: ${theme?.hover}; &:active, &:focus, &:hover { color: ${theme?.gray500}; } - - &:before { - background-color: ${theme?.active}; - top: 0; - left: -15px; - width: 5px; - height: 30px; - border-radius: 0 3px 3px 0; - } `; } return css` @@ -335,7 +336,7 @@ const StyledSidebarItem = styled(Link, { shouldForwardProp: p => typeof p === 'string' && isPropValid(p), })` display: flex; - color: inherit; + color: ${p => (p.isInFloatingAccordion ? p.theme.gray400 : 'inherit')}; position: relative; cursor: pointer; font-size: 15px; @@ -371,7 +372,17 @@ const StyledSidebarItem = styled(Link, { &:hover, &:focus-visible { - color: ${p => (p.isInFloatingAccordion ? p.theme.gray500 : p.theme.white)}; + ${p => { + if (p.isInFloatingAccordion) { + return css` + background-color: ${p.theme.hover}; + color: ${p.theme.gray400}; + `; + } + return css` + color: ${p.theme.white}; + `; + }} } &:focus { @@ -413,8 +424,11 @@ const SidebarItemIcon = styled('span')` } `; -const SidebarItemLabel = styled('span')` - margin-left: 10px; +const SidebarItemLabel = styled('span')<{ + isInFloatingAccordion?: boolean; + isNested?: boolean; +}>` + margin-left: ${p => (p.isNested && p.isInFloatingAccordion ? space(4) : '10px')}; white-space: nowrap; opacity: 1; flex: 1; From 9ed0399ac3b44d114ed2b3a010d39087ae0c8ade Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 16 Apr 2024 16:22:33 -0400 Subject: [PATCH 15/19] updated to use overlay component --- .../components/sidebar/sidebarAccordion.tsx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index a22f4f0251ede1..67a1706e1a7428 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -13,6 +13,7 @@ import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; import {Chevron} from 'sentry/components/chevron'; +import {Overlay} from 'sentry/components/overlay'; import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -142,10 +143,11 @@ function SidebarAccordion({children, ...itemProps}: SidebarAccordionProps) { )} {isOpenInFloatingSidebar && (horizontal || sidebarCollapsed) && ( - {childrenWithProps} - + )} ); @@ -212,7 +214,7 @@ function findChildElementsInTree( return found; } -const FloatingAccordion = styled('div')<{ +const StyledOverlay = styled(Overlay)<{ accordionRef: React.RefObject; horizontal: boolean; }>` @@ -226,18 +228,6 @@ const FloatingAccordion = styled('div')<{ left: ${p => p.horizontal ? 0 : `calc(${p.theme.sidebar.collapsedWidth} + ${space(1)})`}; background-color: white; - - animation: fadeIn 0.3s ease-in-out; - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); `; const SidebarAccordionWrapper = styled('div')` From 972fffd96c29574875377f817e8e5764eb88f8ad Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 16 Apr 2024 16:25:01 -0400 Subject: [PATCH 16/19] remove unneded css --- static/app/components/sidebar/sidebarAccordion.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 67a1706e1a7428..62ae859b4bad4d 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -227,7 +227,6 @@ const StyledOverlay = styled(Overlay)<{ : p.accordionRef.current?.getBoundingClientRect().top}; left: ${p => p.horizontal ? 0 : `calc(${p.theme.sidebar.collapsedWidth} + ${space(1)})`}; - background-color: white; `; const SidebarAccordionWrapper = styled('div')` From ac2e56be2c26d4863ab77a672a8ab29fcbe6cdda Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 16 Apr 2024 20:00:13 -0400 Subject: [PATCH 17/19] change padding to match other areas of sentry --- static/app/components/sidebar/sidebarAccordion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarAccordion.tsx b/static/app/components/sidebar/sidebarAccordion.tsx index 62ae859b4bad4d..4bdddaf2290ea8 100644 --- a/static/app/components/sidebar/sidebarAccordion.tsx +++ b/static/app/components/sidebar/sidebarAccordion.tsx @@ -220,7 +220,7 @@ const StyledOverlay = styled(Overlay)<{ }>` position: absolute; width: ${p => (p.horizontal ? '100%' : '200px')}; - padding: ${space(1.5)}; + padding: ${space(0.5)}; top: ${p => p.horizontal ? p.theme.sidebar.mobileHeight From 7e2257c71dc314bf08a224c45316516270e2a703 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 16 Apr 2024 20:14:53 -0400 Subject: [PATCH 18/19] match height --- static/app/components/sidebar/sidebarItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index 329bbf2fcfe8e0..c7437bf77aa976 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -340,7 +340,7 @@ const StyledSidebarItem = styled(Link, { position: relative; cursor: pointer; font-size: 15px; - height: 30px; + height: ${p => (p.isInFloatingAccordion ? '35px' : '30px')}; flex-shrink: 0; border-radius: ${p => p.theme.borderRadius}; transition: none; From 802a53d9a76954c16f9b7b1066559c2860db9bc3 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Tue, 16 Apr 2024 20:18:01 -0400 Subject: [PATCH 19/19] update hover --- static/app/components/sidebar/sidebarItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/sidebar/sidebarItem.tsx b/static/app/components/sidebar/sidebarItem.tsx index c7437bf77aa976..c1f2ac06beaead 100644 --- a/static/app/components/sidebar/sidebarItem.tsx +++ b/static/app/components/sidebar/sidebarItem.tsx @@ -313,7 +313,7 @@ const getActiveStyle = ({ &:active, &:focus, &:hover { - color: ${theme?.gray500}; + color: ${theme?.gray400}; } `; }