7 changed files with 119 additions and 130 deletions
@ -1,129 +1,61 @@ |
|||||||
import { Icon, Icons } from "components/Icon"; |
import { Icon, Icons } from "components/Icon"; |
||||||
import React, { |
import React, { Fragment } from "react"; |
||||||
MouseEventHandler, |
|
||||||
SyntheticEvent, |
|
||||||
useEffect, |
|
||||||
useState, |
|
||||||
} from "react"; |
|
||||||
|
|
||||||
import { Backdrop, useBackdrop } from "components/layout/Backdrop"; |
import { Listbox, Transition } from "@headlessui/react"; |
||||||
import { ButtonControl } from "./buttons/ButtonControl"; |
|
||||||
|
|
||||||
export interface OptionItem { |
export interface OptionItem { |
||||||
id: string; |
id: number; |
||||||
name: string; |
name: string; |
||||||
} |
} |
||||||
|
|
||||||
interface DropdownProps { |
interface DropdownProps { |
||||||
open: boolean; |
selectedItem: OptionItem; |
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>; |
setSelectedItem: (value: OptionItem) => void; |
||||||
selectedItem: string; |
|
||||||
setSelectedItem: (value: string) => void; |
|
||||||
options: Array<OptionItem>; |
options: Array<OptionItem>; |
||||||
} |
} |
||||||
|
|
||||||
export interface OptionProps { |
|
||||||
option: OptionItem; |
|
||||||
onClick: MouseEventHandler<HTMLDivElement>; |
|
||||||
tabIndex?: number; |
|
||||||
} |
|
||||||
|
|
||||||
function Option({ option, onClick, tabIndex }: OptionProps) { |
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="text-denim-700 flex h-10 cursor-pointer items-center space-x-2 px-4 py-2 text-left transition-colors hover:text-white" |
|
||||||
onClick={onClick} |
|
||||||
tabIndex={tabIndex} |
|
||||||
> |
|
||||||
<input type="radio" className="hidden" id={option.id} /> |
|
||||||
<label htmlFor={option.id} className="cursor-pointer "> |
|
||||||
<div className="item">{option.name}</div> |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>( |
export const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>( |
||||||
(props: DropdownProps, ref) => { |
(props: DropdownProps) => ( |
||||||
const [setBackdrop, backdropProps, highlightedProps] = useBackdrop(); |
<div className="relative my-4 w-72 "> |
||||||
const [delayedSelectedId, setDelayedSelectedId] = useState( |
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}> |
||||||
props.selectedItem |
{({ open }) => ( |
||||||
); |
<> |
||||||
|
<Listbox.Button className="bg-denim-500 focus-visible:ring-bink-500 focus-visible:ring-offset-bink-300 relative w-full cursor-default rounded-lg py-2 pl-3 pr-10 text-left text-white shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-offset-2 sm:text-sm"> |
||||||
useEffect(() => { |
<span className="block truncate">{props.selectedItem.name}</span> |
||||||
let id: NodeJS.Timeout; |
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> |
||||||
|
<Icon |
||||||
if (props.open) { |
icon={Icons.CHEVRON_DOWN} |
||||||
setDelayedSelectedId(props.selectedItem); |
className={`transform transition-transform ${ |
||||||
} else { |
open ? "rotate-180" : "" |
||||||
id = setTimeout(() => { |
}`}
|
||||||
setDelayedSelectedId(props.selectedItem); |
|
||||||
}, 200); |
|
||||||
} |
|
||||||
return () => { |
|
||||||
if (id) clearTimeout(id); |
|
||||||
}; |
|
||||||
/* eslint-disable-next-line */ |
|
||||||
}, [props.open]); |
|
||||||
|
|
||||||
const selectedItem: OptionItem = |
|
||||||
props.options.find((opt) => opt.id === props.selectedItem) || |
|
||||||
props.options[0]; |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
setBackdrop(props.open); |
|
||||||
/* eslint-disable-next-line */ |
|
||||||
}, [props.open]); |
|
||||||
|
|
||||||
const onOptionClick = (e: SyntheticEvent, option: OptionItem) => { |
|
||||||
e.stopPropagation(); |
|
||||||
props.setSelectedItem(option.id); |
|
||||||
props.setOpen(false); |
|
||||||
}; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="min-w-[140px]" |
|
||||||
onClick={() => props.setOpen((open) => !open)} |
|
||||||
> |
|
||||||
<div |
|
||||||
ref={ref} |
|
||||||
className="relative w-full sm:w-auto" |
|
||||||
{...highlightedProps} |
|
||||||
> |
|
||||||
<ButtonControl |
|
||||||
{...props} |
|
||||||
className="sm:justify-left bg-bink-200 hover:bg-bink-300 relative z-20 flex h-10 w-full items-center justify-center space-x-2 rounded-[20px] px-4 py-2 text-white" |
|
||||||
> |
|
||||||
<span className="flex-1">{selectedItem.name}</span> |
|
||||||
<Icon |
|
||||||
icon={Icons.CHEVRON_DOWN} |
|
||||||
className={`transition-transform ${ |
|
||||||
props.open ? "rotate-180" : "" |
|
||||||
}`}
|
|
||||||
/> |
|
||||||
</ButtonControl> |
|
||||||
<div |
|
||||||
className={`bg-denim-300 scrollbar scrollbar-thumb-gray-900 scrollbar-track-gray-100 absolute top-0 z-10 w-full overflow-y-auto rounded-[20px] pt-[40px] transition-all duration-200 ${ |
|
||||||
props.open |
|
||||||
? "block max-h-60 opacity-100" |
|
||||||
: "invisible max-h-0 opacity-0" |
|
||||||
}`}
|
|
||||||
> |
|
||||||
{props.options |
|
||||||
.filter((opt) => opt.id !== delayedSelectedId) |
|
||||||
.map((opt) => ( |
|
||||||
<Option |
|
||||||
option={opt} |
|
||||||
key={opt.id} |
|
||||||
onClick={(e) => onOptionClick(e, opt)} |
|
||||||
tabIndex={props.open ? 0 : undefined} |
|
||||||
/> |
/> |
||||||
))} |
</span> |
||||||
</div> |
</Listbox.Button> |
||||||
</div> |
<Transition |
||||||
<Backdrop onClick={() => props.setOpen(false)} {...backdropProps} /> |
as={Fragment} |
||||||
</div> |
leave="transition ease-in duration-100" |
||||||
); |
leaveFrom="opacity-100" |
||||||
} |
leaveTo="opacity-0" |
||||||
|
> |
||||||
|
<Listbox.Options className="bg-denim-500 scrollbar-thin scrollbar-track-denim-400 scrollbar-thumb-denim-200 absolute bottom-11 z-10 mt-1 max-h-60 w-72 overflow-auto rounded-md py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:bottom-10 sm:text-sm"> |
||||||
|
{props.options.map((opt) => ( |
||||||
|
<Listbox.Option |
||||||
|
className={({ active }) => |
||||||
|
`relative cursor-default select-none py-2 pl-10 pr-4 ${ |
||||||
|
active ? "bg-denim-400 text-bink-700" : "text-white" |
||||||
|
}` |
||||||
|
} |
||||||
|
key={opt.id} |
||||||
|
value={opt} |
||||||
|
> |
||||||
|
{opt.name} |
||||||
|
</Listbox.Option> |
||||||
|
))} |
||||||
|
</Listbox.Options> |
||||||
|
</Transition> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</Listbox> |
||||||
|
</div> |
||||||
|
) |
||||||
); |
); |
||||||
|
Loading…
Reference in new issue