diff --git a/src-docs/src/views/search_bar/props_info.js b/src-docs/src/views/search_bar/props_info.js
index 9b685d8b5eb..06b8a927e2d 100644
--- a/src-docs/src/views/search_bar/props_info.js
+++ b/src-docs/src/views/search_bar/props_info.js
@@ -36,6 +36,11 @@ export const propsInfo = {
required: false,
type: { name: '#SearchFilters[]' },
},
+ hint: {
+ description: 'Renders a hint below the search bar',
+ required: false,
+ type: { name: '#Hint' },
+ },
},
},
},
@@ -504,4 +509,22 @@ export const propsInfo = {
},
},
},
+
+ Hint: {
+ __docgenInfo: {
+ _euiObjectType: 'type',
+ props: {
+ content: {
+ description: 'The hint content to render',
+ required: true,
+ type: { name: 'React.ReactNode' },
+ },
+ popOverProps: {
+ description: 'Optional configuration for the hint popover.',
+ required: false,
+ type: { name: 'EuiInputPopoverProps' },
+ },
+ },
+ },
+ },
};
diff --git a/src-docs/src/views/search_bar/search_bar.js b/src-docs/src/views/search_bar/search_bar.js
index 1aff47dd97c..c586d45b537 100644
--- a/src-docs/src/views/search_bar/search_bar.js
+++ b/src-docs/src/views/search_bar/search_bar.js
@@ -64,6 +64,7 @@ export const SearchBar = () => {
const [query, setQuery] = useState(initialQuery);
const [error, setError] = useState(null);
const [incremental, setIncremental] = useState(false);
+ const [showHint, setShowHint] = useState(false);
const onChange = ({ query, error }) => {
if (error) {
@@ -75,7 +76,11 @@ export const SearchBar = () => {
};
const toggleIncremental = () => {
- setIncremental(!incremental);
+ setIncremental((prev) => !prev);
+ };
+
+ const toggleHint = () => {
+ setShowHint((prev) => !prev);
};
const renderSearch = () => {
@@ -176,6 +181,19 @@ export const SearchBar = () => {
}}
filters={filters}
onChange={onChange}
+ hint={
+ showHint
+ ? {
+ content: (
+
+ Type search terms, e.g. visualization or{' '}
+ -dashboard
+
+ ),
+ popoverProps: { panelStyle: { backgroundColor: '#f7f8fc' } },
+ }
+ : undefined
+ }
/>
);
};
@@ -292,9 +310,9 @@ export const SearchBar = () => {
return (
-
- {renderSearch()}
-
+ {renderSearch()}
+
+
{
onChange={toggleIncremental}
/>
+
+
+
+
{content}
diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx
index df8d99d8a42..6f6f8685c04 100644
--- a/src/components/popover/popover.tsx
+++ b/src/components/popover/popover.tsx
@@ -625,9 +625,10 @@ export class EuiPopover extends Component {
container,
focusTrapProps,
initialFocus: initialFocusProp,
- tabIndex: tabIndexProp,
+ tabIndex: _tabIndexProp,
...rest
} = this.props;
+ const tabIndexProp = panelProps?.tabIndex ?? _tabIndexProp;
const styles = euiPopoverStyles();
const popoverStyles = [styles.euiPopover, { display }];
diff --git a/src/components/search_bar/__snapshots__/search_box.test.tsx.snap b/src/components/search_bar/__snapshots__/search_box.test.tsx.snap
index 7b0248a3764..778ef5654f0 100644
--- a/src/components/search_bar/__snapshots__/search_box.test.tsx.snap
+++ b/src/components/search_bar/__snapshots__/search_box.test.tsx.snap
@@ -12,6 +12,7 @@ exports[`EuiSearchBox render - custom placeholder and incremental 1`] = `
inputRef={[Function]}
isClearable={true}
isLoading={false}
+ onFocus={[Function]}
onSearch={[Function]}
placeholder="..."
/>
@@ -29,6 +30,7 @@ exports[`EuiSearchBox render - no config 1`] = `
inputRef={[Function]}
isClearable={true}
isLoading={false}
+ onFocus={[Function]}
onSearch={[Function]}
placeholder="Search..."
/>
diff --git a/src/components/search_bar/search_bar.test.tsx b/src/components/search_bar/search_bar.test.tsx
index 12656467f23..0bc04ca33e1 100644
--- a/src/components/search_bar/search_bar.test.tsx
+++ b/src/components/search_bar/search_bar.test.tsx
@@ -7,7 +7,9 @@
*/
/* eslint-disable react/no-multi-comp */
-import React from 'react';
+import React, { useState } from 'react';
+import { act } from 'react-dom/test-utils';
+
import { requiredProps } from '../../test';
import { mount, shallow } from 'enzyme';
import { EuiSearchBar } from './search_bar';
@@ -105,4 +107,81 @@ describe('SearchBar', () => {
expect(queryText).toBe('status:inactive');
});
});
+
+ describe('hint', () => {
+ test('renders a hint below the search bar on focus', () => {
+ const component = mount(
+ Hello from hint,
+ }}
+ />
+ );
+
+ const getHint = () => component.find('[data-test-subj="myHint"]');
+
+ let hint = getHint();
+ expect(hint.length).toBe(0);
+
+ act(() => {
+ component.find('input[data-test-subj="searchbar"]').simulate('focus');
+ });
+ component.update();
+
+ hint = getHint();
+ expect(hint.length).toBe(1);
+ expect(hint.text()).toBe('Hello from hint');
+ });
+
+ test('control the visibility of the hint', () => {
+ const TestComp = () => {
+ const [isHintVisible, setIsHintVisible] = useState(false);
+
+ return (
+ <>
+ Hello from hint,
+ popoverProps: {
+ isOpen: isHintVisible,
+ },
+ }}
+ />
+
+ >
+ );
+ };
+
+ const component = mount();
+ const getHint = () => component.find('[data-test-subj="myHint"]');
+
+ let hint = getHint();
+ expect(hint.length).toBe(0);
+
+ act(() => {
+ component.find('input[data-test-subj="searchbar"]').simulate('focus');
+ });
+ component.update();
+
+ hint = getHint();
+ expect(hint.length).toBe(0); // Not visible on focus as it is controlled
+
+ act(() => {
+ component.find('[data-test-subj="showHintBtn"]').simulate('click');
+ });
+ component.update();
+
+ hint = getHint();
+ expect(hint.length).toBe(1);
+ expect(hint.text()).toBe('Hello from hint');
+ });
+ });
});
diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx
index 0b93e45483b..bab5a1e8d49 100644
--- a/src/components/search_bar/search_bar.tsx
+++ b/src/components/search_bar/search_bar.tsx
@@ -7,6 +7,8 @@
*/
import React, { Component, ReactElement } from 'react';
+
+import { htmlIdGenerator } from '../../services/accessibility';
import { isString } from '../../services/predicate';
import { EuiFlexGroup, EuiFlexItem } from '../flex';
import { EuiSearchBox, SchemaType } from './search_box';
@@ -14,6 +16,7 @@ import { EuiSearchFilters, SearchFilterConfig } from './search_filters';
import { Query } from './query';
import { CommonProps } from '../common';
import { EuiFieldSearchProps } from '../form/field_search';
+import { EuiInputPopoverProps } from '../popover';
export { Query, AST as Ast } from './query';
@@ -35,6 +38,24 @@ interface ArgsWithError {
export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError;
+type HintPopOverProps = Partial<
+ Pick<
+ EuiInputPopoverProps,
+ | 'isOpen'
+ | 'closePopover'
+ | 'fullWidth'
+ | 'disableFocusTrap'
+ | 'panelClassName'
+ | 'panelPaddingSize'
+ | 'panelStyle'
+ | 'panelProps'
+ | 'popoverScreenReaderText'
+ | 'repositionOnScroll'
+ | 'zIndex'
+ | 'data-test-subj'
+ >
+>;
+
export interface EuiSearchBarProps extends CommonProps {
onChange?: (args: EuiSearchBarOnChangeArgs) => void | boolean;
@@ -77,6 +98,14 @@ export interface EuiSearchBarProps extends CommonProps {
* Date formatter to use when parsing date values
*/
dateFormat?: object;
+
+ /**
+ * Hint to render below the search bar
+ */
+ hint?: {
+ content: React.ReactNode;
+ popoverProps?: HintPopOverProps;
+ };
}
const parseQuery = (
@@ -99,14 +128,16 @@ interface State {
query: Query;
queryText: string;
error: null | Error;
+ isHintVisible: boolean;
}
-// `state.query` is never null, but can be passed as `null` to `notifyControllingParent`
-// when `error` is not null.
-type StateWithOptionalQuery = Omit & { query: Query | null };
+type NotifyControllingParent = Pick & {
+ query: Query | null; // `state.query` is never null, but can be passed as `null` when an error is present
+};
export class EuiSearchBar extends Component {
static Query = Query;
+ hintId = htmlIdGenerator('__hint')();
constructor(props: EuiSearchBarProps) {
super(props);
@@ -115,6 +146,7 @@ export class EuiSearchBar extends Component {
query,
queryText: query.text,
error: null,
+ isHintVisible: false,
};
}
@@ -135,12 +167,13 @@ export class EuiSearchBar extends Component {
query,
queryText: query.text,
error: null,
+ isHintVisible: prevState.isHintVisible,
};
}
return null;
}
- notifyControllingParent(newState: StateWithOptionalQuery) {
+ notifyControllingParent(newState: NotifyControllingParent) {
const { onChange } = this.props;
if (!onChange) {
return;
@@ -204,12 +237,18 @@ export class EuiSearchBar extends Component {
}
render() {
- const { query, queryText, error } = this.state;
+ const {
+ query,
+ queryText,
+ error,
+ isHintVisible: isHintVisibleState,
+ } = this.state;
const {
box: { schema, ...box } = { schema: '' }, // strip `schema` out to prevent passing it to EuiSearchBox
filters,
toolsLeft,
toolsRight,
+ hint,
} = this.props;
const toolsLeftEl = this.renderTools(toolsLeft);
@@ -226,6 +265,8 @@ export class EuiSearchBar extends Component {
const toolsRightEl = this.renderTools(toolsRight);
+ const isHintVisible = hint?.popoverProps?.isOpen ?? isHintVisibleState;
+
return (
{toolsLeftEl}
@@ -236,6 +277,19 @@ export class EuiSearchBar extends Component {
onSearch={this.onSearch}
isInvalid={error != null}
title={error ? error.message : undefined}
+ aria-describedby={isHintVisible ? `${this.hintId}` : undefined}
+ hint={
+ hint
+ ? {
+ isVisible: isHintVisible,
+ setIsVisible: (isVisible: boolean) => {
+ this.setState({ isHintVisible: isVisible });
+ },
+ id: this.hintId,
+ ...hint,
+ }
+ : undefined
+ }
/>
{filtersBar}
diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx
index 2c0ea14f77c..85a78d3a726 100644
--- a/src/components/search_bar/search_box.tsx
+++ b/src/components/search_bar/search_box.tsx
@@ -8,6 +8,8 @@
import React, { Component } from 'react';
import { EuiFieldSearch, EuiFieldSearchProps } from '../form';
+import { EuiInputPopover } from '../popover';
+import { EuiSearchBarProps } from './search_bar';
export interface SchemaType {
strict?: boolean;
@@ -19,6 +21,11 @@ export interface EuiSearchBoxProps extends EuiFieldSearchProps {
query: string;
// This is optional in EuiFieldSearchProps
onSearch: (queryText: string) => void;
+ hint?: {
+ id: string;
+ isVisible: boolean;
+ setIsVisible: (isVisible: boolean) => void;
+ } & EuiSearchBarProps['hint'];
}
type DefaultProps = Pick;
@@ -39,7 +46,7 @@ export class EuiSearchBox extends Component {
}
render() {
- const { query, incremental, ...rest } = this.props;
+ const { query, incremental, hint, ...rest } = this.props;
let ariaLabel;
if (incremental) {
@@ -50,15 +57,44 @@ export class EuiSearchBox extends Component {
'This is a search bar. After typing your query, hit enter to filter the results lower in the page.';
}
- return (
+ const search = (
(this.inputElement = input)}
fullWidth
defaultValue={query}
incremental={incremental}
aria-label={ariaLabel}
+ onFocus={() => {
+ hint?.setIsVisible(true);
+ }}
{...rest}
/>
);
+
+ if (hint) {
+ return (
+ {
+ hint.setIsVisible(false);
+ }}
+ panelProps={{
+ 'aria-live': undefined,
+ 'aria-modal': undefined,
+ role: undefined,
+ tabIndex: -1,
+ id: hint.id,
+ }}
+ {...hint.popoverProps}
+ >
+ {hint.content}
+
+ );
+ }
+
+ return search;
}
}
diff --git a/upcoming_changelogs/6319.md b/upcoming_changelogs/6319.md
new file mode 100644
index 00000000000..22529d88a38
--- /dev/null
+++ b/upcoming_changelogs/6319.md
@@ -0,0 +1 @@
+- Added the `hint` prop to the ``. This prop lets the consumer render a hint below the search bar that will be displayed on focus.