mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
fix(core-blocks): CB-1361 combobox logic improvement
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user