Compare commits

...

4 Commits

Author SHA1 Message Date
gakshita 9d8c22de91 fix: position 2025-08-28 15:52:34 +05:30
gakshita 6bbbc1b183 Merge branch 'preview' of https://github.com/makeplane/plane into feat-base-ui-select 2025-08-28 14:38:11 +05:30
gakshita 9f278deefd fix: refactor 2025-08-21 19:07:01 +05:30
gakshita e42c14ff71 feat: implemented base ui select 2025-08-21 15:23:23 +05:30
3 changed files with 241 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
export * from "./root";
+122
View File
@@ -0,0 +1,122 @@
import { Check, ChevronDown } from "lucide-react";
// helpers
import { cn } from "@plane/utils";
// types
import { ICustomSelectItemProps, ICustomSelectProps } from "./types";
import { Select } from "@base-ui-components/react/select";
import { convertPlacementToSideAndAlign } from "../utils/placement";
import React from "react";
const CustomSelect = (props: ICustomSelectProps) => {
const {
customButtonClassName = "",
buttonClassName = "",
placement,
children,
className = "",
customButton,
disabled = false,
input = false,
label,
maxHeight = "md",
noChevron = false,
onChange,
optionsClassName = "",
value,
tabIndex,
portalElement,
side,
align,
sideOffset = 8,
} = props;
// side and align calculations
const { finalSide, finalAlign } = React.useMemo(() => {
if (placement) {
const converted = convertPlacementToSideAndAlign(placement);
return { finalSide: converted.side, finalAlign: converted.align };
}
return { finalSide: side, finalAlign: align };
}, [placement, side, align]);
// states
return (
<Select.Root value={value} onValueChange={onChange} disabled={disabled}>
<>
{customButton ? (
<Select.Trigger
className={cn(
"outline-none flex items-center justify-between gap-1 text-xs",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer hover:bg-custom-background-80": !disabled,
},
customButtonClassName
)}
>
{customButton}
</Select.Trigger>
) : (
<Select.Trigger
className={cn(
"outline-none flex w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300",
{
"px-3 py-2 text-sm": input,
"px-2 py-1 text-xs": !input,
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer hover:bg-custom-background-80": !disabled,
},
buttonClassName
)}
>
{label}
{!noChevron && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</Select.Trigger>
)}
</>
<Select.Portal container={portalElement?.current}>
<Select.Backdrop />
<Select.Positioner side={finalSide} sideOffset={sideOffset} align={finalAlign}>
<Select.ScrollUpArrow />
<Select.Popup
className={cn(
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap",
{
"max-h-60": maxHeight === "lg",
"max-h-48": maxHeight === "md",
"max-h-36": maxHeight === "rg",
"max-h-28": maxHeight === "sm",
},
optionsClassName
)}
>
{children}
</Select.Popup>
<Select.ScrollDownArrow />
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
};
const Option = (props: ICustomSelectItemProps) => {
const { children, value, className } = props;
return (
<Select.Item
value={value}
className={cn(
"outline-none focus:bg-custom-background-80 cursor-pointer select-none truncate rounded px-1 py-1.5 text-custom-text-200 flex items-center justify-between gap-2 hover:bg-custom-background-80",
className
)}
>
{children}
<Select.ItemIndicator className={""}>
<Check className={"size-3.5 text-custom-text-200 flex-shrink-0"} />
</Select.ItemIndicator>
</Select.Item>
);
};
CustomSelect.Option = Option;
export { CustomSelect };
+118
View File
@@ -0,0 +1,118 @@
import { TAlign, TPlacement, TSide } from "../utils/placement";
export type ICustomSearchSelectOption = {
value: any;
query: string;
content: React.ReactNode;
disabled?: boolean;
tooltip?: string | React.ReactNode;
};
export interface IDropdownProps {
customButtonClassName?: string;
customButtonTabIndex?: number;
buttonClassName?: string;
className?: string;
customButton?: React.ReactNode;
disabled?: boolean;
input?: boolean;
label?: string | React.ReactNode;
maxHeight?: "sm" | "rg" | "md" | "lg";
noChevron?: boolean;
chevronClassName?: string;
onOpen?: () => void;
optionsClassName?: string;
tabIndex?: number;
useCaptureForOutsideClick?: boolean;
defaultOpen?: boolean;
placement?: TPlacement;
side?: TSide;
align?: TAlign;
sideOffset?: number;
}
export interface IPortalProps {
children: React.ReactNode;
container?: Element | null;
asChild?: boolean;
}
export interface ICustomMenuDropdownProps extends IDropdownProps {
children: React.ReactNode;
ellipsis?: boolean;
noBorder?: boolean;
verticalEllipsis?: boolean;
menuButtonOnClick?: (...args: any) => void;
menuItemsClassName?: string;
onMenuClose?: () => void;
closeOnSelect?: boolean;
portalElement?: Element | null;
openOnHover?: boolean;
ariaLabel?: string;
}
export interface ICustomSelectProps extends IDropdownProps {
children: React.ReactNode;
value: any;
onChange: any;
portalElement?: React.RefObject<HTMLElement>;
}
interface CustomSearchSelectProps {
footerOption?: React.ReactNode;
onChange: any;
onClose?: () => void;
noResultsMessage?: string;
options?: ICustomSearchSelectOption[];
}
interface SingleValueProps {
multiple?: false;
value: any;
}
interface MultipleValuesProps {
multiple?: true;
value: any[] | null;
}
export type ICustomSearchSelectProps = IDropdownProps &
CustomSearchSelectProps &
(SingleValueProps | MultipleValuesProps);
export interface ICustomMenuItemProps {
children: React.ReactNode;
disabled?: boolean;
onClick?: (args?: any) => void;
className?: string;
}
export interface ICustomSelectItemProps {
children: React.ReactNode;
value: any;
className?: string;
}
// Submenu interfaces
export interface ICustomSubMenuProps {
children: React.ReactNode;
trigger: React.ReactNode;
disabled?: boolean;
className?: string;
contentClassName?: string;
placement?: "center" | "start" | "end";
}
export interface ICustomSubMenuTriggerProps {
children: React.ReactNode;
disabled?: boolean;
className?: string;
}
export interface ICustomSubMenuContentProps {
children: React.ReactNode;
className?: string;
placement?: "center" | "start" | "end";
sideOffset?: number;
alignOffset?: number;
}