Skip to content

Commit f07344f

Browse files
RajdeepcRajdeep Chandra
andauthored
fix(picker): scrolling issue inside overlays in Safari iPad (#5868)
* fix(picker): ipad scroll issue fix * fix(picker): story * fix: reset changes in Menu and Picker used for testing * chore: added changeset * chore: added two frame delay in test * chore: added dev mdoe story * chore: updated double delay frame to exclude submenu * chore: updated double delay frame to exclude submenu * chore: revert menu * chore: revert async timing fix * chore: revert menu * fix: safari scroll binding issue with extra frame * fix: test * chore: removed counterproductive tests * chore: excluded the isWebkit check from the coverage --------- Co-authored-by: Rajdeep Chandra <rajdeepc@adobe.com>
1 parent 8a00f23 commit f07344f

File tree

5 files changed

+383
-11
lines changed

5 files changed

+383
-11
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@spectrum-web-components/overlay': patch
3+
'@spectrum-web-components/picker': patch
4+
---
5+
6+
**Fixed** issue where picker menus inside overlays could not scroll to the bottom after selecting an item and reopening. The problem was caused by the overlay's placement calculation happening before the menu fully rendered, resulting in incorrect height measurements.
7+
8+
This fix ensures picker menus maintain proper scrollable height when reopened, regardless of the selected item's position.

1st-gen/packages/menu/src/Menu.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,30 +103,30 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
103103

104104
/**
105105
* Minimum vertical movement (in pixels) required to trigger scrolling detection.
106-
*
106+
*
107107
* This threshold is consistent with other components in the design system:
108108
* - Card component uses 10px for click vs. drag detection
109109
* - Menu component uses 10px for scroll vs. selection detection
110-
*
110+
*
111111
* The 10px threshold is carefully chosen to:
112112
* - Allow for natural finger tremor and accidental touches
113113
* - Distinguish between intentional scroll gestures and taps
114114
* - Provide consistent behavior across the platform
115-
*
115+
*
116116
* @see {@link packages/card/src/Card.ts} for similar threshold usage
117117
*/
118118
private scrollThreshold = 10; // pixels
119119

120120
/**
121121
* Maximum time (in milliseconds) for a movement to be considered scrolling.
122-
*
122+
*
123123
* This threshold is consistent with other timing values in the design system:
124124
* - Longpress duration: 300ms (ActionButton, LongpressController)
125125
* - Scroll detection: 300ms (Menu component)
126-
*
126+
*
127127
* Quick movements within this timeframe are likely intentional scrolls,
128128
* while slower movements are more likely taps or selections.
129-
*
129+
*
130130
* @see {@link packages/action-button/src/ActionButton.ts} for longpress duration
131131
* @see {@link packages/overlay/src/LongpressController.ts} for longpress duration
132132
*/
@@ -576,7 +576,7 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
576576
* Resets the scrolling state after a short delay (100ms) to allow for
577577
* any final touch events to be processed. This delay prevents immediate
578578
* state changes that could interfere with the selection logic.
579-
*
579+
*
580580
* The 100ms delay is consistent with the design system's approach to
581581
* touch event handling and ensures that any final touch events or
582582
* gesture recognition can complete before the scrolling state is reset.
@@ -720,7 +720,7 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
720720
): MenuItem {
721721
const diff = before ? -1 : 1;
722722
const elements = this.rovingTabindexController?.elements || [];
723-
const index = !!menuItem ? elements.indexOf(menuItem) : -1;
723+
const index = menuItem ? elements.indexOf(menuItem) : -1;
724724
let newIndex = Math.min(Math.max(0, index + diff), elements.length - 1);
725725
while (
726726
!this.isFocusableElement(elements[newIndex]) &&
@@ -729,7 +729,7 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
729729
) {
730730
newIndex += diff;
731731
}
732-
return !!this.isFocusableElement(elements[newIndex])
732+
return this.isFocusableElement(elements[newIndex])
733733
? (elements[newIndex] as MenuItem)
734734
: menuItem || elements[0];
735735
}
@@ -1010,7 +1010,7 @@ export class Menu extends SizedMixin(SpectrumElement, { noDefaultSize: true }) {
10101010
}
10111011
});
10121012
}
1013-
if (!!this._updateFocus) {
1013+
if (this._updateFocus) {
10141014
this.rovingTabindexController?.focusOnItem(this._updateFocus);
10151015
this._updateFocus = undefined;
10161016
}

1st-gen/packages/overlay/src/PlacementController.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
shift,
2525
size,
2626
} from '@floating-ui/dom';
27+
import { isWebKit } from '@spectrum-web-components/shared';
2728
import type { VirtualTrigger } from './VirtualTrigger.js';
2829
import type { OpenableElement } from './overlay-types.js';
2930
import type { Overlay } from './Overlay.js';
@@ -278,6 +279,28 @@ export class PlacementController implements ReactiveController {
278279
// Wait for document fonts to be ready before computing placement.
279280
await (document.fonts ? document.fonts.ready : Promise.resolve());
280281

282+
// Safari/iOS-specific fix: Add small delay for picker menus to allow scrollIntoView to complete
283+
// Check if this is a submenu overlay (slot="submenu")
284+
// Submenus need immediate positioning for hover responsiveness
285+
const isSubmenu = Array.from(this.host.elements).some(
286+
(el) => el.getAttribute?.('slot') === 'submenu'
287+
);
288+
289+
// Safari-specific timing fix covered by cross-browser integration tests
290+
/* c8 ignore start */
291+
if (isWebKit() && !isSubmenu) {
292+
const hasMenu = Array.from(this.host.elements).some(
293+
(el) =>
294+
el.tagName === 'SP-MENU' || el.querySelector?.('sp-menu')
295+
);
296+
297+
if (hasMenu) {
298+
// Wait 1 frame for Safari layout to settle after scrollIntoView
299+
await new Promise((resolve) => requestAnimationFrame(resolve));
300+
}
301+
}
302+
/* c8 ignore stop */
303+
281304
// Determine the flip middleware based on the type of trigger element.
282305
const flipMiddleware = !(options.trigger instanceof HTMLElement)
283306
? flip({

1st-gen/packages/overlay/test/overlay.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
import { render, TemplateResult } from '@spectrum-web-components/base';
3636
import { Button } from '@spectrum-web-components/button';
3737
import { Menu } from '@spectrum-web-components/menu';
38+
import '@spectrum-web-components/menu/sp-menu.js';
39+
import '@spectrum-web-components/menu/sp-menu-item.js';
3840
import { Theme } from '@spectrum-web-components/theme';
3941
import '@spectrum-web-components/theme/sp-theme.js';
4042
import '@spectrum-web-components/theme/src/themes.js';
@@ -54,7 +56,7 @@ import {
5456
clickAndHoverTarget,
5557
definedOverlayElement,
5658
virtualElement,
57-
} from '../stories/overlay.stories';
59+
} from '../stories/overlay.stories.js';
5860
// import { isWebKit } from '@spectrum-web-components/shared';
5961

6062
async function styledFixture<T extends Element>(
@@ -476,6 +478,7 @@ describe('Overlays', () => {
476478

477479
const initial = el.getBoundingClientRect();
478480
trigger.updateBoundingClientRect(500, 500);
481+
// Wait for placement computation to complete
479482
await nextFrame();
480483
await nextFrame();
481484
const final = el.getBoundingClientRect();

0 commit comments

Comments
 (0)