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
className={`grid place-items-center text-custom-text-300 hover:text-custom-text-200 ${peekMode === "full" ? "rotate-45" : ""}`}
>
<Icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
</Listbox.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<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"> <div className="space-y-1 p-2">
{PEEK_MODES.map((mode) => ( {PEEK_MODES.map((mode) => (
<Listbox.Option <CustomSelect.Option
key={mode.key} key={mode.key}
value={mode.key} value={mode.key}
className={({ active, selected }) => className="cursor-pointer select-none truncate rounded px-1 py-1.5"
`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"> <div className="flex items-center gap-1.5">
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" /> <mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
{mode.label} {mode.label}
</div> </div>
</Listbox.Option> </CustomSelect.Option>
))} ))}
</div> </div>
</Listbox.Options> </CustomSelect>
</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,20 +174,17 @@ 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"
<Listbox.Button 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"
type="button" customButton={
ref={setReferenceElement} <>
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"
>
<span <span
className={`text-sm ${ className={`text-sm ${
!getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100" !getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
@@ -196,7 +192,6 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
> >
{ROLE[value]} {ROLE[value]}
</span> </span>
<ChevronDown <ChevronDown
className={`size-3 ${ className={`size-3 ${
!getValues(`emails.${index}.role_active`) !getValues(`emails.${index}.role_active`)
@@ -204,40 +199,24 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
: "stroke-onboarding-text-100" : "stroke-onboarding-text-100"
}`} }`}
/> />
</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 }) => ( {Object.entries(ROLE_DETAILS).map(([key, value]) => (
<CustomSelect.Option
key={key}
value={parseInt(key)}
className={"cursor-pointer select-none truncate rounded px-1 py-1.5"}
>
<div className="flex items-center text-wrap gap-2 p-1"> <div className="flex items-center text-wrap gap-2 p-1">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-sm font-medium">{t(value.i18n_title)}</div> <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 className="flex text-xs text-custom-text-300">{t(value.i18n_description)}</div>
</div> </div>
{selected && <Check className="h-4 w-4 shrink-0" />}
</div> </div>
)} </CustomSelect.Option>
</Listbox.Option>
))} ))}
</div> </CustomSelect>
</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,20 +173,17 @@ 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"
<Listbox.Button 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"
type="button" customButton={
ref={setReferenceElement} <>
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"
>
<span <span
className={`text-sm ${ className={`text-sm ${
!getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100" !getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
@@ -203,40 +199,24 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
: "stroke-onboarding-text-100" : "stroke-onboarding-text-100"
}`} }`}
/> />
</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 }) => ( {Object.entries(ROLE_DETAILS).map(([key, value]) => (
<CustomSelect.Option
key={key}
value={parseInt(key)}
className={"cursor-pointer select-none truncate rounded px-1 py-1.5"}
>
<div className="flex items-center text-wrap gap-2 p-1"> <div className="flex items-center text-wrap gap-2 p-1">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-sm font-medium">{t(value.i18n_title)}</div> <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 className="flex text-xs text-custom-text-300">{t(value.i18n_description)}</div>
</div> </div>
{selected && <Check className="h-4 w-4 shrink-0" />}
</div> </div>
)} </CustomSelect.Option>
</Listbox.Option>
))} ))}
</div> </CustomSelect>
</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