Animate admin user menu sheet
This commit is contained in:
@@ -1,13 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChevronRight, CreditCard, FileText, HelpCircle, User, X } from 'lucide-react';
|
import { ChevronRight, CreditCard, FileText, HelpCircle, User, X } from 'lucide-react';
|
||||||
import { XStack, YStack } from '@tamagui/stacks';
|
import { XStack, YStack, SizableText as Text, ListItem, YGroup, Switch, Separator } from 'tamagui';
|
||||||
import { SizableText as Text } from '@tamagui/text';
|
|
||||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||||
import { ListItem } from '@tamagui/list-item';
|
|
||||||
import { YGroup } from '@tamagui/group';
|
|
||||||
import { Switch } from '@tamagui/switch';
|
|
||||||
import { Separator } from 'tamagui';
|
|
||||||
|
|
||||||
import { useAppearance } from '@/hooks/use-appearance';
|
import { useAppearance } from '@/hooks/use-appearance';
|
||||||
import { ADMIN_BILLING_PATH, ADMIN_DATA_EXPORTS_PATH, ADMIN_FAQ_PATH, ADMIN_PROFILE_ACCOUNT_PATH, adminPath } from '../../constants';
|
import { ADMIN_BILLING_PATH, ADMIN_DATA_EXPORTS_PATH, ADMIN_FAQ_PATH, ADMIN_PROFILE_ACCOUNT_PATH, adminPath } from '../../constants';
|
||||||
@@ -23,6 +18,7 @@ type UserMenuSheetProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MENU_WIDTH = 320;
|
const MENU_WIDTH = 320;
|
||||||
|
const MENU_CLOSED_OFFSET = MENU_WIDTH + 32;
|
||||||
|
|
||||||
export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserMenuSheetProps) {
|
export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserMenuSheetProps) {
|
||||||
const { t, i18n } = useTranslation('management');
|
const { t, i18n } = useTranslation('management');
|
||||||
@@ -84,18 +80,18 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
|||||||
zIndex={100000}
|
zIndex={100000}
|
||||||
pointerEvents={open ? 'auto' : 'none'}
|
pointerEvents={open ? 'auto' : 'none'}
|
||||||
>
|
>
|
||||||
<Pressable
|
<YStack
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
|
opacity={open ? 1 : 0}
|
||||||
|
fullscreen
|
||||||
|
backgroundColor={theme.overlay}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
transition: 'opacity 220ms ease',
|
||||||
inset: 0,
|
|
||||||
backgroundColor: theme.overlay,
|
|
||||||
opacity: open ? 1 : 0,
|
|
||||||
transition: 'opacity 180ms ease',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YStack
|
<YStack
|
||||||
|
data-testid="user-menu-sheet-panel"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={0}
|
top={0}
|
||||||
bottom={0}
|
bottom={0}
|
||||||
@@ -107,9 +103,10 @@ export function UserMenuSheet({ open, onClose, user, isMember, navigate }: UserM
|
|||||||
borderColor={theme.border}
|
borderColor={theme.border}
|
||||||
padding="$4"
|
padding="$4"
|
||||||
gap="$3"
|
gap="$3"
|
||||||
|
opacity={open ? 1 : 0}
|
||||||
style={{
|
style={{
|
||||||
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
transform: `translateX(${open ? 0 : MENU_CLOSED_OFFSET}px)`,
|
||||||
transition: 'transform 220ms ease',
|
transition: 'transform 260ms ease, opacity 260ms ease',
|
||||||
boxShadow: `-12px 0 24px ${theme.glassShadow ?? theme.shadow}`,
|
boxShadow: `-12px 0 24px ${theme.glassShadow ?? theme.shadow}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({ t: (key: string, fallback?: string) => fallback ?? key, i18n: { language: 'en-GB' } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||||
|
Pressable: ({ children, onPress, ...props }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||||
|
<button type="button" onClick={onPress} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('tamagui', () => {
|
||||||
|
const Stack = ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>;
|
||||||
|
const Text = ({ children, ...props }: { children: React.ReactNode }) => <span {...props}>{children}</span>;
|
||||||
|
const Switch = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
Switch.Thumb = () => <div />;
|
||||||
|
|
||||||
|
const ListItem = ({ title, iconAfter, ...props }: { title?: React.ReactNode; iconAfter?: React.ReactNode }) => (
|
||||||
|
<div {...props}>
|
||||||
|
{title}
|
||||||
|
{iconAfter}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const YGroup: any = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
YGroup.Item = ({ children }: { children: React.ReactNode }) => <div>{children}</div>;
|
||||||
|
|
||||||
|
return {
|
||||||
|
XStack: Stack,
|
||||||
|
YStack: Stack,
|
||||||
|
SizableText: Text,
|
||||||
|
ListItem,
|
||||||
|
YGroup,
|
||||||
|
Switch,
|
||||||
|
Separator: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../FormControls', () => ({
|
||||||
|
MobileSelect: ({ children }: { children: React.ReactNode }) => <select>{children}</select>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-appearance', () => ({
|
||||||
|
useAppearance: () => ({
|
||||||
|
appearance: 'system',
|
||||||
|
resolved: 'light',
|
||||||
|
updateAppearance: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../theme', () => ({
|
||||||
|
useAdminTheme: () => ({
|
||||||
|
overlay: 'rgba(0,0,0,0.3)',
|
||||||
|
surface: '#ffffff',
|
||||||
|
border: '#e5e7eb',
|
||||||
|
textStrong: '#111827',
|
||||||
|
muted: '#6b7280',
|
||||||
|
surfaceMuted: '#f3f4f6',
|
||||||
|
accentSoft: '#fef2f2',
|
||||||
|
primary: '#FF5A5F',
|
||||||
|
glassShadow: 'rgba(15,23,42,0.14)',
|
||||||
|
shadow: 'rgba(0,0,0,0.12)',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { UserMenuSheet } from '../UserMenuSheet';
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
onClose: vi.fn(),
|
||||||
|
user: { name: 'Ada Lovelace', email: 'ada@example.com' },
|
||||||
|
isMember: false,
|
||||||
|
navigate: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('UserMenuSheet', () => {
|
||||||
|
it('slides in when open', () => {
|
||||||
|
const { getByTestId } = render(<UserMenuSheet {...baseProps} open={true} />);
|
||||||
|
|
||||||
|
expect(getByTestId('user-menu-sheet-panel').style.transform).toBe('translateX(0px)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('slides out when closed', () => {
|
||||||
|
const { getByTestId } = render(<UserMenuSheet {...baseProps} open={false} />);
|
||||||
|
|
||||||
|
expect(getByTestId('user-menu-sheet-panel').style.transform).toMatch(/translateX\(\d+px\)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user