chore: dropdown component (#90)
* wip: add dropdown component * wip: added search functionality on dropdown * chore: dropdown component with properties * style: set width pase on parent * refactor: remove old dropdown component and add headless ui * chore: remove unsued radix component dependency * chore: add yarn.lock on root fodler * refactor: remove old folders from root project * chore: add import on index * chore: apply PR review
This commit is contained in:
parent
292f550466
commit
33ebac510c
|
|
@ -14,6 +14,7 @@
|
||||||
"author": "Fleek",
|
"author": "Fleek",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/providers": "^5.7.2",
|
"@ethersproject/providers": "^5.7.2",
|
||||||
|
"@headlessui/react": "^1.7.8",
|
||||||
"@radix-ui/colors": "^0.1.8",
|
"@radix-ui/colors": "^0.1.8",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { Fragment, useRef, useState } from 'react';
|
||||||
|
import { Combobox as ComboboxLib, Transition } from '@headlessui/react';
|
||||||
|
import { Icon, IconName } from '@/components/core/icon';
|
||||||
|
import { Flex } from '@/components/layout';
|
||||||
|
|
||||||
|
type ComboboxInputProps = {
|
||||||
|
open: boolean;
|
||||||
|
handleInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
handleInputClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComboboxInput = ({
|
||||||
|
open,
|
||||||
|
handleInputChange,
|
||||||
|
handleInputClick,
|
||||||
|
}: ComboboxInputProps) => (
|
||||||
|
<div className="relative">
|
||||||
|
<Icon
|
||||||
|
name="search"
|
||||||
|
size="sm"
|
||||||
|
css={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '$3',
|
||||||
|
top: '0.9375rem',
|
||||||
|
color: 'slate8',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ComboboxLib.Input
|
||||||
|
placeholder="Search"
|
||||||
|
className={`w-full border-solid border border-slate7 py-3 pl-8 pr-10 text-sm bg-transparent leading-5 text-slate11 outline-none ${
|
||||||
|
open ? 'border-b-0 rounded-t-xl bg-black border-slate6' : 'rounded-xl'
|
||||||
|
}`}
|
||||||
|
displayValue={(selectedValue: ComboboxItem) => selectedValue.label}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type ComboboxOptionProps = {
|
||||||
|
option: ComboboxItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComboboxOption = ({ option }: ComboboxOptionProps) => (
|
||||||
|
<ComboboxLib.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option}
|
||||||
|
className={({ active }) =>
|
||||||
|
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm ${
|
||||||
|
active ? 'bg-slate5 text-slate12' : 'bg-transparent'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<Flex css={{ justifyContent: 'space-between' }}>
|
||||||
|
<Flex css={{ flexDirection: 'row' }}>
|
||||||
|
{option.icon && <Icon name={option.icon} css={{ mr: '$2' }} />}
|
||||||
|
<span className={`${active ? 'text-slate12' : 'text-slate11'}`}>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
</Flex>
|
||||||
|
{selected && <Icon name="check" color="white" />}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ComboboxLib.Option>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NoResults = () => (
|
||||||
|
<div className="relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11">
|
||||||
|
Nothing found.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ComboboxItem = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
icon?: IconName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComboboxProps = {
|
||||||
|
items: ComboboxItem[];
|
||||||
|
selectedValue: ComboboxItem | undefined;
|
||||||
|
onChange(option: ComboboxItem): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Combobox: React.FC<ComboboxProps> = ({
|
||||||
|
items,
|
||||||
|
selectedValue = { value: '', label: '' },
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const filteredItems =
|
||||||
|
query === ''
|
||||||
|
? items
|
||||||
|
: items.filter((person) =>
|
||||||
|
person.label
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.includes(query.toLowerCase().replace(/\s+/g, ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setQuery(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputClick = () => {
|
||||||
|
buttonRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComboboxChange = (option: ComboboxItem) => {
|
||||||
|
onChange(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeaveTransition = () => {
|
||||||
|
setQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboboxLib
|
||||||
|
value={selectedValue}
|
||||||
|
by="value"
|
||||||
|
onChange={handleComboboxChange}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div className="w-full cursor-default overflow-hidden bg-transparent text-left focus:outline-none sm:text-sm">
|
||||||
|
<ComboboxInput
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
handleInputClick={handleInputClick}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
|
<ComboboxLib.Button ref={buttonRef} className="hidden" />
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition duration-400 ease-out"
|
||||||
|
leave="transition ease-out duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
afterLeave={handleLeaveTransition}
|
||||||
|
>
|
||||||
|
<ComboboxLib.Options className="absolute max-h-60 w-inherit 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">
|
||||||
|
{filteredItems.length === 0 && query !== '' ? (
|
||||||
|
<NoResults />
|
||||||
|
) : (
|
||||||
|
filteredItems.map((option) => {
|
||||||
|
return <ComboboxOption option={option} />;
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</ComboboxLib.Options>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ComboboxLib>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { Fragment } from 'react';
|
||||||
|
import { Listbox, Transition } from '@headlessui/react';
|
||||||
|
import { Icon } from '@/components/core/icon';
|
||||||
|
import { Flex } from '@/components';
|
||||||
|
|
||||||
|
type DropdownOptionProps = {
|
||||||
|
option: DropdownItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownOption = ({ option }: DropdownOptionProps) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={option.value}
|
||||||
|
className={({ active }) =>
|
||||||
|
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm ${
|
||||||
|
active ? 'bg-slate5 text-slate12' : 'bg-transparent'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
value={option}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<Flex css={{ justifyContent: 'space-between' }}>
|
||||||
|
<span className={`${active ? 'text-slate12' : 'text-slate11'}`}>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
{selected && <Icon name="check" color="white" />}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
);
|
||||||
|
|
||||||
|
type DropdownButtonProps = {
|
||||||
|
selectedValue: DropdownItem | undefined;
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownButton = ({ selectedValue, open }: DropdownButtonProps) => (
|
||||||
|
<Listbox.Button
|
||||||
|
className={`relative w-full cursor-default bg-transparent border-solid border border-slate7 py-3 pl-3.5 pr-10 text-left focus:outline-none sm:text-sm ${
|
||||||
|
open ? 'border-b-0 rounded-t-xl bg-black border-slate6' : 'rounded-xl'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="block truncate text-slate11">
|
||||||
|
{selectedValue ? selectedValue.label : 'Select'}
|
||||||
|
</span>
|
||||||
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
|
<Icon name="chevron-down" />
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type DropdownItem = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownProps = {
|
||||||
|
items: DropdownItem[];
|
||||||
|
selectedValue: DropdownItem | undefined;
|
||||||
|
onChange(option: DropdownItem): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
|
items,
|
||||||
|
selectedValue,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const handleDropdownChange = (option: DropdownItem) => {
|
||||||
|
onChange(option);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Listbox value={selectedValue} by="value" onChange={handleDropdownChange}>
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<DropdownButton selectedValue={selectedValue} open={open} />
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options className="absolute max-h-fit w-full z-10 overflow-auto rounded-b-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm">
|
||||||
|
{items.map((option: DropdownItem) => (
|
||||||
|
<DropdownOption option={option} />
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './combobox';
|
||||||
|
export * from './dropdown';
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
|
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
|
||||||
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
|
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
|
||||||
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
|
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
|
||||||
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
|
|
||||||
import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck';
|
import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck';
|
||||||
|
import { AiOutlineDown } from '@react-icons/all-files/ai/AiOutlineDown';
|
||||||
|
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
|
||||||
|
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
|
||||||
import { MetamaskIcon, EthereumIcon } from './custom';
|
import { MetamaskIcon, EthereumIcon } from './custom';
|
||||||
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
|
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
|
||||||
|
|
||||||
export const IconLibrary = Object.freeze({
|
export const IconLibrary = Object.freeze({
|
||||||
back: IoArrowBackCircleSharp,
|
back: IoArrowBackCircleSharp,
|
||||||
check: AiOutlineCheck,
|
check: AiOutlineCheck,
|
||||||
|
'chevron-down': AiOutlineDown,
|
||||||
ethereum: EthereumIcon,
|
ethereum: EthereumIcon,
|
||||||
github: IoLogoGithub,
|
github: IoLogoGithub,
|
||||||
info: IoInformationCircleSharp,
|
info: IoInformationCircleSharp,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
export * from './button';
|
export * from './button';
|
||||||
|
export * from './combobox';
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
|
export * from './input';
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export const spacing = {
|
||||||
52: '13rem',
|
52: '13rem',
|
||||||
56: '14rem',
|
56: '14rem',
|
||||||
60: '15rem',
|
60: '15rem',
|
||||||
|
62: '15.5rem',
|
||||||
64: '16rem',
|
64: '16rem',
|
||||||
72: '18rem',
|
72: '18rem',
|
||||||
80: '20rem',
|
80: '20rem',
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,14 @@ export const utils = {
|
||||||
btlr: (value: Stitches.PropertyValue<'borderTopLeftRadius'>) => ({
|
btlr: (value: Stitches.PropertyValue<'borderTopLeftRadius'>) => ({
|
||||||
borderTopLeftRadius: value,
|
borderTopLeftRadius: value,
|
||||||
}),
|
}),
|
||||||
|
bt: (value: Stitches.PropertyValue<'borderTopLeftRadius'>) => ({
|
||||||
|
borderTopLeftRadius: value,
|
||||||
|
borderTopRightRadius: value,
|
||||||
|
}),
|
||||||
|
bb: (value: Stitches.PropertyValue<'borderBottomLeftRadius'>) => ({
|
||||||
|
borderBottomRightRadius: value,
|
||||||
|
borderBottomLeftRadius: value,
|
||||||
|
}),
|
||||||
|
|
||||||
ox: (value: Stitches.PropertyValue<'overflowX'>) => ({ overflowX: value }),
|
ox: (value: Stitches.PropertyValue<'overflowX'>) => ({ overflowX: value }),
|
||||||
oy: (value: Stitches.PropertyValue<'overflowY'>) => ({ overflowY: value }),
|
oy: (value: Stitches.PropertyValue<'overflowY'>) => ({ overflowY: value }),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,19 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.tsx'],
|
content: ['./src/**/*.tsx'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
//TODO if we're gonna have ligth mode we should add also the light colors cause tailwind doesn't have them
|
||||||
|
slate5: 'rgba(43, 47, 49, 1)',
|
||||||
|
slate6: 'rgba(49, 53, 56, 1)',
|
||||||
|
slate7: 'rgba(58, 63, 66, 1)',
|
||||||
|
slate11: 'rgba(155, 161, 166, 1)',
|
||||||
|
slate12: 'rgba(236, 237, 238, 1)',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
inherit: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
12
ui/yarn.lock
12
ui/yarn.lock
|
|
@ -1585,6 +1585,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
||||||
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
|
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
|
||||||
|
|
||||||
|
"@headlessui/react@^1.7.8":
|
||||||
|
version "1.7.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.8.tgz#d7b4a95d50f9a386f7d1259d5ff8a6d7eb552ef0"
|
||||||
|
integrity sha512-zcwb0kd7L05hxmoAMIioEaOn235Dg0fUO+iGbLPgLVSjzl/l39V6DTpC2Df49PE5aG5/f5q0PZ9ZHZ78ENNV+A==
|
||||||
|
dependencies:
|
||||||
|
client-only "^0.0.1"
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.8":
|
"@humanwhocodes/config-array@^0.11.8":
|
||||||
version "0.11.8"
|
version "0.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
|
||||||
|
|
@ -4611,6 +4618,11 @@ cli-table3@^0.6.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@colors/colors" "1.5.0"
|
"@colors/colors" "1.5.0"
|
||||||
|
|
||||||
|
client-only@^0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
cliui@^7.0.2:
|
cliui@^7.0.2:
|
||||||
version "7.0.4"
|
version "7.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue