[WEB-2316] chore: Render Tooltips and Drop downs in certain places on hover hover to improve rendering performance (#5456)

* render tooltips and dropdowns in certain places post hover to improve performance

* fix useEffect hooks
This commit is contained in:
rahulramesha
2024-08-29 21:07:49 +05:30
committed by GitHub
parent 33ab6029dc
commit 693085577d
14 changed files with 684 additions and 515 deletions

View File

@@ -0,0 +1,73 @@
import { Combobox } from "@headlessui/react";
import React, {
ElementType,
Fragment,
KeyboardEventHandler,
ReactNode,
Ref,
forwardRef,
useEffect,
useRef,
useState,
} from "react";
type Props = {
as?: ElementType | undefined;
ref?: Ref<HTMLElement> | undefined;
tabIndex?: number | undefined;
className?: string | undefined;
value?: string | string[] | null;
onChange?: (value: any) => void;
disabled?: boolean | undefined;
onKeyDown?: KeyboardEventHandler<HTMLDivElement> | undefined;
multiple?: boolean;
renderByDefault?: boolean;
button: ReactNode;
children: ReactNode;
};
const ComboDropDown = forwardRef((props: Props, ref) => {
const { button, renderByDefault = true, children, ...rest } = props;
const dropDownButtonRef = useRef<HTMLDivElement | null>(null);
const [shouldRender, setShouldRender] = useState(renderByDefault);
const onHover = () => {
setShouldRender(true);
};
useEffect(() => {
const element = dropDownButtonRef.current;
if (!element) return;
element.addEventListener("mouseenter", onHover);
return () => {
element?.removeEventListener("mouseenter", onHover);
};
}, [dropDownButtonRef, shouldRender]);
if (!shouldRender) {
return (
<div ref={dropDownButtonRef} className="h-full flex items-center">
{button}
</div>
);
}
return (
//@ts-ignore
<Combobox {...rest} ref={ref}>
<Combobox.Button as={Fragment}>{button}</Combobox.Button>
{children}
</Combobox>
);
});
const ComboOptions = Combobox.Options;
const ComboOption = Combobox.Option;
const ComboInput = Combobox.Input;
export { ComboDropDown, ComboOptions, ComboOption, ComboInput };

View File

@@ -2,3 +2,4 @@ export * from "./context-menu";
export * from "./custom-menu";
export * from "./custom-select";
export * from "./custom-search-select";
export * from "./combo-box";

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import { Tooltip2 } from "@blueprintjs/popover2";
// helpers
import { cn } from "../../helpers";
@@ -42,37 +42,67 @@ export const Tooltip: React.FC<ITooltipProps> = ({
openDelay = 200,
closeDelay,
isMobile = false,
}) => (
<Tooltip2
disabled={disabled}
hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay}
content={
<div
className={cn(
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
{
hidden: isMobile,
},
className
)}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
}) => {
const toolTipRef = useRef<HTMLDivElement | null>(null);
const [shouldRender, setShouldRender] = useState(false);
const onHover = () => {
setShouldRender(true);
};
useEffect(() => {
const element = toolTipRef.current;
if (!element) return;
element.addEventListener("mouseenter", onHover);
return () => {
element?.removeEventListener("mouseenter", onHover);
};
}, [toolTipRef, shouldRender]);
if (!shouldRender) {
return (
<div ref={toolTipRef} className="h-full flex items-center">
{children}
</div>
}
position={position}
renderTarget={({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isOpen: isTooltipOpen,
ref: eleReference,
...tooltipProps
}) =>
React.cloneElement(children, {
);
}
return (
<Tooltip2
disabled={disabled}
hoverOpenDelay={openDelay}
hoverCloseDelay={closeDelay}
content={
<div
className={cn(
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
{
hidden: isMobile,
},
className
)}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
</div>
}
position={position}
renderTarget={({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isOpen: isTooltipOpen,
ref: eleReference,
...tooltipProps,
...children.props,
})
}
/>
);
...tooltipProps
}) =>
React.cloneElement(children, {
ref: eleReference,
...tooltipProps,
...children.props,
})
}
/>
);
};

View File

