diff --git a/.changeset/fix-tooltip-arrow-flip.md b/.changeset/fix-tooltip-arrow-flip.md new file mode 100644 index 0000000000..82c47e9d9e --- /dev/null +++ b/.changeset/fix-tooltip-arrow-flip.md @@ -0,0 +1,5 @@ +--- +'@storefront-ui/react': patch +--- + +**[FIXED]** `useTooltip` (and `SfTooltip`) now places the arrow on the side facing the trigger after the `flip` middleware moves the tooltip. The arrow side was computed from the requested `placement` instead of the placement Floating UI resolved, so a tooltip flipped from `top` to `bottom` drew its arrow on the far edge. `usePopover` now also returns the resolved `placement`, matching the Vue composable. diff --git a/packages/sfui/frameworks/react/hooks/usePopover/usePopover.ts b/packages/sfui/frameworks/react/hooks/usePopover/usePopover.ts index 03bae51d0d..93ba67ef67 100644 --- a/packages/sfui/frameworks/react/hooks/usePopover/usePopover.ts +++ b/packages/sfui/frameworks/react/hooks/usePopover/usePopover.ts @@ -9,7 +9,14 @@ export function usePopover(options: UsePopoverOptions) { strategy: initialStrategy = SfPopoverStrategy.absolute, } = options; - const { refs, strategy, x, y, middlewareData } = useFloating({ + const { + refs, + strategy, + x, + y, + middlewareData, + placement: floatingPlacement, + } = useFloating({ strategy: initialStrategy, placement, open: isOpen, @@ -23,5 +30,5 @@ export function usePopover(options: UsePopoverOptions) { left: x, }; - return { refs, style, middlewareData }; + return { refs, style, middlewareData, placement: floatingPlacement }; } diff --git a/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts b/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts index 538fe81a0d..cddef2006c 100644 --- a/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts +++ b/packages/sfui/frameworks/react/hooks/useTooltip/useTooltip.ts @@ -24,6 +24,7 @@ export function useTooltip(options?: UseTooltipOptions) { refs, style: floatingStyle, middlewareData, + placement: floatingPlacement, } = usePopover({ isOpen, placement, @@ -34,7 +35,7 @@ export function useTooltip(options?: UseTooltipOptions) { function arrowStyle() { if (middlewareData.arrow) { const { x: arrowX, y: arrowY } = middlewareData.arrow; - const basePlacement = placement.split('-')[0] as SfPopoverBasePlacement; + const basePlacement = floatingPlacement.split('-')[0] as SfPopoverBasePlacement; return { position: 'absolute', width: arrowSize, diff --git a/packages/tests/components/SfTooltip/SfTooltip.PageObject.ts b/packages/tests/components/SfTooltip/SfTooltip.PageObject.ts index 7e1fcc54b4..a5a745ce05 100644 --- a/packages/tests/components/SfTooltip/SfTooltip.PageObject.ts +++ b/packages/tests/components/SfTooltip/SfTooltip.PageObject.ts @@ -17,6 +17,27 @@ export default class SfTooltipObject extends BasePage { return this; } + isTooltipBelowTrigger() { + this.container.then(($trigger) => { + this.tooltip.should(($tooltip) => { + expect($tooltip[0].getBoundingClientRect().top).to.be.greaterThan($trigger[0].getBoundingClientRect().top); + }); + }); + return this; + } + + hasArrowOnTopEdge() { + this.tooltip.should(($tooltip) => { + const arrow = $tooltip.find('span')[0]; + expect(arrow, 'arrow').to.exist; + const tooltipRect = $tooltip[0].getBoundingClientRect(); + const arrowRect = arrow.getBoundingClientRect(); + expect(arrowRect.top, 'arrow top').to.be.lessThan(tooltipRect.top); + expect(arrowRect.bottom, 'arrow bottom').to.be.lessThan(tooltipRect.bottom); + }); + return this; + } + get tooltip() { return this.container.get('[role="tooltip"]'); } diff --git a/packages/tests/components/SfTooltip/SfTooltip.cy.tsx b/packages/tests/components/SfTooltip/SfTooltip.cy.tsx index 808f49f039..89fda55028 100644 --- a/packages/tests/components/SfTooltip/SfTooltip.cy.tsx +++ b/packages/tests/components/SfTooltip/SfTooltip.cy.tsx @@ -91,6 +91,15 @@ describe('SfTooltip', () => { }); }); + describe('When showArrow=true and there is no room above the trigger', () => { + it('Should flip the tooltip below and keep the arrow pointing at the trigger', () => { + const props = { showArrow: true }; + initializeComponent(props); + + page().mouseOver().isTooltipVisible().isTooltipBelowTrigger().hasArrowOnTopEdge(); + }); + }); + describe('When no label', () => { it('Should not show the tooltip', () => { const props = { label: '' };