import { DOCUMENT, NgStyle } from '@angular/common'; import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges, } from '@angular/core'; import { Button } from 'primeng/button'; @Component({ selector: 'shared-light-bottomsheet', templateUrl: './light-bottomsheet.component.html', imports: [NgStyle, Button], styles: [ ` :host { position: fixed; inset: 0; z-index: 1210; } .light-bottomsheet-mask { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.22); } .light-bottomsheet-panel { position: absolute; right: 0; bottom: 0; left: 0; max-height: 90vh; display: flex; flex-direction: column; background: var(--surface-card, #fff); border-radius: 12px 12px 0 0; box-shadow: none; backface-visibility: hidden; overflow: hidden; } .light-bottomsheet-content { display: flex; flex-direction: column; flex: 1; min-height: 0; // padding-bottom: calc(0.75rem + env(safe-area-inset-bottom)); -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } .light-bottomsheet-body { flex: 1; min-height: 0; overflow: auto; } .light-bottomsheet-header { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 0.75rem 1rem; } .light-bottomsheet-title { margin: 0; } .light-bottomsheet-close { border: 0; background: transparent; padding: 0.25rem; line-height: 1; font-size: 1.25rem; } `, ], host: { '[attr.role]': '"complementary"', '[attr.aria-modal]': 'true', '[attr.pfocustrap]': 'true', '[attr.data-pc-name]': "'drawer'", '[attr.data-pc-section]': "'root'", '[attr.aria-hidden]': '!visible', '[style.display]': 'visible ? "block" : "none"', '[style.--sheet-transition]': 'transitionOptions', }, }) export class SharedLightBottomsheetComponent implements OnInit, OnDestroy, OnChanges { @Input() header = ''; @Input() visible = false; @Output() visibleChange = new EventEmitter(); @Input() modal = true; @Input() closable = true; @Input() style: Record | undefined; @Input() mobileDrawerHeight = '90vh'; @Input() transitionOptions = '130ms cubic-bezier(0.2, 0, 0, 1)'; @Output() onHide = new EventEmitter(); private originalParent: Node | null = null; private readonly defaultDrawerZIndex = 1102; private previousBodyOverflow = ''; private previousBodyTouchAction = ''; constructor( private readonly elementRef: ElementRef, private readonly renderer: Renderer2, @Inject(DOCUMENT) private readonly document: Document ) {} ngOnInit() { const host = this.elementRef.nativeElement; this.originalParent = host.parentNode; this.renderer.appendChild(this.document.body, host); this.syncZIndex(); this.toggleBodyScrollLock(this.visible); } ngOnChanges(changes: SimpleChanges) { if (changes['visible'] && this.visible) { this.syncZIndex(); } if (changes['visible']) { this.toggleBodyScrollLock(this.visible); } } ngOnDestroy() { this.toggleBodyScrollLock(false); this.removeDrawer(); } private removeDrawer() { const host = this.elementRef.nativeElement; if (this.originalParent) { this.renderer.appendChild(this.originalParent, host); } } onVisibilityChange(nextValue: boolean) { this.visible = nextValue; this.visibleChange.emit(nextValue); this.toggleBodyScrollLock(nextValue); if (nextValue) { this.syncZIndex(); } if (!nextValue) { this.onHide.emit(); } } onMaskClick() { if (this.modal && this.closable) { this.onVisibilityChange(false); } } private toggleBodyScrollLock(locked: boolean) { const body = this.document.body; if (!body) return; if (locked) { if (!this.previousBodyOverflow) { this.previousBodyOverflow = body.style.overflow || ''; } if (!this.previousBodyTouchAction) { this.previousBodyTouchAction = body.style.touchAction || ''; } this.renderer.setStyle(body, 'overflow', 'hidden'); this.renderer.setStyle(body, 'touch-action', 'none'); return; } this.renderer.removeStyle(body, 'overflow'); this.renderer.removeStyle(body, 'touch-action'); this.previousBodyOverflow = ''; this.previousBodyTouchAction = ''; } private syncZIndex() { const host = this.elementRef.nativeElement; const siblingDrawers = Array.from( this.document.body.querySelectorAll('.p-drawer') ) as HTMLElement[]; const drawerZIndexes = siblingDrawers .map((drawer) => Number.parseInt(drawer.style.zIndex || '0', 10)) .filter((zIndex) => Number.isFinite(zIndex) && zIndex > 0); const maxDrawerZIndex = drawerZIndexes.length ? Math.max(...drawerZIndexes) : this.defaultDrawerZIndex; this.renderer.setStyle(host, 'z-index', `${maxDrawerZIndex + 1}`); } }