diff --git a/packages/ui/src/dropdowns/combo-box.tsx b/packages/ui/src/dropdowns/combo-box.tsx new file mode 100644 index 000000000..1ee96480e --- /dev/null +++ b/packages/ui/src/dropdowns/combo-box.tsx @@ -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 | undefined; + tabIndex?: number | undefined; + className?: string | undefined; + value?: string | string[] | null; + onChange?: (value: any) => void; + disabled?: boolean | undefined; + onKeyDown?: KeyboardEventHandler | 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(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 ( +
+ {button} +
+ ); + } + + return ( + //@ts-ignore + + {button} + {children} + + ); +}); + +const ComboOptions = Combobox.Options; +const ComboOption = Combobox.Option; +const ComboInput = Combobox.Input; + +export { ComboDropDown, ComboOptions, ComboOption, ComboInput }; diff --git a/packages/ui/src/dropdowns/index.ts b/packages/ui/src/dropdowns/index.ts index d77eac129..617778da2 100644 --- a/packages/ui/src/dropdowns/index.ts +++ b/packages/ui/src/dropdowns/index.ts @@ -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"; diff --git a/packages/ui/src/tooltip/tooltip.tsx b/packages/ui/src/tooltip/tooltip.tsx index 30fa5ba5b..26e0b2589 100644 --- a/packages/ui/src/tooltip/tooltip.tsx +++ b/packages/ui/src/tooltip/tooltip.tsx @@ -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 = ({ openDelay = 200, closeDelay, isMobile = false, -}) => ( - - {tooltipHeading &&
{tooltipHeading}
} - {tooltipContent} +}) => { + const toolTipRef = useRef(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 ( +
+ {children}
- } - position={position} - renderTarget={({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - isOpen: isTooltipOpen, - ref: eleReference, - ...tooltipProps - }) => - React.cloneElement(children, { + ); + } + + return ( + + {tooltipHeading &&
{tooltipHeading}
} + {tooltipContent} + + } + 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, + }) + } + /> + ); +}; diff --git a/web/core/components/dropdowns/cycle/index.tsx b/web/core/components/dropdowns/cycle/index.tsx index abf10da81..682767d3b 100644 --- a/web/core/components/dropdowns/cycle/index.tsx +++ b/web/core/components/dropdowns/cycle/index.tsx @@ -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 = observer((props) => { @@ -48,6 +48,7 @@ export const CycleDropdown: React.FC = observer((props) => { tabIndex, value, canRemoveCycle = true, + renderByDefault = true, } = props; // states @@ -72,8 +73,57 @@ export const CycleDropdown: React.FC = observer((props) => { handleClose(); }; + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + return ( - = observer((props) => { onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} + button={comboButton} + renderByDefault={renderByDefault} > - - {button ? ( - - ) : ( - - )} - {isOpen && projectId && ( = observer((props) => { canRemoveCycle={canRemoveCycle} /> )} - + ); }); diff --git a/web/core/components/dropdowns/date-range.tsx b/web/core/components/dropdowns/date-range.tsx index 1b9d8ad20..1681d195f 100644 --- a/web/core/components/dropdowns/date-range.tsx +++ b/web/core/components/dropdowns/date-range.tsx @@ -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) => { @@ -80,6 +81,7 @@ export const DateRangeDropdown: React.FC = (props) => { showTooltip = false, tabIndex, value, + renderByDefault = true, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -131,8 +133,53 @@ export const DateRangeDropdown: React.FC = (props) => { setDateRange(value); }, [value]); + const comboButton = ( + + ); + return ( - = (props) => { if (!isOpen) handleKeyDown(e); } else handleKeyDown(e); }} + button={comboButton} disabled={disabled} + renderByDefault={renderByDefault} > - - - {isOpen && (
= (props) => {
)} -
+ ); }; diff --git a/web/core/components/dropdowns/date.tsx b/web/core/components/dropdowns/date.tsx index 1977d0662..20714ec80 100644 --- a/web/core/components/dropdowns/date.tsx +++ b/web/core/components/dropdowns/date.tsx @@ -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) => { @@ -51,6 +54,7 @@ export const DateDropdown: React.FC = (props) => { tabIndex, value, formatToken, + renderByDefault = true, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -98,8 +102,48 @@ export const DateDropdown: React.FC = (props) => { if (minDate) disabledDays.push({ before: minDate }); if (maxDate) disabledDays.push({ after: maxDate }); + const comboButton = ( + + ); + return ( - = (props) => { if (!isOpen) handleKeyDown(e); } else handleKeyDown(e); }} + button={comboButton} disabled={disabled} + renderByDefault={renderByDefault} > - - - {isOpen && createPortal( @@ -176,6 +181,6 @@ export const DateDropdown: React.FC = (props) => { , document.body )} - + ); }; diff --git a/web/core/components/dropdowns/estimate.tsx b/web/core/components/dropdowns/estimate.tsx index 8b14a5c73..a655e064f 100644 --- a/web/core/components/dropdowns/estimate.tsx +++ b/web/core/components/dropdowns/estimate.tsx @@ -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 = observer((props) => { showTooltip = false, tabIndex, value, + renderByDefault = true, } = props; // states const [query, setQuery] = useState(""); @@ -142,8 +146,54 @@ export const EstimateDropdown: React.FC = observer((props) => { handleClose(); }; + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + return ( - = observer((props) => { onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} + button={comboButton} + renderByDefault={renderByDefault} > - - {button ? ( - - ) : ( - - )} - {isOpen && (
= observer((props) => {
)} -
+ ); }); diff --git a/web/core/components/dropdowns/member/index.tsx b/web/core/components/dropdowns/member/index.tsx index 64dc56635..0559516f4 100644 --- a/web/core/components/dropdowns/member/index.tsx +++ b/web/core/components/dropdowns/member/index.tsx @@ -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 = observer((props) => { @@ -46,6 +47,7 @@ export const MemberDropdown: React.FC = observer((props) => { tabIndex, value, icon, + renderByDefault = true, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -96,61 +98,66 @@ export const MemberDropdown: React.FC = observer((props) => { } }; + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + return ( - - - {button ? ( - - ) : ( - - )} - {isOpen && ( = observer((props) => { referenceElement={referenceElement} /> )} - + ); }); diff --git a/web/core/components/dropdowns/module/index.tsx b/web/core/components/dropdowns/module/index.tsx index b26a5e0ed..d436c9bd3 100644 --- a/web/core/components/dropdowns/module/index.tsx +++ b/web/core/components/dropdowns/module/index.tsx @@ -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 = observer((props) => { showTooltip = false, tabIndex, value, + renderByDefault = true, } = props; // states const [isOpen, setIsOpen] = useState(false); @@ -207,73 +208,78 @@ export const ModuleDropdown: React.FC = observer((props) => { } }, [isOpen]); + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + return ( - - - {button ? ( - - ) : ( - - )} - {isOpen && projectId && ( = observer((props) => { multiple={multiple} /> )} - + ); }); diff --git a/web/core/components/dropdowns/priority.tsx b/web/core/components/dropdowns/priority.tsx index b58b3acdd..b5485a659 100644 --- a/web/core/components/dropdowns/priority.tsx +++ b/web/core/components/dropdowns/priority.tsx @@ -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) => { showTooltip = false, tabIndex, value = "none", + renderByDefault = true, } = props; // states const [query, setQuery] = useState(""); @@ -363,11 +365,54 @@ export const PriorityDropdown: React.FC = (props) => { const ButtonToRender = BORDER_BUTTON_VARIANTS.includes(buttonVariant) ? BorderButton : BACKGROUND_BUTTON_VARIANTS.includes(buttonVariant) - ? BackgroundButton - : TransparentButton; + ? BackgroundButton + : TransparentButton; + + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); return ( - = (props) => { onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} + button={comboButton} + renderByDefault={renderByDefault} > - - {button ? ( - - ) : ( - - )} - {isOpen && (
= (props) => {
)} -
+ ); }; diff --git a/web/core/components/dropdowns/project.tsx b/web/core/components/dropdowns/project.tsx index ea7dea549..17180afdb 100644 --- a/web/core/components/dropdowns/project.tsx +++ b/web/core/components/dropdowns/project.tsx @@ -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 = observer((props) => { @@ -48,6 +51,7 @@ export const ProjectDropdown: React.FC = observer((props) => { showTooltip = false, tabIndex, value, + renderByDefault = true, } = props; // states const [query, setQuery] = useState(""); @@ -112,8 +116,58 @@ export const ProjectDropdown: React.FC = observer((props) => { handleClose(); }; + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + return ( - = observer((props) => { onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} + button={comboButton} + renderByDefault={renderByDefault} > - - {button ? ( - - ) : ( - - )} - {isOpen && (
= observer((props) => {
)} -
+ ); }); diff --git a/web/core/components/dropdowns/state.tsx b/web/core/components/dropdowns/state.tsx index 499e7aa7f..ed72033a2 100644 --- a/web/core/components/dropdowns/state.tsx +++ b/web/core/components/dropdowns/state.tsx @@ -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 = observer((props) => { @@ -50,6 +51,7 @@ export const StateDropdown: React.FC = observer((props) => { showTooltip = false, tabIndex, value, + renderByDefault = true, } = props; // states const [query, setQuery] = useState(""); @@ -125,8 +127,66 @@ export const StateDropdown: React.FC = observer((props) => { handleClose(); }; + const comboButton = ( + <> + {button ? ( + + ) : ( + + )} + + ); + return ( - = observer((props) => { onChange={dropdownOnChange} disabled={disabled} onKeyDown={handleKeyDown} + button={comboButton} + renderByDefault={renderByDefault} > - - {button ? ( - - ) : ( - - )} - {isOpen && (
= observer((props) => {
)} -
+ ); }); diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index baa7dfc09..c5666fb70 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -284,6 +284,7 @@ export const IssueProperties: React.FC = observer((props) => { projectId={issue.project_id} disabled={isReadOnly} buttonVariant="border-with-text" + renderByDefault={isMobile} showTooltip /> @@ -298,6 +299,7 @@ export const IssueProperties: React.FC = observer((props) => { disabled={isReadOnly} buttonVariant="border-without-text" buttonClassName="border" + renderByDefault={isMobile} showTooltip /> @@ -312,6 +314,7 @@ export const IssueProperties: React.FC = observer((props) => { defaultOptions={defaultLabelOptions} onChange={handleLabel} disabled={isReadOnly} + renderByDefault={isMobile} hideDropdownArrow /> @@ -328,6 +331,7 @@ export const IssueProperties: React.FC = observer((props) => { icon={} buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} disabled={isReadOnly} + renderByDefault={isMobile} showTooltip /> @@ -346,6 +350,7 @@ export const IssueProperties: React.FC = observer((props) => { buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} clearIconClassName="!text-custom-text-100" disabled={isReadOnly} + renderByDefault={isMobile} showTooltip /> @@ -365,6 +370,7 @@ export const IssueProperties: React.FC = observer((props) => { showTooltip={issue?.assignee_ids?.length === 0} placeholder="Assignees" tooltipContent="" + renderByDefault={isMobile} /> @@ -379,6 +385,7 @@ export const IssueProperties: React.FC = 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 = observer((props) => { onChange={handleCycle} disabled={isReadOnly} buttonVariant="border-with-text" + renderByDefault={isMobile} showTooltip /> @@ -415,6 +423,7 @@ export const IssueProperties: React.FC = observer((props) => { projectId={issue.project_id} disabled={isReadOnly} buttonVariant="border-with-text" + renderByDefault={isMobile} showTooltip /> diff --git a/web/core/components/issues/issue-layouts/properties/labels.tsx b/web/core/components/issues/issue-layouts/properties/labels.tsx index 403eab3d8..f377e2413 100644 --- a/web/core/components/issues/issue-layouts/properties/labels.tsx +++ b/web/core/components/issues/issue-layouts/properties/labels.tsx @@ -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 = observer((props) => { @@ -50,6 +51,7 @@ export const IssuePropertyLabels: React.FC = observer((pro maxRender = 2, noLabelBorder = false, placeholderText, + renderByDefault = true, } = props; // router const { workspaceSlug: routerWorkspaceSlug } = useParams(); @@ -217,8 +219,26 @@ export const IssuePropertyLabels: React.FC = observer((pro ); + const comboButton = ( + + ); + return ( - = observer((pro onChange={onChange} disabled={disabled} onKeyDown={handleKeyDown} + button={comboButton} + renderByDefault={renderByDefault} multiple > - - - - {isOpen && (
= observer((pro
)} -
+ ); });