АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: канон составных detail-menu и customButton-trigger
This commit is contained in:
@@ -16,19 +16,42 @@ import { cn } from "../utils";
|
||||
import type { TContextMenuItem } from "./context-menu";
|
||||
|
||||
export type TActionDropdownTrigger = React.ReactNode;
|
||||
type TActionDropdownMenuContent = React.ReactNode | ((props: { closeDropdown: () => void }) => React.ReactNode);
|
||||
|
||||
export interface IActionDropdownProps {
|
||||
button?: TActionDropdownTrigger;
|
||||
buttonAsChild?: boolean;
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
items: TContextMenuItem[];
|
||||
menuContent?: TActionDropdownMenuContent;
|
||||
menuClassName?: string;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
placement?: TPlacement;
|
||||
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) =>
|
||||
item.customContent ?? (
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -108,14 +131,27 @@ function ActionDropdownItem(props: TActionDropdownItemProps) {
|
||||
}
|
||||
|
||||
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 [referenceElement, setReferenceElement] = React.useState<HTMLElement | null>(null);
|
||||
const [popperElement, setPopperElement] = React.useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(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, {
|
||||
placement: placement ?? "bottom-end",
|
||||
@@ -172,9 +208,13 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
const setReferenceNode = React.useCallback((node: HTMLElement | null) => {
|
||||
setReferenceElement(node);
|
||||
}, []);
|
||||
|
||||
const defaultButton = (
|
||||
<button
|
||||
ref={setReferenceElement as React.Ref<HTMLButtonElement>}
|
||||
ref={setReferenceNode as React.Ref<HTMLButtonElement>}
|
||||
type="button"
|
||||
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",
|
||||
@@ -196,9 +236,34 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||
</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 ? (
|
||||
<button
|
||||
ref={setReferenceElement as React.Ref<HTMLButtonElement>}
|
||||
ref={setReferenceNode as React.Ref<HTMLButtonElement>}
|
||||
type="button"
|
||||
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",
|
||||
@@ -236,11 +301,19 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||
menuClassName
|
||||
)}
|
||||
>
|
||||
<div className="max-h-[min(85vh,40rem)] space-y-1 overflow-y-auto">
|
||||
{renderedItems.map((item) => (
|
||||
<ActionDropdownItem key={item.key} item={item} onSelect={closeDropdown} />
|
||||
))}
|
||||
</div>
|
||||
{hasMenuContent ? (
|
||||
typeof menuContent === "function" ? (
|
||||
menuContent({ closeDropdown })
|
||||
) : (
|
||||
menuContent
|
||||
)
|
||||
) : (
|
||||
<div className="max-h-[min(85vh,40rem)] space-y-1 overflow-y-auto">
|
||||
{renderedItems.map((item) => (
|
||||
<ActionDropdownItem key={item.key} item={item} onSelect={closeDropdown} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
portalElement ?? document.body
|
||||
@@ -249,7 +322,7 @@ export function ActionDropdown(props: IActionDropdownProps) {
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className={cn("relative h-full", className)} onKeyDownCapture={handleKeyDown}>
|
||||
{customButton ?? defaultButton}
|
||||
{asChildButton ?? customButton ?? defaultButton}
|
||||
{popup}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user