diff --git a/src/components/tolariaEditorFormatting.behavior.test.tsx b/src/components/tolariaEditorFormatting.behavior.test.tsx
index cc8853be..4b007c9d 100644
--- a/src/components/tolariaEditorFormatting.behavior.test.tsx
+++ b/src/components/tolariaEditorFormatting.behavior.test.tsx
@@ -287,6 +287,29 @@ describe('tolariaEditorFormatting behavior', () => {
}))
})
+ it('adds viewport-clamping middleware and preserves caller middleware', () => {
+ const editor = createMockEditor('paragraph')
+ useBlockNoteEditorMock.mockReturnValue(editor)
+
+ render(
+ ,
+ )
+
+ const floatingOptions = positionPopoverState.lastProps?.useFloatingOptions as {
+ middleware: Array<{ name: string }>
+ }
+
+ expect(floatingOptions.middleware.map((middleware) => middleware.name)).toEqual(
+ expect.arrayContaining(['custom-middleware', 'tolariaViewportClamp']),
+ )
+ })
+
it('falls back to top-start and focuses the block type trigger on mouse down', () => {
const editor = createMockEditor('paragraph')
const focusSpy = vi.spyOn(HTMLButtonElement.prototype, 'focus').mockImplementation(() => {})
diff --git a/src/components/tolariaEditorFormatting.tsx b/src/components/tolariaEditorFormatting.tsx
index 4307711c..8c0ace45 100644
--- a/src/components/tolariaEditorFormatting.tsx
+++ b/src/components/tolariaEditorFormatting.tsx
@@ -67,6 +67,9 @@ import {
type TolariaBasicTextStyle = 'bold' | 'italic' | 'strike' | 'code'
const FORMATTER_CLOSE_GRACE_MS = 160
+const FORMATTER_VIEWPORT_PADDING_PX = 8
+type TolariaFloatingOptions = NonNullable
+type TolariaFloatingMiddleware = NonNullable[number]
function isFocusStillWithinToolbar(
currentTarget: EventTarget & Element,
@@ -230,6 +233,42 @@ function textAlignmentToPlacement(
}
}
+function viewportClampMiddleware(): TolariaFloatingMiddleware {
+ return {
+ name: 'tolariaViewportClamp',
+ fn({ x, rects }: { rects: { floating: { width: number } }; x: number }) {
+ const viewportWidth = window.visualViewport?.width ?? window.innerWidth
+ const minX = FORMATTER_VIEWPORT_PADDING_PX
+ const maxX = Math.max(
+ minX,
+ viewportWidth - rects.floating.width - FORMATTER_VIEWPORT_PADDING_PX,
+ )
+
+ return {
+ x: Math.min(Math.max(x, minX), maxX),
+ }
+ },
+ }
+}
+
+function withViewportSafeMiddleware(
+ options?: TolariaFloatingOptions,
+): TolariaFloatingOptions {
+ if (!options) {
+ return {
+ middleware: [viewportClampMiddleware()],
+ }
+ }
+
+ return {
+ ...options,
+ middleware: [
+ ...(options.middleware ?? []),
+ viewportClampMiddleware(),
+ ],
+ }
+}
+
function editorSupportsTextStyle(
style: TolariaBasicTextStyle,
editor: BlockNoteEditor,
@@ -730,7 +769,7 @@ export function TolariaFormattingToolbarController(props: {
}
},
placement,
- ...props.floatingUIOptions?.useFloatingOptions,
+ ...withViewportSafeMiddleware(props.floatingUIOptions?.useFloatingOptions),
},
elementProps: {
style: {