Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
28 KiB
Native Menu Lösung für Expo SDK 54 - Empfehlung
Executive Summary
Status: ✅ LÖSUNG GEFUNDEN
Unser Projekt hat bereits @react-native-menu/menu@2.0.0 installiert (als Zeego-Dependency). Diese Library wurde im September 2023 auf React Native 0.81 aktualisiert und ist damit SDK 54 kompatibel. Wir können Zeego entfernen und direkt mit @react-native-menu/menu arbeiten - kombiniert mit Radix UI für Web.
Zeitaufwand: 8-12 Stunden Risiko: Niedrig - Library ist bereits installiert und getestet Empfehlung: ⭐ Primäre Lösung
Problemanalyse (Nochmal zur Klarstellung)
Warum funktioniert Zeego nicht?
Zeego v3.0.6 ist für Expo SDK 52 (React Native 0.76/0.77) optimiert, nicht für SDK 54 (React Native 0.81). Der Fehler:
Unable to resolve module react-native-ios-context-menu
tritt auf, weil die Zeego-Dependencies (react-native-ios-context-menu und react-native-ios-utilities) einen RCT-Folly Versionskonflikt mit React Native 0.81 haben.
GitHub Issue #173: Build fails with Expo 54 - noch nicht gelöst.
Was bietet Expo UI?
Expo UI (@expo/ui) ist kein Context/Dropdown Menu Framework. Es bietet nur:
- Native Input Components für Jetpack Compose (Android) und SwiftUI (iOS)
- Keine Menu-Komponenten
Expo UI ist also keine Lösung für unser Problem.
Die Lösung: @react-native-menu/menu (Bereits installiert!)
Warum @react-native-menu/menu?
✅ Bereits im Projekt: Version 2.0.0 ist installiert (war Zeego-Dependency) ✅ SDK 54 Ready: v2.0.0 wurde auf React Native 0.81 aktualisiert (September 2023) ✅ Native iOS & Android Menus: UIMenu (iOS 14+) und PopupMenu (Android) ✅ Aktiv maintained: Letztes Update vor 14 Tagen (Stand: September 2025) ✅ Expo Config Plugin: Offizielle Expo-Integration ✅ Feature-reich: Icons, Submenus, Events (onOpenMenu, onCloseMenu)
Library Status
Current Version: 2.0.0 Last Updated: Vor 14 Tagen (September 2025) React Native Support: 0.81+ (getestet im Example-Projekt) iOS: 14+ (UIMenu nativ), iOS 13 Fallback zu ActionSheet Android: PopupMenu (native Android API) Expo: ✅ Expo Config Plugin vorhanden
Architektur-Übersicht
Cross-Platform Strategie
┌─────────────────────────────────────────────────────┐
│ useNativeMenu() │
│ (Unified Hook Interface) │
└─────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ iOS │ │ Android │ │ Web │
└─────────┘ └─────────┘ └─────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UIMenu │ │ PopupMenu │ │ Radix UI │
│ (Native) │ │ (Native) │ │ (Web-only) │
└─────────────┘ └─────────────┘ └─────────────┘
Platform-Specific Implementation
iOS: @react-native-menu/menu → UIMenu API
Android: @react-native-menu/menu → PopupMenu API
Web: Radix UI Dropdown/Context Menu (bereits von Zeego bekannt)
Feature-Vergleich
| Feature | Zeego | @react-native-menu/menu + Radix | Action Sheet |
|---|---|---|---|
| Native iOS Context Menu | ✅ | ✅ | ❌ |
| Native Android Menu | ✅ | ✅ | ❌ |
| Web Support | ✅ Radix | ✅ Radix | ❌ |
| Expo SDK 54 | ❌ Broken | ✅ | ✅ |
| React Native 0.81 | ❌ | ✅ | ✅ |
| Icons (SF Symbols) | ✅ | ✅ | ❌ |
| Submenus | ✅ | ✅ | ❌ |
| Checkboxes | ✅ | ⚠️ Eingeschränkt | ❌ |
| Custom Styling | ✅ | ⚠️ Native Styling | ✅ |
| Unified API | ✅ | ❌ (Platform Switch) | N/A |
| Maintenance Status | ⚠️ SDK 54 Issue | ✅ Aktiv | ✅ Expo Official |
| Setup Complexity | Mittel | Mittel | Niedrig |
Code-Beispiele
1. iOS/Android Native Menu (Context Menu)
// components/organisms/Memory.tsx
import { Platform } from 'react-native';
import { MenuView } from '@react-native-menu/menu';
export const Memory = ({ memory, onDelete, onShare }) => {
return (
<MenuView
title="Memory Actions"
actions={[
{
id: 'share',
title: 'Teilen',
titleColor: '#007AFF',
image: Platform.select({
ios: 'square.and.arrow.up',
android: 'ic_menu_share',
}),
},
{
id: 'delete',
title: 'Löschen',
attributes: {
destructive: true,
},
image: Platform.select({
ios: 'trash',
android: 'ic_menu_delete',
}),
},
]}
onPressAction={({ nativeEvent }) => {
switch (nativeEvent.event) {
case 'share':
onShare();
break;
case 'delete':
onDelete();
break;
}
}}
>
<MemoryContent memory={memory} />
</MenuView>
);
};
2. iOS/Android Dropdown Menu
// features/menus/HeaderMenu.tsx
import { Platform, Pressable } from 'react-native';
import { MenuView } from '@react-native-menu/menu';
export const HeaderMenu = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<MenuView
title="Einstellungen"
onPressAction={({ nativeEvent }) => {
if (nativeEvent.event === 'profile') {
navigation.navigate('Profile');
} else if (nativeEvent.event === 'settings') {
navigation.navigate('Settings');
}
}}
actions={[
{
id: 'profile',
title: 'Profil',
image: Platform.select({
ios: 'person.circle',
android: 'ic_menu_myplaces',
}),
},
{
id: 'settings',
title: 'Einstellungen',
image: Platform.select({
ios: 'gearshape',
android: 'ic_menu_preferences',
}),
},
]}
shouldOpenOnLongPress={false} // Öffnet auf normalen Press (nicht long press)
>
<Pressable>
<Icon name="ellipsis-horizontal" />
</Pressable>
</MenuView>
);
};
3. Web - Radix UI Dropdown
// components/menus/HeaderMenu.web.tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import './HeaderMenu.css'; // Radix UI styling
export const HeaderMenu = () => {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className="IconButton">
<Icon name="ellipsis-horizontal" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="DropdownMenuContent">
<DropdownMenu.Item
className="DropdownMenuItem"
onSelect={() => navigation.navigate('Profile')}
>
Profil
</DropdownMenu.Item>
<DropdownMenu.Item
className="DropdownMenuItem"
onSelect={() => navigation.navigate('Settings')}
>
Einstellungen
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
};
4. Unified Hook (Cross-Platform Abstraction)
// hooks/useNativeMenu.ts
import { Platform } from 'react-native';
import { MenuView } from '@react-native-menu/menu';
export interface MenuAction {
id: string;
title: string;
destructive?: boolean;
icon?: string;
onPress: () => void;
}
export const useNativeMenu = () => {
const showMenu = (actions: MenuAction[]) => {
if (Platform.OS === 'web') {
// Web: Return props for Radix UI
return {
isWeb: true,
actions,
};
}
// Native: Return props for MenuView
return {
isWeb: false,
menuActions: actions.map(action => ({
id: action.id,
title: action.title,
attributes: action.destructive ? { destructive: true } : undefined,
image: action.icon ? Platform.select({
ios: action.icon,
android: `ic_menu_${action.icon}`,
}) : undefined,
})),
onPressAction: ({ nativeEvent }) => {
const action = actions.find(a => a.id === nativeEvent.event);
action?.onPress();
},
};
};
return { showMenu };
};
5. Platform-Specific Component
// components/molecules/MemoPreview.tsx
import { Platform } from 'react-native';
import { MenuView } from '@react-native-menu/menu';
// Separate Web-Komponente importieren
const MemoPreviewWeb = Platform.OS === 'web'
? require('./MemoPreview.web').default
: null;
export const MemoPreview = ({ memo }) => {
// Web: Verwende Radix UI
if (Platform.OS === 'web') {
return <MemoPreviewWeb memo={memo} />;
}
// Native: Verwende @react-native-menu/menu
return (
<MenuView
actions={[
{ id: 'edit', title: 'Bearbeiten', image: 'pencil' },
{ id: 'delete', title: 'Löschen', attributes: { destructive: true } },
]}
onPressAction={({ nativeEvent }) => {
// Handle action
}}
>
<MemoContent memo={memo} />
</MenuView>
);
};
Migrations-Plan
Phase 1: Setup & Dependencies (2 Stunden)
1.1 Dependencies bereinigen
# Zeego entfernen
npm uninstall zeego
# Zeego iOS-Dependencies entfernen
npm uninstall react-native-ios-context-menu react-native-ios-utilities
# @react-native-menu/menu ist bereits installiert (v2.0.0)
# Radix UI für Web installieren
npm install @radix-ui/react-dropdown-menu @radix-ui/react-context-menu
1.2 Expo Config Plugin
app.json ist bereits konfiguriert (Zeego hatte die gleiche Dependency):
{
"plugins": [
"@react-native-menu/menu"
]
}
1.3 Rebuild
# Clean prebuild
npx expo prebuild --clean
# iOS
npx expo run:ios
# Android
npx expo run:android
Phase 2: Dropdown Menus Migration (4-5 Stunden)
Komponenten (11 Stück):
- ✅
features/menus/HeaderMenu.tsx - ✅
features/menus/MemoMenu.tsx - ✅
features/menus/MemoHeaderMenu.tsx - ✅
components/atoms/Pill.tsx - ✅
components/molecules/TableOfContentsMenu.tsx - ✅
features/subscription/SubscriptionMenu.tsx - ✅ Weitere Dropdown-Komponenten
Migration Pattern:
// VORHER (Zeego)
import * as DropdownMenu from 'zeego/dropdown-menu';
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key="item-1" onSelect={handleAction}>
<DropdownMenu.ItemTitle>Action</DropdownMenu.ItemTitle>
<DropdownMenu.ItemIcon ios={{ name: 'star' }} />
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
// NACHHER (Native Menu)
import { MenuView } from '@react-native-menu/menu';
<MenuView
actions={[
{
id: 'item-1',
title: 'Action',
image: Platform.select({
ios: 'star',
android: 'ic_menu_star',
}),
},
]}
onPressAction={({ nativeEvent }) => {
if (nativeEvent.event === 'item-1') handleAction();
}}
shouldOpenOnLongPress={false}
>
<Button />
</MenuView>
Pro Komponente: ~20-30 Minuten
Phase 3: Context Menus Migration (2-3 Stunden)
Komponenten (4 Stück):
- ✅
components/organisms/Memory.tsx(Kritisch - Kern-Feature) - ✅
components/molecules/PromptPreview.tsx - ✅
components/molecules/MemoPreview.tsx - ✅
components/organisms/PhotoGallery.tsx
Migration Pattern:
// VORHER (Zeego)
import * as ContextMenu from 'zeego/context-menu';
<ContextMenu.Root>
<ContextMenu.Trigger>
<MemoryCard />
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item key="share" onSelect={handleShare}>
<ContextMenu.ItemTitle>Teilen</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
// NACHHER (Native Menu - Long Press by default)
import { MenuView } from '@react-native-menu/menu';
<MenuView
actions={[
{
id: 'share',
title: 'Teilen',
image: 'square.and.arrow.up', // SF Symbol
},
]}
onPressAction={({ nativeEvent }) => {
if (nativeEvent.event === 'share') handleShare();
}}
// shouldOpenOnLongPress ist true by default für Context Menus
>
<MemoryCard />
</MenuView>
Pro Komponente: ~30-40 Minuten
Phase 4: Web Support (Optional - 2-3 Stunden)
Wenn Web-Support wichtig ist, erstelle .web.tsx Varianten:
4.1 Platform Extensions Setup
// components/menus/HeaderMenu.tsx (Native)
export { HeaderMenu } from './HeaderMenu.native';
// components/menus/HeaderMenu.native.tsx
import { MenuView } from '@react-native-menu/menu';
// Native implementation
// components/menus/HeaderMenu.web.tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
// Radix UI implementation
4.2 Shared Styling für Web
/* styles/menus.css */
.DropdownMenuContent {
min-width: 220px;
background-color: white;
border-radius: 6px;
padding: 5px;
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35);
}
.DropdownMenuItem {
padding: 8px;
border-radius: 3px;
cursor: pointer;
}
.DropdownMenuItem:hover {
background-color: #f5f5f5;
}
Nur implementieren wenn Web-Support benötigt wird!
Phase 5: Testing & Optimierung (2-3 Stunden)
5.1 Test-Checkliste
iOS Testing:
- Context Menus (Long Press)
- Dropdown Menus (Tap)
- SF Symbols Icons
- Destructive Actions (rote Farbe)
- Nested Submenus
- Events (onOpenMenu, onCloseMenu)
Android Testing:
- Context Menus (Long Press)
- Dropdown Menus (Tap)
- Material Icons
- Destructive Actions
- Performance
Web Testing (falls implementiert):
- Radix UI Dropdowns
- Radix UI Context Menus
- Keyboard Navigation
- Accessibility (ARIA)
5.2 Edge Cases
- Mehrere Menus gleichzeitig geöffnet
- Schnelles Tippen/Schließen
- Screen Rotation während Menu offen
- Dark Mode Support
Vorteile dieser Lösung
✅ Technische Vorteile
- SDK 54 Kompatibel: Library wurde auf RN 0.81 getestet
- Native Performance: Direkte UIMenu/PopupMenu APIs
- Bereits installiert: Keine neuen Dependencies
- Aktiv maintained: Updates vor 14 Tagen
- Expo-Integration: Offizieller Config Plugin
✅ UX Vorteile
- Native Look & Feel: 100% native iOS/Android Menus
- System-Integration: SF Symbols, Material Icons
- Accessibility: Native Accessibility Support
- Gestures: Native Long Press, Tap Gestures
✅ Developer Experience
- Simple API: Weniger Boilerplate als Zeego
- TypeScript Support: Vollständig typisiert
- Debugging: Native Debugging mit Xcode/Android Studio
- Testing: Jest Mocks vorhanden
Nachteile & Lösungen
❌ Kein Unified API wie Zeego
Problem: Verschiedene APIs für iOS/Android vs Web
Lösung 1 (Empfohlen): Platform-specific files
HeaderMenu.tsx → Export default
HeaderMenu.native.tsx → Native implementation
HeaderMenu.web.tsx → Radix UI implementation
Lösung 2: Custom Hook (siehe useNativeMenu oben)
Lösung 3: Wrapper-Komponente
// components/ui/NativeMenu.tsx
export const NativeMenu = Platform.OS === 'web'
? RadixDropdownMenu
: ReactNativeMenu;
❌ Web Support erfordert extra Arbeit
Problem: Web nicht out-of-the-box supported
Lösung:
- Wenn Web wichtig: Phase 4 implementieren (~2-3h)
- Wenn Web nicht wichtig: Weglassen
Realität: Viele Expo-Apps fokussieren sich auf Mobile
❌ Mehr Code als mit Zeego
Problem: ~20-30% mehr Code durch Platform Switches
Lösung:
- Abstraction Layers (Hooks, Wrapper)
- Code-Generierung für repetitive Patterns
- Akzeptieren des Trade-offs für native UX
❌ Checkbox-Support eingeschränkt
Problem: Native Menus haben limitierte Checkbox-Unterstützung
Alternative:
- Separate Settings-Screens
- Toggle-Buttons statt Menu Checkboxes
- Custom Modals für komplexe Selections
Migration Timeline
Aggressive Timeline (3 Tage)
Tag 1 (4h):
- ✅ Dependencies setup
- ✅ Rebuild & Testing
- ✅ 5-6 Dropdown Menus migrieren
Tag 2 (4h):
- ✅ Restliche Dropdown Menus
- ✅ Alle Context Menus
- ✅ Basis-Testing
Tag 3 (4h):
- ✅ Comprehensive Testing
- ✅ Bug Fixes
- ✅ Performance-Optimierung
Komfortable Timeline (5 Tage)
Tag 1-2: Dependencies + Dropdown Menus Tag 3: Context Menus Tag 4: Web Support (optional) Tag 5: Testing + Optimierung
Enterprise Timeline (2 Wochen)
Woche 1: Migration + Basic Testing Woche 2: Web Support + Comprehensive Testing + QA
Code-Organisation Best Practices
1. Zentralisierte Menu Actions
// config/menuActions.ts
export const MEMO_ACTIONS = {
EDIT: {
id: 'edit',
title: 'Bearbeiten',
icon: {
ios: 'pencil',
android: 'ic_menu_edit',
},
},
DELETE: {
id: 'delete',
title: 'Löschen',
destructive: true,
icon: {
ios: 'trash',
android: 'ic_menu_delete',
},
},
SHARE: {
id: 'share',
title: 'Teilen',
icon: {
ios: 'square.and.arrow.up',
android: 'ic_menu_share',
},
},
};
2. Menu Action Builder
// utils/menuBuilder.ts
import { Platform } from 'react-native';
export const buildMenuAction = (action: MenuActionConfig) => ({
id: action.id,
title: action.title,
attributes: action.destructive ? { destructive: true } : undefined,
image: action.icon ? Platform.select({
ios: action.icon.ios,
android: action.icon.android,
}) : undefined,
titleColor: action.color,
});
3. Reusable Menu Component
// components/ui/NativeMenu.tsx
interface NativeMenuProps {
actions: MenuActionConfig[];
onAction: (actionId: string) => void;
children: React.ReactNode;
isContextMenu?: boolean;
}
export const NativeMenu: FC<NativeMenuProps> = ({
actions,
onAction,
children,
isContextMenu = false,
}) => {
const menuActions = actions.map(buildMenuAction);
return (
<MenuView
actions={menuActions}
onPressAction={({ nativeEvent }) => onAction(nativeEvent.event)}
shouldOpenOnLongPress={isContextMenu}
>
{children}
</MenuView>
);
};
4. Verwendung
// components/organisms/Memory.tsx
import { NativeMenu } from '~/components/ui/NativeMenu';
import { MEMO_ACTIONS } from '~/config/menuActions';
export const Memory = ({ memo }) => {
const handleAction = (actionId: string) => {
switch (actionId) {
case 'edit':
editMemo(memo);
break;
case 'delete':
deleteMemo(memo);
break;
case 'share':
shareMemo(memo);
break;
}
};
return (
<NativeMenu
actions={[MEMO_ACTIONS.EDIT, MEMO_ACTIONS.DELETE, MEMO_ACTIONS.SHARE]}
onAction={handleAction}
isContextMenu
>
<MemoCard memo={memo} />
</NativeMenu>
);
};
Technische Details
@react-native-menu/menu Features
iOS (UIMenu API)
<MenuView
title="Title"
actions={[
{
id: 'action-id',
title: 'Action Title',
titleColor: '#007AFF',
subtitle: 'Optional subtitle',
image: 'sf.symbol.name', // SF Symbol
imageColor: '#FF3B30',
attributes: {
destructive: boolean,
disabled: boolean,
hidden: boolean,
},
state: 'on' | 'off' | 'mixed', // Checkbox state
},
]}
onPressAction={({ nativeEvent }) => {
// nativeEvent.event = action id
}}
onOpenMenu={() => console.log('Opened')}
onCloseMenu={() => console.log('Closed')}
shouldOpenOnLongPress={true} // Context menu
isAnchoredToRight={false}
themeVariant="dark" | "light"
>
<YourComponent />
</MenuView>
Android (PopupMenu API)
<MenuView
title="Title"
actions={[
{
id: 'action-id',
title: 'Action Title',
image: 'ic_menu_icon', // Material Icon
// Android unterstützt weniger Optionen
},
]}
onPressAction={({ nativeEvent }) => {
// Handle action
}}
>
<YourComponent />
</MenuView>
Nested Menus (iOS & Android)
<MenuView
actions={[
{
id: 'parent',
title: 'Parent Menu',
subactions: [
{ id: 'child-1', title: 'Child 1' },
{ id: 'child-2', title: 'Child 2' },
],
},
]}
onPressAction={({ nativeEvent }) => {
// Handles parent and child actions
}}
>
<YourComponent />
</MenuView>
SF Symbols Guide (iOS)
// Common SF Symbols
const SF_SYMBOLS = {
// Actions
edit: 'pencil',
delete: 'trash',
share: 'square.and.arrow.up',
copy: 'doc.on.doc',
download: 'arrow.down.circle',
upload: 'arrow.up.circle',
// Objects
photo: 'photo',
video: 'video',
document: 'doc',
folder: 'folder',
// UI
settings: 'gearshape',
profile: 'person.circle',
search: 'magnifyingglass',
filter: 'line.3.horizontal.decrease.circle',
// Status
check: 'checkmark',
close: 'xmark',
warning: 'exclamationmark.triangle',
info: 'info.circle',
};
Material Icons Guide (Android)
// Common Material Icons
const MATERIAL_ICONS = {
// Actions
edit: 'ic_menu_edit',
delete: 'ic_menu_delete',
share: 'ic_menu_share',
copy: 'ic_menu_copy_holo_dark',
// Objects
camera: 'ic_menu_camera',
gallery: 'ic_menu_gallery',
// UI
preferences: 'ic_menu_preferences',
myplaces: 'ic_menu_myplaces', // Profile
search: 'ic_menu_search',
// Status
add: 'ic_menu_add',
close: 'ic_menu_close_clear_cancel',
};
Alternative Lösungen (Falls @react-native-menu/menu nicht funktioniert)
Option B: Zeego Fix abwarten + Workaround
Wenn du Zeego wirklich bevorzugst:
- GitHub Issue monitoren: Issue #173 beobachten
- Temporärer Workaround: Downgrade auf Expo SDK 52
- Zeitaufwand: Unbekannt (kann Wochen dauern)
Nicht empfohlen: Blockiert SDK 54 Features
Option C: react-native-ios-context-menu direkt (iOS only)
Nur für iOS-exklusive Apps:
import { ContextMenuView } from 'react-native-ios-context-menu';
<ContextMenuView
menuConfig={{
menuTitle: 'Title',
menuItems: [
{
actionKey: 'action1',
actionTitle: 'Action 1',
icon: { iconType: 'SYSTEM', iconValue: 'trash' },
},
],
}}
onPressMenuItem={({ nativeEvent }) => {
// Handle
}}
>
<YourComponent />
</ContextMenuView>
Nachteile:
- Nur iOS
- In Maintenance Mode (Autor macht kein OSS mehr)
- Keine Android/Web Unterstützung
Option D: Custom Implementation
Nur als letzter Ausweg:
- 20-30 Stunden Aufwand
- Kein natives Look-and-Feel
- Hoher Wartungsaufwand
Testing-Strategie
Unit Tests
// __tests__/NativeMenu.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { NativeMenu } from '../components/ui/NativeMenu';
jest.mock('@react-native-menu/menu', () => ({
MenuView: 'MenuView',
}));
describe('NativeMenu', () => {
it('renders children', () => {
const { getByText } = render(
<NativeMenu actions={[]} onAction={jest.fn()}>
<Text>Test</Text>
</NativeMenu>
);
expect(getByText('Test')).toBeTruthy();
});
it('calls onAction with correct id', () => {
const onAction = jest.fn();
const { getByTestId } = render(
<NativeMenu
actions={[{ id: 'test', title: 'Test' }]}
onAction={onAction}
>
<View testID="trigger" />
</NativeMenu>
);
// Simulate menu press
fireEvent(getByTestId('trigger'), 'onPressAction', {
nativeEvent: { event: 'test' },
});
expect(onAction).toHaveBeenCalledWith('test');
});
});
E2E Tests (Detox)
// e2e/menus.test.ts
describe('Native Menus', () => {
it('should open context menu on long press', async () => {
await element(by.id('memory-card')).longPress();
await expect(element(by.text('Löschen'))).toBeVisible();
});
it('should execute action on tap', async () => {
await element(by.id('memory-card')).longPress();
await element(by.text('Löschen')).tap();
await expect(element(by.id('memory-card'))).not.toBeVisible();
});
});
Rollback-Plan
Falls Probleme auftreten:
1. Git Branch Strategy
# Vor Migration
git checkout -b migration/native-menus
git push -u origin migration/native-menus
# Bei Problemen
git checkout main
git branch -D migration/native-menus
2. Package Lock
# package.json vor Migration sichern
cp package.json package.json.backup
cp package-lock.json package-lock.json.backup
# Rollback
mv package.json.backup package.json
mv package-lock.json.backup package-lock.json
npm install
3. Zeego Restore
# Falls Rollback nötig
npm install zeego@3.0.6
npx expo prebuild --clean
Performance-Optimierung
1. Action Memoization
const memoActions = useMemo(() =>
actions.map(buildMenuAction),
[actions]
);
<MenuView actions={memoActions} />
2. Handler Optimization
const handleAction = useCallback((actionId: string) => {
switch (actionId) {
case 'delete':
deleteHandler();
break;
// ...
}
}, [deleteHandler, /* deps */]);
3. Lazy Loading (Web)
// Radix UI nur auf Web laden
const DropdownMenu = Platform.OS === 'web'
? lazy(() => import('@radix-ui/react-dropdown-menu'))
: null;
FAQ
Q: Funktioniert @react-native-menu/menu mit Expo Go?
A: Nein, erfordert Custom Development Build (wie Zeego auch).
Q: Brauche ich Web Support?
A: Nur wenn deine App im Browser laufen soll. Viele Expo-Apps sind Mobile-only.
Q: Wie ist die Performance vs Zeego?
A: Identisch - beide nutzen die gleichen nativen APIs.
Q: Was ist mit Dark Mode?
A: Native Menus respektieren automatisch das System-Theme.
Q: Kann ich Custom Styling verwenden?
A: Nein - native Menus nutzen System-Styling. Für Custom UI: eigene Modals bauen.
Q: Unterstützt es Animationen?
A: Ja - native System-Animationen (nicht konfigurierbar).
Zusammenfassung & Empfehlung
✅ Empfohlene Lösung: @react-native-menu/menu
Warum:
- ✅ Bereits installiert (v2.0.0)
- ✅ SDK 54 kompatibel (RN 0.81 getestet)
- ✅ Native iOS & Android Menus
- ✅ Aktiv maintained
- ✅ Niedriges Risiko
Zeitaufwand: 8-12 Stunden Risiko: Niedrig UX: Native (best-in-class)
Migrations-Reihenfolge
- Sofort: Dependencies setup (30 min)
- Phase 1: Dropdown Menus (4-5h)
- Phase 2: Context Menus (2-3h)
- Phase 3: Testing (2-3h)
- Optional: Web Support (2-3h)
Nächste Schritte
- ✅ Team-Entscheidung einholen
- ✅ Git Branch erstellen
- ✅ Phase 1 starten (Dependencies)
- ✅ Iterativ migrieren
- ✅ Testen auf allen Plattformen
Dokument erstellt: 30. September 2025 Autor: Claude Code Analyse Status: ✅ Production Ready Review: Empfohlen für Team-Review