Skip to content

Commit 9b419cf

Browse files
fix(logs): Prevent cell action menu clicks from toggling row visibility
Clicking "Copy to clipboard" (or any item) in the ellipsis dropdown menu on a log row also toggled the row's expanded/collapsed state. The dropdown menu renders in a React portal, but React propagates synthetic events from portals through the component tree. The row's onPointerUp handler checked isInsideButton(event.target) to skip the toggle, but portal menu items render as <li role="menuitem">, which didn't match the button check. Add a DOM containment guard: verify the event target is actually inside the row element before toggling. Portal-originating events fail this check since their DOM is in a separate subtree. Refs LOGS-638 Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> Made-with: Cursor
1 parent 352c3c9 commit 9b419cf

File tree

2 files changed

+56
-1
lines changed

2 files changed

+56
-1
lines changed

static/app/views/explore/logs/tables/logsTableRow.spec.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,55 @@ describe('logsTableRow', () => {
489489
expect(parsedData).not.toHaveProperty('sentry.item_id');
490490
});
491491

492+
it('does not toggle row when clicking cell action menu items', async () => {
493+
const mockWriteText = jest.fn().mockResolvedValue(undefined);
494+
Object.defineProperty(window.navigator, 'clipboard', {
495+
value: {
496+
writeText: mockWriteText,
497+
},
498+
writable: true,
499+
});
500+
501+
render(
502+
<ProviderWrapper>
503+
<LogRowContent
504+
dataRow={rowData}
505+
highlightTerms={[]}
506+
meta={LogFixtureMeta(rowData)}
507+
sharedHoverTimeoutRef={
508+
{
509+
current: null,
510+
} as React.MutableRefObject<NodeJS.Timeout | null>
511+
}
512+
/>
513+
</ProviderWrapper>,
514+
{organization, initialRouterConfig}
515+
);
516+
517+
const logTableRow = await screen.findByTestId('log-table-row');
518+
await userEvent.click(logTableRow);
519+
520+
await waitFor(() => {
521+
expect(rowDetailsMock).toHaveBeenCalledTimes(1);
522+
});
523+
524+
// Row is expanded - verify details are visible
525+
expect(await screen.findByRole('button', {name: 'Copy as JSON'})).toBeInTheDocument();
526+
527+
// Open the ellipsis context menu on a cell
528+
const actionsButton = screen.getAllByRole('button', {name: 'Actions'})[0]!;
529+
await userEvent.click(actionsButton);
530+
531+
// Click "Copy to clipboard" in the dropdown menu
532+
const copyItem = await screen.findByRole('menuitemradio', {
533+
name: 'Copy to clipboard',
534+
});
535+
await userEvent.click(copyItem);
536+
537+
// Row should still be expanded - the cell action should not toggle visibility
538+
expect(screen.getByRole('button', {name: 'Copy as JSON'})).toBeInTheDocument();
539+
});
540+
492541
it('renders fields with data scrubbing meta information', async () => {
493542
const traceItemMock = MockApiClient.addMockResponse({
494543
url: `/projects/${organization.slug}/${project.slug}/trace-items/${rowDataWithScrubbedFields[OurLogKnownFieldKey.ID]}/`,

static/app/views/explore/logs/tables/logsTableRow.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,13 @@ export const LogRowContent = memo(function LogRowContent({
185185

186186
function onPointerUp(event: SyntheticEvent) {
187187
if (event.target instanceof Element && isInsideButton(event.target)) {
188-
// do not expand the context menu if you clicked a button
188+
return;
189+
}
190+
if (
191+
event.target instanceof Node &&
192+
event.currentTarget instanceof Node &&
193+
!event.currentTarget.contains(event.target)
194+
) {
189195
return;
190196
}
191197
if (window.getSelection()?.toString() === '') {

0 commit comments

Comments
 (0)