Skip to content

Commit 61e360b

Browse files
fix: prevent backdrop click-through in iOS home screen apps (#12056) (#12062)
Co-authored-by: Diego Cardoso <diego@vaadin.com>
1 parent f558f82 commit 61e360b

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

packages/app-layout/src/vaadin-app-layout-mixin.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,9 @@ export const AppLayoutMixin = (superclass) =>
448448
}
449449

450450
/** @private */
451-
_onBackdropClick() {
451+
_onBackdropClick(event) {
452+
// End the ghost click window on the first click (see `_onBackdropTouchend`).
453+
event.currentTarget.style.removeProperty('pointer-events');
452454
this._close();
453455
}
454456

@@ -458,6 +460,18 @@ export const AppLayoutMixin = (superclass) =>
458460
// on clickable element behind the backdrop
459461
event.preventDefault();
460462

463+
// On iOS home screen (standalone) apps, `preventDefault()` on `touchend`
464+
// does not reliably suppress the synthesized "ghost" click, which occurs
465+
// on medium-length taps only. Keep the invisible backdrop hit-testable for
466+
// a short while after closing, so that if a ghost click occurs it lands on
467+
// the backdrop instead of the element behind it.
468+
if (navigator.standalone) {
469+
const backdrop = event.currentTarget;
470+
backdrop.style.pointerEvents = 'auto';
471+
// The ghost click arrives shortly after `touchend`.
472+
setTimeout(() => backdrop.style.removeProperty('pointer-events'), 400);
473+
}
474+
461475
this._close();
462476
}
463477

packages/app-layout/test/app-layout.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,48 @@ describe('vaadin-app-layout', () => {
432432
expect(event.defaultPrevented).to.be.true;
433433
});
434434

435+
it('should not make the backdrop hit-testable after touchend when not standalone', () => {
436+
makeSoloTouchEvent('touchend', null, backdrop);
437+
expect(backdrop.style.pointerEvents).to.equal('');
438+
});
439+
440+
// The backdrop is kept hit-testable for a while to catch the ghost click.
441+
describe('standalone (iOS home screen)', () => {
442+
let standaloneDescriptor, clock;
443+
444+
beforeEach(() => {
445+
standaloneDescriptor = Object.getOwnPropertyDescriptor(navigator, 'standalone');
446+
Object.defineProperty(navigator, 'standalone', { configurable: true, value: true });
447+
clock = sinon.useFakeTimers();
448+
});
449+
450+
afterEach(() => {
451+
clock.restore();
452+
if (standaloneDescriptor) {
453+
Object.defineProperty(navigator, 'standalone', standaloneDescriptor);
454+
} else {
455+
delete navigator.standalone;
456+
}
457+
});
458+
459+
it('should keep the backdrop hit-testable after touchend', () => {
460+
makeSoloTouchEvent('touchend', null, backdrop);
461+
expect(backdrop.style.pointerEvents).to.equal('auto');
462+
});
463+
464+
it('should stop keeping the backdrop hit-testable after the timeout', () => {
465+
makeSoloTouchEvent('touchend', null, backdrop);
466+
clock.tick(500);
467+
expect(backdrop.style.pointerEvents).to.equal('');
468+
});
469+
470+
it('should stop keeping the backdrop hit-testable after a click', () => {
471+
makeSoloTouchEvent('touchend', null, backdrop);
472+
backdrop.click();
473+
expect(backdrop.style.pointerEvents).to.equal('');
474+
});
475+
});
476+
435477
it('should close the drawer when calling helper method', () => {
436478
layout.constructor.dispatchCloseOverlayDrawerEvent();
437479
expect(layout.drawerOpened).to.be.false;

0 commit comments

Comments
 (0)