forked from cerc-io/snowballtools-base
205 lines
6.6 KiB
TypeScript
205 lines
6.6 KiB
TypeScript
import { useState, ComponentPropsWithoutRef, useRef, useEffect } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useSelect } from 'downshift';
|
||
import { UserSelectTheme, userSelectTheme } from './UserSelect.theme';
|
||
import { EmptyUserSelectItem, UserSelectItem } from './UserSelectItem';
|
||
import {
|
||
BuildingIcon,
|
||
ChevronUpDown,
|
||
SettingsSlidersIcon,
|
||
} from 'components/shared/CustomIcon';
|
||
import { WavyBorder } from 'components/shared/WavyBorder';
|
||
import { cn } from 'utils/classnames';
|
||
|
||
export type UserSelectOption = {
|
||
value: string;
|
||
label: string;
|
||
imgSrc?: string;
|
||
};
|
||
|
||
interface UserSelectProps
|
||
extends Omit<ComponentPropsWithoutRef<'div'>, 'value' | 'onChange'>,
|
||
UserSelectTheme {
|
||
options: UserSelectOption[];
|
||
value?: UserSelectOption;
|
||
}
|
||
|
||
export const UserSelect = ({ options, value }: UserSelectProps) => {
|
||
const theme = userSelectTheme();
|
||
const navigate = useNavigate();
|
||
|
||
const [selectedItem, setSelectedItem] = useState<UserSelectOption | null>(
|
||
(value as UserSelectOption) || null,
|
||
);
|
||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
||
'bottom',
|
||
);
|
||
const popoverRef = useRef(null); // Ref for the popover
|
||
const inputWrapperRef = useRef(null); // Ref for the input wrapper
|
||
|
||
// Calculate and update popover position
|
||
useEffect(() => {
|
||
if (dropdownOpen && popoverRef.current && inputWrapperRef.current) {
|
||
const popover = popoverRef.current;
|
||
// @ts-expect-error – we know it's not null lol
|
||
const input = inputWrapperRef.current.getBoundingClientRect();
|
||
const spaceBelow = window.innerHeight - input.bottom;
|
||
const spaceAbove = input.top;
|
||
// @ts-expect-error – we know it's not null lol
|
||
const popoverHeight = popover.offsetHeight;
|
||
|
||
// Determine if there's enough space below
|
||
if (spaceBelow >= popoverHeight) {
|
||
setDropdownPosition('bottom');
|
||
} else if (spaceAbove >= popoverHeight) {
|
||
setDropdownPosition('top');
|
||
} else {
|
||
// Default to bottom if neither has enough space, but you could also set logic to choose the side with more space
|
||
setDropdownPosition('bottom');
|
||
}
|
||
}
|
||
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
||
|
||
useEffect(() => {
|
||
setSelectedItem(value as UserSelectOption);
|
||
}, [value]);
|
||
|
||
const handleSelectedItemChange = (selectedItem: UserSelectOption | null) => {
|
||
setSelectedItem(selectedItem);
|
||
navigate(`/${selectedItem?.value}`);
|
||
};
|
||
|
||
const isSelected = (item: UserSelectOption) =>
|
||
selectedItem?.value === item.value;
|
||
|
||
const {
|
||
isOpen,
|
||
getMenuProps,
|
||
getToggleButtonProps,
|
||
highlightedIndex,
|
||
getItemProps,
|
||
openMenu,
|
||
} = useSelect({
|
||
items: options,
|
||
// @ts-expect-error – there are two params but we don't need the second one
|
||
isItemDisabled: (item) => item.disabled,
|
||
onSelectedItemChange: ({ selectedItem }) => {
|
||
if (selectedItem) {
|
||
handleSelectedItemChange(selectedItem);
|
||
}
|
||
},
|
||
onIsOpenChange: ({ isOpen }) => {
|
||
setDropdownOpen(isOpen ?? false);
|
||
},
|
||
itemToString: (item) => (item ? item.label : ''),
|
||
});
|
||
|
||
const handleManage = () => {
|
||
//TODO: implement manage handler
|
||
};
|
||
|
||
return (
|
||
<div className={theme.container()}>
|
||
{/* Input */}
|
||
<div
|
||
{...getToggleButtonProps(
|
||
{
|
||
ref: inputWrapperRef,
|
||
},
|
||
{ suppressRefError: true },
|
||
)}
|
||
onClick={() => !dropdownOpen && openMenu()}
|
||
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
|
||
>
|
||
<div className="flex gap-3 w-full mr-2">
|
||
<img
|
||
src={selectedItem?.imgSrc || '/logo.svg'}
|
||
alt="Snowball Logo"
|
||
className="h-10 w-10 rounded-lg"
|
||
/>
|
||
<div className="flex flex-col justify-center h-10 w-full">
|
||
{selectedItem?.label ? (
|
||
<p className="text-sm text-elements-high-em">
|
||
{selectedItem?.label}
|
||
</p>
|
||
) : (
|
||
<div className="animate-pulse h-1.5 mb-1 w-full rounded-full bg-elements-on-disabled" />
|
||
)}
|
||
<p className="text-xs text-elements-low-em">Team</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="h-4 w-4 text-slate-400">
|
||
<ChevronUpDown size={16} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Popover */}
|
||
<ul
|
||
{...getMenuProps({ ref: popoverRef }, { suppressRefError: true })}
|
||
id="popover"
|
||
ref={popoverRef}
|
||
className={cn(theme.popover({ isOpen }), {
|
||
// Position the popover based on the dropdown position
|
||
'top-[27.5%]': dropdownPosition === 'bottom',
|
||
'bottom-[92.5%]': dropdownPosition === 'top',
|
||
})}
|
||
>
|
||
<div className={theme.popoverItemWrapper()}>
|
||
{/* Settings header */}
|
||
<div className="flex justify-between h-8 items-center">
|
||
<div className="flex gap-1 text-elements-mid-em">
|
||
<BuildingIcon size={16} />
|
||
<p className="text-xs font-medium">Other teams</p>
|
||
</div>
|
||
<div
|
||
className="flex gap-1 text-elements-link cursor-pointer"
|
||
onClick={handleManage}
|
||
>
|
||
<p className="text-xs font-medium">Manage</p>
|
||
<SettingsSlidersIcon size={16} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Organization */}
|
||
{isOpen && options.length !== 0 ? (
|
||
options.map((item, index) => (
|
||
<UserSelectItem
|
||
{...getItemProps({ item, index })}
|
||
key={item.value}
|
||
selected={isSelected(item)}
|
||
option={item}
|
||
hovered={highlightedIndex === index}
|
||
/>
|
||
))
|
||
) : (
|
||
<EmptyUserSelectItem />
|
||
)}
|
||
</div>
|
||
|
||
{/* WavyBorder */}
|
||
{/* //TODO:remove if personal dont exist */}
|
||
<WavyBorder />
|
||
|
||
<div className={theme.popoverItemWrapper()}>
|
||
{/* //TODO:Personal (replace options with Personal Options) */}
|
||
{isOpen && options.length !== 0 ? (
|
||
options.map((item, index) => (
|
||
<UserSelectItem
|
||
{...getItemProps({ item, index: 99 })}
|
||
key={item.value}
|
||
selected={isSelected(item)}
|
||
option={item}
|
||
hovered={highlightedIndex === index}
|
||
/>
|
||
))
|
||
) : (
|
||
<EmptyUserSelectItem />
|
||
)}
|
||
</div>
|
||
</ul>
|
||
</div>
|
||
);
|
||
};
|