fix(core-blocks): CB-1361 combobox logic improvement

This commit is contained in:
Naumov Alexey
2021-10-28 22:21:28 +03:00
parent 62025869d7
commit bb9709cd6f
4 changed files with 98 additions and 73 deletions
@@ -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<any, any> | ObjectProps<any, any, any>) {
rest = filterLayoutFakeProps(rest);
const translate = useTranslate();
const context = useContext(FormContext);
const ref = useRef<HTMLInputElement>(null);
const menu = useMenuState({
@@ -157,6 +151,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
currentId: null,
gutter: 4,
});
const [searchValue, setSearchValue] = useState<string | null>(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<HTMLInputElement>) => {
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<HTMLButtonElement, 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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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))(
<field className={className}>
<field-label title={title} as='label'>{children}{rest.required && ' *'}</field-label>
@@ -242,23 +253,35 @@ export const Combobox: ComboboxType = observer(function Combobox({
disabled={disabled}
readOnly={readOnly}
onChange={handleChange}
onKeyDown={handleKeyDown}
{...rest}
/>
{(selectedItem && !readOnly && searchable) && (
<IconButton type="button" name="reject" viewBox="0 0 11 11" onClick={handleRemove} />
)}
<MenuButton {...menu} disabled={readOnly || disabled}><Icon name="arrow" viewBox="0 0 16 16" {...use({ focus: menu.visible })} /></MenuButton>
<MenuButton {...menu} disabled={readOnly || disabled}>
<Icon name="arrow" viewBox="0 0 16 16" {...use({ focus: menu.visible })} />
</MenuButton>
<Menu
{...menu}
aria-label={propertyName}
unstable_initialFocusRef={ref}
unstable_finalFocusRef={ref}
unstable_initialFocusRef={ref}
>
{filteredItems.map(item => (
<MenuItem key={keySelector(item)} id={keySelector(item)} type='button' {...menu} onClick={handleMenuSelect}>
{valueSelector(item)}
{!filteredItems.length ? (
<MenuItem id='placeholder' disabled {...menu}>
{translate('combobox_no_options_placeholder')}
</MenuItem>
))}
) : (
filteredItems.map(item => (
<MenuItem
key={keySelector(item)}
id={keySelector(item)}
type='button'
{...menu}
onClick={event => handleSelect(event.currentTarget.id)}
>
{valueSelector(item)}
</MenuItem>
))
)}
</Menu>
</input-box>
</field>
@@ -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'],
];
@@ -2,4 +2,5 @@ export default [
['block_properties_table_name', 'Название'],
['block_properties_table_value', 'Значение'],
['block_properties_table_add', 'Добавить параметр'],
['combobox_no_options_placeholder', 'Нет совпадений'],
];
@@ -115,7 +115,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
const booleanProviderProperties = driver.data?.providerProperties?.slice().filter(property => property.dataType === 'Boolean');
return styled(useStyles(styles, BASE_CONTAINERS_STYLES))(
<SubmittingForm ref={formRef} onChange={handleFormChange} onSubmit={save}>
<SubmittingForm ref={formRef} onChange={handleFormChange}>
<ColoredContainer wrap overflow parent gap>
<Container medium gap>
<Group form gap>