@@ -1,11 +1,10 @@
"use client";
import { Fragment, ReactNode, useRef, useState } from "react";
import { ReactNode, useRef, useState } from "react";
import { observer } from "mobx-react";
import { ChevronDown } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { ContrastIcon } from "@plane/ui";
import { ComboDropDown, ContrastIcon } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -26,6 +25,7 @@ type Props = TDropdownProps & {
projectId: string | undefined;
value: string | null;
canRemoveCycle?: boolean;
renderByDefault?: boolean;
};
export const CycleDropdown: React.FC<Props> = observer((props) => {
@@ -48,6 +48,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
tabIndex,
value,
canRemoveCycle = true,
renderByDefault = true,
} = props;
// states
@@ -72,8 +73,57 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
handleClose();
};
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Cycle"
tooltipContent={selectedName ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (!!selectedName || !!placeholder) && (
<span className="max-w-40 flex-grow truncate">{selectedName ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -82,53 +132,9 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Cycle"
tooltipContent={selectedName ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (!!selectedName || !!placeholder) && (
<span className="max-w-40 flex-grow truncate">{selectedName ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && projectId && (
<CycleOptions
isOpen={isOpen}
@@ -138,6 +144,6 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
canRemoveCycle={canRemoveCycle}
/>
)}
</Combobox>
</ComboDropDown>
);
});

View File

@@ -7,7 +7,7 @@ import { usePopper } from "react-popper";
import { ArrowRight, CalendarDays } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { Button } from "@plane/ui";
import { Button, ComboDropDown } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper";
@@ -49,6 +49,7 @@ type Props = {
from: Date | undefined;
to: Date | undefined;
};
renderByDefault?: boolean;
};
export const DateRangeDropdown: React.FC<Props> = (props) => {
@@ -80,6 +81,7 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
@@ -131,8 +133,53 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
setDateRange(value);
}, [value]);
const comboButton = (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Date range"
tooltipContent={
<>
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
{" - "}
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
</>
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<span
className={cn("h-full flex items-center justify-center gap-1 rounded-sm flex-grow", buttonFromDateClassName)}
>
{!hideIcon.from && icon}
{dateRange.from ? renderFormattedDate(dateRange.from) : placeholder.from}
</span>
<ArrowRight className="h-3 w-3 flex-shrink-0" />
<span
className={cn("h-full flex items-center justify-center gap-1 rounded-sm flex-grow", buttonToDateClassName)}
>
{!hideIcon.to && icon}
{dateRange.to ? renderFormattedDate(dateRange.to) : placeholder.to}
</span>
</DropdownButton>
</button>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -142,58 +189,10 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
if (!isOpen) handleKeyDown(e);
} else handleKeyDown(e);
}}
button={comboButton}
disabled={disabled}
renderByDefault={renderByDefault}
>
<Combobox.Button as={React.Fragment}>
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Date range"
tooltipContent={
<>
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
{" - "}
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
</>
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<span
className={cn(
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
buttonFromDateClassName
)}
>
{!hideIcon.from && icon}
{dateRange.from ? renderFormattedDate(dateRange.from) : placeholder.from}
</span>
<ArrowRight className="h-3 w-3 flex-shrink-0" />
<span
className={cn(
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
buttonToDateClassName
)}
>
{!hideIcon.to && icon}
{dateRange.to ? renderFormattedDate(dateRange.to) : placeholder.to}
</span>
</DropdownButton>
</button>
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
@@ -250,6 +249,6 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
</div>
</Combobox.Options>
)}
</Combobox>
</ComboDropDown>
);
};

View File

@@ -4,6 +4,8 @@ import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { ComboDropDown } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, getDate } from "@/helpers/date-time.helper";
@@ -27,6 +29,7 @@ type Props = TDropdownProps & {
value: Date | string | null;
closeOnSelect?: boolean;
formatToken?: string;
renderByDefault?: boolean;
};
export const DateDropdown: React.FC<Props> = (props) => {
@@ -51,6 +54,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
tabIndex,
value,
formatToken,
renderByDefault = true,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
@@ -98,8 +102,48 @@ export const DateDropdown: React.FC<Props> = (props) => {
if (minDate) disabledDays.push({ before: minDate });
if (maxDate) disabledDays.push({ after: maxDate });
const comboButton = (
<button
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
ref={setReferenceElement}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={value ? renderFormattedDate(value, formatToken) : "None"}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && icon}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{value ? renderFormattedDate(value, formatToken) : placeholder}</span>
)}
{isClearable && !disabled && isDateSelected && (
<X
className={cn("h-2.5 w-2.5 flex-shrink-0", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onChange(null);
}}
/>
)}
</DropdownButton>
</button>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -109,49 +153,10 @@ export const DateDropdown: React.FC<Props> = (props) => {
if (!isOpen) handleKeyDown(e);
} else handleKeyDown(e);
}}
button={comboButton}
disabled={disabled}
renderByDefault={renderByDefault}
>
<Combobox.Button as={React.Fragment}>
<button
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
ref={setReferenceElement}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={value ? renderFormattedDate(value, formatToken) : "None"}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && icon}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">
{value ? renderFormattedDate(value, formatToken) : placeholder}
</span>
)}
{isClearable && !disabled && isDateSelected && (
<X
className={cn("h-2.5 w-2.5 flex-shrink-0", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onChange(null);
}}
/>
)}
</DropdownButton>
</button>
</Combobox.Button>
{isOpen &&
createPortal(
<Combobox.Options data-prevent-outside-click static>
@@ -176,6 +181,6 @@ export const DateDropdown: React.FC<Props> = (props) => {
</Combobox.Options>,
document.body
)}
</Combobox>
</ComboDropDown>
);
};

