From 7f3cdfd4e12d81d6dd461e652737f9f61246604c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Oct 2022 16:07:51 +0200 Subject: [PATCH 01/10] Add hint prop to render below the search box --- src/components/search_bar/search_bar.tsx | 43 ++++++++++++++++++++++-- src/components/search_bar/search_box.tsx | 37 ++++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index 0b93e45483b..dc33da9c734 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -14,6 +14,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'; @@ -77,6 +78,27 @@ 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?: Pick< + EuiInputPopoverProps, + | 'isOpen' + | 'closePopover' + | 'fullWidth' + | 'disableFocusTrap' + | 'panelClassName' + | 'panelPaddingSize' + | 'panelStyle' + | 'panelProps' + | 'popoverScreenReaderText' + | 'repositionOnScroll' + | 'zIndex' + >; + }; } const parseQuery = ( @@ -99,11 +121,14 @@ 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 StateWithOptionalQuery = Omit & { + query: Query | null; +}; export class EuiSearchBar extends Component { static Query = Query; @@ -115,6 +140,7 @@ export class EuiSearchBar extends Component { query, queryText: query.text, error: null, + isHintVisible: false, }; } @@ -135,6 +161,7 @@ export class EuiSearchBar extends Component { query, queryText: query.text, error: null, + isHintVisible: prevState.isHintVisible, }; } return null; @@ -204,12 +231,13 @@ export class EuiSearchBar extends Component { } render() { - const { query, queryText, error } = this.state; + const { query, queryText, error, isHintVisible } = 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); @@ -236,6 +264,17 @@ export class EuiSearchBar extends Component { onSearch={this.onSearch} isInvalid={error != null} title={error ? error.message : undefined} + hint={ + hint + ? { + isVisible: isHintVisible, + setIsVisible: (isVisible: boolean) => { + this.setState({ isHintVisible: isVisible }); + }, + ...hint, + } + : undefined + } /> {filtersBar} diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index 2c0ea14f77c..eacfea19876 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,10 @@ export interface EuiSearchBoxProps extends EuiFieldSearchProps { query: string; // This is optional in EuiFieldSearchProps onSearch: (queryText: string) => void; + hint?: { + isVisible: boolean; + setIsVisible: (isVisible: boolean) => void; + } & EuiSearchBarProps['hint']; } type DefaultProps = Pick; @@ -39,7 +45,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 +56,42 @@ 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': false, + role: undefined, + }} + {...hint.popOverProps} + > + {hint.content} + + ); + } + + return search; } } From dd536377548b314003a068afb96e71617a219655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Oct 2022 16:08:24 +0200 Subject: [PATCH 02/10] Update example to show hint --- src-docs/src/views/search_bar/search_bar.js | 34 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src-docs/src/views/search_bar/search_bar.js b/src-docs/src/views/search_bar/search_bar.js index 1aff47dd97c..b8b5572edc8 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} From ea44af2fec4828f223dd4896812b11b7c9c7207d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 19 Oct 2022 16:19:31 +0200 Subject: [PATCH 03/10] Update props info --- src-docs/src/views/search_bar/props_info.js | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) 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' }, + }, + }, + }, + }, }; From d33119532d6d937bb8a86dd2d8197ab1463706dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 20 Oct 2022 09:19:00 +0200 Subject: [PATCH 04/10] Add unit tests --- src/components/search_bar/search_bar.test.tsx | 81 ++++++++++++++++++- src/components/search_bar/search_bar.tsx | 33 ++++---- 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/src/components/search_bar/search_bar.test.tsx b/src/components/search_bar/search_bar.test.tsx index 12656467f23..ea85a0b8c52 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 dc33da9c734..24e62faef4e 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -36,6 +36,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; @@ -84,20 +102,7 @@ export interface EuiSearchBarProps extends CommonProps { */ hint?: { content: React.ReactNode; - popOverProps?: Pick< - EuiInputPopoverProps, - | 'isOpen' - | 'closePopover' - | 'fullWidth' - | 'disableFocusTrap' - | 'panelClassName' - | 'panelPaddingSize' - | 'panelStyle' - | 'panelProps' - | 'popoverScreenReaderText' - | 'repositionOnScroll' - | 'zIndex' - >; + popOverProps?: HintPopOverProps; }; } From c1aa5610274a2d77cc5bba0847b306d3412311b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 20 Oct 2022 09:27:11 +0200 Subject: [PATCH 05/10] Add release note --- upcoming_changelogs/6319.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 upcoming_changelogs/6319.md 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. From b060ca91733abe5afe9009bd5ee91ad3f1ff3fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 20 Oct 2022 16:46:36 +0200 Subject: [PATCH 06/10] Update jest snapshot --- .../search_bar/__snapshots__/search_box.test.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) 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..." /> From 4a653d7ce8441d46fe6bc99a48b87336942864be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 25 Oct 2022 12:04:01 +0100 Subject: [PATCH 07/10] Rename popoverProps --- src-docs/src/views/search_bar/search_bar.js | 2 +- src/components/search_bar/search_bar.test.tsx | 2 +- src/components/search_bar/search_bar.tsx | 2 +- src/components/search_bar/search_box.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-docs/src/views/search_bar/search_bar.js b/src-docs/src/views/search_bar/search_bar.js index b8b5572edc8..c586d45b537 100644 --- a/src-docs/src/views/search_bar/search_bar.js +++ b/src-docs/src/views/search_bar/search_bar.js @@ -190,7 +190,7 @@ export const SearchBar = () => { -dashboard ), - popOverProps: { panelStyle: { backgroundColor: '#f7f8fc' } }, + popoverProps: { panelStyle: { backgroundColor: '#f7f8fc' } }, } : undefined } diff --git a/src/components/search_bar/search_bar.test.tsx b/src/components/search_bar/search_bar.test.tsx index ea85a0b8c52..0bc04ca33e1 100644 --- a/src/components/search_bar/search_bar.test.tsx +++ b/src/components/search_bar/search_bar.test.tsx @@ -145,7 +145,7 @@ describe('SearchBar', () => { box={{ 'data-test-subj': 'searchbar' }} hint={{ content: Hello from hint, - popOverProps: { + popoverProps: { isOpen: isHintVisible, }, }} diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index 24e62faef4e..38fd71601ca 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -102,7 +102,7 @@ export interface EuiSearchBarProps extends CommonProps { */ hint?: { content: React.ReactNode; - popOverProps?: HintPopOverProps; + popoverProps?: HintPopOverProps; }; } diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index eacfea19876..d5377855ebc 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -85,7 +85,7 @@ export class EuiSearchBox extends Component { 'aria-modal': false, role: undefined, }} - {...hint.popOverProps} + {...hint.popoverProps} > {hint.content} From 3ba98f0597c1af5877f360b7208b5fc698736dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 25 Oct 2022 12:04:37 +0100 Subject: [PATCH 08/10] Add aria-describedby to hint --- src/components/search_bar/search_bar.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index 38fd71601ca..d6b1885ff31 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'; @@ -51,6 +53,7 @@ type HintPopOverProps = Partial< | 'repositionOnScroll' | 'zIndex' | 'data-test-subj' + | 'id' > >; @@ -137,6 +140,7 @@ type StateWithOptionalQuery = Omit & { export class EuiSearchBar extends Component { static Query = Query; + hintId = htmlIdGenerator('__hint')(); constructor(props: EuiSearchBarProps) { super(props); @@ -236,7 +240,12 @@ export class EuiSearchBar extends Component { } render() { - const { query, queryText, error, isHintVisible } = 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, @@ -259,6 +268,11 @@ export class EuiSearchBar extends Component { const toolsRightEl = this.renderTools(toolsRight); + const popoverProps: HintPopOverProps | undefined = hint + ? { id: this.hintId, ...hint.popoverProps } + : undefined; + const isHintVisible = hint?.popoverProps?.isOpen ?? isHintVisibleState; + return ( {toolsLeftEl} @@ -269,6 +283,7 @@ export class EuiSearchBar extends Component { onSearch={this.onSearch} isInvalid={error != null} title={error ? error.message : undefined} + aria-describedby={isHintVisible ? `${this.hintId}` : undefined} hint={ hint ? { @@ -277,6 +292,7 @@ export class EuiSearchBar extends Component { this.setState({ isHintVisible: isVisible }); }, ...hint, + popoverProps, } : undefined } From 7fa168b8c8e253ff1c1f9cd885587ad16819daae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 27 Oct 2022 11:23:31 +0100 Subject: [PATCH 09/10] Address CR feedback --- src/components/popover/popover.tsx | 3 ++- src/components/search_bar/search_bar.tsx | 6 +----- src/components/search_bar/search_box.tsx | 3 +++ 3 files changed, 6 insertions(+), 6 deletions(-) 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/search_bar.tsx b/src/components/search_bar/search_bar.tsx index d6b1885ff31..72c9adc77c7 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -53,7 +53,6 @@ type HintPopOverProps = Partial< | 'repositionOnScroll' | 'zIndex' | 'data-test-subj' - | 'id' > >; @@ -268,9 +267,6 @@ export class EuiSearchBar extends Component { const toolsRightEl = this.renderTools(toolsRight); - const popoverProps: HintPopOverProps | undefined = hint - ? { id: this.hintId, ...hint.popoverProps } - : undefined; const isHintVisible = hint?.popoverProps?.isOpen ?? isHintVisibleState; return ( @@ -291,8 +287,8 @@ export class EuiSearchBar extends Component { setIsVisible: (isVisible: boolean) => { this.setState({ isHintVisible: isVisible }); }, + id: this.hintId, ...hint, - popoverProps, } : undefined } diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index d5377855ebc..671913664c7 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -22,6 +22,7 @@ export interface EuiSearchBoxProps extends EuiFieldSearchProps { // This is optional in EuiFieldSearchProps onSearch: (queryText: string) => void; hint?: { + id: string; isVisible: boolean; setIsVisible: (isVisible: boolean) => void; } & EuiSearchBarProps['hint']; @@ -84,6 +85,8 @@ export class EuiSearchBox extends Component { 'aria-live': undefined, 'aria-modal': false, role: undefined, + tabIndex: -1, + id: hint.id, }} {...hint.popoverProps} > From 12058181003ef376a47e136fa660464b8d32bf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Fri, 28 Oct 2022 13:57:33 +0100 Subject: [PATCH 10/10] Address CR feedback --- src/components/search_bar/search_bar.tsx | 8 +++----- src/components/search_bar/search_box.tsx | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index 72c9adc77c7..bab5a1e8d49 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -131,10 +131,8 @@ interface State { 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 { @@ -175,7 +173,7 @@ export class EuiSearchBar extends Component { return null; } - notifyControllingParent(newState: StateWithOptionalQuery) { + notifyControllingParent(newState: NotifyControllingParent) { const { onChange } = this.props; if (!onChange) { return; diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index 671913664c7..85a78d3a726 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -83,7 +83,7 @@ export class EuiSearchBox extends Component { }} panelProps={{ 'aria-live': undefined, - 'aria-modal': false, + 'aria-modal': undefined, role: undefined, tabIndex: -1, id: hint.id,