Some checks failed
Queue Release Build / prepare (push) Successful in 30s
Deploy Web Apps / deploy (push) Successful in 7m8s
Queue Release Build / build-windows (push) Successful in 28m11s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-linux (push) Has started running
103 lines
2.9 KiB
TypeScript
103 lines
2.9 KiB
TypeScript
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 `<ng-content>` 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()) {
|
|
* <app-bottom-sheet (dismissed)="close()">
|
|
* <my-menu-items />
|
|
* </app-bottom-sheet>
|
|
* }
|
|
* ```
|
|
*/
|
|
@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<string | null>(null);
|
|
|
|
/** Optional ARIA label when no visible title is provided. */
|
|
readonly ariaLabel = input<string>('Menu');
|
|
|
|
/** Emits when the user dismisses the sheet (backdrop tap, swipe-down, or Escape). */
|
|
readonly dismissed = output<undefined>();
|
|
|
|
/** 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);
|
|
}
|
|
}
|