diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index dca9d00f4ca29..aa5d42ad2a0d7 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -5,16 +5,17 @@ * 2.0. */ -import React from 'react'; -import { act, render, screen, fireEvent } from '@testing-library/react'; -import { of, BehaviorSubject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import type { ChromeStyle } from '@kbn/core-chrome-browser'; import { applicationServiceMock } from '@kbn/core/public/mocks'; -import { globalSearchPluginMock } from '@kbn/global-search-plugin/public/mocks'; import { GlobalSearchBatchedResults, GlobalSearchResult } from '@kbn/global-search-plugin/public'; -import { SearchBar } from './search_bar'; +import { globalSearchPluginMock } from '@kbn/global-search-plugin/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { TrackUiMetricFn } from '../types'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { BehaviorSubject, of } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import type { TrackUiMetricFn } from '../types'; +import { SearchBar } from './search_bar'; jest.mock( 'react-virtualized-auto-sizer', @@ -87,145 +88,210 @@ describe('SearchBar', () => { expect(await screen.findAllByTestId('nav-search-option')).toHaveLength(list.length); }; - it('correctly filters and sorts results', async () => { - searchService.find - .mockReturnValueOnce( - of( - createBatch('Discover', 'Canvas'), - createBatch({ id: 'Visualize', type: 'test' }, 'Graph') - ) - ) - .mockReturnValueOnce(of(createBatch('Discover', { id: 'My Dashboard', type: 'test' }))); - - render( - - - - ); - - expect(searchService.find).toHaveBeenCalledTimes(0); - - await focusAndUpdate(); - - expect(searchService.find).toHaveBeenCalledTimes(1); - expect(searchService.find).toHaveBeenCalledWith({}, {}); - await assertSearchResults(['Canvas • Kibana', 'Discover • Kibana', 'Graph • Kibana']); - - simulateTypeChar('d'); - - await assertSearchResults(['Discover • Kibana', 'My Dashboard • Test']); - expect(searchService.find).toHaveBeenCalledTimes(2); - expect(searchService.find).toHaveBeenLastCalledWith({ term: 'd' }, {}); - - expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus'); - expect(trackUiMetric).nthCalledWith(2, 'count', 'search_request'); - expect(trackUiMetric).toHaveBeenCalledTimes(2); - }); + describe('chromeStyle: classic', () => { + const chromeStyle$ = of('classic'); - it('supports keyboard shortcuts', async () => { - render( - - - - ); - act(() => { - fireEvent.keyDown(window, { key: '/', ctrlKey: true, metaKey: true }); + it('correctly filters and sorts results', async () => { + searchService.find + .mockReturnValueOnce( + of( + createBatch('Discover', 'Canvas'), + createBatch({ id: 'Visualize', type: 'test' }, 'Graph') + ) + ) + .mockReturnValueOnce(of(createBatch('Discover', { id: 'My Dashboard', type: 'test' }))); + + render( + + + + ); + + expect(searchService.find).toHaveBeenCalledTimes(0); + + await focusAndUpdate(); + + expect(searchService.find).toHaveBeenCalledTimes(1); + expect(searchService.find).toHaveBeenCalledWith({}, {}); + await assertSearchResults(['Canvas • Kibana', 'Discover • Kibana', 'Graph • Kibana']); + + simulateTypeChar('d'); + + await assertSearchResults(['Discover • Kibana', 'My Dashboard • Test']); + expect(searchService.find).toHaveBeenCalledTimes(2); + expect(searchService.find).toHaveBeenLastCalledWith({ term: 'd' }, {}); + + expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus'); + expect(trackUiMetric).nthCalledWith(2, 'count', 'search_request'); + expect(trackUiMetric).toHaveBeenCalledTimes(2); }); - const inputElement = await screen.findByTestId('nav-search-input'); - - expect(document.activeElement).toEqual(inputElement); + it('supports keyboard shortcuts', async () => { + render( + + + + ); + act(() => { + fireEvent.keyDown(window, { key: '/', ctrlKey: true, metaKey: true }); + }); + + const inputElement = await screen.findByTestId('nav-search-input'); + + expect(document.activeElement).toEqual(inputElement); + + expect(trackUiMetric).nthCalledWith(1, 'count', 'shortcut_used'); + expect(trackUiMetric).nthCalledWith(2, 'count', 'search_focus'); + expect(trackUiMetric).toHaveBeenCalledTimes(2); + }); - expect(trackUiMetric).nthCalledWith(1, 'count', 'shortcut_used'); - expect(trackUiMetric).nthCalledWith(2, 'count', 'search_focus'); - expect(trackUiMetric).toHaveBeenCalledTimes(2); - }); + it('only display results from the last search', async () => { + const firstSearchTrigger = new BehaviorSubject(false); + const firstSearch = firstSearchTrigger.pipe( + filter((event) => event), + map(() => { + return createBatch('Discover', 'Canvas'); + }) + ); + const secondSearch = of(createBatch('Visualize', 'Map')); + + searchService.find.mockReturnValueOnce(firstSearch).mockReturnValueOnce(secondSearch); + + render( + + + + ); + + await focusAndUpdate(); + + expect(searchService.find).toHaveBeenCalledTimes(1); + // + simulateTypeChar('d'); + await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']); + + firstSearchTrigger.next(true); + + update(); + + await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']); + }); - it('only display results from the last search', async () => { - const firstSearchTrigger = new BehaviorSubject(false); - const firstSearch = firstSearchTrigger.pipe( - filter((event) => event), - map(() => { - return createBatch('Discover', 'Canvas'); - }) - ); - const secondSearch = of(createBatch('Visualize', 'Map')); - - searchService.find.mockReturnValueOnce(firstSearch).mockReturnValueOnce(secondSearch); - - render( - - - - ); - - await focusAndUpdate(); - - expect(searchService.find).toHaveBeenCalledTimes(1); - // - simulateTypeChar('d'); - await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']); - - firstSearchTrigger.next(true); - - update(); - - await assertSearchResults(['Visualize • Kibana', 'Map • Kibana']); + it('tracks the application navigated to', async () => { + searchService.find.mockReturnValueOnce( + of(createBatch('Discover', { id: 'My Dashboard', type: 'test' })) + ); + + render( + + + + ); + + expect(searchService.find).toHaveBeenCalledTimes(0); + + await focusAndUpdate(); + + expect(searchService.find).toHaveBeenCalledTimes(1); + expect(searchService.find).toHaveBeenCalledWith({}, {}); + await assertSearchResults(['Discover • Kibana']); + + const navSearchOptionToClick = await screen.findByTestId('nav-search-option'); + act(() => { + fireEvent.click(navSearchOptionToClick); + }); + + expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus'); + expect(trackUiMetric).nthCalledWith(2, 'click', [ + 'user_navigated_to_application', + 'user_navigated_to_application_discover', + ]); + expect(trackUiMetric).toHaveBeenCalledTimes(2); + }); }); - it('tracks the application navigated to', async () => { - searchService.find.mockReturnValueOnce( - of(createBatch('Discover', { id: 'My Dashboard', type: 'test' })) - ); - - render( - - - - ); - - expect(searchService.find).toHaveBeenCalledTimes(0); - - await focusAndUpdate(); - - expect(searchService.find).toHaveBeenCalledTimes(1); - expect(searchService.find).toHaveBeenCalledWith({}, {}); - await assertSearchResults(['Discover • Kibana']); - - const navSearchOptionToClick = await screen.findByTestId('nav-search-option'); - act(() => { - fireEvent.click(navSearchOptionToClick); + describe('chromeStyle: project', () => { + const chromeStyle$ = of('project'); + + it('supports keyboard shortcuts', async () => { + render( + + + + ); + + act(() => { + fireEvent.keyDown(window, { key: '/', ctrlKey: true, metaKey: true }); + }); + + const inputElement = await screen.findByTestId('nav-search-input'); + + expect(document.activeElement).toEqual(inputElement); + + fireEvent.click(await screen.findByTestId('nav-search-conceal')); + expect(screen.queryAllByTestId('nav-search-input')).toHaveLength(0); + + expect(trackUiMetric).nthCalledWith(1, 'count', 'shortcut_used'); + expect(trackUiMetric).nthCalledWith(2, 'count', 'search_focus'); + expect(trackUiMetric).toHaveBeenCalledTimes(2); }); - expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus'); - expect(trackUiMetric).nthCalledWith(2, 'click', [ - 'user_navigated_to_application', - 'user_navigated_to_application_discover', - ]); - expect(trackUiMetric).toHaveBeenCalledTimes(2); + it('supports show/hide', async () => { + render( + + + + ); + + fireEvent.click(await screen.findByTestId('nav-search-reveal')); + expect(await screen.findByTestId('nav-search-input')).toBeVisible(); + + fireEvent.click(await screen.findByTestId('nav-search-conceal')); + expect(screen.queryAllByTestId('nav-search-input')).toHaveLength(0); + + expect(trackUiMetric).nthCalledWith(1, 'count', 'search_focus'); + }); }); }); diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index f454359e636df..72659515425c5 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -6,6 +6,7 @@ */ import { + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormLabel, @@ -23,6 +24,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useEvent from 'react-use/lib/useEvent'; import useMountedState from 'react-use/lib/useMountedState'; +import useObservable from 'react-use/lib/useObservable'; import { Subscription } from 'rxjs'; import { blurEvent, CLICK_METRIC, COUNT_METRIC, getClickMetric, isMac, sort } from '.'; import { resultToOption, suggestionToOption } from '../lib'; @@ -51,10 +53,18 @@ export const SearchBar: FC = ({ taggingApi, navigateToUrl, trackUiMetric, + chromeStyle$, ...props }) => { const isMounted = useMountedState(); const { euiTheme } = useEuiTheme(); + const chromeStyle = useObservable(chromeStyle$); + + // These hooks are used when on chromeStyle set to 'project' + const [isVisible, setIsVisible] = useState(false); + const visibilityButtonRef = useRef(null); + + // General hooks const [initialLoad, setInitialLoad] = useState(false); const [searchValue, setSearchValue] = useState(''); const [searchTerm, setSearchTerm] = useState(''); @@ -178,14 +188,16 @@ export const SearchBar: FC = ({ if (event.key === '/' && (isMac ? event.metaKey : event.ctrlKey)) { event.preventDefault(); trackUiMetric(METRIC_TYPE.COUNT, COUNT_METRIC.SHORTCUT_USED); - if (searchRef) { + if (chromeStyle === 'project' && !isVisible) { + visibilityButtonRef.current?.click(); + } else if (searchRef) { searchRef.focus(); } else if (buttonRef) { (buttonRef.children[0] as HTMLButtonElement).click(); } } }, - [buttonRef, searchRef, trackUiMetric] + [chromeStyle, isVisible, buttonRef, searchRef, trackUiMetric] ); const onChange = useCallback( @@ -244,6 +256,49 @@ export const SearchBar: FC = ({ useEvent('keydown', onKeyDown); + if (chromeStyle === 'project' && !isVisible) { + const onShowSearch = () => { + setIsVisible(true); + }; + return ( + + ); + } + + const getAppendForChromeStyle = () => { + if (chromeStyle === 'project') { + return ( + { + setIsVisible(false); + }} + /> + ); + } + + if (showAppend) { + return ( + + {isMac ? '⌘/' : '^/'} + + ); + } + }; + return ( = ({ singleSelection={true} renderOption={(option) => euiSelectableTemplateSitewideRenderOptions(option, searchTerm)} searchProps={{ + autoFocus: chromeStyle === 'project', value: searchValue, onInput: (e: React.UIEvent) => setSearchValue(e.currentTarget.value), 'data-test-subj': 'nav-search-input', @@ -270,14 +326,7 @@ export const SearchBar: FC = ({ setShowAppend(!searchValue.length); }, fullWidth: true, - append: showAppend ? ( - - {isMac ? '⌘/' : '^/'} - - ) : undefined, + append: getAppendForChromeStyle(), }} emptyMessage={} noMatchesMessage={} diff --git a/x-pack/plugins/global_search_bar/public/components/types.ts b/x-pack/plugins/global_search_bar/public/components/types.ts index 88546cda3f80f..968b904e3fa38 100644 --- a/x-pack/plugins/global_search_bar/public/components/types.ts +++ b/x-pack/plugins/global_search_bar/public/components/types.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { ChromeStyle } from '@kbn/core-chrome-browser'; import type { ApplicationStart } from '@kbn/core/public'; import type { GlobalSearchPluginStart } from '@kbn/global-search-plugin/public'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; +import { Observable } from 'rxjs'; import { TrackUiMetricFn } from '../types'; /* @internal */ @@ -18,4 +20,5 @@ export interface SearchBarProps { taggingApi?: SavedObjectTaggingPluginStart; basePathUrl: string; darkMode: boolean; + chromeStyle$: Observable; } diff --git a/x-pack/plugins/global_search_bar/public/plugin.tsx b/x-pack/plugins/global_search_bar/public/plugin.tsx index 239ad78f67c6a..7be3d9227a99b 100644 --- a/x-pack/plugins/global_search_bar/public/plugin.tsx +++ b/x-pack/plugins/global_search_bar/public/plugin.tsx @@ -5,15 +5,14 @@ * 2.0. */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Observable } from 'rxjs'; +import { ChromeNavControl, CoreStart, Plugin } from '@kbn/core/public'; +import { GlobalSearchPluginStart } from '@kbn/global-search-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; -import { ApplicationStart, CoreTheme, CoreStart, Plugin } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { GlobalSearchPluginStart } from '@kbn/global-search-plugin/public'; import { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import React from 'react'; +import ReactDOM from 'react-dom'; import { SearchBar } from './components/search_bar'; import { TrackUiMetricFn } from './types'; @@ -28,69 +27,48 @@ export class GlobalSearchBarPlugin implements Plugin<{}, {}> { return {}; } - public start( - core: CoreStart, - { globalSearch, savedObjectsTagging, usageCollection }: GlobalSearchBarPluginStartDeps - ) { + public start(core: CoreStart, startDeps: GlobalSearchBarPluginStartDeps) { + core.chrome.navControls.registerCenter(this.getNavControl({ core, ...startDeps })); + return {}; + } + + private getNavControl(deps: { core: CoreStart } & GlobalSearchBarPluginStartDeps) { + const { core, globalSearch, savedObjectsTagging, usageCollection } = deps; + const { application, http, theme, uiSettings } = core; + let trackUiMetric: TrackUiMetricFn = () => {}; if (usageCollection) { trackUiMetric = (...args) => { + // track UI Counter metrics usageCollection.reportUiCounter('global_search_bar', ...args); + + // TODO track EBT metrics using core.analytics }; } - core.chrome.navControls.registerCenter({ + const navControl: ChromeNavControl = { order: 1000, - mount: (container) => - this.mount({ - container, - globalSearch, - savedObjectsTagging, - navigateToUrl: core.application.navigateToUrl, - basePathUrl: core.http.basePath.prepend('/plugins/globalSearchBar/assets/'), - darkMode: core.uiSettings.get('theme:darkMode'), - theme$: core.theme.theme$, - trackUiMetric, - }), - }); - return {}; - } - - private mount({ - container, - globalSearch, - savedObjectsTagging, - navigateToUrl, - basePathUrl, - darkMode, - theme$, - trackUiMetric, - }: { - container: HTMLElement; - globalSearch: GlobalSearchPluginStart; - savedObjectsTagging?: SavedObjectTaggingPluginStart; - navigateToUrl: ApplicationStart['navigateToUrl']; - basePathUrl: string; - darkMode: boolean; - theme$: Observable; - trackUiMetric: TrackUiMetricFn; - }) { - ReactDOM.render( - - - - - , - container - ); + mount: (container) => { + ReactDOM.render( + + + + + , + container + ); - return () => ReactDOM.unmountComponentAtNode(container); + return () => ReactDOM.unmountComponentAtNode(container); + }, + }; + return navControl; } } diff --git a/x-pack/plugins/global_search_bar/public/strings.ts b/x-pack/plugins/global_search_bar/public/strings.ts index ce599ff3fefd5..c50d7f5792d88 100644 --- a/x-pack/plugins/global_search_bar/public/strings.ts +++ b/x-pack/plugins/global_search_bar/public/strings.ts @@ -14,6 +14,12 @@ export const i18nStrings = { popoverButton: i18n.translate('xpack.globalSearchBar.searchBar.mobileSearchButtonAriaLabel', { defaultMessage: 'Site-wide search', }), + showSearchAriaText: i18n.translate('xpack.globalSearchBar.searchBar.showSearchAriaText', { + defaultMessage: 'Show search bar', + }), + closeSearchAriaText: i18n.translate('xpack.globalSearchBar.searchBar.closeSearchAriaText', { + defaultMessage: 'Close search bar', + }), keyboardShortcutTooltip: { prefix: i18n.translate('xpack.globalSearchBar.searchBar.shortcutTooltip.description', { defaultMessage: 'Keyboard shortcut', diff --git a/x-pack/plugins/global_search_bar/tsconfig.json b/x-pack/plugins/global_search_bar/tsconfig.json index e8cc744d9a0df..daa4baf21a718 100644 --- a/x-pack/plugins/global_search_bar/tsconfig.json +++ b/x-pack/plugins/global_search_bar/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/kibana-react-plugin", "@kbn/i18n", "@kbn/saved-objects-tagging-oss-plugin", + "@kbn/core-chrome-browser", ], "exclude": [ "target/**/*",