Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
eacc645
fix(nav): Fix TopBar trigger visual hierarchy
priscilawebdev Aug 3, 2026
80ecdde
feat(nav): Add three responsive sizes for TopBar action triggers
priscilawebdev Aug 3, 2026
a11ec45
feat(nav): Add hotkey tooltips to collapsed TopBar triggers
priscilawebdev Aug 3, 2026
d9c0c77
feat(nav): Collapse TopBar feedback into an overflow menu
priscilawebdev Aug 3, 2026
00d35f8
fix(nav): Only tooltip Ask Seer when it is icon-only
priscilawebdev Aug 3, 2026
1a6d641
fix(nav): Close the gap left by empty TopBar outlets
priscilawebdev Aug 3, 2026
6b3bb9b
ref(nav): Key TopBar trigger collapse on the navigation layout
priscilawebdev Aug 3, 2026
685412d
ref(nav): Use positive TopBar label state
priscilawebdev Aug 3, 2026
39aee1e
fix(nav): Match TopBar actions to container size
priscilawebdev Aug 3, 2026
c2d9c43
Merge branch 'master' into worktree-synchronous-hatching-canyon
priscilawebdev Aug 4, 2026
32e547b
ref(navigation): Name top bar action display states
priscilawebdev Aug 4, 2026
a7c475a
ref(navigation): Expose top bar action layout
priscilawebdev Aug 4, 2026
a077c01
ref(navigation): Clarify mobile search action
priscilawebdev Aug 4, 2026
dbac463
fix(navigation): Keep What's New modal data current
priscilawebdev Aug 4, 2026
73d4321
test(navigation): Exercise action container queries
priscilawebdev Aug 4, 2026
04ff07c
test(navigation): Cover responsive Feedback action
priscilawebdev Aug 4, 2026
e3cd849
test(navigation): Exercise What's New modal lifecycle
priscilawebdev Aug 4, 2026
90b7b93
test(navigation): Verify mobile What's New modal
priscilawebdev Aug 4, 2026
a7caaa8
ref(navigation): Configure actions from TopBar
priscilawebdev Aug 4, 2026
4f02d8e
fix(navigation): Refine TopBar action breakpoints
priscilawebdev Aug 4, 2026
b6cf702
ref(navigation): Remove redundant action keys
priscilawebdev Aug 4, 2026
4a4d502
fix(navigation): Stabilize TopBar action presentation
priscilawebdev Aug 4, 2026
a39b33f
ref(navigation): Omit empty TopBar outlets
priscilawebdev Aug 4, 2026
e1b4b5c
fix(navigation): Clarify Command Palette trigger
priscilawebdev Aug 4, 2026
94cee02
ref(navigation): Compose primary footer items
priscilawebdev Aug 4, 2026
dbfea9d
fix(navigation): Preserve icon-only action feedback
priscilawebdev Aug 4, 2026
55159b9
fix(navigation): Join mobile Command Palette action
priscilawebdev Aug 4, 2026
7a77391
Merge branch 'master' into worktree-synchronous-hatching-canyon
priscilawebdev Aug 4, 2026
5783071
fix(navigation): Isolate mobile What's New failures
priscilawebdev Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions static/app/views/navigation/index.mobile.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {BroadcastFixture} from 'sentry-fixture/broadcast';
import {GroupSearchViewFixture} from 'sentry-fixture/groupSearchView';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {UserFixture} from 'sentry-fixture/user';

import {
render,
renderGlobalModal,
screen,
userEvent,
within,
Expand Down Expand Up @@ -185,6 +187,35 @@ describe('mobile navigation', () => {
).toBeInTheDocument();
});

it("moves the Command Palette into the mobile row and What's New into the Help menu", async () => {
const context = navigationContext();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/broadcasts/',
body: [BroadcastFixture({title: 'Mobile Broadcast', hasSeen: true})],
});

render(
<PrimaryNavigationContextProvider>
<Navigation />
</PrimaryNavigationContextProvider>,
context
);
renderGlobalModal({organization: context.organization});

const commandPaletteButton = screen.getByRole('button', {
name: 'Open Command Palette',
});
expect(commandPaletteButton.parentElement).toContainElement(
screen.getByRole('button', {name: 'Help'})
);
expect(screen.queryByRole('button', {name: "What's New"})).not.toBeInTheDocument();

await userEvent.click(screen.getByRole('button', {name: 'Help'}));
await userEvent.click(screen.getByRole('menuitemradio', {name: "What's New"}));

expect(await screen.findByText('Mobile Broadcast')).toBeInTheDocument();
});

