From bb9709cd6f30130b2cb0cb8b43048e00945ec63a Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 28 Oct 2021 22:21:28 +0300 Subject: [PATCH] fix(core-blocks): CB-1361 combobox logic improvement --- .../core-blocks/src/FormControls/Combobox.tsx | 167 ++++++++++-------- webapp/packages/core-blocks/src/locales/en.ts | 1 + webapp/packages/core-blocks/src/locales/ru.ts | 1 + .../src/ConnectionForm/Options/Options.tsx | 2 +- 4 files changed, 98 insertions(+), 73 deletions(-) diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 73a03c2818..79e3f01b91 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -7,23 +7,16 @@ */ import { observer } from 'mobx-react-lite'; -import { - useLayoutEffect, useCallback, useState, useRef, useContext, useEffect -} from 'react'; -import { - useMenuState, - Menu, - MenuItem, - MenuButton -} from 'reakit/Menu'; +import { useLayoutEffect, useCallback, useState, useRef, useContext, useEffect } from 'react'; +import { useMenuState, Menu, MenuItem, MenuButton } from 'reakit/Menu'; import styled, { css, use } from 'reshadow'; +import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import { filterLayoutFakeProps } from '../Containers/filterLayoutFakeProps'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; import { Icon } from '../Icon'; -import { IconButton } from '../IconButton'; import { baseFormControlStyles } from './baseFormControlStyles'; import { FormContext } from './FormContext'; @@ -56,7 +49,7 @@ const styles = composes( outline: none; padding: 4px; cursor: pointer; - &:hover { + &:hover, &:focus { opacity: 0.7; } } @@ -150,6 +143,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ ...rest }: ControlledProps | ObjectProps) { rest = filterLayoutFakeProps(rest); + const translate = useTranslate(); const context = useContext(FormContext); const ref = useRef(null); const menu = useMenuState({ @@ -157,6 +151,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ currentId: null, gutter: 4, }); + const [searchValue, setSearchValue] = useState(null); let value: string | number | readonly string[] | undefined = controlledValue ?? defaultValue ?? undefined; @@ -172,62 +167,78 @@ export const Combobox: ComboboxType = observer(function Combobox({ inputValue = searchValue; } - const handleChange = useCallback( - (event: React.ChangeEvent) => { - const value = event.target.value; - onChange(value, name); - setSearchValue(value); - }, - [name, onChange] - ); - - const handleRemove = useCallback( - () => { - menu.hide(); - if (state) { - state[name] = null; - } - if (onSelect) { - onSelect(null, name, value); - } - if (context) { - context.change(null, name); - } - setSearchValue(null); - }, - [value, state, name, menu, context, onSelect] - ); - - const handleMenuSelect = useCallback( - (event: React.MouseEvent) => { - menu.hide(); - const id = event.currentTarget.id; - if (state) { - state[name] = id; - } - if (onSelect) { - onSelect(id, name, value); - } - if (context) { - context.change(id, name); - } - setSearchValue(null); - }, - [value, state, name, menu, context, onSelect] - ); - - useEffect(() => { - if (ref.current === document.activeElement && inputValue === searchValue) { - menu.show(); - } - }); - - useLayoutEffect(() => onSwitch?.(menu.visible), [onSwitch, menu.visible]); - const filteredItems = items.filter( item => !searchValue || valueSelector(item).toUpperCase().includes(searchValue.toUpperCase()) ); + const handleChange = useCallback((event: React.ChangeEvent) => { + const value = event.target.value; + onChange(value, name); + setSearchValue(value); + }, [name, onChange]); + + const handleSelect = useCallback((id: any) => { + id = id ?? value ?? ''; + + menu.hide(); + if (state) { + state[name] = id; + } + if (onSelect) { + onSelect(id, name, value); + } + if (context) { + context.change(id, name); + } + setSearchValue(null); + }, [value, state, name, menu, context, onSelect]); + + const matchItems = useCallback((input?: boolean) => { + if (!searchValue) { + return; + } + + if (!filteredItems.length) { + setSearchValue(null); + return; + } + + if (filteredItems.length === 1) { + handleSelect(keySelector(filteredItems[0])); + return; + } + + if (filteredItems.length > 0) { + if (input) { + handleSelect(keySelector(filteredItems[0])); + } else { + setSearchValue(null); + } + } + }, [filteredItems, keySelector, handleSelect, searchValue]); + + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + matchItems(true); + } + }, [matchItems]); + + useEffect(() => { + if (ref.current === document.activeElement) { + if (inputValue === searchValue) { + menu.show(); + } + } else { + if (!menu.visible) { + matchItems(); + } + } + }, [inputValue, searchValue, matchItems, menu]); + + useLayoutEffect(() => { + onSwitch?.(menu.visible); + }, [onSwitch, menu.visible]); + return styled(useStyles(baseFormControlStyles, styles))( {children}{rest.required && ' *'} @@ -242,23 +253,35 @@ export const Combobox: ComboboxType = observer(function Combobox({ disabled={disabled} readOnly={readOnly} onChange={handleChange} + onKeyDown={handleKeyDown} {...rest} /> - {(selectedItem && !readOnly && searchable) && ( - - )} - + + + - {filteredItems.map(item => ( - - {valueSelector(item)} + {!filteredItems.length ? ( + + {translate('combobox_no_options_placeholder')} - ))} + ) : ( + filteredItems.map(item => ( + handleSelect(event.currentTarget.id)} + > + {valueSelector(item)} + + )) + )} diff --git a/webapp/packages/core-blocks/src/locales/en.ts b/webapp/packages/core-blocks/src/locales/en.ts index 14008230a4..fb141e339a 100644 --- a/webapp/packages/core-blocks/src/locales/en.ts +++ b/webapp/packages/core-blocks/src/locales/en.ts @@ -2,4 +2,5 @@ export default [ ['block_properties_table_name', 'Name'], ['block_properties_table_value', 'Value'], ['block_properties_table_add', 'Add property'], + ['combobox_no_options_placeholder', 'No options'], ]; diff --git a/webapp/packages/core-blocks/src/locales/ru.ts b/webapp/packages/core-blocks/src/locales/ru.ts index 05545077ed..7381b53bd2 100644 --- a/webapp/packages/core-blocks/src/locales/ru.ts +++ b/webapp/packages/core-blocks/src/locales/ru.ts @@ -2,4 +2,5 @@ export default [ ['block_properties_table_name', 'Название'], ['block_properties_table_value', 'Значение'], ['block_properties_table_add', 'Добавить параметр'], + ['combobox_no_options_placeholder', 'Нет совпадений'], ]; diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx index d7ffd627af..8e044efb82 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx @@ -115,7 +115,7 @@ export const Options: TabContainerPanelComponent = observe const booleanProviderProperties = driver.data?.providerProperties?.slice().filter(property => property.dataType === 'Boolean'); return styled(useStyles(styles, BASE_CONTAINERS_STYLES))( - +