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