frontend_nyla/documentation/sidebar.md
2025-06-27 12:43:07 +02:00

463 lines
11 KiB
Markdown

# Sidebar Component Documentation
## Architecture
```
src/
├── components/Sidebar/
│ ├── Sidebar.tsx # Main container component
│ ├── SidebarItem.tsx # Individual menu item
│ ├── SidebarSubmenu.tsx # Submenu component
│ └── SidebarUser.tsx # User info display
├── machines/
│ └── sidebarMachine.ts # State machine definition
└── hooks/machines/
│ └── useSidebarMachine.ts # React hook integration
└── contexts/
└── SidebarData.tsx # Navigation data provider
```
## State Machine Explanation
### Core Concepts
**Events** - Things that can happen:
- `TOGGLE_ITEM` - User clicks a menu item
- `CLOSE_ALL` - Close all submenus
- `NAVIGATE` - User navigates to a new route
**Context** - Data the machine remembers:
```typescript
interface SidebarContext {
openItemId: string | null; // Which submenu is open (only one allowed)
activePath: string; // Current route for highlighting
}
```
**States** - Possible configurations:
- `collapsed` - No submenus are open
- `expanded` - One submenu is open
### State Transitions
```mermaid
stateDiagram-v2
[*] --> collapsed
collapsed --> expanded : TOGGLE_ITEM(id)
expanded --> collapsed : TOGGLE_ITEM(same_id)
expanded --> expanded : TOGGLE_ITEM(different_id)
collapsed --> collapsed : CLOSE_ALL
expanded --> collapsed : CLOSE_ALL
```
### Machine Definition
```typescript
export const sidebarMachine = setup({
types: {
context: {} as SidebarContext,
events: {} as SidebarEvent,
},
}).createMachine({
id: 'sidebar',
initial: 'collapsed',
context: {
openItemId: null,
activePath: '/',
},
states: {
collapsed: {
on: {
TOGGLE_ITEM: {
target: 'expanded',
actions: assign({
openItemId: ({ event }) => event.itemId,
}),
},
},
},
expanded: {
on: {
TOGGLE_ITEM: [
{
// Same item clicked - close it
guard: ({ context, event }) => context.openItemId === event.itemId,
target: 'collapsed',
actions: assign({ openItemId: null }),
},
{
// Different item clicked - switch to it
target: 'expanded',
actions: assign({
openItemId: ({ event }) => event.itemId,
}),
},
],
},
},
},
});
```
## React Hook Integration
### useSidebarMachine Hook
The `useSidebarMachine` hook bridges XState with React components:
```typescript
export const useSidebarMachine = () => {
// 1. Create machine actor
const [state, send] = useActor(sidebarMachine);
// 2. Sync with React Router
const location = useLocation();
useEffect(() => {
send({ type: 'NAVIGATE', path: location.pathname });
}, [location.pathname, send]);
// 3. Return easy-to-use API
return {
// State queries
hasOpenSubmenu: sidebarState.hasOpenSubmenu,
openItemId: sidebarState.openItemId,
currentState: state.value,
// Actions
toggleItem: (itemId: string) => send({ type: 'TOGGLE_ITEM', itemId }),
closeAll: () => send({ type: 'CLOSE_ALL' }),
isItemOpen: (itemId: string) => sidebarState.isItemOpen(itemId),
isItemActive: (itemPath?: string) => location.pathname === itemPath,
};
};
```
### Hook Benefits
1. **Encapsulation**: State machine logic is hidden from components
2. **Type Safety**: TypeScript ensures correct usage
3. **Automatic Sync**: Route changes update the machine automatically
4. **Simple API**: Components only need simple functions
## Data Loading (SidebarData)
### Structure
```typescript
interface SidebarItemType {
id: string;
name: string;
link?: string;
icon?: React.ComponentType;
submenu?: SidebarItemType[];
}
```
### Implementation
```typescript
const useSidebarData = () => {
const { t } = useLanguage();
return useMemo(() => [
{
id: '1',
name: t('nav.team'),
link: '/team-bereich',
icon: MdOutlineWorkOutline,
},
{
id: '2',
name: t('nav.dashboard'),
link: '/dashboard',
icon: LuTicket,
submenu: [ // Optional submenu
{ id: '2-1', name: 'Analytics', link: '/dashboard/analytics' },
{ id: '2-2', name: 'Reports', link: '/dashboard/reports' },
],
},
], [t]);
};
```
### Key Features
- **Internationalization**: Uses language context for translations
- **Memoization**: Prevents unnecessary re-renders
- **Flexible Structure**: Supports nested submenus
- **Icon Support**: React component icons
## Component Interactions
### Sidebar (Main Container)
```typescript
const Sidebar: React.FC<SidebarProps> = ({ data }) => {
const sidebarMachine = useSidebarMachine();
return (
<div className={styles.sidebarContainer}>
{/* Logo and User sections */}
<div className={styles.sidebar}>
{data.map(item => (
<SidebarItem
key={item.id}
item={item}
isOpen={sidebarMachine.isItemOpen(item.id)}
onToggle={() => sidebarMachine.toggleItem(item.id)}
isActive={sidebarMachine.isItemActive(item.link)}
/>
))}
</div>
</div>
);
};
```
**Responsibilities:**
- Initialize state machine
- Pass state and actions to children
- Handle layout and styling
### SidebarItem (Individual Menu Item)
```typescript
const SidebarItem: React.FC<SidebarItemProps> = ({
item,
isOpen,
onToggle,
isActive
}) => {
const hasSubItems = item.submenu && item.submenu.length > 0;
const toggleSubmenu = (e: React.MouseEvent) => {
if (hasSubItems) {
e.preventDefault();
onToggle(); // Call parent's toggle function
}
};
return (
<div className={styles.menu}>
<li className={`${isActive ? styles.active : ""}`}>
{hasSubItems ? (
<a href="#" onClick={toggleSubmenu}>
{item.name}
<IoIosArrowDown className={`${isOpen ? styles.rotated : ''}`} />
</a>
) : (
<Link to={item.link || "#"}>{item.name}</Link>
)}
</li>
{hasSubItems && <SidebarSubmenu item={item} isOpen={isOpen} />}
</div>
);
};
```
**Key Changes from Original:**
-**Removed**: `useState` for local state
-**Added**: Props from parent state machine
-**Benefit**: No independent state management
### SidebarSubmenu (Nested Menu)
```typescript
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
>
{/* Submenu items */}
</motion.div>
)}
</AnimatePresence>
);
};
```
**Responsibilities:**
- Render submenu when `isOpen` is true
- Handle animations with Framer Motion
- Manage text overflow behavior
## Data Flow
```
1. User clicks menu item
2. SidebarItem calls onToggle()
3. onToggle sends TOGGLE_ITEM event to machine
4. Machine transitions states and updates context
5. Hook returns new state
6. Sidebar re-renders with new isOpen values
7. SidebarSubmenu shows/hides based on isOpen
```
## Adding New States
### Example: Adding "Pinned" State
1. **Update Events:**
```typescript
export type SidebarEvent =
| { type: 'TOGGLE_ITEM'; itemId: string }
| { type: 'PIN_SIDEBAR' } // New event
| { type: 'UNPIN_SIDEBAR' } // New event
| { type: 'CLOSE_ALL' }
| { type: 'NAVIGATE'; path: string };
```
2. **Update Context:**
```typescript
export interface SidebarContext {
openItemId: string | null;
activePath: string;
isPinned: boolean; // New context property
}
```
3. **Add New States:**
```typescript
states: {
collapsed: {
on: {
PIN_SIDEBAR: 'pinnedCollapsed',
// ... existing transitions
},
},
expanded: {
on: {
PIN_SIDEBAR: 'pinnedExpanded',
// ... existing transitions
},
},
pinnedCollapsed: { // New state
on: {
UNPIN_SIDEBAR: 'collapsed',
TOGGLE_ITEM: 'pinnedExpanded',
},
},
pinnedExpanded: { // New state
on: {
UNPIN_SIDEBAR: 'expanded',
// ... similar to expanded
},
},
},
```
4. **Update Hook:**
```typescript
return {
// ... existing returns
isPinned: state.matches('pinnedCollapsed') || state.matches('pinnedExpanded'),
pinSidebar: () => send({ type: 'PIN_SIDEBAR' }),
unpinSidebar: () => send({ type: 'UNPIN_SIDEBAR' }),
};
```
## Best Practices
### State Machine Design
1. **Keep States Simple**: Each state should represent a distinct UI mode
2. **Use Guards Wisely**: For conditional transitions based on context
3. **Minimize Context**: Only store data that affects behavior
4. **Clear Event Names**: Use descriptive, action-oriented names
### Component Architecture
1. **Single Source of Truth**: State machine holds all navigation state
2. **Props Down**: Pass state and actions as props
3. **Events Up**: Send events to the machine, not direct state updates
4. **Separation of Concerns**: Components handle UI, machine handles logic
### Performance
1. **Memoize Data**: Use `useMemo` for sidebar data
2. **Avoid Deep Objects**: Keep context flat when possible
3. **Selective Subscriptions**: Only subscribe to needed state slices
## Debugging
### Development Tools
1. **Debug Props**: Hook returns `_debugState` and `_debugSend`
2. **State Logging**: Log state changes in development
3. **XState DevTools**: Use `@xstate/inspect` for visual debugging
### Common Issues
1. **Multiple Items Open**: Check guard conditions in TOGGLE_ITEM
2. **State Not Updating**: Ensure events are sent correctly
3. **Route Sync Issues**: Verify NAVIGATE event is sent on route change
## Testing
### Unit Testing State Machine
```typescript
import { sidebarMachine } from '../machines/sidebarMachine';
test('should open submenu when item toggled', () => {
const state = sidebarMachine.transition('collapsed', {
type: 'TOGGLE_ITEM',
itemId: 'menu-1',
});
expect(state.value).toBe('expanded');
expect(state.context.openItemId).toBe('menu-1');
});
```
### Integration Testing
```typescript
import { render, fireEvent } from '@testing-library/react';
import { SidebarWithData } from '../components/Sidebar';
test('should close submenu when same item clicked twice', () => {
const { getByText } = render(<SidebarWithData />);
const menuItem = getByText('Dashboard');
fireEvent.click(menuItem); // Open
fireEvent.click(menuItem); // Close
expect(/* submenu is closed */).toBeTruthy();
});
```
## Migration Guide
### From useState to State Machine
**Before:**
```typescript
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
```
**After:**
```typescript
const { isItemOpen, toggleItem } = useSidebarMachine();
// Use: isItemOpen(itemId) and toggleItem(itemId)
```
### Benefits of Migration
1. **Predictable State**: No more impossible state combinations
2. **Centralized Logic**: All sidebar behavior in one place
3. **Better Testing**: State machine logic is easily testable
4. **Type Safety**: TypeScript prevents invalid transitions
5. **Debugging**: Clear state visualization and logging