UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

177 lines 7.61 kB
import { ScreenService, Shade, createComponent } from '@furystack/shades'; import { cssVariableTheme } from '../../services/css-variable-theme.js'; import { LayoutService } from '../../services/layout-service.js'; export { DrawerToggleButton } from './drawer-toggle-button.js'; const DEFAULT_DRAWER_WIDTH = '240px'; /** * Standalone Drawer component for sidebars and navigation panels. * * Works with LayoutService for state management and supports three variants: * - **permanent**: Always visible, cannot be closed * - **collapsible**: Can be toggled open/closed, pushes content * - **temporary**: Overlays content with backdrop, closes on backdrop click * * Supports responsive behavior via `collapseOnBreakpoint` prop which * auto-collapses the drawer below a specified screen size. * * @example * ```tsx * // Simple collapsible drawer * <Drawer position="left" variant="collapsible"> * <nav>Navigation items...</nav> * </Drawer> * * // Responsive drawer that collapses on mobile * <Drawer * position="left" * variant="collapsible" * collapseOnBreakpoint="md" * > * <nav>Navigation items...</nav> * </Drawer> * * // Temporary drawer (mobile navigation) * <Drawer position="left" variant="temporary"> * <nav>Mobile navigation...</nav> * </Drawer> * ``` */ export const Drawer = Shade({ customElementName: 'shade-drawer', css: { display: 'block', // Drawer container '& .drawer-container': { position: 'fixed', top: 'var(--layout-content-margin-top, 0px)', bottom: '0', zIndex: '1000', overflow: 'hidden', transition: `width ${cssVariableTheme.transitions.duration.slow} ${cssVariableTheme.transitions.easing.easeInOut}, transform ${cssVariableTheme.transitions.duration.slow} ${cssVariableTheme.transitions.easing.easeInOut}`, fontFamily: cssVariableTheme.typography.fontFamily, background: cssVariableTheme.background.paper, }, // Left drawer positioning '& .drawer-container.drawer-left': { left: '0', borderRight: `1px solid ${cssVariableTheme.divider}`, }, // Right drawer positioning '& .drawer-container.drawer-right': { right: '0', borderLeft: `1px solid ${cssVariableTheme.divider}`, }, // Closed state for permanent/collapsible '& .drawer-container.closed': { width: '0 !important', overflow: 'hidden', }, // Content wrapper for proper overflow '& .drawer-content': { height: '100%', overflow: 'auto', }, // Backdrop for temporary drawer '& .drawer-backdrop': { position: 'fixed', top: '0', left: '0', right: '0', bottom: '0', backgroundColor: cssVariableTheme.action.backdrop, zIndex: '999', opacity: '0', pointerEvents: 'none', transition: `opacity ${cssVariableTheme.transitions.duration.slow} ${cssVariableTheme.transitions.easing.easeInOut}`, }, '& .drawer-backdrop.visible': { opacity: '1', pointerEvents: 'auto', }, }, render: ({ props, children, injector, useObservable, useDisposable }) => { const layoutService = injector.get(LayoutService); const screenService = injector.get(ScreenService); const { position, variant, width = DEFAULT_DRAWER_WIDTH, defaultOpen, collapseOnBreakpoint } = props; // Determine initial open state based on variant const getInitialOpenState = () => { if (variant === 'permanent') return true; if (variant === 'temporary') return defaultOpen ?? false; // collapsible defaults to true return defaultOpen ?? true; }; // Initialize drawer state if not already set const currentState = layoutService.drawerState.getValue()[position]; if (!currentState) { layoutService.initDrawer(position, { open: getInitialOpenState(), width, variant, }); } else if (currentState.width !== width) { // Update width if it changed layoutService.setDrawerWidth(position, width); } // Clean up drawer state from LayoutService when this component is disposed useDisposable('drawer-cleanup', () => ({ [Symbol.dispose]: () => layoutService.removeDrawer(position), })); // Subscribe to drawer state const [drawerState] = useObservable('drawerState', layoutService.drawerState); const isOpen = drawerState[position]?.open ?? false; // Set up responsive breakpoint listener useDisposable(`breakpoint-listener-${collapseOnBreakpoint}`, () => { if (!collapseOnBreakpoint || variant === 'permanent') { return { [Symbol.dispose]: () => { } }; } const breakpointObservable = screenService.screenSize.atLeast[collapseOnBreakpoint]; const subscription = breakpointObservable.subscribe((isAtLeast) => { // When screen becomes smaller than breakpoint, close the drawer // When screen becomes larger than breakpoint, open the drawer (for collapsible) if (variant === 'collapsible') { layoutService.setDrawerOpen(position, isAtLeast); } else if (variant === 'temporary' && isAtLeast) { // For temporary drawers, close when screen is large enough layoutService.setDrawerOpen(position, false); } }); return subscription; }); // Handle backdrop click for temporary drawer const handleBackdropClick = () => { if (variant === 'temporary') { layoutService.setDrawerOpen(position, false); } }; // Calculate drawer style const getDrawerStyle = () => { if (variant === 'temporary') { // Temporary drawers use transform for slide animation return { width, transform: isOpen ? 'translateX(0)' : position === 'left' ? 'translateX(-100%)' : 'translateX(100%)', }; } // Permanent and collapsible drawers animate width return { width: isOpen ? width : '0', }; }; // Build container class list const containerClasses = ['drawer-container', `drawer-${position}`]; if (!isOpen && variant !== 'temporary') { containerClasses.push('closed'); } // Show backdrop only for open temporary drawers const showBackdrop = variant === 'temporary' && isOpen; return (createComponent(createComponent, null, variant === 'temporary' && (createComponent("div", { className: `drawer-backdrop ${showBackdrop ? 'visible' : ''}`, onclick: handleBackdropClick, "data-testid": `drawer-backdrop-${position}` })), createComponent("div", { className: containerClasses.join(' '), style: getDrawerStyle(), "data-testid": `drawer-${position}`, "data-variant": variant, "data-open": isOpen ? 'true' : 'false' }, createComponent("div", { className: "drawer-content" }, children)))); }, }); //# sourceMappingURL=index.js.map