& { icon: string }) {
+ const { isHorizontalLayout } = useContext(LaunchBarContext);
+
+ return (
+ }
+ titlePosition={isHorizontalLayout ? "bottom" : "right"}
+ titleOptions={{ animation: false }}
+ dropdownOptions={{
+ ...dropdownOptions,
+ popperConfig: {
+ placement: isHorizontalLayout ? "bottom" : "right"
+ }
+ }}
+ {...props}
+ >{children}
+ )
+}
+
+export function useLauncherIconAndTitle(note: FNote) {
+ const title = useNoteProperty(note, "title");
+
+ // React to changes.
+ useNoteLabel(note, "iconClass");
+ useNoteLabel(note, "workspaceIconClass");
+
+ return {
+ icon: note.getIcon(),
+ title: escapeHtml(title ?? "")
+ };
+}
diff --git a/apps/client/src/widgets/quick_search_launcher.ts b/apps/client/src/widgets/quick_search_launcher.ts
deleted file mode 100644
index f64d1ca1d..000000000
--- a/apps/client/src/widgets/quick_search_launcher.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import utils from "../services/utils.js";
-import QuickSearchWidget from "./quick_search.js";
-
-/**
- * Similar to the {@link QuickSearchWidget} but meant to be included inside the launcher bar.
- *
- *
- * Adds specific tweaks such as:
- *
- * - Hiding the widget on mobile.
- */
-export default class QuickSearchLauncherWidget extends QuickSearchWidget {
-
- private isHorizontalLayout: boolean;
-
- constructor(isHorizontalLayout: boolean) {
- super();
- this.isHorizontalLayout = isHorizontalLayout;
- }
-
- isEnabled() {
- if (!this.isHorizontalLayout) {
- // The quick search widget is added somewhere else on the vertical layout.
- return false;
- }
-
- if (utils.isMobile()) {
- // The widget takes too much spaces to be included in the mobile layout.
- return false;
- }
-
- return super.isEnabled();
- }
-}
diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx
index 28489005d..ba5430f38 100644
--- a/apps/client/src/widgets/react/ActionButton.tsx
+++ b/apps/client/src/widgets/react/ActionButton.tsx
@@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks";
import keyboard_actions from "../../services/keyboard_actions";
+import { HTMLAttributes } from "preact";
-export interface ActionButtonProps {
+export interface ActionButtonProps extends Pick, "onClick" | "onAuxClick" | "onContextMenu"> {
text: string;
titlePosition?: "top" | "right" | "bottom" | "left";
icon: string;
className?: string;
- onClick?: (e: MouseEvent) => void;
triggerCommand?: CommandNames;
noIconActionClass?: boolean;
frame?: boolean;
@@ -16,14 +16,15 @@ export interface ActionButtonProps {
disabled?: boolean;
}
-export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) {
+export default function ActionButton({ text, icon, className, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled, ...restProps }: ActionButtonProps) {
const buttonRef = useRef(null);
const [ keyboardShortcut, setKeyboardShortcut ] = useState();
useStaticTooltip(buttonRef, {
title: keyboardShortcut?.length ? `${text} (${keyboardShortcut?.join(",")})` : text,
placement: titlePosition ?? "bottom",
- fallbackPlacements: [ titlePosition ?? "bottom" ]
+ fallbackPlacements: [ titlePosition ?? "bottom" ],
+ animation: false
});
useEffect(() => {
@@ -35,8 +36,8 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
return ;
}
diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx
index 3ea79bc6c..5416e38ac 100644
--- a/apps/client/src/widgets/react/Dropdown.tsx
+++ b/apps/client/src/widgets/react/Dropdown.tsx
@@ -1,16 +1,22 @@
-import { Dropdown as BootstrapDropdown } from "bootstrap";
-import { ComponentChildren } from "preact";
+import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
+import { ComponentChildren, HTMLAttributes } from "preact";
import { CSSProperties, HTMLProps } from "preact/compat";
-import { useCallback, useEffect, useRef, useState } from "preact/hooks";
-import { useUniqueName } from "./hooks";
+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, "id" | "className"> {
buttonClassName?: string;
+ buttonProps?: Partial & DataAttributes>;
isStatic?: boolean;
children: ComponentChildren;
title?: string;
dropdownContainerStyle?: CSSProperties;
dropdownContainerClassName?: string;
+ dropdownContainerRef?: MutableRef;
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;
@@ -21,29 +27,53 @@ export interface DropdownProps extends Pick, "id" | "c
forceShown?: boolean;
onShown?: () => void;
onHidden?: () => void;
+ dropdownOptions?: Partial;
+ dropdownRef?: MutableRef;
+ titlePosition?: "top" | "right" | "bottom" | "left";
+ titleOptions?: Partial;
}
-export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden }: DropdownProps) {
- const dropdownRef = useRef(null);
+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(null);
const triggerRef = useRef(null);
+ const dropdownContainerRef = useRef(null);
+
+ const { showTooltip, hideTooltip } = useTooltip(containerRef, {
+ ...titleOptions,
+ placement: titlePosition ?? "bottom",
+ fallbackPlacements: [ titlePosition ?? "bottom" ],
+ trigger: "manual"
+ });
const [ shown, setShown ] = useState(false);
useEffect(() => {
- if (!triggerRef.current) return;
+ if (!triggerRef.current || !dropdownContainerRef.current) return;
- const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
+ const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current, dropdownOptions);
+ if (dropdownRef) {
+ dropdownRef.current = dropdown;
+ }
if (forceShown) {
dropdown.show();
setShown(true);
}
- return () => dropdown.dispose();
+
+ // 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);
@@ -51,23 +81,32 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
}, []);
useEffect(() => {
- if (!dropdownRef.current) return;
+ if (!containerRef.current) return;
+ if (externalContainerRef) externalContainerRef.current = containerRef.current;
- const $dropdown = $(dropdownRef.current);
- $dropdown.on("show.bs.dropdown", onShown);
- $dropdown.on("hide.bs.dropdown", onHidden);
+ 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);
};
- }, []); // Add dependency array
+ }, [ onShown, onHidden ]);
const ariaId = useUniqueName("button");
return (
-
+