Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Euisearchbar] Add option to render hint #6319

Merged
merged 10 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: {
sebelga marked this conversation as resolved.
Show resolved Hide resolved
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
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');
});
});
});
48 changes: 46 additions & 2 deletions src/components/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

Expand Down Expand Up @@ -77,6 +96,14 @@ export interface EuiSearchBarProps extends CommonProps {
* Date formatter to use when parsing date values
*/
dateFormat?: object;

/**
* Hint to render below the search bar
*/
hint?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hint needs to have its tabindex removed or set to "-1". Right now it has a tabindex of "0" that allows it to take keyboard focus, but because we're using a portal, it's not a sibling element to the input it represents. This situation could be confusing for users who navigate the screen with sight and assistive technology together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this something that should be handled globally by the <EuiInputPopover /> component for every consumer who want to display a popover underneath an <input /> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into it, the z-index is set to 2000. Setting it to -1 is not possible as it hides the popover. If this needs to be changed globally for all consumers of the EuiInputPopover I'd prefer it to be a different PR as I don't know the impact if could have.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I was looking at tabindex in the HTML structure. I agree z-index needs to remain where it is to ensure the popover appears and is above other block elements in the stacking order. A tabindex of -1 ensures the hint cannot take keyboard focus.

content: React.ReactNode;
popOverProps?: HintPopOverProps;
};
}

const parseQuery = (
Expand All @@ -99,11 +126,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<State, 'query'> & { query: Query | null };
type StateWithOptionalQuery = Omit<State, 'query' | 'isHintVisible'> & {
query: Query | null;
};
sebelga marked this conversation as resolved.
Show resolved Hide resolved

export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
static Query = Query;
Expand All @@ -115,6 +145,7 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
query,
queryText: query.text,
error: null,
isHintVisible: false,
};
}

Expand All @@ -135,6 +166,7 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
query,
queryText: query.text,
error: null,
isHintVisible: prevState.isHintVisible,
};
}
return null;
Expand Down Expand Up @@ -204,12 +236,13 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
}

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);
Expand All @@ -236,6 +269,17 @@ export class EuiSearchBar extends Component<EuiSearchBarProps, State> {
onSearch={this.onSearch}
sebelga marked this conversation as resolved.
Show resolved Hide resolved
isInvalid={error != null}
title={error ? error.message : undefined}
hint={
hint
? {
isVisible: isHintVisible,
sebelga marked this conversation as resolved.
Show resolved Hide resolved
setIsVisible: (isVisible: boolean) => {
this.setState({ isHintVisible: isVisible });
},
...hint,
}
: undefined
}
/>
</EuiFlexItem>
{filtersBar}
Expand Down
37 changes: 35 additions & 2 deletions src/components/search_bar/search_box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<EuiSearchBoxProps, 'placeholder' | 'incremental'>;
Expand All @@ -39,7 +45,7 @@ export class EuiSearchBox extends Component<EuiSearchBoxProps> {
}

render() {
const { query, incremental, ...rest } = this.props;
const { query, incremental, hint, ...rest } = this.props;

let ariaLabel;
if (incremental) {
Expand All @@ -50,15 +56,42 @@ export class EuiSearchBox extends Component<EuiSearchBoxProps> {
'This is a search bar. After typing your query, hit enter to filter the results lower in the page.';
}

return (
const search = (
<EuiFieldSearch
inputRef={(input) => (this.inputElement = input)}
fullWidth
defaultValue={query}
incremental={incremental}
aria-label={ariaLabel}
onFocus={() => {
hint?.setIsVisible(true);
}}
{...rest}
/>
);

if (hint) {
return (
<EuiInputPopover
disableFocusTrap
input={search}
isOpen={hint.isVisible}
fullWidth
closePopover={() => {
hint.setIsVisible(false);
}}
panelProps={{
'aria-live': undefined,
'aria-modal': false,
sebelga marked this conversation as resolved.
Show resolved Hide resolved
role: undefined,
}}
sebelga marked this conversation as resolved.
Show resolved Hide resolved
{...hint.popOverProps}
>
{hint.content}
</EuiInputPopover>
);
}

return search;
}
}
Loading