Skip to content

Commit

Permalink
[Euisearchbar] Add option to render hint (elastic#6319)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored and chandlerprall committed Nov 1, 2022
1 parent 3883a47 commit 3733f71
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 13 deletions.
23 changes: 23 additions & 0 deletions src-docs/src/views/search_bar/props_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
},
},
},
},
Expand Down Expand Up @@ -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' },
},
},
},
},
};
34 changes: 30 additions & 4 deletions src-docs/src/views/search_bar/search_bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -75,7 +76,11 @@ export const SearchBar = () => {
};

const toggleIncremental = () => {
setIncremental(!incremental);
setIncremental((prev) => !prev);
};

const toggleHint = () => {
setShowHint((prev) => !prev);
};

const renderSearch = () => {
Expand Down Expand Up @@ -176,6 +181,19 @@ export const SearchBar = () => {
}}
filters={filters}
onChange={onChange}
hint={
showHint
? {
content: (
<span>
Type search terms, e.g. <strong>visualization</strong> or{' '}
<strong>-dashboard</strong>
</span>
),
popoverProps: { panelStyle: { backgroundColor: '#f7f8fc' } },
}
: undefined
}
/>
);
};
Expand Down Expand Up @@ -292,16 +310,24 @@ export const SearchBar = () => {

return (
<Fragment>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>{renderSearch()}</EuiFlexItem>

{renderSearch()}
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiSwitch
label="Incremental"
checked={incremental}
onChange={toggleIncremental}
/>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiSwitch
label="Show hint"
checked={showHint}
onChange={toggleHint}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
{content}
Expand Down
3 changes: 2 additions & 1 deletion src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,10 @@ export class EuiPopover extends Component<Props, State> {
container,
focusTrapProps,
initialFocus: initialFocusProp,
tabIndex: tabIndexProp,
tabIndex: _tabIndexProp,
...rest
} = this.props;
const tabIndexProp = panelProps?.tabIndex ?? _tabIndexProp;

const styles = euiPopoverStyles();
const popoverStyles = [styles.euiPopover, { display }];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`EuiSearchBox render - custom placeholder and incremental 1`] = `
inputRef={[Function]}
isClearable={true}
isLoading={false}
onFocus={[Function]}
onSearch={[Function]}
placeholder="..."
/>
Expand All @@ -29,6 +30,7 @@ exports[`EuiSearchBox render - no config 1`] = `
inputRef={[Function]}
isClearable={true}
isLoading={false}
onFocus={[Function]}
onSearch={[Function]}
placeholder="Search..."
/>
Expand Down
81 changes: 80 additions & 1 deletion src/components/search_bar/search_bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
<EuiSearchBar
query="status:active"
box={{ 'data-test-subj': 'searchbar' }}
hint={{
content: <span data-test-subj="myHint">Hello from hint</span>,
}}
/>
);

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 (
<>
<EuiSearchBar
box={{ 'data-test-subj': 'searchbar' }}
hint={{
content: <span data-test-subj="myHint">Hello from hint</span>,
popoverProps: {
isOpen: isHintVisible,
},
}}
/>
<button
data-test-subj="showHintBtn"
onClick={() => setIsHintVisible(true)}
>
Show hint
</button>
</>
);
};

const component = mount(<TestComp />);
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');
});
});
});
64 changes: 59 additions & 5 deletions src/components/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
*/

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';
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';

Expand All @@ -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;

Expand Down Expand Up @@ -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 = (
Expand All @@ -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<State, 'query'> & { query: Query | null };
type NotifyControllingParent = Pick<State, 'queryText' | 'error'> & {
query: Query | null; // `state.query` is never null, but can be passed as `null` when an error is present
};

export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
static Query = Query;
hintId = htmlIdGenerator('__hint')();

constructor(props: EuiSearchBarProps) {
super(props);
Expand All @@ -115,6 +146,7 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
query,
queryText: query.text,
error: null,
isHintVisible: false,
};
}

Expand All @@ -135,12 +167,13 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
query,
queryText: query.text,
error: null,
isHintVisible: prevState.isHintVisible,
};
}
return null;
}

notifyControllingParent(newState: StateWithOptionalQuery) {
notifyControllingParent(newState: NotifyControllingParent) {
const { onChange } = this.props;
if (!onChange) {
return;
Expand Down Expand Up @@ -204,12 +237,18 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
}

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);
Expand All @@ -226,6 +265,8 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {

const toolsRightEl = this.renderTools(toolsRight);

const isHintVisible = hint?.popoverProps?.isOpen ?? isHintVisibleState;

return (
<EuiFlexGroup gutterSize="m" alignItems="center" wrap>
{toolsLeftEl}
Expand All @@ -236,6 +277,19 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
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
}
/>
</EuiFlexItem>
{filtersBar}
Expand Down
Loading

0 comments on commit 3733f71

Please sign in to comment.