From e8f782703ded3c1896783dedad0d03b4ec944032 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Mon, 9 Feb 2026 17:21:19 +0100 Subject: [PATCH] Support floating strategy and named portals Allow ActionButtons to use fixed positioning via a floatingStrategy prop passed to useFloating. Support portal="aboveNavigationWidgets" to render action buttons in the high z-index portal used by hotspot tooltips. Turn linkPreviewFloatingStrategy into a general floatingStrategy prop on EditableLink so it applies to both the link tooltip and the action buttons. --- .../inlineEditing/ActionButtons-spec.js | 29 +++++++++++++++++++ .../inlineEditing/EditableLink-spec.js | 14 ++++++++- .../support/toHaveAncestorWithInlineStyle.js | 13 +++++++++ .../frontend/inlineEditing/ActionButtons.js | 11 ++++--- .../frontend/inlineEditing/EditableLink.js | 7 +++-- 5 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 entry_types/scrolled/package/spec/support/toHaveAncestorWithInlineStyle.js diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/ActionButtons-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/ActionButtons-spec.js index 5b04dbd029..73e4b6b6ed 100644 --- a/entry_types/scrolled/package/spec/frontend/inlineEditing/ActionButtons-spec.js +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/ActionButtons-spec.js @@ -5,6 +5,7 @@ import {ActionButtons} from 'frontend/inlineEditing/ActionButtons'; import {render} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; +import 'support/toHaveAncestorWithInlineStyle'; describe('ActionButtons', () => { it('renders icons and texts for each button', () => { @@ -45,6 +46,34 @@ describe('ActionButtons', () => { expect(button).toHaveTextContent(''); }); + it('uses absolute position for floating element by default', () => { + const {getByRole} = render( + + ); + + expect(getByRole('button', {name: 'Edit'})).toHaveAncestorWithInlineStyle('position: absolute'); + }); + + it('supports floatingStrategy prop', () => { + const {getByRole} = render( + + ); + + expect(getByRole('button', {name: 'Edit'})).toHaveAncestorWithInlineStyle('position: fixed'); + }); + + it('renders in portal with given id when portal is a string', () => { + render( + + ); + + const portalContainer = document.getElementById('floating-ui-above-navigation-widgets'); + expect(portalContainer).toBeInTheDocument(); + expect(portalContainer.querySelector('button')).toBeInTheDocument(); + }); + it('triggers individual click handlers', async () => { const onEdit = jest.fn(); const onLink = jest.fn(); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableLink-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableLink-spec.js index b5d382e16b..24bb3d01c8 100644 --- a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableLink-spec.js +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableLink-spec.js @@ -8,7 +8,8 @@ import {useFakeTranslations} from 'pageflow/testHelpers'; import {renderInContentElement} from 'pageflow-scrolled/testHelpers'; import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom/extend-expect' +import '@testing-library/jest-dom/extend-expect'; +import 'support/toHaveAncestorWithInlineStyle'; jest.mock('frontend/inlineEditing/useSelectLinkDestination'); @@ -152,6 +153,17 @@ describe('EditableLink', () => { expect(onChange).toHaveBeenCalledWith(null); }); + it('passes floatingStrategy to action buttons', () => { + render( + + Some link + + ); + + expect(screen.getByRole('button', {name: 'Select link destination'})).toHaveAncestorWithInlineStyle('position: fixed'); + }); + it('triggers onClick when tooltip is clicked', async () => { const onClick = jest.fn(); const user = userEvent.setup(); diff --git a/entry_types/scrolled/package/spec/support/toHaveAncestorWithInlineStyle.js b/entry_types/scrolled/package/spec/support/toHaveAncestorWithInlineStyle.js new file mode 100644 index 0000000000..a8a0a48d21 --- /dev/null +++ b/entry_types/scrolled/package/spec/support/toHaveAncestorWithInlineStyle.js @@ -0,0 +1,13 @@ +expect.extend({ + toHaveAncestorWithInlineStyle(element, styleSubstring) { + const ancestor = element.closest(`[style*="${styleSubstring}"]`); + const pass = !!ancestor; + + return { + pass, + message: () => pass + ? `expected element not to have ancestor with inline style '${styleSubstring}'` + : `expected element to have ancestor with inline style '${styleSubstring}'` + }; + } +}); diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/ActionButtons.js b/entry_types/scrolled/package/src/frontend/inlineEditing/ActionButtons.js index b14d7dc6af..287309580e 100644 --- a/entry_types/scrolled/package/src/frontend/inlineEditing/ActionButtons.js +++ b/entry_types/scrolled/package/src/frontend/inlineEditing/ActionButtons.js @@ -21,10 +21,11 @@ const icons = { unlink }; -export function ActionButtons({buttons, position, portal, size = 'md'}) { +export function ActionButtons({buttons, position, portal, floatingStrategy, size = 'md'}) { const iconSize = size === 'md' ? 15 : 20; const {refs, floatingStyles, middlewareData} = useFloating({ + strategy: floatingStrategy, placement: position === 'center' ? 'bottom' : position === 'inside' ? 'top-end' : position === 'outsideLeft' ? 'bottom-start' : @@ -41,7 +42,8 @@ export function ActionButtons({buttons, position, portal, size = 'md'}) { - +
@@ -68,12 +70,13 @@ export function ActionButtons({buttons, position, portal, size = 'md'}) { ); } -function Portal({enabled, children}) { +function Portal({enabled, aboveNavigationWidgets, children}) { const floatingPortalRoot = useFloatingPortalRoot(); if (enabled) { return ( - + {children} ); diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableLink.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableLink.js index f498100458..0cb82b3615 100644 --- a/entry_types/scrolled/package/src/frontend/inlineEditing/EditableLink.js +++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EditableLink.js @@ -16,7 +16,7 @@ export function EditableLink({ linkPreviewDisabled, linkPreviewPosition = 'below', linkPreviewAlign = 'center', - linkPreviewFloatingStrategy, + floatingStrategy, actionButtonPosition = 'outside', actionButtonVisible = 'whenSelected', actionButtonPortal, @@ -42,7 +42,7 @@ export function EditableLink({ return (
@@ -64,7 +64,8 @@ export function EditableLink({ text: t('pageflow_scrolled.inline_editing.remove_link'), onClick: handleRemoveLink}] : [])]} position={actionButtonPosition} - portal={actionButtonPortal} />} + portal={actionButtonPortal} + floatingStrategy={floatingStrategy} />}
); }