diff --git a/packages/eui/changelogs/upcoming/9643.md b/packages/eui/changelogs/upcoming/9643.md new file mode 100644 index 000000000000..5cd28f12fabe --- /dev/null +++ b/packages/eui/changelogs/upcoming/9643.md @@ -0,0 +1,15 @@ +- Replaced native browser `title` attributes with `EuiToolTip` across the following components for consistent tooltips: + - `EuiAvatar` + - `EuiBasicTable` + - `EuiBreadcrumbs` + - `EuiComboBox` + - `EuiDataGrid` + - `EuiAutoRefresh` + - `EuiSuperDatePicker` + - `EuiFieldPassword` + - `EuiMarkdownEditor` + - `EuiPagination` + - `EuiSearchBar` + - `EuiSelectable` + - `EuiTextTruncate` +- Extended `EuiToolTip`'s `display` prop to support `"flex"` diff --git a/packages/eui/src/components/avatar/__snapshots__/avatar.test.tsx.snap b/packages/eui/src/components/avatar/__snapshots__/avatar.test.tsx.snap index 8c74f3a7dd26..f797133340ea 100644 --- a/packages/eui/src/components/avatar/__snapshots__/avatar.test.tsx.snap +++ b/packages/eui/src/components/avatar/__snapshots__/avatar.test.tsx.snap @@ -1,360 +1,456 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiAvatar allows a name composed entirely of whitespace 1`] = ` - + + + `; exports[`EuiAvatar is rendered 1`] = ` - + + + `; exports[`EuiAvatar props casing capitalize is rendered 1`] = ` - + + + `; exports[`EuiAvatar props casing lowercase is rendered 1`] = ` - + + + `; exports[`EuiAvatar props casing none is rendered 1`] = ` - + + + `; exports[`EuiAvatar props casing uppercase is rendered 1`] = ` - + + + `; exports[`EuiAvatar props color as null is rendered 1`] = ` - + + + `; exports[`EuiAvatar props color as plain is rendered 1`] = ` - + + + `; exports[`EuiAvatar props color as string is rendered 1`] = ` - + + + `; exports[`EuiAvatar props color as subdued is rendered 1`] = ` - + + + `; exports[`EuiAvatar props iconType and iconColor as null is rendered 1`] = ` - + + `; exports[`EuiAvatar props iconType and iconColor is rendered 1`] = ` - + + `; exports[`EuiAvatar props iconType and iconSize is rendered 1`] = ` - + + `; exports[`EuiAvatar props iconType is rendered 1`] = ` - + + `; exports[`EuiAvatar props imageUrl is rendered 1`] = ` - + `; exports[`EuiAvatar props isDisabled is rendered 1`] = ` - + + + `; exports[`EuiAvatar props size l is rendered 1`] = ` - + + + `; exports[`EuiAvatar props size m is rendered 1`] = ` - + + + `; exports[`EuiAvatar props size s is rendered 1`] = ` - + + + `; exports[`EuiAvatar props size xl is rendered 1`] = ` - + + + `; exports[`EuiAvatar props type is rendered 1`] = ` - + + + `; diff --git a/packages/eui/src/components/avatar/avatar.test.tsx b/packages/eui/src/components/avatar/avatar.test.tsx index f0d3fc0fd168..0bb494904eea 100644 --- a/packages/eui/src/components/avatar/avatar.test.tsx +++ b/packages/eui/src/components/avatar/avatar.test.tsx @@ -7,6 +7,7 @@ */ import React from 'react'; +import { fireEvent } from '@testing-library/react'; import { shouldRenderCustomStyles } from '../../test/internal'; import { requiredProps } from '../../test/required_props'; import { render } from '../../test/rtl'; @@ -168,6 +169,17 @@ describe('EuiAvatar', () => { }); }); + describe('tooltip', () => { + it('shows a tooltip with the avatar name on hover', () => { + const { getByRole, queryByRole } = render(); + expect(queryByRole('tooltip')).not.toBeInTheDocument(); + + fireEvent.mouseOver(getByRole('img')); + + expect(getByRole('tooltip')).toHaveTextContent('Jane Doe'); + }); + }); + test('should throw error if color is not a hex', () => { const component = () => render(); diff --git a/packages/eui/src/components/avatar/avatar.tsx b/packages/eui/src/components/avatar/avatar.tsx index 746537783964..c2f1386b55a4 100644 --- a/packages/eui/src/components/avatar/avatar.tsx +++ b/packages/eui/src/components/avatar/avatar.tsx @@ -22,6 +22,7 @@ import { } from '../../services/color'; import { toInitials, useEuiMemoizedStyles, useEuiTheme } from '../../services'; import { IconType, EuiIcon, IconSize, IconColor } from '../icon'; +import { EuiToolTip } from '../tool_tip'; import { euiAvatarStyles } from './avatar.styles'; @@ -80,7 +81,8 @@ export type EuiAvatarProps = Omit, 'color'> & CommonProps & _EuiAvatarContent & { /** - * Full name of avatar for title attribute and calculating initial if not provided + * Full name of the avatar. Used as the accessible label (`aria-label`), + * tooltip content and used to derive initials when `initials` is not provided. */ name: string; @@ -200,14 +202,13 @@ export const EuiAvatar: FunctionComponent = ({ return avatarStyle?.color; }, [iconColor, avatarStyle?.color, isForcedColors, euiTheme]); - return ( + const avatarNode = (
{!imageUrl && @@ -217,6 +218,7 @@ export const EuiAvatar: FunctionComponent = ({ size={iconSize || size} type={iconType} color={iconCustomColor} + aria-hidden={true} /> ) : (
); + + // `EuiAvatar` is not interactive so we don't need to add a `tabIndex`. + // It already has `aria-label`, the tooltip is only visual. + return name ? ( + + {avatarNode} + + ) : ( + avatarNode + ); }; // TODO: Migrate to a service diff --git a/packages/eui/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap b/packages/eui/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap index d7b91c4c9368..238906a2cd3f 100644 --- a/packages/eui/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap +++ b/packages/eui/src/components/basic_table/__snapshots__/basic_table.test.tsx.snap @@ -237,31 +237,35 @@ exports[`EuiBasicTable renders (kitchen sink) with pagination, selection, sortin
-
- + + + - - +
-
+ diff --git a/packages/eui/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap b/packages/eui/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap index 648e39840f35..5a16bf682e17 100644 --- a/packages/eui/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap +++ b/packages/eui/src/components/basic_table/__snapshots__/collapsed_item_actions.test.tsx.snap @@ -11,7 +11,7 @@ exports[`CollapsedItemActions custom actions 1`] = ` > - - + + - diff --git a/packages/eui/src/components/basic_table/basic_table.test.tsx b/packages/eui/src/components/basic_table/basic_table.test.tsx index d64d2d392641..fa9812fb62ba 100644 --- a/packages/eui/src/components/basic_table/basic_table.test.tsx +++ b/packages/eui/src/components/basic_table/basic_table.test.tsx @@ -831,9 +831,18 @@ describe('EuiBasicTable', () => { }; const { getByTestSubject } = render(); - expect(getByTestSubject('deleteAction-1')).toBeDisabled(); - expect(getByTestSubject('deleteAction-2')).toBeDisabled(); - expect(getByTestSubject('deleteAction-3')).toBeDisabled(); + expect(getByTestSubject('deleteAction-1')).toHaveAttribute( + 'aria-disabled', + 'true' + ); + expect(getByTestSubject('deleteAction-2')).toHaveAttribute( + 'aria-disabled', + 'true' + ); + expect(getByTestSubject('deleteAction-3')).toHaveAttribute( + 'aria-disabled', + 'true' + ); }); test('multiple actions', () => { @@ -855,7 +864,7 @@ describe('EuiBasicTable', () => { getAllByTestSubject('euiCollapsedItemActionsButton').forEach( (button) => { - expect(button).toBeDisabled(); + expect(button).toHaveAttribute('aria-disabled', 'true'); } ); }); diff --git a/packages/eui/src/components/basic_table/basic_table.tsx b/packages/eui/src/components/basic_table/basic_table.tsx index dbfcfafb2248..b160a6f36a24 100644 --- a/packages/eui/src/components/basic_table/basic_table.tsx +++ b/packages/eui/src/components/basic_table/basic_table.tsx @@ -82,6 +82,7 @@ import { euiBasicTableBodyLoading, safariLoadingWorkaround, } from './basic_table.styles'; +import { EuiToolTip } from '../tool_tip'; type DataTypeProfiles = Record< EuiTableDataType, @@ -743,17 +744,24 @@ export class EuiBasicTable extends Component< defaults={['Select all rows', 'Deselect rows']} > {([selectAllRows, deselectRows]: string[]) => ( - + + + )} ); @@ -995,7 +1003,8 @@ export class EuiBasicTable extends Component< colSpan={colSpan} mobileOptions={{ width: '100%' }} > - {error} + {' '} + {error} ); @@ -1184,15 +1193,20 @@ export class EuiBasicTable extends Component< values={{ index: displayedRowIndex + 1 }} > {(selectThisRow: string) => ( - + + + )} , diff --git a/packages/eui/src/components/basic_table/collapsed_item_actions.tsx b/packages/eui/src/components/basic_table/collapsed_item_actions.tsx index 742f6c251af0..4a50637c42aa 100644 --- a/packages/eui/src/components/basic_table/collapsed_item_actions.tsx +++ b/packages/eui/src/components/basic_table/collapsed_item_actions.tsx @@ -137,17 +137,24 @@ export const CollapsedItemActions = ({ ? allActionsButtonDisabledAriaLabel : allActionsButtonAriaLabel } - title={actionsDisabled ? allActionsButtonDisabledAriaLabel : undefined} iconType="boxesVertical" color="text" isDisabled={actionsDisabled} + hasAriaDisabled={actionsDisabled} onClick={() => setPopoverOpen((isOpen) => !isOpen)} data-test-subj="euiCollapsedItemActionsButton" /> ); - const withTooltip = !actionsDisabled && ( - {popoverButton} + const withTooltip = ( + + {popoverButton} + ); return ( @@ -155,7 +162,7 @@ export const CollapsedItemActions = ({ className={className} id={`${itemId}-actions`} isOpen={popoverOpen} - button={withTooltip || popoverButton} + button={withTooltip} closePopover={closePopover} panelPaddingSize="none" anchorPosition="leftCenter" diff --git a/packages/eui/src/components/basic_table/default_item_action.tsx b/packages/eui/src/components/basic_table/default_item_action.tsx index 16733202b239..6e4765789af3 100644 --- a/packages/eui/src/components/basic_table/default_item_action.tsx +++ b/packages/eui/src/components/basic_table/default_item_action.tsx @@ -81,15 +81,13 @@ export const DefaultItemAction = ({ className={className} aria-labelledby={ariaLabelId} isDisabled={!enabled} + hasAriaDisabled={!enabled} color={color} iconType={icon} onClick={onClick} href={href} target={action.target} data-test-subj={dataTestSubj} - // If action is disabled, the normal tooltip can't show - attempt to - // provide some amount of affordance with a browser title tooltip - title={!enabled ? tooltipContent : undefined} /> ); // actionContent (action.name) is a ReactNode and must be rendered @@ -105,6 +103,7 @@ export const DefaultItemAction = ({ className={className} size="s" isDisabled={!enabled} + hasAriaDisabled={!enabled} color={color as EuiButtonEmptyProps['color']} iconType={icon} onClick={onClick} @@ -118,17 +117,12 @@ export const DefaultItemAction = ({ ); } - return enabled ? ( + return ( <> {button} {/* SR text has to be rendered outside the tooltip, otherwise EuiToolTip's own aria-labelledby won't properly clone */} {ariaLabelledBy} - ) : ( - <> - {button} - {ariaLabelledBy} - ); }; diff --git a/packages/eui/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap b/packages/eui/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap index 7bc4f8e16efa..98da30e0c46f 100644 --- a/packages/eui/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap +++ b/packages/eui/src/components/breadcrumbs/__snapshots__/_breadcrumb_content.test.tsx.snap @@ -7,25 +7,29 @@ exports[`EuiBreadcrumbContent breadcrumbs with popovers renders with \`popoverCo class="euiPopover euiPopover-isOpen emotion-euiPopover-inline-block-euiBreadcrumb__popoverWrapper-page" data-test-subj="popover" > - + + Toggles a popover + + + - Clicking this button will toggle a popover dialog. + + +
- + + - Clicking this button will toggle a popover dialog. + + +
diff --git a/packages/eui/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap b/packages/eui/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap index c00967f7d3fa..f6eb1d0eab63 100644 --- a/packages/eui/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap +++ b/packages/eui/src/components/breadcrumbs/__snapshots__/breadcrumbs.test.tsx.snap @@ -41,27 +41,31 @@ exports[`EuiBreadcrumbs is rendered 1`] = `
- + + - Clicking this button will toggle a popover dialog. + + +
  • - + + - Clicking this button will toggle a popover dialog. + + +
  • - + + - Clicking this button will toggle a popover dialog. + + +
  • - + + - Clicking this button will toggle a popover dialog. + + +
  • - + + - Clicking this button will toggle a popover dialog. + + +
  • - + + - Clicking this button will toggle a popover dialog. + + +
  • { fireEvent.click(getByTestSubject('popoverClose')); await waitForEuiPopoverClose(); }); + + it('exposes the tooltip to screen readers only when an explicit `title` is passed', () => { + const { getByTestSubject, rerender } = render( + + ); + + // Without explicit `title` prop, the tooltip uses the visible text and SR output is suppressed + fireEvent.mouseOver(getByTestSubject('popoverToggle')); + expect(getByTestSubject('popoverToggle')).not.toHaveAttribute( + 'aria-describedby' + ); + + rerender( + + ); + + // With an explicit `title` prop, the tooltip is announced with `aria-describedby` + fireEvent.mouseOver(getByTestSubject('popoverToggle')); + expect(getByTestSubject('popoverToggle')).toHaveAttribute( + 'aria-describedby' + ); + }); }); describe('highlightLastBreadcrumb', () => { diff --git a/packages/eui/src/components/breadcrumbs/_breadcrumb_content.tsx b/packages/eui/src/components/breadcrumbs/_breadcrumb_content.tsx index 7322100910b3..bf0c5f9982d6 100644 --- a/packages/eui/src/components/breadcrumbs/_breadcrumb_content.tsx +++ b/packages/eui/src/components/breadcrumbs/_breadcrumb_content.tsx @@ -22,6 +22,7 @@ import { EuiLink } from '../link'; import { EuiPopover } from '../popover'; import { EuiIcon } from '../icon'; import { useEuiI18n } from '../i18n'; +import { EuiToolTip } from '../tool_tip'; import type { EuiBreadcrumbProps, _EuiBreadcrumbProps } from './types'; import { @@ -47,6 +48,7 @@ export const EuiBreadcrumbContent: FunctionComponent< isOnlyBreadcrumb, highlightLastBreadcrumb, truncateLastBreadcrumb, + title: propTitle, ...rest }) => { const isApplication = type === 'application'; @@ -84,10 +86,12 @@ export const EuiBreadcrumbContent: FunctionComponent< !isApplication && styles.isInteractive; + const hasExplicitTitle = propTitle != null && propTitle !== ''; + return ( {(ref, innerText) => { - const title = innerText === '' ? undefined : innerText; + const title = propTitle || (innerText === '' ? undefined : innerText); const baseProps = { ref, title, @@ -101,6 +105,8 @@ export const EuiBreadcrumbContent: FunctionComponent< return ( & Pick<_EuiBreadcrumbProps, 'type' | 'isLastBreadcrumb'> & { breadcrumbCss: ArrayCSSInterpolation; truncationCss: ArrayCSSInterpolation; + hasExplicitTitle: boolean; }; const EuiBreadcrumbPopover = forwardRef< HTMLButtonElement, @@ -155,6 +162,7 @@ const EuiBreadcrumbPopover = forwardRef< color, type, title, + hasExplicitTitle, 'aria-current': ariaCurrent, className, isLastBreadcrumb, @@ -194,6 +202,25 @@ const EuiBreadcrumbPopover = forwardRef< ...truncationCss, ]; + const linkButton = ( + + {children} + + + ); + return ( - {children} - - + title ? ( + + {linkButton} + + ) : ( + linkButton + ) } > {typeof popoverContent === 'function' diff --git a/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap b/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap index 3a6adcf33024..5027ad19241c 100644 --- a/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap +++ b/packages/eui/src/components/card/__snapshots__/card.test.tsx.snap @@ -207,18 +207,22 @@ exports[`EuiCard props an avatar icon 1`] = `
    - + +
    +
    extends Component< const hasOnFocusBadge = onFocusBadge && optionIsFocused && !optionIsDisabled; + const hasNativeTruncation = + !hasTruncationProps && !searchValue && rowHeight !== 'auto'; + return ( extends Component< // uses the original `options` array for the index to ensure a stable `id`, otherwise `aria-activedescendant` // loses focus on selecting an option (due to actively removing it from the list) id={rootId(`_option-${options.indexOf(option)}`)} + title={hasNativeTruncation && !toolTipContent ? label : undefined} key={option.key ?? option.label} - title={label} prepend={option.prepend} append={ hasOnFocusBadge ? ( diff --git a/packages/eui/src/components/comment_list/__snapshots__/comment.test.tsx.snap b/packages/eui/src/components/comment_list/__snapshots__/comment.test.tsx.snap index d6ca3c7ce541..19c826e6494b 100644 --- a/packages/eui/src/components/comment_list/__snapshots__/comment.test.tsx.snap +++ b/packages/eui/src/components/comment_list/__snapshots__/comment.test.tsx.snap @@ -16,9 +16,9 @@ exports[`EuiComment is rendered 1`] = ` aria-label="" class="euiAvatar euiAvatar--m euiAvatar--user euiCommentAvatar emotion-euiAvatar-user-m-uppercase-subdued" role="img" - title="" >
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    -
    - -
    + +
    +
    - + +
    - + +
    diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/__snapshots__/date_popover_button.test.tsx.snap b/packages/eui/src/components/date_picker/super_date_picker/date_popover/__snapshots__/date_popover_button.test.tsx.snap index 8a828829d60a..0313d4a5a426 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/__snapshots__/date_popover_button.test.tsx.snap +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/__snapshots__/date_popover_button.test.tsx.snap @@ -10,7 +10,6 @@ exports[`EuiDatePopoverButton renders 1`] = ` aria-expanded="false" class="euiDatePopoverButton euiDatePopoverButton--start emotion-euiDatePopoverButton" data-test-subj="superDatePickerstartDatePopoverButton" - title="" type="button" /> diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx index 46c54a4e4060..96bc98373051 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx @@ -26,6 +26,7 @@ import { useEuiI18n } from '../../../i18n'; import { EuiFormRow, EuiFieldText, EuiFormLabel } from '../../../form'; import { EuiFlexGroup } from '../../../flex'; import { EuiButtonIcon } from '../../../button'; +import { EuiToolTip } from '../../../tool_tip'; import { EuiCode } from '../../../code'; import { EuiDatePicker, EuiDatePickerProps } from '../../date_picker'; @@ -226,16 +227,17 @@ export const EuiAbsoluteTab: FunctionComponent = ({ /> {hasUnparsedText && ( - + + + )} { /* !important needed to override date range picker nested styles */ flex-grow: 0.5 !important; /* stylelint-disable-line declaration-no-important */ `, + tooltipAnchor: css` + ${logicalCSS('height', '100%')} + ${logicalCSS('width', '100%')} + `, }; }; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx index 2b3f4b419ff5..15ed1bd31632 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx @@ -18,6 +18,7 @@ import { useEuiMemoizedStyles } from '../../../../services'; import { CommonProps } from '../../../common'; import { useEuiI18n } from '../../../i18n'; import { EuiPopover, EuiPopoverProps } from '../../../popover'; +import { EuiToolTip } from '../../../tool_tip'; import { TimeOptions } from '../time_options'; import { useFormatTimeString } from '../pretty_duration'; @@ -121,12 +122,11 @@ export const EuiDatePopoverButton: FunctionComponent< title = outdatedTitle; } - const button = ( + const rawButton = ( - + + + `; diff --git a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx index 1891680f4e2d..8a791000e6e2 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx @@ -20,6 +20,7 @@ import { useEuiMemoizedStyles } from '../../../../services'; import { useEuiI18n } from '../../../i18n'; import { EuiPopover } from '../../../popover'; import { EuiFormAppendPrependButtonProps, EuiFormPrepend } from '../../../form'; +import { EuiToolTip } from '../../../tool_tip'; import { euiQuickSelectPopoverStyles } from './quick_select_popover.styles'; import { EuiQuickSelectPanel } from './quick_select_panel'; import { EuiQuickSelect } from './quick_select'; @@ -93,7 +94,7 @@ export const EuiQuickSelectPopover: FunctionComponent< [_applyTime, closePopover] ); - const buttonlabel = useEuiI18n( + const buttonLabel = useEuiI18n( 'euiQuickSelectPopover.buttonLabel', 'Date quick select' ); @@ -106,17 +107,18 @@ export const EuiQuickSelectPopover: FunctionComponent< }; const quickSelectButton = ( - + + + ); return ( diff --git a/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap b/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap index 994b8ccf3029..cf493570821d 100644 --- a/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap +++ b/packages/eui/src/components/form/field_password/__snapshots__/field_password.test.tsx.snap @@ -109,19 +109,23 @@ exports[`EuiFieldPassword props dual dual type also renders append 1`] = ` Span - + + `; @@ -158,20 +162,24 @@ exports[`EuiFieldPassword props dual dualToggleProps is rendered 1`] = `
    - + +
    `; @@ -389,19 +397,23 @@ exports[`EuiFieldPassword props type dual is rendered 1`] = `
    - + +
    `; diff --git a/packages/eui/src/components/form/field_password/field_password.test.tsx b/packages/eui/src/components/form/field_password/field_password.test.tsx index 34031db78dfe..aa0d9d0ca405 100644 --- a/packages/eui/src/components/form/field_password/field_password.test.tsx +++ b/packages/eui/src/components/form/field_password/field_password.test.tsx @@ -122,7 +122,6 @@ describe('EuiFieldPassword', () => { aria-label="Show password as plain text. Note: this will visually expose your password on the screen." class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary" data-test-subj="toggleButton" - title="Show password as plain text. Note: this will visually expose your password on the screen." type="button" > { aria-label="Mask password" class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary" data-test-subj="toggleButton" - title="Mask password" type="button" > = ( const isVisible = inputType === 'text'; return ( - ) => - handleToggle(e, isVisible) - } - /> + + ) => + handleToggle(e, isVisible) + } + /> + ); } }, [ diff --git a/packages/eui/src/components/header/header.a11y.tsx b/packages/eui/src/components/header/header.a11y.tsx index 7dc6162be3ac..6cacdee08298 100644 --- a/packages/eui/src/components/header/header.a11y.tsx +++ b/packages/eui/src/components/header/header.a11y.tsx @@ -404,7 +404,7 @@ describe('EuiHeader', () => { }); it('has zero violations when a hidden breadcrumb is expanded', () => { - cy.get('button[title="See collapsed breadcrumbs"]').realClick(); + cy.get('[aria-label="See collapsed breadcrumbs"]').realClick(); cy.get('a[data-test-subj="cy-breadcrumb-hidden"]').should('exist'); cy.checkAxe(); }); diff --git a/packages/eui/src/components/markdown_editor/__snapshots__/markdown_editor.test.tsx.snap b/packages/eui/src/components/markdown_editor/__snapshots__/markdown_editor.test.tsx.snap index e25640ad62af..9aa3bd3ab335 100644 --- a/packages/eui/src/components/markdown_editor/__snapshots__/markdown_editor.test.tsx.snap +++ b/packages/eui/src/components/markdown_editor/__snapshots__/markdown_editor.test.tsx.snap @@ -229,7 +229,7 @@ exports[`EuiMarkdownEditor is rendered 1`] = ` /> - + + + + + + + + +
      @@ -340,20 +352,24 @@ exports[`EuiPagination props activePage is rendered 1`] = `
    - + + + `; @@ -549,34 +565,42 @@ exports[`EuiPagination props pageCount can be 0 1`] = ` data-euiicon-type="chevronSingleLeft" /> - - + + + + + + `; @@ -738,20 +762,24 @@ exports[`EuiPagination props pageCount is rendered 1`] = `
  • - + + + `; diff --git a/packages/eui/src/components/pagination/pagination_button_arrow.tsx b/packages/eui/src/components/pagination/pagination_button_arrow.tsx index d845b087e4ac..b5ca044d572d 100644 --- a/packages/eui/src/components/pagination/pagination_button_arrow.tsx +++ b/packages/eui/src/components/pagination/pagination_button_arrow.tsx @@ -16,6 +16,7 @@ import { import { keysOf } from '../common'; import { useEuiI18n } from '../i18n'; import { useEuiTheme } from '../../services'; +import { EuiToolTip } from '../tool_tip'; import { euiPaginationButtonStyles } from './pagination_button.styles'; const typeToIconTypeMap = { @@ -61,13 +62,12 @@ export const EuiPaginationButtonArrow: FunctionComponent = ({ buttonProps['aria-controls'] = ariaControls; } - return ( + const button = ( = ({ {...buttonProps} /> ); + + return disabled ? ( + button + ) : ( + + {button} + + ); }; diff --git a/packages/eui/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap b/packages/eui/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap index 102a52d13053..22094b7ac4f4 100644 --- a/packages/eui/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap +++ b/packages/eui/src/components/search_bar/__snapshots__/search_bar.test.tsx.snap @@ -7,36 +7,41 @@ exports[`SearchBar render - box 1`] = `
    -
    - + class="euiFormControlLayoutCustomIcon emotion-euiFormControlLayoutCustomIcon" + > + +
    +
    -
    -
    + `; @@ -48,35 +53,40 @@ exports[`SearchBar render - no config, no query 1`] = `
    -
    - + class="euiFormControlLayoutCustomIcon emotion-euiFormControlLayoutCustomIcon" + > + +
    +
    -
    -
    + `; @@ -88,50 +98,55 @@ exports[`SearchBar render - provided query, filters 1`] = `
    -
    - -
    - -
    -
    + +
    - - + +
    -
    +
    -
    - + class="euiFormControlLayoutCustomIcon emotion-euiFormControlLayoutCustomIcon" + > + +
    +
    -
    -
    +
    { cy.get('button').click(); cy.get('[data-test-subj="euiSelectableList"] li') .first() - .should('have.attr', 'title', 'feature'); + .should('contain.text', 'feature'); }); it('allows options as an array', () => { @@ -97,7 +97,7 @@ describe('FieldValueSelectionFilter', () => { cy.get('button').click(); cy.get('[data-test-subj="euiSelectableList"] li') .eq(1) - .should('have.attr', 'title', 'Text'); + .should('contain.text', 'Text'); }); it('allows fields in options', () => { @@ -133,7 +133,7 @@ describe('FieldValueSelectionFilter', () => { cy.get('button').click(); cy.get('[data-test-subj="euiSelectableList"] li') .eq(2) - .should('have.attr', 'title', 'Bug'); + .should('contain.text', 'bug'); }); it('allows all configurations', () => { @@ -168,21 +168,21 @@ describe('FieldValueSelectionFilter', () => { cy.mount(); cy.get('button').click(); - cy.get('li[role="option"][title="feature"]') + cy.contains('li[role="option"]', 'feature') .should('have.attr', 'aria-checked', 'false') .click(); cy.get('.euiNotificationBadge').should('have.text', '1'); - cy.get('li[role="option"][title="feature"]').should( + cy.contains('li[role="option"]', 'feature').should( 'have.attr', 'aria-checked', 'true' ); // Popover should still be open when multiselect is true/or - cy.get('li[role="option"][title="Bug"]') + cy.contains('li[role="option"]', 'bug') .should('have.attr', 'aria-checked', 'false') .click(); cy.get('.euiNotificationBadge').should('have.text', '2'); - cy.get('li[role="option"][title="Bug"]').should( + cy.contains('li[role="option"]', 'bug').should( 'have.attr', 'aria-checked', 'true' @@ -193,11 +193,11 @@ describe('FieldValueSelectionFilter', () => { cy.mount(); cy.get('button').click(); - cy.get('li[role="option"][title="feature"]') + cy.contains('li[role="option"]', 'feature') .should('have.attr', 'aria-selected', 'false') .click(); cy.get('.euiNotificationBadge').should('have.text', '1'); - cy.get('li[role="option"][title="feature"]').should( + cy.contains('li[role="option"]', 'feature').should( 'have.attr', 'aria-selected', 'true' @@ -205,18 +205,18 @@ describe('FieldValueSelectionFilter', () => { // Multiselect false should close the popover, so we need to re-open it cy.get('button').click(); - cy.get('li[role="option"][title="Bug"]') + cy.contains('li[role="option"]', 'bug') .should('have.attr', 'aria-selected', 'false') .click(); // Filter count should have remained at 1 cy.get('.euiNotificationBadge').should('have.text', '1'); - cy.get('li[role="option"][title="Bug"]').should( + cy.contains('li[role="option"]', 'bug').should( 'have.attr', 'aria-selected', 'true' ); // 'featured' should now be unchecked - cy.get('li[role="option"][title="feature"]').should( + cy.contains('li[role="option"]', 'feature').should( 'have.attr', 'aria-selected', 'false' @@ -225,15 +225,13 @@ describe('FieldValueSelectionFilter', () => { }); describe('auto-close testing', () => { - const selectFilter = (state: 'checked' | 'selected' = 'checked') => { + const selectFilter = () => { // Open popover cy.get('button').click(); cy.get('.euiPopover__panel').should('exist'); // Select filter option - cy.get('li[role="option"][title="feature"]') - .should('have.attr', `aria-${state}`, 'false') - .click(); + cy.contains('li[role="option"]', 'feature').click(); }; describe('undefined', () => { @@ -244,7 +242,7 @@ describe('FieldValueSelectionFilter', () => { multiSelect={true} /> ); - selectFilter('checked'); + selectFilter(); cy.get('.euiPopover__panel').should('exist'); }); @@ -255,7 +253,7 @@ describe('FieldValueSelectionFilter', () => { multiSelect={false} /> ); - selectFilter('selected'); + selectFilter(); cy.get('.euiPopover__panel').should('not.exist'); }); }); @@ -268,7 +266,7 @@ describe('FieldValueSelectionFilter', () => { multiSelect={true} /> ); - selectFilter('checked'); + selectFilter(); cy.get('.euiPopover__panel').should('exist'); }); @@ -279,7 +277,7 @@ describe('FieldValueSelectionFilter', () => { multiSelect={false} /> ); - selectFilter('selected'); + selectFilter(); cy.get('.euiPopover__panel').should('exist'); }); }); @@ -292,7 +290,7 @@ describe('FieldValueSelectionFilter', () => { multiSelect={true} /> ); - selectFilter('checked'); + selectFilter(); cy.get('.euiPopover__panel').should('not.exist'); }); @@ -304,7 +302,7 @@ describe('FieldValueSelectionFilter', () => { /> ); - selectFilter('selected'); + selectFilter(); cy.get('.euiPopover__panel').should('not.exist'); }); }); @@ -318,11 +316,11 @@ describe('FieldValueSelectionFilter', () => { cy.get('button').click(); getOptions().should('have.length', 3); - getOptions().last().should('have.attr', 'title', 'Bug').click(); + getOptions().last().should('contain.text', 'bug').click(); // Should have moved to the top of the list and retained active focus getOptions() .first() - .should('have.attr', 'title', 'Bug') + .should('contain.text', 'bug') .should('have.attr', 'aria-checked', 'true'); cy.get('ul[role="listbox"]') .should('have.attr', 'aria-activedescendant') @@ -334,10 +332,10 @@ describe('FieldValueSelectionFilter', () => { cy.get('button').click(); getOptions().should('have.length', 3); - getOptions().last().should('have.attr', 'title', 'Bug').click(); + getOptions().last().should('contain.text', 'bug').click(); getOptions() .last() - .should('have.attr', 'title', 'Bug') + .should('contain.text', 'bug') .should('have.attr', 'aria-checked', 'true'); cy.get('ul[role="listbox"]') .should('have.attr', 'aria-activedescendant') @@ -363,7 +361,7 @@ describe('FieldValueSelectionFilter', () => { cy.get('button').click(); cy.get('[data-test-subj="euiSelectableList"] li') .first() - .should('have.attr', 'title', 'feature'); + .should('contain.text', 'feature'); }); it('has active filters, field is global', () => { @@ -385,7 +383,7 @@ describe('FieldValueSelectionFilter', () => { cy.get('.euiNotificationBadge').should('not.be.undefined'); cy.get('[data-test-subj="euiSelectableList"] li') .first() - .should('have.attr', 'title', 'Bug'); + .should('contain.text', 'bug'); }); it('has inactive filters, fields in options', () => { @@ -421,7 +419,7 @@ describe('FieldValueSelectionFilter', () => { cy.get('button').click(); cy.get('[data-test-subj="euiSelectableList"] li') .eq(2) - .should('have.attr', 'title', 'Bug'); + .should('contain.text', 'bug'); }); it('has active filters, fields in options', () => { @@ -458,7 +456,7 @@ describe('FieldValueSelectionFilter', () => { cy.get('.euiNotificationBadge').should('not.be.undefined'); cy.get('[data-test-subj="euiSelectableList"] li') .eq(0) - .should('have.attr', 'title', 'Bug'); + .should('contain.text', 'bug'); }); it('caches options if options is a function and config.cache is set', () => { diff --git a/packages/eui/src/components/search_bar/search_bar.test.tsx b/packages/eui/src/components/search_bar/search_bar.test.tsx index 77dc9a151d47..9a0ce7c71b9e 100644 --- a/packages/eui/src/components/search_bar/search_bar.test.tsx +++ b/packages/eui/src/components/search_bar/search_bar.test.tsx @@ -79,10 +79,10 @@ describe('SearchBar', () => { onChange: () => {}, }; - const { container, findByTitle } = render(); + const { container, findByText } = render(); // Wait for FieldValueSelectionFilter to finish updating its state on init - await findByTitle('Tag'); + await findByText('Tag'); expect(container.firstChild).toMatchSnapshot(); }); diff --git a/packages/eui/src/components/search_bar/search_bar.tsx b/packages/eui/src/components/search_bar/search_bar.tsx index 4c9730f417ed..f5736499a4bc 100644 --- a/packages/eui/src/components/search_bar/search_bar.tsx +++ b/packages/eui/src/components/search_bar/search_bar.tsx @@ -11,6 +11,7 @@ import React, { Component, ReactElement } from 'react'; import { RenderWithEuiTheme, htmlIdGenerator } from '../../services'; import { isString } from '../../services/predicate'; import { EuiFlexGroup, EuiFlexItem } from '../flex'; +import { EuiToolTip } from '../tool_tip'; import { EuiSearchBox } from './search_box'; import { EuiSearchBarFilters, SearchFilterConfig } from './search_filters'; import { Query } from './query'; @@ -282,6 +283,28 @@ export class EuiSearchBar extends Component { const isHintVisible = hint?.popoverProps?.isOpen ?? isHintVisibleState; + const searchBox = ( + { + this.setState({ isHintVisible: isVisible }); + }, + id: this.hintId, + ...hint, + } + : undefined + } + /> + ); + return ( {(euiTheme) => ( @@ -292,26 +315,9 @@ export class EuiSearchBar extends Component { css={euiSearchBar__searchHolder(euiTheme)} grow={true} > - { - this.setState({ isHintVisible: isVisible }); - }, - id: this.hintId, - ...hint, - } - : undefined - } - /> + + {searchBox} + {filters && ( = ({ placeholder, incremental, hint, + onFocus, ...rest }) => { const inputRef = useRef(null); @@ -66,8 +67,9 @@ export const EuiSearchBox: FunctionComponent = ({ incremental={incremental} aria-label={incremental ? ariaLabelIncremental : ariaLabelEnter} placeholder={placeholder ?? defaultPlaceholder} - onFocus={() => { + onFocus={(e) => { hint?.setIsVisible(true); + onFocus?.(e); }} {...rest} /> diff --git a/packages/eui/src/components/selectable/selectable.spec.tsx b/packages/eui/src/components/selectable/selectable.spec.tsx index 4fe7b12ea6b0..1099c69f07c5 100644 --- a/packages/eui/src/components/selectable/selectable.spec.tsx +++ b/packages/eui/src/components/selectable/selectable.spec.tsx @@ -85,9 +85,7 @@ describe('EuiSelectable', () => { .realClick() .realType('enc') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Enceladus'); + cy.get('li[role=option]').first().should('contain.text', 'Enceladus'); }); }); @@ -99,9 +97,7 @@ describe('EuiSelectable', () => { .realClick() .realType('enc') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Enceladus'); + cy.get('li[role=option]').first().should('contain.text', 'Enceladus'); }); // Clear search using ENTER @@ -109,9 +105,7 @@ describe('EuiSelectable', () => { .focus() .realPress('{enter}') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Titan'); + cy.get('li[role=option]').first().should('contain.text', 'Titan'); }); // Search/filter again @@ -119,9 +113,7 @@ describe('EuiSelectable', () => { .realClick() .realType('enc') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Enceladus'); + cy.get('li[role=option]').first().should('contain.text', 'Enceladus'); }); // Clear search using SPACE @@ -129,9 +121,7 @@ describe('EuiSelectable', () => { .focus() .realPress('Space') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Titan'); + cy.get('li[role=option]').first().should('contain.text', 'Titan'); }); // Ensure the clear button does not respond to up/down arrow keys @@ -139,23 +129,17 @@ describe('EuiSelectable', () => { .realClick() .realType('titan') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Titan'); + cy.get('li[role=option]').first().should('contain.text', 'Titan'); }); cy.get('[data-test-subj="clearSearchButton"]') .focus() .realPress('ArrowDown') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Titan'); + cy.get('li[role=option]').first().should('contain.text', 'Titan'); }) .realPress('ArrowUp') .then(() => { - cy.get('li[role=option]') - .first() - .should('have.attr', 'title', 'Titan'); + cy.get('li[role=option]').first().should('contain.text', 'Titan'); }); }); diff --git a/packages/eui/src/components/selectable/selectable_list/selectable_list.tsx b/packages/eui/src/components/selectable/selectable_list/selectable_list.tsx index bb221f0d877b..6b8115449221 100644 --- a/packages/eui/src/components/selectable/selectable_list/selectable_list.tsx +++ b/packages/eui/src/components/selectable/selectable_list/selectable_list.tsx @@ -493,7 +493,11 @@ export class EuiSelectableList extends Component< this.onAddOrRemoveOption(option, event); }} isFocused={isFocused} - title={searchableLabel || label} + title={ + !truncationProps && !option.toolTipContent + ? searchableLabel || label + : undefined + } checked={checked} disabled={disabled} prepend={prepend} diff --git a/packages/eui/src/components/table/table_header_cell.test.tsx b/packages/eui/src/components/table/table_header_cell.test.tsx index a208793c5d93..135a154fd083 100644 --- a/packages/eui/src/components/table/table_header_cell.test.tsx +++ b/packages/eui/src/components/table/table_header_cell.test.tsx @@ -337,6 +337,13 @@ describe('EuiTableHeaderCell', () => { getByTestSubject('icon') ); }); + + it('shows a title attribute with cell text on non-sortable column', () => { + const { getByText } = renderInTableHeader( + Label + ); + expect(getByText('Label')).toHaveAttribute('title', 'Label'); + }); }); describe('sticky', () => { diff --git a/packages/eui/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap b/packages/eui/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap index 710970223e0d..bcba8cbac5d2 100644 --- a/packages/eui/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap +++ b/packages/eui/src/components/table/table_pagination/__snapshots__/table_pagination.test.tsx.snap @@ -53,20 +53,24 @@ exports[`EuiTablePagination renders 1`] = ` > Page 2 of 5 - + +
      @@ -173,20 +177,24 @@ exports[`EuiTablePagination renders 1`] = `
    - + +
    diff --git a/packages/eui/src/components/text_truncate/text_truncate.spec.tsx b/packages/eui/src/components/text_truncate/text_truncate.spec.tsx index 85ded6160732..1b73fecf523a 100644 --- a/packages/eui/src/components/text_truncate/text_truncate.spec.tsx +++ b/packages/eui/src/components/text_truncate/text_truncate.spec.tsx @@ -34,9 +34,9 @@ describe('EuiTextTruncate', () => { getTruncatedText().should('not.exist'); }); - it('renders truncated text and a title when truncation is needed', () => { + it('renders truncated text and a tooltip when truncation is needed', () => { cy.mount(); - cy.get('#text').should('have.attr', 'title', props.text); + cy.get('#text').parent().should('have.class', 'euiToolTipAnchor'); cy.get('#text [data-test-subj="fullText"]').should('have.text', props.text); getTruncatedText().should('exist'); }); @@ -296,12 +296,14 @@ describe('EuiTextTruncate', () => { getTruncatedText('#text1').should('have.text', ''); getTruncatedText('#text2').should('have.text', ''); - cy.get('@spyConsoleError') - .should( - 'be.calledWith', + // The error should be logged at least once per component. The wrapping + // `EuiToolTip` can cause extra renders so we don't assert an exact count. + cy.get('@spyConsoleError').should((spy: any) => { + expect(spy).to.have.been.calledWith( 'The truncation ellipsis is larger than the available width. No text can be rendered.' - ) - .should('be.calledTwice'); + ); + expect(spy.callCount).to.be.at.least(2); + }); }); }); diff --git a/packages/eui/src/components/text_truncate/text_truncate.test.tsx b/packages/eui/src/components/text_truncate/text_truncate.test.tsx index bb2080638ca1..11703ae94871 100644 --- a/packages/eui/src/components/text_truncate/text_truncate.test.tsx +++ b/packages/eui/src/components/text_truncate/text_truncate.test.tsx @@ -18,7 +18,7 @@ jest.mock('./utils', () => ({ })); import { EuiTextTruncate } from './text_truncate'; -import { act } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; describe('EuiTextTruncate', () => { beforeEach(() => jest.clearAllMocks()); @@ -101,12 +101,31 @@ describe('EuiTextTruncate', () => { ); expect(onResize).toHaveBeenCalledWith(0); - expect(container.firstChild).toHaveAttribute( + expect(container.querySelector('[data-resize-observer]')).toHaveAttribute( 'data-resize-observer', 'true' ); }); }); + describe('tooltip', () => { + it('renders a tooltip with the full text when truncating (width=0)', () => { + const { container, queryByRole } = render( + + ); + expect(queryByRole('tooltip')).not.toBeInTheDocument(); + fireEvent.mouseOver(container.querySelector('.euiTextTruncate')!); + expect(queryByRole('tooltip')).toHaveTextContent('Hello world'); + }); + + it('does not render a tooltip when not truncating', () => { + const { container, queryByRole } = render( + + ); + fireEvent.mouseOver(container.querySelector('.euiTextTruncate')!); + expect(queryByRole('tooltip')).not.toBeInTheDocument(); + }); + }); + // We can't unit test the actual truncation logic in JSDOM - see Cypress spec tests instead }); diff --git a/packages/eui/src/components/text_truncate/text_truncate.tsx b/packages/eui/src/components/text_truncate/text_truncate.tsx index 6e6c852dcf69..3f8a59c5ea49 100644 --- a/packages/eui/src/components/text_truncate/text_truncate.tsx +++ b/packages/eui/src/components/text_truncate/text_truncate.tsx @@ -24,6 +24,7 @@ import { EuiResizeObserverProps, } from '../observer/resize_observer'; import type { CommonProps } from '../common'; +import { EuiToolTip } from '../tool_tip'; import { TruncationUtils } from './utils'; import { euiTextTruncateStyles } from './text_truncate.styles'; @@ -216,12 +217,11 @@ const EuiTextTruncateWithWidth: FunctionComponent< const styles = useEuiMemoizedStyles(euiTextTruncateStyles); - return ( + const content = (
    {isTruncating ? ( @@ -249,6 +249,14 @@ const EuiTextTruncateWithWidth: FunctionComponent< )}
    ); + + return isTruncating ? ( + + {content} + + ) : ( + content + ); }; const EuiTextTruncateWithResizeObserver: FunctionComponent< diff --git a/packages/eui/src/components/timeline/__snapshots__/timeline.test.tsx.snap b/packages/eui/src/components/timeline/__snapshots__/timeline.test.tsx.snap index c3b2b6ca8692..54ee80e3df64 100644 --- a/packages/eui/src/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/packages/eui/src/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -14,17 +14,22 @@ exports[`EuiTimeline is rendered with items 1`] = `
    - + +
    - + +