mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): add support for searching in select component (#4638)
Implement search functionality in the select component for better user experience.
This commit is contained in:
parent
d9a469dee3
commit
f2b3f39422
2 changed files with 102 additions and 2 deletions
|
@ -129,3 +129,29 @@
|
|||
font: var(--font-body-2);
|
||||
padding: _.unit(2);
|
||||
}
|
||||
|
||||
.searchInputContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search {
|
||||
margin-right: _.unit(2);
|
||||
color: var(--color-text-secondary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
color: var(--color-text);
|
||||
font: var(--font-body-2);
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import classNames from 'classnames';
|
||||
import type { ReactEventHandler, ReactNode } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Close from '@/assets/icons/close.svg';
|
||||
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
||||
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
|
||||
import SearchIcon from '@/assets/icons/search.svg';
|
||||
import useWindowResize from '@/hooks/use-window-resize';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import Dropdown, { DropdownItem } from '../Dropdown';
|
||||
|
@ -27,6 +30,7 @@ type Props<T> = {
|
|||
placeholder?: ReactNode;
|
||||
isClearable?: boolean;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
isSearchEnabled?: boolean;
|
||||
};
|
||||
|
||||
function Select<T extends string>({
|
||||
|
@ -39,10 +43,21 @@ function Select<T extends string>({
|
|||
placeholder,
|
||||
isClearable,
|
||||
size = 'large',
|
||||
isSearchEnabled,
|
||||
}: Props<T>) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
const [searchInputContainerStyles, setSearchInputContainerStyles] = useState({});
|
||||
const anchorRef = useRef<HTMLInputElement>(null);
|
||||
const current = options.find((option) => value && option.value === value);
|
||||
const filteredOptions = useMemo(() => {
|
||||
return searchInputValue
|
||||
? options.filter(({ value }) =>
|
||||
value.toLocaleLowerCase().includes(searchInputValue.toLocaleLowerCase())
|
||||
)
|
||||
: options;
|
||||
}, [searchInputValue, options]);
|
||||
|
||||
const handleSelect = (value: T) => {
|
||||
onChange?.(value);
|
||||
|
@ -55,6 +70,45 @@ function Select<T extends string>({
|
|||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const getSearchInputContainerStyles = () => {
|
||||
if (!anchorRef.current) {
|
||||
return {};
|
||||
}
|
||||
const element = anchorRef.current;
|
||||
const cs = getComputedStyle(element);
|
||||
const paddingX = Number.parseFloat(cs.paddingLeft) + Number.parseFloat(cs.paddingRight);
|
||||
const paddingY = Number.parseFloat(cs.paddingTop) + Number.parseFloat(cs.paddingBottom);
|
||||
const borderX = Number.parseFloat(cs.borderLeftWidth) + Number.parseFloat(cs.borderRightWidth);
|
||||
const borderY = Number.parseFloat(cs.borderTopWidth) + Number.parseFloat(cs.borderBottomWidth);
|
||||
return {
|
||||
position: 'fixed',
|
||||
width: `${element.offsetWidth - paddingX - borderX}px`,
|
||||
height: `${element.offsetHeight - paddingY - borderY}px`,
|
||||
top: `${
|
||||
element.getBoundingClientRect().top +
|
||||
Number.parseFloat(cs.borderTopWidth) +
|
||||
Number.parseFloat(cs.paddingTop)
|
||||
}px`,
|
||||
left: `${
|
||||
element.getBoundingClientRect().left +
|
||||
Number.parseFloat(cs.borderLeftWidth) +
|
||||
Number.parseFloat(cs.paddingLeft)
|
||||
}px`,
|
||||
backgroundColor: cs.backgroundColor,
|
||||
};
|
||||
};
|
||||
|
||||
useWindowResize(() => {
|
||||
setSearchInputContainerStyles(getSearchInputContainerStyles());
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
anchorRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
setSearchInputContainerStyles(getSearchInputContainerStyles());
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
@ -102,9 +156,29 @@ function Select<T extends string>({
|
|||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
setSearchInputValue('');
|
||||
}}
|
||||
>
|
||||
{options.map(({ value, title }) => (
|
||||
{isSearchEnabled && isOpen && (
|
||||
<div style={searchInputContainerStyles} className={styles.searchInputContainer}>
|
||||
<SearchIcon className={styles.search} />
|
||||
<input
|
||||
ref={(input) => input?.focus()}
|
||||
className={styles.searchInput}
|
||||
value={searchInputValue}
|
||||
role="searchbox"
|
||||
autoComplete="off"
|
||||
placeholder={t('general.type_to_search')}
|
||||
onChange={(event) => {
|
||||
setSearchInputValue(event.target.value);
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filteredOptions.map(({ value, title }) => (
|
||||
<DropdownItem
|
||||
key={value}
|
||||
onClick={() => {
|
||||
|
|
Loading…
Reference in a new issue