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..12ae76778af6e 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', @@ -52,6 +53,7 @@ describe('SearchBar', () => { const basePathUrl = '/plugins/globalSearchBar/assets/'; const darkMode = false; + const chromeStyle$ = new BehaviorSubject('classic'); beforeEach(() => { applications = applicationServiceMock.createStartContract(); @@ -104,6 +106,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + chromeStyle$={chromeStyle$} trackUiMetric={trackUiMetric} /> @@ -136,6 +139,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + chromeStyle$={chromeStyle$} trackUiMetric={trackUiMetric} /> @@ -172,6 +176,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + chromeStyle$={chromeStyle$} trackUiMetric={trackUiMetric} /> @@ -203,6 +208,7 @@ describe('SearchBar', () => { navigateToUrl={applications.navigateToUrl} basePathUrl={basePathUrl} darkMode={darkMode} + chromeStyle$={chromeStyle$} trackUiMetric={trackUiMetric} /> @@ -229,3 +235,5 @@ describe('SearchBar', () => { expect(trackUiMetric).toHaveBeenCalledTimes(2); }); }); + +// FIXME: add tests for chromeStyle === 'project' 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..74dd7384ef651 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, @@ -22,7 +23,9 @@ import type { GlobalSearchFindParams, GlobalSearchResult } from '@kbn/global-sea import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import useEvent from 'react-use/lib/useEvent'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; 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'; @@ -46,15 +49,25 @@ const EmptyMessage = () => ( ); +const GLOBAL_SEARCH_BAR_VISIBLE_KEY = 'GLOBAL_SEARCH_BAR_VISIBLE' as const; + export const SearchBar: FC = ({ globalSearch, 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] = useLocalStorage(GLOBAL_SEARCH_BAR_VISIBLE_KEY, false); + const visibilityButtonRef = useRef(null); + + // General hooks const [initialLoad, setInitialLoad] = useState(false); const [searchValue, setSearchValue] = useState(''); const [searchTerm, setSearchTerm] = useState(''); @@ -178,14 +191,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 +259,47 @@ 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 ( = ({ value: searchValue, onInput: (e: React.UIEvent) => setSearchValue(e.currentTarget.value), 'data-test-subj': 'nav-search-input', - inputRef: setSearchRef, + inputRef: (input) => { + setSearchRef(input); + if (chromeStyle === 'project' && input) { + // while the input ref is in flight, we set focus on it + // to autofocus the input when it appears + input.focus(); + } + }, compressed: true, 'aria-label': i18nStrings.placeholderText, placeholder: i18nStrings.placeholderText, @@ -270,14 +333,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/**/*",