chore: refactor style combobox (#209)
* refactor: refactor combobox based on designs * style: combobox with search separated
This commit is contained in:
parent
09e50adefc
commit
361855946a
|
|
@ -3,13 +3,7 @@ import {
|
|||
ComboboxInputProps as ComboboxLibInputProps,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import React, {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { forwardRef, Fragment, useEffect, useState } from 'react';
|
||||
|
||||
import { Icon, IconName } from '@/components/core/icon';
|
||||
import { Flex } from '@/components/layout';
|
||||
|
|
@ -18,54 +12,28 @@ import { useDebounce } from '@/hooks/use-debounce';
|
|||
import { Separator } from '../separator.styles';
|
||||
import { cleanString } from './combobox.utils';
|
||||
|
||||
type ComboboxInputProps = {
|
||||
/**
|
||||
* If it's true, the list of options will be displayed
|
||||
*/
|
||||
open: boolean;
|
||||
/**
|
||||
* Name of the left icon to display in the input
|
||||
*/
|
||||
leftIcon: IconName;
|
||||
/**
|
||||
* Value to indicate it's invalid
|
||||
*/
|
||||
error?: boolean;
|
||||
} & ComboboxLibInputProps<'input', ComboboxItem>;
|
||||
type ComboboxInputProps = ComboboxLibInputProps<'input', ComboboxItem>;
|
||||
|
||||
const ComboboxInput: React.FC<ComboboxInputProps> = ({
|
||||
open,
|
||||
leftIcon,
|
||||
error,
|
||||
...props
|
||||
}: ComboboxInputProps) => (
|
||||
<div className="relative w-full">
|
||||
<Icon
|
||||
name={leftIcon}
|
||||
name="search"
|
||||
size="sm"
|
||||
css={{
|
||||
position: 'absolute',
|
||||
left: '$3',
|
||||
top: '$3',
|
||||
fontSize: '$xl',
|
||||
color: 'slate8',
|
||||
color: '$slate8',
|
||||
}}
|
||||
/>
|
||||
<ComboboxLib.Input
|
||||
placeholder="Search"
|
||||
className={`w-full border-solid border h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${
|
||||
open
|
||||
? 'border-b-0 rounded-t-xl bg-black border-slate6'
|
||||
: `rounded-xl bg-transparent cursor-pointer ${
|
||||
error ? 'border-red9' : 'border-slate7'
|
||||
}`
|
||||
}`}
|
||||
displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
|
||||
className={`w-full h-9 py-3 px-10 text-sm bg-transparent leading-5 text-slate11 outline-none `}
|
||||
{...props}
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -154,6 +122,7 @@ export type ComboboxProps = {
|
|||
* Value to indicate it's invalid
|
||||
*/
|
||||
error?: boolean;
|
||||
css?: string; //tailwind css
|
||||
};
|
||||
|
||||
export const Combobox: React.FC<ComboboxProps> = ({
|
||||
|
|
@ -164,6 +133,7 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|||
onChange,
|
||||
onBlur,
|
||||
error = false,
|
||||
css,
|
||||
}) => {
|
||||
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
|
||||
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
|
||||
|
|
@ -186,8 +156,6 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|||
setFilteredItems(items);
|
||||
}, [items]);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleSearch = useDebounce((searchValue: string) => {
|
||||
if (searchValue === '') {
|
||||
setFilteredItems(items);
|
||||
|
|
@ -216,10 +184,6 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|||
handleSearch(event.target.value);
|
||||
};
|
||||
|
||||
const handleInputClick = (): void => {
|
||||
buttonRef.current?.click();
|
||||
};
|
||||
|
||||
const handleComboboxChange = (optionSelected: ComboboxItem): void => {
|
||||
onChange(optionSelected);
|
||||
};
|
||||
|
|
@ -239,16 +203,33 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|||
onChange={handleComboboxChange}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<ComboboxInput
|
||||
onChange={handleInputChange}
|
||||
onClick={handleInputClick}
|
||||
open={open}
|
||||
leftIcon={leftIcon}
|
||||
onBlur={onBlur}
|
||||
error={error}
|
||||
/>
|
||||
<ComboboxLib.Button ref={buttonRef} className="hidden" />
|
||||
<div className={`relative w-full ${css ? css : ''}`}>
|
||||
<div className="relative w-full">
|
||||
<Icon
|
||||
name={leftIcon}
|
||||
size="sm"
|
||||
css={{
|
||||
position: 'absolute',
|
||||
left: '$3',
|
||||
top: '$3',
|
||||
fontSize: '$xl',
|
||||
color: 'slate8',
|
||||
}}
|
||||
/>
|
||||
<ComboboxLib.Button
|
||||
className={`w-full text-left border-solid border rounded-xl h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${
|
||||
error ? 'border-red9' : 'border-slate7'
|
||||
}`}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{selectedValue && selectedValue.label
|
||||
? selectedValue.label
|
||||
: 'Search'}
|
||||
</ComboboxLib.Button>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
|
|
@ -259,28 +240,35 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|||
leaveTo="opacity-0"
|
||||
afterLeave={handleLeaveTransition}
|
||||
>
|
||||
<ComboboxLib.Options className="absolute max-h-60 w-full z-10 overflow-auto rounded-b-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
|
||||
{[...autocompleteItems, ...filteredItems].length === 0 ||
|
||||
filteredItems === undefined ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<>
|
||||
{autocompleteItems.length > 0 && <span>Create new</span>}
|
||||
{autocompleteItems.map((autocompleteOption: ComboboxItem) => (
|
||||
<ComboboxOption
|
||||
key={autocompleteOption.value}
|
||||
option={autocompleteOption}
|
||||
/>
|
||||
))}
|
||||
{autocompleteItems.length > 0 && filteredItems.length > 0 && (
|
||||
<Separator css={{ mb: '$2' }} />
|
||||
)}
|
||||
{filteredItems.map((option: ComboboxItem) => (
|
||||
<ComboboxOption key={option.value} option={option} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ComboboxLib.Options>
|
||||
<div className="absolute max-h-60 mt-2 w-full z-10 overflow-auto rounded-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
|
||||
<ComboboxInput onChange={handleInputChange} onBlur={onBlur} />
|
||||
<Separator />
|
||||
<ComboboxLib.Options className="mt-1">
|
||||
{[...autocompleteItems, ...filteredItems].length === 0 ||
|
||||
filteredItems === undefined ? (
|
||||
<NoResults />
|
||||
) : (
|
||||
<>
|
||||
{autocompleteItems.length > 0 && <span>Create new</span>}
|
||||
{autocompleteItems.map(
|
||||
(autocompleteOption: ComboboxItem) => (
|
||||
<ComboboxOption
|
||||
key={autocompleteOption.value}
|
||||
option={autocompleteOption}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{autocompleteItems.length > 0 &&
|
||||
filteredItems.length > 0 && (
|
||||
<Separator css={{ mb: '$2' }} />
|
||||
)}
|
||||
{filteredItems.map((option: ComboboxItem) => (
|
||||
<ComboboxOption key={option.value} option={option} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ComboboxLib.Options>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ export const LogoFileInput = StyledInputFile;
|
|||
|
||||
type InputProps = {
|
||||
leftIcon?: IconName;
|
||||
css?: string; //tailwind css
|
||||
} & React.ComponentPropsWithRef<typeof InputStyled>;
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const { leftIcon, ...ownProps } = props;
|
||||
const { leftIcon, css, ...ownProps } = props;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={`relative ${css ? css : ''}`}>
|
||||
{leftIcon && (
|
||||
<InputIconStyled name={leftIcon} css={{ fontSize: '$lg' }} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { Combobox, Flex } from '@/components';
|
||||
import { Combobox, ComboboxItem, Flex } from '@/components';
|
||||
|
||||
const itemsCombobox = [
|
||||
{ label: 'Item 1', value: 'item-1' },
|
||||
|
|
@ -9,15 +9,16 @@ const itemsCombobox = [
|
|||
];
|
||||
|
||||
export const ComboboxTest: React.FC = () => {
|
||||
const [selectedValue, setSelectedValue] = useState('');
|
||||
const [selectedValueAutocomplete, setSelectedValueAutocomplete] =
|
||||
useState('');
|
||||
const [selectedValue, setSelectedValue] = useState({} as ComboboxItem);
|
||||
const [selectedValueAutocomplete, setSelectedValueAutocomplete] = useState(
|
||||
{} as ComboboxItem
|
||||
);
|
||||
|
||||
const handleComboboxChange = (value: string): void => {
|
||||
const handleComboboxChange = (value: ComboboxItem): void => {
|
||||
setSelectedValue(value);
|
||||
};
|
||||
|
||||
const handleComboboxChangeAutocomplete = (value: string): void => {
|
||||
const handleComboboxChangeAutocomplete = (value: ComboboxItem): void => {
|
||||
setSelectedValueAutocomplete(value);
|
||||
};
|
||||
|
||||
|
|
@ -32,12 +33,14 @@ export const ComboboxTest: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<h1>Components Test</h1>
|
||||
<Flex css={{ width: '400px', gap: '$2' }}>
|
||||
<Flex css={{ width: '600px', gap: '$2' }}>
|
||||
<Combobox
|
||||
items={itemsCombobox}
|
||||
selectedValue={selectedValue}
|
||||
onChange={handleComboboxChange}
|
||||
leftIcon="github"
|
||||
/>
|
||||
|
||||
<Combobox
|
||||
items={itemsCombobox}
|
||||
selectedValue={selectedValueAutocomplete}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { ToastTest } from './toast-test';
|
|||
export const ComponentsTest: React.FC = () => {
|
||||
return (
|
||||
<Flex css={{ flexDirection: 'column' }}>
|
||||
<ComboboxTest />
|
||||
<ColorPickerTest />
|
||||
<ToastTest />
|
||||
<ComboboxTest />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export const GithubRepositoryConnection: React.FC = () => {
|
|||
leftIcon="search"
|
||||
placeholder="Search repo"
|
||||
onChange={handleSearchChange}
|
||||
css="flex-1"
|
||||
/>
|
||||
</Flex>
|
||||
{queryLoading === 'loading' ||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const UserOrgsCombobox: React.FC = () => {
|
|||
selectedValue={selectedUserOrg}
|
||||
onChange={handleUserOrgChange}
|
||||
leftIcon="github"
|
||||
css="flex-1"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue