From b49e9d58f17abfa9d2824a0a0347cb61ff9b014a Mon Sep 17 00:00:00 2001 From: Guillaume Peoc'h Date: Wed, 11 May 2022 14:28:30 +0200 Subject: [PATCH] SONAR-16340 Replace keymaster in Search --- .../main/js/app/components/search/Search.tsx | 51 +++++++++++-------- .../search/__tests__/Search-test.tsx | 23 ++++++--- .../sonar-web/src/main/js/helpers/keycodes.ts | 8 ++- .../src/main/js/helpers/testUtils.ts | 9 +++- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/search/Search.tsx index 4ddad49b88e..47549c7d890 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.tsx +++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import key from 'keymaster'; import { debounce, keyBy, uniqBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -29,7 +28,7 @@ import SearchBox from '../../../components/controls/SearchBox'; import ClockIcon from '../../../components/icons/ClockIcon'; import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { KeyboardCodes } from '../../../helpers/keycodes'; +import { KeyboardKeys } from '../../../helpers/keycodes'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; import { getComponentOverviewUrl } from '../../../helpers/urls'; @@ -77,11 +76,7 @@ export class Search extends React.PureComponent { componentDidMount() { this.mounted = true; - key('s', () => { - this.focusInput(); - this.openSearch(); - return false; - }); + document.addEventListener('keydown', this.handleSKeyDown); } componentDidUpdate(_prevProps: WithRouterProps, prevState: State) { @@ -92,7 +87,7 @@ export class Search extends React.PureComponent { componentWillUnmount() { this.mounted = false; - key.unbind('s'); + document.removeEventListener('keydown', this.handleSKeyDown); } focusInput = () => { @@ -227,9 +222,8 @@ export class Search extends React.PureComponent { const list = this.getPlainComponentsList(results, more); const index = list.indexOf(selected); return index > 0 ? { selected: list[index - 1] } : null; - } else { - return null; } + return null; }); }; @@ -239,9 +233,8 @@ export class Search extends React.PureComponent { const list = this.getPlainComponentsList(results, more); const index = list.indexOf(selected); return index >= 0 && index < list.length - 1 ? { selected: list[index + 1] } : null; - } else { - return null; } + return null; }); }; @@ -278,22 +271,38 @@ export class Search extends React.PureComponent { } }; + handleSKeyDown = (event: KeyboardEvent) => { + const { tagName } = event.target as HTMLElement; + const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'; + if (event.key === KeyboardKeys.KeyS && !isInput) { + event.preventDefault(); + this.focusInput(); + this.openSearch(); + } + }; + handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.nativeEvent.code) { - case KeyboardCodes.Enter: + switch (event.nativeEvent.key) { + case KeyboardKeys.Enter: event.preventDefault(); + event.nativeEvent.stopImmediatePropagation(); this.openSelected(); - return; - case KeyboardCodes.UpArrow: + break; + case KeyboardKeys.UpArrow: event.preventDefault(); + event.nativeEvent.stopImmediatePropagation(); this.selectPrevious(); - return; - case KeyboardCodes.DownArrow: + break; + case KeyboardKeys.Escape: + event.preventDefault(); + event.nativeEvent.stopImmediatePropagation(); + this.closeSearch(); + break; + case KeyboardKeys.DownArrow: event.preventDefault(); + event.nativeEvent.stopImmediatePropagation(); this.selectNext(); - // keep this return to prevent fall-through in case more cases will be adder later - // eslint-disable-next-line no-useless-return - return; + break; } }; diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx index 77f6e7b2fbd..39d4e1d3895 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx @@ -19,9 +19,9 @@ */ import { shallow, ShallowWrapper } from 'enzyme'; import * as React from 'react'; -import { KeyboardCodes } from '../../../../helpers/keycodes'; +import { KeyboardKeys } from '../../../../helpers/keycodes'; import { mockRouter } from '../../../../helpers/testMocks'; -import { elementKeydown } from '../../../../helpers/testUtils'; +import { elementKeydown, keydown } from '../../../../helpers/testUtils'; import { ComponentQualifier } from '../../../../types/component'; import { Search } from '../Search'; @@ -57,7 +57,7 @@ it('opens selected project on enter', () => { selected: selectedKey }); - elementKeydown(form.find('SearchBox'), KeyboardCodes.Enter); + elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter); expect(router.push).toBeCalledWith({ pathname: '/dashboard', query: { id: selectedKey } }); }); @@ -73,7 +73,7 @@ it('opens selected portfolio on enter', () => { selected: selectedKey }); - elementKeydown(form.find('SearchBox'), KeyboardCodes.Enter); + elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter); expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); }); @@ -89,7 +89,7 @@ it('opens selected subportfolio on enter', () => { selected: selectedKey }); - elementKeydown(form.find('SearchBox'), KeyboardCodes.Enter); + elementKeydown(form.find('SearchBox'), KeyboardKeys.Enter); expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } }); }); @@ -101,6 +101,15 @@ it('shows warning about short input', () => { expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); }); +it('should open the results when pressing key S and close it when pressing Escape', () => { + const router = mockRouter(); + const form = shallowRender({ router }); + keydown({ key: KeyboardKeys.KeyS }); + expect(form.state().open).toBe(true); + elementKeydown(form.find('SearchBox'), KeyboardKeys.Escape); + expect(form.state().open).toBe(false); +}); + function shallowRender(props: Partial = {}) { return shallow( // @ts-ignore @@ -113,12 +122,12 @@ function component(key: string, qualifier = ComponentQualifier.Project) { } function next(form: ShallowWrapper, expected: string) { - elementKeydown(form.find('SearchBox'), KeyboardCodes.DownArrow); + elementKeydown(form.find('SearchBox'), KeyboardKeys.DownArrow); expect(form.state().selected).toBe(expected); } function prev(form: ShallowWrapper, expected: string) { - elementKeydown(form.find('SearchBox'), KeyboardCodes.UpArrow); + elementKeydown(form.find('SearchBox'), KeyboardKeys.UpArrow); expect(form.state().selected).toBe(expected); } diff --git a/server/sonar-web/src/main/js/helpers/keycodes.ts b/server/sonar-web/src/main/js/helpers/keycodes.ts index eb3229aebd2..04c3b908fac 100644 --- a/server/sonar-web/src/main/js/helpers/keycodes.ts +++ b/server/sonar-web/src/main/js/helpers/keycodes.ts @@ -37,6 +37,11 @@ export enum KeyboardCodes { } export enum KeyboardKeys { + Escape = 'Escape', + UpArrow = 'ArrowUp', + DownArrow = 'ArrowDown', + Enter = 'Enter', + Space = ' ', Alt = 'Alt', KeyF = 'f', KeyA = 'a', @@ -44,6 +49,5 @@ export enum KeyboardKeys { KeyI = 'i', KeyC = 'c', KeyT = 't', - Space = ' ', - Escape = 'Escape' + KeyS = 's' } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index e43030383a4..1b1abba7dbe 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -87,10 +87,15 @@ export function keydown(args: { code?: KeyboardCodes; key?: KeyboardKeys }): voi document.dispatchEvent(event); } -export function elementKeydown(element: ShallowWrapper, code: KeyboardCodes): void { +export function elementKeydown(element: ShallowWrapper, key: KeyboardKeys): void { const event = { currentTarget: { element }, - nativeEvent: { code }, + nativeEvent: { + key, + stopImmediatePropagation: () => { + /* noop */ + } + }, preventDefault() { /*noop*/ } -- 2.39.5