forked from cerc-io/snowballtools-base
Merge pull request #141 from snowball-tools/andrehadianto/T-4904-home-org-switcher
[T-4904] home org switcher
This commit is contained in:
commit
30ecd41975
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const BuildingIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.75 19.25H14.25M3.75 19.25V5.75C3.75 4.64543 4.64543 3.75 5.75 3.75H12.25C13.3546 3.75 14.25 4.64543 14.25 5.75V8M3.75 19.25H1.75M14.25 19.25V8M14.25 19.25H20.25M14.25 8H18.25C19.3546 8 20.25 8.89543 20.25 10V19.25M20.25 19.25H22.25M10.25 8.75H7.75M7.75 12.75H10.25"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const ChevronUpDown = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 15L12 19L16 15M8 9L12 5L16 9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -4,6 +4,7 @@ export * from './CheckIcon';
|
|||||||
export * from './ChevronGrabberHorizontal';
|
export * from './ChevronGrabberHorizontal';
|
||||||
export * from './ChevronLeft';
|
export * from './ChevronLeft';
|
||||||
export * from './ChevronRight';
|
export * from './ChevronRight';
|
||||||
|
export * from './ChevronUpDown';
|
||||||
export * from './InfoSquareIcon';
|
export * from './InfoSquareIcon';
|
||||||
export * from './WarningIcon';
|
export * from './WarningIcon';
|
||||||
export * from './SearchIcon';
|
export * from './SearchIcon';
|
||||||
@ -26,6 +27,7 @@ export * from './GithubIcon';
|
|||||||
export * from './GitTeaIcon';
|
export * from './GitTeaIcon';
|
||||||
export * from './LockIcon';
|
export * from './LockIcon';
|
||||||
export * from './PencilIcon';
|
export * from './PencilIcon';
|
||||||
|
export * from './BuildingIcon';
|
||||||
export * from './CheckRadioIcon';
|
export * from './CheckRadioIcon';
|
||||||
export * from './ChevronDownIcon';
|
export * from './ChevronDownIcon';
|
||||||
export * from './BranchIcon';
|
export * from './BranchIcon';
|
||||||
|
@ -62,13 +62,13 @@ const SelectItem = forwardRef<HTMLLIElement, SelectItemProps>(
|
|||||||
<p className={theme.label()} data-disabled={disabled}>
|
<p className={theme.label()} data-disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
{orientation === 'horizontal' && description && (
|
|
||||||
<span className={theme.dot()} />
|
|
||||||
)}
|
|
||||||
{description && (
|
{description && (
|
||||||
|
<>
|
||||||
|
{orientation === 'horizontal' && <span className={theme.dot()} />}
|
||||||
<p className={theme.description()} data-disabled={disabled}>
|
<p className={theme.description()} data-disabled={disabled}>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{renderRightIcon}
|
{renderRightIcon}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Organization } from 'gql-client';
|
import { Organization } from 'gql-client';
|
||||||
|
|
||||||
import { Option } from '@material-tailwind/react';
|
|
||||||
import { useDisconnect } from 'wagmi';
|
import { useDisconnect } from 'wagmi';
|
||||||
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import AsyncSelect from 'components/shared/AsyncSelect';
|
|
||||||
import {
|
import {
|
||||||
ChevronGrabberHorizontal,
|
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
LifeBuoyIcon,
|
LifeBuoyIcon,
|
||||||
@ -17,6 +14,7 @@ import {
|
|||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Tabs } from 'components/shared/Tabs';
|
import { Tabs } from 'components/shared/Tabs';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { UserSelect } from 'components/shared/UserSelect';
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
@ -37,6 +35,22 @@ export const Sidebar = () => {
|
|||||||
setSelectedOrgSlug(orgSlug);
|
setSelectedOrgSlug(orgSlug);
|
||||||
}, [orgSlug]);
|
}, [orgSlug]);
|
||||||
|
|
||||||
|
const formattedSelected = useMemo(() => {
|
||||||
|
const selected = organizations.find((org) => org.slug === selectedOrgSlug);
|
||||||
|
return {
|
||||||
|
value: selected?.slug ?? '',
|
||||||
|
label: selected?.name ?? '',
|
||||||
|
imgSrc: '/logo.svg',
|
||||||
|
};
|
||||||
|
}, [organizations, selectedOrgSlug, orgSlug]);
|
||||||
|
const formattedSelectOptions = useMemo(() => {
|
||||||
|
return organizations.map((org) => ({
|
||||||
|
value: org.slug,
|
||||||
|
label: org.name,
|
||||||
|
imgSrc: '/logo.svg',
|
||||||
|
}));
|
||||||
|
}, [organizations, selectedOrgSlug, orgSlug]);
|
||||||
|
|
||||||
const handleLogOut = useCallback(() => {
|
const handleLogOut = useCallback(() => {
|
||||||
disconnect();
|
disconnect();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
@ -57,49 +71,10 @@ export const Sidebar = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
{/* Switch organization */}
|
{/* Switch organization */}
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
<AsyncSelect
|
<UserSelect
|
||||||
containerProps={{ className: 'h-14 border-none' }}
|
value={formattedSelected}
|
||||||
labelProps={{ className: 'before:border-none after:border-none' }}
|
options={formattedSelectOptions}
|
||||||
className="bg-white rounded-lg shadow border-none"
|
|
||||||
value={selectedOrgSlug}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSelectedOrgSlug(value!);
|
|
||||||
navigate(`/${value}`);
|
|
||||||
}}
|
|
||||||
selected={(_, index) => (
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
src="/logo.svg"
|
|
||||||
alt="Application Logo"
|
|
||||||
className="h-8 w-8 rounded-lg"
|
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<div className="text-sm font-semibold">
|
|
||||||
{organizations[index!]?.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">Organization</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
|
|
||||||
>
|
|
||||||
{/* // TODO: Show label organization and manage in option */}
|
|
||||||
{organizations.map((org) => (
|
|
||||||
<Option key={org.id} value={org.slug}>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<img
|
|
||||||
src="/logo.svg"
|
|
||||||
alt="Application Logo"
|
|
||||||
className="h-8 w-8 rounded-lg"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-semibold">{org.name}</div>
|
|
||||||
<div className="text-xs text-gray-500">Organization</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</AsyncSelect>
|
|
||||||
<Tabs defaultValue="Projects" orientation="vertical">
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
{[
|
{[
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { tv, VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const userSelectTheme = tv({
|
||||||
|
slots: {
|
||||||
|
container: ['flex', 'flex-col', 'relative', 'gap-2'],
|
||||||
|
inputWrapper: [
|
||||||
|
'relative',
|
||||||
|
'flex',
|
||||||
|
'flex-wrap',
|
||||||
|
'gap-1',
|
||||||
|
'min-w-[200px]',
|
||||||
|
'w-full',
|
||||||
|
'rounded-lg',
|
||||||
|
'bg-transparent',
|
||||||
|
'text-elements-mid-em',
|
||||||
|
'shadow-sm',
|
||||||
|
'border',
|
||||||
|
'border-border-interactive',
|
||||||
|
'focus-ring',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
'disabled:border-none',
|
||||||
|
],
|
||||||
|
input: ['outline-none'],
|
||||||
|
popover: [
|
||||||
|
'mt-12',
|
||||||
|
'z-20',
|
||||||
|
'absolute',
|
||||||
|
'flex-col',
|
||||||
|
'gap-0.5',
|
||||||
|
'min-w-full',
|
||||||
|
'bg-surface-floating',
|
||||||
|
'shadow-dropdown',
|
||||||
|
'w-auto',
|
||||||
|
'max-h-60',
|
||||||
|
'overflow-auto',
|
||||||
|
'border',
|
||||||
|
'border-gray-200',
|
||||||
|
'rounded-xl',
|
||||||
|
],
|
||||||
|
popoverItemWrapper: ['flex', 'flex-col', 'px-2', 'py-2', 'gap-1'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
isOpen: {
|
||||||
|
true: {
|
||||||
|
popover: ['flex'],
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
popover: ['hidden'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hasValue: {
|
||||||
|
true: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserSelectTheme = VariantProps<typeof userSelectTheme>;
|
@ -0,0 +1,208 @@
|
|||||||
|
import React, {
|
||||||
|
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,
|
||||||
|
})}
|
||||||
|
ref={inputWrapperRef}
|
||||||
|
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">Organization</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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { tv, VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const userSelectItemTheme = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: [
|
||||||
|
'p-2',
|
||||||
|
'gap-3',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-between',
|
||||||
|
'rounded-lg',
|
||||||
|
'cursor-pointer',
|
||||||
|
],
|
||||||
|
content: ['flex', 'gap-3', 'items-center'],
|
||||||
|
img: ['h-10', 'w-10', 'rounded-lg'],
|
||||||
|
selectedIcon: ['h-5', 'w-5', 'text-controls-primary'],
|
||||||
|
label: ['text-sm', 'text-elements-high-em', 'tracking-[-0.006em]'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
active: {
|
||||||
|
true: {
|
||||||
|
wrapper: ['bg-base-bg-emphasized'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserSelectItemTheme = VariantProps<typeof userSelectItemTheme>;
|
@ -0,0 +1,66 @@
|
|||||||
|
import React, { forwardRef, ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
|
||||||
|
import {
|
||||||
|
userSelectItemTheme,
|
||||||
|
UserSelectItemTheme,
|
||||||
|
} from './UserSelectItem.theme';
|
||||||
|
import { CheckRadioIcon } from 'components/shared/CustomIcon';
|
||||||
|
import { UserSelectOption } from 'components/shared/UserSelect';
|
||||||
|
import { OmitCommon } from 'types/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
|
||||||
|
* @type {MergedComponentPropsWithoutRef}
|
||||||
|
*/
|
||||||
|
type MergedComponentPropsWithoutRef = OmitCommon<
|
||||||
|
ComponentPropsWithoutRef<'li'>,
|
||||||
|
Omit<
|
||||||
|
Overwrite<UseComboboxGetItemPropsReturnValue, UserSelectOption[]>,
|
||||||
|
'index' | 'item'
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface UserSelectItemProps
|
||||||
|
extends MergedComponentPropsWithoutRef,
|
||||||
|
UserSelectItemTheme {
|
||||||
|
selected: boolean;
|
||||||
|
option: UserSelectOption;
|
||||||
|
hovered?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSelectItem = forwardRef<HTMLLIElement, UserSelectItemProps>(
|
||||||
|
({ className, selected, hovered, option, ...props }, ref) => {
|
||||||
|
const theme = userSelectItemTheme();
|
||||||
|
|
||||||
|
const { value, label, imgSrc = './logo.svg' } = option;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
className={theme.wrapper({ className, active: selected || hovered })}
|
||||||
|
>
|
||||||
|
<div className={theme.content()}>
|
||||||
|
<img src={imgSrc} alt={`${value}-logo`} className={theme.img()} />
|
||||||
|
<p className={theme.label()}>{label}</p>
|
||||||
|
</div>
|
||||||
|
{selected && <CheckRadioIcon className={theme.selectedIcon()} />}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EmptyUserSelectItem = () => {
|
||||||
|
const theme = userSelectItemTheme();
|
||||||
|
return (
|
||||||
|
<li className={theme.wrapper()}>
|
||||||
|
<div className={theme.content()}>
|
||||||
|
<p className={theme.label()}>No results found</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UserSelectItem.displayName = 'UserSelectItem';
|
||||||
|
|
||||||
|
export { UserSelectItem };
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './UserSelectItem';
|
||||||
|
export * from './UserSelectItem.theme';
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './UserSelect';
|
||||||
|
export * from './UserSelect.theme';
|
Loading…
Reference in New Issue
Block a user