import { Component, HostListener, computed, input, output, signal } from '@angular/core'; import { ThemeNodeDirective } from '../../../domains/theme'; /** * Mobile bottom-sheet container. * * Renders a backdrop + a panel anchored to the bottom of the viewport that slides up from below. * Intended for use on phone-sized viewports where context menus, action sheets, and confirmation * dialogs are better presented as bottom sheets than as floating popovers or centered modals. * * The component is layout-only: callers project their content via `` and listen for * the `dismissed` output to close themselves. Drag-to-dismiss is supported via touch gestures. * * Desktop callers should not render this component; use the original popover/modal layout instead. * * @example * ```html * @if (isMobile()) { * * * * } * ``` */ @Component({ selector: 'app-bottom-sheet', standalone: true, imports: [ThemeNodeDirective], templateUrl: './bottom-sheet.component.html', styleUrl: './bottom-sheet.component.scss' }) export class BottomSheetComponent { /** Optional title rendered at the top of the sheet. Omit for an unlabeled action sheet. */ readonly title = input(null); /** Optional ARIA label when no visible title is provided. */ readonly ariaLabel = input('Menu'); /** Emits when the user dismisses the sheet (backdrop tap, swipe-down, or Escape). */ readonly dismissed = output(); /** Pixels the sheet is currently dragged downward. Drives the translate transform. */ protected readonly dragOffset = signal(0); /** Visible transform offset in CSS pixels (only positive values move the sheet down). */ protected readonly translateY = computed(() => Math.max(0, this.dragOffset())); private touchStartY: number | null = null; @HostListener('document:keydown.escape') protected onEscape(): void { this.dismissed.emit(undefined); } protected onBackdropClick(): void { this.dismissed.emit(undefined); } protected onHandleTouchStart(event: TouchEvent): void { const touch = event.touches[0]; if (!touch) { return; } this.touchStartY = touch.clientY; } protected onHandleTouchMove(event: TouchEvent): void { if (this.touchStartY === null) { return; } const touch = event.touches[0]; if (!touch) { return; } const delta = touch.clientY - this.touchStartY; // Only allow dragging downward; ignore upward drags. this.dragOffset.set(Math.max(0, delta)); } protected onHandleTouchEnd(): void { // Dismiss if the user dragged the sheet down by more than 80px; otherwise snap back. if (this.dragOffset() > 80) { this.dismissed.emit(undefined); } this.touchStartY = null; this.dragOffset.set(0); } }