|
|
@@ -1,4 +1,4 @@
|
|
|
-import { describe, it, expect } from 'vitest';
|
|
|
+import { describe, it, expect, afterEach } from 'vitest';
|
|
|
import { computePopoverPosition } from '../../utils/popoverPosition';
|
|
|
|
|
|
/**
|
|
|
@@ -114,3 +114,93 @@ describe('computePopoverPosition (#1447)', () => {
|
|
|
expect(pos.top).toBe(middleTrigger.bottom + 12); // 332
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+/**
|
|
|
+ * Tests for #1669: on iPhone Safari the bottom URL bar overlays the layout
|
|
|
+ * viewport, so window.innerHeight reports more vertical space than is
|
|
|
+ * actually visible. The popover's Start button rendered behind the toolbar.
|
|
|
+ * The helper now defaults viewportHeight from visualViewport.height when
|
|
|
+ * present so flip-above triggers against the real visible area.
|
|
|
+ */
|
|
|
+describe('computePopoverPosition (#1669, iOS Safari visualViewport)', () => {
|
|
|
+ const originalVisualViewport = Object.getOwnPropertyDescriptor(window, 'visualViewport');
|
|
|
+ const originalInnerHeight = Object.getOwnPropertyDescriptor(window, 'innerHeight');
|
|
|
+
|
|
|
+ afterEach(() => {
|
|
|
+ if (originalVisualViewport) {
|
|
|
+ Object.defineProperty(window, 'visualViewport', originalVisualViewport);
|
|
|
+ } else {
|
|
|
+ // jsdom didn't set it; remove anything we added so other tests see the
|
|
|
+ // pristine state.
|
|
|
+ // @ts-expect-error — deleting an optional property on window
|
|
|
+ delete window.visualViewport;
|
|
|
+ }
|
|
|
+ if (originalInnerHeight) {
|
|
|
+ Object.defineProperty(window, 'innerHeight', originalInnerHeight);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('flips above when visualViewport is shorter than innerHeight (iOS toolbar visible)', () => {
|
|
|
+ // Simulate the iPhone 17 Safari case: layout viewport says 800, but the
|
|
|
+ // bottom URL bar overlay takes 100px so visualViewport reports 700.
|
|
|
+ Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true });
|
|
|
+ Object.defineProperty(window, 'visualViewport', {
|
|
|
+ value: { height: 700 },
|
|
|
+ configurable: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Trigger near the visual-viewport bottom: bottom=650 + gap 4 + height
|
|
|
+ // 320 = 974 > 700-8. Without the fix (innerHeight=800), 974 > 800-8 is
|
|
|
+ // also true so it would flip — fine. But subtract: 650+324=974 > 792 (yes)
|
|
|
+ // — so flip happens with either. To prove visualViewport matters we need
|
|
|
+ // a trigger that fits *under innerHeight* but overflows *under
|
|
|
+ // visualViewport*: bottom=400, height=320, total=724. 724 < 800-8 = 792
|
|
|
+ // (no flip with innerHeight), but 724 > 700-8 = 692 (flip with
|
|
|
+ // visualViewport).
|
|
|
+ const trigger = { top: 380, bottom: 400, left: 400, right: 440 };
|
|
|
+ const pos = computePopoverPosition({
|
|
|
+ triggerRect: trigger,
|
|
|
+ popoverWidth: 240,
|
|
|
+ estimatedHeight: 320,
|
|
|
+ // Intentionally NO viewportHeight — exercise the default path.
|
|
|
+ viewportWidth: 1024,
|
|
|
+ });
|
|
|
+ // Above placement: trigger.top - gap - height = 380 - 4 - 320 = 56.
|
|
|
+ expect(pos.top).toBe(56);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('falls back to innerHeight when visualViewport is unavailable', () => {
|
|
|
+ // Some older WebViews / jsdom configurations don't expose visualViewport.
|
|
|
+ // @ts-expect-error — deleting an optional property on window
|
|
|
+ delete window.visualViewport;
|
|
|
+ Object.defineProperty(window, 'innerHeight', { value: 768, configurable: true });
|
|
|
+
|
|
|
+ // Trigger near the bottom should still flip above using innerHeight.
|
|
|
+ const trigger = { top: 680, bottom: 700, left: 400, right: 440 };
|
|
|
+ const pos = computePopoverPosition({
|
|
|
+ triggerRect: trigger,
|
|
|
+ popoverWidth: 240,
|
|
|
+ estimatedHeight: 320,
|
|
|
+ viewportWidth: 1024,
|
|
|
+ });
|
|
|
+ expect(pos.top).toBe(680 - 4 - 320); // 356 (trigger.top - gap - height)
|
|
|
+ });
|
|
|
+
|
|
|
+ it('respects an explicit viewportHeight even when visualViewport is set', () => {
|
|
|
+ // Tests pass viewportHeight explicitly; that override must still win.
|
|
|
+ Object.defineProperty(window, 'visualViewport', {
|
|
|
+ value: { height: 200 },
|
|
|
+ configurable: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ const pos = computePopoverPosition({
|
|
|
+ triggerRect: { top: 300, bottom: 320, left: 400, right: 440 },
|
|
|
+ popoverWidth: 240,
|
|
|
+ estimatedHeight: 320,
|
|
|
+ viewportHeight: 768,
|
|
|
+ viewportWidth: 1024,
|
|
|
+ });
|
|
|
+ // 320 + 320 = 640 < 768 - 8, so no flip — uses the override, not the 200.
|
|
|
+ expect(pos.top).toBe(324);
|
|
|
+ });
|
|
|
+});
|