From 2a961aac29bf9d60436805d98e17ede2362cdfe4 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 26 Apr 2024 14:49:17 +0200 Subject: [PATCH 01/18] feat(EuiToolTip): add controlled isOpen support duplicate of b181317234d38e686da913766b76587da11cfef6 --- src/components/tool_tip/tool_tip.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 1fe33d4ca87b7eb4b5bc030014d79a0a39b6e745 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 26 Apr 2024 15:17:54 +0200 Subject: [PATCH 02/18] feat(EuiSelectable): add support for toolTipContent and toolTipProps for options --- .../_selectable_list_item.scss | 4 ++ .../selectable_list/selectable_list_item.tsx | 71 ++++++++++++++----- .../selectable/selectable_option.tsx | 9 +++ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/src/components/selectable/selectable_list/_selectable_list_item.scss b/src/components/selectable/selectable_list/_selectable_list_item.scss index 9ff1966fef4..0c67fcf57f8 100644 --- a/src/components/selectable/selectable_list/_selectable_list_item.scss +++ b/src/components/selectable/selectable_list/_selectable_list_item.scss @@ -56,3 +56,7 @@ @include euiTextTruncate; } } + +.euiSelectableListItem__tooltipAnchor { + width: 100%; +} diff --git a/src/components/selectable/selectable_list/selectable_list_item.tsx b/src/components/selectable/selectable_list/selectable_list_item.tsx index cd41811fee8..24ce8105e91 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.tsx @@ -7,13 +7,14 @@ */ import classNames from 'classnames'; -import React, { Component, LiHTMLAttributes } from 'react'; +import React, { AriaAttributes, Component, LiHTMLAttributes } from 'react'; import { CommonProps, keysOf } from '../../common'; import { EuiI18n } from '../../i18n'; import { EuiIcon, IconColor, IconType } from '../../icon'; import { EuiScreenReaderOnly } from '../../accessibility'; import { EuiBadge, EuiBadgeProps } from '../../badge'; +import { EuiToolTip, EuiToolTipProps } from '../../tool_tip'; import type { EuiSelectableOption, @@ -90,6 +91,14 @@ export type EuiSelectableListItemProps = LiHTMLAttributes & * Wrapping only works if virtualization is off. */ textWrap?: EuiSelectableOption['textWrap']; + /** + * Optional custom tooltip content for the button + */ + toolTipContent?: EuiToolTipProps['content']; + /** + * Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)** + */ + toolTipProps?: Partial>; }; export class EuiSelectableListItem extends Component { @@ -147,7 +156,10 @@ export class EuiSelectableListItem extends Component paddingSize = 's', role = 'option', searchable, + style, textWrap, + toolTipContent, + toolTipProps, ...rest } = this.props; @@ -164,6 +176,8 @@ export class EuiSelectableListItem extends Component [`euiSelectableListItem__text--${textWrap}`]: textWrap, }); + const hasToolTip = !disabled && toolTipContent; + let optionIcon: React.ReactNode; if (showIcons) { const { icon, color } = resolveIconAndColor(checked); @@ -337,24 +351,45 @@ export class EuiSelectableListItem extends Component ); - return ( -
  • - - {optionIcon} - {prependNode} - - {children} - {screenReaderText} - - {appendNode} + const content = ( + + {optionIcon} + {prependNode} + + {children} + {screenReaderText} + {appendNode} + + ); + + const optionProps = { + role: role, + 'aria-disabled': disabled, + 'aria-checked': this.isChecked( + role, + checked + ) as AriaAttributes['aria-checked'], // Whether the item is "checked" + 'aria-selected': !disabled && isFocused, // Whether the item has keyboard focus per W3 spec + ...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 +
  • + + {content} + +
  • + ) : ( +
  • + {content}
  • ); } diff --git a/src/components/selectable/selectable_option.tsx b/src/components/selectable/selectable_option.tsx index 1e1ff0e2093..70ac21e2529 100644 --- a/src/components/selectable/selectable_option.tsx +++ b/src/components/selectable/selectable_option.tsx @@ -9,6 +9,7 @@ import React, { HTMLAttributes } from 'react'; import { CommonProps, ExclusiveUnion } from '../common'; import type { EuiTextTruncateProps } from '../text_truncate'; +import { EuiToolTipProps } from '../tool_tip'; export type EuiSelectableOptionCheckedType = 'on' | 'off' | 'mixed' | undefined; @@ -74,6 +75,14 @@ export type EuiSelectableOptionBase = CommonProps & { * text will always take precedence. */ truncationProps?: Partial>; + /** + * Optional custom tooltip content for the button + */ + toolTipContent?: EuiToolTipProps['content']; + /** + * Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)** + */ + toolTipProps?: Partial>; }; type _EuiSelectableGroupLabelOption = Omit< From ae9cf1d8c21a33330708ad09284a1c99f7c5ea45 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 26 Apr 2024 15:27:19 +0200 Subject: [PATCH 03/18] test(EuiSelectable): add test for tooltips on options --- .../selectable_list_item.test.tsx.snap | 144 ++++++++++++++++++ .../selectable_list/selectable_list.test.tsx | 78 +++++++++- .../selectable_list_item.test.tsx | 47 +++++- 3 files changed, 262 insertions(+), 7 deletions(-) diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap index e14f6d4dfa9..19905d2f6c5 100644 --- a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap +++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap @@ -506,3 +506,147 @@ exports[`EuiSelectableListItem props textWrap can be "wrap" 1`] = ` `; + +exports[`EuiSelectableListItem props tooltip behavior on mouseover 1`] = ` + +
    +
  • + + + + + + Item content + + + + +
  • +
    +
    + + +`; + +exports[`EuiSelectableListItem props tooltip behavior when isFocused 1`] = ` + +
    +
  • + + + + + + Item content + + + + + + + +
  • +
    +
    + + +`; diff --git a/src/components/selectable/selectable_list/selectable_list.test.tsx b/src/components/selectable/selectable_list/selectable_list.test.tsx index aa6484b7d88..57299444e67 100644 --- a/src/components/selectable/selectable_list/selectable_list.test.tsx +++ b/src/components/selectable/selectable_list/selectable_list.test.tsx @@ -7,7 +7,8 @@ */ import React from 'react'; -import { render } from '../../../test/rtl'; +import { fireEvent } from '@testing-library/react'; +import { render, waitForEuiToolTipVisible } from '../../../test/rtl'; import { requiredProps } from '../../../test/required_props'; import { EuiSelectableList } from './selectable_list'; @@ -78,13 +79,11 @@ describe('EuiSelectableListItem', () => { searchValue="Mi" /> ); - expect(container.querySelector('.euiMark')).toHaveTextContent('Mi'); expect( container.querySelector('.euiTextTruncate') ).not.toBeInTheDocument(); }); - it('renders an EuiTextTruncate component when truncating text', () => { const { container, getByTestSubject } = render( { searchValue="titan" /> ); - expect(getByTestSubject('titanOption')).toContainElement( container.querySelector('.euiTextTruncate') ); }); - it('does not highlight/mark the current `searchValue` if `isPreFiltered.highlightSearch` is false', () => { const { container } = render( { searchValue="Mi" /> ); - expect(container.querySelector('.euiMark')).not.toBeInTheDocument(); }); }); @@ -389,6 +385,76 @@ describe('EuiSelectableListItem', () => { expect(container.querySelector('.euiTextTruncate')).toBeInTheDocument(); }); }); + + describe('toolTipContent & tooltipProps', () => { + 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', + }, + ]; + + const { getByTestSubject } = render( + + ); + + fireEvent.mouseOver(getByTestSubject('titanOption')); + await waitForEuiToolTipVisible(); + + expect(getByTestSubject('optionToolTip')).toBeInTheDocument(); + expect(getByTestSubject('optionToolTip')).toHaveTextContent( + 'I am a tooltip!' + ); + }); + + it('renders a tooltip with applied props when activeOptionIndex is set', 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 waitForEuiToolTipVisible(); + + expect(getByTestSubject('optionToolTip')).toBeInTheDocument(); + expect(getByTestSubject('optionToolTip')).toHaveTextContent( + 'I am a tooltip!' + ); + }); + }); }); describe('truncation performance optimization', () => { diff --git a/src/components/selectable/selectable_list/selectable_list_item.test.tsx b/src/components/selectable/selectable_list/selectable_list_item.test.tsx index 20342768c88..972a2e036f0 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.test.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.test.tsx @@ -7,10 +7,11 @@ */ import React from 'react'; -import { render } from '../../../test/rtl'; +import { render, waitForEuiToolTipVisible } from '../../../test/rtl'; import { requiredProps } from '../../../test/required_props'; import { EuiSelectableListItem, PADDING_SIZES } from './selectable_list_item'; +import { fireEvent } from '@testing-library/react'; describe('EuiSelectableListItem', () => { test('is rendered', () => { @@ -162,5 +163,49 @@ describe('EuiSelectableListItem', () => { expect(container.firstChild).toMatchSnapshot(); }); }); + + test('tooltip behavior on mouseover', async () => { + const { baseElement, getByRole, getByTestSubject } = render( + + Item content + + ); + + fireEvent.mouseOver(getByRole('option')); + await waitForEuiToolTipVisible(); + + expect(getByTestSubject('listItemToolTip')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + + test('tooltip behavior when isFocused', async () => { + const { baseElement, getByTestSubject } = render( + + Item content + + ); + + await waitForEuiToolTipVisible(); + + expect(getByTestSubject('listItemToolTip')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); }); }); From 5cd93d4631e5816c80633791ef6752bf15f3b503 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 26 Apr 2024 15:27:46 +0200 Subject: [PATCH 04/18] docs(storybook): add standalone tooltip story for EuiSelectable --- .../selectable/selectable.stories.tsx | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 src/components/selectable/selectable.stories.tsx diff --git a/src/components/selectable/selectable.stories.tsx b/src/components/selectable/selectable.stories.tsx new file mode 100644 index 00000000000..0fa6bff0d71 --- /dev/null +++ b/src/components/selectable/selectable.stories.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; + +import { + enableFunctionToggleControls, + hideStorybookControls, +} from '../../../.storybook/utils'; +import { ToolTipPositions } from '../tool_tip'; +import { EuiSelectableOption } from './selectable_option'; +import { + EuiSelectable, + EuiSelectableOnChangeEvent, + EuiSelectableProps, +} from './selectable'; + +const toolTipProps = { + toolTipContent: 'This is a tooltip!', + toolTipProps: { position: 'bottom' as ToolTipPositions }, + value: 4, +}; + +const options = [ + { + label: 'Titan', + 'data-test-subj': 'titanOption', + }, + { + label: 'Enceladus is disabled', + disabled: true, + }, + { + label: 'Mimas', + checked: 'on', + }, + { + label: 'Dione', + }, + { + label: 'Iapetus', + checked: 'on', + }, + { + label: 'Phoebe', + }, + { + label: 'Rhea', + }, + { + label: + "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology", + }, + { + label: 'Tethys', + }, + { + label: 'Hyperion', + }, +] as EuiSelectableOption[]; + +const meta: Meta = { + title: 'Forms/EuiSelectable', + component: EuiSelectable, + argTypes: { + singleSelection: { control: 'radio', options: [true, false, 'always'] }, + emptyMessage: { control: 'text' }, + loadingMessage: { control: 'text' }, + noMatchesMessage: { control: 'text' }, + selectableScreenReaderText: { control: 'text' }, + }, + args: { + searchable: false, + singleSelection: false, + isPreFiltered: false, + }, +}; +enableFunctionToggleControls(meta, ['onChange', 'onActiveOptionChange']); +hideStorybookControls(meta, ['aria-label']); + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + args: { + options, + // cast as any to align with earier teasing/QA + children: 'list' as any, + searchProps: { + 'data-test-subj': 'selectableSearchHere', + }, + // setting up for easier testing/QA + allowExclusions: false, + isLoading: false, + emptyMessage: '', + loadingMessage: '', + noMatchesMessage: '', + selectableScreenReaderText: '', + }, + render: ({ ...args }: EuiSelectableProps) => , +}; + +export const WithTooltip: Story = { + args: { + options: options.map((option) => ({ ...option, ...toolTipProps })), + searchable: false, + }, + render: ({ ...args }: EuiSelectableProps) => , +}; +// hide props as they are not relevant for testing the story args +hideStorybookControls(WithTooltip, [ + 'allowExclusions', + 'children', + 'onChange', + 'isLoading', + 'renderOption', + 'emptyMessage', + 'errorMessage', + 'height', + 'isPreFiltered', + 'listProps', + 'loadingMessage', + 'noMatchesMessage', + 'onActiveOptionChange', + 'selectableScreenReaderText', + 'searchProps', +]); + +const StatefulSelectable = ({ + options, + onChange, + ...rest +}: EuiSelectableProps) => { + const [selectableOptions, setOptions] = useState(options); + + const handleOnChange = ( + options: EuiSelectableOption[], + event: EuiSelectableOnChangeEvent, + changedOption: EuiSelectableOption + ) => { + setOptions(options); + onChange?.(options, event, changedOption); + }; + + return ( + + {(list, search) => ( + <> + {search} + {list} + + )} + + ); +}; From f78cabc398ea445da314ec4954d2c248deb44037 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 26 Apr 2024 15:29:23 +0200 Subject: [PATCH 05/18] docs: add EUI docs example --- .../views/selectable/selectable_example.js | 39 +++++++++++ .../views/selectable/selectable_tool_tips.tsx | 64 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src-docs/src/views/selectable/selectable_tool_tips.tsx diff --git a/src-docs/src/views/selectable/selectable_example.js b/src-docs/src/views/selectable/selectable_example.js index 9f6dae7c1b1..0caa4538262 100644 --- a/src-docs/src/views/selectable/selectable_example.js +++ b/src-docs/src/views/selectable/selectable_example.js @@ -33,6 +33,9 @@ const selectableExclusionSource = require('!!raw-loader!./selectable_exclusion') import SelectableMixed from './selectable_mixed'; const selectableMixedSource = require('!!raw-loader!./selectable_mixed'); +import SelectableToolTips from './selectable_tool_tips'; +const selectableToolTipsSource = require('!!raw-loader!./selectable_tool_tips'); + import SelectableMessages from './selectable_messages'; const selectableMessagesSource = require('!!raw-loader!./selectable_messages'); @@ -287,6 +290,42 @@ export const SelectableExample = { `, }, + { + title: 'Options can have tooltips', + source: [ + { + type: GuideSectionTypes.TSX, + code: selectableToolTipsSource, + }, + ], + text: ( +

    + You can add tooltips to the options by passing{' '} + toolTipContent. Use toolTipProps{' '} + to pass additional EuiToolTipProps to the tooltip. +

    + ), + props, + demo: , + snippet: ` setOptions(newOptions)} +> + {list => list} +`, + }, + { title: 'Messages and loading', source: [ diff --git a/src-docs/src/views/selectable/selectable_tool_tips.tsx b/src-docs/src/views/selectable/selectable_tool_tips.tsx new file mode 100644 index 00000000000..d8b61b0643f --- /dev/null +++ b/src-docs/src/views/selectable/selectable_tool_tips.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; + +import { EuiSelectable, EuiSelectableOption } from '../../../../src'; + +export default () => { + const [options, setOptions] = useState([ + { + label: 'Titan', + 'data-test-subj': 'titanOption', + toolTipContent: 'Lorem ipsum', + }, + { + label: 'Enceladus is disabled', + disabled: true, + toolTipContent: 'Lorem ipsum', + }, + { + label: 'Mimas', + checked: 'on', + toolTipContent: 'Lorem ipsum', + }, + { + label: 'Dione', + toolTipContent: 'Lorem ipsum', + }, + { + label: 'Iapetus', + checked: 'on', + 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', + }, + ]); + + return ( + setOptions(newOptions)} + > + {(list) => list} + + ); +}; From 1929ac44a1d8707fe867d8884649c1afa3fb68af Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 26 Apr 2024 15:40:43 +0200 Subject: [PATCH 06/18] chore: add changelog --- changelogs/upcoming/7715.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/upcoming/7715.md diff --git a/changelogs/upcoming/7715.md b/changelogs/upcoming/7715.md new file mode 100644 index 00000000000..907e69c31ed --- /dev/null +++ b/changelogs/upcoming/7715.md @@ -0,0 +1,2 @@ +- Added support for `toolTipContent` and `toolTipProps` props on `EuiSelectable` options + From 79e47764604a34f85885b53daf030c6d068da1e6 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Mon, 29 Apr 2024 10:40:28 +0200 Subject: [PATCH 07/18] fix: ensure to cancel requestAnimationFrame on EuiSelectableList on unmount --- .../selectable/selectable_list/selectable_list.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/selectable/selectable_list/selectable_list.tsx b/src/components/selectable/selectable_list/selectable_list.tsx index 874344e333e..9a5d3d3b88e 100644 --- a/src/components/selectable/selectable_list/selectable_list.tsx +++ b/src/components/selectable/selectable_list/selectable_list.tsx @@ -180,6 +180,8 @@ export class EuiSelectableList extends Component< isVirtualized: true, }; + private animationFrameId: number | undefined; + constructor(props: EuiSelectableListProps) { super(props); @@ -196,6 +198,15 @@ export class EuiSelectableList extends Component< listRef: FixedSizeList | null = null; listBoxRef: HTMLUListElement | null = null; + componentWillUnmount(): void { + // ensure requestAnimationFrame is canceled on unmount as + // it could potentially run on a next tick otherwise + if (this.animationFrameId !== undefined) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = undefined; + } + } + setListRef = (ref: FixedSizeList | null) => { this.listRef = ref; @@ -508,7 +519,7 @@ export class EuiSelectableList extends Component< this.focusBadgeOffset = this.props.onFocusBadge === false ? 0 : 46; // Wait a tick for the listbox ref to update before proceeding - requestAnimationFrame(() => { + this.animationFrameId = requestAnimationFrame(() => { const scrollbarOffset = this.listBoxRef ? containerWidth - this.listBoxRef.offsetWidth : 0; From 4df1f0b3333e6bcc3b94192b7e867f48a99c2014 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Mon, 29 Apr 2024 10:45:48 +0200 Subject: [PATCH 08/18] chore: add changelog --- changelogs/upcoming/7715.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelogs/upcoming/7715.md b/changelogs/upcoming/7715.md index 907e69c31ed..f4b60264dfb 100644 --- a/changelogs/upcoming/7715.md +++ b/changelogs/upcoming/7715.md @@ -1,2 +1,6 @@ - Added support for `toolTipContent` and `toolTipProps` props on `EuiSelectable` options +**Bug fixes** + +- Fixed issue with unmounted component state updates on requestAnimationFrame for `EuiSelectable` + From 06e62cca4160896e39abb9590908e12db4399b08 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 2 May 2024 14:37:50 +0200 Subject: [PATCH 09/18] refactor: remove isOpen from EuiTooltip --- src/components/tool_tip/tool_tip.tsx | 17 ----------------- 1 file changed, 17 deletions(-) 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 9fc40abc1f50e590c048ed8eb831b4d1944b9e08 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 2 May 2024 14:40:34 +0200 Subject: [PATCH 10/18] refactor: use EuiToolTip ref show/hideToolTip API - dry out props - ensure tooltip is visible on mount when isFocused - update default tooltip position --- .../selectable/selectable.stories.tsx | 2 +- .../selectable_list/selectable_list_item.tsx | 46 +++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/components/selectable/selectable.stories.tsx b/src/components/selectable/selectable.stories.tsx index 0fa6bff0d71..7bf7d369c14 100644 --- a/src/components/selectable/selectable.stories.tsx +++ b/src/components/selectable/selectable.stories.tsx @@ -23,7 +23,7 @@ import { const toolTipProps = { toolTipContent: 'This is a tooltip!', - toolTipProps: { position: 'bottom' as ToolTipPositions }, + toolTipProps: { position: 'left' as ToolTipPositions }, value: 4, }; diff --git a/src/components/selectable/selectable_list/selectable_list_item.tsx b/src/components/selectable/selectable_list/selectable_list_item.tsx index 24ce8105e91..5372d65c4c0 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.tsx @@ -7,14 +7,19 @@ */ import classNames from 'classnames'; -import React, { AriaAttributes, Component, LiHTMLAttributes } from 'react'; +import React, { + AriaAttributes, + Component, + createRef, + LiHTMLAttributes, +} from 'react'; import { CommonProps, keysOf } from '../../common'; import { EuiI18n } from '../../i18n'; import { EuiIcon, IconColor, IconType } from '../../icon'; import { EuiScreenReaderOnly } from '../../accessibility'; import { EuiBadge, EuiBadgeProps } from '../../badge'; -import { EuiToolTip, EuiToolTipProps } from '../../tool_tip'; +import { EuiToolTip } from '../../tool_tip'; import type { EuiSelectableOption, @@ -94,11 +99,11 @@ export type EuiSelectableListItemProps = LiHTMLAttributes & /** * Optional custom tooltip content for the button */ - toolTipContent?: EuiToolTipProps['content']; + toolTipContent?: EuiSelectableOption['toolTipContent']; /** * Optional props to pass to the underlying **[EuiToolTip](/#/display/tooltip)** */ - toolTipProps?: Partial>; + toolTipProps?: EuiSelectableOption['toolTipProps']; }; export class EuiSelectableListItem extends Component { @@ -108,10 +113,20 @@ export class EuiSelectableListItem extends Component textWrap: 'truncate', }; + tooltipRef = createRef(); + constructor(props: EuiSelectableListItemProps) { super(props); } + componentDidMount(): void { + const { disabled, isFocused, toolTipContent } = this.props; + + if (isFocused === true && !disabled && !!toolTipContent) { + this.toggleToolTip(true); + } + } + // aria-checked is intended to be used with role="checkbox" but // the MDN documentation lists it as a possibility for role="option". // See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-checked @@ -141,6 +156,16 @@ export class EuiSelectableListItem extends Component } }; + toggleToolTip = (isFocused: boolean) => { + if (!this.tooltipRef?.current) return; + + if (isFocused) { + this.tooltipRef.current.showToolTip(); + } else { + this.tooltipRef.current.hideToolTip(); + } + }; + render() { const { children, @@ -176,7 +201,11 @@ export class EuiSelectableListItem extends Component [`euiSelectableListItem__text--${textWrap}`]: textWrap, }); - const hasToolTip = !disabled && toolTipContent; + const hasToolTip = !disabled && !!toolTipContent; + + if (hasToolTip) { + this.toggleToolTip(isFocused ?? false); + } let optionIcon: React.ReactNode; if (showIcons) { @@ -375,14 +404,15 @@ export class EuiSelectableListItem 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 + // This extra wrapper is needed to ensure proper semantics and that the tooltip has + // a correct context for positioning while also ensuring to wrap the interactive option
  • {content} From 6142110ffb1f251c4dd8f1457263714fa8407b00 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 2 May 2024 14:41:03 +0200 Subject: [PATCH 11/18] docs: add alternatice tooltip position to EUI docs example --- src-docs/src/views/selectable/selectable_tool_tips.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src-docs/src/views/selectable/selectable_tool_tips.tsx b/src-docs/src/views/selectable/selectable_tool_tips.tsx index d8b61b0643f..48b778ea0cd 100644 --- a/src-docs/src/views/selectable/selectable_tool_tips.tsx +++ b/src-docs/src/views/selectable/selectable_tool_tips.tsx @@ -22,6 +22,7 @@ export default () => { { label: 'Dione', toolTipContent: 'Lorem ipsum', + toolTipProps: { position: 'bottom' }, }, { label: 'Iapetus', From 1dd28bcc3a24f94be373ce5257ce44376c700eb3 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 2 May 2024 14:50:55 +0200 Subject: [PATCH 12/18] docs(storybook): use include instead of hide for selected controls --- .../selectable/selectable.stories.tsx | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/components/selectable/selectable.stories.tsx b/src/components/selectable/selectable.stories.tsx index 7bf7d369c14..2d743693bc1 100644 --- a/src/components/selectable/selectable.stories.tsx +++ b/src/components/selectable/selectable.stories.tsx @@ -107,30 +107,17 @@ export const Playground: Story = { }; export const WithTooltip: Story = { + parameters: { + controls: { + include: ['options', 'singleSelection', 'searchable'], + }, + }, args: { options: options.map((option) => ({ ...option, ...toolTipProps })), searchable: false, }, render: ({ ...args }: EuiSelectableProps) => , }; -// hide props as they are not relevant for testing the story args -hideStorybookControls(WithTooltip, [ - 'allowExclusions', - 'children', - 'onChange', - 'isLoading', - 'renderOption', - 'emptyMessage', - 'errorMessage', - 'height', - 'isPreFiltered', - 'listProps', - 'loadingMessage', - 'noMatchesMessage', - 'onActiveOptionChange', - 'selectableScreenReaderText', - 'searchProps', -]); const StatefulSelectable = ({ options, From c5d30a4c13a4c0ab659e28e4afce71abbaa2e38a Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Thu, 2 May 2024 14:54:35 +0200 Subject: [PATCH 13/18] test(VRT): add references for EuiSelectable stories --- ...e_desktop_Forms_EuiSelectable_Playground.png | Bin 0 -> 9680 bytes ...desktop_Forms_EuiSelectable_With_Tooltip.png | Bin 0 -> 9680 bytes ...me_mobile_Forms_EuiSelectable_Playground.png | Bin 0 -> 20587 bytes ..._mobile_Forms_EuiSelectable_With_Tooltip.png | Bin 0 -> 20587 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .loki/reference/chrome_desktop_Forms_EuiSelectable_Playground.png create mode 100644 .loki/reference/chrome_desktop_Forms_EuiSelectable_With_Tooltip.png create mode 100644 .loki/reference/chrome_mobile_Forms_EuiSelectable_Playground.png create mode 100644 .loki/reference/chrome_mobile_Forms_EuiSelectable_With_Tooltip.png diff --git a/.loki/reference/chrome_desktop_Forms_EuiSelectable_Playground.png b/.loki/reference/chrome_desktop_Forms_EuiSelectable_Playground.png new file mode 100644 index 0000000000000000000000000000000000000000..a56a1d996f87214fe7308837d8a6a90ffc756068 GIT binary patch literal 9680 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 M(~p%uT)+Rn0R6~lS^xk5 literal 0 HcmV?d00001 diff --git a/.loki/reference/chrome_desktop_Forms_EuiSelectable_With_Tooltip.png b/.loki/reference/chrome_desktop_Forms_EuiSelectable_With_Tooltip.png new file mode 100644 index 0000000000000000000000000000000000000000..a56a1d996f87214fe7308837d8a6a90ffc756068 GIT binary patch literal 9680 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 M(~p%uT)+Rn0R6~lS^xk5 literal 0 HcmV?d00001 diff --git a/.loki/reference/chrome_mobile_Forms_EuiSelectable_Playground.png b/.loki/reference/chrome_mobile_Forms_EuiSelectable_Playground.png new file mode 100644 index 0000000000000000000000000000000000000000..7fac77a3b49495d62752663e6f39d9302a6453aa GIT binary patch literal 20587 zcmc$`cR1UB`#!9Dq6;lWTU6~Ftyz0hBYd<)OHr|EQ#(Y=q++D4s-pG? zB2#Hj>ly_YxgD1zml_7?w!B6mM+jk5MHy9v~9~r*N zSR@7n8x9g%SEx^Kr!JluIm7#6BBerL=}wgSpqZUPwjvVkXG9ovg4Lw^x;jgznAM`q z*d8wEpXiNxm$OK}bR+DeLc-HcJM{10YwFLR{Nm;1z3^qP0V-ytJv;7AO-+B)mI5b?0$1OvlAH!rw=4?REqUwP{pt^U|Y z3(*uQ*Kt7zyPSRErAwD+b?V@59JmsiNB&Z&+8J#K1R^mT>hYa&ikXWSx0@z?5XuX| z?GJWKI1h!x1{J_fAFzKI-t40>rmxT0;N&JZCup>BJfCrcV_Q)~qZF%su zYU&k~f?w}W7ZetrBpO>^+D&%^diNRBeGclg(sA*1@T!;UQ2cjoE;$<+M$wplL+m;1jlGCqT`cAsRz~DiZd>T8^f1`oql#!~ zo(stXYx*#Bs($AFr;eGvLalT@h_`6HA)BSoqZ7%vo z_3kj(n~=3_l`vhQ+GIBR_*N6kb^tv#lYhz%3}*d|noIX{<5HuJl*ZDCh?A+EHy>@K zN_Z;&@pOcw-u}SN=7U28&&Zl0hnTwKxFp4whpil`c5&dyX>+}%T(6;C)|I~&Hk=lQD6I;ykirC_5ja!{QO|=;5}Is^_IIc6!_7#2<12gAm%l&hQ8r78 z%LW*w#|OBHU?)E)IE{9w*czulgkQOMvAmUg&$q`efP*7V+ZO|-gmz-4sq1-5OA9Z=a+sx z_zJbh;A1U6Ym-+I#bt(CT3Yq4c})0EogH$;nR5OZf?sjYL;t;H{zlzh@nk zRuA^2Ps>aS1U|p8Pz!6-HX1fJVd(h%yXWvKj*ca(fsJNsJ7$L)JIHHuepM!AegwA* zSy@@ZPQ_C1+`FeInquzWxaaOVQCX@4y#xXcT+rIi_0bgdH_pz^`3H@>>G9DqEIi`K;g2qI7f0Pa6 z_w)S8C|-?nLR#T0;S$tQO>IYpD*V7gf+Pr0_%0dfic2z&l_yN^D)fhkAa8DLCPzw?2BUMYC{RE?7Xx z*Wwkay6^oc3dI$=(X3XXBLWK247<`dhd1Mqa$6-|%Bor^-;poULWzqBJv@i0R`fU_ z;fA32X97JUGBWE&)*qky-lMNF73|7v#W(y-Vub|7m8D@j`Vjqh;Rhg&Xov1P54M8u zz^-Uk)9N`MXqgJ%ge!yJh}Pne+!w*iM;S6?ilaPEuIvP_?(8YJkv3@4N@0GA*-*u7 zR4XO6sGucC8P!ZxlG!7-?R2lNW&U=4bqi-uvav_r*eusk%szPAJ&~HsNx5=9oL5y{)oHrtQQ8B)FHbXr z3oKutdb7q<1Z$1jrZ%6z|5|^Rl(?@orOqrT?=`IFtJUa7?KVk83VJkEROm83g$xYG zt7y+@hhU0SW$0^0&#VnxEp2Z6^Z0F~v#uHZb}VP%@TcM&*S!$OiEql34k2Av%e?#{ zy_MgOO4_sTR&}g}eDP`=6A@w0l6*3y#&`zXz0`Qhx1#u|-o;7m^sXZXU0d_2RvPs% zGOEMKWa8>++4zf-VB6(X=0)6*x!J^@qD=57Au4K=D|_{@a~X>XA&TT5sN809Uu&r} z1YW4al@})Po`Nj;&P;LQ%kJ#$lKK^1MikW9;uXXj$T8(b^-6^Y)@S5Jj zprgT1-?fxsvA0>Sq?G1-u}El*Gu+(1NDKjxgGuDTwvnCrRCbSUwDtGq8K1ft57{LD z;%AXj?LK7H`>%~7x2>m`W;g}U)Wk%;hvOHF+(P1`G=wDV{uXO@TY?dT#doxPoBz|k z2;n?G;}tNfK=lh+|Hi*yXg-(`pSQuEUAfFzQfObqo|bkvZS6t`-x1aOwoCS8naKWvI3+^y{OCyte2~5H zQ}JmTORm8oNhfp5^3~O7-xfXx|4%B^&6<}x{LV(++bSyH=6dxmdOaY9HSuPBJ#~rU zVfRHBo%{A{y%&d#oG}7YxAjddg3;EWdH5jODVjpY?#YcKO7ORdJ$1#djlyW<90d+4MXx#}ZO!$$_4I0YT12`sduZ;KU!pk^9~zHYBKLV6n$x0bTmtDz8&+Q@}#NEb@~U5O zN0s*%L0yb&s#U=B_DOnoxo>H;p^0^2x8rG^3UIh=SA!YX_MsnE#$SLQxwLPQK9ji|g_oVO#*I$*nP&6=h-`^;=#!&gy zhhEkVg->1f1RE;bauM@bKZ>^(GsKDWh@Ah=9}jONm{EsuAl$76nqvo3vIJ2tDO)|i z*Zppie$|P|%bB;mU(wLiGPFsUs!^C|`x3#jnf6tlh|V>3f*sh~<&(=6?`defTexdf zSe&m6Kit~_m(|d8CnXRllTvUIZ-Z6CsOLUPUBSuIWLnJv&_1(=klGeZeFm-pQ`X| z_UcXK=Xa5g7S@n;jI=C^=$swwtV3jdjBB&8W_m!8FQZnN^JQ*)z(3j=9~S(S0&hvy z`C7R>(yMD4{&N5B)`qRjwZjRxDWcq%Ur8p6qG>FT5?d+UlDWxom?E5!rJMiPe{3&PQX ztG=%+Rr0*nG;J~X;*m*s^UPs(F=2_1HMIRWFmtJ~l_N$@&wxKxKfKXQ(l@5PQ_R>c zq!PqY$-8`d7;+R|yK>)8&Hpmob)ZM>tqlAU zO2DSyoL$2BVH_P$OXE+Pk=9BZs<0|@?)CV- z9kz2f9~C;ShXYJy2_px9aq4&NA03pID#@)hWsK<@4XtnCJlt#)Iwjg34W{TS z!=E1)To`$Oc&&5m0H87?s`iF(*7g3SSDkhwfijL#EnlhH9Fg=!H4f$pg>1OQLkGJ! z18Zfvz$K9M$6vYIMWSb=9_5tA%Tv#=_EKX3+XQxY-=~YF2Gf9HgMkNI; zr3wH?8o$5~R(Dep{`BEVawB+;hZZ%WX{ksMR@(&;hGuRio{r({>E@bguyyiD%U7GW zj0*DHgHxNwm0cgd`qGuEX!WKKE}J+V=u8%rtf{NKcQqTgR>JIBM0)$Fo{8dmY<0Qp zQ7bYg`0?B%Uj)JUo+C1d0Lc!lV>rQ(6LcDV^3urcf_-EMk@B2leAC$}0n%$3+)21x z7fIE?JX7`|@ASVG?Vk5K#l2ID9>LEn?Ji7@kAIw6@o}G)5xs5Zd9RmU&gq?n8wS1f zd$8$PIHJV02RvkoGI)Nb{S7XfXEzhR+5SXFPc1O2D@#_p04<0##i=kA87yFC?+AJ4 z{Jx!VR>L(cesrxnD}R09&qlcA(jV!bT0-mK&Vna;#J<*V?`Eg16Vi0St8T{4k-QQ# z!9KdrZZ7x9aI9SR4#+em$brokPj@L_58?>4LQV8X@Ob%jx=r0|zqVDce zQhIKD>GAcFo{tx}IV3;|tn1`j-x-$T>zc4Ne5*cNnnNPpYsyL&b{KR4V)z)s7f zEdc?Hvq{$x*O5ACY;&#T<;#DMo;cXzytBBzJy^?VRvDP!<;CnvEgfv0=<}1}#?N>; zlg8hKWfP3wRhTs&ZP|&|c#XgBTdyD}ikw?tzc`7+e=bird(3EXZJbVxNO^Erel??^H}~X{Mv8 z3bbm}8k?EE*-d}`JW6;ZRL95A7Q-rdW_zBD=p4yO+9Zdv zoC(9y)Q}T>*z-;!6M%?4E0L2}iu5u1>e;#8l}8mE@P`&*^^Vt=b8(sisl%Fim2g>ia01Jb_DoAweH1y zLh%B+scHEuqZfTG`Y0}sXPzT{z2#?SW{AxZhB@=!l@uFMjJdlQy-eM;Q2Tnpdz;9u zww=~kcEs7Bn6sLKHLm{Uo7}?s%E2EpL@}V$1JzO2`-bPU(|+gfec;L}(Fj5^T}gb| zy&udeC}`lM@ml$*Jt)XdoZT~T{_eAHnovKO(#s=Wd+;#6K`~p=qR#5g$=gC>?1i+5 z+eU_icWw)6khIftSe&Iws}ROu=+kSo#AJ9***8ZJ4~m&s6HCf-_00We1aTElS61#h zt<)3)^vjCUA!%plNOFM66zA~|Z_KrOk58S|nBLMnFs*$AIFqP%=Tq`V1VDThc)!wq zH4&jj*gDDjiE-CNI*#?^&u#;N~;fa+?{T05*DyI1)ix(F1jsgq+a}l%ApC+uI-5 zKZB768TOrL=KUvt0G;va>hw4YzUpE&mSvv1dj z_h;R!*HK%a`n2DlsRTndL=vjhnTqaT2m}>c%yrpk_p4e@8js1M8wT<{d9Hw~XN;); z)M#j7JmMAv4iQhJ-OIn9UT`kWD_!JY_}7U8)XGAd49SVUbgW%W&;U>}2|H3u6r}gyRpkzvBsYtz-(KCzk)u?&D z{!1r+s5*cDG#SRyI;!z5u6L%oYiP8y-q6xuf|kaFEB|&Z!FTDydqGiW7&R!);3rrh zx!aM|;Wv*TD|LRE*i23a%tbJ$Sv3M6`hZN->eLQ^DIV74+#JO68a*9QaJNr-)ecq0 zM4dK^9-Q=Pw-rBOY4LgEcE0^iaEPjE#Ma!BFAnF+9syvZq=ezU-gpIs!h&DWz_dr( zj2A#_0)l<)+w(Nr2d1QjPRYd&6I2s$C8iW}5fQ6!Q)L!54&O1|k%2{Bl{}T?j~7}V zs~<=Hne@VN-50;r(4ndl+hD>cAS$IXRIy}qjQjz=U;tA|$Z*4ewF95^tNYqg33GJu z`gPsf*abv>p#;G4pZp&Y@oT5ee}Pv1-ze;Fe?5X|^~~Vv4^j85|HXS(JboL#>CX*b z`p3%V$Nyg<=YKHHuM2Q;|4b1Y?9wx|i%v3KFMgo{NKrX(i$#3@Ry?mhavOg%I5?yM z|NQy0gx}(v8wQC$=!b+n`a>!)sJ4^h=6s6t92cn?aiO(u;O8TizT{D58G+n*1C*oj zr0j$Ap>o0vNR?UhdY8H86;94UB)VJSz+Z&^r}=9)cW8~FvGXv5%nznT@7k^5QHmdI zZ^&Ho0gp;(0F4YdWWcKJudST5JeYBgt4B3Y4z%l-_||o6h8~peD&mxzmFb9RaK{ne zKPTjk_4IP<)@Gf%v#yT_3DWE3G&D7nc!AW>U``Lk&fk~0uFc2C=Zn=bG)(+#^VzP} zjGi9{{#eaN?G(Ywyl%D=n)Cg;vYoB%oLFc;A+A2((V*}SFYjae=FI%pu-&PpgKv`p zr$VS>8#RV%u<6;-qb_T{4=RE6q8ms{M3v`CL*!zBh%5_K!Z<464 zqvLyVaq;z84o&R!4S6HsTvn}Aanp1u*WNz64qp=I1lebzYSqh?h*kmgnO3%C(Y3F7(?`EJ zMr(Ai2JajEA1^>VF9cv9AY4i=d_Gs>@vT9dbjiff@MZ5`>Dm`l_DP9}h=Kyun>TN= z%fI^Am3Gf^jRaE#@+z^h2F$3XERVX24qjIXB9g^w>9wK?&u3Pmed&nrXI9*P0+(=| zzoM$H=8uJrhXqq6QiRfxpNVUm?SkO<(uM|k*n02Tz4fN-l8WLdkoWJwT~`q(fjikK zGIQuF{f&(c+W1yzerV%fImogtKYsoqtqA}*b!mS^Cn?>(qB%g?WvmP}K@(}DkLxET zCkfoV`6xB@+F51{0!Wl%2WyhBsZA;ZTUe+<$jE@v<_|&)+#7b#XV0ExxpuAO+nZAY z;)41oPJ{yKPq|17brvzvhIrxXLaQo!-GSzMeA&n%Zy8b)V8rha(=T! z+M;iKTOE2H)H35ey|uNw0E;g0{o2^jpd3nDyN{)-`#X_w6MgWOgMq%S@xDZX1U?8= z%)C63E5t&tSI=yhrfJqNXD1Rw71>+D#DNmE14yB5aA^W3VPRuFHVOtmsZ7OC~+Qx*?`T-}CYN)S|aL5WMSD6T*)0Ge#0<7It) zml)gFKl4SiWf}wCiLp#CK0eL2HykdDd+)M$3 z=V-HU0M3(um$2Th_$rmqmS2i$P)@N7e9!AR=-@osneFQG(r{^C9@1xWK)TJyA(c<8 zY>nkaLihiM#9UQ;nriyy7zt?Db1?>7E*(NHqt!uivt8+^sHmtZ=?7(H4NtvE=B39O;t1Oor1s51c_X&jTP2d|jC_`i zcLXqn$S&Sr^mvXeFL&Dt-kgS*)mhh#2RO&ucVh3($?$8X-GF$>W>N}yUcuB=+*E9A zdeAD7=X)ohyQN&P?MIAP@7|bG1V=={v;7TaA}iF&)YMc^Sa_ge=Nwkj%iK)}-w<%_ zSU>alv#RYHrdoaHq&JV=^v$g89QAgLd+Fx)a?qia4`O2J8-VI&0U9@WQ%;_nh2`2S z`QYISCITN=3T61TU%i~mxij~KfS~*0(L9M3tQa1_1sk97IlK=uFN9UXe$FVC$pYCoh_MK%ewg}nZSMb zgPuM+?)gwR(>n=myb!Y;vYRtStj1`%b{t*d$r#YHD z6{pl2_GSlBWm1AJH|Emm@Fca2hfXM9=~?sv-H9BBYz(&=rXcIM}5`=|KC zMF=IC5qOy9kRcFO_3y66#>rqECH$9;0zZn}qW|qkGYEDS(P1Z*Ze(VLpb-2|W+{nN zH|K?dl+1)`nJX+82l8wJ8x{9fzeR4m;og5t72A6#BNJpNo-P@@eb>%Gr|$!~AAlx> zq2bEBjrNYv{_iK&u5ery%I+>|4BA?##j`G)Qrdm*_9>`(JcKl%Fn<|jbrTvgmQu-s z0g=6j!(I67hq2_u#FyVDIa<1YfS}j*KSmg&IZlr%W?8c4HJHX5v?Gi92XckoC9$RcfM_C+q;j*VoYZA$y`qz8~5&2D%B4agyIgucpd-#SfQ;D{e4f@!Uu-i zVqUa@kel}4MMIP4!ohqvDXSM8!s7RrTQ%Y1dt3DOPK6$SP9U2f)Y4#Z8!xXa*2aCa z8)IGBINh~bk&m7Ba;}J!i()Y-(kA=hw9+vd zJuCO`i?0(V0MMc6VRqb{Jok&zb5ljq=U31DkbE?<6ou7?uak?iA@JGX zsl;-_A@Wb_%Hym?&sN%s5T&Kgn7uWzA@Vpqq)=0=@xii%C&clOXEeUFc0p>6;y*rE z#jLpK5saHarC0^3Y8M&6B{r7NI)ZUE_PR^Qok)PgKgTKU&uz}OS=4O7?uP8^i<=BW$bmNkpf(rO>6HepFhg69XU zxeb8v(zaTF`$FXb@-;FZMn(v&(_wY&_kusE#a3QP>4P!x-h zF;HD?Y|wE48=5z%OKf7kB`ldOR)y_=)NIgFBJ^x%odHGhY8R^nyJc zzmP<9GcmM1L}_x@o82&_ScKX}ObkfiPRltyEv>EDh!ilL`+C%L4R6c;R)qYg#YT9Kc;q-HphF6r zM#k$8bekjWH~bH+m)b#~By5?O$h`Y{tl4t@qAWs@A=_`{XkQNPR6m>FxpD{W@8BFMSVqewn_4qT`}I4WSqhroqUp%u;$nk= z!)jv6<_9`%%$vGrR`2!(Y?}7pzfQ|jw={uy3Q>YI($v)avorkZ8F4R9&bN(^?Sz7r zfkDFmfO`PZbpj7SHt+HQ^+e_H@Po$PzH^*eTd(BXJLPiX6wPwCQba*X*ch@>8fgfU zr`9==|8RD%1vuMTxq{yXF0r#CJZ)O%PWi)h#G01N#7{CO^X|=zu&uX@ZZyiGyGaTC z(@Cja>$l!B*xe& zHBudbs@u01Ep}G+v$>gZp=$K_0BwWA$k<909Q~di-I%NQp0aggb@8D)X5o>M7-s`Z zgS_mqARI5*VQZpvJ&bHt^YXpo_OgC~|)DP4v**2--=AKFG$GPM+6=IHsaqCvsg0{x4gik^9av^*9z8Qh`b2_m*K25Y0DEOtPY|^!7jLy!pWL*CF0bFzgy+B6IYILoMEFs_ zM6QQ!9q2P|FdiabD_ZNhK${gI+CsI?e53zF&)$K5oU)3_VQHhVv0lv|XnJ@wPJcsl zGd|Tc=$pJa2q2(uX=lelu$E|wCU8~8DX!-9_4gB8m8fH>*u8X&1|021EC(jQQ|MGJ z^-v;xi!_hH1v~-e;?ZtX;5nM2Pv|{#=p+9e@dQfn^JowcNoxQK0}x2c4t1kDo^-YF z+{ySFOza*C=JLt*?n*7mRf+IW(LN9)Pe%_1_%h5km7kR!dk(WZi;QIH#B+}TW<2I^k z&C)M`?~=mC2IBQ9~TA6Uz*d8Imbjy>~;DLC| zjj>T*hNW^I=|o6hT*#|P`!4x~FRscW4UGU?m8h)FsYkQnZbHtUxwnwvcD&_?E9=u@ z#2LkwKkw$#hDwgDtavugP3oOFbL?oGxmg4Ir{p32R!}g`3604Q>5K=`Cq}Wz+Kizj z6N~c1so$wd`ME09SJ+3fbKl~_SzmiT$N*J9GH=Xvna|9DLcl+LT+Nl4Q*bW91|;cq zzSk6i*)4jlZCg66KRY{_mye&1oIbG|h#55#X3g~O8iD8Qm>}JxskO+Y>~eigP3NTW zb6rXp=0K6uPW#Nz?Po&9AqX0cmP4M` zy%KO)ABKbJm{77a^q?>+n1)*22?mDLew#S?V&Gtp4gJ&XR_7Q7RLsNTmoTyS2#_Jf z%{Ql*$zt72Hz4dvZe@i}Q^y=ol-cleww2EY28Q!bh3r3%^dJoP6d#`Rrf2oQ&EAwx z)NlaH%Fof37Y7&$GIoK)?T;h(7tS?h?|(5jxG>5t=U3CTMI>y1dc_GK=2~wOSLY`w zqe&191O$42YNgL?&;VBaoPDrZ#7`4{&8`?+ytLn&N$Ye!$N_Q7d1=>N!gbWJrM0C5 zfzU8VyQTD#4WGO--P+xb)Ohk_u73^CH9q0bgU)_>db{Pvk2{i*rXvab3vf-*zPBq= z+S{+zX2)}We>mT`{8y>Q9a^3o;!#uRVr%ri@kWN1dL_~TjPM@zf@+5{RM;d5dU<&i}hf|znKh32(^L&JFj|TRCEkD zuc=`(K00U+LRr{SirXf24IjT1kF*6YwAI865H$Zh`*$;h^r27tzvP_#PjnI8Od(Sb zD_DlcpYQ(V*!_0tBJ1dXLZJT+^!op73^}AYpI-r>S5Wv4oHvG3S`APadt?7&9zA3z z2TPUk%+w|mr3Shc3k&O(zdy|o^fd?y3W187RxbobnJNCbqLx6x&W<{7WKv-$T)p*^ zIQdO;qQ{gL0*Rzh%fW|y=09Xd=>q=ouyh6vg2|y}KwjRYj8tQA_27LE3y1Z0hkVM~ zteEbL7rBRAU79kS>nR|M1 z3bX}3!gXLz2%%fTXd_OXvW7Pzo3G8`e?TrvZ2er23>h0 z*%>n;SQ82@pVwa?0BIhIqQVZ;Wes(2K%zc;IBW#*lC=aK1OrhlLbcW{8hd}rspJS$ za2)~9&cLwiFFW;`l6uvt(F(;Cxa|odHh>}hJaM}kJQ2(Z1G1UVM^>3Pwx@6=5J*W@ zz^sgd$3Y`y)La;eD@+nHDXFT`Kk%Q>@fj&d`33qZrdH;VnDIe_?p>pQQwM?Tlh9h{ z0}LDV?AuZIiIDW!&iY5Wxw-Is&?UpHwEIQMeWWmZZ}(B->gq&dax&t}mj_6z4Q|-K z`3b;VHZ?vwbLI@p&(exUS^y)<`St7c-mb$zk$1O@IpB`<7p}Z3$_mgHGAZv%&`~H| z>rzk$4G1tlHy{tl7X`;sD40-9yoEpL-wBE%Boo?eJtjNBp6>lvcGIltRj(tr6Q3ig zoER2$bm9xe`LN4HTlEXAUFdLFPvFKdbYXLYX+qrw{*d-0;5+FcmjNVSp_%n&yi!mU%%ehf!d@* zQ@~)86W4s|ejC{&n*fjX( z+fg1+j%hr7YWVEg%Scz?a3|hDbdb*mewb{I5D*u?HHVJpiH$wVc7^2x7j>*w9WXK( zV=tCm%`g9qN#Grn9sp#0L$pbs|7{&N#?r%uwuw`^_{kYej>j$So$G|@X`FEeuyi=@bzAo&Gp$SaoO%uJ5+h???q)uS@OId?5nM@|m8@LC94BOvjS?}}b zVbft_RY6Cxb5D+sR<^8af1y_I?^Fk&Tdzld06ZN;o0+Y&X{LCO5iR@HJn!x3 z5aM{;A-Xu$4I1*CwwGU=WoAy{`|&v~E$!i8Sl8pXr?`;{-<=o@ysK5Zq>Rp`U1x$I z4X%Us93paGN?O|A88;5@cMy88nm7*K3>yz#b3M%A`T2%?dR==O18hR zuMbI?9IE*0eKFF+rsJyoGadPrml&E{PPEw$SAJY3Zb6Y@sNWjG&j zZ0%&9LlL3Yu7g#9{yU)x9bgGkQtLe43z`{j?jCIdh{6;=+D)k-dH3jh%mDK9$KvCt zO+s^uF_~iz2SInDWzk`j9$Okp|MBFiezoJ*)$6b1?FXJf^len8Ag1hFPe$$) zHII-LPcd=Lt$2biGM}xurGYP%HoTB}ritSqVfLf~DpB9Lv#%xE%H)tgj*d2R6EXpL zlT|Ea??Y*bM;W@-l(X}jx+sP^T+*!sW!v5vmPM-@j#5UOY#e%AofV-RujB((e|()f zCnm47yq)Qv;y2n2_SXWQS)tIV)EGm>#cnDM8@}V|XFYrF+#4=`>iFKHV>{!G^o?k! zLcSaoYi{LEnrQ*_88108=>~wC4(0u~hBegH-6EN;Awa>FwZc=Y{!Ew^C>oF9HGdOy zXmxc`nzwlN zEFZc%|DvGK&*Gl2l$2SiF>+|gRc^gVNs055TJKOqOnR;3nlYrWreuT@4eq?g4)lXz zO`OkK((vZgxwz0kGx+*0G<)dNy}Y9Y#b^8RhwLTXOz6n(PxrKPS)(_p6DUxLW0$|L z`A#VKFLX%%20E6phDH<+O>|8xq|ABLV|sSO4@&#`^eurDQ)5FCH8M1uE2}OPk`XXP zN1X*I9#k0Wpj-~zOyxiJi1v;~3*7sozI8_$t{L0pP-b#-sOVSgdh9%Ieh$A4K2KC-Jt{)d}p;Gy?MV z>(^hOP{FQSA4tHtn=G&$0`;5B^2=+`O@ICmH-I)dgq*ko!EUCN0_F7Tu$0#i4^_3t zFM(1mDJ3PSbntPN>3Slbgcp(S#xpPNHhphKM#aV9K^v$MbRq{dRL(6{JKO=Pjr;xr zr=*0$q0$ogXFtgBnPHivb5qX}5&Zg$7eGlsCeO#83N5+F6X`#fKIk-Pm(rCCxP@Gm z6QG7RaT`CvDgOzTmS)Cx{%TJ6x!L*gpe5tSkLhshix)3~!!h_XCVd7tqCS5<6k5%` zJkJ6OCm`Y_UFLKt!*%AJot;4MBmempeAx0jFNEqj#ynQ~{j)V}rA0PX+&Q7GErDfjRx5qov{ogF@tS}$)dwuSp_4eus9(2)1 z0#R0Z-a|Gq@l$cwv3}VP%7KkmBNRVXRW$+G+h*{iJZPYm6g)Hv{~4hM;Bm){pAes< zDf!qbztTfJ9z;EaOv5`BYyHg zu5fT7*T0d0TZI6`rznm3`S}G~sq`dx-`*h^a^s>)_vx_bL7juNl{h)5!Xd?m8*%hr zy9$C>566EL9z7_{`OiTSOl^js3rJrdPXAZgP$c5U1rEzUH}A55O6~tbOZ#)rFAD#! z=EY~XzAMxJ(|7WpwcG!@!*cszb4gOZeYI<9+Z`0DJU7)7KEA!+{Leo}HF_O385o%N z4*xHJH4!ID6VYMem9JH`>#K7H#_M6e%I%_1?q_# zW?HVus3<=O{AaeDHdAC{w#h&cUxA&h*K~0PQ)HKdze}hXNEPSr0v~8qQU22nU*CJ# z09S$bWyWjpn<*VE-ea*8xecJLXu?@VGEieL}sB5?l&c z@sn49A3asty-AT+v64Ua^mc+8%ZGvdbhm+-6sQ-wF_1Iw-aTYnTC9h9GBQNB2)iMm zTlP@tZ|%CZN58BykK&Lf=bqyJ*k(H)nFQ#ctLMI<b4}Cu&=0_L4dj7CII!%ET z0QNSyaZ70c#6QUubtU=EQm}XLa%rnRhym2h&lrxpx@rdj8371p_WPFGXXmdcU3etHGx`_XR25<>}%@j+&_qTPh zC8U1#Q6)bjR%e6umk(66$=&$dK!5P=<}4++H>#e-~GU(1OA3t~=ZvQm2O!JU{`G(FO7?f5kzkKPWuiR`ZY1Zk9eMA;) zw&d42$5sRaP+vZjVbgpiu}~;xOk7n({63GQkV(k(Gqw2LKTdnA&8SCXv=S~b9A=HSyW-cp_*3v;;Pkm zT!F9BX1nQ)!pY@CVc!kLz)9RePWoB?`AtHIAVyC9AXyAEAFrORtD^EID=;vb6^DN4 zM_|i%8Rj)`mzo2kFduiKr8W$byr(Uj-`_U@nfFGhCvhTm% z^M?6xW~y`LGoJ;dQf~_z6gU=kaqf>dn#wKftq5r9M_z?hcij84wQvc<^L+cK*|#Br zB1Dcex%`0M8QdXd7X?x4gf4+%bZqz@b}Or%7a}PsX@(0?nKsqZim`%TUy^Pr@%Zf4 zw9^`|OIT@iP~7SGNo5;Ddxqk6-Y1nT(Ns zOnK>d=g!rOb>)bXSPM2b>QHWTBya^l58}kr>kk$hmqwj=A)Q=tU|RURLR-{Jc_A_* zBeck9(M7bM;kPF-SL4MXW{vi)ziTHRtf&*maf^MLSGc*E%CcSe<-HEXO7X==k=@sX zL;-1EbI_?E@!~sBfhC=X*7UD2FuYS1Qs!QJB_Ft1S7*6&YWE0e1%25urizq}kX#FK+v{t=7%HN+pW-Ct4sIJdeQXEHQWVj?HFBt`>s!s2tDU)^G zPc#?M6A#4w>03@OU3Bg23L?&5>zgt=!tnL$qY8=*h);puBXSC{p0b>R!WqQ`w>`*j z1IGMifs>V=I$2K~eH1QBEJesR1*M0mZBZEEiHV6<&iovdl!SkGJzrZ}3(xPC3eBxQ zm}&a7p&rYXz{{!N_cZKbg=V%Z$l5@oj$6|V0&wd>orwXbRS>Z5#Cb~xrSae+h2k!O z|3vffIEcakj%e+8;zKp0Q=bCTw!N)wX!ss!J=kJMzzKN#EC`F0Zwh#ULR|{FXp&jo zgODb*30t&UEF-tot07DFuWo78Jd3gOdFckN$1lXJ?__;wogxoD_&?d9 zd9sI+sm=Z%(swo+ejZ`?CoJr_^9{)RH#~SSAA0<^e1>{@avt8eu6UW? z3^K)~%a;L~%K@^~LxU*m zXk`Jx3<9#h}#b$2IHU0dH!Y8;}^5=otcS;-b-JZ z?ms;FMjWS)*LX|{nbCfVi@i=O$d!S82DS5GV_>pbm8h3w@3 zLfEIZn>sLj&n7WR@-g6lMw?&``C5Ny+*bIRlr~wK-`Z+~z(HB{dUVhdK47XDMh6&)jLKF|96oNE1w%1e5*5{qsv$9cAq3J+OH8e)+z$Lt< zJ%}L$5!fz2fJXa6;Qzb)bdhrYdAYx4y%SUOYK3F8(OJh32jT?cEWZ`1E#Vvs8-4>7 z`r}=2<8~`alG>u=NE!77+O>$D>lueEnWqLtPlw)?q89ZRTn_@QFa+eOrGtW#{7get z(i1}t>9}`k2TO9H<)!7=22B{?q4%A;s?FBh7Xs;-N~QVvPye50uK$_sJB;sUzI7;F zIJYBLQ`(@S>T61Un;O0OR#CBJ)R#CBwx&c7HPy0f4ksQ6Xm4v7)LbYMjF0CXt zb-pBs`f?f+DK~~Ivpu=($Njpy{R8jk`8@B>`~7}i&+~d-E-u;ME(5pf(Zbs`pQ6=Q z$QKG|uo=}0EDqNc6B~ON zvsFyLka_8cW2PtF&}jdqTyq`-9-Fj&IX|CP;}3{p3Ww_#8kYXxA11jf6E^O@7AX0* zRNbjlu2IkBf%SI={HTA+VO_1GgV3(6Q@|B_ONOmRw<1lP!70hdI*IJp~w7eM8nclJ?ey{qkxE)l-T0@}M# zYSDgDc6M1=@OkGyEXsEewtPyH!g^ousLeAlA44rmJ8c8auMxAJ@?RFZRC=l^%MY3f zV(pppEpeYBnc;)CIh^3s{!)<@GT^TJ0~wwuL6JJfUN>m<-#ra8Xp5~_LXv)UQcvmQ zQUcaFGZAZ{rx2r+Ri;oRV*_fPfHxSMgrCV?1{I(oU6HcD*&rFSZPND9HjJaVvb8R4 zs;R{Q;HPre$%L&ca|730r5_#mjp4cIK&`SYMElc#5F&XQ#<6d{va$US>Q*V@KV6+fdNzjQ(!P^M@5btE!5 z-pigPCnv;^7}d)aQB@|EtJJ!5FFpfUVSAf4zCPpb_7)15X?sdDz{%e^;qdyMi1P6jFfEL4tIMf^kgv9io1YY6s+R9IGK9 zAi?i3;d(`LYN|}w(E)g0G#psgy?#>GYG+|0mGD!f(jpdOHn2x-aCmu305tM*wNw-u zeR&G!5DDmLz|pX=1--Dbinq2m@{e2J*6LZ&uJY$m8`~JLYgG9aDG!Ecyu#r!0H)%R zTj7wYVZqDjQL!*n>4@>n zDTnhRWCjX76$O9Vy|y+L*{WF7EtTfZ%~E_zIDpGU-Q(l+NCNovB?eI2*Dr~eMs%#e zXCe9)(5Z4mm?`xX2s$;;cJNJ?ifmq^kjZBF=_K!_1}?W_i5RzcE&L9D`u;Q5Xs14c zC$MCdP|*l3?&D(4qT@`h9L$(skQk(#&7K}u2{lZ@5{bkl5h!fE>B2X%*%w(s$fJg9 zi0*DM|MUu5k;R(INcOU4oN+txQ%I{i8LBy)SA~Ju-q8XGTpmUOP2E zX#Qi0hd0lfp*_f_^x+w`l0tJSPt*K;1K%(0q3Diey~D$}N}PtMW%iQdpBy(f8szgn dhiCq9`_B#LO0fX_>zd*EiH?l@xHCNc#$O8fjXVGV literal 0 HcmV?d00001 diff --git a/.loki/reference/chrome_mobile_Forms_EuiSelectable_With_Tooltip.png b/.loki/reference/chrome_mobile_Forms_EuiSelectable_With_Tooltip.png new file mode 100644 index 0000000000000000000000000000000000000000..7fac77a3b49495d62752663e6f39d9302a6453aa GIT binary patch literal 20587 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 literal 0 HcmV?d00001 From e629294e39fb6154ee8966abc44c61f52364dfb3 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 7 May 2024 14:13:52 -0700 Subject: [PATCH 14/18] [opinionated] Remove `selectable_list.test.tsx` changes - my 2c: I don't think they cover anything the tests in `selectable_list_item.test.tsx` don't already cover --- .../selectable_list/selectable_list.test.tsx | 73 +------------------ 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/src/components/selectable/selectable_list/selectable_list.test.tsx b/src/components/selectable/selectable_list/selectable_list.test.tsx index 57299444e67..0dba8ccea82 100644 --- a/src/components/selectable/selectable_list/selectable_list.test.tsx +++ b/src/components/selectable/selectable_list/selectable_list.test.tsx @@ -7,8 +7,7 @@ */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { render, waitForEuiToolTipVisible } from '../../../test/rtl'; +import { render } from '../../../test/rtl'; import { requiredProps } from '../../../test/required_props'; import { EuiSelectableList } from './selectable_list'; @@ -385,76 +384,6 @@ describe('EuiSelectableListItem', () => { expect(container.querySelector('.euiTextTruncate')).toBeInTheDocument(); }); }); - - describe('toolTipContent & tooltipProps', () => { - 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', - }, - ]; - - const { getByTestSubject } = render( - - ); - - fireEvent.mouseOver(getByTestSubject('titanOption')); - await waitForEuiToolTipVisible(); - - expect(getByTestSubject('optionToolTip')).toBeInTheDocument(); - expect(getByTestSubject('optionToolTip')).toHaveTextContent( - 'I am a tooltip!' - ); - }); - - it('renders a tooltip with applied props when activeOptionIndex is set', 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 waitForEuiToolTipVisible(); - - expect(getByTestSubject('optionToolTip')).toBeInTheDocument(); - expect(getByTestSubject('optionToolTip')).toHaveTextContent( - 'I am a tooltip!' - ); - }); - }); }); describe('truncation performance optimization', () => { From efe7a6b110a55e9fe4e17fa7805b8a7eec7eb562 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 7 May 2024 14:26:02 -0700 Subject: [PATCH 15/18] [opinionated refactor] move a11y attrs back to `
  • `, add manual `aria-describedby` - the `role="option"` should (I believe) go on the `
  • ` element to be semantically correct - we can tweak or manually inherit `aria-describedby` from the tooltip as necessary - this also lets us remove an extra unnecessary `` wrapper around the tooltips content --- .../selectable_list_item.test.tsx.snap | 70 +++++++++---------- .../selectable_list_item.test.tsx | 7 +- .../selectable_list/selectable_list_item.tsx | 69 +++++++++--------- 3 files changed, 74 insertions(+), 72 deletions(-) diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap index 19905d2f6c5..f089db9ce5c 100644 --- a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap +++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap @@ -513,28 +513,26 @@ exports[`EuiSelectableListItem props tooltip behavior on mouseover 1`] = ` >
  • + - - - Item content - + Item content @@ -575,46 +573,44 @@ exports[`EuiSelectableListItem props tooltip behavior when isFocused 1`] = ` >
  • + + Item content + + + - - Item content - - diff --git a/src/components/selectable/selectable_list/selectable_list_item.test.tsx b/src/components/selectable/selectable_list/selectable_list_item.test.tsx index 972a2e036f0..d537beb473e 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.test.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.test.tsx @@ -165,7 +165,7 @@ describe('EuiSelectableListItem', () => { }); test('tooltip behavior on mouseover', async () => { - const { baseElement, getByRole, getByTestSubject } = render( + const { baseElement, getByTestSubject } = render( { ); - fireEvent.mouseOver(getByRole('option')); + const tooltipAnchor = baseElement.querySelector( + '.euiSelectableListItem__tooltipAnchor' + ); + fireEvent.mouseOver(tooltipAnchor!); await waitForEuiToolTipVisible(); expect(getByTestSubject('listItemToolTip')).toBeInTheDocument(); diff --git a/src/components/selectable/selectable_list/selectable_list_item.tsx b/src/components/selectable/selectable_list/selectable_list_item.tsx index 5372d65c4c0..b6690500e64 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.tsx @@ -8,10 +8,10 @@ import classNames from 'classnames'; import React, { - AriaAttributes, Component, - createRef, LiHTMLAttributes, + ReactElement, + createRef, } from 'react'; import { CommonProps, keysOf } from '../../common'; @@ -114,6 +114,7 @@ export class EuiSelectableListItem extends Component }; tooltipRef = createRef(); + tooltipId: string | undefined; constructor(props: EuiSelectableListItemProps) { super(props); @@ -121,9 +122,13 @@ export class EuiSelectableListItem extends Component componentDidMount(): void { const { disabled, isFocused, toolTipContent } = this.props; + const hasToolTip = !disabled && !!toolTipContent; - if (isFocused === true && !disabled && !!toolTipContent) { - this.toggleToolTip(true); + if (hasToolTip) { + this.tooltipId = this.tooltipRef.current?.state.id; + if (isFocused) { + this.toggleToolTip(true); + } } } @@ -181,7 +186,6 @@ export class EuiSelectableListItem extends Component paddingSize = 's', role = 'option', searchable, - style, textWrap, toolTipContent, toolTipProps, @@ -380,7 +384,7 @@ export class EuiSelectableListItem extends Component ); - const content = ( + const content: ReactElement = ( {optionIcon} {prependNode} @@ -392,34 +396,33 @@ export class EuiSelectableListItem extends Component ); - const optionProps = { - role: role, - 'aria-disabled': disabled, - 'aria-checked': this.isChecked( - role, - checked - ) as AriaAttributes['aria-checked'], // Whether the item is "checked" - 'aria-selected': !disabled && isFocused, // Whether the item has keyboard focus per W3 spec - ...rest, - }; + const ariaDescribedBy = hasToolTip + ? classNames(this.tooltipId, rest['aria-describedby']) + : rest['aria-describedby']; - return hasToolTip ? ( - // This extra wrapper is needed to ensure proper semantics and that the tooltip has - // a correct context for positioning while also ensuring to wrap the interactive option -
  • - - {content} - -
  • - ) : ( -
  • - {content} + return ( +
  • + {hasToolTip ? ( + + {content} + + ) : ( + content + )}
  • ); } From a3572638bda82bed5a59c398b52c4aa619e172ee Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 7 May 2024 14:47:02 -0700 Subject: [PATCH 16/18] Docs pass - move tooltip example to below truncation, as the options feel related (ways of handling excess information) - make option examples/tooltip content more helpful and match combobox example + add link to EuiToolTip page and make props tab more specific --- .../views/selectable/selectable_example.js | 94 +++++++++++-------- .../views/selectable/selectable_tool_tips.tsx | 49 ++-------- 2 files changed, 64 insertions(+), 79 deletions(-) diff --git a/src-docs/src/views/selectable/selectable_example.js b/src-docs/src/views/selectable/selectable_example.js index 92ceb07facc..027f37ba7fe 100644 --- a/src-docs/src/views/selectable/selectable_example.js +++ b/src-docs/src/views/selectable/selectable_example.js @@ -9,6 +9,7 @@ import { EuiSelectableMessage, EuiText, EuiTextTruncate, + EuiToolTip, EuiCallOut, EuiLink, } from '../../../../src'; @@ -33,9 +34,6 @@ const selectableExclusionSource = require('!!raw-loader!./selectable_exclusion') import SelectableMixed from './selectable_mixed'; const selectableMixedSource = require('!!raw-loader!./selectable_mixed'); -import SelectableToolTips from './selectable_tool_tips'; -const selectableToolTipsSource = require('!!raw-loader!./selectable_tool_tips'); - import SelectableMessages from './selectable_messages'; const selectableMessagesSource = require('!!raw-loader!./selectable_messages'); @@ -45,6 +43,9 @@ const selectableSizingSource = require('!!raw-loader!./selectable_sizing'); import Truncation from './selectable_truncation'; const truncationSource = require('!!raw-loader!./selectable_truncation'); +import SelectableToolTips from './selectable_tool_tips'; +const selectableToolTipsSource = require('!!raw-loader!./selectable_tool_tips'); + import SelectableCustomRender from './selectable_custom_render'; const selectableCustomRenderSource = require('!!raw-loader!./selectable_custom_render'); @@ -293,42 +294,6 @@ export const SelectableExample = { `, }, - { - title: 'Options can have tooltips', - source: [ - { - type: GuideSectionTypes.TSX, - code: selectableToolTipsSource, - }, - ], - text: ( -

    - You can add tooltips to the options by passing{' '} - toolTipContent. Use toolTipProps{' '} - to pass additional EuiToolTipProps to the tooltip. -

    - ), - props, - demo: , - snippet: ` setOptions(newOptions)} -> - {list => list} -`, - }, - { title: 'Messages and loading', source: [ @@ -478,6 +443,57 @@ export const SelectableExample = { `, demo: , }, + + { + title: 'Tooltips', + source: [ + { + type: GuideSectionTypes.TSX, + code: selectableToolTipsSource, + }, + ], + text: ( + <> +

    + If you have longer information that you need to make available to + users outside of truncated text, one approach could be adding + tooltip descriptions to individual options by passing{' '} + toolTipContent. +

    +

    + You can additionally customize individual tooltip behavior by + passing toolTipProps, which accepts any + configuration that{' '} + + EuiToolTip + {' '} + accepts. +

    + + ), + props: { + EuiSelectableOptionProps, + EuiToolTip, + }, + demo: , + snippet: ` setOptions(newOptions)} +> + {list => list} +`, + }, { title: 'Rendering the options', source: [ diff --git a/src-docs/src/views/selectable/selectable_tool_tips.tsx b/src-docs/src/views/selectable/selectable_tool_tips.tsx index 48b778ea0cd..2536041f47e 100644 --- a/src-docs/src/views/selectable/selectable_tool_tips.tsx +++ b/src-docs/src/views/selectable/selectable_tool_tips.tsx @@ -6,57 +6,26 @@ export default () => { const [options, setOptions] = useState([ { label: 'Titan', - 'data-test-subj': 'titanOption', - toolTipContent: 'Lorem ipsum', + toolTipContent: + 'Titan is the largest moon of Saturn and the second-largest in the Solar System', }, { - label: 'Enceladus is disabled', - disabled: true, - toolTipContent: 'Lorem ipsum', - }, - { - label: 'Mimas', - checked: 'on', - toolTipContent: 'Lorem ipsum', - }, - { - label: 'Dione', - toolTipContent: 'Lorem ipsum', - toolTipProps: { position: 'bottom' }, - }, - { - label: 'Iapetus', - checked: 'on', - toolTipContent: 'Lorem ipsum', - }, - { - label: 'Phoebe', - toolTipContent: 'Lorem ipsum', - }, - { - label: 'Rhea', - toolTipContent: 'Lorem ipsum', - }, - { - label: + label: 'Pandora', + toolTipContent: "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', + label: 'Iapetus', + toolTipContent: "Iapetus is the outermost of Saturn's large moons", + toolTipProps: { position: 'bottom' }, }, ]); return ( setOptions(newOptions)} > {(list) => list} From a784e8f63b2a6cc10338afc54dd7ba7d9ccd25d6 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 7 May 2024 15:52:33 -0700 Subject: [PATCH 17/18] [storybook] misc cleanup - fix error on keyboard up/down navigation of tooltip story - `enableFunctionToggleControls` has to only be set on the playground otherwise it throws an error because it's not included - ts `as` cast cleanup --- .../selectable/selectable.stories.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/selectable/selectable.stories.tsx b/src/components/selectable/selectable.stories.tsx index 2d743693bc1..467189b76bb 100644 --- a/src/components/selectable/selectable.stories.tsx +++ b/src/components/selectable/selectable.stories.tsx @@ -13,7 +13,7 @@ import { enableFunctionToggleControls, hideStorybookControls, } from '../../../.storybook/utils'; -import { ToolTipPositions } from '../tool_tip'; + import { EuiSelectableOption } from './selectable_option'; import { EuiSelectable, @@ -23,11 +23,11 @@ import { const toolTipProps = { toolTipContent: 'This is a tooltip!', - toolTipProps: { position: 'left' as ToolTipPositions }, + toolTipProps: { position: 'left' as const }, value: 4, }; -const options = [ +const options: EuiSelectableOption[] = [ { label: 'Titan', 'data-test-subj': 'titanOption', @@ -63,7 +63,7 @@ const options = [ { label: 'Hyperion', }, -] as EuiSelectableOption[]; +]; const meta: Meta = { title: 'Forms/EuiSelectable', @@ -81,7 +81,6 @@ const meta: Meta = { isPreFiltered: false, }, }; -enableFunctionToggleControls(meta, ['onChange', 'onActiveOptionChange']); hideStorybookControls(meta, ['aria-label']); export default meta; @@ -90,11 +89,6 @@ type Story = StoryObj; export const Playground: Story = { args: { options, - // cast as any to align with earier teasing/QA - children: 'list' as any, - searchProps: { - 'data-test-subj': 'selectableSearchHere', - }, // setting up for easier testing/QA allowExclusions: false, isLoading: false, @@ -102,9 +96,13 @@ export const Playground: Story = { loadingMessage: '', noMatchesMessage: '', selectableScreenReaderText: '', + searchProps: { + 'data-test-subj': 'selectableSearchHere', + }, }, render: ({ ...args }: EuiSelectableProps) => , }; +enableFunctionToggleControls(Playground, ['onChange', 'onActiveOptionChange']); export const WithTooltip: Story = { parameters: { From 48515874d2229171bbd98f882a5f94c28546cbd0 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Tue, 7 May 2024 17:13:51 -0700 Subject: [PATCH 18/18] [opinionated refactor] Convert EuiSelectableListItem from a class component to a function component - not sure if this is too extra, I found this easier to grok personally. If it feels too much for this PR, we can revert it or pull it out to a separate PR --- .../selectable_list_item.test.tsx.snap | 4 +- .../selectable_list/selectable_list.tsx | 1 - .../selectable_list/selectable_list_item.tsx | 380 +++++++++--------- 3 files changed, 182 insertions(+), 203 deletions(-) diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap index f089db9ce5c..793ebda150a 100644 --- a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap +++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap @@ -514,7 +514,7 @@ exports[`EuiSelectableListItem props tooltip behavior on mouseover 1`] = `
  • @@ -574,7 +574,7 @@ exports[`EuiSelectableListItem props tooltip behavior when isFocused 1`] = `
  • extends Component< event.persist(); // NOTE: This is needed for React v16 backwards compatibility this.onAddOrRemoveOption(option, event); }} - ref={ref ? ref.bind(null, index) : undefined} isFocused={isFocused} title={searchableLabel || label} checked={checked} diff --git a/src/components/selectable/selectable_list/selectable_list_item.tsx b/src/components/selectable/selectable_list/selectable_list_item.tsx index b6690500e64..20c1295de2d 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.tsx @@ -8,10 +8,12 @@ import classNames from 'classnames'; import React, { - Component, + FunctionComponent, LiHTMLAttributes, ReactElement, - createRef, + useState, + useEffect, + useMemo, } from 'react'; import { CommonProps, keysOf } from '../../common'; @@ -106,115 +108,43 @@ export type EuiSelectableListItemProps = LiHTMLAttributes & toolTipProps?: EuiSelectableOption['toolTipProps']; }; -export class EuiSelectableListItem extends Component { - static defaultProps = { - showIcons: true, - onFocusBadge: true, - textWrap: 'truncate', - }; - - tooltipRef = createRef(); - tooltipId: string | undefined; - - constructor(props: EuiSelectableListItemProps) { - super(props); - } - - componentDidMount(): void { - const { disabled, isFocused, toolTipContent } = this.props; - const hasToolTip = !disabled && !!toolTipContent; - - if (hasToolTip) { - this.tooltipId = this.tooltipRef.current?.state.id; - if (isFocused) { - this.toggleToolTip(true); - } - } - } +export const EuiSelectableListItem: FunctionComponent< + EuiSelectableListItemProps +> = ({ + children, + className, + disabled, + checked, + isFocused, + showIcons = true, + prepend, + append, + allowExclusions, + onFocusBadge = true, + paddingSize = 's', + role = 'option', + searchable, + textWrap = 'truncate', + toolTipContent, + toolTipProps, + 'aria-describedby': _ariaDescribedBy, + ...rest +}) => { + const classes = classNames( + 'euiSelectableListItem', + { 'euiSelectableListItem-isFocused': isFocused }, + paddingSizeToClassNameMap[paddingSize], + className + ); - // aria-checked is intended to be used with role="checkbox" but - // the MDN documentation lists it as a possibility for role="option". - // See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-checked - // and https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role - isChecked = (role: string, checked: EuiSelectableOptionCheckedType) => { - const rolesThatCanBeMixed = ['option', 'checkbox', 'menuitemcheckbox']; - const rolesThatCanBeChecked = [ - ...rolesThatCanBeMixed, - 'radio', - 'menuitemradio', - 'switch', - ]; - if (!rolesThatCanBeChecked.includes(role)) return undefined; + const textClasses = classNames('euiSelectableListItem__text', { + [`euiSelectableListItem__text--${textWrap}`]: textWrap, + }); - switch (checked) { - case 'on': - case 'off': - return true; - case 'mixed': - if (rolesThatCanBeMixed.includes(role)) { - return 'mixed'; - } else { - return false; - } - default: - return false; - } - }; - - toggleToolTip = (isFocused: boolean) => { - if (!this.tooltipRef?.current) return; - - if (isFocused) { - this.tooltipRef.current.showToolTip(); - } else { - this.tooltipRef.current.hideToolTip(); - } - }; - - render() { - const { - children, - className, - disabled, - checked, - isFocused, - showIcons, - prepend, - append, - allowExclusions, - onFocusBadge, - paddingSize = 's', - role = 'option', - searchable, - textWrap, - toolTipContent, - toolTipProps, - ...rest - } = this.props; - - const classes = classNames( - 'euiSelectableListItem', - { - 'euiSelectableListItem-isFocused': isFocused, - }, - paddingSizeToClassNameMap[paddingSize], - className - ); - - const textClasses = classNames('euiSelectableListItem__text', { - [`euiSelectableListItem__text--${textWrap}`]: textWrap, - }); - - const hasToolTip = !disabled && !!toolTipContent; - - if (hasToolTip) { - this.toggleToolTip(isFocused ?? false); - } - - let optionIcon: React.ReactNode; + const optionIcon = useMemo(() => { if (showIcons) { const { icon, color } = resolveIconAndColor(checked); - optionIcon = ( + return ( /> ); } + }, [showIcons, checked]); + + const prependNode = useMemo(() => { + if (prepend) { + return {prepend}; + } + }, [prepend]); + + const onFocusBadgeNode = useMemo(() => { + const defaultOnFocusBadgeProps: EuiBadgeProps = { + 'aria-hidden': true, + iconType: 'returnKey', + iconSide: 'left', + color: 'hollow', + }; + + if (onFocusBadge === true) { + return ( + + ); + } else if (typeof onFocusBadge !== 'boolean' && !!onFocusBadge) { + const { children, className, ...restBadgeProps } = onFocusBadge; + return ( + + {children} + + ); + } + }, [onFocusBadge]); + const showOnFocusBadge = !!(isFocused && !disabled && onFocusBadgeNode); + const appendNode = useMemo(() => { + if (append || showOnFocusBadge) { + return ( + + {append} {showOnFocusBadge ? onFocusBadgeNode : null} + + ); + } + }, [append, showOnFocusBadge, onFocusBadgeNode]); + + const screenReaderText = useMemo(() => { let state: React.ReactNode; let instructions: React.ReactNode; + const screenReaderStrings = { checked: { state: ( @@ -323,57 +305,7 @@ export class EuiSelectableListItem extends Component break; } - let prependNode: React.ReactNode; - if (prepend) { - prependNode = ( - {prepend} - ); - } - - let appendNode: React.ReactNode; - if (append || !!onFocusBadge) { - let onFocusBadgeNode: React.ReactNode; - const defaultOnFocusBadgeProps: EuiBadgeProps = { - 'aria-hidden': true, - iconType: 'returnKey', - iconSide: 'left', - color: 'hollow', - }; - - if (onFocusBadge === true) { - onFocusBadgeNode = ( - - ); - } else if (typeof onFocusBadge !== 'boolean' && !!onFocusBadge) { - const { children, className, ...restBadgeProps } = onFocusBadge; - onFocusBadgeNode = ( - - {children} - - ); - } - - // Only display the append wrapper if append exists or isFocused - if (append || (isFocused && !disabled)) { - appendNode = ( - - {append} {isFocused && !disabled ? onFocusBadgeNode : null} - - ); - } - } - - const screenReaderText = (state || instructions) && ( + return state || instructions ? (
    {state || instructions ? '. ' : null} @@ -382,48 +314,96 @@ export class EuiSelectableListItem extends Component {instructions}
    - ); + ) : null; + }, [checked, searchable, allowExclusions]); - const content: ReactElement = ( - - {optionIcon} - {prependNode} - - {children} - {screenReaderText} - - {appendNode} - - ); + // aria-checked is intended to be used with role="checkbox" but + // the MDN documentation lists it as a possibility for role="option". + // See https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-checked + // and https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role + const ariaChecked = useMemo(() => { + const rolesThatCanBeMixed = ['option', 'checkbox', 'menuitemcheckbox']; + const rolesThatCanBeChecked = [ + ...rolesThatCanBeMixed, + 'radio', + 'menuitemradio', + 'switch', + ]; + if (!rolesThatCanBeChecked.includes(role)) return undefined; - const ariaDescribedBy = hasToolTip - ? classNames(this.tooltipId, rest['aria-describedby']) - : rest['aria-describedby']; + switch (checked) { + case 'on': + case 'off': + return true; + case 'mixed': + if (rolesThatCanBeMixed.includes(role)) { + return 'mixed'; + } else { + return false; + } + default: + return false; + } + }, [role, checked]); - return ( -
  • - {hasToolTip ? ( - - {content} - - ) : ( - content - )} -
  • - ); - } -} + const hasToolTip = !!toolTipContent && !disabled; + const [tooltipRef, setTooltipRef] = useState(null); // Needs to be state and not a ref to trigger useEffect + const [ariaDescribedBy, setAriaDescribedBy] = useState(_ariaDescribedBy); + + // Manually trigger the tooltip on keyboard focus + useEffect(() => { + if (!tooltipRef) return; + + if (isFocused) { + tooltipRef.showToolTip(); + } else { + tooltipRef.hideToolTip(); + } + }, [isFocused, tooltipRef]); + + // Manually set the `aria-describedby` id on the
  • wrapper + useEffect(() => { + if (tooltipRef) { + const tooltipId = tooltipRef.state.id; + setAriaDescribedBy(classNames(tooltipId, _ariaDescribedBy)); + } + }, [tooltipRef, _ariaDescribedBy]); + + const content: ReactElement = ( + + {optionIcon} + {prependNode} + + {children} + {screenReaderText} + + {appendNode} + + ); + + return ( +
  • + {hasToolTip ? ( + + {content} + + ) : ( + content + )} +
  • + ); +};