Redesign: Show menu as sheet on mobile (#40441)

This commit is contained in:
diondiondion
2026-09-09 16:16:08 +00:00
committed by GitHub
parent e2097e8c51
commit e4af40477b
4 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
import type { ComponentPropsWithRef } from 'react';
import { useLayoutEffect, useRef } from 'react';
import classNames from 'classnames';
import { useMergedRefs } from '@/mastodon/hooks/useMergedRefs';
import { useOnClickOutside } from '@/mastodon/hooks/useOnClickOutside';
import { useScrollSensor } from '@/mastodon/hooks/useScrollSensor';
import classes from './styles.module.scss';
interface BottomSheetProps extends Omit<
ComponentPropsWithRef<'dialog'>,
'onClose'
> {
onClose: (e: Event) => void;
}
export const BottomSheet: React.FC<BottomSheetProps> = ({
children,
className,
onClose,
...props
}) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
dialog.showModal();
return () => {
dialog.requestClose();
};
}, []);
useOnClickOutside(contentRef, (e) => {
onClose(e);
});
const { sensor, isInViewport } = useScrollSensor({
placement: 'bottom',
tolerance: 10,
});
return (
<dialog
{...props}
className={classNames(
classes.dialog,
!isInViewport && classes.withGradient,
)}
ref={useMergedRefs(props.ref, dialogRef)}
>
<div ref={contentRef} className={classNames(className, classes.sheet)}>
{children}
</div>
{sensor}
</dialog>
);
};

View File

@@ -0,0 +1,44 @@
@use '@/styles/mastodon/mixins';
.dialog {
width: 100%;
max-width: unset;
max-height: 100dvh;
margin: initial;
margin-top: auto;
padding: 0;
border: none;
background: none;
overscroll-behavior: contain;
&::backdrop {
opacity: 0.9;
background-color: var(--color-bg-overlay);
}
&::after {
@include mixins.scroll-gradient(bottom);
position: fixed;
bottom: 0;
opacity: 0;
transition: opacity ease 250ms;
}
}
.withGradient::after {
opacity: 1;
}
.sheet {
padding: var(--space-md);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
background: var(--color-bg-primary);
overflow: hidden;
text-align: start;
@include mixins.elevation-1;
border-inline: none;
border-bottom: none;
}

View File

@@ -1,7 +1,9 @@
import classNames from 'classnames';
import { useBreakpoint } from '@/mastodon/features/ui/hooks/useBreakpoint';
import type { PolymorphicProps } from '@/types/polymorphic';
import { BottomSheet } from '../bottom_sheet';
import { Popover } from '../popover';
import type { PopoverProps } from '../popover';
@@ -65,6 +67,16 @@ export const PopoverMenuCard = <As extends React.ElementType>({
className,
...props
}: PopoverMenuCardProps<As>) => {
const isMobile = useBreakpoint('openable');
if (isMobile && isOpen) {
return (
<BottomSheet {...props} onClose={onClose}>
{children}
</BottomSheet>
);
}
return (
<Popover
isOpen={isOpen}

View File

@@ -1,4 +1,5 @@
@use '@/styles/mastodon/mixins';
@use '@/styles/mastodon/variables';
.card {
@include mixins.elevation-1;
@@ -41,6 +42,7 @@
--cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
border-radius: var(--radius-sm);
@@ -61,6 +63,10 @@
text-align: start;
}
@media (width < variables.$mobile-menu-breakpoint) {
min-height: 48px;
}
&:hover {
text-decoration: none;
}