diff --git a/.changeset/fix-card-horizontal-double-click.md b/.changeset/fix-card-horizontal-double-click.md new file mode 100644 index 000000000..bb03227a2 --- /dev/null +++ b/.changeset/fix-card-horizontal-double-click.md @@ -0,0 +1,5 @@ +--- +"@clickhouse/click-ui": patch +--- + +Fix `CardHorizontal` double `onClick` handler bug where clicking the inner Button would invoke `onButtonClick` and window.open(infoUrl) twice due to event bubbling. Added e.stopPropagation() to the button's click handler. diff --git a/src/components/CardHorizontal/CardHorizontal.test.tsx b/src/components/CardHorizontal/CardHorizontal.test.tsx index 38f14a619..3c394ce08 100644 --- a/src/components/CardHorizontal/CardHorizontal.test.tsx +++ b/src/components/CardHorizontal/CardHorizontal.test.tsx @@ -1,4 +1,5 @@ import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { CardHorizontal, CardHorizontalProps } from '@/components/CardHorizontal'; import { renderCUI } from '@/utils/test-utils'; @@ -187,4 +188,31 @@ describe('CardHorizontal Component', () => { expect(windowOpenSpy).not.toHaveBeenCalled(); windowOpenSpy.mockRestore(); }); + + it('should call onButtonClick only once when inner button is clicked', async () => { + const onButtonClick = vitest.fn(); + const { getByRole } = renderCard({ + title: 'Test Card', + infoText: 'Click me', + onButtonClick, + }); + + await userEvent.click(getByRole('button')); + + expect(onButtonClick).toHaveBeenCalledTimes(1); + }); + + it('should open infoUrl only once when inner button is clicked', async () => { + const windowOpenSpy = vitest.spyOn(window, 'open').mockImplementation(() => null); + const { getByRole } = renderCard({ + title: 'Test Card', + infoText: 'Click me', + infoUrl: 'https://example.com', + }); + + await userEvent.click(getByRole('button')); + + expect(windowOpenSpy).toHaveBeenCalledTimes(1); + windowOpenSpy.mockRestore(); + }); }); diff --git a/src/components/CardHorizontal/CardHorizontal.tsx b/src/components/CardHorizontal/CardHorizontal.tsx index d00628c61..8dca9ff54 100644 --- a/src/components/CardHorizontal/CardHorizontal.tsx +++ b/src/components/CardHorizontal/CardHorizontal.tsx @@ -1,3 +1,4 @@ +import { useCallback, type MouseEvent } from 'react'; import { styled } from 'styled-components'; import { Badge } from '@/components/Badge'; import { Button } from '@/components/Button'; @@ -195,19 +196,30 @@ export const CardHorizontal = ({ onButtonClick, ...props }: CardHorizontalProps) => { - const handleClick = (e: React.MouseEvent) => { - if (disabled) { - e.preventDefault(); - return; - } + const handleClick = useCallback( + (e: MouseEvent) => { + if (disabled) { + e.preventDefault(); + return; + } - if (typeof onButtonClick === 'function') { - onButtonClick(e); - } - if (infoUrl && infoUrl.length > 0) { - window.open(infoUrl, '_blank'); - } - }; + if (typeof onButtonClick === 'function') { + onButtonClick(e); + } + if (infoUrl && infoUrl.length > 0) { + window.open(infoUrl, '_blank'); + } + }, + [disabled, onButtonClick, infoUrl] + ); + + const handleButtonClick = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + handleClick(e); + }, + [handleClick] + ); return (