feat(modal): enhance modal behavior and viewport handling for iOS devices
This commit is contained in:
parent
71ef09b413
commit
6f87aae939
|
|
@ -51,6 +51,10 @@ describe('ModalHostComponent', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
doc.body.style.overflow = '';
|
doc.body.style.overflow = '';
|
||||||
doc.body.style.paddingRight = '';
|
doc.body.style.paddingRight = '';
|
||||||
|
doc.body.style.position = '';
|
||||||
|
doc.body.style.top = '';
|
||||||
|
doc.body.style.left = '';
|
||||||
|
doc.body.style.width = '';
|
||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -99,6 +103,10 @@ describe('ModalHostComponent', () => {
|
||||||
expect(service.activeModal()).toBeNull();
|
expect(service.activeModal()).toBeNull();
|
||||||
expect(doc.body.style.overflow).toBe('');
|
expect(doc.body.style.overflow).toBe('');
|
||||||
expect(doc.body.style.paddingRight).toBe('');
|
expect(doc.body.style.paddingRight).toBe('');
|
||||||
|
expect(doc.body.style.position).toBe('');
|
||||||
|
expect(doc.body.style.top).toBe('');
|
||||||
|
expect(doc.body.style.left).toBe('');
|
||||||
|
expect(doc.body.style.width).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes on backdrop click when enabled', () => {
|
it('closes on backdrop click when enabled', () => {
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,23 @@ export class ModalHostComponent {
|
||||||
const body = this.document.body;
|
const body = this.document.body;
|
||||||
const previousOverflow = body.style.overflow;
|
const previousOverflow = body.style.overflow;
|
||||||
const previousPaddingRight = body.style.paddingRight;
|
const previousPaddingRight = body.style.paddingRight;
|
||||||
|
const previousPosition = body.style.position;
|
||||||
|
const previousTop = body.style.top;
|
||||||
|
const previousLeft = body.style.left;
|
||||||
|
const previousWidth = body.style.width;
|
||||||
const view = this.document.defaultView;
|
const view = this.document.defaultView;
|
||||||
|
const scrollX = view?.scrollX ?? 0;
|
||||||
|
const scrollY = view?.scrollY ?? 0;
|
||||||
const scrollbarWidth = view
|
const scrollbarWidth = view
|
||||||
? Math.max(0, view.innerWidth - this.document.documentElement.clientWidth)
|
? Math.max(0, view.innerWidth - this.document.documentElement.clientWidth)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
const activeElement = this.document.activeElement;
|
||||||
|
|
||||||
|
if (activeElement instanceof HTMLElement && activeElement !== body) {
|
||||||
|
activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
if (scrollbarWidth > 0 && view) {
|
if (scrollbarWidth > 0 && view) {
|
||||||
const currentPaddingRight =
|
const currentPaddingRight =
|
||||||
Number.parseFloat(view.getComputedStyle(body).paddingRight) || 0;
|
Number.parseFloat(view.getComputedStyle(body).paddingRight) || 0;
|
||||||
|
|
@ -68,6 +80,47 @@ export class ModalHostComponent {
|
||||||
|
|
||||||
body.style.overflow = 'hidden';
|
body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
const isIos = view
|
||||||
|
? /iPad|iPhone|iPod/.test(view.navigator.userAgent) ||
|
||||||
|
(view.navigator.platform === 'MacIntel' && view.navigator.maxTouchPoints > 1)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (isIos) {
|
||||||
|
body.style.position = 'fixed';
|
||||||
|
body.style.top = `${-scrollY}px`;
|
||||||
|
body.style.left = `${-scrollX}px`;
|
||||||
|
body.style.width = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewportFrame: number | undefined;
|
||||||
|
const viewportSyncTimers: number[] = [];
|
||||||
|
const syncVisualViewport = () => {
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportFrame !== undefined) {
|
||||||
|
view.cancelAnimationFrame(viewportFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportFrame = view.requestAnimationFrame(() => {
|
||||||
|
viewportFrame = undefined;
|
||||||
|
this.modalShell()?.setVisualViewport(view.visualViewport, view.scrollX, view.scrollY);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
view?.visualViewport?.addEventListener('resize', syncVisualViewport);
|
||||||
|
view?.visualViewport?.addEventListener('scroll', syncVisualViewport);
|
||||||
|
view?.addEventListener('orientationchange', syncVisualViewport);
|
||||||
|
syncVisualViewport();
|
||||||
|
|
||||||
|
if (view) {
|
||||||
|
viewportSyncTimers.push(
|
||||||
|
view.setTimeout(syncVisualViewport, 100),
|
||||||
|
view.setTimeout(syncVisualViewport, 300),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key !== 'Escape' || !this.activeModal()?.config.closeOnEscape) {
|
if (event.key !== 'Escape' || !this.activeModal()?.config.closeOnEscape) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -82,8 +135,26 @@ export class ModalHostComponent {
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
this.document.removeEventListener('keydown', onKeyDown);
|
this.document.removeEventListener('keydown', onKeyDown);
|
||||||
|
view?.visualViewport?.removeEventListener('resize', syncVisualViewport);
|
||||||
|
view?.visualViewport?.removeEventListener('scroll', syncVisualViewport);
|
||||||
|
view?.removeEventListener('orientationchange', syncVisualViewport);
|
||||||
|
|
||||||
|
if (viewportFrame !== undefined) {
|
||||||
|
view?.cancelAnimationFrame(viewportFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportSyncTimers.forEach((timer) => view?.clearTimeout(timer));
|
||||||
|
|
||||||
body.style.overflow = previousOverflow;
|
body.style.overflow = previousOverflow;
|
||||||
body.style.paddingRight = previousPaddingRight;
|
body.style.paddingRight = previousPaddingRight;
|
||||||
|
body.style.position = previousPosition;
|
||||||
|
body.style.top = previousTop;
|
||||||
|
body.style.left = previousLeft;
|
||||||
|
body.style.width = previousWidth;
|
||||||
|
|
||||||
|
if (isIos) {
|
||||||
|
view?.scrollTo(scrollX, scrollY);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<div
|
<div
|
||||||
|
#shell
|
||||||
class="modal-shell"
|
class="modal-shell"
|
||||||
[class.modal-shell--fullscreen-media]="presentation() === 'fullscreen-media'"
|
[class.modal-shell--fullscreen-media]="presentation() === 'fullscreen-media'"
|
||||||
(click)="backdropClick.emit()"
|
(click)="backdropClick.emit()"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
|
|
||||||
.modal-shell {
|
.modal-shell {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
top: var(--modal-viewport-top, 0);
|
||||||
|
left: var(--modal-viewport-left, 0);
|
||||||
|
width: var(--modal-viewport-width, 100vw);
|
||||||
|
height: var(--modal-viewport-height, 100dvh);
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { ModalPresentation, ModalSize } from '../../../core/services/modal.servi
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ModalShellComponent {
|
export class ModalShellComponent {
|
||||||
|
private readonly shell = viewChild.required<ElementRef<HTMLElement>>('shell');
|
||||||
private readonly panel = viewChild.required<ElementRef<HTMLElement>>('panel');
|
private readonly panel = viewChild.required<ElementRef<HTMLElement>>('panel');
|
||||||
|
|
||||||
readonly title = input<string | undefined>();
|
readonly title = input<string | undefined>();
|
||||||
|
|
@ -47,6 +48,28 @@ export class ModalShellComponent {
|
||||||
return this.title() ? this.titleId : null;
|
return this.title() ? this.titleId : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVisualViewport(viewport: VisualViewport | null, layoutScrollX = 0, layoutScrollY = 0): void {
|
||||||
|
const shell = this.shell().nativeElement;
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
shell.style.removeProperty('--modal-viewport-top');
|
||||||
|
shell.style.removeProperty('--modal-viewport-left');
|
||||||
|
shell.style.removeProperty('--modal-viewport-width');
|
||||||
|
shell.style.removeProperty('--modal-viewport-height');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pageTop/pageLeft are a useful fallback for WebKit versions that update
|
||||||
|
// offsetTop/offsetLeft one frame late after dismissing a native control.
|
||||||
|
const top = Math.max(viewport.offsetTop, viewport.pageTop - layoutScrollY);
|
||||||
|
const left = Math.max(viewport.offsetLeft, viewport.pageLeft - layoutScrollX);
|
||||||
|
|
||||||
|
shell.style.setProperty('--modal-viewport-top', `${top}px`);
|
||||||
|
shell.style.setProperty('--modal-viewport-left', `${left}px`);
|
||||||
|
shell.style.setProperty('--modal-viewport-width', `${viewport.width}px`);
|
||||||
|
shell.style.setProperty('--modal-viewport-height', `${viewport.height}px`);
|
||||||
|
}
|
||||||
|
|
||||||
focusInitialElement(): void {
|
focusInitialElement(): void {
|
||||||
const panel = this.panel().nativeElement;
|
const panel = this.panel().nativeElement;
|
||||||
const focusTarget = panel.querySelector<HTMLElement>(
|
const focusTarget = panel.querySelector<HTMLElement>(
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<title>ShopitFront</title>
|
<title>ShopitFront</title>
|
||||||
<base href="/">
|
<base href="/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E">
|
name="viewport"
|
||||||
</head>
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||||
<body>
|
/>
|
||||||
<app-root></app-root>
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" />
|
||||||
</body>
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue