From 34153a7e9c4b8fa82450522e47c1d58cf3be4921 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 22:52:36 +0200
Subject: [PATCH 01/21] feat(EuiToolTip): add controlled isOpen support
---
src/components/tool_tip/tool_tip.test.tsx | 20 ++++++++++++++++++++
src/components/tool_tip/tool_tip.tsx | 17 +++++++++++++++++
2 files changed, 37 insertions(+)
diff --git a/src/components/tool_tip/tool_tip.test.tsx b/src/components/tool_tip/tool_tip.test.tsx
index f534910f8cd..7a11a5c0bb8 100644
--- a/src/components/tool_tip/tool_tip.test.tsx
+++ b/src/components/tool_tip/tool_tip.test.tsx
@@ -182,6 +182,26 @@ describe('EuiToolTip', () => {
});
});
+ describe('isOpen', () => {
+ it('shows/hides the tooltip', async () => {
+ const { rerender } = render(
+
+ Trigger
+
+ );
+
+ await waitForEuiToolTipVisible();
+
+ rerender(
+
+ Trigger
+
+ );
+
+ await waitForEuiToolTipHidden();
+ });
+ });
+
describe('ref methods', () => {
// Although we don't publicly recommend it, consumers may need to reach into EuiToolTip
// class methods to manually control visibility state via `show/hideToolTip`.
diff --git a/src/components/tool_tip/tool_tip.tsx b/src/components/tool_tip/tool_tip.tsx
index b9bdb19a436..65ed118beae 100644
--- a/src/components/tool_tip/tool_tip.tsx
+++ b/src/components/tool_tip/tool_tip.tsx
@@ -98,6 +98,10 @@ export interface EuiToolTipProps extends CommonProps {
* Suggested position. If there is not enough room for it this will be changed.
*/
position: ToolTipPositions;
+ /**
+ * For controlled use to show/hide the tooltip
+ */
+ isOpen?: boolean;
/**
* When `true`, the tooltip's position is re-calculated when the user
* scrolls. This supports having fixed-position tooltip anchors.
@@ -153,6 +157,10 @@ export class EuiToolTip extends Component {
if (this.props.repositionOnScroll) {
window.addEventListener('scroll', this.positionToolTip, true);
}
+
+ if (this.props.isOpen) {
+ this.showToolTip();
+ }
}
componentWillUnmount() {
@@ -166,6 +174,14 @@ export class EuiToolTip extends Component {
requestAnimationFrame(this.testAnchor);
}
+ if (prevProps.isOpen !== this.props.isOpen) {
+ if (this.props.isOpen) {
+ this.showToolTip();
+ } else {
+ this.hideToolTip();
+ }
+ }
+
// update scroll listener
if (prevProps.repositionOnScroll !== this.props.repositionOnScroll) {
if (this.props.repositionOnScroll) {
@@ -307,6 +323,7 @@ export class EuiToolTip extends Component {
delay,
display,
repositionOnScroll,
+ isOpen,
...rest
} = this.props;
From 63721a905415a674ef9fc8f848b5cf2cde91778c Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:04:08 +0200
Subject: [PATCH 02/21] feat(EuiComboBox): add EuiToolTip support on option
---
src/components/combo_box/types.ts | 9 +++++
.../filter_group/filter_select_item.tsx | 35 ++++++++++++++++++-
2 files changed, 43 insertions(+), 1 deletion(-)
diff --git a/src/components/combo_box/types.ts b/src/components/combo_box/types.ts
index df687fc95ae..ecd524811ca 100644
--- a/src/components/combo_box/types.ts
+++ b/src/components/combo_box/types.ts
@@ -10,6 +10,7 @@ import { ButtonHTMLAttributes, ReactNode } from 'react';
import { CommonProps } from '../common';
import type { _EuiComboBoxProps } from './combo_box';
+import { EuiToolTipProps } from '../tool_tip';
// note similarity to `Option` in `components/selectable/types.tsx`
export interface EuiComboBoxOptionOption<
@@ -24,6 +25,14 @@ export interface EuiComboBoxOptionOption<
prepend?: ReactNode;
append?: ReactNode;
truncationProps?: _EuiComboBoxProps['truncationProps'];
+ /**
+ * Optional custom tooltip content for the button
+ */
+ toolTipContent?: EuiToolTipProps['content'];
+ /**
+ * Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)**
+ */
+ toolTipProps?: Partial>;
}
export type OptionHandler = (option: EuiComboBoxOptionOption) => void;
diff --git a/src/components/filter_group/filter_select_item.tsx b/src/components/filter_group/filter_select_item.tsx
index 9725e2f6978..cb2a21bf441 100644
--- a/src/components/filter_group/filter_select_item.tsx
+++ b/src/components/filter_group/filter_select_item.tsx
@@ -13,6 +13,7 @@ import { withEuiTheme, WithEuiThemeProps } from '../../services';
import { CommonProps } from '../common';
import { EuiFlexGroup, EuiFlexItem } from '../flex';
+import { EuiToolTip, EuiToolTipProps } from '../tool_tip';
import { EuiIcon } from '../icon';
import { euiFilterSelectItemStyles } from './filter_select_item.styles';
@@ -24,6 +25,14 @@ export interface EuiFilterSelectItemProps
checked?: FilterChecked;
showIcons?: boolean;
isFocused?: boolean;
+ /**
+ * Optional custom tooltip content for the button
+ */
+ toolTipContent?: EuiToolTipProps['content'];
+ /**
+ * Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)**
+ */
+ toolTipProps?: Partial>;
}
const resolveIconAndColor = (checked?: FilterChecked) => {
@@ -79,6 +88,9 @@ export class EuiFilterSelectItemClass extends Component<
checked,
isFocused,
showIcons,
+ toolTipContent,
+ toolTipProps,
+ style,
...rest
} = this.props;
@@ -90,6 +102,9 @@ export class EuiFilterSelectItemClass extends Component<
const classes = classNames('euiFilterSelectItem', className);
+ const hasToolTip =
+ !disabled && React.isValidElement(children) && toolTipContent;
+
let iconNode;
if (showIcons) {
const { icon, color } = resolveIconAndColor(checked);
@@ -100,7 +115,7 @@ export class EuiFilterSelectItemClass extends Component<
);
}
- return (
+ const optionItem = (
(this.buttonRef = ref)}
role="option"
@@ -110,6 +125,7 @@ export class EuiFilterSelectItemClass extends Component<
css={cssStyles}
disabled={disabled}
aria-disabled={disabled}
+ style={!hasToolTip ? style : undefined}
{...rest}
>
);
+
+ return hasToolTip ? (
+ // This extra wrapper is needed to ensure that the tooltip has a correct context
+ // for positioning while also ensuring to wrap the interactive option
+
+
+ {optionItem}
+
+
+ ) : (
+ optionItem
+ );
}
}
From 0734902f5d369c380a5b485fd098c6661aa42b60 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:04:40 +0200
Subject: [PATCH 03/21] test(EuiComboBox): add tests for toolTipContent and
toolTipProps
---
src/components/combo_box/combo_box.test.tsx | 722 +++++++++++---------
1 file changed, 385 insertions(+), 337 deletions(-)
diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx
index c62797a2c3b..10643adf2cc 100644
--- a/src/components/combo_box/combo_box.test.tsx
+++ b/src/components/combo_box/combo_box.test.tsx
@@ -8,7 +8,11 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
-import { render, showEuiComboBoxOptions } from '../../test/rtl';
+import {
+ render,
+ showEuiComboBoxOptions,
+ waitForEuiToolTipVisible,
+} from '../../test/rtl';
import {
shouldRenderCustomStyles,
testOnReactVersion,
@@ -22,6 +26,8 @@ import type { EuiComboBoxOptionOption } from './types';
interface Options {
'data-test-subj'?: string;
label: string;
+ toolTipContent?: string;
+ toolTipProps?: {};
}
const options: Options[] = [
{
@@ -216,426 +222,468 @@ describe('EuiComboBox', () => {
});
});
- describe('placeholder', () => {
- it('renders', () => {
- const { getByTestSubject } = render(
-
- );
- const searchInput = getByTestSubject('comboBoxSearchInput');
+ describe('toolTipContent & tooltipProps', () => {
+ const options = [
+ {
+ label: 'Titan',
+ 'data-test-subj': 'titanOption',
+ toolTipContent: 'I am a tooltip!',
+ toolTipProps: {
+ 'data-test-subj': 'optionToolTip',
+ },
+ },
+ {
+ label: 'Enceladus',
+ },
+ {
+ label: 'Mimas',
+ },
+ ];
- expect(searchInput).toHaveAttribute('placeholder', 'Select something');
- expect(searchInput).toHaveStyle('inline-size: 100%');
- });
+ it('renders a tooltip with applied props on mouseover', async () => {
+ const { getByTestSubject } = render( );
- it('does not render the placeholder if a selection has been made', () => {
- const { getByTestSubject } = render(
-
- );
- const searchInput = getByTestSubject('comboBoxSearchInput');
- expect(searchInput).not.toHaveAttribute('placeholder');
- });
+ await showEuiComboBoxOptions();
- it('does not render the placeholder if a search value exists', () => {
- const { getByTestSubject } = render(
-
- );
- const searchInput = getByTestSubject('comboBoxSearchInput');
- expect(searchInput).toHaveAttribute('placeholder');
+ fireEvent.mouseOver(getByTestSubject('titanOption'));
+ await waitForEuiToolTipVisible();
- fireEvent.change(searchInput, { target: { value: 'some search' } });
- expect(searchInput).not.toHaveAttribute('placeholder');
+ expect(getByTestSubject('optionToolTip')).toBeInTheDocument();
+ expect(getByTestSubject('optionToolTip')).toHaveTextContent(
+ 'I am a tooltip!'
+ );
});
- });
- test('isDisabled', () => {
- const { container, queryByTestSubject, queryByTitle } = render(
-
- );
-
- expect(container.firstElementChild!.className).toContain('-isDisabled');
- expect(queryByTestSubject('comboBoxSearchInput')).toBeDisabled();
-
- expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy();
- expect(queryByTestSubject('comboBoxToggleListButton')).toBeFalsy();
- expect(
- queryByTitle('Remove Titan from selection in this group')
- ).toBeFalsy();
- });
-
- test('fullWidth', () => {
- // TODO: Should likely be a visual screenshot test
- const { container } = render(
-
- );
-
- expect(container.innerHTML).toContain('euiFormControlLayout--fullWidth');
- expect(container.innerHTML).toContain('euiComboBox--fullWidth');
- expect(container.innerHTML).toContain(
- 'euiComboBox__inputWrap--fullWidth'
- );
- });
-
- test('autoFocus', () => {
- const { getByTestSubject } = render(
-
- );
-
- expect(document.activeElement).toBe(
- getByTestSubject('comboBoxSearchInput')
- );
- });
-
- test('aria-label / aria-labelledby renders on the input, not on the wrapper', () => {
- const { getByTestSubject } = render(
-
- );
- const input = getByTestSubject('comboBoxSearchInput');
-
- expect(input).toHaveAttribute('aria-label', 'Test label');
- expect(input).toHaveAttribute('aria-labelledby', 'test-heading-id');
- });
-
- test('inputRef', () => {
- const inputRefCallback = jest.fn();
-
- const { getByRole } = render(
-
- );
- expect(inputRefCallback).toHaveBeenCalledTimes(1);
-
- expect(getByRole('combobox')).toBe(inputRefCallback.mock.calls[0][0]);
- });
-
- test('onSearchChange', () => {
- const onSearchChange = jest.fn();
- const { getByTestSubject, queryAllByRole } = render(
-
- );
- const input = getByTestSubject('comboBoxSearchInput');
-
- fireEvent.change(input, { target: { value: 'no results' } });
- expect(onSearchChange).toHaveBeenCalledWith('no results', false);
- expect(queryAllByRole('option')).toHaveLength(0);
-
- fireEvent.change(input, { target: { value: 'titan' } });
- expect(onSearchChange).toHaveBeenCalledWith('titan', true);
- expect(queryAllByRole('option')).toHaveLength(2);
- });
- });
-
- it('does not show multiple checkmarks with duplicate labels', async () => {
- const options = [
- { label: 'Titan', key: 'titan1' },
- { label: 'Titan', key: 'titan2' },
- { label: 'Tethys' },
- ];
- const { baseElement } = render(
-
- );
- await showEuiComboBoxOptions();
-
- const dropdownOptions = baseElement.querySelectorAll(
- '.euiFilterSelectItem'
- );
- expect(
- dropdownOptions[0]!.querySelector('[data-euiicon-type="check"]')
- ).toBeFalsy();
- expect(
- dropdownOptions[1]!.querySelector('[data-euiicon-type="check"]')
- ).toBeTruthy();
- });
+ it('renders a tooltip with applied props on focus', async () => {
+ const { getByTestSubject } = render( );
+ await showEuiComboBoxOptions();
- describe('behavior', () => {
- describe('hitting "Enter"', () => {
- describe('when the search input matches a value', () => {
- it('selects the option', () => {
- const onChange = jest.fn();
- const { getByTestSubject } = render(
-
- );
+ const input = getByTestSubject('comboBoxSearchInput');
+ fireEvent.keyDown(input, { key: keys.ARROW_DOWN });
- const input = getByTestSubject('comboBoxSearchInput');
- fireEvent.change(input, { target: { value: 'red' } });
- fireEvent.keyDown(input, { key: 'Enter' });
+ await waitForEuiToolTipVisible();
- expect(onChange).toHaveBeenCalledWith([{ label: 'Red' }]);
- });
+ expect(getByTestSubject('optionToolTip')).toBeInTheDocument();
+ expect(getByTestSubject('optionToolTip')).toHaveTextContent(
+ 'I am a tooltip!'
+ );
+ });
- it('accounts for group labels', () => {
- const onChange = jest.fn();
+ describe('placeholder', () => {
+ it('renders', () => {
const { getByTestSubject } = render(
);
+ const searchInput = getByTestSubject('comboBoxSearchInput');
- const input = getByTestSubject('comboBoxSearchInput');
- fireEvent.change(input, { target: { value: 'blue' } });
- fireEvent.keyDown(input, { key: 'Enter' });
-
- expect(onChange).toHaveBeenCalledWith([{ label: 'Blue' }]);
+ expect(searchInput).toHaveAttribute(
+ 'placeholder',
+ 'Select something'
+ );
+ expect(searchInput).toHaveStyle('inline-size: 100%');
});
- });
-
- describe('when `onCreateOption` is passed', () => {
- it('fires the callback when there is input', () => {
- const onCreateOptionHandler = jest.fn();
+ it('does not render the placeholder if a selection has been made', () => {
const { getByTestSubject } = render(
);
- const input = getByTestSubject('comboBoxSearchInput');
-
- fireEvent.change(input, { target: { value: 'foo' } });
- fireEvent.keyDown(input, { key: 'Enter' });
-
- expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
- expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options);
+ const searchInput = getByTestSubject('comboBoxSearchInput');
+ expect(searchInput).not.toHaveAttribute('placeholder');
});
- it('does not fire the callback when there is no input', () => {
- const onCreateOptionHandler = jest.fn();
-
+ it('does not render the placeholder if a search value exists', () => {
const { getByTestSubject } = render(
-
+
);
- const input = getByTestSubject('comboBoxSearchInput');
+ const searchInput = getByTestSubject('comboBoxSearchInput');
+ expect(searchInput).toHaveAttribute('placeholder');
- fireEvent.keyDown(input, { key: 'Enter' });
-
- expect(onCreateOptionHandler).not.toHaveBeenCalled();
+ fireEvent.change(searchInput, { target: { value: 'some search' } });
+ expect(searchInput).not.toHaveAttribute('placeholder');
});
});
- });
- describe('tabbing off the search input', () => {
- it("closes the options list if the user isn't navigating the options", async () => {
- const keyDownBubbled = jest.fn();
-
- const { getByTestSubject } = render(
-
-
-
+ test('isDisabled', () => {
+ const { container, queryByTestSubject, queryByTitle } = render(
+
);
- await showEuiComboBoxOptions();
- const mockEvent = { key: keys.TAB, shiftKey: true };
- fireEvent.keyDown(getByTestSubject('comboBoxSearchInput'), mockEvent);
+ expect(container.firstElementChild!.className).toContain('-isDisabled');
+ expect(queryByTestSubject('comboBoxSearchInput')).toBeDisabled();
- // If the TAB keydown bubbled up to the wrapper, then a browser DOM would shift the focus
- expect(keyDownBubbled).toHaveBeenCalledWith(
- expect.objectContaining(mockEvent)
- );
+ expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy();
+ expect(queryByTestSubject('comboBoxToggleListButton')).toBeFalsy();
+ expect(
+ queryByTitle('Remove Titan from selection in this group')
+ ).toBeFalsy();
});
- it('calls onCreateOption', () => {
- const onCreateOptionHandler = jest.fn();
-
- const { getByTestSubject } = render(
+ test('fullWidth', () => {
+ // TODO: Should likely be a visual screenshot test
+ const { container } = render(
);
- const input = getByTestSubject('comboBoxSearchInput');
- fireEvent.change(input, { target: { value: 'foo' } });
- fireEvent.blur(input);
-
- expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
- expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options);
- });
-
- it('does nothing if the user is navigating the options', async () => {
- const keyDownBubbled = jest.fn();
-
- const { getByTestSubject } = render(
-
-
-
+ expect(container.innerHTML).toContain(
+ 'euiFormControlLayout--fullWidth'
+ );
+ expect(container.innerHTML).toContain('euiComboBox--fullWidth');
+ expect(container.innerHTML).toContain(
+ 'euiComboBox__inputWrap--fullWidth'
);
- await showEuiComboBoxOptions();
-
- // Navigate to an option then tab off
- const input = getByTestSubject('comboBoxSearchInput');
- fireEvent.keyDown(input, { key: keys.ARROW_DOWN });
- fireEvent.keyDown(input, { key: keys.TAB });
-
- // If the TAB keydown did not bubble to the wrapper, then the tab event was prevented
- expect(keyDownBubbled).not.toHaveBeenCalled();
});
- });
- describe('clear button', () => {
- it('renders when options are selected', () => {
+ test('autoFocus', () => {
const { getByTestSubject } = render(
-
+
);
- expect(getByTestSubject('comboBoxClearButton')).toBeInTheDocument();
- });
-
- it('does not render when no options are selected', () => {
- const { queryByTestSubject } = render(
-
+ expect(document.activeElement).toBe(
+ getByTestSubject('comboBoxSearchInput')
);
-
- expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy();
});
- it('does not render when isClearable is false', () => {
- const { queryByTestSubject } = render(
+ test('aria-label / aria-labelledby renders on the input, not on the wrapper', () => {
+ const { getByTestSubject } = render(
);
+ const input = getByTestSubject('comboBoxSearchInput');
- expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy();
+ expect(input).toHaveAttribute('aria-label', 'Test label');
+ expect(input).toHaveAttribute('aria-labelledby', 'test-heading-id');
});
- it('calls the onChange callback with empty array', () => {
- const onChangeHandler = jest.fn();
+ test('inputRef', () => {
+ const inputRefCallback = jest.fn();
- const { getByTestSubject } = render(
-
+ const { getByRole } = render(
+
);
- fireEvent.click(getByTestSubject('comboBoxClearButton'));
+ expect(inputRefCallback).toHaveBeenCalledTimes(1);
- expect(onChangeHandler).toHaveBeenCalledTimes(1);
- expect(onChangeHandler).toHaveBeenCalledWith([]);
+ expect(getByRole('combobox')).toBe(inputRefCallback.mock.calls[0][0]);
});
- it('focuses the input', () => {
- const { getByTestSubject } = render(
- {}}
- />
+ test('onSearchChange', () => {
+ const onSearchChange = jest.fn();
+ const { getByTestSubject, queryAllByRole } = render(
+
);
- fireEvent.click(getByTestSubject('comboBoxClearButton'));
+ const input = getByTestSubject('comboBoxSearchInput');
- expect(document.activeElement).toBe(
- getByTestSubject('comboBoxSearchInput')
- );
+ fireEvent.change(input, { target: { value: 'no results' } });
+ expect(onSearchChange).toHaveBeenCalledWith('no results', false);
+ expect(queryAllByRole('option')).toHaveLength(0);
+
+ fireEvent.change(input, { target: { value: 'titan' } });
+ expect(onSearchChange).toHaveBeenCalledWith('titan', true);
+ expect(queryAllByRole('option')).toHaveLength(2);
});
});
- describe('sortMatchesBy', () => {
- const sortMatchesByOptions = [
- { label: 'Something is Disabled' },
- ...options,
+ it('does not show multiple checkmarks with duplicate labels', async () => {
+ const options = [
+ { label: 'Titan', key: 'titan1' },
+ { label: 'Titan', key: 'titan2' },
+ { label: 'Tethys' },
];
+ const { baseElement } = render(
+
+ );
+ await showEuiComboBoxOptions();
- test('"none"', () => {
- const { getByTestSubject, getAllByRole } = render(
-
- );
- fireEvent.change(getByTestSubject('comboBoxSearchInput'), {
- target: { value: 'di' },
+ const dropdownOptions = baseElement.querySelectorAll(
+ '.euiFilterSelectItem'
+ );
+ expect(
+ dropdownOptions[0]!.querySelector('[data-euiicon-type="check"]')
+ ).toBeFalsy();
+ expect(
+ dropdownOptions[1]!.querySelector('[data-euiicon-type="check"]')
+ ).toBeTruthy();
+ });
+
+ describe('behavior', () => {
+ describe('hitting "Enter"', () => {
+ describe('when the search input matches a value', () => {
+ it('selects the option', () => {
+ const onChange = jest.fn();
+ const { getByTestSubject } = render(
+
+ );
+
+ const input = getByTestSubject('comboBoxSearchInput');
+ fireEvent.change(input, { target: { value: 'red' } });
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ expect(onChange).toHaveBeenCalledWith([{ label: 'Red' }]);
+ });
+
+ it('accounts for group labels', () => {
+ const onChange = jest.fn();
+ const { getByTestSubject } = render(
+
+ );
+
+ const input = getByTestSubject('comboBoxSearchInput');
+ fireEvent.change(input, { target: { value: 'blue' } });
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ expect(onChange).toHaveBeenCalledWith([{ label: 'Blue' }]);
+ });
});
- const foundOptions = getAllByRole('option');
- expect(foundOptions).toHaveLength(2);
- expect(foundOptions[0]).toHaveTextContent('Something is Disabled');
- expect(foundOptions[1]).toHaveTextContent('Dione');
+ describe('when `onCreateOption` is passed', () => {
+ it('fires the callback when there is input', () => {
+ const onCreateOptionHandler = jest.fn();
+
+ const { getByTestSubject } = render(
+
+ );
+ const input = getByTestSubject('comboBoxSearchInput');
+
+ fireEvent.change(input, { target: { value: 'foo' } });
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
+ expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options);
+ });
+
+ it('does not fire the callback when there is no input', () => {
+ const onCreateOptionHandler = jest.fn();
+
+ const { getByTestSubject } = render(
+
+ );
+ const input = getByTestSubject('comboBoxSearchInput');
+
+ fireEvent.keyDown(input, { key: 'Enter' });
+
+ expect(onCreateOptionHandler).not.toHaveBeenCalled();
+ });
+ });
});
- test('"startsWith"', () => {
- const { getByTestSubject, getAllByRole } = render(
-
- );
- fireEvent.change(getByTestSubject('comboBoxSearchInput'), {
- target: { value: 'di' },
+ describe('tabbing off the search input', () => {
+ it("closes the options list if the user isn't navigating the options", async () => {
+ const keyDownBubbled = jest.fn();
+ const { getByTestSubject } = render(
+
+
+
+ );
+ await showEuiComboBoxOptions();
+ const mockEvent = { key: keys.TAB, shiftKey: true };
+ fireEvent.keyDown(getByTestSubject('comboBoxSearchInput'), mockEvent);
+ // If the TAB keydown bubbled up to the wrapper, then a browser DOM would shift the focus
+ expect(keyDownBubbled).toHaveBeenCalledWith(
+ expect.objectContaining(mockEvent)
+ );
});
+ it('calls onCreateOption', () => {
+ const onCreateOptionHandler = jest.fn();
+ const { getByTestSubject } = render(
+
+ );
+ const input = getByTestSubject('comboBoxSearchInput');
+ fireEvent.change(input, { target: { value: 'foo' } });
+ fireEvent.blur(input);
+ expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
+ expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options);
+ });
+ it('does nothing if the user is navigating the options', async () => {
+ const keyDownBubbled = jest.fn();
+ const { getByTestSubject } = render(
+
+
+
+ );
+ await showEuiComboBoxOptions();
+ // Navigate to an option then tab off
+ const input = getByTestSubject('comboBoxSearchInput');
+ fireEvent.keyDown(input, { key: keys.ARROW_DOWN });
+ fireEvent.keyDown(input, { key: keys.TAB });
+ // If the TAB keydown did not bubble to the wrapper, then the tab event was prevented
+ expect(keyDownBubbled).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('clear button', () => {
+ it('renders when options are selected', () => {
+ const { getByTestSubject } = render(
+
+ );
+
+ expect(getByTestSubject('comboBoxClearButton')).toBeInTheDocument();
+ });
+
+ it('does not render when no options are selected', () => {
+ const { queryByTestSubject } = render(
+
+ );
+
+ expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy();
+ });
+
+ it('does not render when isClearable is false', () => {
+ const { queryByTestSubject } = render(
+
+ );
+
+ expect(queryByTestSubject('comboBoxClearButton')).toBeFalsy();
+ });
+
+ it('calls the onChange callback with empty array', () => {
+ const onChangeHandler = jest.fn();
+
+ const { getByTestSubject } = render(
+
+ );
+ fireEvent.click(getByTestSubject('comboBoxClearButton'));
+
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
+ expect(onChangeHandler).toHaveBeenCalledWith([]);
+ });
+
+ it('focuses the input', () => {
+ const { getByTestSubject } = render(
+ {}}
+ />
+ );
+ fireEvent.click(getByTestSubject('comboBoxClearButton'));
- const foundOptions = getAllByRole('option');
- expect(foundOptions).toHaveLength(2);
- expect(foundOptions[0]).toHaveTextContent('Dione');
- expect(foundOptions[1]).toHaveTextContent('Something is Disabled');
+ expect(document.activeElement).toBe(
+ getByTestSubject('comboBoxSearchInput')
+ );
+ });
});
- });
- describe('isCaseSensitive', () => {
- const isCaseSensitiveOptions = [{ label: 'Case sensitivity' }];
+ describe('sortMatchesBy', () => {
+ const sortMatchesByOptions = [
+ { label: 'Something is Disabled' },
+ ...options,
+ ];
- test('false', () => {
- const { getByTestSubject, queryAllByRole } = render(
-
- );
- fireEvent.change(getByTestSubject('comboBoxSearchInput'), {
- target: { value: 'case' },
+ test('"none"', () => {
+ const { getByTestSubject, getAllByRole } = render(
+
+ );
+ fireEvent.change(getByTestSubject('comboBoxSearchInput'), {
+ target: { value: 'di' },
+ });
+
+ const foundOptions = getAllByRole('option');
+ expect(foundOptions).toHaveLength(2);
+ expect(foundOptions[0]).toHaveTextContent('Something is Disabled');
+ expect(foundOptions[1]).toHaveTextContent('Dione');
});
- expect(queryAllByRole('option')).toHaveLength(1);
+ test('"startsWith"', () => {
+ const { getByTestSubject, getAllByRole } = render(
+
+ );
+ fireEvent.change(getByTestSubject('comboBoxSearchInput'), {
+ target: { value: 'di' },
+ });
+
+ const foundOptions = getAllByRole('option');
+ expect(foundOptions).toHaveLength(2);
+ expect(foundOptions[0]).toHaveTextContent('Dione');
+ expect(foundOptions[1]).toHaveTextContent('Something is Disabled');
+ });
});
- test('true', () => {
- const { getByTestSubject, queryAllByRole } = render(
-
- );
- const input = getByTestSubject('comboBoxSearchInput');
+ describe('isCaseSensitive', () => {
+ const isCaseSensitiveOptions = [{ label: 'Case sensitivity' }];
- fireEvent.change(input, { target: { value: 'case' } });
- expect(queryAllByRole('option')).toHaveLength(0);
+ test('false', () => {
+ const { getByTestSubject, queryAllByRole } = render(
+
+ );
+ fireEvent.change(getByTestSubject('comboBoxSearchInput'), {
+ target: { value: 'case' },
+ });
- fireEvent.change(input, { target: { value: 'Case' } });
- expect(queryAllByRole('option')).toHaveLength(1);
+ expect(queryAllByRole('option')).toHaveLength(1);
+ });
+
+ test('true', () => {
+ const { getByTestSubject, queryAllByRole } = render(
+
+ );
+ const input = getByTestSubject('comboBoxSearchInput');
+
+ fireEvent.change(input, { target: { value: 'case' } });
+ expect(queryAllByRole('option')).toHaveLength(0);
+
+ fireEvent.change(input, { target: { value: 'Case' } });
+ expect(queryAllByRole('option')).toHaveLength(1);
+ });
});
});
});
From 5813c357bfced6273cd024f80ac9bbcf8deccc46 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:05:24 +0200
Subject: [PATCH 04/21] fix(EuiComboBox): fix isLoading layout for mobile
---
.../combo_box/combo_box_options_list/combo_box_options_list.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx
index f32adcd125c..aef427c28ab 100644
--- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx
+++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx
@@ -327,7 +327,7 @@ export class EuiComboBoxOptionsList extends Component<
if (isLoading) {
emptyStateContent = (
-
+
From c3e19f87458c59a64fa82b165d61e7b9f77d5fe4 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:06:01 +0200
Subject: [PATCH 05/21] docs(storybook): add separate tooltip story for
ComboBox
---
.../combo_box/combo_box.stories.tsx | 122 +++++++++++++-----
1 file changed, 87 insertions(+), 35 deletions(-)
diff --git a/src/components/combo_box/combo_box.stories.tsx b/src/components/combo_box/combo_box.stories.tsx
index c283edaf250..9f60a833567 100644
--- a/src/components/combo_box/combo_box.stories.tsx
+++ b/src/components/combo_box/combo_box.stories.tsx
@@ -10,15 +10,23 @@ import React, { useCallback, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
+import { hideStorybookControls } from '../../../.storybook/utils';
+import { ToolTipPositions } from '../tool_tip';
import { EuiComboBox, EuiComboBoxProps } from './combo_box';
import { EuiComboBoxOptionMatcher } from './types';
import { EuiCode } from '../code';
+const toolTipProps = {
+ toolTipContent: 'This is a tooltip!',
+ toolTipProps: { position: 'bottom' as ToolTipPositions },
+ value: 4,
+};
+
const options = [
{ label: 'Item 1' },
{ label: 'Item 2' },
{ label: 'Item 3' },
- { label: 'Item 4' },
+ { label: 'Item 4', disabled: true },
{ label: 'Item 5' },
];
@@ -64,41 +72,85 @@ export default meta;
type Story = StoryObj>;
export const Playground: Story = {
- render: function Render({ singleSelection, onCreateOption, ...args }) {
- const [selectedOptions, setSelectedOptions] = useState(
- args.selectedOptions
- );
- const onChange: EuiComboBoxProps<{}>['onChange'] = (options, ...args) => {
- setSelectedOptions(options);
- action('onChange')(options, ...args);
- };
- const _onCreateOption: EuiComboBoxProps<{}>['onCreateOption'] = (
- searchValue,
- ...args
- ) => {
- const createdOption = { label: searchValue };
- setSelectedOptions((prevState) =>
- !prevState || singleSelection
- ? [createdOption]
- : [...prevState, createdOption]
- );
- action('onCreateOption')(searchValue, ...args);
- };
- return (
-
- );
+ render: (args) => ,
+};
+
+export const WithTooltip: Story = {
+ args: {
+ options: options.map((option) => ({ ...option, ...toolTipProps })),
},
+ render: (args) => ,
+};
+// hide props as they are not relevant for testing the story args
+hideStorybookControls(WithTooltip, [
+ 'append',
+ 'aria-label',
+ 'aria-labelledby',
+ 'async',
+ 'autoFocus',
+ 'compressed',
+ 'customOptionText',
+ 'delimiter',
+ 'id',
+ 'inputPopoverProps',
+ 'inputRef',
+ 'isCaseSensitive',
+ 'isClearable',
+ 'isDisabled',
+ 'isInvalid',
+ 'isLoading',
+ 'noSuggestions',
+ 'onBlur',
+ 'onChange',
+ 'onCreateOption',
+ 'onFocus',
+ 'onKeyDown',
+ 'onSearchChange',
+ 'placeholder',
+ 'prepend',
+ 'renderOption',
+ 'rowHeight',
+ 'singleSelection',
+ 'sortMatchesBy',
+ 'truncationProps',
+]);
+
+const StatefulComboBox = ({
+ singleSelection,
+ onCreateOption,
+ ...args
+}: EuiComboBoxProps<{}>) => {
+ const [selectedOptions, setSelectedOptions] = useState(args.selectedOptions);
+ const onChange: EuiComboBoxProps<{}>['onChange'] = (options, ...args) => {
+ setSelectedOptions(options);
+ action('onChange')(options, ...args);
+ };
+ const _onCreateOption: EuiComboBoxProps<{}>['onCreateOption'] = (
+ searchValue,
+ ...args
+ ) => {
+ const createdOption = { label: searchValue };
+ setSelectedOptions((prevState) =>
+ !prevState || singleSelection
+ ? [createdOption]
+ : [...prevState, createdOption]
+ );
+ action('onCreateOption')(searchValue, ...args);
+ };
+ return (
+
+ );
};
export const CustomMatcher: Story = {
From 7615fa132fa6ee28b8fbd14d2a9b6fb912b88197 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:15:15 +0200
Subject: [PATCH 06/21] test(EuiComboBox): update input options placement
---
src/components/combo_box/combo_box.test.tsx | 49 ++++++++++++++-------
1 file changed, 33 insertions(+), 16 deletions(-)
diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx
index 10643adf2cc..34e9d3ddea3 100644
--- a/src/components/combo_box/combo_box.test.tsx
+++ b/src/components/combo_box/combo_box.test.tsx
@@ -223,24 +223,24 @@ describe('EuiComboBox', () => {
});
describe('toolTipContent & tooltipProps', () => {
- const options = [
- {
- label: 'Titan',
- 'data-test-subj': 'titanOption',
- toolTipContent: 'I am a tooltip!',
- toolTipProps: {
- 'data-test-subj': 'optionToolTip',
+ it('renders a tooltip with applied props on mouseover', async () => {
+ const options = [
+ {
+ label: 'Titan',
+ 'data-test-subj': 'titanOption',
+ toolTipContent: 'I am a tooltip!',
+ toolTipProps: {
+ 'data-test-subj': 'optionToolTip',
+ },
},
- },
- {
- label: 'Enceladus',
- },
- {
- label: 'Mimas',
- },
- ];
+ {
+ label: 'Enceladus',
+ },
+ {
+ label: 'Mimas',
+ },
+ ];
- it('renders a tooltip with applied props on mouseover', async () => {
const { getByTestSubject } = render( );
await showEuiComboBoxOptions();
@@ -255,6 +255,23 @@ describe('EuiComboBox', () => {
});
it('renders a tooltip with applied props on focus', async () => {
+ const options = [
+ {
+ label: 'Titan',
+ 'data-test-subj': 'titanOption',
+ toolTipContent: 'I am a tooltip!',
+ toolTipProps: {
+ 'data-test-subj': 'optionToolTip',
+ },
+ },
+ {
+ label: 'Enceladus',
+ },
+ {
+ label: 'Mimas',
+ },
+ ];
+
const { getByTestSubject } = render( );
await showEuiComboBoxOptions();
From ad0a3c5fd8d1b11c535a5cb620422b838809c437 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:43:28 +0200
Subject: [PATCH 07/21] refactor(EuiComboBox): prevent tooltip props from being
applied on input
---
src/components/combo_box/combo_box_input/combo_box_input.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/components/combo_box/combo_box_input/combo_box_input.tsx b/src/components/combo_box/combo_box_input/combo_box_input.tsx
index 4453cf2379b..2b60ad1d59d 100644
--- a/src/components/combo_box/combo_box_input/combo_box_input.tsx
+++ b/src/components/combo_box/combo_box_input/combo_box_input.tsx
@@ -190,6 +190,8 @@ export class EuiComboBoxInput extends Component<
append,
prepend,
truncationProps,
+ toolTipContent,
+ toolTipProps,
...rest
} = option;
const pillOnClose =
From 3c66b4359c44e5247c550659df862fbc54e0e7a2 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:43:58 +0200
Subject: [PATCH 08/21] docs(EuiComboBox): add EuiDocs example
---
.../src/views/combo_box/combo_box_example.js | 39 ++++++++
src-docs/src/views/combo_box/tool_tips.js | 99 +++++++++++++++++++
2 files changed, 138 insertions(+)
create mode 100644 src-docs/src/views/combo_box/tool_tips.js
diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js
index 010edf189ca..69c6c90e2f5 100644
--- a/src-docs/src/views/combo_box/combo_box_example.js
+++ b/src-docs/src/views/combo_box/combo_box_example.js
@@ -83,6 +83,26 @@ const renderOptionSnippet = ` `;
+import ToolTips from './tool_tips';
+const toolTipsSource = require('!!raw-loader!./tool_tips');
+const toolTipsSnippet = ` `;
+
import Truncation from './truncation';
const truncationSource = require('!!raw-loader!./truncation');
const truncationSnippet = `
+ You can add tooltips to the options by passing{' '}
+ toolTipContent . Use toolTipProps {' '}
+ to pass additional EuiToolTipProps to the tooltip.
+
+ ),
+ props: { EuiComboBox, EuiComboBoxOptionOption },
+ snippet: toolTipsSnippet,
+ demo: ,
+ },
{
title: 'Truncation',
source: [
diff --git a/src-docs/src/views/combo_box/tool_tips.js b/src-docs/src/views/combo_box/tool_tips.js
new file mode 100644
index 00000000000..fa2f3aec2b7
--- /dev/null
+++ b/src-docs/src/views/combo_box/tool_tips.js
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+
+import { EuiComboBox } from '../../../../src/components';
+import { DisplayToggles } from '../form_controls/display_toggles';
+
+const optionsStatic = [
+ {
+ label: 'Titan',
+ 'data-test-subj': 'titanOption',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Enceladus is disabled',
+ disabled: true,
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Mimas',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Dione',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Iapetus',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Phoebe',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Rhea',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label:
+ "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Tethys',
+ toolTipContent: 'Lorem ipsum',
+ },
+ {
+ label: 'Hyperion',
+ toolTipContent: 'Lorem ipsum',
+ },
+];
+export default () => {
+ const [options, setOptions] = useState(optionsStatic);
+ const [selectedOptions, setSelected] = useState([options[2], options[4]]);
+
+ const onChange = (selectedOptions) => {
+ setSelected(selectedOptions);
+ };
+
+ const onCreateOption = (searchValue, flattenedOptions = []) => {
+ const normalizedSearchValue = searchValue.trim().toLowerCase();
+
+ if (!normalizedSearchValue) {
+ return;
+ }
+
+ const newOption = {
+ label: searchValue,
+ };
+
+ // Create the option if it doesn't exist.
+ if (
+ flattenedOptions.findIndex(
+ (option) => option.label.trim().toLowerCase() === normalizedSearchValue
+ ) === -1
+ ) {
+ setOptions([...options, newOption]);
+ }
+
+ // Select the option.
+ setSelected([...selectedOptions, newOption]);
+ };
+
+ return (
+ /* DisplayToggles wrapper for Docs only */
+
+
+
+ );
+};
From 7b5236cc7f46696b82bf3d32746d555028f7bb77 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:48:57 +0200
Subject: [PATCH 09/21] chore: add changelog
---
changelogs/upcoming/7700.md | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 changelogs/upcoming/7700.md
diff --git a/changelogs/upcoming/7700.md b/changelogs/upcoming/7700.md
new file mode 100644
index 00000000000..e22e6165dc2
--- /dev/null
+++ b/changelogs/upcoming/7700.md
@@ -0,0 +1,6 @@
+- Added support for `EuiTooltip` on options of `EuiComboBox`
+
+**Bug fixes**
+
+- Fixed a visual layout bug for `EuiComboBox` with `isLoading` in mobile views
+
From 664dc88a29fee78ad3ef0cae8e88536c40c41357 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 2 May 2024 12:20:24 +0200
Subject: [PATCH 10/21] refactor: revert adding isOpen on EuiToolTip and use
ref API for show/hide instead for options
- dry out prop types
- update tests
---
.../filter_group/filter_select_item.tsx | 24 +++++++++++--------
src/components/tool_tip/tool_tip.test.tsx | 20 ----------------
src/components/tool_tip/tool_tip.tsx | 17 -------------
3 files changed, 14 insertions(+), 47 deletions(-)
diff --git a/src/components/filter_group/filter_select_item.tsx b/src/components/filter_group/filter_select_item.tsx
index cb2a21bf441..618a846c3ca 100644
--- a/src/components/filter_group/filter_select_item.tsx
+++ b/src/components/filter_group/filter_select_item.tsx
@@ -6,15 +6,16 @@
* Side Public License, v 1.
*/
-import React, { ButtonHTMLAttributes, Component } from 'react';
+import React, { ButtonHTMLAttributes, Component, createRef } from 'react';
import classNames from 'classnames';
import { withEuiTheme, WithEuiThemeProps } from '../../services';
import { CommonProps } from '../common';
import { EuiFlexGroup, EuiFlexItem } from '../flex';
-import { EuiToolTip, EuiToolTipProps } from '../tool_tip';
+import { EuiToolTip } from '../tool_tip';
import { EuiIcon } from '../icon';
+import { EuiComboBoxOptionOption } from '../combo_box';
import { euiFilterSelectItemStyles } from './filter_select_item.styles';
@@ -25,14 +26,8 @@ export interface EuiFilterSelectItemProps
checked?: FilterChecked;
showIcons?: boolean;
isFocused?: boolean;
- /**
- * Optional custom tooltip content for the button
- */
- toolTipContent?: EuiToolTipProps['content'];
- /**
- * Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)**
- */
- toolTipProps?: Partial>;
+ toolTipContent?: EuiComboBoxOptionOption['toolTipContent'];
+ toolTipProps?: EuiComboBoxOptionOption['toolTipProps'];
}
const resolveIconAndColor = (checked?: FilterChecked) => {
@@ -64,6 +59,7 @@ export class EuiFilterSelectItemClass extends Component<
};
buttonRef: HTMLButtonElement | null = null;
+ tooltipRef = createRef();
state = {
hasFocus: false,
@@ -75,6 +71,14 @@ export class EuiFilterSelectItemClass extends Component<
}
};
+ toggleToolTip = (isFocused: boolean) => {
+ if (isFocused) {
+ this.tooltipRef?.current?.showToolTip();
+ } else {
+ this.tooltipRef?.current?.hideToolTip();
+ }
+ };
+
hasFocus = () => {
return this.state.hasFocus;
};
diff --git a/src/components/tool_tip/tool_tip.test.tsx b/src/components/tool_tip/tool_tip.test.tsx
index 7a11a5c0bb8..f534910f8cd 100644
--- a/src/components/tool_tip/tool_tip.test.tsx
+++ b/src/components/tool_tip/tool_tip.test.tsx
@@ -182,26 +182,6 @@ describe('EuiToolTip', () => {
});
});
- describe('isOpen', () => {
- it('shows/hides the tooltip', async () => {
- const { rerender } = render(
-
- Trigger
-
- );
-
- await waitForEuiToolTipVisible();
-
- rerender(
-
- Trigger
-
- );
-
- await waitForEuiToolTipHidden();
- });
- });
-
describe('ref methods', () => {
// Although we don't publicly recommend it, consumers may need to reach into EuiToolTip
// class methods to manually control visibility state via `show/hideToolTip`.
diff --git a/src/components/tool_tip/tool_tip.tsx b/src/components/tool_tip/tool_tip.tsx
index 65ed118beae..b9bdb19a436 100644
--- a/src/components/tool_tip/tool_tip.tsx
+++ b/src/components/tool_tip/tool_tip.tsx
@@ -98,10 +98,6 @@ export interface EuiToolTipProps extends CommonProps {
* Suggested position. If there is not enough room for it this will be changed.
*/
position: ToolTipPositions;
- /**
- * For controlled use to show/hide the tooltip
- */
- isOpen?: boolean;
/**
* When `true`, the tooltip's position is re-calculated when the user
* scrolls. This supports having fixed-position tooltip anchors.
@@ -157,10 +153,6 @@ export class EuiToolTip extends Component {
if (this.props.repositionOnScroll) {
window.addEventListener('scroll', this.positionToolTip, true);
}
-
- if (this.props.isOpen) {
- this.showToolTip();
- }
}
componentWillUnmount() {
@@ -174,14 +166,6 @@ export class EuiToolTip extends Component {
requestAnimationFrame(this.testAnchor);
}
- if (prevProps.isOpen !== this.props.isOpen) {
- if (this.props.isOpen) {
- this.showToolTip();
- } else {
- this.hideToolTip();
- }
- }
-
// update scroll listener
if (prevProps.repositionOnScroll !== this.props.repositionOnScroll) {
if (this.props.repositionOnScroll) {
@@ -323,7 +307,6 @@ export class EuiToolTip extends Component {
delay,
display,
repositionOnScroll,
- isOpen,
...rest
} = this.props;
From ffc84a9e3fb869301757ce9f13cadc9f9023173b Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 2 May 2024 12:21:12 +0200
Subject: [PATCH 11/21] refactor: use remove tooltip wrapper in favor of using
anchorProps for styles
---
.../filter_group/filter_select_item.tsx | 41 ++++++++++++-------
1 file changed, 27 insertions(+), 14 deletions(-)
diff --git a/src/components/filter_group/filter_select_item.tsx b/src/components/filter_group/filter_select_item.tsx
index 618a846c3ca..dbd24c108d9 100644
--- a/src/components/filter_group/filter_select_item.tsx
+++ b/src/components/filter_group/filter_select_item.tsx
@@ -106,8 +106,23 @@ export class EuiFilterSelectItemClass extends Component<
const classes = classNames('euiFilterSelectItem', className);
- const hasToolTip =
- !disabled && React.isValidElement(children) && toolTipContent;
+ const hasToolTip = !disabled && toolTipContent;
+ let anchorProps = undefined;
+
+ if (hasToolTip) {
+ const anchorStyles = toolTipProps?.anchorProps?.style
+ ? { ...toolTipProps?.anchorProps?.style, ...style }
+ : style;
+
+ anchorProps = toolTipProps?.anchorProps
+ ? {
+ ...toolTipProps.anchorProps,
+ style: anchorStyles,
+ }
+ : { style };
+
+ this.toggleToolTip(isFocused ?? false);
+ }
let iconNode;
if (showIcons) {
@@ -150,18 +165,16 @@ export class EuiFilterSelectItemClass extends Component<
);
return hasToolTip ? (
- // This extra wrapper is needed to ensure that the tooltip has a correct context
- // for positioning while also ensuring to wrap the interactive option
-
-
- {optionItem}
-
-
+
+ {optionItem}
+
) : (
optionItem
);
From 90197c5b096c456301487f08efd87cfb8cf7f9cd Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 2 May 2024 12:22:00 +0200
Subject: [PATCH 12/21] chore: add PR feedback
---
changelogs/upcoming/7700.md | 2 +-
.../combo_box/combo_box.stories.tsx | 41 +++----------------
src/components/combo_box/combo_box.test.tsx | 2 +-
3 files changed, 8 insertions(+), 37 deletions(-)
diff --git a/changelogs/upcoming/7700.md b/changelogs/upcoming/7700.md
index e22e6165dc2..8a77f98a47a 100644
--- a/changelogs/upcoming/7700.md
+++ b/changelogs/upcoming/7700.md
@@ -1,4 +1,4 @@
-- Added support for `EuiTooltip` on options of `EuiComboBox`
+- Updated `EuiComboBox`'s `options` to support including tooltip details for selectable options. Use `toolTipContent` to render tooltip information, and `toolTipProps` to optionally customize the tooltip rendering behavior
**Bug fixes**
diff --git a/src/components/combo_box/combo_box.stories.tsx b/src/components/combo_box/combo_box.stories.tsx
index 9f60a833567..dba7d675efe 100644
--- a/src/components/combo_box/combo_box.stories.tsx
+++ b/src/components/combo_box/combo_box.stories.tsx
@@ -10,7 +10,6 @@ import React, { useCallback, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
-import { hideStorybookControls } from '../../../.storybook/utils';
import { ToolTipPositions } from '../tool_tip';
import { EuiComboBox, EuiComboBoxProps } from './combo_box';
import { EuiComboBoxOptionMatcher } from './types';
@@ -18,7 +17,7 @@ import { EuiCode } from '../code';
const toolTipProps = {
toolTipContent: 'This is a tooltip!',
- toolTipProps: { position: 'bottom' as ToolTipPositions },
+ toolTipProps: { position: 'left' as ToolTipPositions },
value: 4,
};
@@ -76,44 +75,16 @@ export const Playground: Story = {
};
export const WithTooltip: Story = {
+ parameters: {
+ controls: {
+ include: ['fullWidth', 'options', 'selectedOptions'],
+ },
+ },
args: {
options: options.map((option) => ({ ...option, ...toolTipProps })),
},
render: (args) => ,
};
-// hide props as they are not relevant for testing the story args
-hideStorybookControls(WithTooltip, [
- 'append',
- 'aria-label',
- 'aria-labelledby',
- 'async',
- 'autoFocus',
- 'compressed',
- 'customOptionText',
- 'delimiter',
- 'id',
- 'inputPopoverProps',
- 'inputRef',
- 'isCaseSensitive',
- 'isClearable',
- 'isDisabled',
- 'isInvalid',
- 'isLoading',
- 'noSuggestions',
- 'onBlur',
- 'onChange',
- 'onCreateOption',
- 'onFocus',
- 'onKeyDown',
- 'onSearchChange',
- 'placeholder',
- 'prepend',
- 'renderOption',
- 'rowHeight',
- 'singleSelection',
- 'sortMatchesBy',
- 'truncationProps',
-]);
const StatefulComboBox = ({
singleSelection,
diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx
index 34e9d3ddea3..00533c38e36 100644
--- a/src/components/combo_box/combo_box.test.tsx
+++ b/src/components/combo_box/combo_box.test.tsx
@@ -254,7 +254,7 @@ describe('EuiComboBox', () => {
);
});
- it('renders a tooltip with applied props on focus', async () => {
+ it('renders a tooltip with applied props on keyboard navigation', async () => {
const options = [
{
label: 'Titan',
From fee66ff325c06a751399649dcbb9c6c5696254f2 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 2 May 2024 12:22:32 +0200
Subject: [PATCH 13/21] docs: update EuiComboBox tooltip docs example
---
src-docs/src/views/combo_box/tool_tips.js | 99 ----------------------
src-docs/src/views/combo_box/tool_tips.tsx | 43 ++++++++++
2 files changed, 43 insertions(+), 99 deletions(-)
delete mode 100644 src-docs/src/views/combo_box/tool_tips.js
create mode 100644 src-docs/src/views/combo_box/tool_tips.tsx
diff --git a/src-docs/src/views/combo_box/tool_tips.js b/src-docs/src/views/combo_box/tool_tips.js
deleted file mode 100644
index fa2f3aec2b7..00000000000
--- a/src-docs/src/views/combo_box/tool_tips.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { useState } from 'react';
-
-import { EuiComboBox } from '../../../../src/components';
-import { DisplayToggles } from '../form_controls/display_toggles';
-
-const optionsStatic = [
- {
- label: 'Titan',
- 'data-test-subj': 'titanOption',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Enceladus is disabled',
- disabled: true,
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Mimas',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Dione',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Iapetus',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Phoebe',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Rhea',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label:
- "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Tethys',
- toolTipContent: 'Lorem ipsum',
- },
- {
- label: 'Hyperion',
- toolTipContent: 'Lorem ipsum',
- },
-];
-export default () => {
- const [options, setOptions] = useState(optionsStatic);
- const [selectedOptions, setSelected] = useState([options[2], options[4]]);
-
- const onChange = (selectedOptions) => {
- setSelected(selectedOptions);
- };
-
- const onCreateOption = (searchValue, flattenedOptions = []) => {
- const normalizedSearchValue = searchValue.trim().toLowerCase();
-
- if (!normalizedSearchValue) {
- return;
- }
-
- const newOption = {
- label: searchValue,
- };
-
- // Create the option if it doesn't exist.
- if (
- flattenedOptions.findIndex(
- (option) => option.label.trim().toLowerCase() === normalizedSearchValue
- ) === -1
- ) {
- setOptions([...options, newOption]);
- }
-
- // Select the option.
- setSelected([...selectedOptions, newOption]);
- };
-
- return (
- /* DisplayToggles wrapper for Docs only */
-
-
-
- );
-};
diff --git a/src-docs/src/views/combo_box/tool_tips.tsx b/src-docs/src/views/combo_box/tool_tips.tsx
new file mode 100644
index 00000000000..20a30d2c7ce
--- /dev/null
+++ b/src-docs/src/views/combo_box/tool_tips.tsx
@@ -0,0 +1,43 @@
+import React, { useState } from 'react';
+
+import {
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+} from '../../../../src/components';
+
+const options: Array> = [
+ {
+ label: 'Titan',
+ toolTipContent:
+ 'Titan is the largest moon of Saturn and the second-largest in the Solar System',
+ },
+ {
+ label: 'Pandora',
+ toolTipContent:
+ "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
+ },
+ {
+ label: 'Iapetus',
+ toolTipContent: "Iapetus is the outermost of Saturn's large moons",
+ toolTipProps: { position: 'bottom' },
+ },
+];
+export default () => {
+ const [selectedOptions, setSelected] = useState([options[2]]);
+
+ const onChange = (
+ selectedOptions: Array>
+ ) => {
+ setSelected(selectedOptions);
+ };
+
+ return (
+
+ );
+};
From 67a9db73b2c4124d916338be3452ac134f420a27 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 2 May 2024 13:13:36 +0200
Subject: [PATCH 14/21] chore: cleanup wrongly removed type check
---
src/components/filter_group/filter_select_item.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/components/filter_group/filter_select_item.tsx b/src/components/filter_group/filter_select_item.tsx
index dbd24c108d9..11c65a68cc2 100644
--- a/src/components/filter_group/filter_select_item.tsx
+++ b/src/components/filter_group/filter_select_item.tsx
@@ -106,7 +106,8 @@ export class EuiFilterSelectItemClass extends Component<
const classes = classNames('euiFilterSelectItem', className);
- const hasToolTip = !disabled && toolTipContent;
+ const hasToolTip =
+ !disabled && React.isValidElement(children) && toolTipContent;
let anchorProps = undefined;
if (hasToolTip) {
From b8ebf6159ca5539aa9f7fcdb953f846fd4c1b330 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 2 May 2024 17:32:31 +0200
Subject: [PATCH 15/21] chore: add comment
---
src/components/filter_group/filter_select_item.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/components/filter_group/filter_select_item.tsx b/src/components/filter_group/filter_select_item.tsx
index 11c65a68cc2..492069a9347 100644
--- a/src/components/filter_group/filter_select_item.tsx
+++ b/src/components/filter_group/filter_select_item.tsx
@@ -107,7 +107,10 @@ export class EuiFilterSelectItemClass extends Component<
const classes = classNames('euiFilterSelectItem', className);
const hasToolTip =
- !disabled && React.isValidElement(children) && toolTipContent;
+ // we're using isValidElement here as EuiToolTipAnchor uses
+ // cloneElement to enhance the element with required attributes
+ React.isValidElement(children) && !disabled && toolTipContent;
+
let anchorProps = undefined;
if (hasToolTip) {
From 7e48f0ab18f1e522cf6f6a9862c0768ffb81dd62 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 2 May 2024 11:54:30 -0700
Subject: [PATCH 16/21] Fix Firefox behavior by checking for
`scrollTarget.contains`
+ clean up comments
+ remove now-unnecessary setTimeout workaround, this one is stronker
---
src/components/popover/input_popover.tsx | 35 +++++++++++++-----------
1 file changed, 19 insertions(+), 16 deletions(-)
diff --git a/src/components/popover/input_popover.tsx b/src/components/popover/input_popover.tsx
index 99fdbead25b..c79ba2d0b40 100644
--- a/src/components/popover/input_popover.tsx
+++ b/src/components/popover/input_popover.tsx
@@ -169,33 +169,36 @@ export const EuiInputPopover: FunctionComponent = ({
useEffect(() => {
// When the popover opens, add a scroll listener to the page (& remove it after)
if (closeOnScroll && panelEl) {
- // Close the popover, but only if the scroll event occurs outside the input or the popover itself
const closePopoverOnScroll = (event: Event) => {
- if (!panelEl || !inputEl || !event.target) return;
const scrollTarget = event.target as Node;
- if (
- panelEl.contains(scrollTarget) === false &&
- inputEl.contains(scrollTarget) === false
- ) {
- closePopover();
+ // Basic existence check
+ if (!panelEl || !inputEl || !scrollTarget) {
+ return;
}
+ // Do not close the popover if the input or popover itself was scrolled
+ if (panelEl.contains(scrollTarget) || inputEl.contains(scrollTarget)) {
+ return;
+ }
+ // Firefox will trigger a scroll event in many common situations (e.g. docs side nav)
+ // when the options list div is appended to the DOM. To work around this, we should
+ // check if the element that scrolled actually contains/will affect the input
+ if (!scrollTarget.contains(inputEl)) {
+ return;
+ }
+
+ closePopover();
};
- // Firefox will trigger a scroll event in many common situations when the options list div is appended
- // to the DOM; in testing it was always within 100ms, but setting a timeout here for 500ms to be safe
- const timeoutId = setTimeout(() => {
- window.addEventListener('scroll', closePopoverOnScroll, {
- passive: true, // for better performance as we won't call preventDefault
- capture: true, // scroll events don't bubble, they must be captured instead
- });
- }, 500);
+ window.addEventListener('scroll', closePopoverOnScroll, {
+ passive: true, // for better performance as we won't call preventDefault
+ capture: true, // scroll events don't bubble, they must be captured instead
+ });
return () => {
window.removeEventListener('scroll', closePopoverOnScroll, {
capture: true,
});
- clearTimeout(timeoutId);
};
}
}, [closeOnScroll, closePopover, panelEl, inputEl]);
From aa016c1980c763bc40a42dcd5684266d40893bd6 Mon Sep 17 00:00:00 2001
From: Cee Chen
Date: Thu, 2 May 2024 12:48:08 -0700
Subject: [PATCH 17/21] Update/add Cypress tests
---
src/components/popover/input_popover.spec.tsx | 55 +++++++++++--------
1 file changed, 33 insertions(+), 22 deletions(-)
diff --git a/src/components/popover/input_popover.spec.tsx b/src/components/popover/input_popover.spec.tsx
index 10f17ca37d1..8a3b0a18938 100644
--- a/src/components/popover/input_popover.spec.tsx
+++ b/src/components/popover/input_popover.spec.tsx
@@ -236,44 +236,55 @@ describe('EuiPopover', () => {
const [isOpen, setIsOpen] = useState(true);
return (
-
-
setIsOpen(false)}
- input={
- setIsOpen(true)}
- rows={1}
- defaultValue={`hello\nworld`}
- />
- }
- >
-
+
+
setIsOpen(false)}
+ input={
+ setIsOpen(true)}
+ rows={1}
+ defaultValue={`hello\nworld`}
+ />
+ }
>
- Popover content
-
-
+
+
+
+
);
};
it('closes the popover when the user scrolls outside of the component', () => {
cy.mount( );
- cy.wait(500); // Wait for the setTimeout in the useEffect
// Scrolling the input or popover should not close the popover
cy.get('[data-test-subj="inputWithScroll"]').scrollTo('bottom');
cy.get('[data-popover-panel]').should('exist');
+ // Scrolling an element that doesn't contain/affect the input should not close the popover
+ cy.get('[data-test-subj="scrollingSibling"]').scrollTo('bottom');
+ cy.get('[data-popover-panel]').should('exist');
+
cy.get('[data-test-subj="popoverWithScroll"]').scrollTo('bottom');
cy.wait(500); // Wait a tick for false positives
cy.get('[data-popover-panel]').should('exist');
- // Scrolling anywhere else should close the popover
+ // Scrolling the actual body should close the popover
cy.scrollTo('bottom');
cy.get('[data-popover-panel]').should('not.exist');
From dbd8866e47fec8903a7d200ebf6aceb982a143d5 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Fri, 3 May 2024 09:54:11 +0200
Subject: [PATCH 18/21] test(VRT): add base snapshots for EuiComboBox
---
...me_desktop_Forms_EuiComboBox_With_Tooltip.png | Bin 0 -> 2171 bytes
...ome_mobile_Forms_EuiComboBox_With_Tooltip.png | Bin 0 -> 4950 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 .loki/reference/chrome_desktop_Forms_EuiComboBox_With_Tooltip.png
create mode 100644 .loki/reference/chrome_mobile_Forms_EuiComboBox_With_Tooltip.png
diff --git a/.loki/reference/chrome_desktop_Forms_EuiComboBox_With_Tooltip.png b/.loki/reference/chrome_desktop_Forms_EuiComboBox_With_Tooltip.png
new file mode 100644
index 0000000000000000000000000000000000000000..b78bb812930527b67a1e9f111844bec9a8c77f73
GIT binary patch
literal 2171
zcmV->2!!{EP)l00001b5ch_0Itp)
z=>Px#1am@3R0s$N2z&@+hyVZxB}qgLWqb3OGHG`L)UdE
z%5a|W`V_>+njlFM#E@6f-MxdbSd55|HAOm3A|i@neI6hDeihb)d60a56_O+|c@PoN
zq!QxH5JJG`S0Q2QdMqQ<0MSUr7%ShOI1P9_fku$YZl
zv>+ZmeM6BBg@`DsOqxLa0d>wMj}aELaZYyM5fM!!q%ja8A_||52X1`P)P|nE0rd3r
z0|2bncvur+u_7fINegWej&OW4HVZ2`IT@EPUj_i2KYt!=ZEZ7k=`lJxJF#igCahVr
zCT#a0A__NE)$s4yn|S}+C%ARH7403}@c9Gq`2%S0=*F$vt$6?3C%9UB6Pgx%8{CFv
z8gE%yS=bUte}6wRGc%Eqkr7h;;^Jb|*4BMAuB5jcDo&wm6a$cD8R;z8T_re&_)XjJTDJdy|
z-EPN;6DN?Hn+uoAg=5E#;mVaOxO3+YT3T9g;J^U@fGo=h1Olk3sX<9e3G(yvQCeDx
z3l}axk|fmB)Qq`)TwEMdQ&U5ZCp|qK)z#I=$;m-;b2FNon~{@~gX-#Pq^GCPSi6ac
zLe&7&Xi27>{czxypDEwl#9o=LLd<;?Xk7)C#)Z#3SW@Y=U;Hz9BPkMP#p2Pci=
zt+x+i+xA^(Yn$G)Hte&o+-^7a@86F@hYkS%R8_^|#fyXA$HWY;@RlSg`1i=?k*=YXTY*@D%m;TcLRnx%;O961t>+m5TQt`6DR*{G_jg57S1-EK!!RTZ+cvr$)9
zH*0Ya5si4bTPjEZzCZwe|L_sxkE7uAT_`Gi10Q|-PyFV$zk}Z&fY0Z{FW)T2#Y>k_
zRQSeYB@ZA>Zy-52Impe;#lC&}P*_-q`ucigWMn|oG^C}a;qc+Z)4q0jc{%d)^P%fH
z0)YT577JFdUX5$lu8mv&`0?X#xm;Mje0i{J)v8rEb?OvOo;=BcbRwE{^KFUv@_y&2
zvPIvp;l;HMT)0vXApzf6zXt9e585BN$NV-_JDl5~C<+e!^&PzVt752{hEG5J423@{
zzz*lO$8Lvcz1OQhlN9&J3OAeDv1a+=M}9w7)HmM8%9KTs4ugoE0#|Eqj+%uf34tHH
zya5-k)}ynl7XXm7(1s0b9XRukOVGZyP$b=vg7q(~oYMXNe(WR;{N--|fL|2-6tBLT
z``B&xZ-<9lnGg}pwiVAki~5_*!PVuOuEX`=CFoyE6de!TW0Hp~8?fTJXQzC>AMVJ*
z`gIwA5J+1Q`Z9>g@IWadqFI(0AB&9VSBxst$61c9JyfqooEp7RXCJ%rK
zgor32(j6&SU`q(O{sLP99BE6#7}3-*fe;ZzfDi)NFKs}&BW1!FS3&^KJ5rGS(uSy8
z!#3#{h{
zMMOl?6Hrvh1__eIY=mbZ5b1b`i0BF785mw%E}2ckE3ErGz6ggzL`2c9&*Os-fZ1e#
z81gE*s_N+O9fZLk5Eo;D*<^V1iCIKMG^ePl2A^MrXV4E#0~T24K?pINKt?M0{0h82
x1&X4LyR|S85zRe92*|Poi&=)*^#Aqe{{vJ`$bz@&A+!Jh002ovPDHLkV1gVV3$FkG
literal 0
HcmV?d00001
diff --git a/.loki/reference/chrome_mobile_Forms_EuiComboBox_With_Tooltip.png b/.loki/reference/chrome_mobile_Forms_EuiComboBox_With_Tooltip.png
new file mode 100644
index 0000000000000000000000000000000000000000..eba304a0eb9b24c3680b2980af4c47fde93372ce
GIT binary patch
literal 4950
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
KQkjBv@c#hj!l@hp
literal 0
HcmV?d00001
From 8c1b6934d8be73a8daec32ec9c734d4184972eb4 Mon Sep 17 00:00:00 2001
From: Tomasz Kajtoch
Date: Mon, 29 Apr 2024 16:55:13 +0200
Subject: [PATCH 19/21] feat: Add `optionMatcher` prop to `EuiSelectable` and
`EuiComboBox` components (#7709)
---
changelogs/upcoming/7709.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 changelogs/upcoming/7709.md
diff --git a/changelogs/upcoming/7709.md b/changelogs/upcoming/7709.md
new file mode 100644
index 00000000000..a1bf329af06
--- /dev/null
+++ b/changelogs/upcoming/7709.md
@@ -0,0 +1 @@
+- Added a new, optional `optionMatcher` prop to `EuiSelectable` and `EuiComboBox` allowing passing a custom option matcher function to these components and controlling option filtering for given search string
From c50b604148147494219dd88050309983980ce5f9 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Thu, 18 Apr 2024 23:06:01 +0200
Subject: [PATCH 20/21] docs(storybook): add separate tooltip story for
ComboBox
---
.../combo_box/combo_box.stories.tsx | 76 +++++++++----------
1 file changed, 38 insertions(+), 38 deletions(-)
diff --git a/src/components/combo_box/combo_box.stories.tsx b/src/components/combo_box/combo_box.stories.tsx
index dba7d675efe..27827b45387 100644
--- a/src/components/combo_box/combo_box.stories.tsx
+++ b/src/components/combo_box/combo_box.stories.tsx
@@ -86,44 +86,6 @@ export const WithTooltip: Story = {
render: (args) => ,
};
-const StatefulComboBox = ({
- singleSelection,
- onCreateOption,
- ...args
-}: EuiComboBoxProps<{}>) => {
- const [selectedOptions, setSelectedOptions] = useState(args.selectedOptions);
- const onChange: EuiComboBoxProps<{}>['onChange'] = (options, ...args) => {
- setSelectedOptions(options);
- action('onChange')(options, ...args);
- };
- const _onCreateOption: EuiComboBoxProps<{}>['onCreateOption'] = (
- searchValue,
- ...args
- ) => {
- const createdOption = { label: searchValue };
- setSelectedOptions((prevState) =>
- !prevState || singleSelection
- ? [createdOption]
- : [...prevState, createdOption]
- );
- action('onCreateOption')(searchValue, ...args);
- };
- return (
-
- );
-};
-
export const CustomMatcher: Story = {
render: function Render({ singleSelection, onCreateOption, ...args }) {
const [selectedOptions, setSelectedOptions] = useState(
@@ -165,3 +127,41 @@ export const CustomMatcher: Story = {
);
},
};
+
+const StatefulComboBox = ({
+ singleSelection,
+ onCreateOption,
+ ...args
+}: EuiComboBoxProps<{}>) => {
+ const [selectedOptions, setSelectedOptions] = useState(args.selectedOptions);
+ const onChange: EuiComboBoxProps<{}>['onChange'] = (options, ...args) => {
+ setSelectedOptions(options);
+ action('onChange')(options, ...args);
+ };
+ const _onCreateOption: EuiComboBoxProps<{}>['onCreateOption'] = (
+ searchValue,
+ ...args
+ ) => {
+ const createdOption = { label: searchValue };
+ setSelectedOptions((prevState) =>
+ !prevState || singleSelection
+ ? [createdOption]
+ : [...prevState, createdOption]
+ );
+ action('onCreateOption')(searchValue, ...args);
+ };
+ return (
+
+ );
+};
From 9929c3e3d89ece2723a1b7a16046bfb6cb4a12d5 Mon Sep 17 00:00:00 2001
From: Lene Gadewoll
Date: Fri, 3 May 2024 18:39:14 +0200
Subject: [PATCH 21/21] chore: cleanup
-use const as type
- revert newline changes
---
src/components/combo_box/combo_box.stories.tsx | 3 +--
src/components/combo_box/combo_box.test.tsx | 11 +++++++++++
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/components/combo_box/combo_box.stories.tsx b/src/components/combo_box/combo_box.stories.tsx
index 27827b45387..771f277889c 100644
--- a/src/components/combo_box/combo_box.stories.tsx
+++ b/src/components/combo_box/combo_box.stories.tsx
@@ -10,14 +10,13 @@ import React, { useCallback, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
-import { ToolTipPositions } from '../tool_tip';
import { EuiComboBox, EuiComboBoxProps } from './combo_box';
import { EuiComboBoxOptionMatcher } from './types';
import { EuiCode } from '../code';
const toolTipProps = {
toolTipContent: 'This is a tooltip!',
- toolTipProps: { position: 'left' as ToolTipPositions },
+ toolTipProps: { position: 'left' as const },
value: 4,
};
diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx
index 00533c38e36..251222921e6 100644
--- a/src/components/combo_box/combo_box.test.tsx
+++ b/src/components/combo_box/combo_box.test.tsx
@@ -525,21 +525,26 @@ describe('EuiComboBox', () => {
describe('tabbing off the search input', () => {
it("closes the options list if the user isn't navigating the options", async () => {
const keyDownBubbled = jest.fn();
+
const { getByTestSubject } = render(
);
await showEuiComboBoxOptions();
+
const mockEvent = { key: keys.TAB, shiftKey: true };
fireEvent.keyDown(getByTestSubject('comboBoxSearchInput'), mockEvent);
+
// If the TAB keydown bubbled up to the wrapper, then a browser DOM would shift the focus
expect(keyDownBubbled).toHaveBeenCalledWith(
expect.objectContaining(mockEvent)
);
});
+
it('calls onCreateOption', () => {
const onCreateOptionHandler = jest.fn();
+
const { getByTestSubject } = render(
{
/>
);
const input = getByTestSubject('comboBoxSearchInput');
+
fireEvent.change(input, { target: { value: 'foo' } });
fireEvent.blur(input);
+
expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
expect(onCreateOptionHandler).toHaveBeenCalledWith('foo', options);
});
+
it('does nothing if the user is navigating the options', async () => {
const keyDownBubbled = jest.fn();
+
const { getByTestSubject } = render(
);
await showEuiComboBoxOptions();
+
// Navigate to an option then tab off
const input = getByTestSubject('comboBoxSearchInput');
fireEvent.keyDown(input, { key: keys.ARROW_DOWN });
fireEvent.keyDown(input, { key: keys.TAB });
+
// If the TAB keydown did not bubble to the wrapper, then the tab event was prevented
expect(keyDownBubbled).not.toHaveBeenCalled();
});