← back to Dw Theme Boost Fix
snippets/overlay.drawer.liquid
384 lines
{% doc %}
Renders a overlay.drawer component.
@param {string} slot - Content to render within the drawer
@param {string} [id] - The id of the drawer
@param {string} [label] - The label of the drawer
@param {string} [placement] - The placement of the drawer ('start' or 'end'), default: 'start'
@param {string} [gutter] - ('none'|'2xs'|'xs'|'sm'|'md'|'lg'|'xl') - Space between the drawer and the edge of the screen, default: 'lg'
@param {string} [size] - The CSS max-width of the drawer, default: 480px
@param {string} [style] - Inline styles to apply
To open the drawer, trigger a click event on any element with data-open-drawer="{{ id }}"
@example
{% render 'overlay.drawer', slot: blocks %}
{% enddoc %}
{% liquid
assign id = id | default: 'overlay-drawer'
assign label = label | default: ''
assign slot = slot | default: ''
assign placement = placement | default: 'start'
assign gutter = gutter | default: 'lg'
assign size = size | default: '480px'
assign style = style | default: ''
assign style = style | append: '--overlay-drawer-gutter: var(--gap-size-' | append: gutter | append: ');'
assign style = style | append: '--overlay-drawer-size: ' | append: size | append: ';'
%}
<overlay-drawer
id="{{ id }}"
class="
overlay-drawer
overlay-drawer--{{ placement }}
"
style="{{ style }}"
open="false"
>
<div class="overlay-drawer__inner">
<div class="overlay-drawer__header">
{% if label != blank %}
<div class="overlay-drawer__label">
{{ label }}
</div>
{% endif %}
<div class="overlay-drawer__close-button">
{% render 'element.button',
icon: 'close',
inline: true,
transparent: true,
inverted: true,
type: 'button',
attributes: '
data-drawer-close-button
aria-label="Close"
'
%}
</div>
</div>
{{ slot }}
</div>
</overlay-drawer>
{% render 'style.primitive-tokens' %}
{% render 'style.normalize' %}
{% stylesheet %}
:root {
--overlay-drawer-size: 480px;
--overlay-drawer-gutter: var(--gap-size-none); /* better name? */
--overlay-drawer-label-margin-bottom: var(--size-4);
--overlay-drawer-padding-inline: var(--size-4);
--overlay-drawer-padding-inline--xs: initial;
--overlay-drawer-padding-inline--sm: initial;
--overlay-drawer-padding-inline--md: initial;
--overlay-drawer-padding-inline--lg: initial;
--overlay-drawer-padding-inline--xl: initial;
--overlay-drawer-padding-block: var(--size-8);
--overlay-drawer-padding-block--xs: initial;
--overlay-drawer-padding-block--sm: initial;
--overlay-drawer-padding-block--md: initial;
--overlay-drawer-padding-block--lg: initial;
--overlay-drawer-padding-block--xl: initial;
--overlay-drawer-background: initial;
--overlay-drawer-overlay-color: rgba(0, 0, 0, 0.5);
--overlay-drawer-z-index: 9999;
--overlay-drawer-transition-duration: 0.3s;
--overlay-drawer-transition-timing-function: ease-in-out;
}
/* Scroll lock when drawer is open */
body:has(.overlay-drawer[open='true']) {
overflow: hidden;
}
.overlay-drawer {
--_width: calc(100% - var(--overlay-drawer-gutter));
--_max-width: var(--overlay-drawer-size);
--_padding-inline--xs: var(--overlay-drawer-padding-inline--xs, var(--overlay-drawer-padding-inline));
--_padding-inline--sm: var(--overlay-drawer-padding-inline--sm, var(--_padding-inline--xs));
--_padding-inline--md: var(--overlay-drawer-padding-inline--md, var(--_padding-inline--sm));
--_padding-inline--lg: var(--overlay-drawer-padding-inline--lg, var(--_padding-inline--md));
--_padding-inline--xl: var(--overlay-drawer-padding-inline--xl, var(--_padding-inline--lg));
--_padding-inline: var(--_padding-inline--xs);
--_padding-block--xs: var(--overlay-drawer-padding-block--xs, var(--overlay-drawer-padding-block));
--_padding-block--sm: var(--overlay-drawer-padding-block--sm, var(--_padding-block--xs));
--_padding-block--md: var(--overlay-drawer-padding-block--md, var(--_padding-block--sm));
--_padding-block--lg: var(--overlay-drawer-padding-block--lg, var(--_padding-block--md));
--_padding-block--xl: var(--overlay-drawer-padding-block--xl, var(--_padding-block--lg));
--_padding-block: var(--_padding-block--xs);
--_background: var(--overlay-drawer-background, var(--color-secondary, #fff));
display: block;
position: fixed;
inset: 0;
height: 100%;
width: 100%;
transition-property: background-color;
transition-duration: var(--overlay-drawer-transition-duration);
transition-timing-function: var(--overlay-drawer-transition-timing-function);
cursor: pointer;
z-index: var(--overlay-drawer-z-index);
&[open='false'] {
pointer-events: none;
}
&[open='true'] {
background-color: var(--overlay-drawer-overlay-color);
}
}
.overlay-drawer__inner {
background: var(--_background);
cursor: initial;
position: absolute;
width: var(--_width);
max-width: var(--_max-width);
top: 0;
transition-property: transform;
transition-duration: var(--overlay-drawer-transition-duration);
transition-timing-function: var(--overlay-drawer-transition-timing-function);
height: 100%;
overflow-y: auto;
padding-inline: var(--_padding-inline);
padding-block: var(--_padding-block);
.overlay-drawer--start & {
left: max(calc(-1 * var(--_width)), calc(-1 * var(--_max-width)));
}
.overlay-drawer--start[open='true'] & {
transform: translateX(100%);
}
.overlay-drawer--end & {
right: max(calc(-1 * var(--_width)), calc(-1 * var(--_max-width)));
}
.overlay-drawer--end[open='true'] & {
transform: translateX(-100%);
}
}
.overlay-drawer__header {
display: flex;
justify-content: flex-end;
align-items: center;
margin-block-end: var(--overlay-drawer-label-margin-bottom);
&:has(.overlay-drawer__label) {
justify-content: space-between;
}
}
.overlay-drawer__close-button {
line-height: 0;
}
{% endstylesheet %}
{% style %}
@media ({% render 'utility.breakpoint', size: 'sm' %}) {
.overlay-drawer {
--_padding-inline: var(--_padding-inline--sm);
--_padding-block: var(--_padding-block--sm);
}
}
@media ({% render 'utility.breakpoint', size: 'md' %}) {
.overlay-drawer {
--_padding-inline: var(--_padding-inline--md);
--_padding-block: var(--_padding-block--md);
}
}
@media ({% render 'utility.breakpoint', size: 'lg' %}) {
.overlay-drawer {
--_padding-inline: var(--_padding-inline--lg);
--_padding-block: var(--_padding-block--lg);
}
}
@media ({% render 'utility.breakpoint', size: 'xl' %}) {
.overlay-drawer {
--_padding-inline: var(--_padding-inline--xl);
--_padding-block: var(--_padding-block--xl);
}
}
{% endstyle %}
{% javascript %}
class OverlayDrawer extends HTMLElement {
constructor() {
super();
// Store arrow functions to ensure same reference for add/remove
}
connectedCallback() {
this.closeButton = null;
this.triggers = null;
this.previouslyFocused = document.activeElement;
this.trapFocusHandlers = {};
this.abortController = new AbortController();
this.closeButton = this.querySelector('[data-drawer-close-button]');
this.triggers = document.querySelectorAll(
`[data-open-drawer="${this.id}"]`
);
const signal = this.abortController.signal;
this.moveToBody();
this.closeButton.addEventListener('click', () => this.close(), { signal });
this.triggers.forEach((trigger) => {
trigger.addEventListener('click', (event) => {
event.preventDefault();
this.open();
}, { signal });
});
document.addEventListener('keydown', (event) => this.handleKeydown(event), { signal });
this.addEventListener('click', (event) => this.handleOverlayClick(event), { signal });
}
disconnectedCallback() {
this.abortController.abort();
}
open() {
this.previouslyFocused = document.activeElement;
this.setAttribute('open', 'true');
this.trapFocus(this);
}
close() {
this.setAttribute('open', 'false');
this.removeTrapFocus(this.previouslyFocused);
}
moveToBody() {
if (this.parentNode === document.body) return;
// Store the original parent and next sibling for potential restoration
this.originalParent = this.parentNode;
this.originalNextSibling = this.nextSibling;
// Move the element to the body
document.body.appendChild(this);
}
handleOverlayClick(event) {
if (event.target === this) this.close();
}
handleKeydown(event) {
if (event.key === 'Escape' && this.getAttribute('open') === 'true') {
this.close();
}
}
removeTrapFocus(elementToFocus = null) {
if (this.trapFocusAbortController) {
this.trapFocusAbortController.abort();
this.trapFocusAbortController = null;
}
if (elementToFocus) {
try {
elementToFocus.focus({ preventScroll: true })
} catch (e) {
elementToFocus.focus()
}
}
}
forceFocus(element, options = {}) {
let savedTabIndex = element.tabIndex
element.tabIndex = -1
element.dataset.tabIndex = savedTabIndex
element.focus()
if (typeof options.className !== 'undefined') {
element.classList.add(options.className)
}
element.addEventListener('blur', callback)
function callback(event) {
event.target.removeEventListener(event.type, callback)
element.tabIndex = savedTabIndex
delete element.dataset.tabIndex
if (typeof options.className !== 'undefined') {
element.classList.remove(options.className)
}
}
}
focusable(container) {
return Array.from(
container.querySelectorAll(
"summary, a[href], button:enabled, [tabindex]:not([tabindex^='-']), [draggable], area, input:not([type=hidden]):enabled, select:enabled, textarea:enabled, object, iframe"
)
)
}
trapFocus(container, options = {}) {
let elements = this.focusable(container)
let elementToFocus = options.elementToFocus || container
let first = elements[0]
let last = elements[elements.length - 1]
this.removeTrapFocus()
// Create new AbortController for focus trap events
this.trapFocusAbortController = new AbortController();
const signal = this.trapFocusAbortController.signal;
this.trapFocusHandlers.focusin = function (event) {
if (container !== event.target && !container.contains(event.target)) {
this.forceFocus(first || container, options)
}
if (event.target !== container && event.target !== last && event.target !== first) return
document.addEventListener('keydown', this.trapFocusHandlers.keydown, { signal })
}.bind(this)
this.trapFocusHandlers.focusout = function () {
document.removeEventListener('keydown', this.trapFocusHandlers.keydown)
}.bind(this)
this.trapFocusHandlers.keydown = function (event) {
if (event.keyCode !== 9) return // If not TAB key
// On the last focusable element and tab forward, focus the first element.
if (event.target === last && !event.shiftKey) {
event.preventDefault()
this.forceFocus(first || container, options)
}
// On the first focusable element and tab backward, focus the last element.
if ((event.target === container || event.target === first) && event.shiftKey) {
event.preventDefault()
this.forceFocus(last || container, options)
}
}.bind(this)
document.addEventListener('focusout', this.trapFocusHandlers.focusout, { signal })
document.addEventListener('focusin', this.trapFocusHandlers.focusin, { signal })
this.forceFocus(elementToFocus, options)
}
}
customElements.define('overlay-drawer', OverlayDrawer);
{% endjavascript %}