Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 1 addition & 9 deletions static/app/components/events/eventTags/eventTagsTreeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,15 +283,7 @@ function EventTagsTreeRowDropdown({
visitExternalLinkAction(content.value),
];

return (
<KeyValueTreeRowActions
ariaLabel={t('Tag Actions Menu')}
items={items}
// Inline, the menu is trapped in the focused row's stacking context and
// renders under positioned content further down the page.
usePortal
/>
);
return <KeyValueTreeRowActions ariaLabel={t('Tag Actions Menu')} items={items} />;
}

function EventTagsTreeValue({
Expand Down
14 changes: 10 additions & 4 deletions static/app/components/keyValueTree/keyValueTreeRowActions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {useState} from 'react';
import {useContext, useState} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {IconEllipsis} from '@sentry/icons/ellipsis';

import {type MenuItemProps} from '@sentry/scraps/dropdownMenu';
import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
import {RevealOnHover} from '@sentry/scraps/revealOnHover';
import {TooltipContext} from '@sentry/scraps/tooltip';

import {openNavigateToExternalLinkModal} from 'sentry/actionCreators/modal';
import {t} from 'sentry/locale';
Expand All @@ -28,18 +30,22 @@ export function visitExternalLinkAction(value: KeyValueTreeValue): MenuItemProps
export function KeyValueTreeRowActions({
ariaLabel,
items,
usePortal,
}: {
ariaLabel: string;
items: MenuItemProps[];
usePortal?: boolean;
}) {
const theme = useTheme();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {container} = useContext(TooltipContext);

return (
<RevealOnHover.Action visible={isMenuOpen}>
<TreeValueDropdown
usePortal={usePortal}
// Inline, the menu is clipped by scroll containers and trapped in the focused row's stacking context.
usePortal
// Drawers and modals sit above the dropdown z-index, so join their container and layer.
portalContainerRef={{current: container}}
zIndex={container ? theme.zIndex.modal : undefined}
preventOverflowOptions={{padding: 4}}
position="bottom-end"
size="xs"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {useState} from 'react';
import {LocationFixture} from 'sentry-fixture/locationFixture';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ThemeFixture} from 'sentry-fixture/theme';

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

import type {MenuItemProps} from '@sentry/scraps/dropdownMenu';
import {TooltipContext} from '@sentry/scraps/tooltip';

import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
Expand Down Expand Up @@ -285,9 +287,11 @@ describe('attributesTree', () => {
}
// jsdom does not evaluate :hover styles, so bypass the hidden action's pointer-events check.
await userEvent.click(actionsButton, {pointerEventsCheck: 0});
expect(await within(row).findByText('Visible Action')).toBeInTheDocument();
expect(await within(row).findByText('Disabled Action')).toBeInTheDocument();
expect(within(row).queryByText('Hidden Action')).not.toBeInTheDocument();
expect(actionsButton).toHaveAttribute('aria-expanded', 'true');
expect(within(row).queryByRole('menu')).not.toBeInTheDocument();
expect(await screen.findByText('Visible Action')).toBeInTheDocument();
expect(await screen.findByText('Disabled Action')).toBeInTheDocument();
expect(screen.queryByText('Hidden Action')).not.toBeInTheDocument();
}
});

Expand Down Expand Up @@ -418,4 +422,36 @@ describe('attributesTree', () => {
screen.getByTestId('tree-key-sentry.logger.name')
);
});

it('renders the actions menu in the surrounding overlay container when one is provided', async () => {
function OverlayContainer({children}: {children: React.ReactNode}) {
const [container, setContainer] = useState<HTMLDivElement | null>(null);

return (
<div data-test-id="overlay-container" ref={setContainer}>
<TooltipContext value={{container}}>{children}</TooltipContext>
</div>
);
}

render(
<OverlayContainer>
<AttributesTree
attributes={[{type: 'str', value: 'test value', name: 'test'}]}
rendererExtra={{theme, location, navigate: jest.fn(), organization}}
/>
</OverlayContainer>
);

await userEvent.click(screen.getByRole('button', {name: 'Attribute Actions Menu'}), {
pointerEventsCheck: 0,
});

expect(
await within(screen.getByTestId('overlay-container')).findByRole('menu')
).toBeInTheDocument();
expect(
within(screen.getByTestId('attribute-tree-row')).queryByRole('menu')
).not.toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion static/packages/scraps/src/dropdownMenu/dropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ interface BaseDropdownMenuProps
/**
* Reference to the container element that the portal should be rendered into.
*/
portalContainerRef?: React.RefObject<HTMLElement | null>;
portalContainerRef?: React.RefObject<Element | DocumentFragment | null>;
/**
* Tag name for the outer wrap, defaults to `div`
*/
Expand Down
Loading