diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2023-12-06 11:06:12 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-12-06 20:02:46 +0000 |
commit | bde5b0da90c05ef4128287a6062490046217cd2e (patch) | |
tree | 6e429bb18991955161c8d95c25e09f98644606a2 /server/sonar-web/design-system/src/components | |
parent | 567ff7d183d5fb025abcad808fcd3cb5e8d5bb2e (diff) | |
download | sonarqube-bde5b0da90c05ef4128287a6062490046217cd2e.tar.gz sonarqube-bde5b0da90c05ef4128287a6062490046217cd2e.zip |
SONAR-21029 Fix SearchSelectDropdown search input
Diffstat (limited to 'server/sonar-web/design-system/src/components')
6 files changed, 37 insertions, 192 deletions
diff --git a/server/sonar-web/design-system/src/components/input/InputSearch.tsx b/server/sonar-web/design-system/src/components/input/InputSearch.tsx index 879b12165cb..c219332300e 100644 --- a/server/sonar-web/design-system/src/components/input/InputSearch.tsx +++ b/server/sonar-web/design-system/src/components/input/InputSearch.tsx @@ -39,6 +39,7 @@ interface Props { className?: string; id?: string; innerRef?: React.RefCallback<HTMLInputElement>; + inputId?: string; loading?: boolean; maxLength?: number; minLength?: number; @@ -55,24 +56,27 @@ interface Props { const DEFAULT_MAX_LENGTH = 100; -export function InputSearch({ - autoFocus, - id, - className, - innerRef, - onBlur, - onChange, - onFocus, - onKeyDown, - onMouseDown, - placeholder, - loading, - minLength, - maxLength = DEFAULT_MAX_LENGTH, - size = 'medium', - value: parentValue, - searchInputAriaLabel, -}: PropsWithChildren<Props>) { +export function InputSearch(props: PropsWithChildren<Props>) { + const { + autoFocus, + id, + className, + innerRef, + inputId, + onBlur, + onChange, + onFocus, + onKeyDown, + onMouseDown, + placeholder, + loading, + minLength, + maxLength = DEFAULT_MAX_LENGTH, + size = 'medium', + value: parentValue, + searchInputAriaLabel, + } = props; + const intl = useIntl(); const input = useRef<null | HTMLElement>(null); const [value, setValue] = useState(parentValue ?? ''); @@ -151,6 +155,7 @@ export function InputSearch({ aria-label={searchInputAriaLabel ?? placeholder} autoComplete="off" className={inputClassName} + id={inputId} maxLength={maxLength} onBlur={onBlur} onChange={handleInputChange} diff --git a/server/sonar-web/design-system/src/components/input/SearchSelect.tsx b/server/sonar-web/design-system/src/components/input/SearchSelect.tsx index 8f3ccd78d7a..951b607d53d 100644 --- a/server/sonar-web/design-system/src/components/input/SearchSelect.tsx +++ b/server/sonar-web/design-system/src/components/input/SearchSelect.tsx @@ -20,13 +20,13 @@ import classNames from 'classnames'; import { omit } from 'lodash'; import React, { RefObject } from 'react'; -import { GroupBase, InputProps, components } from 'react-select'; +import { GroupBase, InputProps } from 'react-select'; import AsyncSelect, { AsyncProps } from 'react-select/async'; import Select from 'react-select/dist/declarations/src/Select'; import { INPUT_SIZES } from '../../helpers'; import { Key } from '../../helpers/keyboard'; +import { InputSearch } from './InputSearch'; import { LabelValueSelectOption, SelectProps, selectStyle } from './InputSelect'; -import { SearchSelectControlledInput } from './SearchSelectControlledInput'; type SearchSelectProps< V, @@ -98,26 +98,24 @@ export function SearchSelectInput< const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const target = event.target as HTMLInputElement; - if (event.key === Key.Escape && target.value !== '') { + if (event.key === Key.Escape.toString() && target.value !== '') { event.stopPropagation(); onChange(''); } }; return ( - <SearchSelectControlledInput + <InputSearch + {...omit(props, 'value', 'aria-label', 'id')} + autoFocus + inputId={props.id} loading={isLoading && inputValue.length >= (minLength ?? 0)} minLength={minLength} onChange={onChange} + onKeyDown={handleKeyDown} + placeholder={placeholder as string} + searchInputAriaLabel={props['aria-label']} size="full" - value={inputValue} - > - <components.Input - {...props} - onKeyDown={handleKeyDown} - placeholder={placeholder as string} - style={{}} - /> - </SearchSelectControlledInput> + /> ); } diff --git a/server/sonar-web/design-system/src/components/input/SearchSelectControlledInput.tsx b/server/sonar-web/design-system/src/components/input/SearchSelectControlledInput.tsx deleted file mode 100644 index f737ce04fe7..00000000000 --- a/server/sonar-web/design-system/src/components/input/SearchSelectControlledInput.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import React, { PropsWithChildren } from 'react'; -import { useIntl } from 'react-intl'; -import { INPUT_SIZES } from '../../helpers/constants'; -import { isDefined } from '../../helpers/types'; -import { InputSizeKeys } from '../../types/theme'; -import { Spinner } from '../Spinner'; -import { CloseIcon } from '../icons/CloseIcon'; -import { - InputSearchWrapper, - StyledInputWrapper, - StyledInteractiveIcon, - StyledNote, - StyledSearchIcon, -} from './InputSearch'; - -interface Props { - className?: string; - id?: string; - loading?: boolean; - minLength?: number; - onChange: (value: string) => void; - onMouseDown?: React.MouseEventHandler<HTMLInputElement>; - size?: InputSizeKeys; - value: string; -} - -export function SearchSelectControlledInput({ - id, - className, - onChange, - onMouseDown, - loading, - minLength, - size = 'medium', - value, - children, -}: PropsWithChildren<Props>) { - const intl = useIntl(); - const tooShort = isDefined(minLength) && value.length > 0 && value.length < minLength; - - return ( - <InputSearchWrapper - className={className} - id={id} - onMouseDown={onMouseDown} - style={{ '--inputSize': INPUT_SIZES[size] }} - title={ - tooShort && isDefined(minLength) - ? intl.formatMessage({ id: 'select2.tooShort' }, { 0: minLength }) - : '' - } - > - <StyledInputWrapper className="sw-flex sw-items-center"> - {children} - <Spinner className="sw-z-normal" loading={loading ?? false}> - <StyledSearchIcon /> - </Spinner> - {value !== '' && ( - <StyledInteractiveIcon - Icon={CloseIcon} - aria-label={intl.formatMessage({ id: 'clear' })} - className="it__search-box-clear" - onClick={() => { - onChange(''); - }} - size="small" - /> - )} - - {tooShort && isDefined(minLength) && ( - <StyledNote className="sw-ml-1" role="note"> - {intl.formatMessage({ id: 'select2.tooShort' }, { 0: minLength })} - </StyledNote> - )} - </StyledInputWrapper> - </InputSearchWrapper> - ); -} - -SearchSelectControlledInput.displayName = 'SearchSelectControlledInput'; // so that tests don't see the obfuscated production name diff --git a/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx b/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx index 355b232f9a3..44f9fb6bae8 100644 --- a/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx +++ b/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx @@ -169,7 +169,6 @@ export function SearchSelectDropdown< Option: IconOption, ...rest.components, }} - inputValue={inputValue} loadOptions={debouncedLoadOptions.current} menuIsOpen minLength={minLength} @@ -177,7 +176,7 @@ export function SearchSelectDropdown< onInputChange={handleInputChange} placeholder={placeholder} selectRef={ref} - value={value} + size="large" /> </StyledSearchSelectWrapper> </SearchHighlighterContext.Provider> diff --git a/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectControlledInput-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectControlledInput-test.tsx deleted file mode 100644 index 289dd96b1e3..00000000000 --- a/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectControlledInput-test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { screen } from '@testing-library/react'; -import { renderWithContext } from '../../../helpers/testUtils'; -import { FCProps } from '../../../types/misc'; -import { InputSearch } from '../InputSearch'; -import { SearchSelectControlledInput } from '../SearchSelectControlledInput'; - -it('should work properly when input is passed as a children', async () => { - const onChange = jest.fn(); - const { user } = setupWithProps({ - onChange, - value: 'foo', - children: <input onChange={onChange} />, - }); - await user.click(screen.getByLabelText('clear')); - expect(onChange).toHaveBeenCalledWith(''); -}); - -it('should warn when input is too short', () => { - setupWithProps({ - value: 'f', - children: <input />, - }); - expect(screen.getByRole('note')).toBeInTheDocument(); -}); - -function setupWithProps(props: Partial<FCProps<typeof InputSearch>> = {}) { - return renderWithContext( - <SearchSelectControlledInput - maxLength={150} - minLength={2} - onChange={jest.fn()} - placeholder="placeholder" - searchInputAriaLabel="" - value="foo" - {...props} - />, - ); -} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx index e0ae78bf93e..b2e845e831a 100644 --- a/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx +++ b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx @@ -44,7 +44,7 @@ it('should render select input and be able to search and select an option', asyn await user.click(screen.getByRole('combobox')); expect(screen.getByText('label1')).toBeInTheDocument(); expect(screen.getByText('different')).toBeInTheDocument(); - await user.type(screen.getByRole('combobox', { name: 'label' }), 'label'); + await user.type(screen.getByRole('searchbox', { name: 'label' }), 'label'); expect(await screen.findByText('label')).toBeInTheDocument(); expect(screen.queryByText('different')).not.toBeInTheDocument(); await user.click(screen.getByText('label')); @@ -60,7 +60,7 @@ it('should handle key navigation', async () => { renderSearchSelectDropdown(); await user.tab(); await user.keyboard('{Enter}'); - await user.type(screen.getByRole('combobox', { name: 'label' }), 'label'); + await user.type(screen.getByRole('searchbox', { name: 'label' }), 'label'); expect(await screen.findByText('label')).toBeInTheDocument(); expect(screen.queryByText('different')).not.toBeInTheDocument(); await user.keyboard('{Escape}'); |