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

[EuiButtonGroup] Add support for EuiToolTip for button tooltips #7461

Merged
merged 18 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 17 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/7461.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added the following properties to `EuiButtonGroup`'s `options` configs: `toolTipContent`, `toolTipProps`, and `title`. These new properties allow wrapping buttons in `EuiToolTips`, and additionally customizing or disabling the native browser `title` tooltip.
59 changes: 56 additions & 3 deletions src-docs/src/views/button/button_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,28 @@ const buttonGroupCompressedSnippet = [
/>`,
];

import ButtonGroupToolTips from './button_group_tooltips';
const buttonGroupToolTipsSource = require('!!raw-loader!./button_group_tooltips');
const buttonGroupToolTipsSnippet = [
`<EuiButtonGroup
legend={legend}
options={[
{
id,
label,
toolTipContent,
toolTipProps: {
title,
delay,
position,
},
},
]}
idSelected={idSelected}
onChange={(optionId) => {}}
/>`,
];

export const ButtonExample = {
title: 'Button',
intro: (
Expand Down Expand Up @@ -653,7 +675,7 @@ export const ButtonExample = {
],
text: (
<>
<h3>Icon only button groups</h3>
<h3 id="buttonGroup-isIconOnly">Icon only button groups</h3>
<p>
If you&apos;re just displaying a group of icons, add the prop{' '}
<EuiCode>isIconOnly</EuiCode>.
Expand All @@ -672,10 +694,9 @@ export const ButtonExample = {
code: buttonGroupCompressedSource,
},
],

text: (
<>
<h3>Button groups in forms</h3>
<h3 id="buttonGroup-compressed">Button groups in forms</h3>
<p>
When using button groups within compressed forms, match the form
elements by adding <EuiCode>{'buttonSize="compressed"'}</EuiCode>.
Expand All @@ -697,6 +718,38 @@ export const ButtonExample = {
props: { EuiButtonGroup, EuiButtonGroupOptionProps },
demoPanelProps: { color: 'subdued' },
},
{
source: [
{
type: GuideSectionTypes.JS,
code: buttonGroupToolTipsSource,
},
],
text: (
<>
<h3 id="buttonGroup-toolTipContent">Button group tooltips</h3>
<p>
Buttons within a button group will automatically display a default
browser tooltip containing the button <EuiCode>label</EuiCode> text.
This can be customized or unset via the <EuiCode>title</EuiCode>{' '}
property in your <EuiCode>options</EuiCode> button configuration.
</p>
<p>
To instead display an <EuiCode>EuiToolTip</EuiCode> around your
button(s), pass the <EuiCode>toolTipContent</EuiCode> property. You
can also use <EuiCode>toolTipProps</EuiCode> to customize tooltip
placement, title, and any other prop that{' '}
<Link to="/#/display/tooltip">
<strong>EuiToolTip</strong>
</Link>{' '}
accepts.
</p>
</>
),
demo: <ButtonGroupToolTips />,
snippet: buttonGroupToolTipsSnippet,
props: { EuiButtonGroupOptionProps },
},
],
guidelines: <Guidelines />,
};
42 changes: 42 additions & 0 deletions src-docs/src/views/button/button_group_tooltips.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { useState } from 'react';

import { EuiButtonGroup } from '../../../../src/components';

export default () => {
const toggleButtons = [
{
id: 'buttonGroup__0',
label: 'Default title',
},
{
id: 'buttonGroup__1',
label: 'Custom tooltip content',
toolTipContent: 'This is a custom tooltip',
},
{
id: 'buttonGroup__2',
label: 'Custom tooltip props',
toolTipContent: 'This is another custom tooltip',
toolTipProps: {
title: 'My custom title',
delay: 'regular',
position: 'right',
},
},
];

const [toggleIdSelected, setToggleIdSelected] = useState('buttonGroup__1');

const onChange = (optionId) => {
setToggleIdSelected(optionId);
};

return (
<EuiButtonGroup
legend="This is a group with tooltips"
options={toggleButtons}
idSelected={toggleIdSelected}
onChange={(id) => onChange(id)}
/>
);
};
41 changes: 37 additions & 4 deletions src/components/button/button_group/button_group.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ const meta: Meta<EuiButtonGroupProps> = {
options: {
control: 'array',
},
buttonSize: {
control: 'select',
},
},
args: {
// Component defaults
Expand Down Expand Up @@ -109,7 +106,6 @@ const EuiButtonGroupMulti = (props: any) => {
<EuiButtonGroup
type="multi"
{...props}
options={options}
onChange={onChange}
idToSelectedMap={idToSelectedMap}
/>
Expand All @@ -126,3 +122,40 @@ export const MultiSelection: Story = {
},
argTypes: disableStorybookControls(['type']),
};

export const WithTooltips: Story = {
render: ({ ...args }) => <EuiButtonGroupMulti {...args} />,
args: {
legend: 'EuiButtonGroup - tooltip UI testing',
isIconOnly: true, // Start example with icons to demonstrate usefulness of tooltips
options: [
{
id: 'button1',
iconType: 'securitySignal',
label: 'No tooltip',
},
{
id: 'button2',
iconType: 'securitySignalResolved',
label: 'Standard tooltip',
toolTipContent: 'Hello world',
},
{
id: 'customToolTipProps',
iconType: 'securitySignalDetected',
label: 'Custom tooltip',
toolTipContent: 'Custom tooltip position and delay',
toolTipProps: {
position: 'right',
delay: 'regular',
title: 'Hello world',
},
// Consumers could also opt to hide titles if preferred
title: '',
},
],
type: 'multi',
idToSelectedMap: { button1: true },
},
argTypes: disableStorybookControls(['type']),
};
4 changes: 3 additions & 1 deletion src/components/button/button_group/button_group.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export const euiButtonGroupButtonsStyles = (euiThemeContext: UseEuiTheme) => {
fullWidth: css`
${logicalCSS('width', '100%')}

.euiButtonGroupButton {
.euiButtonGroupButton,
.euiButtonGroup__tooltipWrapper {
flex: 1;
${logicalCSS('width', '100%')}
}
`,
// Sizes
Expand Down
97 changes: 95 additions & 2 deletions src/components/button/button_group/button_group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@

import React from 'react';
import { css } from '@emotion/react';
import { render } from '../../../test/rtl';
import { fireEvent } from '@testing-library/react';
import {
render,
waitForEuiToolTipHidden,
waitForEuiToolTipVisible,
} from '../../../test/rtl';
import { requiredProps as commonProps } from '../../../test';
import { shouldRenderCustomStyles } from '../../../test/internal';

import { BUTTON_COLORS } from '../../../themes/amsterdam/global_styling/mixins';
import {
EuiButtonGroup,
EuiButtonGroupOptionProps,
EuiButtonGroupProps,
} from './button_group';
import { BUTTON_COLORS } from '../../../themes/amsterdam/global_styling/mixins';

const SIZES: Array<EuiButtonGroupProps['buttonSize']> = [
's',
Expand Down Expand Up @@ -216,4 +221,92 @@ describe('EuiButtonGroup', () => {
'text-transform: uppercase'
);
});

describe('tooltips', () => {
it('shows a tooltip on hover and focus', async () => {
const { getByTestSubject, getByRole } = render(
<EuiButtonGroup
{...requiredMultiProps}
isIconOnly
options={[
...options,
{
id: 'buttonWithTooltip',
label: 'Option 4',
toolTipContent: 'I am a tooltip',
},
]}
/>
);
fireEvent.mouseOver(getByTestSubject('buttonWithTooltip'));
await waitForEuiToolTipVisible();

expect(getByRole('tooltip')).toHaveTextContent('I am a tooltip');

fireEvent.mouseOut(getByTestSubject('buttonWithTooltip'));
await waitForEuiToolTipHidden();

fireEvent.focus(getByTestSubject('buttonWithTooltip'));
await waitForEuiToolTipVisible();
fireEvent.blur(getByTestSubject('buttonWithTooltip'));
await waitForEuiToolTipHidden();
});

it('allows customizing the tooltip via `toolTipProps`', async () => {
const { getByTestSubject } = render(
<EuiButtonGroup
{...requiredMultiProps}
isIconOnly
options={[
...options,
{
id: 'buttonWithTooltip',
label: 'Option 4',
toolTipContent: 'I am a tooltip',
toolTipProps: {
position: 'right',
delay: 'regular',
'data-test-subj': 'toolTipTest',
},
},
]}
/>
);
fireEvent.mouseOver(getByTestSubject('buttonWithTooltip'));
await waitForEuiToolTipVisible();

expect(getByTestSubject('toolTipTest')).toHaveAttribute(
'data-position',
'right'
);
});

it('allows consumers to unset the `title` in favor of a tooltip', () => {
const reallyLongLabel =
'This is a really long label that we know will be truncated, so we show a tooltip instead and hide the title';

const { getByTestSubject } = render(
<EuiButtonGroup
{...requiredMultiProps}
isIconOnly
options={[
...options,
{
id: 'buttonWithTooltip',
label: reallyLongLabel,
toolTipContent: reallyLongLabel,
title: undefined,
},
]}
/>
);
expect(getByTestSubject('buttonWithTooltip')).not.toHaveAttribute(
'title'
);
expect(getByTestSubject('button01')).toHaveAttribute(
'title',
'Option two'
);
});
});
});
19 changes: 17 additions & 2 deletions src/components/button/button_group/button_group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { EuiScreenReaderOnly } from '../../accessibility';
import { CommonProps } from '../../common';

import { _EuiButtonColor } from '../../../themes/amsterdam/global_styling/mixins';
import { EuiToolTipProps } from '../../../components/tool_tip';
import { EuiButtonDisplayContentProps } from '../button_display/_button_display_content';
import { EuiButtonGroupButton } from './button_group_button';
import {
Expand Down Expand Up @@ -46,6 +47,20 @@ export interface EuiButtonGroupOptionProps
* The type of the underlying HTML button
*/
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
/**
* By default, will use the button text for the native browser title.
*
* This can be either customized or unset via `title: ''` if necessary.
*/
title?: ButtonHTMLAttributes<HTMLButtonElement>['title'];
/**
* Optional custom tooltip content for the button
*/
toolTipContent?: EuiToolTipProps['content'];
/**
* Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)**
*/
toolTipProps?: Partial<Omit<EuiToolTipProps, 'content' | 'children'>>;
}

export type EuiButtonGroupProps = CommonProps & {
Expand Down Expand Up @@ -174,10 +189,10 @@ export const EuiButtonGroup: FunctionComponent<Props> = ({
</EuiScreenReaderOnly>

<div css={cssStyles} className="euiButtonGroup__buttons">
{options.map((option, index) => {
{options.map((option) => {
return (
<EuiButtonGroupButton
key={index}
key={option.id}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I switched from using index to option.id for the key since using index would cause the wrong tooltips to be displayed when modifying the options array of an existing button group (something we do in Discover).

isDisabled={isDisabled}
{...(option as EuiButtonGroupOptionProps)}
onClick={
Expand Down
Loading
Loading