Created
January 14, 2020 13:24
-
-
Save michaelcpuckett/cb2cee57aefb6dd4823cdc4f9d3e7dd7 to your computer and use it in GitHub Desktop.
Polyfill `overscroll-chaining` in iOS Safari
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<title>X App</title> | |
<meta name="viewport" content="width=device-width, height=device-height, viewport-fit=cover, initial-scale=1" /> | |
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> | |
<meta name="apple-mobile-web-app-capable" content="yes" /> | |
<meta name="mobile-web-app-capable" content="yes" /> | |
<link rel="shortcut icon" href="icons/32x32.png" type="image/x-icon" /> | |
<link rel="apple-touch-icon" href="icons/256x256.png" /> | |
<link rel="manifest" href="/manifest.json"> | |
</head> | |
<body> | |
<style> | |
* { | |
box-sizing: border-box; | |
} | |
@supports not (overscroll-behavior: contain) { | |
* { | |
touch-action: none; | |
} | |
x-sidebar, | |
x-sidebar *, | |
x-scrollable, | |
x-scrollable * { | |
touch-action: pan-x pan-y; | |
} | |
} | |
html, | |
body, | |
.screen { | |
height: 100%; | |
width: 100%; | |
overscroll-behavior: contain; | |
} | |
html { | |
height: 100%; | |
} | |
@supports (z-index: min(0, 1)) { | |
html { | |
border-top: env(safe-area-inset-top, 0) solid var(--swatch-brand-primary); | |
height: min(100vh, calc(100% + env(safe-area-inset-top, 0) + env(safe-area-inset-bottom, 0))); | |
} | |
} | |
body { | |
display: flex; | |
flex-direction: column; | |
flex-grow: 0; | |
flex-basis: 100%; | |
flex-shrink: 0; | |
margin: 0; | |
padding: 0; | |
background: var(--swatch-brand-primary); | |
} | |
.screen { | |
flex-grow: 0; | |
flex-basis: 100%; | |
flex-shrink: 0; | |
display: grid; | |
grid-template-columns: 100%; | |
grid-template-rows: auto minmax(0, 1fr); | |
} | |
.header, | |
.subheader, | |
.footer { | |
height: var(--chrome-section-height); | |
padding: 0 var(--page-padding-horizontal, 0); | |
} | |
.page { | |
height: 100%; | |
} | |
.page-inner { | |
padding: var(--page-padding-vertical, 0) var(--page-padding-horizontal, 0); | |
} | |
@supports (z-index: max(0, 1)) { | |
.page-inner { | |
padding-left: max(var(--page-padding-horizontal, 0), env(safe-area-inset-left, 0)); | |
padding-right: max(var(--page-padding-horizontal, 0), env(safe-area-inset-right, 0)); | |
} | |
} | |
.middle { | |
position: relative; | |
} | |
</style> | |
<template id="x-scrollable-support-template"> | |
<style> | |
:host { | |
overflow: auto; | |
will-change: transform; | |
overscroll-behavior: contain; | |
touch-action: pan-x pan-y; | |
height: 100%; | |
width: 100%; | |
} | |
</style> | |
<slot></slot> | |
</template> | |
<template id="x-scrollable-unsupported-template"> | |
<style> | |
:host { | |
overflow: auto; | |
will-change: transform; | |
overscroll-behavior: contain; | |
touch-action: pan-x pan-y; | |
height: 100%; | |
width: 100%; | |
display: grid; | |
grid-template-columns: 100%; | |
grid-template-rows: minmax(max-content, calc(100% + 3px)); | |
} | |
:host([hidden]) { | |
display: none; | |
} | |
.scrollable-inner { | |
width: 100%; | |
justify-content: center; | |
min-height: calc(100% + 3px); | |
touch-action: pan-x pan-y; | |
} | |
:host([horizontal]) { | |
grid-template-columns: max-content; | |
} | |
:host([horizontal]) .scrollable-inner { | |
width: max-content; | |
} | |
</style> | |
<div class="scrollable-inner"> | |
<slot></slot> | |
</div> | |
</template> | |
<script> | |
;(() => { | |
const shouldPolyfill = !window.CSS.supports('overscroll-behavior', 'contain') | |
window.customElements.define('x-scrollable', class XScrollable extends HTMLElement { | |
constructor() { | |
super() | |
} | |
async handleScroll() { | |
await new Promise(resolve => window.requestAnimationFrame(resolve)) | |
const { | |
scrollTop, | |
scrollLeft, | |
scrollHeight, | |
clientHeight | |
} = this | |
const atTop = scrollTop === 0 | |
const beforeTop = 1 | |
const atBottom = scrollTop === scrollHeight - clientHeight | |
const beforeBottom = scrollHeight - clientHeight - 1 | |
if (atTop) { | |
this.scrollTo(scrollLeft, beforeTop) | |
} else if (atBottom) { | |
this.scrollTo(scrollLeft, beforeBottom) | |
} | |
} | |
connectedCallback() { | |
const template = window.document.getElementById(`x-scrollable-${shouldPolyfill ? 'unsupported' : 'supported'}-template`).content.cloneNode(true) | |
this.attachShadow({ mode: 'open' }) | |
this.shadowRoot.append(template) | |
if (shouldPolyfill) { | |
this.boundHandleScroll = this.handleScroll.bind(this) | |
this.addEventListener('scroll', this.boundHandleScroll) | |
if (window.IntersectionObserver) { | |
const intersectionObserver = new IntersectionObserver(this.boundHandleScroll, { | |
root: this.parentElement, | |
rootMargin: '0px', | |
threshold: 1.0 | |
}) | |
intersectionObserver.observe(this) | |
} else { | |
this.handleScroll() | |
} | |
} | |
} | |
disconnectedCallback() { | |
this.removeEventListener('scroll', this.boundHandleScroll) | |
} | |
}) | |
})() | |
</script> | |
<aside class="screen"> | |
<div class="top"> | |
<header class="header"> | |
<a href="/"> | |
<img src="logo.svg" alt="X App" draggable="false" /> | |
</a> | |
<x-sidebar-toggle>Toggle Sidebar</x-sidebar-toggle> | |
</header> | |
<nav class="subheader" aria-label="Subheader"> | |
<button>Earn Points</button> | |
<span>500 points</span> | |
</nav> | |
</div> | |
<main class="middle"> | |
<x-scrollable role="region" class="page page-home" aria-labelledby="h1"> | |
<div class="page-inner"> | |
<h1 id="h1">Main Content</h1> | |
<p> | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies tincidunt lectus imperdiet commodo. Aenean ornare, metus vitae elementum maximus, tortor diam elementum risus, non pharetra justo enim non libero. Donec sed ligula eleifend velit placerat blandit. Fusce dictum sagittis posuere. Mauris congue, lacus id pellentesque sagittis, odio leo pellentesque eros, sed hendrerit risus tellus ac felis. Donec tincidunt dui in commodo iaculis. Phasellus dictum, sem eget imperdiet dignissim, augue lorem tristique nulla, nec feugiat sem augue id lorem. Pellentesque nisl justo, fermentum quis dignissim sed, malesuada in velit. Suspendisse in laoreet tellus, sed tempus orci. Curabitur id egestas ipsum. Etiam sit amet sodales arcu. Nunc sed ligula semper, mollis dui ut, imperdiet nisl. Morbi eget pharetra tellus. Vestibulum a justo id metus mattis interdum eget sit amet orci. | |
</p> | |
<p> | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut turpis non neque sagittis viverra. Pellentesque hendrerit lacus ut nulla malesuada dignissim. Nunc at iaculis velit, at hendrerit magna. Ut faucibus luctus pellentesque. Proin auctor, mi in pulvinar lobortis, neque urna auctor neque, dictum vulputate metus ligula vel eros. Praesent velit nisl, finibus eget dui quis, interdum porttitor odio. Aenean tellus ligula, aliquam eu pretium a, venenatis eget tortor. Nulla quam ante, fermentum vitae erat et, faucibus posuere purus. Donec venenatis mollis risus vitae imperdiet. Sed efficitur lacus sit amet ex semper, ut finibus mi vehicula. | |
</p> | |
<p> | |
Etiam sed velit tempus, suscipit ex a, vestibulum diam. Aenean accumsan feugiat dolor vitae egestas. Sed rutrum fringilla fringilla. Nullam porttitor egestas iaculis. Etiam commodo elit a diam tempor pretium. Vestibulum a mi tristique, tincidunt dolor finibus, porta nibh. Proin blandit posuere cursus. Morbi et odio vel ante volutpat eleifend quis ut erat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus viverra purus eget imperdiet tincidunt. Proin malesuada non mi sed aliquam. Nullam ac magna ut nibh convallis ullamcorper. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; | |
</p> | |
<p> | |
Quisque nec ipsum ex. Curabitur sollicitudin sapien ac arcu elementum, at convallis elit fermentum. Quisque iaculis sapien ante, vitae tempus nisl feugiat non. Cras feugiat laoreet lacus eu dictum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Etiam nec consequat mauris. Donec luctus libero nec efficitur ullamcorper. Fusce condimentum id nunc vel iaculis. Donec congue odio non consequat accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In nec massa nisi. Sed consectetur volutpat ligula, nec pellentesque justo. Aliquam erat velit, gravida non massa vel, ullamcorper hendrerit metus. Phasellus euismod nulla id augue lacinia, non ornare nulla tristique. Donec consequat orci eu luctus luctus. | |
</p> | |
<p> | |
Suspendisse facilisis fermentum volutpat. Suspendisse dictum tincidunt velit et consectetur. Proin nec enim lectus. Nullam non ornare tellus, non mattis augue. Donec sollicitudin velit vel felis elementum, maximus commodo quam luctus. Integer in tincidunt dolor. Pellentesque ornare aliquam eros, eget aliquam nisl rutrum egestas. Nunc tempor mollis mi mollis hendrerit. Nullam risus urna, congue vel vestibulum at, facilisis in odio. Vestibulum non sem tincidunt, ultricies magna ac, hendrerit ex. Sed maximus fringilla velit, ac eleifend orci viverra vel. Integer ac dictum odio. Donec est lacus, elementum sed lorem fringilla, venenatis cursus nibh. | |
</p> | |
</div> | |
</x-scrollable> | |
<x-sidebar id="sidebar" hidden> | |
<h2>Sidebar</h2> | |
<x-sidebar-toggle>Close Sidebar</x-sidebar-toggle> | |
<p> | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies tincidunt lectus imperdiet commodo. Aenean ornare, metus vitae elementum maximus, tortor diam elementum risus, non pharetra justo enim non libero. Donec sed ligula eleifend velit placerat blandit. Fusce dictum sagittis posuere. Mauris congue, lacus id pellentesque sagittis, odio leo pellentesque eros, sed hendrerit risus tellus ac felis. Donec tincidunt dui in commodo iaculis. Phasellus dictum, sem eget imperdiet dignissim, augue lorem tristique nulla, nec feugiat sem augue id lorem. Pellentesque nisl justo, fermentum quis dignissim sed, malesuada in velit. Suspendisse in laoreet tellus, sed tempus orci. Curabitur id egestas ipsum. Etiam sit amet sodales arcu. Nunc sed ligula semper, mollis dui ut, imperdiet nisl. Morbi eget pharetra tellus. Vestibulum a justo id metus mattis interdum eget sit amet orci. | |
</p> | |
<p> | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ut turpis non neque sagittis viverra. Pellentesque hendrerit lacus ut nulla malesuada dignissim. Nunc at iaculis velit, at hendrerit magna. Ut faucibus luctus pellentesque. Proin auctor, mi in pulvinar lobortis, neque urna auctor neque, dictum vulputate metus ligula vel eros. Praesent velit nisl, finibus eget dui quis, interdum porttitor odio. Aenean tellus ligula, aliquam eu pretium a, venenatis eget tortor. Nulla quam ante, fermentum vitae erat et, faucibus posuere purus. Donec venenatis mollis risus vitae imperdiet. Sed efficitur lacus sit amet ex semper, ut finibus mi vehicula. | |
</p> | |
<p> | |
Etiam sed velit tempus, suscipit ex a, vestibulum diam. Aenean accumsan feugiat dolor vitae egestas. Sed rutrum fringilla fringilla. Nullam porttitor egestas iaculis. Etiam commodo elit a diam tempor pretium. Vestibulum a mi tristique, tincidunt dolor finibus, porta nibh. Proin blandit posuere cursus. Morbi et odio vel ante volutpat eleifend quis ut erat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus viverra purus eget imperdiet tincidunt. Proin malesuada non mi sed aliquam. Nullam ac magna ut nibh convallis ullamcorper. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; | |
</p> | |
</x-sidebar> | |
</main> | |
<div class="bottom"> | |
<nav class="footer" aria-label="Footer"> | |
<button>Restart Game</button> | |
</nav> | |
</div> | |
</div> | |
<div class="modal-dialog" role="dialog" aria-modal="true" hidden> | |
<x-scrollable> | |
<div class="modal-dialog-inner"> | |
<h2>Delete 5 Items?</h2> | |
</div> | |
</x-scrollable> | |
</div> | |
<template id="x-sidebar-template"> | |
<style> | |
* { | |
box-sizing: border-box; | |
} | |
:host { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, .5); | |
display: flex; | |
} | |
@supports (backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px)) { | |
:host { | |
-webkit-backdrop-filter: blur(2px); | |
backdrop-filter: blur(2px); | |
} | |
} | |
:host([hidden]) { | |
display: none; | |
} | |
.sidebar { | |
height: 100%; | |
width: var(--sidebar-width); | |
background-color: var(--swatch-background-tertiary); | |
} | |
.sidebar-inner { | |
height: 100%; | |
width: 100%; | |
padding-top: var(--page-padding-vertical, 0); | |
padding-bottom: var(--page-padding-vertical, 0); | |
padding-left: var(--page-padding-horizonal, 0); | |
padding-right: var(--page-padding-horizontal, 0); | |
} | |
@supports (z-index: max(0, 1)) { | |
.sidebar-inner { | |
padding-left: max(var(--page-padding-horizontal, 0), env(safe-area-inset-left, 0)); | |
} | |
} | |
</style> | |
<aside class="sidebar" role="complementary" aria-label="Sidebar"> | |
<x-scrollable> | |
<div class="sidebar-inner"> | |
<slot></slot> | |
</div> | |
</x-scrollable> | |
</aside> | |
</template> | |
<script> | |
window.customElements.define('x-sidebar', class XSidebar extends HTMLElement { | |
constructor() { | |
super() | |
this.boundHandleClick = this.handleClick.bind(this) | |
} | |
static get observedAttributes() { | |
return ['hidden'] | |
} | |
set hidden(value) { | |
this.setAttribute('hidden', value) | |
} | |
handleClick({ target }) { | |
if (target === this) { | |
const hidden = this.hasAttribute('hidden') | |
if (hidden) { | |
this.removeAttribute('hidden') | |
} else { | |
this.setAttribute('hidden', '') | |
} | |
this.dispatchEvent(new CustomEvent('change', { detail: !hidden })) | |
} | |
} | |
connectedCallback() { | |
const template = window.document.getElementById('x-sidebar-template').content.cloneNode(true) | |
this.attachShadow({ mode: 'open' }) | |
this.shadowRoot.append(template) | |
this.addEventListener('click', this.boundHandleClick) | |
this.addEventListener('change', this.resetScrollPosition) | |
} | |
attributeChangedCallback() { | |
window.requestAnimationFrame(() => { | |
this.shadowRoot.querySelector('x-scrollable').scrollTo(0, 0) | |
}) | |
} | |
disconnectedCallback() { | |
this.removeEventListener('click', this.boundHandleChange) | |
this.removeEventListener('change', this.resetScrollPosition) | |
} | |
}) | |
</script> | |
<template id="x-sidebar-toggle-template"> | |
<button type="button" aria-controls="sidebar" aria-expanded="false"> | |
<x-icon glyph="hamburger"></x-icon> | |
<slot></slot> | |
</button> | |
</template> | |
<script> | |
;(() => { | |
const sidebar = window.document.querySelector('x-sidebar') | |
window.customElements.define('x-sidebar-toggle', class XSidebarToggle extends HTMLElement { | |
constructor() { | |
super() | |
this.boundHandleChange = this.handleChange.bind(this) | |
sidebar.addEventListener('change', this.boundHandleChange) | |
this.addEventListener('click', this.handleClick) | |
} | |
handleChange({ target, detail: isShown }) { | |
this.setAttribute('aria-expanded', isShown ? 'true' : 'false') | |
} | |
handleClick() { | |
const isHidden = sidebar.hasAttribute('hidden') | |
if (isHidden) { | |
sidebar.removeAttribute('hidden') | |
} else { | |
sidebar.setAttribute('hidden', '') | |
} | |
sidebar.dispatchEvent(new CustomEvent('change', { detail: isHidden })) | |
} | |
connectedCallback() { | |
const template = window.document.getElementById('x-sidebar-toggle-template').content.cloneNode(true) | |
this.attachShadow({ mode: 'open' }) | |
this.shadowRoot.append(template) | |
} | |
disconnectedCallback() { | |
sidebar.removeEventListener('change', this.boundHandleChange) | |
this.addEventListener('click', this.handleClick) | |
} | |
}) | |
})() | |
</script> | |
<style> | |
:root { | |
--swatch-brand-primary: deepskyblue; | |
--swatch-background-primary: #ddd; | |
--swatch-background-secondary: #fff; | |
--swatch-background-tertiary: lightblue; | |
--page-padding-horizontal: 1em; | |
--page-padding-vertical: 1em; | |
--sidebar-width: 320px; | |
--chrome-section-height: 4em; | |
} | |
.header, | |
.subheader, | |
.footer { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
} | |
.header, | |
.footer { | |
background: var(--swatch-brand-primary); | |
} | |
.subheader { | |
background: var(--swatch-background-secondary); | |
} | |
.footer { | |
padding-bottom: env(safe-area-inset-bottom, 0); | |
box-sizing: content-box; | |
} | |
.page-home { | |
background: var(--swatch-background-primary); | |
} | |
x-sidebar-toggle { | |
display: inline-grid; | |
width: max-content; | |
height: max-content; | |
} | |
</style> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment