АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: канон составных detail-menu и customButton-trigger
This commit is contained in:
parent
c86fc16cdf
commit
082948a69c
|
|
@ -8,10 +8,12 @@ import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import { PlusIcon } from "@plane/propel/icons";
|
import { PlusIcon } from "@plane/propel/icons";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { TIssueServiceType } from "@plane/types";
|
import type { TIssueServiceType } from "@plane/types";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import type { TContextMenuItem } from "@plane/ui";
|
||||||
|
import { ActionDropdown } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
// Plane-web
|
// Plane-web
|
||||||
|
|
@ -40,33 +42,32 @@ export const RelationActionButton = observer(function RelationActionButton(props
|
||||||
};
|
};
|
||||||
|
|
||||||
// button element
|
// button element
|
||||||
const customButtonElement = customButton ? <>{customButton}</> : <PlusIcon className="h-4 w-4" />;
|
const items: TContextMenuItem[] = Object.values(ISSUE_RELATION_OPTIONS).flatMap((item, index) =>
|
||||||
|
item
|
||||||
return (
|
? [
|
||||||
<CustomMenu
|
{
|
||||||
customButton={customButtonElement}
|
key: `${item.key}-${index}`,
|
||||||
placement="bottom-start"
|
title: t(item.i18n_label),
|
||||||
disabled={disabled}
|
action: () => handleOnClick(item.key),
|
||||||
maxHeight="lg"
|
customContent: (
|
||||||
closeOnSelect
|
|
||||||
>
|
|
||||||
{Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => {
|
|
||||||
if (!item) return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={index}
|
|
||||||
onClick={() => {
|
|
||||||
handleOnClick(item.key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{item.icon(12)}
|
{item.icon(12)}
|
||||||
<span>{t(item.i18n_label)}</span>
|
<span>{t(item.i18n_label)}</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
);
|
);
|
||||||
})}
|
|
||||||
</CustomMenu>
|
return (
|
||||||
|
<ActionDropdown
|
||||||
|
items={items}
|
||||||
|
button={customButton ?? <PlusIcon className="h-4 w-4" />}
|
||||||
|
buttonAsChild={!!customButton}
|
||||||
|
buttonClassName={!customButton ? getIconButtonStyling("ghost", "sm") : undefined}
|
||||||
|
placement="bottom-start"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import { PlusIcon, WorkItemsIcon } from "@plane/propel/icons";
|
import { PlusIcon, WorkItemsIcon } from "@plane/propel/icons";
|
||||||
import type { TIssue, TIssueServiceType } from "@plane/types";
|
import type { TIssue, TIssueServiceType } from "@plane/types";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import type { TContextMenuItem } from "@plane/ui";
|
||||||
|
import { ActionDropdown } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
|
|
||||||
|
|
@ -66,37 +68,29 @@ export const SubIssuesActionButton = observer(function SubIssuesActionButton(pro
|
||||||
};
|
};
|
||||||
|
|
||||||
// options
|
// options
|
||||||
const optionItems = [
|
const optionItems: TContextMenuItem[] = [
|
||||||
{
|
{
|
||||||
i18n_label: "common.create_new",
|
key: "create-new",
|
||||||
icon: <PlusIcon className="h-3 w-3" />,
|
title: t("common.create_new"),
|
||||||
onClick: handleCreateNew,
|
icon: PlusIcon,
|
||||||
|
action: handleCreateNew,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
i18n_label: "common.add_existing",
|
key: "add-existing",
|
||||||
icon: <WorkItemsIcon className="h-3 w-3" />,
|
title: t("common.add_existing"),
|
||||||
onClick: handleAddExisting,
|
icon: WorkItemsIcon,
|
||||||
|
action: handleAddExisting,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// button element
|
|
||||||
const customButtonElement = customButton ? <>{customButton}</> : <PlusIcon className="h-4 w-4" />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu customButton={customButtonElement} placement="bottom-start" disabled={disabled} closeOnSelect>
|
<ActionDropdown
|
||||||
{optionItems.map((item, index) => (
|
items={optionItems}
|
||||||
<CustomMenu.MenuItem
|
button={customButton ?? <PlusIcon className="h-4 w-4" />}
|
||||||
key={index}
|
buttonAsChild={!!customButton}
|
||||||
onClick={() => {
|
buttonClassName={!customButton ? getIconButtonStyling("ghost", "sm") : undefined}
|
||||||
item.onClick();
|
placement="bottom-start"
|
||||||
}}
|
disabled={disabled}
|
||||||
>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{item.icon}
|
|
||||||
<span>{t(item.i18n_label)}</span>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,22 @@ import React from "react";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "title"> & {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
compactView?: boolean;
|
compactView?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function IssueDetailWidgetButton(props: Props) {
|
export const IssueDetailWidgetButton = React.forwardRef<HTMLButtonElement, Props>(function IssueDetailWidgetButton(
|
||||||
const { icon, title, disabled = false, compactView = false } = props;
|
props,
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const { icon, title, disabled = false, compactView = false, className, ...buttonProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
ref={ref}
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size="xl"
|
size="xl"
|
||||||
|
|
@ -27,11 +32,13 @@ export function IssueDetailWidgetButton(props: Props) {
|
||||||
"border-transparent shadow-none focus-visible:outline-none",
|
"border-transparent shadow-none focus-visible:outline-none",
|
||||||
compactView
|
compactView
|
||||||
? "h-10 rounded-[18px] bg-layer-2/80 px-4 backdrop-blur-xl hover:bg-layer-2-active"
|
? "h-10 rounded-[18px] bg-layer-2/80 px-4 backdrop-blur-xl hover:bg-layer-2-active"
|
||||||
: "rounded-md"
|
: "rounded-md",
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
{icon && icon}
|
{icon && icon}
|
||||||
<span className="text-body-xs-medium">{title}</span>
|
<span className="text-body-xs-medium">{title}</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,11 @@ import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { MinusCircle } from "lucide-react";
|
import { MinusCircle } from "lucide-react";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { getIconButtonStyling } from "@plane/propel/icon-button";
|
||||||
import type { TIssue } from "@plane/types";
|
import type { TIssue } from "@plane/types";
|
||||||
// component
|
// component
|
||||||
// ui
|
// ui
|
||||||
import { ControlLink, CustomMenu } from "@plane/ui";
|
import { ActionDropdown, ControlLink } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { generateWorkItemLink } from "@plane/utils";
|
import { generateWorkItemLink } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -92,19 +93,38 @@ export const IssueParentDetail = observer(function IssueParentDetail(props: TIss
|
||||||
</div>
|
</div>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
|
|
||||||
<CustomMenu ellipsis optionsClassName="p-1.5">
|
<ActionDropdown
|
||||||
<div className="border-b border-strong text-11 font-medium text-secondary">{t("issue.sibling.label")}</div>
|
items={[]}
|
||||||
|
buttonClassName={getIconButtonStyling("ghost", "sm")}
|
||||||
|
placement="bottom-end"
|
||||||
|
menuClassName="p-1.5"
|
||||||
|
menuContent={({ closeDropdown }) => (
|
||||||
|
<div className="min-w-[12rem]">
|
||||||
|
<div className="border-b border-strong px-2 pb-2 text-11 font-medium text-secondary">
|
||||||
|
{t("issue.sibling.label")}
|
||||||
|
</div>
|
||||||
|
|
||||||
<IssueParentSiblings workspaceSlug={workspaceSlug} currentIssue={issue} parentIssue={parentIssue} />
|
<IssueParentSiblings
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
currentIssue={issue}
|
||||||
|
parentIssue={parentIssue}
|
||||||
|
/>
|
||||||
|
|
||||||
<CustomMenu.MenuItem
|
<button
|
||||||
onClick={() => issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })}
|
type="button"
|
||||||
className="flex items-center gap-2 py-2 text-danger-primary"
|
onClick={() => {
|
||||||
|
issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null });
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-danger-primary transition-colors hover:bg-white/6"
|
||||||
>
|
>
|
||||||
<MinusCircle className="h-4 w-4" />
|
<MinusCircle className="h-4 w-4" />
|
||||||
<span>{t("issue.remove.parent.label")}</span>
|
<span>{t("issue.remove.parent.label")}</span>
|
||||||
</CustomMenu.MenuItem>
|
</button>
|
||||||
</CustomMenu>
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// ui
|
|
||||||
import { CustomMenu } from "@plane/ui";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { generateWorkItemLink } from "@plane/utils";
|
import { generateWorkItemLink } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -16,12 +14,13 @@ import { useProject } from "@/hooks/store/use-project";
|
||||||
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
||||||
|
|
||||||
type TIssueParentSiblingItem = {
|
type TIssueParentSiblingItem = {
|
||||||
|
closeDropdown?: () => void;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueParentSiblingItem = observer(function IssueParentSiblingItem(props: TIssueParentSiblingItem) {
|
export const IssueParentSiblingItem = observer(function IssueParentSiblingItem(props: TIssueParentSiblingItem) {
|
||||||
const { workspaceSlug, issueId } = props;
|
const { closeDropdown, workspaceSlug, issueId } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const {
|
const {
|
||||||
|
|
@ -43,12 +42,14 @@ export const IssueParentSiblingItem = observer(function IssueParentSiblingItem(p
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<button
|
||||||
<CustomMenu.MenuItem
|
type="button"
|
||||||
key={issueDetail.id}
|
className="flex w-full items-center gap-2 rounded-[0.9rem] px-2 py-2 text-left text-secondary transition-colors hover:bg-white/6"
|
||||||
onClick={() => window.open(workItemLink, "_blank", "noopener,noreferrer")}
|
onClick={() => {
|
||||||
|
closeDropdown?.();
|
||||||
|
window.open(workItemLink, "_blank", "noopener,noreferrer");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 py-0.5">
|
|
||||||
{issueDetail.project_id && projectDetails?.identifier && (
|
{issueDetail.project_id && projectDetails?.identifier && (
|
||||||
<IssueIdentifier
|
<IssueIdentifier
|
||||||
projectId={issueDetail.project_id}
|
projectId={issueDetail.project_id}
|
||||||
|
|
@ -58,8 +59,6 @@ export const IssueParentSiblingItem = observer(function IssueParentSiblingItem(p
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,14 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
import { IssueParentSiblingItem } from "./sibling-item";
|
import { IssueParentSiblingItem } from "./sibling-item";
|
||||||
|
|
||||||
export type TIssueParentSiblings = {
|
export type TIssueParentSiblings = {
|
||||||
|
closeDropdown?: () => void;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
currentIssue: TIssue;
|
currentIssue: TIssue;
|
||||||
parentIssue: TIssue;
|
parentIssue: TIssue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueParentSiblings = observer(function IssueParentSiblings(props: TIssueParentSiblings) {
|
export const IssueParentSiblings = observer(function IssueParentSiblings(props: TIssueParentSiblings) {
|
||||||
const { workspaceSlug, currentIssue, parentIssue } = props;
|
const { closeDropdown, workspaceSlug, currentIssue, parentIssue } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
fetchSubIssues,
|
fetchSubIssues,
|
||||||
|
|
@ -41,18 +42,23 @@ export const IssueParentSiblings = observer(function IssueParentSiblings(props:
|
||||||
return (
|
return (
|
||||||
<div className="my-1">
|
<div className="my-1">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center gap-2 px-1 py-1 text-left text-11 whitespace-nowrap text-secondary">
|
<div className="flex items-center gap-2 px-2 py-1.5 text-left text-11 whitespace-nowrap text-secondary">
|
||||||
Loading
|
Loading
|
||||||
</div>
|
</div>
|
||||||
) : subIssueIds && subIssueIds.length > 0 ? (
|
) : subIssueIds && subIssueIds.length > 0 ? (
|
||||||
subIssueIds.map(
|
subIssueIds.map(
|
||||||
(issueId) =>
|
(issueId) =>
|
||||||
currentIssue.id != issueId && (
|
currentIssue.id != issueId && (
|
||||||
<IssueParentSiblingItem key={issueId} workspaceSlug={workspaceSlug} issueId={issueId} />
|
<IssueParentSiblingItem
|
||||||
|
key={issueId}
|
||||||
|
closeDropdown={closeDropdown}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
issueId={issueId}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2 px-1 py-1 text-left text-11 whitespace-nowrap text-secondary">
|
<div className="flex items-center gap-2 px-2 py-1.5 text-left text-11 whitespace-nowrap text-secondary">
|
||||||
No sibling work items
|
No sibling work items
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,42 @@ import { cn } from "../utils";
|
||||||
import type { TContextMenuItem } from "./context-menu";
|
import type { TContextMenuItem } from "./context-menu";
|
||||||
|
|
||||||
export type TActionDropdownTrigger = React.ReactNode;
|
export type TActionDropdownTrigger = React.ReactNode;
|
||||||
|
type TActionDropdownMenuContent = React.ReactNode | ((props: { closeDropdown: () => void }) => React.ReactNode);
|
||||||
|
|
||||||
export interface IActionDropdownProps {
|
export interface IActionDropdownProps {
|
||||||
button?: TActionDropdownTrigger;
|
button?: TActionDropdownTrigger;
|
||||||
|
buttonAsChild?: boolean;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
items: TContextMenuItem[];
|
items: TContextMenuItem[];
|
||||||
|
menuContent?: TActionDropdownMenuContent;
|
||||||
menuClassName?: string;
|
menuClassName?: string;
|
||||||
onOpenChange?: (isOpen: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
placement?: TPlacement;
|
placement?: TPlacement;
|
||||||
portalElement?: Element | null;
|
portalElement?: Element | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assignRef = <T,>(ref: React.Ref<T> | undefined, value: T) => {
|
||||||
|
if (!ref) return;
|
||||||
|
if (typeof ref === "function") {
|
||||||
|
ref(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(ref as React.MutableRefObject<T>).current = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const composeEventHandlers = <E extends { defaultPrevented?: boolean }>(
|
||||||
|
...handlers: Array<((event: E) => void) | undefined>
|
||||||
|
) => {
|
||||||
|
return (event: E) => {
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler?.(event);
|
||||||
|
if (event.defaultPrevented) break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const renderActionContent = (item: TContextMenuItem) =>
|
const renderActionContent = (item: TContextMenuItem) =>
|
||||||
item.customContent ?? (
|
item.customContent ?? (
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
|
|
@ -108,14 +131,27 @@ function ActionDropdownItem(props: TActionDropdownItemProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionDropdown(props: IActionDropdownProps) {
|
export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
const { button, buttonClassName, className, disabled = false, items, menuClassName, onOpenChange, placement, portalElement } = props;
|
const {
|
||||||
|
button,
|
||||||
|
buttonAsChild = false,
|
||||||
|
buttonClassName,
|
||||||
|
className,
|
||||||
|
disabled = false,
|
||||||
|
items,
|
||||||
|
menuContent,
|
||||||
|
menuClassName,
|
||||||
|
onOpenChange,
|
||||||
|
placement,
|
||||||
|
portalElement,
|
||||||
|
} = props;
|
||||||
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [referenceElement, setReferenceElement] = React.useState<HTMLElement | null>(null);
|
const [referenceElement, setReferenceElement] = React.useState<HTMLElement | null>(null);
|
||||||
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
const renderedItems = items.filter((item) => item.shouldRender !== false);
|
||||||
const isDropdownDisabled = disabled || renderedItems.length === 0;
|
const hasMenuContent = menuContent !== undefined && menuContent !== null;
|
||||||
|
const isDropdownDisabled = disabled || (!hasMenuContent && renderedItems.length === 0);
|
||||||
|
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-end",
|
placement: placement ?? "bottom-end",
|
||||||
|
|
@ -172,9 +208,13 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||||
|
|
||||||
|
const setReferenceNode = React.useCallback((node: HTMLElement | null) => {
|
||||||
|
setReferenceElement(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const defaultButton = (
|
const defaultButton = (
|
||||||
<button
|
<button
|
||||||
ref={setReferenceElement as React.Ref<HTMLButtonElement>}
|
ref={setReferenceNode as React.Ref<HTMLButtonElement>}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"clickable relative grid place-items-center rounded-sm border-0 bg-transparent p-1 text-secondary shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 hover:text-primary",
|
"clickable relative grid place-items-center rounded-sm border-0 bg-transparent p-1 text-secondary shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 hover:text-primary",
|
||||||
|
|
@ -196,9 +236,34 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const asChildButton =
|
||||||
|
buttonAsChild && React.isValidElement(button) && button.type !== React.Fragment
|
||||||
|
? React.cloneElement(button, {
|
||||||
|
ref: (node: HTMLElement | null) => {
|
||||||
|
setReferenceNode(node);
|
||||||
|
assignRef((button as React.ReactElement & { ref?: React.Ref<HTMLElement> }).ref, node);
|
||||||
|
},
|
||||||
|
className: cn((button.props as { className?: string }).className, buttonClassName),
|
||||||
|
disabled: isDropdownDisabled || (button.props as { disabled?: boolean }).disabled,
|
||||||
|
onClick: composeEventHandlers(
|
||||||
|
(button.props as { onClick?: (event: React.MouseEvent<HTMLElement>) => void }).onClick,
|
||||||
|
handleTriggerClick
|
||||||
|
),
|
||||||
|
onKeyDown: composeEventHandlers(
|
||||||
|
(button.props as { onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void }).onKeyDown,
|
||||||
|
handleKeyDown
|
||||||
|
),
|
||||||
|
type: (button.props as { type?: string }).type ?? "button",
|
||||||
|
"aria-haspopup": "menu",
|
||||||
|
"aria-expanded": isOpen,
|
||||||
|
"aria-label": (button.props as { ["aria-label"]?: string })["aria-label"] ?? "Work item actions",
|
||||||
|
"data-action-dropdown-trigger": "true",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const customButton = button ? (
|
const customButton = button ? (
|
||||||
<button
|
<button
|
||||||
ref={setReferenceElement as React.Ref<HTMLButtonElement>}
|
ref={setReferenceNode as React.Ref<HTMLButtonElement>}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"clickable block h-full rounded-full border-0 bg-transparent p-0 shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0",
|
"clickable block h-full rounded-full border-0 bg-transparent p-0 shadow-none outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0",
|
||||||
|
|
@ -236,11 +301,19 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
menuClassName
|
menuClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{hasMenuContent ? (
|
||||||
|
typeof menuContent === "function" ? (
|
||||||
|
menuContent({ closeDropdown })
|
||||||
|
) : (
|
||||||
|
menuContent
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<div className="max-h-[min(85vh,40rem)] space-y-1 overflow-y-auto">
|
<div className="max-h-[min(85vh,40rem)] space-y-1 overflow-y-auto">
|
||||||
{renderedItems.map((item) => (
|
{renderedItems.map((item) => (
|
||||||
<ActionDropdownItem key={item.key} item={item} onSelect={closeDropdown} />
|
<ActionDropdownItem key={item.key} item={item} onSelect={closeDropdown} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
portalElement ?? document.body
|
portalElement ?? document.body
|
||||||
|
|
@ -249,7 +322,7 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={dropdownRef} className={cn("relative h-full", className)} onKeyDownCapture={handleKeyDown}>
|
<div ref={dropdownRef} className={cn("relative h-full", className)} onKeyDownCapture={handleKeyDown}>
|
||||||
{customButton ?? defaultButton}
|
{asChildButton ?? customButton ?? defaultButton}
|
||||||
{popup}
|
{popup}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue