chore: drop-downs improvements and bug fixes (#3433)

* chore: dropdowns should close on selecting an option

* style: @plane/ui dropdown styling

* refactor: @plane/ui dropdowns

* fix: build errors

* fix: list layout dropdowns positioning

* fix: priority dropdown text in dark mode
This commit is contained in:
Aaryan Khandelwal 2024-01-23 14:25:09 +05:30 committed by GitHub
parent f88109ef04
commit 801f75f406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 308 additions and 394 deletions

View file

@ -2,7 +2,6 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useCycle } from "hooks/store";
@ -14,21 +13,14 @@ import { ContrastIcon } from "@plane/ui";
import { cn } from "helpers/common.helper";
// types
import { ICycle } from "@plane/types";
import { TButtonVariants } from "./types";
import { TDropdownProps } from "./types";
type Props = {
type Props = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
onChange: (val: string | null) => void;
placement?: Placement;
projectId: string;
value: string | null;
tabIndex?: number;
};
type ButtonProps = {
@ -291,6 +283,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>

View file

@ -3,7 +3,6 @@ import { Popover } from "@headlessui/react";
import DatePicker from "react-datepicker";
import { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react";
// import "react-datepicker/dist/react-datepicker.css";
// hooks
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
@ -11,24 +10,17 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
import { renderFormattedDate } from "helpers/date-time.helper";
import { cn } from "helpers/common.helper";
// types
import { TButtonVariants } from "./types";
import { Placement } from "@popperjs/core";
import { TDropdownProps } from "./types";
type Props = {
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
disabled?: boolean;
type Props = TDropdownProps & {
icon?: React.ReactNode;
isClearable?: boolean;
minDate?: Date;
maxDate?: Date;
onChange: (val: Date | null) => void;
placeholder: string;
placement?: Placement;
value: Date | string | null;
closeOnSelect?: boolean;
tabIndex?: number;
};
type ButtonProps = {
@ -118,6 +110,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
buttonClassName = "",
buttonContainerClassName,
buttonVariant,
className = "",
disabled = false,
icon = <CalendarDays className="h-3 w-3 flex-shrink-0" />,
isClearable = true,
@ -160,102 +153,103 @@ export const DateDropdown: React.FC<Props> = (props) => {
useOutsideClickDetector(dropdownRef, closeDropdown);
return (
<Popover ref={dropdownRef} tabIndex={tabIndex} className="h-full flex-shrink-0" onKeyDown={handleKeyDown}>
{({ close }) => (
<>
<Popover.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={openDropdown}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
hideText
/>
) : null}
</button>
</Popover.Button>
{isOpen && (
<Popover.Panel className="fixed z-10" static>
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<DatePicker
selected={value ? new Date(value) : null}
onChange={(val) => {
onChange(val);
if (closeOnSelect) close();
}}
dateFormat="dd-MM-yyyy"
minDate={minDate}
maxDate={maxDate}
calendarClassName="shadow-custom-shadow-rg rounded"
inline
/>
</div>
</Popover.Panel>
<Popover
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full flex-shrink-0", className)}
onKeyDown={handleKeyDown}
>
<Popover.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
</>
onClick={openDropdown}
>
{buttonVariant === "border-with-text" ? (
<BorderButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
hideText
/>
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
hideText
/>
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
date={value}
className={buttonClassName}
icon={icon}
placeholder={placeholder}
isClearable={isClearable && isDateSelected}
onClear={() => onChange(null)}
hideText
/>
) : null}
</button>
</Popover.Button>
{isOpen && (
<Popover.Panel className="fixed z-10" static>
<div className="my-1" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<DatePicker
selected={value ? new Date(value) : null}
onChange={(val) => {
onChange(val);
if (closeOnSelect) closeDropdown();
}}
dateFormat="dd-MM-yyyy"
minDate={minDate}
maxDate={maxDate}
calendarClassName="shadow-custom-shadow-rg rounded"
inline
/>
</div>
</Popover.Panel>
)}
</Popover>
);

View file

@ -2,7 +2,6 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
import sortBy from "lodash/sortBy";
// hooks
@ -12,21 +11,14 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
// helpers
import { cn } from "helpers/common.helper";
// types
import { TButtonVariants } from "./types";
import { TDropdownProps } from "./types";
type Props = {
type Props = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
onChange: (val: number | null) => void;
placement?: Placement;
projectId: string;
value: number | null;
tabIndex?: number;
};
type ButtonProps = {
@ -280,6 +272,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>

View file

@ -217,6 +217,9 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={() => {
if (!multiple) closeDropdown();
}}
>
{({ selected }) => (
<>

View file

@ -1,26 +1,18 @@
import { Placement } from "@popperjs/core";
import { TButtonVariants } from "../types";
import { TDropdownProps } from "../types";
export type MemberDropdownProps = {
export type MemberDropdownProps = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
placeholder?: string;
placement?: Placement;
tabIndex?: number;
} & (
| {
multiple: false;
onChange: (val: string | null) => void;
value: string | null;
}
| {
multiple: true;
onChange: (val: string[]) => void;
value: string[];
}
);
| {
multiple: false;
onChange: (val: string | null) => void;
value: string | null;
}
| {
multiple: true;
onChange: (val: string[]) => void;
value: string[];
}
);

View file

@ -1,4 +1,4 @@
import { Fragment, useState } from "react";
import { Fragment, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
@ -13,6 +13,8 @@ import { Avatar } from "@plane/ui";
import { cn } from "helpers/common.helper";
// types
import { MemberDropdownProps } from "./types";
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((props) => {
const {
@ -28,9 +30,13 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
placeholder = "Members",
placement,
value,
tabIndex,
} = props;
// states
const [query, setQuery] = useState("");
const [isOpen, setIsOpen] = useState(false);
// refs
const dropdownRef = useRef<HTMLDivElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -78,13 +84,24 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
};
if (multiple) comboboxProps.multiple = true;
const openDropdown = () => {
setIsOpen(true);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
useOutsideClickDetector(dropdownRef, closeDropdown);
return (
<Combobox
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full flex-shrink-0", {
className,
})}
{...comboboxProps}
handleKeyDown={handleKeyDown}
>
<Combobox.Button as={Fragment}>
{button ? (
@ -186,6 +203,9 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={() => {
if (!multiple) closeDropdown();
}}
>
{({ selected }) => (
<>

View file

@ -2,7 +2,6 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useModule } from "hooks/store";
@ -14,21 +13,14 @@ import { DiceIcon } from "@plane/ui";
import { cn } from "helpers/common.helper";
// types
import { IModule } from "@plane/types";
import { TButtonVariants } from "./types";
import { TDropdownProps } from "./types";
type Props = {
type Props = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
onChange: (val: string | null) => void;
placement?: Placement;
projectId: string;
value: string | null;
tabIndex?: number;
};
type DropdownOptions =
@ -186,9 +178,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full flex-shrink-0", {
className,
})}
className={cn("h-full flex-shrink-0", className)}
value={value}
onChange={onChange}
disabled={disabled}
@ -291,6 +281,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>

View file

@ -1,7 +1,6 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
@ -12,23 +11,17 @@ import { PriorityIcon } from "@plane/ui";
import { cn } from "helpers/common.helper";
// types
import { TIssuePriorities } from "@plane/types";
import { TButtonVariants } from "./types";
import { TDropdownProps } from "./types";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
import { useTheme } from "next-themes";
type Props = {
type Props = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
highlightUrgent?: boolean;
onChange: (val: TIssuePriorities) => void;
placement?: Placement;
value: TIssuePriorities;
tabIndex?: number;
};
type ButtonProps = {
@ -236,43 +229,20 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
},
],
});
// next-themes
// TODO: remove this after new theming implementation
const { resolvedTheme } = useTheme();
const options = ISSUE_PRIORITIES.map((priority) => {
const priorityClasses = {
urgent: "bg-red-500/20 text-red-950 border-red-500",
high: "bg-orange-500/20 text-orange-950 border-orange-500",
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
none: "bg-custom-background-80 border-custom-border-300",
};
return {
value: priority.key,
query: priority.key,
content: (
<div className="flex items-center gap-2">
<div
className={cn("grid place-items-center border rounded p-0.5 flex-shrink-0", priorityClasses[priority.key], {
"bg-red-500 border-red-500": priority.key === "urgent" && highlightUrgent,
})}
>
<PriorityIcon
priority={priority.key}
size={14}
className={cn({
"text-white": priority.key === "urgent" && highlightUrgent,
// centre align the icons if text is hidden
"translate-x-[0.0625rem]": priority.key === "high",
"translate-x-0.5": priority.key === "medium",
"translate-x-1": priority.key === "low",
})}
/>
</div>
<span className="flex-grow truncate">{priority.title}</span>
</div>
),
};
});
const options = ISSUE_PRIORITIES.map((priority) => ({
value: priority.key,
query: priority.key,
content: (
<div className="flex items-center gap-2">
<PriorityIcon priority={priority.key} size={14} withContainer />
<span className="flex-grow truncate">{priority.title}</span>
</div>
),
}));
const filteredOptions =
query === "" ? options : options.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
@ -325,14 +295,18 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
{buttonVariant === "border-with-text" ? (
<BorderButton
priority={value}
className={buttonClassName}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
/>
) : buttonVariant === "border-without-text" ? (
<BorderButton
priority={value}
className={buttonClassName}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
hideText
@ -340,14 +314,18 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
) : buttonVariant === "background-with-text" ? (
<BackgroundButton
priority={value}
className={buttonClassName}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
/>
) : buttonVariant === "background-without-text" ? (
<BackgroundButton
priority={value}
className={buttonClassName}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
hideText
@ -355,14 +333,18 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
) : buttonVariant === "transparent-with-text" ? (
<TransparentButton
priority={value}
className={buttonClassName}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
/>
) : buttonVariant === "transparent-without-text" ? (
<TransparentButton
priority={value}
className={buttonClassName}
className={cn(buttonClassName, {
"text-white": resolvedTheme === "dark",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
hideText
@ -400,6 +382,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>

View file

@ -2,7 +2,6 @@ import { Fragment, ReactNode, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useProject } from "hooks/store";
@ -13,20 +12,13 @@ import { cn } from "helpers/common.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IProject } from "@plane/types";
import { TButtonVariants } from "./types";
import { TDropdownProps } from "./types";
type Props = {
type Props = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
onChange: (val: string) => void;
placement?: Placement;
value: string | null;
tabIndex?: number;
};
type ButtonProps = {
@ -166,9 +158,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full flex-shrink-0", {
className,
})}
className={cn("h-full flex-shrink-0", className)}
value={value}
onChange={onChange}
disabled={disabled}
@ -271,6 +261,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>

View file

@ -2,7 +2,6 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { usePopper } from "react-popper";
import { Placement } from "@popperjs/core";
import { Check, ChevronDown, Search } from "lucide-react";
// hooks
import { useApplication, useProjectState } from "hooks/store";
@ -14,21 +13,14 @@ import { StateGroupIcon } from "@plane/ui";
import { cn } from "helpers/common.helper";
// types
import { IState } from "@plane/types";
import { TButtonVariants } from "./types";
import { TDropdownProps } from "./types";
type Props = {
type Props = TDropdownProps & {
button?: ReactNode;
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
dropdownArrow?: boolean;
onChange: (val: string) => void;
placement?: Placement;
projectId: string;
value: string;
tabIndex?: number;
};
type ButtonProps = {
@ -159,9 +151,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full flex-shrink-0", {
className,
})}
className={cn("h-full flex-shrink-0", className)}
value={value}
onChange={onChange}
disabled={disabled}
@ -264,6 +254,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
onClick={closeDropdown}
>
{({ selected }) => (
<>

View file

@ -1,3 +1,5 @@
import { Placement } from "@popperjs/core";
export type TButtonVariants =
| "border-with-text"
| "border-without-text"
@ -5,3 +7,13 @@ export type TButtonVariants =
| "background-without-text"
| "transparent-with-text"
| "transparent-without-text";
export type TDropdownProps = {
buttonClassName?: string;
buttonContainerClassName?: string;
buttonVariant: TButtonVariants;
className?: string;
disabled?: boolean;
placement?: Placement;
tabIndex?: number;
};