diff --git a/src/components/feedback/Popover.tsx b/src/components/feedback/Popover.tsx index e5814ec76..2e625af4c 100644 --- a/src/components/feedback/Popover.tsx +++ b/src/components/feedback/Popover.tsx @@ -133,9 +133,9 @@ function usePopoverPositioning( // First of all, open popover if it's using the native API, otherwise its // size is 0x0 and positioning calculations won't work. - const popover = popoverRef.current; + const popover = popoverRef.current!; if (asNativePopover) { - popover?.togglePopover(true); + popover.togglePopover(true); } const cleanup = adjustPopoverPositioning(); @@ -151,12 +151,18 @@ function usePopoverPositioning( capture: true, }); + // Readjust popover positioning if its resized, in case it dropped-up, and + // it needs to be moved down + const observer = new ResizeObserver(adjustPopoverPositioning); + observer.observe(popover); + return () => { if (asNativePopover) { popover?.togglePopover(false); } cleanup(); listeners.removeAll(); + observer.disconnect(); }; }, [adjustPopoverPositioning, asNativePopover, popoverOpen, popoverRef]); } diff --git a/src/components/feedback/test/Popover-test.js b/src/components/feedback/test/Popover-test.js index af8cd5d70..42ae05c88 100644 --- a/src/components/feedback/test/Popover-test.js +++ b/src/components/feedback/test/Popover-test.js @@ -1,4 +1,4 @@ -import { mount } from '@hypothesis/frontend-testing'; +import { mount, waitFor } from '@hypothesis/frontend-testing'; import { useRef, useState } from 'preact/hooks'; import Popover, { POPOVER_VIEWPORT_HORIZONTAL_GAP } from '../Popover'; @@ -73,6 +73,20 @@ describe('Popover', () => { return popoverTop < buttonTop; }; + const getDistanceBetweenButtonAndPopover = wrapper => { + const appearedAbove = popoverAppearedAbove(wrapper); + const { top: popoverTop, bottom: popoverBottom } = getPopover(wrapper) + .getDOMNode() + .getBoundingClientRect(); + const { top: buttonTop, bottom: buttonBottom } = getToggleButton(wrapper) + .getDOMNode() + .getBoundingClientRect(); + + return Math.abs( + appearedAbove ? popoverBottom - buttonTop : buttonBottom - popoverTop, + ); + }; + [ { restoreFocusOnClose: undefined, // Defaults to true @@ -151,6 +165,42 @@ describe('Popover', () => { }); }); + [true, false].forEach(asNativePopover => { + it('repositions popover if it is resized after being open', async () => { + const wrapper = createComponent( + { + asNativePopover, + children: ( + <> +
one
+two
+three
+ > + ), + }, + { paddingTop: 1000 }, // Ensure popover appears above + ); + togglePopover(wrapper); + + // This applies only when the popover appears above, so let's verify it + assert.isTrue(popoverAppearedAbove(wrapper)); + + // After opening the popover, its distance to the button should be the + // same, even if it's resized + const distanceBeforeResizing = + getDistanceBetweenButtonAndPopover(wrapper); + wrapper.setProps({ children: 'Just one line' }); + + // Repositioning happens asynchronously via ResizeObserver, so we need to + // wait for it to eventually happen. + await waitFor( + () => + distanceBeforeResizing === + getDistanceBetweenButtonAndPopover(wrapper), + ); + }); + }); + context('when popover is supported', () => { [undefined, true].forEach(asNativePopover => { it('opens via popover API', async () => {