= ({
{...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`] = `
+
`;
@@ -48,35 +53,40 @@ exports[`SearchBar render - no config, no query 1`] = `
+
`;
@@ -88,50 +98,55 @@ exports[`SearchBar render - provided query, filters 1`] = `
+
+
{
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
-
+
+
-
+
+
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`] = `
@@ -88,17 +93,22 @@ exports[`EuiTimeline props gutterSize l is rendered 1`] = `
@@ -162,17 +172,22 @@ exports[`EuiTimeline props gutterSize m is rendered 1`] = `
@@ -236,17 +251,22 @@ exports[`EuiTimeline props gutterSize xl is rendered 1`] = `
diff --git a/packages/eui/src/components/timeline/__snapshots__/timeline_item.test.tsx.snap b/packages/eui/src/components/timeline/__snapshots__/timeline_item.test.tsx.snap
index c3ff6894238c..964faced6dbc 100644
--- a/packages/eui/src/components/timeline/__snapshots__/timeline_item.test.tsx.snap
+++ b/packages/eui/src/components/timeline/__snapshots__/timeline_item.test.tsx.snap
@@ -14,9 +14,9 @@ exports[`EuiTimelineItem is rendered 1`] = `
aria-label=""
class="euiAvatar euiAvatar--m euiAvatar--user emotion-euiAvatar-user-m-uppercase-subdued"
role="img"
- title=""
>
@@ -43,17 +43,22 @@ exports[`EuiTimelineItem props EuiAvatar is passed as an icon 1`] = `
@@ -146,9 +156,9 @@ exports[`EuiTimelineItem props verticalAlign top is rendered 1`] = `
aria-label=""
class="euiAvatar euiAvatar--m euiAvatar--user emotion-euiAvatar-user-m-uppercase-subdued"
role="img"
- title=""
>
diff --git a/packages/eui/src/components/tool_tip/tool_tip.styles.ts b/packages/eui/src/components/tool_tip/tool_tip.styles.ts
index 685b2638a776..7919058cb187 100644
--- a/packages/eui/src/components/tool_tip/tool_tip.styles.ts
+++ b/packages/eui/src/components/tool_tip/tool_tip.styles.ts
@@ -102,4 +102,7 @@ export const euiToolTipAnchorStyles = () => ({
inlineBlock: css`
display: inline-block;
`,
+ flex: css`
+ display: flex;
+ `,
});
diff --git a/packages/eui/src/components/tool_tip/tool_tip.tsx b/packages/eui/src/components/tool_tip/tool_tip.tsx
index 3ff6820a02cd..c3be612773f5 100644
--- a/packages/eui/src/components/tool_tip/tool_tip.tsx
+++ b/packages/eui/src/components/tool_tip/tool_tip.tsx
@@ -36,7 +36,7 @@ import { EuiToolTipArrow } from './tool_tip_arrow';
import { toolTipManager } from './tool_tip_manager';
export const POSITIONS = ['top', 'right', 'bottom', 'left'] as const;
-const DISPLAYS = ['inlineBlock', 'block'] as const;
+const DISPLAYS = ['inlineBlock', 'block', 'flex'] as const;
export const DEFAULT_TOOLTIP_OFFSET = 16;
diff --git a/packages/website/docs/components/display/avatar.mdx b/packages/website/docs/components/display/avatar.mdx
index a379c924b77d..d66a706ea396 100644
--- a/packages/website/docs/components/display/avatar.mdx
+++ b/packages/website/docs/components/display/avatar.mdx
@@ -6,6 +6,10 @@ keywords: [EuiAvatar]
The **EuiAvatar** component typically creates a user icon. It will accept `name` (required) and `image` props and will configure the display and accessibility as needed. By default, the background colors come from the set of colors used for visualizations. Otherwise you can pass a hex value to the `color` prop.
+:::accessibility Accessibility note
+The `name` prop provides a meaningful label for both assistive technology (`aria-label`) and sighted users (tooltip content). The avatar itself is not interactive and is not a keyboard focus stop.
+:::
+
## Component
```tsx interactive