Last active
January 8, 2021 18:04
-
-
Save peerreynders/68ce3d14cccfe764355703968f40e7ed to your computer and use it in GitHub Desktop.
A progressive disclosure component with regular-elements
This file contains hidden or 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"> | |
<!-- | |
"A progressive disclosure component" | |
https://piccalil.li/tutorial/a-progressive-disclosure-component | |
with regular-elements | |
https://github.com/WebReflection/regular-elements | |
instead of a web component | |
--> | |
<head> | |
<meta charset="utf-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<title>Disclosure toggle with regular-elements</title> | |
<style> | |
/* General presentation styles */ | |
html { | |
--color-dark: #212d40; | |
--color-light: #f3f3f3; | |
--color-light-glare: #ffffff; | |
--color-light-glare-tl: rgba(255, 255, 255, 0.4); | |
--color-primary: #98b06f; | |
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, | |
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', | |
'Segoe UI Symbol'; | |
background: var(--color-dark); | |
} | |
body { | |
font-family: var(--font-family); | |
} | |
body { | |
padding: 1.5rem; | |
line-height: 1.4; | |
color: var(--color-light); | |
} | |
p { | |
margin: 0; | |
} | |
article > * + * { | |
margin-top: 1.5em; | |
} | |
hr { | |
border: none; | |
border-top: 1px dashed #ccc; | |
margin: 1.5rem 0; | |
} | |
a:not([class]) { | |
color: currentcolor; | |
} | |
a:not([class]):hover, | |
a:not([class]):focus { | |
text-decoration: none; | |
} | |
/* Utilities */ | |
/* Flow utility: https://24ways.org/2018/managing-flow-and-rhythm-with-css-custom-properties/ */ | |
.u-flow { | |
--flow-space: 1em; | |
} | |
.u-flow > * + * { | |
margin-top: var(--flow-space); | |
} | |
/* Objects */ | |
.o-container { | |
max-width: 30rem; | |
margin: 0 auto; | |
} | |
.o-terms { | |
--flow-space: 2rem; | |
} | |
/* Button component */ | |
.c-button { | |
display: inline-block; | |
border: none; | |
padding: 0.6rem 1.2rem 0.8rem 1.2rem; | |
text-decoration: none; | |
background: var(--color-primary); | |
color: var(--color-dark); | |
font-family: var(--font-family); | |
font-size: 1.2rem; | |
font-weight: 700; | |
cursor: pointer; | |
text-align: center; | |
transition: background 250ms ease-in-out, transform 150ms ease; | |
-webkit-appearance: none; | |
-moz-appearance: none; | |
} | |
.c-button:hover, | |
.c-button:focus { | |
background: var(--color-light-glare); | |
} | |
.c-button:focus { | |
outline: 1px solid var(--color-dark); | |
outline-offset: -4px; | |
} | |
.c-button:active { | |
transform: scale(0.99); | |
} | |
/* Link button component */ | |
.c-link-button { | |
display: none; /* We hide this button by default / if there's no JS available */ | |
text-decoration: underline; | |
text-decoration-skip-ink: auto; | |
background: transparent; | |
padding: 0; | |
margin: 0; | |
border: 0; | |
color: var(--color-primary); | |
font-family: var(--font-family); | |
font-size: 1rem; | |
cursor: pointer; | |
-webkit-appearance: none; | |
-moz-appearance: none; | |
} | |
/* When the link-button has a correct aria attribute, we can show it */ | |
.c-link-button[aria-expanded] { | |
display: inline-flex; | |
align-items: center; | |
} | |
.c-link-button svg { | |
opacity: 0.8; | |
margin-left: 0.5rem; | |
transition: all 250ms ease-in-out; | |
font-size: 0.8rem; | |
} | |
.c-link-button[aria-expanded='true'] svg { | |
transform: rotate(-180deg); | |
} | |
.c-link-button:hover, | |
.c-link-button:focus { | |
text-decoration: none; | |
} | |
.c-link-button:focus { | |
text-decoration: none; | |
outline: 1px solid var(--color-light-glare-tl); | |
outline-offset: 0.6rem; | |
} | |
.c-link-button:active { | |
transform: scale(0.99); | |
} | |
/* Panel component which is completely controlled by a valid sibling | |
button element, such as the link button. We only conceal the content | |
if its sibling button has the correct aria attribute, so we can enforce | |
accessibility and progressively enhance the UI */ | |
[aria-expanded] + .c-panel { | |
--panel-max-height: 500px; | |
transition: all 200ms ease; | |
position: relative; | |
overflow-y: auto; | |
overflow-x: hidden; | |
max-height: 0; | |
visibility: hidden; | |
-webkit-overflow-scrolling: touch; | |
} | |
[aria-expanded] + .c-panel .c-panel__inner { | |
transition: all 500ms ease; | |
transition-delay: 50ms; | |
opacity: 0; | |
transform: translateY(1rem); | |
padding-top: 1.5rem; | |
} | |
[aria-expanded='true'] + .c-panel { | |
max-height: var(--panel-max-height); | |
visibility: visible; | |
} | |
[aria-expanded='true'] + .c-panel .c-panel__inner { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
</style> | |
</head> | |
<body> | |
<main class="o-container u-flow"> | |
<h1>Buy this thing!</h1> | |
<p>It is very good and there's absolutely no hidden exceptions!</p> | |
<a href="http://example.com" class="c-button">Buy it now!!</a> | |
<!-- BEGIN disclosure-toggle HTML fragment --> | |
<div class="o-terms" data-relement="disclosure-toggle"> | |
<!-- BEGIN link-button HTML fragment --> | |
<button class="c-link-button js-trigger" type="button" hidden> | |
See the terms & conditions | |
<svg | |
viewbox="0 0 512 512" | |
aria-hidden="true" | |
fill="currentColor" | |
width="1em" | |
height="1em" | |
> | |
<path d="M60 99.333l196 196 196-196 60 60-256 256-256-256z"></path> | |
</svg> | |
</button> | |
<!-- END link-button HTML fragment --> | |
<!-- BEGIN panel HTML fragment --> | |
<div class="c-panel js-panel"> | |
<article class="c-panel__inner js-panel__inner"> | |
<p> | |
Here's some secret terms and conditions that we didn't want you to | |
see because they explain how the product isn't actually very good. | |
</p> | |
<p> | |
Lorem ipsum dolor sit amet, appareat pertinax et ius, ne pro nibh | |
consulatu consetetur, nulla virtute definitiones nec in. Ad consul | |
feugait eligendi mea, mutat tamquam ei mei. No hinc graecis | |
phaedrum pro, cu erat ipsum sed, ut novum dissentiunt ullamcorper | |
pro. <a href="#">Dicat aliquid dissentias</a> in per, meis alterum | |
quaestio mei eu, vero praesent ex eam. Ei nam homero noluisse | |
dissentiunt, ut vim quot putent. Vis elitr accusam accommodare id, | |
cu usu quaestio conceptam, habeo tibique placerat eos cu. An | |
nullam corpora consulatu qui, graeci euripidis est et. | |
</p> | |
<p> | |
In tantas scripta nominati quo, ne essent maluisset voluptaria | |
nam. Dicat putent feugiat ei sed, te vis delicata gubergren | |
honestatis, ius liber blandit delicata ut. Vix et dictas detracto | |
voluptua, vis an inani dicunt. Qui inciderint intellegebat ea, | |
cetero verear cu duo. | |
</p> | |
<p> | |
Vix id etiam sapientem. Vix case velit feugait eu. Id diceret | |
delenit perpetua vis, has sale utamur aeterno te. Cum et | |
consetetur mediocritatem. | |
</p> | |
</article> | |
</div> | |
<!-- BEGIN panel HTML fragment --> | |
</div> | |
<!-- END disclosure-toggle HTML fragment --> | |
</main> | |
<script src="https://unpkg.com/regular-elements"></script> | |
<script type="module"> | |
// Note: CSS is looking for `[aria-expanded="true"]` to show content | |
const ARIA_EXPANDED = 'aria-expanded'; | |
function toggleAriaExpanded(element) { | |
const nextValue = | |
element.getAttribute(ARIA_EXPANDED) === 'false' ? 'true' : 'false'; | |
element.setAttribute(ARIA_EXPANDED, nextValue); | |
return nextValue; | |
} | |
// CSS Custom Property that is updated with the panel's content's height | |
const MAX_HEIGHT_KEY = '--panel-max-height'; | |
function makeToggle(trigger, panel, panelInner) { | |
const toggleTrigger = () => toggleAriaExpanded(trigger); | |
return () => { | |
if (!(panel && panelInner)) return; | |
// setMaxHeight - looks for content within the panel and | |
// calculates its height. This makes the transition smoother. | |
const height = panelInner.getBoundingClientRect().height; | |
panel.style.setProperty(MAX_HEIGHT_KEY, `${height}px`); | |
// toggle 'aria-expanded' after `setMaxHeight` had some time to complete | |
queueMicrotask(toggleTrigger); | |
}; | |
} | |
function makeToggleHandler(trigger, panel, panelInner) { | |
const toggle = makeToggle(trigger, panel, panelInner); | |
const handler = { | |
handleEvent, | |
dispose, | |
}; | |
toggle(); | |
trigger.addEventListener('click', handler); | |
return handler; | |
// https://medium.com/@WebReflection/dom-handleevent-a-cross-platform-standard-since-year-2000-5bf17287fd38 | |
function handleEvent(event) { | |
switch (event.type) { | |
case 'click': | |
toggle(); | |
break; | |
} | |
} | |
function dispose() { | |
if (!trigger) return; | |
trigger.removeEventListener('click', this); | |
trigger = undefined; | |
this.handleEvent = undefined; | |
} | |
} | |
// Store handlers for cleanup | |
const handlers = new WeakMap(); | |
function connectedCallback() { | |
const trigger = this.querySelector('.js-trigger'); | |
const panel = this.querySelector('.js-panel'); | |
const panelInner = this.querySelector('.js-panel__inner'); | |
const handler = makeToggleHandler(trigger, panel, panelInner); | |
handlers.set(this, handler); | |
trigger.removeAttribute('hidden'); | |
} | |
function disconnectedCallback() { | |
const handler = handlers.get(this); | |
if (handler) { | |
handlers.delete(this); | |
handler.dispose(); | |
} | |
} | |
const definition = { | |
connectedCallback, | |
disconnectedCallback, | |
}; | |
self.regularElements.define( | |
'[data-relement="disclosure-toggle"]', | |
definition | |
); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment