Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiBasicTable] Pass click events to actions onClick #7667

Merged
merged 4 commits into from
Apr 10, 2024
Merged
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
1 change: 1 addition & 0 deletions changelogs/upcoming/7667.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiBasicTable` and `EuiInMemoryTable`'s `columns[].actions[]`'s to pass back click events to `onClick` callbacks as the second callback
8 changes: 5 additions & 3 deletions src/components/basic_table/action_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { ReactElement, ReactNode } from 'react';
import { ReactElement, ReactNode, MouseEvent } from 'react';
import { EuiIconType } from '../icon/icon';
import { EuiButtonIconProps } from '../button/button_icon/button_icon';
import { EuiButtonEmptyProps } from '../button/button_empty';
Expand All @@ -26,9 +26,11 @@ export interface DefaultItemActionBase<T extends object> {
*/
description: string | ((item: T) => string);
/**
* A handler function to execute the action
* A handler function to execute the action. Passes back the current row
* item as the first argument, and the originating React click event
* as a second argument.
*/
onClick?: (item: T) => void;
onClick?: (item: T, event: MouseEvent) => void;
href?: string | ((item: T) => string);
target?: string;
/**
Expand Down
42 changes: 42 additions & 0 deletions src/components/basic_table/collapsed_item_actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,48 @@ describe('CollapsedItemActions', () => {
await waitForEuiPopoverClose();
});

test('default actions - passes back the original click event as well as the row item to onClick', async () => {
const onClick = jest.fn();
const onClickStopPropagation = jest.fn((item, event) => {
event.stopPropagation();
});

const props = {
actions: [
{
name: '1',
description: '',
onClick,
'data-test-subj': 'onClick',
},
{
name: '2',
description: '',
onClick: onClickStopPropagation,
'data-test-subj': 'onClickStopPropagation',
},
],
itemId: 'id',
item: { id: 'xyz' },
actionsDisabled: false,
};

const { getByTestSubject } = render(<CollapsedItemActions {...props} />);
fireEvent.click(getByTestSubject('euiCollapsedItemActionsButton'));
await waitForEuiPopoverOpen();

fireEvent.click(getByTestSubject('onClickStopPropagation'));
expect(onClickStopPropagation).toHaveBeenCalledWith(
props.item,
expect.objectContaining({ stopPropagation: expect.any(Function) })
);
// Popover should still be open if propagation was stopped
await waitForEuiPopoverOpen();

fireEvent.click(getByTestSubject('onClick'));
await waitForEuiPopoverClose();
});

test('custom actions', async () => {
const props = {
actions: [
Expand Down
31 changes: 13 additions & 18 deletions src/components/basic_table/collapsed_item_actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import React, {
ReactElement,
} from 'react';

import { isString } from '../../services/predicate';
import { EuiContextMenuItem, EuiContextMenuPanel } from '../context_menu';
import { EuiPopover } from '../popover';
import { EuiButtonIcon } from '../button';
Expand Down Expand Up @@ -45,11 +44,7 @@ export const CollapsedItemActions = <T extends {}>({
className,
}: CollapsedItemActionsProps<T>) => {
const [popoverOpen, setPopoverOpen] = useState(false);

const onClickItem = useCallback((onClickAction?: () => void) => {
setPopoverOpen(false);
onClickAction?.();
}, []);
const closePopover = useCallback(() => setPopoverOpen(false), []);

const controls = useMemo(() => {
return actions.reduce<ReactElement[]>((controls, action, index) => {
Expand All @@ -69,16 +64,13 @@ export const CollapsedItemActions = <T extends {}>({
key={index}
className="euiBasicTable__collapsedCustomAction"
>
<span onClick={() => onClickItem()}>{actionControl}</span>
<span onClick={closePopover}>{actionControl}</span>
</EuiContextMenuItem>
);
} else {
const buttonIcon = action.icon;
let icon;
if (buttonIcon) {
icon = isString(buttonIcon) ? buttonIcon : buttonIcon(item);
}

const icon = action.icon
? callWithItemIfFunction(item)(action.icon)
: undefined;
const buttonContent = callWithItemIfFunction(item)(action.name);
const toolTipContent = callWithItemIfFunction(item)(action.description);
const href = callWithItemIfFunction(item)(action.href);
Expand All @@ -97,9 +89,12 @@ export const CollapsedItemActions = <T extends {}>({
target={target}
icon={icon}
data-test-subj={dataTestSubj}
onClick={() =>
onClickItem(onClick ? () => onClick(item) : undefined)
}
onClick={(event) => {
event.persist();
onClick?.(item, event);
// Allow consumer events to prevent the popover from closing if necessary
if (!event.isPropagationStopped()) closePopover();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice and clean! 🎉

}}
toolTipContent={toolTipContent}
toolTipProps={{ delay: 'long' }}
>
Expand All @@ -109,7 +104,7 @@ export const CollapsedItemActions = <T extends {}>({
}
return controls;
}, []);
}, [actions, actionsDisabled, item, onClickItem]);
}, [actions, actionsDisabled, item, closePopover]);

const popoverButton = (
<EuiI18n
Expand Down Expand Up @@ -153,7 +148,7 @@ export const CollapsedItemActions = <T extends {}>({
id={`${itemId}-actions`}
isOpen={popoverOpen}
button={withTooltip || popoverButton}
closePopover={() => setPopoverOpen(false)}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="leftCenter"
>
Expand Down
26 changes: 26 additions & 0 deletions src/components/basic_table/default_item_action.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,30 @@ describe('DefaultItemAction', () => {
await waitForEuiToolTipVisible();
expect(getByText('goodbye tooltip')).toBeInTheDocument();
});

it('passes back the original click event as well as the row item to onClick', () => {
const onClick = jest.fn((item, event) => {
event.preventDefault();
});

const action: EmptyButtonAction<Item> = {
name: 'onClick',
description: 'test',
onClick,
'data-test-subj': 'onClick',
};
const props = {
action,
enabled: true,
item: { id: 'xyz' },
};

const { getByTestSubject } = render(<DefaultItemAction {...props} />);

fireEvent.click(getByTestSubject('onClick'));
expect(onClick).toHaveBeenCalledWith(
props.item,
expect.objectContaining({ preventDefault: expect.any(Function) })
);
});
});
32 changes: 16 additions & 16 deletions src/components/basic_table/default_item_action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
* Side Public License, v 1.
*/

import React, { ReactElement, ReactNode } from 'react';
import React, { ReactElement, ReactNode, MouseEvent, useCallback } from 'react';

import { isString } from '../../services/predicate';
import {
EuiButtonEmpty,
EuiButtonIcon,
Expand Down Expand Up @@ -42,28 +41,29 @@ export const DefaultItemAction = <T extends object>({
or 'href' string. If you want to provide a custom action control, make sure to define the 'render' callback`);
}

