-
+
)}
);
await act(async () => {});
+ makeScrollable(screen.getByTestId('outer-scroller'), 120);
+ makeScrollable(screen.getByTestId('inner-scroller'), 0);
- dragDown(screen.getByTestId('marked-scroll-row'), 100, 240);
+ dragDown(screen.getByTestId('scroll-row'), 100, 240);
expect(requestClose).not.toHaveBeenCalled();
} finally {
diff --git a/src/app/components/MobileSwipeDownModal.tsx b/src/app/components/MobileSwipeDownModal.tsx
index 0b5126f81..80cc1fdc7 100644
--- a/src/app/components/MobileSwipeDownModal.tsx
+++ b/src/app/components/MobileSwipeDownModal.tsx
@@ -29,6 +29,7 @@ interface MobileSwipeDownModalProps {
sheetStyle?: CSSProperties;
keyboardAware?: boolean;
zIndex?: number;
+ overlayDragHandle?: boolean;
}
type FocusTrapOptions = ComponentProps
['focusTrapOptions'];
@@ -52,20 +53,21 @@ function getKeyboardOverlap(): number {
return Math.max(0, window.innerHeight - viewport.offsetTop - viewport.height);
}
-/** Nearest ancestor between `from` and the sheet that can actually scroll vertically. */
-function findScroller(from: HTMLElement | null, boundary: HTMLElement): HTMLElement | null {
+/** Whether any scrollable ancestor is scrolled away from its top edge. */
+function hasScrolledAncestor(from: HTMLElement | null, boundary: HTMLElement): boolean {
let element = from;
while (element && element !== boundary) {
const { overflowY } = window.getComputedStyle(element);
if (
(overflowY === 'auto' || overflowY === 'scroll') &&
- element.scrollHeight > element.clientHeight
+ element.scrollHeight > element.clientHeight &&
+ element.scrollTop > 0
) {
- return element;
+ return true;
}
element = element.parentElement;
}
- return null;
+ return false;
}
export function useMobileSheetClose() {
@@ -104,6 +106,7 @@ export function MobileSwipeDownModal({
sheetStyle,
keyboardAware = false,
zIndex,
+ overlayDragHandle = false,
}: MobileSwipeDownModalProps) {
const sheetRef = useRef(null);
const backdropTouchRef = useRef(false);
@@ -240,12 +243,13 @@ export function MobileSwipeDownModal({
const from = target instanceof HTMLElement ? target : null;
// The handle is not over any content, so it always drags.
if (from?.closest(`[${HANDLE_ATTRIBUTE}]`)) return true;
+ // Mobile menu actions activate on pointer-up in Android WebView, so dismissing
+ // from one could also invoke the action.
if (from?.closest(`[${NO_DRAG_ATTRIBUTE}]`)) return false;
if (window.getSelection()?.toString()) return false;
if (Date.now() - dragBlockedAtRef.current < DRAG_BLOCKED_COOLDOWN_MS) return false;
- const scroller = findScroller(from, sheet);
- if (scroller && scroller.scrollTop > 0) {
+ if (hasScrolledAncestor(from, sheet)) {
dragBlockedAtRef.current = Date.now();
return false;
}
@@ -308,6 +312,7 @@ export function MobileSwipeDownModal({
className={css.MessageMobileDragHandle}
data-gestures="ignore"
data-testid="mobile-sheet-drag-handle"
+ style={overlayDragHandle ? { position: 'absolute', top: 0, right: 0, left: 0 } : undefined}
{...{ [HANDLE_ATTRIBUTE]: '' }}
>
@@ -381,7 +386,11 @@ export function MobileSwipeDownModal({
className={`${css.MessageMobileOptionsContainer} ${sheetClassName ?? ''} ${
portalRef ? css.MessageMobileOptionsContainerContained : ''
} ${animationCss.SheetEntrance}`}
- style={{ ...(shouldReduceMotion ? { animation: 'none' } : undefined), ...sheetStyle }}
+ style={{
+ ...(overlayDragHandle ? { overflow: 'hidden' } : undefined),
+ ...(shouldReduceMotion ? { animation: 'none' } : undefined),
+ ...sheetStyle,
+ }}
onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
onPointerMove={(e: React.PointerEvent) => e.stopPropagation()}
onPointerUp={(e: React.PointerEvent) => e.stopPropagation()}
diff --git a/src/app/components/ResponsiveMenu.css.ts b/src/app/components/ResponsiveMenu.css.ts
index 31d8d34f3..1855d1ad0 100644
--- a/src/app/components/ResponsiveMenu.css.ts
+++ b/src/app/components/ResponsiveMenu.css.ts
@@ -32,9 +32,10 @@ export const SheetContent = style({
// drag handle.
globalStyle(`${SheetContent} > *:last-child`, {
// !important beats the inline maxWidth/width the callers set for their desktop popout.
+ // Do not override maxHeight: callers use it to give nested Scroll elements a
+ // definite scrollport.
width: '100% !important',
maxWidth: 'none !important',
- maxHeight: '100%',
position: 'relative',
display: 'flex',
flexDirection: 'column',
@@ -45,3 +46,9 @@ globalStyle(`${SheetContent} > *:last-child`, {
paddingTop: '0 !important',
paddingBottom: `${config.space.S400} !important`,
});
+
+// Desktop menus often constrain direct content groups instead of the menu itself.
+// Let those groups stretch with the mobile sheet while preserving their desktop width.
+globalStyle(`${SheetContent} > *:last-child > *`, {
+ maxWidth: 'none !important',
+});
diff --git a/src/app/components/ResponsiveMenu.tsx b/src/app/components/ResponsiveMenu.tsx
index 51b1620d2..1105e123c 100644
--- a/src/app/components/ResponsiveMenu.tsx
+++ b/src/app/components/ResponsiveMenu.tsx
@@ -33,6 +33,7 @@ type ResponsiveMenuProps = {
surfaceColor?: string;
/** Raises a mobile sheet above a parent fullscreen overlay. */
mobileZIndex?: number;
+ overlayDragHandle?: boolean;
};
function MenuDialog({
@@ -141,6 +142,7 @@ export function ResponsiveMenu({
mobile = 'sheet',
surfaceColor,
mobileZIndex,
+ overlayDragHandle = false,
}: ResponsiveMenuProps) {
// Null outside a provider, where desktop is the safe assumption.
const isMobile = useScreenSizeOptionally() === ScreenSize.Mobile;
@@ -198,6 +200,7 @@ export function ResponsiveMenu({
requestClose={requestClose}
sheetStyle={sheetStyle}
zIndex={mobileZIndex}
+ overlayDragHandle={overlayDragHandle}
>
{() => (
window.innerHeight / 2 ? 'End' : 'Start'}
returnFocusOnDeactivate
surfaceColor={surfaceColor}
+ overlayDragHandle
menu={