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][EuiInMemoryTable] Enable more action props to accept an optional callback + fix missing tooltips on collapsed actions #7373

Merged
merged 11 commits into from
Nov 20, 2023
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
17 changes: 17 additions & 0 deletions changelogs/upcoming/7373.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
- Updated the actions column in `EuiBasicTable` and `EuiInMemoryTable`s. Alongside `name`, the `description`, `href`, and `data-test-subj` properties now also accept an optional callback that the current `item` will be passed to
- Updated `EuiContextMenuItem` with a new `toolTipProps` prop

**Bug fixes**

- Fixed `EuiBasicTable` and `EuiInMemoryTable` actions not showing tooltip descriptions when rendered in the all actions popover menu
- Fixed missing underlines on `EuiContextMenu` link hover

**Deprecations**

- Deprecated `EuiContextMenuItem`'s `toolTipTitle` prop. Use `toolTipProps.title` instead
- Deprecated `EuiContextMenuItem`'s `toolTipPosition` prop. Use `toolTipProps.position` instead

**Accessibility**

- Fixed `EuiBasicTable` and `EuiInMemoryTable` actions not correctly reading out action descriptions to screen readers
- Fixed `EuiBasicTable` and `EuiInMemoryTable` primary actions not visibly appearing on keyboard focus
11 changes: 11 additions & 0 deletions scripts/jest/setup/throw_on_console_error.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ console.error = (message, ...rest) => {
return;
}

// Silence RTL act() errors, that appear to primarily come from the fact
// that we have multiple versions of `@testing-library/dom` installed
if (
typeof message === 'string' &&
message.startsWith(
'Warning: The current testing environment is not configured to support act(...)'
)
) {
return;
}