const onClick = action.onClick ? () => action.onClick!(item) : undefined;

const buttonColor = action.color;
let color: EuiButtonIconProps['color'] = 'primary';
if (buttonColor) {
color = isString(buttonColor) ? buttonColor : buttonColor(item);
}

const buttonIcon = action.icon;
let icon;
if (buttonIcon) {
icon = isString(buttonIcon) ? buttonIcon : buttonIcon(item);
}
const onClick = useCallback(
(event: MouseEvent) => {
if (!action.onClick) return;
event.persist(); // TODO: Remove once React 16 support is dropped
action.onClick!(item, event);
},
[action.onClick, item]
);

let button;
const color: EuiButtonIconProps['color'] = action.color
? callWithItemIfFunction(item)(action.color)
: 'primary';
const icon = action.icon
? callWithItemIfFunction(item)(action.icon)
: undefined;
const actionContent = callWithItemIfFunction(item)(action.name);
const tooltipContent = callWithItemIfFunction(item)(action.description);
const href = callWithItemIfFunction(item)(action.href);
const dataTestSubj = callWithItemIfFunction(item)(action['data-test-subj']);

const ariaLabelId = useGeneratedHtmlId();
let ariaLabelledBy: ReactNode;
let button;

if (action.type === 'icon') {
if (!icon) {
Expand Down
Loading