diff --git a/package.json b/package.json index 1f6277adb91..883ebdd158f 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "@emotion/eslint-plugin": "^11.11.0", "@emotion/jest": "^11.11.0", "@emotion/react": "^11.11.0", - "@faker-js/faker": "^7.6.0", + "@faker-js/faker": "^8.0.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", "@storybook/addon-essentials": "^7.3.1", "@storybook/addon-interactions": "^7.3.1", diff --git a/src-docs/src/views/tables/in_memory/in_memory_search.tsx b/src-docs/src/views/tables/in_memory/in_memory_search.tsx index 7d27f0e5c3a..f92b3558736 100644 --- a/src-docs/src/views/tables/in_memory/in_memory_search.tsx +++ b/src-docs/src/views/tables/in_memory/in_memory_search.tsx @@ -11,7 +11,6 @@ import { EuiSpacer, EuiSwitch, EuiFlexGroup, - EuiFlexItem, EuiCallOut, EuiCode, } from '../../../../../src/components'; @@ -27,16 +26,23 @@ type User = { }; const users: User[] = []; +const usersWithSpecialCharacters: User[] = []; for (let i = 0; i < 20; i++) { - users.push({ + const userData = { id: i + 1, - firstName: faker.name.firstName(), - lastName: faker.name.lastName(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), github: faker.internet.userName(), dateOfBirth: faker.date.past(), online: faker.datatype.boolean(), - location: faker.address.country(), + location: faker.location.country(), + }; + users.push(userData); + usersWithSpecialCharacters.push({ + ...userData, + firstName: `${userData.firstName} "${faker.string.symbol(10)}"`, + lastName: `${userData.lastName} ${faker.internet.emoji()}`, }); } @@ -108,6 +114,7 @@ export default () => { const [incremental, setIncremental] = useState(false); const [filters, setFilters] = useState(false); const [contentBetween, setContentBetween] = useState(false); + const [textSearchFormat, setTextSearchFormat] = useState(false); const search: EuiSearchBarProps = { box: { @@ -138,34 +145,34 @@ export default () => { return ( <> - - setIncremental(!incremental)} - /> - - - setFilters(!filters)} - /> - - - setContentBetween(!contentBetween)} - /> - + setIncremental(!incremental)} + /> + setFilters(!filters)} + /> + setContentBetween(!contentBetween)} + /> + setTextSearchFormat(!textSearchFormat)} + /> { expect(tableContent.at(2).text()).toBe('baz'); }); }); + + describe('text search format', () => { + it('allows searching for any text with special characters in it', () => { + const specialCharacterSearch = + '!@#$%^&*(){}+=-_hello:world"`<>?/👋~.,;|\\'; + const items = [ + { title: specialCharacterSearch }, + { title: 'no special characters' }, + ]; + const columns = [{ field: 'title', name: 'Title' }]; + + const { getByTestSubject, container } = render( + + ); + fireEvent.keyUp(getByTestSubject('searchbox'), { + target: { value: specialCharacterSearch }, + }); + + const tableContent = container.querySelectorAll( + '.euiTableRowCell .euiTableCellContent' + ); + expect(tableContent).toHaveLength(1); // only 1 match + expect(tableContent[0]).toHaveTextContent(specialCharacterSearch); + }); + }); }); diff --git a/src/components/basic_table/in_memory_table.tsx b/src/components/basic_table/in_memory_table.tsx index dd606fd58d2..e146bcfe704 100644 --- a/src/components/basic_table/in_memory_table.tsx +++ b/src/components/basic_table/in_memory_table.tsx @@ -23,11 +23,15 @@ import { PropertySort } from '../../services'; import { Pagination as PaginationBarType } from './pagination_bar'; import { isString } from '../../services/predicate'; import { Comparators, Direction } from '../../services/sort'; -import { EuiSearchBar, Query } from '../search_bar'; +import { + EuiSearchBar, + EuiSearchBarProps, + Query, + SchemaType, +} from '../search_bar/search_bar'; +import { EuiSearchBox } from '../search_bar/search_box'; import { EuiSpacer } from '../spacer'; import { CommonProps } from '../common'; -import { EuiSearchBarProps } from '../search_bar/search_bar'; -import { SchemaType } from '../search_bar/search_box'; import { EuiTablePaginationProps, euiTablePaginationDefaults, @@ -76,6 +80,18 @@ type InMemoryTableProps = Omit< * Configures #Search. */ search?: Search; + /** + * By default, tables use `eql` format for search which allows using advanced filters. + * + * However, certain special characters (such as quotes, parentheses, and colons) + * are reserved for EQL syntax and will error if used. + * If your table does not require filter search and instead requires searching for certain + * symbols, use a plain `text` search format instead (note that filters will be ignored + * in this format). + * + * @default "eql" + */ + searchFormat?: 'eql' | 'text'; /** * Configures #Pagination */ @@ -285,6 +301,7 @@ export class EuiInMemoryTable extends Component< static defaultProps = { responsive: true, tableLayout: 'fixed', + searchFormat: 'eql', }; tableRef: React.RefObject; @@ -521,9 +538,34 @@ export class EuiInMemoryTable extends Component< })); }; + // Alternative to onQueryChange - allows consumers to specify they want the + // search bar to ignore EQL syntax and only use the searchbar for plain text + onPlainTextSearch = (searchValue: string) => { + const escapedQueryText = searchValue.replace(/["\\]/g, '\\$&'); + const finalQuery = `"${escapedQueryText}"`; + this.setState({ + query: EuiSearchBar.Query.parse(finalQuery), + }); + }; + renderSearchBar() { - const { search } = this.props; - if (search) { + const { search, searchFormat } = this.props; + if (!search) return; + + let searchBar: ReactNode; + + if (searchFormat === 'text') { + const _searchBoxProps = (search as EuiSearchBarProps)?.box || {}; // Work around | boolean type + const { schema, ...searchBoxProps } = _searchBoxProps; // Destructure `schema` so it doesn't get rendered to DOM + + searchBar = ( + + ); + } else { let searchBarProps: Omit = {}; if (isEuiSearchBarProps(search)) { @@ -538,13 +580,17 @@ export class EuiInMemoryTable extends Component< } } - return ( - <> - - - + searchBar = ( + ); } + + return ( + <> + {searchBar} + + + ); } resolveSearchSchema(): SchemaType { @@ -653,6 +699,7 @@ export class EuiInMemoryTable extends Component< tableLayout, items: _unuseditems, search, + searchFormat, onTableChange, executeQueryOptions, allowNeutralSort, diff --git a/src/components/search_bar/search_bar.tsx b/src/components/search_bar/search_bar.tsx index a91f60c8b54..783440d90f0 100644 --- a/src/components/search_bar/search_bar.tsx +++ b/src/components/search_bar/search_bar.tsx @@ -11,7 +11,7 @@ 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 { EuiSearchBox } from './search_box'; import { EuiSearchBarFilters, SearchFilterConfig } from './search_filters'; import { Query } from './query'; import { CommonProps } from '../common'; @@ -36,6 +36,12 @@ interface ArgsWithError { error: Error; } +export interface SchemaType { + strict?: boolean; + fields?: any; + flags?: string[]; +} + export type EuiSearchBarOnChangeArgs = ArgsWithQuery | ArgsWithError; type HintPopOverProps = Partial< diff --git a/src/components/search_bar/search_box.tsx b/src/components/search_bar/search_box.tsx index 85a78d3a726..403e9840da3 100644 --- a/src/components/search_bar/search_box.tsx +++ b/src/components/search_bar/search_box.tsx @@ -6,21 +6,23 @@ * Side Public License, v 1. */ -import React, { Component } from 'react'; +import React, { FunctionComponent, useRef } from 'react'; + +import { useUpdateEffect } from '../../services'; +import { useEuiI18n } from '../i18n'; import { EuiFieldSearch, EuiFieldSearchProps } from '../form'; import { EuiInputPopover } from '../popover'; -import { EuiSearchBarProps } from './search_bar'; -export interface SchemaType { - strict?: boolean; - fields?: any; - flags?: string[]; -} +import { EuiSearchBarProps } from './search_bar'; export interface EuiSearchBoxProps extends EuiFieldSearchProps { query: string; // This is optional in EuiFieldSearchProps onSearch: (queryText: string) => void; + /** + * @default Search... + */ + placeholder?: string; hint?: { id: string; isVisible: boolean; @@ -28,73 +30,73 @@ export interface EuiSearchBoxProps extends EuiFieldSearchProps { } & EuiSearchBarProps['hint']; } -type DefaultProps = Pick; +export const EuiSearchBox: FunctionComponent = ({ + query, + placeholder, + incremental, + hint, + ...rest +}) => { + const inputRef = useRef(null); -export class EuiSearchBox extends Component { - static defaultProps: DefaultProps = { - placeholder: 'Search...', - incremental: false, - }; - - private inputElement: HTMLInputElement | null = null; - - componentDidUpdate(oldProps: EuiSearchBoxProps) { - if (oldProps.query !== this.props.query && this.inputElement != null) { - this.inputElement.value = this.props.query; - this.inputElement.dispatchEvent(new Event('change')); + useUpdateEffect(() => { + if (inputRef.current) { + inputRef.current.value = query; + inputRef.current.dispatchEvent(new Event('change')); } - } + }, [query]); - render() { - const { query, incremental, hint, ...rest } = this.props; + const defaultPlaceholder = useEuiI18n( + 'euiSearchBox.placeholder', + 'Search...' + ); + const ariaLabelIncremental = useEuiI18n( + 'euiSearchBox.incrementalAriaLabel', + 'This is a search bar. As you type, the results lower in the page will automatically filter.' + ); + const ariaLabelEnter = useEuiI18n( + 'euiSearchBox.ariaLabel', + 'This is a search bar. After typing your query, hit enter to filter the results lower in the page.' + ); - let ariaLabel; - if (incremental) { - ariaLabel = - 'This is a search bar. As you type, the results lower in the page will automatically filter.'; - } else { - ariaLabel = - 'This is a search bar. After typing your query, hit enter to filter the results lower in the page.'; - } + const search = ( + (inputRef.current = input)} + fullWidth + defaultValue={query} + incremental={incremental} + aria-label={incremental ? ariaLabelIncremental : ariaLabelEnter} + placeholder={placeholder ?? defaultPlaceholder} + onFocus={() => { + hint?.setIsVisible(true); + }} + {...rest} + /> + ); - const search = ( - (this.inputElement = input)} + if (hint) { + return ( + { - hint?.setIsVisible(true); + closePopover={() => { + hint.setIsVisible(false); + }} + panelProps={{ + 'aria-live': undefined, + 'aria-modal': undefined, + role: undefined, + tabIndex: -1, + id: hint.id, }} - {...rest} - /> + {...hint.popoverProps} + > + {hint.content} + ); - - 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; } -} + + return search; +}; diff --git a/upcoming_changelogs/7175.md b/upcoming_changelogs/7175.md new file mode 100644 index 00000000000..b570f6c3d93 --- /dev/null +++ b/upcoming_changelogs/7175.md @@ -0,0 +1,5 @@ +- Updated `EuiInMemoryTable` with a new `searchFormat` prop (defaults to `eql`). When setting this prop to `text`, the built-in search bar will ignore EQL syntax and allow searching for plain strings with special characters and symbols. + +**Bug fixes** + +- Fixed missing i18n in `EuiSearchBar`'s default placeholder and aria-label text diff --git a/yarn.lock b/yarn.lock index 6d603d1194b..8ef2aad9d05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2423,10 +2423,10 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.43.0.tgz#559ca3d9ddbd6bf907ad524320a0d14b85586af0" integrity sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg== -"@faker-js/faker@^7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" - integrity sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw== +"@faker-js/faker@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.0.2.tgz#bab698c5d3da9c52744e966e0e3eedb6c8b05c37" + integrity sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A== "@fal-works/esbuild-plugin-global-externals@^2.1.2": version "2.1.2"