chore: replaced listbox with our custom implementation

This commit is contained in:
gakshita
2025-08-18 19:47:21 +05:30
parent 9cf564caae
commit fda8c728b5
4 changed files with 98 additions and 161 deletions

View File

@@ -3,9 +3,8 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Link2, MoveRight } from "lucide-react"; import { Link2, MoveRight } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
// ui // ui
import { CenterPanelIcon, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui"; import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui";
// helpers // helpers
import { copyTextToClipboard } from "@/helpers/string.helper"; import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks // hooks
@@ -66,49 +65,29 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
<MoveRight className="size-4" /> <MoveRight className="size-4" />
</button> </button>
)} )}
<Listbox <CustomSelect
as="div"
value={peekMode} value={peekMode}
onChange={(val) => setPeekMode(val)} onChange={(val: any) => setPeekMode(val)}
className="relative flex-shrink-0 text-left" className="relative flex-shrink-0 text-left"
customButtonClassName={`grid place-items-center text-custom-text-300 hover:text-custom-text-200 ${peekMode === "full" ? "rotate-45" : ""}`}
customButton={<Icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />}
optionsClassName="absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none"
> >
<Listbox.Button <div className="space-y-1 p-2">
className={`grid place-items-center text-custom-text-300 hover:text-custom-text-200 ${peekMode === "full" ? "rotate-45" : ""}`} {PEEK_MODES.map((mode) => (
> <CustomSelect.Option
<Icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" /> key={mode.key}
</Listbox.Button> value={mode.key}
className="cursor-pointer select-none truncate rounded px-1 py-1.5"
<Transition >
as={React.Fragment} <div className="flex items-center gap-1.5">
enter="transition ease-out duration-100" <mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
enterFrom="transform opacity-0 scale-95" {mode.label}
enterTo="transform opacity-100 scale-100" </div>
leave="transition ease-in duration-75" </CustomSelect.Option>
leaveFrom="transform opacity-100 scale-100" ))}
leaveTo="transform opacity-0 scale-95" </div>
> </CustomSelect>
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
<div className="space-y-1 p-2">
{PEEK_MODES.map((mode) => (
<Listbox.Option
key={mode.key}
value={mode.key}
className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
<div className="flex items-center gap-1.5">
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
{mode.label}
</div>
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</Listbox>
</div> </div>
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && ( {isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
<div className="flex flex-shrink-0 items-center gap-2"> <div className="flex flex-shrink-0 items-center gap-2">

View File

@@ -15,15 +15,14 @@ import {
} from "react-hook-form"; } from "react-hook-form";
// icons // icons
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { ChevronDown, Plus, XCircle } from "lucide-react";
import { Listbox } from "@headlessui/react";
// plane imports // plane imports
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants"; import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import { IUser, IWorkspace } from "@plane/types"; import { IUser, IWorkspace } from "@plane/types";
// ui // ui
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants // constants
// helpers // helpers
// hooks // hooks
@@ -175,69 +174,49 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
name={`emails.${index}.role`} name={`emails.${index}.role`}
rules={{ required: true }} rules={{ required: true }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Listbox <CustomSelect
as="div"
value={value} value={value}
onChange={(val) => { onChange={(val: any) => {
onChange(val); onChange(val);
setValue(`emails.${index}.role_active`, true); setValue(`emails.${index}.role_active`, true);
}} }}
className="w-full flex-shrink-0 text-left" className="w-full flex-shrink-0 text-left"
optionsClassName="p-2 absolute space-y-1 z-10 mt-1 h-fit w-48 sm:w-60 rounded-md border border-custom-border-300 bg-custom-background-100 shadow-sm focus:outline-none"
customButtonClassName="flex w-full items-center justify-between gap-1 rounded-md px-2.5 py-2 text-sm border-[0.5px] border-custom-border-300"
customButton={
<>
<span
className={`text-sm ${
!getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
} sm:text-sm`}
>
{ROLE[value]}
</span>
<ChevronDown
className={`size-3 ${
!getValues(`emails.${index}.role_active`)
? "stroke-onboarding-text-400"
: "stroke-onboarding-text-100"
}`}
/>
</>
}
> >
<Listbox.Button {Object.entries(ROLE_DETAILS).map(([key, value]) => (
type="button" <CustomSelect.Option
ref={setReferenceElement} key={key}
className="flex w-full items-center justify-between gap-1 rounded-md px-2.5 py-2 text-sm border-[0.5px] border-custom-border-300" value={parseInt(key)}
> className={"cursor-pointer select-none truncate rounded px-1 py-1.5"}
<span
className={`text-sm ${
!getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
} sm:text-sm`}
> >
{ROLE[value]} <div className="flex items-center text-wrap gap-2 p-1">
</span> <div className="flex flex-col">
<div className="text-sm font-medium">{t(value.i18n_title)}</div>
<ChevronDown <div className="flex text-xs text-custom-text-300">{t(value.i18n_description)}</div>
className={`size-3 ${ </div>
!getValues(`emails.${index}.role_active`) </div>
? "stroke-onboarding-text-400" </CustomSelect.Option>
: "stroke-onboarding-text-100" ))}
}`} </CustomSelect>
/>
</Listbox.Button>
<Listbox.Options as="div">
<div
className="p-2 absolute space-y-1 z-10 mt-1 h-fit w-48 sm:w-60 rounded-md border border-custom-border-300 bg-custom-background-100 shadow-sm focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{Object.entries(ROLE_DETAILS).map(([key, value]) => (
<Listbox.Option
as="div"
key={key}
value={parseInt(key)}
className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-onboarding-background-400/40" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<div className="flex items-center text-wrap gap-2 p-1">
<div className="flex flex-col">
<div className="text-sm font-medium">{t(value.i18n_title)}</div>
<div className="flex text-xs text-custom-text-300">{t(value.i18n_description)}</div>
</div>
{selected && <Check className="h-4 w-4 shrink-0" />}
</div>
)}
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Listbox>
)} )}
/> />
</div> </div>

View File

@@ -15,15 +15,14 @@ import {
} from "react-hook-form"; } from "react-hook-form";
// icons // icons
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { ChevronDown, Plus, XCircle } from "lucide-react";
import { Listbox } from "@headlessui/react";
// plane imports // plane imports
import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants"; import { ROLE, ROLE_DETAILS, EUserPermissions, MEMBER_TRACKER_EVENTS, MEMBER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import { EOnboardingSteps, IWorkspace } from "@plane/types"; import { EOnboardingSteps, IWorkspace } from "@plane/types";
// ui // ui
import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, CustomSelect, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
// constants // constants
// helpers // helpers
@@ -174,69 +173,50 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
name={`emails.${index}.role`} name={`emails.${index}.role`}
rules={{ required: true }} rules={{ required: true }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Listbox <CustomSelect
as="div"
value={value} value={value}
onChange={(val) => { onChange={(val: any) => {
onChange(val); onChange(val);
setValue(`emails.${index}.role_active`, true); setValue(`emails.${index}.role_active`, true);
}} }}
className="w-full flex-shrink-0 text-left" className="w-full flex-shrink-0 text-left"
customButtonClassName="flex w-full items-center justify-between gap-1 rounded-md px-2.5 py-2 text-sm border-[0.5px] border-custom-border-300"
optionsClassName="p-2 absolute space-y-1 z-10 mt-1 h-fit w-48 sm:w-60 rounded-md border border-custom-border-300 bg-custom-background-100 shadow-sm focus:outline-none"
customButton={
<>
<span
className={`text-sm ${
!getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
} sm:text-sm`}
>
{ROLE[value]}
</span>
<ChevronDown
className={`size-3 ${
!getValues(`emails.${index}.role_active`)
? "stroke-onboarding-text-400"
: "stroke-onboarding-text-100"
}`}
/>
</>
}
> >
<Listbox.Button {Object.entries(ROLE_DETAILS).map(([key, value]) => (
type="button" <CustomSelect.Option
ref={setReferenceElement} key={key}
className="flex w-full items-center justify-between gap-1 rounded-md px-2.5 py-2 text-sm border-[0.5px] border-custom-border-300" value={parseInt(key)}
> className={"cursor-pointer select-none truncate rounded px-1 py-1.5"}
<span
className={`text-sm ${
!getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
} sm:text-sm`}
> >
{ROLE[value]} <div className="flex items-center text-wrap gap-2 p-1">
</span> <div className="flex flex-col">
<div className="text-sm font-medium">{t(value.i18n_title)}</div>
<ChevronDown <div className="flex text-xs text-custom-text-300">{t(value.i18n_description)}</div>
className={`size-3 ${ </div>
!getValues(`emails.${index}.role_active`) </div>
? "stroke-onboarding-text-400" </CustomSelect.Option>
: "stroke-onboarding-text-100" ))}
}`} </CustomSelect>
/>
</Listbox.Button>
<Listbox.Options as="div">
<div
className="p-2 absolute space-y-1 z-10 mt-1 h-fit w-48 sm:w-60 rounded-md border border-custom-border-300 bg-custom-background-100 shadow-sm focus:outline-none"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
{Object.entries(ROLE_DETAILS).map(([key, value]) => (
<Listbox.Option
as="div"
key={key}
value={parseInt(key)}
className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-onboarding-background-400/40" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<div className="flex items-center text-wrap gap-2 p-1">
<div className="flex flex-col">
<div className="text-sm font-medium">{t(value.i18n_title)}</div>
<div className="flex text-xs text-custom-text-300">{t(value.i18n_description)}</div>
</div>
{selected && <Check className="h-4 w-4 shrink-0" />}
</div>
)}
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Listbox>
)} )}
/> />
</div> </div>

View File

@@ -124,7 +124,6 @@ export const LanguageTimezone = observer(() => {
value={profile?.language} value={profile?.language}
label={profile?.language ? getLanguageLabel(profile?.language) : "Select a language"} label={profile?.language ? getLanguageLabel(profile?.language) : "Select a language"}
onChange={handleLanguageChange} onChange={handleLanguageChange}
buttonClassName={"border-none"}
className="rounded-md border !border-custom-border-200" className="rounded-md border !border-custom-border-200"
optionsClassName="w-full" optionsClassName="w-full"
input input