diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.module.scss b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.module.scss
index 56ab0470784..a65a7432490 100644
--- a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.module.scss
+++ b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.module.scss
@@ -31,9 +31,7 @@
box-sizing: border-box;
display: flex;
align-items: center;
- align-self: center;
- width: 100%;
- max-width: 400px;
+ align-self: stretch;
padding: var(--space-2xs);
background-color: var(--color-bg-primary);
border-radius: var(--radius-round);
@@ -48,3 +46,35 @@
.avatar img {
border-radius: var(--radius-round);
}
+
+.slideOutWrapper {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ pointer-events: none;
+ background: rgb(from var(--color-bg-overlay) r g b/50%);
+ opacity: 0;
+ transition: opacity 250ms ease;
+
+ &[data-is-open='true'] {
+ pointer-events: auto;
+ opacity: 1;
+ }
+}
+
+.slideOut {
+ // We extend the size of the slideOut to go beyond the viewport
+ // so that when it's pulled away from the side it was opened from,
+ // the background underneath the menu isn't revealed
+ --pull-tolerance: 70px;
+
+ position: fixed;
+ inset-block: 0;
+ inset-inline-start: calc(-1 * var(--pull-tolerance));
+ width: var(--navigation-max-width);
+ max-width: calc(100vw - var(--space-3xl));
+ padding-inline-start: var(--pull-tolerance);
+ touch-action: pan-y;
+ background: var(--color-bg-primary);
+ box-shadow: var(--dropdown-shadow);
+}
diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx
index d841c6e782b..fed7ddf63be 100644
--- a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx
+++ b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx
@@ -1,71 +1,223 @@
+import { useCallback, useEffect, useRef } from 'react';
+
import { FormattedMessage } from 'react-intl';
+import { useLocation } from 'react-router';
+
import {
BellIcon,
ChatCircleIcon,
HouseIcon,
MagnifyingGlassIcon,
+ HamburgerIcon,
} from '@phosphor-icons/react';
+import { animated, useSpring } from '@react-spring/web';
+import { useDrag } from '@use-gesture/react';
+import { closeNavigation, openNavigation } from '@/mastodon/actions/navigation';
import { Avatar } from '@/mastodon/components/avatar';
+import { IconButton } from '@/mastodon/components/button/redesign';
import { FOCUS_TARGET } from '@/mastodon/components/navigation_focus_target';
import { ComposeRedesignButton } from '@/mastodon/features/compose/redesign/trigger';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useIdentity } from '@/mastodon/identity_context';
import { selectUnreadNotificationGroupsCount } from '@/mastodon/selectors/notifications';
-import { useAppSelector } from '@/mastodon/store';
+import { useAppDispatch, useAppSelector } from '@/mastodon/store';
+import { RedesignNavigationPanel } from '.';
import classes from './mobile_nav.module.scss';
import { MobileNavLink } from './navigation_link';
export const RedesignMobileNavigation: React.FC = () => {
- const { accountId } = useIdentity();
+ const dispatch = useAppDispatch();
+
+ const { accountId, signedIn } = useIdentity();
const account = useAccount(accountId);
+
const notificationsCount = useAppSelector(
selectUnreadNotificationGroupsCount,
);
+
+ const handleOpenNavigation = useCallback(() => {
+ dispatch(openNavigation());
+ }, [dispatch]);
+
+ if (!signedIn) {
+ return null;
+ }
+
return (
-
+ Menu
+
+
+
+ >
+ );
+};
+
+const MENU_WIDTH = 320;
+
+const SlideOutNavigation: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const location = useLocation();
+ const overlayRef = useRef(null);
+
+ const isOpen = useAppSelector((state) => state.navigation.open);
+
+ useEffect(() => {
+ dispatch(closeNavigation());
+ }, [dispatch, location]);
+
+ useEffect(() => {
+ const handleDocumentClick = (e: MouseEvent) => {
+ if (overlayRef.current && e.target === overlayRef.current) {
+ dispatch(closeNavigation());
+ }
+ };
+
+ const handleDocumentKeyUp = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ dispatch(closeNavigation());
+ }
+ };
+
+ document.addEventListener('click', handleDocumentClick);
+ document.addEventListener('keyup', handleDocumentKeyUp);
+
+ return () => {
+ document.removeEventListener('click', handleDocumentClick);
+ document.removeEventListener('keyup', handleDocumentKeyUp);
+ };
+ }, [dispatch]);
+
+ const isLtrDir = getComputedStyle(document.body).direction !== 'rtl';
+
+ const OPEN_MENU_OFFSET = isLtrDir ? -MENU_WIDTH : MENU_WIDTH;
+
+ const [{ x }, spring] = useSpring(
+ () => ({
+ x: isOpen ? 0 : OPEN_MENU_OFFSET,
+ onRest: {
+ x({ value }: { value: number }) {
+ if (value === 0) {
+ dispatch(openNavigation());
+ } else if (value === OPEN_MENU_OFFSET) {
+ dispatch(closeNavigation());
+ }
+ },
+ },
+ }),
+ [isOpen],
+ );
+
+ const bind = useDrag(
+ ({
+ last,
+ offset: [xOffset],
+ velocity: [xVelocity],
+ direction: [xDirection],
+ cancel,
+ }) => {
+ const logicalXDirection = isLtrDir ? -xDirection : xDirection;
+ const logicalXOffset = isLtrDir ? -xOffset : xOffset;
+ const hasReachedDragThreshold = logicalXOffset < -70;
+
+ if (hasReachedDragThreshold) {
+ cancel();
+ }
+
+ if (last) {
+ const isAboveOpenThreshold = logicalXOffset > MENU_WIDTH / 2;
+ const isQuickFlick = xVelocity > 0.5 && logicalXDirection > 0;
+
+ if (isAboveOpenThreshold || isQuickFlick) {
+ void spring.start({ x: OPEN_MENU_OFFSET });
+ } else {
+ void spring.start({ x: 0 });
+ }
+ } else {
+ void spring.start({ x: xOffset, immediate: true });
+ }
+ },
+ {
+ from: () => [x.get(), 0],
+ axis: 'x',
+ filterTaps: true,
+ bounds: isLtrDir ? { right: 0 } : { left: 0 },
+ rubberband: true,
+ },
+ );
+
+ const previouslyFocusedElementRef = useRef(null);
+
+ useEffect(() => {
+ const overlay = overlayRef.current;
+ if (isOpen && overlay) {
+ const firstLink = overlay.querySelector('a');
+ previouslyFocusedElementRef.current =
+ document.activeElement as HTMLElement;
+ firstLink?.focus();
+ } else {
+ previouslyFocusedElementRef.current?.focus();
+ }
+ }, [isOpen]);
+
+ return (
+
);
};
diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss b/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss
index 4cff65ccb57..15f65d84bc6 100644
--- a/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss
+++ b/app/javascript/mastodon/features/navigation_panel/redesign/navigation_link.module.scss
@@ -1,4 +1,5 @@
@use '@/styles/mastodon/mixins';
+@use '@/styles/mastodon/variables';
.wrapper {
padding-bottom: var(--space-3xs);
@@ -33,6 +34,10 @@
color: var(--color-text-secondary);
}
+ @media (width < variables.$mobile-menu-breakpoint) {
+ min-height: 40px;
+ }
+
&:hover,
&:focus-visible,
&[aria-current='page'] {
diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss b/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss
index 29bac75d290..1159ba26c4c 100644
--- a/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss
+++ b/app/javascript/mastodon/features/navigation_panel/redesign/styles.module.scss
@@ -7,6 +7,14 @@
height: 100%;
overflow-y: auto;
scrollbar-width: thin;
+
+ // Approximate height of fixed navigation elements
+ // to ensure focused elements are scrolled into view
+ // as needed.
+ --header-height: 80px;
+ --footer-height: 200px;
+
+ scroll-padding-block: var(--header-height) var(--footer-height);
}
.list {
diff --git a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx
index e53485421a5..6bac9746e4a 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx
+++ b/app/javascript/mastodon/features/ui/components/columns_area/redesign.tsx
@@ -3,7 +3,6 @@ import { useCallback } from 'react';
import classNames from 'classnames';
import { ComposeRedesignButton } from '@/mastodon/features/compose/redesign/trigger';
-import { CollapsibleNavigationPanel } from '@/mastodon/features/navigation_panel';
import { RedesignNavigationPanel } from '@/mastodon/features/navigation_panel/redesign';
import { RedesignMobileNavigation } from '@/mastodon/features/navigation_panel/redesign/mobile_nav';
import { useAppSelector } from '@/mastodon/store';
@@ -74,11 +73,6 @@ export const ColumnsAreaRedesign: React.FC<{
{children}
-
- {isMobile && (
- // Legacy mobile navigation, to be removed
-
- )}
);