Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/app/components/MobileSwipeDownModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ describe('MobileSwipeDownModal', () => {
expect(panel).toHaveStyle({ backgroundColor: '#663399' });
});

it('can overlay the drag handle without reserving a header row', () => {
render(
<MobileSwipeDownModal requestClose={vi.fn<() => void>()} overlayDragHandle>
{() => <div data-testid="under-handle-content" />}
</MobileSwipeDownModal>
);

expect(screen.getByTestId('mobile-sheet-drag-handle')).toHaveStyle({
position: 'absolute',
top: '0px',
});
expect(screen.getByTestId('under-handle-content').parentElement?.parentElement).toHaveStyle({
overflow: 'hidden',
});
});

it('lifts the sheet clear of the keyboard with a transition, without resizing it', () => {
const viewport = mockVisualViewport(800);
let contentRenderCount = 0;
Expand Down Expand Up @@ -468,23 +484,27 @@ describe('MobileSwipeDownModal', () => {
}
});

it('leaves a marked scroll container to native scrolling at its top', async () => {
it('leaves the gesture to an outer list scrolled away from the top', async () => {
const requestClose = vi.fn<() => void>();
const restore = stubElementAnimations();

try {
render(
<MobileSwipeDownModal requestClose={requestClose}>
{() => (
<div data-mobile-sheet-no-drag="" data-testid="marked-scroller">
<div data-testid="marked-scroll-row" />
<div data-testid="outer-scroller" style={{ overflowY: 'auto' }}>
<div data-testid="inner-scroller" style={{ overflowY: 'auto' }}>
<div data-testid="scroll-row" />
</div>
</div>
)}
</MobileSwipeDownModal>
);
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 {
Expand Down
25 changes: 17 additions & 8 deletions src/app/components/MobileSwipeDownModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface MobileSwipeDownModalProps {
sheetStyle?: CSSProperties;
keyboardAware?: boolean;
zIndex?: number;
overlayDragHandle?: boolean;
}

type FocusTrapOptions = ComponentProps<typeof FocusTrap>['focusTrapOptions'];
Expand All @@ -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() {
Expand Down Expand Up @@ -104,6 +106,7 @@ export function MobileSwipeDownModal({
sheetStyle,
keyboardAware = false,
zIndex,
overlayDragHandle = false,
}: MobileSwipeDownModalProps) {
const sheetRef = useRef<HTMLDivElement>(null);
const backdropTouchRef = useRef(false);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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]: '' }}
>
<div className={css.MessageMobileDragIndicator} />
Expand Down Expand Up @@ -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()}
Expand Down
9 changes: 8 additions & 1 deletion src/app/components/ResponsiveMenu.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
});
3 changes: 3 additions & 0 deletions src/app/components/ResponsiveMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ResponsiveMenuProps = {
surfaceColor?: string;
/** Raises a mobile sheet above a parent fullscreen overlay. */
mobileZIndex?: number;
overlayDragHandle?: boolean;
};

function MenuDialog({
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -198,6 +200,7 @@ export function ResponsiveMenu({
requestClose={requestClose}
sheetStyle={sheetStyle}
zIndex={mobileZIndex}
overlayDragHandle={overlayDragHandle}
>
{() => (
<MobileSheetFocusTrap
Expand Down
1 change: 1 addition & 0 deletions src/app/components/UserRoomProfileRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState })
align={cords.y > window.innerHeight / 2 ? 'End' : 'Start'}
returnFocusOnDeactivate
surfaceColor={surfaceColor}
overlayDragHandle
menu={
<Menu style={{ width: toRem(340) }}>
<SpaceProvider value={space ?? null}>
Expand Down
4 changes: 4 additions & 0 deletions src/app/components/user-profile/PowerChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ export function PowerChip({
style={{
padding: config.space.S100,
maxWidth: toRem(200),
maxHeight: 'calc(80vh - 5rem)',
overflowY: 'auto',
overscrollBehavior: 'contain',
touchAction: 'pan-y',
backgroundColor: innerColor,
}}
>
Expand Down
13 changes: 11 additions & 2 deletions src/app/components/user-profile/UserChips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,17 @@ export function MutualRoomsChip({
backgroundColor: innerColor,
}}
>
<Box grow="Yes">
<Scroll size="300" hideTrack>
<Box grow="Yes" style={{ minHeight: 0 }}>
<Scroll
size="300"
hideTrack
style={{
maxHeight: 'calc(80vh - 2rem)',
overflowY: 'auto',
overscrollBehavior: 'contain',
touchAction: 'pan-y',
}}
>
<Box
direction="Column"
gap="400"
Expand Down
11 changes: 10 additions & 1 deletion src/app/components/user-profile/UserRoomProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,16 @@ export function UserRoomProfile({
};

return (
<Box direction="Column" style={{ color: textColor }}>
<Box
direction="Column"
style={{
color: textColor,
maxHeight: 'calc(85vh - 2rem)',
overflowY: 'auto',
overscrollBehavior: 'contain',
touchAction: 'pan-y',
}}
>
<UserHero
userId={userId}
avatarUrl={avatarUrl}
Expand Down
1 change: 0 additions & 1 deletion src/app/features/room/room-pin-menu/RoomPinMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,6 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
<Box grow="Yes">
<Scroll
ref={scrollRef}
data-mobile-sheet-no-drag=""
size="300"
hideTrack
visibility="Hover"
Expand Down
Loading