diff --git a/packages/react/src/BlockNoteTheme.ts b/packages/react/src/BlockNoteTheme.ts index 9d6d6ede56..4ff342bc87 100644 --- a/packages/react/src/BlockNoteTheme.ts +++ b/packages/react/src/BlockNoteTheme.ts @@ -41,7 +41,8 @@ export type ComponentStyles = Partial<{ Editor: CSSObject; // Wraps Formatting Toolbar & Hyperlink Toolbar Toolbar: CSSObject; - // Appears on hover for Formatting Toolbar & Hyperlink Toolbar buttons + // Appears on hover for Formatting Toolbar + // & Hyperlink Toolbar buttons Tooltip: CSSObject; SlashMenu: CSSObject; SideMenu: CSSObject; @@ -105,6 +106,7 @@ export const blockNoteToMantineTheme = (theme: Theme): MantineThemeOverride => { boxShadow: shadow, color: theme.colors.menu.text, padding: "2px", + overflowY: "scroll", ".mantine-Menu-label": { backgroundColor: theme.colors.menu.background, color: theme.colors.menu.text, diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx index 2eca1eb91a..d728c39c63 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/ColorStyleButton.tsx @@ -6,6 +6,7 @@ import { ColorIcon } from "../../../SharedComponents/ColorPicker/components/Colo import { ColorPicker } from "../../../SharedComponents/ColorPicker/components/ColorPicker"; import { useEditorContentChange } from "../../../hooks/useEditorContentChange"; import { useEditorSelectionChange } from "../../../hooks/useEditorSelectionChange"; +import { usePreventMenuOverflow } from "../../../hooks/usePreventMenuOverflow"; export const ColorStyleButton = (props: { editor: BlockNoteEditor; @@ -51,8 +52,10 @@ export const ColorStyleButton = (props: { [props.editor] ); + const { ref, updateMaxHeight } = usePreventMenuOverflow(); + return ( - + (props: { )} /> - - - +
+ + + +
); }; diff --git a/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx index 14acb8b9bb..176c72faa3 100644 --- a/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx +++ b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx @@ -4,6 +4,7 @@ import { ToolbarDropdownItemProps, } from "./ToolbarDropdownItem"; import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget"; +import { usePreventMenuOverflow } from "../../../hooks/usePreventMenuOverflow"; export type ToolbarDropdownProps = { items: ToolbarDropdownItemProps[]; @@ -13,12 +14,17 @@ export type ToolbarDropdownProps = { export function ToolbarDropdown(props: ToolbarDropdownProps) { const selectedItem = props.items.filter((p) => p.isSelected)[0]; + const { ref, updateMaxHeight } = usePreventMenuOverflow(); + if (!selectedItem) { return null; } return ( - + - - {props.items.map((item) => ( - - ))} - +
+ + {props.items.map((item) => ( + + ))} + +
); } diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx index f506655b85..c97fcf00d6 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DefaultButtons/BlockColorsButton.tsx @@ -6,12 +6,15 @@ import { BlockSchema, PartialBlock } from "@blocknote/core"; import { DragHandleMenuProps } from "../DragHandleMenu"; import { DragHandleMenuItem } from "../DragHandleMenuItem"; import { ColorPicker } from "../../../../SharedComponents/ColorPicker/components/ColorPicker"; +import { usePreventMenuOverflow } from "../../../../hooks/usePreventMenuOverflow"; export const BlockColorsButton = ( props: DragHandleMenuProps & { children: ReactNode } ) => { const [opened, setOpened] = useState(false); + const { ref, updateMaxHeight } = usePreventMenuOverflow(); + const menuCloseTimer = useRef(); const startMenuCloseTimer = useCallback(() => { @@ -27,8 +30,13 @@ export const BlockColorsButton = ( if (menuCloseTimer.current) { clearTimeout(menuCloseTimer.current); } + + if (!opened) { + updateMaxHeight(); + } + setOpened(true); - }, []); + }, [opened, updateMaxHeight]); if ( !("textColor" in props.block.props) || @@ -50,26 +58,28 @@ export const BlockColorsButton = ( - - - props.editor.updateBlock(props.block, { - props: { textColor: color }, - } as PartialBlock) - } - setBackgroundColor={(color) => - props.editor.updateBlock(props.block, { - props: { backgroundColor: color }, - } as PartialBlock) - } - /> - +
+ + + props.editor.updateBlock(props.block, { + props: { textColor: color }, + } as PartialBlock) + } + setBackgroundColor={(color) => + props.editor.updateBlock(props.block, { + props: { backgroundColor: color }, + } as PartialBlock) + } + /> + +
); diff --git a/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx b/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx index 1062799004..b67dd98836 100644 --- a/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx +++ b/packages/react/src/SideMenu/components/DragHandleMenu/DragHandleMenu.tsx @@ -13,6 +13,8 @@ export const DragHandleMenu = (props: { children: ReactNode }) => { }); return ( - {props.children} + + {props.children} + ); }; diff --git a/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx b/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx index 35b79eb423..dd808010a1 100644 --- a/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx +++ b/packages/react/src/SlashMenu/components/SlashMenuPositioner.tsx @@ -10,6 +10,7 @@ import { FC, useEffect, useMemo, useRef, useState } from "react"; import { ReactSlashMenuItem } from "../ReactSlashMenuItem"; import { DefaultSlashMenu } from "./DefaultSlashMenu"; +import { usePreventMenuOverflow } from "../../hooks/usePreventMenuOverflow"; export type SlashMenuProps = Pick, "itemCallback"> & @@ -53,6 +54,8 @@ export const SlashMenuPositioner = < [referencePos.current] // eslint-disable-line ); + const { ref, updateMaxHeight } = usePreventMenuOverflow(); + const slashMenuElement = useMemo(() => { if (!filteredItems || keyboardHoveredItemIndex === undefined) { return null; @@ -61,21 +64,25 @@ export const SlashMenuPositioner = < const SlashMenu = props.slashMenu || DefaultSlashMenu; return ( - props.editor.slashMenu.itemCallback(item)} - keyboardHoveredItemIndex={keyboardHoveredItemIndex} - /> +
+ props.editor.slashMenu.itemCallback(item)} + keyboardHoveredItemIndex={keyboardHoveredItemIndex} + /> +
); }, [ filteredItems, keyboardHoveredItemIndex, props.editor.slashMenu, props.slashMenu, + ref, ]); return ( (null); + + return { + ref: ref, + updateMaxHeight: () => { + // Seems a small delay is necessary to get the correct DOM rect - likely + // because the menu is not yet fully rendered when the `onOpen` event is + // fired. + setTimeout(() => { + if (!ref.current) { + return; + } + + if (ref.current.childElementCount > 0) { + // Reset any previously set max-height + (ref.current.firstElementChild as HTMLDivElement).style.maxHeight = + "none"; + + // Get the menu DOM rect + const domRect = + ref.current.firstElementChild!.getBoundingClientRect(); + + // Set the menu max height, based on the Tippy position. Checking if + // the top of the menu is above the viewport (position < 0) is a quick + // way to check if the placement is "top" or "bottom". + ( + ref.current.firstElementChild as HTMLDivElement + ).style.maxHeight = `${Math.min( + domRect.top >= 0 + ? window.innerHeight - domRect.top - 20 + : domRect.bottom - 20 + )}px`; + } + }, 10); + }, + }; +} diff --git a/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-chromium-linux.png b/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-chromium-linux.png index c429cb72bc..8e9ffc9b25 100644 Binary files a/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-chromium-linux.png and b/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-chromium-linux.png differ diff --git a/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-firefox-linux.png b/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-firefox-linux.png index bbfe06425c..356a66d015 100644 Binary files a/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-firefox-linux.png and b/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-firefox-linux.png differ diff --git a/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-webkit-linux.png b/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-webkit-linux.png index c6e18efc29..0c85730353 100644 Binary files a/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-webkit-linux.png and b/tests/end-to-end/theming/theming.test.ts-snapshots/dark-formatting-toolbar-webkit-linux.png differ