describe('secondary nav route inference', () => {
it('opens secondary navigation by default when on a sub-view', async () => {
render(
Expand Down
24 changes: 23 additions & 1 deletion static/app/views/navigation/mobileNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Flex, type FlexProps, Stack} from '@sentry/scraps/layout';
import {SizeProvider} from '@sentry/scraps/sizeContext';
import {useScrollLock} from '@sentry/scraps/useScrollLock';

import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {IconMenu} from 'sentry/icons';
import {t} from 'sentry/locale';
import {useOnClickOutside} from 'sentry/utils/useOnClickOutside';
Expand All @@ -20,7 +21,12 @@ import {
PrimaryNavigationItems,
} from 'sentry/views/navigation/navigation';
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
import {
PrimaryNavigationHelpMenu,
useWhatsNewHelpMenuOptions,
} from 'sentry/views/navigation/primary/helpMenu';
import {OrganizationDropdown} from 'sentry/views/navigation/primary/organizationDropdown';
import {SearchButton} from 'sentry/views/navigation/searchButton';
import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components';
import {SecondaryNavigationContent} from 'sentry/views/navigation/secondary/content';
import {useSecondaryNavigation} from 'sentry/views/navigation/secondaryNavigationContext';
Expand Down Expand Up @@ -48,6 +54,15 @@ function MobileNavigationHeader(props: FlexProps<'header'>) {
);
}

function MobileWhatsNewHelpMenu() {
const whatsNewHelpMenuOptions = useWhatsNewHelpMenuOptions();
return <PrimaryNavigationHelpMenu {...whatsNewHelpMenuOptions} />;
}

function MobileHelpMenuFallback() {
return <PrimaryNavigationHelpMenu />;
}

function MobilePrimaryNavigation() {
const {view} = useSecondaryNavigation();

Expand Down Expand Up @@ -136,7 +151,14 @@ export function MobileNavigation() {
/>
<Stack gap="md" direction="row">
<PrimaryNavigation.ButtonBar orientation="horizontal">
<PrimaryNavigationFooterItems />
<PrimaryNavigationFooterItems>
<PrimaryNavigation.ButtonContainer>
{buttonProps => <SearchButton {...buttonProps} />}
</PrimaryNavigation.ButtonContainer>
Comment thread
cursor[bot] marked this conversation as resolved.
<ErrorBoundary customComponent={MobileHelpMenuFallback}>
<MobileWhatsNewHelpMenu />
</ErrorBoundary>
</PrimaryNavigationFooterItems>
</PrimaryNavigation.ButtonBar>
<PrimaryNavigationFooterItemsUserDropdown />
</Stack>
Expand Down
16 changes: 9 additions & 7 deletions static/app/views/navigation/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Fragment, type RefObject, useMemo, useRef} from 'react';
import {Fragment, type PropsWithChildren, type RefObject, useMemo, useRef} from 'react';
import {mergeProps} from '@react-aria/utils';
import {motion, type MotionProps} from 'framer-motion';

Expand Down Expand Up @@ -92,7 +92,12 @@ export function Navigation() {
paddingBottom="md"
>
<PrimaryNavigation.FooterItems>
<PrimaryNavigationFooterItems />
<PrimaryNavigationFooterItems>
<ErrorBoundary customComponent={null}>
<PrimaryNavigationWhatsNew />
</ErrorBoundary>
<PrimaryNavigationHelpMenu />
</PrimaryNavigationFooterItems>
</PrimaryNavigation.FooterItems>
<PrimaryNavigation.FooterItems>
<PrimaryNavigationFooterItemsUserDropdown />
Expand Down Expand Up @@ -284,7 +289,7 @@ export function PrimaryNavigationItems({listRef}: PrimaryNavigationItemsProps) {
/**
* Returns the list of items from the footer of the primary navigation
*/
export function PrimaryNavigationFooterItems() {
export function PrimaryNavigationFooterItems({children}: PropsWithChildren) {
const organization = useOrganization();

return (
Expand All @@ -304,10 +309,7 @@ export function PrimaryNavigationFooterItems() {
<ErrorBoundary customComponent={null}>
<PrimaryNavigationServiceIncidents />
</ErrorBoundary>
<ErrorBoundary customComponent={null}>
<PrimaryNavigationWhatsNew />
</ErrorBoundary>
<PrimaryNavigationHelpMenu />
{children}
</Fragment>
);
}
Expand Down
11 changes: 8 additions & 3 deletions static/app/views/navigation/primary/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) {
>
<NavigationButton
{...triggerProps}
aria-label={layout === 'mobile' ? undefined : props.label}
aria-label={props.label}
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
if (organization) {
trackAnalytics('navigation.primary_item_clicked', {
Expand Down Expand Up @@ -361,18 +361,22 @@ function NavigationButton(props: DistributedOmit<ButtonProps, 'size'>) {
const {layout} = usePrimaryNavigation();

return (
<Flex align="center" padding="xs" justify="center">
<PrimaryNavigationButtonContainer>
{p => (
<ButtonWithOverflowVisible
{...p}
{...props}
{...(layout === 'mobile' ? {variant: 'secondary'} : {variant: props.variant})}
/>
)}
</Flex>
</PrimaryNavigationButtonContainer>
);
}

function PrimaryNavigationButtonContainer(props: React.ComponentProps<typeof Flex>) {
return <Flex align="center" padding="xs" justify="center" {...props} />;
}

/**
* @TODO(JonasBadalic) Scraps buttons have been setting overflow hidden onto the inner surface wrapper ever since
* we inherited that component, and we need to override that to ensure that the indicator is visible as it will
Expand Down Expand Up @@ -569,6 +573,7 @@ export const PrimaryNavigation = {
ListItem: PrimaryNavigationListItem,
Link: PrimaryNavigationLink,
Button: PrimaryNavigationButton,
ButtonContainer: PrimaryNavigationButtonContainer,
ButtonBar: PrimaryNavigationButtonBar,
Menu: PrimaryNavigationMenu,
ButtonOverlay: PrimaryNavigationButtonOverlay,
Expand Down
38 changes: 36 additions & 2 deletions static/app/views/navigation/primary/helpMenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import {BroadcastFixture} from 'sentry-fixture/broadcast';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {
render,
renderGlobalModal,
screen,
userEvent,
} from 'sentry-test/reactTestingLibrary';

import {ConfigStore} from 'sentry/stores/configStore';
import {ModalStore} from 'sentry/stores/modalStore';
import * as intercom from 'sentry/utils/intercom';
import {PrimaryNavigationHelpMenu} from 'sentry/views/navigation/primary/helpMenu';
import {
PrimaryNavigationHelpMenu,
useWhatsNewHelpMenuOptions,
} from 'sentry/views/navigation/primary/helpMenu';

function HelpMenuWithWhatsNew() {
const whatsNewOptions = useWhatsNewHelpMenuOptions();
return <PrimaryNavigationHelpMenu {...whatsNewOptions} />;
}

jest.mock('sentry/utils/intercom', () => ({
showIntercom: jest.fn(),
Expand All @@ -18,6 +33,7 @@ async function expandResourcesSubmenu() {
describe('PrimaryNavigationHelpMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
ModalStore.reset();
ConfigStore.set('supportEmail', 'support@sentry.io');
});

Expand All @@ -31,4 +47,22 @@ describe('PrimaryNavigationHelpMenu', () => {

expect(intercom.showIntercom).toHaveBeenCalledWith(organization.slug);
});

it("updates What's New when broadcasts finish loading", async () => {
const organization = OrganizationFixture();
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/broadcasts/`,
match: [MockApiClient.matchQuery({show: 'latest', limit: '3'})],
asyncDelay: 100,
body: [BroadcastFixture({id: '1', title: 'New Broadcast', hasSeen: true})],
});

render(<HelpMenuWithWhatsNew />, {organization});
renderGlobalModal({organization});

await userEvent.click(screen.getByRole('button', {name: 'Help'}));
await userEvent.click(screen.getByRole('menuitemradio', {name: "What's New"}));

expect(await screen.findByText('New Broadcast')).toBeInTheDocument();
});
});
52 changes: 50 additions & 2 deletions static/app/views/navigation/primary/helpMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {useEffect} from 'react';
import {Fragment, useEffect} from 'react';

import {Flex} from '@sentry/scraps/layout';

import {openModal} from 'sentry/actionCreators/modal';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
IconBroadcast,
IconBuilding,
IconDiscord,
IconDocs,
Expand All @@ -26,8 +29,20 @@ import {showIntercom} from 'sentry/utils/intercom';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import {useOrganization} from 'sentry/utils/useOrganization';
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
import {
useWhatsNewBroadcasts,
WhatsNewContent,
} from 'sentry/views/navigation/primary/whatsNew';

interface PrimaryNavigationHelpMenuProps {
additionalItems?: MenuItemProps[];
indicator?: 'accent' | 'danger' | 'warning';
}

export function PrimaryNavigationHelpMenu() {
export function PrimaryNavigationHelpMenu({
additionalItems = [],
indicator,
}: PrimaryNavigationHelpMenuProps = {}) {
const organization = useOrganization();
const contactSupportItem = getContactSupportItem(organization);
const openForm = useFeedbackForm();
Expand All @@ -38,6 +53,7 @@ export function PrimaryNavigationHelpMenu() {
}, [organization]);

const items: MenuItemProps[] = [
...additionalItems,
{
key: 'resources',
label: t('Resources'),
Expand Down Expand Up @@ -199,10 +215,42 @@ export function PrimaryNavigationHelpMenu() {
analyticsKey="help"
label={t('Help')}
icon={<IconEllipsis />}
indicator={indicator}
/>
);
}

export function useWhatsNewHelpMenuOptions(): PrimaryNavigationHelpMenuProps {
const {unseenPostIds} = useWhatsNewBroadcasts();

return {
additionalItems: [
{
key: 'whats-new',
label: t("What's New"),
leadingItems: (
<MenuIcon>
<IconBroadcast />
</MenuIcon>
),
onAction() {
openModal(({Header, Body}) => (
<Fragment>
<Header closeButton>{t("What's New")}</Header>
<Body>
<ErrorBoundary customComponent={null}>
<WhatsNewContent />
</ErrorBoundary>
</Body>
</Fragment>
));
},
},
],
indicator: unseenPostIds.length > 0 ? 'accent' : undefined,
};
}

function getContactSupportItem(organization: Organization): MenuItemProps | null {
const supportEmail = ConfigStore.get('supportEmail');

Expand Down
Loading
Loading