Skip to content

Commit b98c3ad

Browse files
committed
feat(nav): Open secondary navigation in overlay when collapsed
1 parent 9a2532a commit b98c3ad

21 files changed

+290
-296
lines changed

static/app/components/hovercard.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {Fragment, useCallback, useRef} from 'react';
1+
import {createContext, Fragment, useCallback, useContext, useMemo, useRef} from 'react';
22
import {createPortal} from 'react-dom';
33
import {useTheme} from '@emotion/react';
44
import styled from '@emotion/styled';
5+
import type {State} from '@popperjs/core';
56
import {useResizeObserver} from '@react-aria/utils';
67
import {AnimatePresence} from 'framer-motion';
78

@@ -16,6 +17,10 @@ interface HovercardProps extends Omit<UseHoverOverlayProps, 'isHoverable'> {
1617
* Classname to apply to the hovercard
1718
*/
1819
children: React.ReactNode;
20+
/**
21+
* Whether to animate the hovercard in/out
22+
*/
23+
animated?: boolean;
1924
/**
2025
* Element to display in the body
2126
*/
@@ -47,11 +52,33 @@ type UseOverOverlayState = ReturnType<typeof useHoverOverlay>;
4752
interface HovercardContentProps
4853
extends Pick<
4954
HovercardProps,
50-
'bodyClassName' | 'className' | 'header' | 'body' | 'tipColor' | 'tipBorderColor'
55+
| 'animated'
56+
| 'bodyClassName'
57+
| 'className'
58+
| 'header'
59+
| 'body'
60+
| 'tipColor'
61+
| 'tipBorderColor'
5162
> {
5263
hoverOverlayState: Omit<UseOverOverlayState, 'isOpen' | 'wrapTrigger'>;
5364
}
5465

66+
interface HovercardProviderValue {
67+
isOpen: boolean;
68+
reset: () => void;
69+
update: (() => Promise<Partial<State>>) | null;
70+
}
71+
72+
const HovercardContext = createContext<HovercardProviderValue>({
73+
isOpen: false,
74+
reset: () => {},
75+
update: null,
76+
});
77+
78+
export function useHovercardContext() {
79+
return useContext(HovercardContext);
80+
}
81+
5582
function useUpdateOverlayPositionOnContentChange({
5683
update,
5784
}: Pick<UseOverOverlayState, 'update'>) {
@@ -70,6 +97,7 @@ function useUpdateOverlayPositionOnContentChange({
7097
}
7198

7299
function HovercardContent({
100+
animated,
73101
body,
74102
bodyClassName,
75103
className,
@@ -84,7 +112,7 @@ function HovercardContent({
84112
return (
85113
<PositionWrapper zIndex={theme.zIndex.hovercard} {...overlayProps}>
86114
<StyledHovercard
87-
animated
115+
animated={animated}
88116
arrowProps={{
89117
...arrowProps,
90118
size: 20,
@@ -114,6 +142,7 @@ function Hovercard({
114142
displayTimeout = 100,
115143
tipBorderColor = 'translucentBorder',
116144
tipColor = 'backgroundElevated',
145+
animated = true,
117146
...hoverOverlayProps
118147
}: HovercardProps): React.ReactElement {
119148
const {wrapTrigger, isOpen, ...hoverOverlayState} = useHoverOverlay({
@@ -124,16 +153,25 @@ function Hovercard({
124153
...hoverOverlayProps,
125154
});
126155

156+
const contextValue = useMemo<HovercardProviderValue>(
157+
() => ({
158+
isOpen,
159+
reset: hoverOverlayState.reset,
160+
update: hoverOverlayState.update,
161+
}),
162+
[isOpen, hoverOverlayState.reset, hoverOverlayState.update]
163+
);
127164
// Nothing to render if no header or body. Be consistent with wrapping the
128165
// children with the trigger in the case that the body / header is set while
129166
// the trigger is hovered.
130167
if (!body && !header) {
131168
return <Fragment>{wrapTrigger(children)}</Fragment>;
132169
}
133170

134-
const hovercardContent = isOpen && (
171+
const hovercardContent = isOpen ? (
135172
<HovercardContent
136173
{...{
174+
animated,
137175
body,
138176
bodyClassName,
139177
className,
@@ -143,13 +181,20 @@ function Hovercard({
143181
hoverOverlayState,
144182
}}
145183
/>
146-
);
184+
) : null;
147185

148186
return (
149-
<Fragment>
187+
<HovercardContext.Provider value={contextValue}>
150188
{wrapTrigger(children)}
151-
{createPortal(<AnimatePresence>{hovercardContent}</AnimatePresence>, document.body)}
152-
</Fragment>
189+
{createPortal(
190+
animated ? (
191+
<AnimatePresence>{hovercardContent}</AnimatePresence>
192+
) : (
193+
hovercardContent
194+
),
195+
document.body
196+
)}
197+
</HovercardContext.Provider>
153198
);
154199
}
155200

static/app/utils/useHoverOverlay.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function makeDefaultPopperModifiers(arrowElement: HTMLElement | null, offset: nu
5353
options: {
5454
padding: 12,
5555
altAxis: true,
56+
boundary: document.body,
5657
},
5758
},
5859
];

static/app/views/admin/adminLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {t} from 'sentry/locale';
55
import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
66
import useOrganization from 'sentry/utils/useOrganization';
77
import {prefersStackedNav} from 'sentry/views/nav/prefersStackedNav';
8+
import {PrimaryNavGroup} from 'sentry/views/nav/types';
89
import {BreadcrumbProvider} from 'sentry/views/settings/components/settingsBreadcrumb/context';
910
import SettingsLayout from 'sentry/views/settings/components/settingsLayout';
1011
import SettingsNavigation from 'sentry/views/settings/components/settingsNavigation';
@@ -40,6 +41,7 @@ export function AdminNavigation() {
4041
],
4142
},
4243
]}
44+
primaryNavGroup={PrimaryNavGroup.ADMIN}
4345
/>
4446
);
4547
}

static/app/views/nav/index.spec.tsx

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ describe('Nav', function () {
7878
body: {},
7979
});
8080

81+
MockApiClient.addMockResponse({
82+
url: `/organizations/org-slug/explore/saved/`,
83+
body: [],
84+
});
85+
86+
MockApiClient.addMockResponse({
87+
url: `/organizations/org-slug/dashboards/`,
88+
body: [],
89+
});
90+
8191
ConfigStore.set('user', {
8292
...ConfigStore.get('user'),
8393
options: {
@@ -243,58 +253,48 @@ describe('Nav', function () {
243253

244254
await userEvent.click(screen.getByRole('button', {name: 'Collapse'}));
245255

246-
expect(screen.getByTestId('collapsed-secondary-sidebar')).toBeInTheDocument();
256+
await waitFor(() => {
257+
expect(
258+
screen.queryByRole('navigation', {name: 'Secondary Navigation'})
259+
).not.toBeInTheDocument();
260+
});
247261

248-
await userEvent.click(screen.getByRole('button', {name: 'Expand'}));
262+
await userEvent.hover(screen.getByRole('link', {name: 'Issues'}));
249263

250264
expect(
251-
screen.queryByTestId('collapsed-secondary-sidebar')
252-
).not.toBeInTheDocument();
265+
await screen.findByRole('navigation', {name: 'Secondary Navigation'})
266+
).toBeInTheDocument();
267+
268+
await userEvent.click(screen.getByRole('button', {name: 'Expand'}));
253269
});
254270

255271
it('remembers collapsed state', async function () {
256272
localStorage.setItem(NAV_SIDEBAR_COLLAPSED_LOCAL_STORAGE_KEY, 'true');
257273

258274
renderNav();
259275

260-
expect(
261-
await screen.findByTestId('collapsed-secondary-sidebar')
262-
).toBeInTheDocument();
263-
expect(screen.getByRole('button', {name: 'Expand'})).toBeInTheDocument();
276+
await waitFor(() => {
277+
expect(
278+
screen.queryByRole('navigation', {name: 'Secondary Navigation'})
279+
).not.toBeInTheDocument();
280+
});
264281
});
265282

266-
it('expands on hover', async function () {
283+
it('closes secondary nav overlay when navigating to a new route', async function () {
267284
localStorage.setItem(NAV_SIDEBAR_COLLAPSED_LOCAL_STORAGE_KEY, 'true');
268285

269286
renderNav();
270287

271-
expect(
272-
await screen.findByTestId('collapsed-secondary-sidebar')
273-
).toBeInTheDocument();
288+
await userEvent.hover(screen.getByRole('link', {name: 'Explore'}));
274289

275-
expect(screen.getByTestId('collapsed-secondary-sidebar')).toHaveAttribute(
276-
'data-visible',
277-
'false'
278-
);
290+
await screen.findByRole('navigation', {name: 'Secondary Navigation'});
279291

280-
// Moving pointer over the primary navigation should expand the sidebar
281-
await userEvent.hover(
282-
screen.getByRole('navigation', {name: 'Primary Navigation'})
283-
);
284-
expect(screen.getByTestId('collapsed-secondary-sidebar')).toHaveAttribute(
285-
'data-visible',
286-
'true'
287-
);
292+
await userEvent.click(screen.getByRole('link', {name: 'Traces'}));
288293

289-
// Moving pointer away should hide the sidebar
290-
await userEvent.unhover(
291-
screen.getByRole('navigation', {name: 'Primary Navigation'})
292-
);
293294
await waitFor(() => {
294-
expect(screen.getByTestId('collapsed-secondary-sidebar')).toHaveAttribute(
295-
'data-visible',
296-
'false'
297-
);
295+
expect(
296+
screen.queryByRole('navigation', {name: 'Secondary Navigation'})
297+
).not.toBeInTheDocument();
298298
});
299299
});
300300
});

static/app/views/nav/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import styled from '@emotion/styled';
44

55
import {useNavContext} from 'sentry/views/nav/context';
66
import MobileTopbar from 'sentry/views/nav/mobileTopbar';
7+
import {SecondaryNav} from 'sentry/views/nav/secondary/secondary';
78
import {SecondaryNavContent} from 'sentry/views/nav/secondary/secondaryNavContent';
89
import {Sidebar} from 'sentry/views/nav/sidebar';
910
import {
1011
NavigationTourProvider,
1112
useStackedNavigationTour,
1213
} from 'sentry/views/nav/tour/tour';
1314
import {NavLayout} from 'sentry/views/nav/types';
15+
import {useActiveNavGroup} from 'sentry/views/nav/useActiveNavGroup';
1416

1517
function NavContent() {
1618
const {layout, navParentRef} = useNavContext();
1719
const {currentStepId, endTour} = useStackedNavigationTour();
1820
const tourIsActive = currentStepId !== null;
21+
const activeNavGroup = useActiveNavGroup();
1922

2023
// The tour only works with the sidebar layout, so if we change to the mobile
2124
// layout in the middle of the tour, it needs to end.
@@ -32,7 +35,9 @@ function NavContent() {
3235
isMobile={layout === NavLayout.MOBILE}
3336
>
3437
{layout === NavLayout.SIDEBAR ? <Sidebar /> : <MobileTopbar />}
35-
<SecondaryNavContent />
38+
<SecondaryNav>
39+
<SecondaryNavContent group={activeNavGroup} />
40+
</SecondaryNav>
3641
</NavContainer>
3742
);
3843
}

0 commit comments

Comments
 (0)