Files
dees-catalog-mobile/ts_web/elements/00group-layout/dees-mobile-applayout/dees-mobile-applayout.ts
2025-12-22 10:53:15 +00:00

352 lines
8.7 KiB
TypeScript

import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import { demoFunc } from './dees-mobile-applayout.demo.js';
export type TNavigationDirection = 'forward' | 'back' | 'none';
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-applayout': DeesMobileApplayout;
}
}
@customElement('dees-mobile-applayout')
export class DeesMobileApplayout extends DeesElement {
public static demo = demoFunc;
@property({ type: Boolean, reflect: true, attribute: 'keyboard-visible' })
accessor keyboardVisible: boolean = false;
@property({ type: Boolean })
accessor showNavigation: boolean = true;
@property({ type: Boolean })
accessor isIOS: boolean = false;
@property({ type: Boolean })
accessor isPWA: boolean = false;
@state()
accessor navigationDirection: TNavigationDirection = 'none';
@state()
accessor isTransitioning: boolean = false;
private keyboardBlurTimeout?: number;
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
:host {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--dees-background);
}
.app-layout {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"spacer"
"content"
"navigation";
height: 100vh;
overflow: hidden;
}
.ios-keyboard-spacer {
grid-area: spacer;
height: 0;
transition: height 300ms ease-out;
background: var(--dees-background);
}
.ios-keyboard-spacer.active {
height: 340px;
}
/* Mobile-first: smooth scrolling behavior for keyboard visibility */
:host([keyboard-visible]) .main-content {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Main content area */
.main-content {
grid-area: content;
overflow: hidden;
position: relative;
}
/* View transition container */
.view-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.view-wrapper {
position: absolute;
inset: 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
will-change: transform, opacity;
}
/* Forward navigation: entering view slides from right */
.view-wrapper.entering.forward {
animation: slideInFromRight 300ms ease-out forwards;
}
/* Forward navigation: exiting view slides to left with fade */
.view-wrapper.exiting.forward {
animation: slideOutToLeft 300ms ease-out forwards;
}
/* Back navigation: entering view slides from left */
.view-wrapper.entering.back {
animation: slideInFromLeft 300ms ease-out forwards;
}
/* Back navigation: exiting view slides to right */
.view-wrapper.exiting.back {
animation: slideOutToRight 300ms ease-out forwards;
}
/* No animation */
.view-wrapper.entering.none {
opacity: 1;
transform: none;
}
@keyframes slideInFromRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideOutToLeft {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(-30%);
opacity: 0;
}
}
@keyframes slideInFromLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideOutToRight {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
/* Bottom navigation */
.navigation-slot {
grid-area: navigation;
z-index: var(--dees-z-sticky, 200);
}
/* Mobile-first: hide bottom navigation when keyboard is visible */
:host([keyboard-visible]) .navigation-slot {
display: none;
}
/* Desktop: show navigation even with keyboard */
@media (min-width: 641px) {
:host([keyboard-visible]) .navigation-slot {
display: block;
}
}
/* Mobile-first: allow overflow during drag */
:host-context(body.dragging) .app-layout {
overflow: visible !important;
}
:host-context(body.dragging) .main-content {
overflow: visible !important;
}
/* Desktop: maintain normal overflow behavior during drag */
@media (min-width: 641px) {
:host-context(body.dragging) .app-layout {
overflow: hidden;
}
:host-context(body.dragging) .main-content {
overflow-y: auto;
}
}
.loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
color: var(--dees-muted-foreground);
}
.spinner {
width: 3rem;
height: 3rem;
border: 3px solid var(--dees-border);
border-top-color: var(--dees-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
`,
];
async connectedCallback() {
await super.connectedCallback();
// Listen for keyboard events
this.addEventListener('input-focus', this.handleInputFocus as EventListener);
this.addEventListener('input-blur', this.handleInputBlur as EventListener);
// Detect iOS PWA
this.detectEnvironment();
// Listen for viewport changes to detect keyboard (iOS PWA only)
if (this.isIOS && this.isPWA && 'visualViewport' in window) {
window.visualViewport?.addEventListener('resize', this.handleViewportResize);
this.handleViewportResize();
}
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.removeEventListener('input-focus', this.handleInputFocus as EventListener);
this.removeEventListener('input-blur', this.handleInputBlur as EventListener);
if (this.isIOS && this.isPWA && 'visualViewport' in window) {
window.visualViewport?.removeEventListener('resize', this.handleViewportResize);
}
}
private detectEnvironment() {
// Detect iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
// Detect PWA mode
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone === true;
this.isIOS = isIOS;
this.isPWA = isPWA;
}
private handleInputFocus = () => {
if (this.keyboardBlurTimeout) {
clearTimeout(this.keyboardBlurTimeout);
this.keyboardBlurTimeout = undefined;
}
if (window.innerWidth <= 640) {
this.keyboardVisible = true;
}
};
private handleInputBlur = () => {
if (this.keyboardBlurTimeout) {
clearTimeout(this.keyboardBlurTimeout);
}
if (window.innerWidth <= 640) {
this.keyboardBlurTimeout = window.setTimeout(() => {
this.keyboardVisible = false;
this.keyboardBlurTimeout = undefined;
}, 150);
}
};
private handleViewportResize = () => {
if (window.visualViewport) {
const viewport = window.visualViewport;
const keyboardHeight = window.innerHeight - viewport.height;
if (keyboardHeight > 50) {
this.keyboardVisible = true;
} else {
this.keyboardVisible = false;
}
}
};
/**
* Navigate with animation transition
* Call this method when changing views
*/
public navigateWithTransition(direction: TNavigationDirection) {
if (this.isTransitioning) return;
this.navigationDirection = direction;
this.isTransitioning = true;
// Clear transition state after animation
setTimeout(() => {
this.isTransitioning = false;
this.navigationDirection = 'none';
}, 300);
}
public render(): TemplateResult {
const showKeyboardSpacer = this.keyboardVisible && this.isIOS && this.isPWA;
return html`
<div class="app-layout">
<div class="ios-keyboard-spacer ${showKeyboardSpacer ? 'active' : ''}"></div>
<main class="main-content">
<div class="view-container">
<div class="view-wrapper entering ${this.navigationDirection}">
<slot></slot>
</div>
</div>
</main>
${this.showNavigation ? html`
<div class="navigation-slot">
<slot name="navigation"></slot>
</div>
` : ''}
</div>
`;
}
}