// Print React validateDOMNesting warning as a console.warn instead
// of throwing an error.
// TODO: Remove when edge-case DOM nesting is fixed in all components
Expand Down
18 changes: 11 additions & 7 deletions src-docs/src/views/tables/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,15 @@ export default () => {
} else {
let actions: Array<DefaultItemAction<User>> = [
{
name: 'Elastic.co',
description: 'Go to elastic.co',
name: 'User profile',
description: ({ firstName, lastName }) =>
`Visit ${firstName} ${lastName}'s profile`,
icon: 'editorLink',
color: 'primary',
type: 'icon',
href: 'https://elastic.co',
target: '_blank',
enabled: ({ online }) => !!online,
href: ({ id }) => `${window.location.href}?id=${id}`,
target: '_self',
'data-test-subj': 'action-outboundlink',
},
];
Expand All @@ -205,18 +207,20 @@ export default () => {
},
{
name: (user: User) => (user.id ? 'Delete' : 'Remove'),
description: 'Delete this user',
description: ({ firstName, lastName }) =>
`Delete ${firstName} ${lastName}`,
icon: 'trash',
color: 'danger',
type: 'icon',
onClick: deleteUser,
isPrimary: true,
'data-test-subj': 'action-delete',
'data-test-subj': ({ id }) => `action-delete-${id}`,
},
{
name: 'Edit',
isPrimary: true,
available: ({ online }: { online: boolean }) => !online,
available: ({ online }) => !online,
enabled: ({ online }) => !!online,
description: 'Edit this user',
icon: 'pencil',
type: 'icon',
Expand Down
10 changes: 8 additions & 2 deletions src-docs/src/views/tables/actions/actions_section.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import React from 'react';
import { EuiBasicTable } from '../../../../../src/components';

import { EuiBasicTable, EuiCode } from '../../../../../src/components';

import { GuideSectionTypes } from '../../../components';

import { EuiTableActionsColumnType } from '!!prop-loader!../../../../../src/components/basic_table/table_types';
import { CustomItemAction } from '!!prop-loader!../../../../../src/components/basic_table/action_types';
import { DefaultItemActionProps as DefaultItemAction } from '../props/props';

import Table from './actions';
import { EuiCode } from '../../../../../src/components/code';
const source = require('!!raw-loader!./actions');

export const section = {
Expand Down Expand Up @@ -40,5 +45,6 @@ export const section = {
</>
),
components: { EuiBasicTable },
props: { EuiTableActionsColumnType, DefaultItemAction, CustomItemAction },
demo: <Table />,
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CollapsedItemActions custom actions 1`] = `
<body>
<body
class=""
>
<div>
<div
class="euiPopover euiPopover-isOpen emotion-euiPopover-inline-block"
Expand Down Expand Up @@ -176,29 +178,38 @@ exports[`CollapsedItemActions default actions 1`] = `
tabindex="-1"
>
<div>
<button
class="euiContextMenuItem euiBasicTable__collapsedAction emotion-euiContextMenuItem-m-center"
data-test-subj="defaultAction"
type="button"
<span
class="euiToolTipAnchor eui-displayBlock emotion-euiToolTipAnchor-inlineBlock"
>
<span
class="euiContextMenuItem__text emotion-euiContextMenuItem__text"
<button
class="euiContextMenuItem euiBasicTable__collapsedAction emotion-euiContextMenuItem-m-center"
data-test-subj="defaultAction"
type="button"
>
default1
</span>
</button>
<a
class="euiContextMenuItem euiBasicTable__collapsedAction emotion-euiContextMenuItem-m-center"
href="https://www.elastic.co/"
rel="noopener noreferrer"
target="_blank"
<span
class="euiContextMenuItem__text emotion-euiContextMenuItem__text"
>
default1
</span>
</button>
</span>
<span
class="euiToolTipAnchor eui-displayBlock emotion-euiToolTipAnchor-inlineBlock"
>
<span
class="euiContextMenuItem__text emotion-euiContextMenuItem__text"
<a
class="euiContextMenuItem euiBasicTable__collapsedAction emotion-euiContextMenuItem-m-center"
data-test-subj="xyz-link"
href="#/xyz"
rel="noopener noreferrer"
target="_blank"
>
default2
</span>
</a>
<span
class="euiContextMenuItem__text emotion-euiContextMenuItem__text"
>
name xyz
</span>
</a>
</span>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,86 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DefaultItemAction render - button 1`] = `
<EuiToolTip
content="action 1"
delay="long"
display="inlineBlock"
position="top"
exports[`DefaultItemAction renders an EuiButtonEmpty when \`type="button" 1`] = `
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<EuiButtonEmpty
color="primary"
flush="right"
isDisabled={false}
onClick={[Function]}
size="s"
<button
class="euiButtonEmpty emotion-euiButtonDisplay-euiButtonEmpty-s-empty-primary-flush-right"
type="button"
>
action1
</EuiButtonEmpty>
</EuiToolTip>
`;

exports[`DefaultItemAction render - default button 1`] = `
<EuiToolTip
content="action 1"
delay="long"
display="inlineBlock"
position="top"
>
<EuiButtonEmpty
color="primary"
flush="right"
isDisabled={false}
onClick={[Function]}
size="s"
>
action1
</EuiButtonEmpty>
</EuiToolTip>
`;

exports[`DefaultItemAction render - icon 1`] = `
<EuiToolTip
content="action 1"
delay="long"
display="inlineBlock"
position="top"
>
<EuiButtonIcon
aria-labelledby="generated-id"
color="primary"
iconType="trash"
isDisabled={false}
onClick={[Function]}
/>
<EuiScreenReaderOnly>
<span
id="generated-id"
class="euiButtonEmpty__content emotion-euiButtonDisplayContent"
>
<span>
<span
class="eui-textTruncate euiButtonEmpty__text"
>
action1
</span>
</span>
</EuiScreenReaderOnly>
</EuiToolTip>
</button>
</span>
`;

exports[`DefaultItemAction render - name 1`] = `
<EuiToolTip
content="action 1"
delay="long"
display="inlineBlock"
position="top"
>
<EuiButtonEmpty
color="primary"
flush="right"
isDisabled={false}
onClick={[Function]}
size="s"
exports[`DefaultItemAction renders an EuiButtonIcon with screen reader text when \`type="icon"\` 1`] = `
<div>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-labelledby="generated-id"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="trash"
/>
</button>
</span>
<span
class="emotion-euiScreenReaderOnly"
id="generated-id"
>
<span>
xyz
action1
</span>
</EuiButtonEmpty>
</EuiToolTip>
</span>
</div>
`;
21 changes: 13 additions & 8 deletions src/components/basic_table/action_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ type EuiButtonIconColorFunction<T> = (item: T) => ButtonColor;

export interface DefaultItemActionBase<T> {
/**
* The display name of the action (will be the button caption)
* The display name of the action (will render as visible text if rendered within a collapsed menu)
*/
name: ReactNode | ((item: T) => ReactNode);
/**
* Describes the action (will be the button title)
* Describes the action (will render as tooltip content)
*/
description: string;
description: string | ((item: T) => string);
/**
* A handler function to execute the action
*/
onClick?: (item: T) => void;
href?: string;
href?: string | ((item: T) => string);
target?: string;
/**
* A callback function that determines whether the action is available
Expand All @@ -40,7 +40,7 @@ export interface DefaultItemActionBase<T> {
*/
enabled?: (item: T) => boolean;
isPrimary?: boolean;
'data-test-subj'?: string;
'data-test-subj'?: string | ((item: T) => string);
}

export interface DefaultItemEmptyButtonAction<T>
Expand Down Expand Up @@ -88,8 +88,13 @@ export interface CustomItemAction<T> {

export type Action<T> = DefaultItemAction<T> | CustomItemAction<T>;

export const isCustomItemAction = (
action: DefaultItemAction<any> | CustomItemAction<any>
): action is CustomItemAction<any> => {
export const isCustomItemAction = <T>(
action: DefaultItemAction<T> | CustomItemAction<T>
): action is CustomItemAction<T> => {
return action.hasOwnProperty('render');
};

export const callWithItemIfFunction =
<T>(item: T) =>
<U>(prop: U | ((item: T) => U)): U =>
typeof prop === 'function' ? (prop as Function)(item) : prop;
Loading
Loading