mirror of
https://github.com/zadam/trilium.git
synced 2025-12-29 18:54:29 +01:00
146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
|
import { ComponentChildren, HTMLAttributes } from "preact";
|
|
import { CSSProperties, HTMLProps } from "preact/compat";
|
|
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
|
|
import { useTooltip, useUniqueName } from "./hooks";
|
|
|
|
type DataAttributes = {
|
|
[key: `data-${string}`]: string | number | boolean | undefined;
|
|
};
|
|
|
|
export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "className"> {
|
|
buttonClassName?: string;
|
|
buttonProps?: Partial<HTMLAttributes<HTMLButtonElement> & DataAttributes>;
|
|
isStatic?: boolean;
|
|
children: ComponentChildren;
|
|
title?: string;
|
|
dropdownContainerStyle?: CSSProperties;
|
|
dropdownContainerClassName?: string;
|
|
dropdownContainerRef?: MutableRef<HTMLDivElement | null>;
|
|
hideToggleArrow?: boolean;
|
|
/** If set to true, then the dropdown button will be considered an icon action (without normal border and sized for icons only). */
|
|
iconAction?: boolean;
|
|
noSelectButtonStyle?: boolean;
|
|
noDropdownListStyle?: boolean;
|
|
disabled?: boolean;
|
|
text?: ComponentChildren;
|
|
forceShown?: boolean;
|
|
onShown?: () => void;
|
|
onHidden?: () => void;
|
|
dropdownOptions?: Partial<BootstrapDropdown.Options>;
|
|
dropdownRef?: MutableRef<BootstrapDropdown | null>;
|
|
titlePosition?: "top" | "right" | "bottom" | "left";
|
|
titleOptions?: Partial<Tooltip.Options>;
|
|
}
|
|
|
|
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) {
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
const dropdownContainerRef = useRef<HTMLUListElement | null>(null);
|
|
|
|
const { showTooltip, hideTooltip } = useTooltip(containerRef, {
|
|
...titleOptions,
|
|
placement: titlePosition ?? "bottom",
|
|
fallbackPlacements: [ titlePosition ?? "bottom" ],
|
|
trigger: "manual"
|
|
});
|
|
|
|
const [ shown, setShown ] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!triggerRef.current || !dropdownContainerRef.current) return;
|
|
|
|
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current, dropdownOptions);
|
|
if (dropdownRef) {
|
|
dropdownRef.current = dropdown;
|
|
}
|
|
if (forceShown) {
|
|
dropdown.show();
|
|
setShown(true);
|
|
}
|
|
|
|
// React to popup container size changes, which can affect the positioning.
|
|
const resizeObserver = new ResizeObserver(() => dropdown.update());
|
|
resizeObserver.observe(dropdownContainerRef.current);
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
dropdown.dispose();
|
|
};
|
|
}, []);
|
|
|
|
const onShown = useCallback(() => {
|
|
setShown(true);
|
|
externalOnShown?.();
|
|
hideTooltip();
|
|
}, [ hideTooltip ]);
|
|
|
|
const onHidden = useCallback(() => {
|
|
setShown(false);
|
|
externalOnHidden?.();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
if (externalContainerRef) externalContainerRef.current = containerRef.current;
|
|
|
|
const $dropdown = $(containerRef.current);
|
|
$dropdown.on("show.bs.dropdown", (e) => {
|
|
// Stop propagation causing multiple shows for nested dropdowns.
|
|
e.stopPropagation();
|
|
onShown();
|
|
});
|
|
$dropdown.on("hide.bs.dropdown", (e) => {
|
|
// Stop propagation causing multiple hides for nested dropdowns.
|
|
e.stopPropagation();
|
|
onHidden();
|
|
});
|
|
|
|
// Add proper cleanup
|
|
return () => {
|
|
$dropdown.off("show.bs.dropdown", onShown);
|
|
$dropdown.off("hide.bs.dropdown", onHidden);
|
|
};
|
|
}, [ onShown, onHidden ]);
|
|
|
|
const ariaId = useUniqueName("button");
|
|
|
|
return (
|
|
<div ref={containerRef} class={`dropdown ${className ?? ""}`} style={{ display: "flex" }} title={title}>
|
|
<button
|
|
className={`${iconAction ? "icon-action" : "btn"} ${!noSelectButtonStyle ? "select-button" : ""} ${buttonClassName ?? ""} ${!hideToggleArrow ? "dropdown-toggle" : ""}`}
|
|
ref={triggerRef}
|
|
type="button"
|
|
data-bs-toggle="dropdown"
|
|
data-bs-display={ isStatic ? "static" : undefined }
|
|
aria-haspopup="true"
|
|
aria-expanded="false"
|
|
id={id ?? ariaId}
|
|
disabled={disabled}
|
|
onMouseEnter={showTooltip}
|
|
onMouseLeave={hideTooltip}
|
|
{...buttonProps}
|
|
>
|
|
{text}
|
|
<span className="caret" />
|
|
</button>
|
|
|
|
<ul
|
|
class={`dropdown-menu ${isStatic ? "static" : ""} ${dropdownContainerClassName ?? ""} ${!noDropdownListStyle ? "tn-dropdown-list" : ""}`}
|
|
style={dropdownContainerStyle}
|
|
aria-labelledby={ariaId}
|
|
ref={dropdownContainerRef}
|
|
onClick={(e) => {
|
|
// Prevent clicks directly inside the dropdown from closing.
|
|
if (e.target === dropdownContainerRef.current) {
|
|
e.stopPropagation();
|
|
}
|
|
}}
|
|
>
|
|
{shown && children}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|