View File

@@ -1,9 +1,11 @@
import { Fragment, ReactNode, useRef, useState } from "react";
import { ReactNode, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { usePopper } from "react-popper";
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { ComboDropDown } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -27,6 +29,7 @@ type Props = TDropdownProps & {
onClose?: () => void;
projectId: string | undefined;
value: string | undefined | null;
renderByDefault?: boolean;
};
type DropdownOptions =
@@ -56,6 +59,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
} = props;
// states
const [query, setQuery] = useState("");
@@ -142,8 +146,54 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
handleClose();
};
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Estimate"
tooltipContent={selectedEstimate ? selectedEstimate?.value : placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedEstimate ? selectedEstimate?.value : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -152,50 +202,9 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Estimate"
tooltipContent={selectedEstimate ? selectedEstimate?.value : placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
{(selectedEstimate || placeholder) && BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedEstimate ? selectedEstimate?.value : placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
@@ -263,6 +272,6 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
</div>
</Combobox.Options>
)}
</Combobox>
</ComboDropDown>
);
});

View File

@@ -1,8 +1,8 @@
import { Fragment, useRef, useState } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import { ChevronDown, LucideIcon } from "lucide-react";
// headless ui
import { Combobox } from "@headlessui/react";
// ui
import { ComboDropDown } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -21,6 +21,7 @@ type Props = {
projectId?: string;
icon?: LucideIcon;
onClose?: () => void;
renderByDefault?: boolean;
} & MemberDropdownProps;
export const MemberDropdown: React.FC<Props> = observer((props) => {
@@ -46,6 +47,7 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
tabIndex,
value,
icon,
renderByDefault = true,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
@@ -96,61 +98,66 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
}
};
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={cn("text-xs", buttonClassName)}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={tooltipContent ?? `${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} icon={icon} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate leading-5">
{getDisplayName(value, showUserDetails, placeholder)}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onChange={dropdownOnChange}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={cn("text-xs", buttonClassName)}
isActive={isOpen}
tooltipHeading={placeholder}
tooltipContent={tooltipContent ?? `${value?.length ?? 0} assignee${value?.length !== 1 ? "s" : ""}`}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && <ButtonAvatars showTooltip={showTooltip} userIds={value} icon={icon} />}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate leading-5">
{getDisplayName(value, showUserDetails, placeholder)}
</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<MemberOptions
isOpen={isOpen}
@@ -159,6 +166,6 @@ export const MemberDropdown: React.FC<Props> = observer((props) => {
referenceElement={referenceElement}
/>
)}
</Combobox>
</ComboDropDown>
);
});

View File

@@ -1,11 +1,10 @@
"use client";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { ChevronDown, X } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { DiceIcon, Tooltip } from "@plane/ui";
import { ComboDropDown, DiceIcon, Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -27,6 +26,7 @@ type Props = TDropdownProps & {
projectId: string | undefined;
showCount?: boolean;
onClose?: () => void;
renderByDefault?: boolean;
} & (
| {
multiple: false;
@@ -170,6 +170,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
} = props;
// states
const [isOpen, setIsOpen] = useState(false);
@@ -207,73 +208,78 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
}
}, [isOpen]);
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Module"
tooltipContent={
Array.isArray(value)
? `${value
.map((moduleId) => getModuleNameById(moduleId))
.toString()
.replaceAll(",", ", ")}`
: ""
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<ButtonContent
disabled={disabled}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
placeholder={placeholder}
showCount={showCount}
showTooltip={showTooltip}
value={value}
onChange={onChange as any}
/>
</DropdownButton>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
className={cn("h-full", className)}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
{...comboboxProps}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Module"
tooltipContent={
Array.isArray(value)
? `${value
.map((moduleId) => getModuleNameById(moduleId))
.toString()
.replaceAll(",", ", ")}`
: ""
}
showTooltip={showTooltip}
variant={buttonVariant}
>
<ButtonContent
disabled={disabled}
dropdownArrow={dropdownArrow}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
placeholder={placeholder}
showCount={showCount}
showTooltip={showTooltip}
value={value}
onChange={onChange as any}
/>
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && projectId && (
<ModuleOptions
isOpen={isOpen}
@@ -283,6 +289,6 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
multiple={multiple}
/>
)}
</Combobox>
</ComboDropDown>
);
});

View File

@@ -8,7 +8,7 @@ import { Combobox } from "@headlessui/react";
// types
import { TIssuePriorities } from "@plane/types";
// ui
import { PriorityIcon, Tooltip } from "@plane/ui";
import { ComboDropDown, PriorityIcon, Tooltip } from "@plane/ui";
// constants
import { ISSUE_PRIORITIES } from "@/constants/issue";
// helpers
@@ -29,6 +29,7 @@ type Props = TDropdownProps & {
onChange: (val: TIssuePriorities) => void;
onClose?: () => void;
value: TIssuePriorities | undefined | null;
renderByDefault?: boolean;
};
type ButtonProps = {
@@ -305,6 +306,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
showTooltip = false,
tabIndex,
value = "none",
renderByDefault = true,
} = props;
// states
const [query, setQuery] = useState("");
@@ -363,11 +365,54 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant)
? BorderButton
: BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant)
? BackgroundButton
: TransparentButton;
? BackgroundButton
: TransparentButton;
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<ButtonToRender
priority={value ?? undefined}
className={cn(buttonClassName, {
"text-custom-text-200": resolvedTheme?.includes("dark") || resolvedTheme === "custom",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
showTooltip={showTooltip}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
/>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -382,47 +427,9 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<ButtonToRender
priority={value ?? undefined}
className={cn(buttonClassName, {
"text-custom-text-200": resolvedTheme?.includes("dark") || resolvedTheme === "custom",
})}
highlightUrgent={highlightUrgent}
dropdownArrow={dropdownArrow && !disabled}
dropdownArrowClassName={dropdownArrowClassName}
hideIcon={hideIcon}
placeholder={placeholder}
showTooltip={showTooltip}
hideText={BUTTON_VARIANTS_WITHOUT_TEXT.includes(buttonVariant)}
/>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
@@ -471,6 +478,6 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
</div>
</Combobox.Options>
)}
</Combobox>
</ComboDropDown>
);
};

View File

@@ -5,6 +5,8 @@ import { Check, ChevronDown, Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// types
import { IProject } from "@plane/types";
// ui
import { ComboDropDown } from "@plane/ui";
// components
import { Logo } from "@/components/common";
// helpers
@@ -27,6 +29,7 @@ type Props = TDropdownProps & {
onClose?: () => void;
renderCondition?: (project: IProject) => boolean;
value: string | null;
renderByDefault?: boolean;
};
export const ProjectDropdown: React.FC<Props> = observer((props) => {
@@ -48,6 +51,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
} = props;
// states
const [query, setQuery] = useState("");
@@ -112,8 +116,58 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
handleClose();
};
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Project"
tooltipContent={selectedProject?.name ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && selectedProject && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={selectedProject.logo_props} size={12} />
</span>
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate max-w-40">{selectedProject?.name ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -122,54 +176,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="Project"
tooltipContent={selectedProject?.name ?? placeholder}
showTooltip={showTooltip}
variant={buttonVariant}
>
{!hideIcon && selectedProject && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={selectedProject.logo_props} size={12} />
</span>
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate max-w-40">{selectedProject?.name ?? placeholder}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
@@ -225,6 +234,6 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
</div>
</Combobox.Options>
)}
</Combobox>
</ComboDropDown>
);
});

View File

@@ -7,7 +7,7 @@ import { usePopper } from "react-popper";
import { Check, ChevronDown, Search } from "lucide-react";
import { Combobox } from "@headlessui/react";
// ui
import { Spinner, StateGroupIcon } from "@plane/ui";
import { ComboDropDown, Spinner, StateGroupIcon } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@@ -29,6 +29,7 @@ type Props = TDropdownProps & {
projectId: string | undefined;
showDefaultState?: boolean;
value: string | undefined | null;
renderByDefault?: boolean;
};
export const StateDropdown: React.FC<Props> = observer((props) => {
@@ -50,6 +51,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
showTooltip = false,
tabIndex,
value,
renderByDefault = true,
} = props;
// states
const [query, setQuery] = useState("");
@@ -125,8 +127,66 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
handleClose();
};
const comboButton = (
<>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="State"
tooltipContent={selectedState?.name ?? "State"}
showTooltip={showTooltip}
variant={buttonVariant}
>
{stateLoader ? (
<Spinner className="h-3.5 w-3.5" />
) : (
<>
{!hideIcon && (
<StateGroupIcon
stateGroup={selectedState?.group ?? "backlog"}
color={selectedState?.color ?? "rgba(var(--color-text-300))"}
className="h-3 w-3 flex-shrink-0"
/>
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
)}
{dropdownArrow && (
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
)}
</>
)}
</DropdownButton>
</button>
)}
</>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
tabIndex={tabIndex}
@@ -135,65 +195,9 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
onChange={dropdownOnChange}
disabled={disabled}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
>
<Combobox.Button as={Fragment}>
{button ? (
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
</button>
) : (
<button
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
},
buttonContainerClassName
)}
onClick={handleOnClick}
>
<DropdownButton
className={buttonClassName}
isActive={isOpen}
tooltipHeading="State"
tooltipContent={selectedState?.name ?? "State"}
showTooltip={showTooltip}
variant={buttonVariant}
>
{stateLoader ? (
<Spinner className="h-3.5 w-3.5" />
) : (
<>
{!hideIcon && (
<StateGroupIcon
stateGroup={selectedState?.group ?? "backlog"}
color={selectedState?.color ?? "rgba(var(--color-text-300))"}
className="h-3 w-3 flex-shrink-0"
/>
)}
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
<span className="flex-grow truncate">{selectedState?.name ?? "State"}</span>
)}
{dropdownArrow && (
<ChevronDown
className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)}
aria-hidden="true"
/>
)}
</>
)}
</DropdownButton>
</button>
)}
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
@@ -246,6 +250,6 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
</div>
</Combobox.Options>
)}
</Combobox>
</ComboDropDown>
);
});

View File

@@ -284,6 +284,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
renderByDefault={isMobile}
showTooltip
/>
</div>
@@ -298,6 +299,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
disabled={isReadOnly}
buttonVariant="border-without-text"
buttonClassName="border"
renderByDefault={isMobile}
showTooltip
/>
</div>
@@ -312,6 +314,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
defaultOptions={defaultLabelOptions}
onChange={handleLabel}
disabled={isReadOnly}
renderByDefault={isMobile}
hideDropdownArrow
/>
</div>
@@ -328,6 +331,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
disabled={isReadOnly}
renderByDefault={isMobile}
showTooltip
/>
</div>
@@ -346,6 +350,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""}
clearIconClassName="!text-custom-text-100"
disabled={isReadOnly}
renderByDefault={isMobile}
showTooltip
/>
</div>
@@ -365,6 +370,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
showTooltip={issue?.assignee_ids?.length === 0}
placeholder="Assignees"
tooltipContent=""
renderByDefault={isMobile}
/>
</div>
</WithDisplayPropertiesHOC>
@@ -379,6 +385,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
value={issue?.module_ids ?? []}
onChange={handleModule}
disabled={isReadOnly}
renderByDefault={isMobile}
multiple
buttonVariant="border-with-text"
showCount
@@ -399,6 +406,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
onChange={handleCycle}
disabled={isReadOnly}
buttonVariant="border-with-text"
renderByDefault={isMobile}
showTooltip
/>
</div>
@@ -415,6 +423,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
renderByDefault={isMobile}
showTooltip
/>
</div>

View File

@@ -10,7 +10,7 @@ import { Combobox } from "@headlessui/react";
// types
import { IIssueLabel } from "@plane/types";
// ui
import { Tooltip } from "@plane/ui";
import { ComboDropDown, Tooltip } from "@plane/ui";
// hooks
import { useLabel } from "@/hooks/store";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
@@ -32,6 +32,7 @@ export interface IIssuePropertyLabels {
noLabelBorder?: boolean;
placeholderText?: string;
onClose?: () => void;
renderByDefault?: boolean;
}
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
@@ -50,6 +51,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
maxRender = 2,
noLabelBorder = false,
placeholderText,
renderByDefault = true,
} = props;
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
@@ -217,8 +219,26 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
</div>
);
const comboButton = (
<button
ref={setReferenceElement}
type="button"
className={`clickable flex w-full items-center justify-between gap-1 text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: value.length <= maxRender
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={handleOnClick}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
);
return (
<Combobox
<ComboDropDown
as="div"
ref={dropdownRef}
className={`w-auto max-w-full flex-shrink-0 text-left ${className}`}
@@ -226,26 +246,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
onChange={onChange}
disabled={disabled}
onKeyDown={handleKeyDown}
button={comboButton}
renderByDefault={renderByDefault}
multiple
>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`clickable flex w-full items-center justify-between gap-1 text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: value.length <= maxRender
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={handleOnClick}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
@@ -307,6 +311,6 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
</div>
</Combobox.Options>
)}
</Combobox>
</ComboDropDown>
);
});