mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Compare commits
1 Commits
6f04e21146
...
feature/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
344f4bc98c |
@@ -46,6 +46,8 @@ const CSS_CLASS = Object.freeze({
|
||||
// Custom js events
|
||||
const CUSTOM_EVENT = Object.freeze({
|
||||
RESIZE_END: 'loco.resizeEnd',
|
||||
VISIT_START: 'visit.start',
|
||||
MODAL_OPEN: 'modal.open',
|
||||
// ...
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export {default as Example} from './modules/Example';
|
||||
export {default as Load} from './modules/Load';
|
||||
export {default as Modal} from './modules/Modal';
|
||||
export {default as Scroll} from './modules/Scroll';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { module } from 'modujs';
|
||||
import modularLoad from 'modularload';
|
||||
import { CUSTOM_EVENT } from '../config';
|
||||
|
||||
export default class extends module {
|
||||
constructor(m) {
|
||||
@@ -14,6 +15,12 @@ export default class extends module {
|
||||
}
|
||||
});
|
||||
|
||||
load.on('loading', (transition, oldContainer) => {
|
||||
const args = { transition, oldContainer };
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent(CUSTOM_EVENT.VISIT_START, { detail: args }))
|
||||
});
|
||||
|
||||
load.on('loaded', (transition, oldContainer, newContainer) => {
|
||||
this.call('destroy', oldContainer, 'app');
|
||||
this.call('update', newContainer, 'app');
|
||||
|
||||
195
assets/scripts/modules/Modal.js
Normal file
195
assets/scripts/modules/Modal.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import { createFocusTrap } from 'focus-trap'
|
||||
import { module as Module } from 'modujs'
|
||||
import { $html } from '../utils/dom'
|
||||
import { CUSTOM_EVENT } from '../config'
|
||||
|
||||
/**
|
||||
* Generic component to display a modal.
|
||||
*
|
||||
*/
|
||||
export default class Modal extends Module {
|
||||
/**
|
||||
* Creates a new Modal.
|
||||
*
|
||||
* @param {object} options - The module options.
|
||||
* @param {string} options.dataName - The module data attribute name.
|
||||
* @throws {TypeError} If the class does not have an active CSS class defined.
|
||||
*/
|
||||
|
||||
static CLASS = {
|
||||
EL: 'is-open',
|
||||
HTML: 'has-modal-open',
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
super(options)
|
||||
|
||||
// Data
|
||||
this.moduleName = options.name
|
||||
this.dataName = this.getData('name') || options.dataName
|
||||
|
||||
// Bindings
|
||||
this.toggle = this.toggle.bind(this)
|
||||
this.onModalOpen = this.onModalOpen.bind(this)
|
||||
this.onVisitStart = this.onVisitStart.bind(this)
|
||||
|
||||
// UI
|
||||
this.$togglers = document.querySelectorAll(`[data-${this.dataName}-toggler]`)
|
||||
this.$focusTrapTargets = Array.from(this.el.querySelectorAll(`[data-${this.dataName}-target]`))
|
||||
|
||||
// Focus trap options
|
||||
this.focusTrapOptions = {
|
||||
/**
|
||||
* There is a delay between when the class is applied
|
||||
* and when the element is focusable
|
||||
*/
|
||||
checkCanFocusTrap: (trapContainers) => {
|
||||
const results = trapContainers.map((trapContainer) => {
|
||||
return new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (
|
||||
getComputedStyle(trapContainer).visibility !==
|
||||
'hidden'
|
||||
) {
|
||||
resolve()
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 5)
|
||||
})
|
||||
})
|
||||
|
||||
// Return a promise that resolves when all the trap containers are able to receive focus
|
||||
return Promise.all(results)
|
||||
},
|
||||
|
||||
onActivate: () => {
|
||||
this.el.classList.add(Modal.CLASS.EL)
|
||||
$html.classList.add(Modal.CLASS.HTML)
|
||||
$html.classList.add('has-'+this.dataName+'-open')
|
||||
this.el.setAttribute('aria-hidden', false)
|
||||
this.isOpen = true
|
||||
|
||||
this.onActivate?.();
|
||||
},
|
||||
|
||||
onPostActivate: () => {
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.setAttribute('aria-expanded', true)
|
||||
})
|
||||
},
|
||||
|
||||
onDeactivate: () => {
|
||||
this.el.classList.remove(Modal.CLASS.EL)
|
||||
$html.classList.remove(Modal.CLASS.HTML)
|
||||
$html.classList.remove('has-'+this.dataName+'-open')
|
||||
this.el.setAttribute('aria-hidden', true)
|
||||
this.isOpen = false
|
||||
|
||||
this.onDeactivate?.();
|
||||
},
|
||||
|
||||
onPostDeactivate: () => {
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.setAttribute('aria-expanded', false)
|
||||
})
|
||||
},
|
||||
|
||||
clickOutsideDeactivates: true,
|
||||
}
|
||||
|
||||
this.isOpen = false
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Lifecycle
|
||||
/////////////////
|
||||
init() {
|
||||
this.onBeforeInit?.()
|
||||
|
||||
this.focusTrap = createFocusTrap(
|
||||
this.$focusTrapTargets.length > 0 ? this.$focusTrapTargets : [this.el],
|
||||
this.focusTrapOptions
|
||||
)
|
||||
|
||||
this.bindEvents()
|
||||
|
||||
this.onInit?.()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.focusTrap?.deactivate?.({
|
||||
returnFocus: false,
|
||||
})
|
||||
|
||||
this.unbindEvents()
|
||||
|
||||
this.onDestroy?.()
|
||||
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Events
|
||||
/////////////////
|
||||
bindEvents() {
|
||||
window.addEventListener(CUSTOM_EVENT.VISIT_START, this.onVisitStart)
|
||||
window.addEventListener(CUSTOM_EVENT.MODAL_OPEN, this.onModalOpen)
|
||||
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.addEventListener('click', this.toggle)
|
||||
})
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
window.removeEventListener(CUSTOM_EVENT.VISIT_START, this.onVisitStart)
|
||||
window.removeEventListener(CUSTOM_EVENT.MODAL_OPEN, this.onModalOpen)
|
||||
|
||||
this.$togglers.forEach(($toggler) => {
|
||||
$toggler.removeEventListener('click', this.toggle)
|
||||
})
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Callbacks
|
||||
/////////////////
|
||||
onVisitStart() {
|
||||
// Close the modal on page change
|
||||
this.close()
|
||||
}
|
||||
|
||||
onModalOpen(event) {
|
||||
// Close the modal if another one is opened
|
||||
if (event.detail !== this.el) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Methods
|
||||
/////////////////
|
||||
toggle(event) {
|
||||
if (this.el.classList.contains(Modal.CLASS.EL)) {
|
||||
this.close(event)
|
||||
} else {
|
||||
this.open(event)
|
||||
}
|
||||
}
|
||||
|
||||
open(args) {
|
||||
if (this.isOpen) return
|
||||
|
||||
this.focusTrap?.activate?.()
|
||||
|
||||
this.onOpen?.(args)
|
||||
|
||||
window.dispatchEvent(new CustomEvent(CUSTOM_EVENT.MODAL_OPEN, { detail: this.el }))
|
||||
}
|
||||
|
||||
close(args) {
|
||||
if (!this.isOpen) return
|
||||
|
||||
this.focusTrap?.deactivate?.()
|
||||
|
||||
this.onClose?.(args)
|
||||
}
|
||||
}
|
||||
52
assets/styles/components/_modal.scss
Normal file
52
assets/styles/components/_modal.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
// ==========================================================================
|
||||
// Components / Modal
|
||||
// ==========================================================================
|
||||
|
||||
.c-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
max-width: inherit;
|
||||
max-height: 100lvh;
|
||||
margin: 0;
|
||||
padding: 0 var(--grid-margin);
|
||||
background: transparent;
|
||||
border: none;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
// Backdrop
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: color(darkest, 0.5);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html.is-first-loaded & {
|
||||
transition: visibility speed(normal);
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.c-modal_inner {
|
||||
width: 100%;
|
||||
max-width: rem(500px);
|
||||
padding: $unit-small;
|
||||
background-color: color(lightest);
|
||||
}
|
||||
@@ -57,6 +57,7 @@
|
||||
@import "components/text";
|
||||
@import "components/button";
|
||||
@import "components/form";
|
||||
@import "components/modal";
|
||||
|
||||
// Utilities
|
||||
// ==========================================================================
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "@locomotivemtl/boilerplate",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"focus-trap": "^7.5.4",
|
||||
"locomotive-scroll": "^5.0.0-beta.13",
|
||||
"modujs": "^1.4.2",
|
||||
"modularload": "^1.2.6",
|
||||
@@ -1595,6 +1596,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/focus-trap": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz",
|
||||
"integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==",
|
||||
"dependencies": {
|
||||
"tabbable": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
@@ -3545,6 +3554,11 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||
},
|
||||
"node_modules/tiny-glob": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||
@@ -4931,6 +4945,14 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"focus-trap": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz",
|
||||
"integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==",
|
||||
"requires": {
|
||||
"tabbable": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
@@ -5681,8 +5703,7 @@
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.0.tgz",
|
||||
"integrity": "sha512-rdVMIi7Q4B0XbXqNUEI+Z4E+pueiu/CS5E6vRCQommzdQ/sgsS4dK42U7GX8oJR+TJOtT+Qv3GkNo6iijUMp3Q==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"postcss-selector-parser": {
|
||||
"version": "6.0.9",
|
||||
@@ -5714,8 +5735,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-1.0.1.tgz",
|
||||
"integrity": "sha1-RRJT3o5YRKNI6WOtXt13aesSlRM=",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"posthtml-parser": {
|
||||
"version": "0.4.2",
|
||||
@@ -6434,6 +6454,11 @@
|
||||
"resolved": "https://registry.npmjs.org/svg4everybody/-/svg4everybody-2.1.9.tgz",
|
||||
"integrity": "sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0="
|
||||
},
|
||||
"tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||
},
|
||||
"tiny-glob": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
|
||||
@@ -6594,8 +6619,7 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"build": "node --no-warnings build/build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"focus-trap": "^7.5.4",
|
||||
"locomotive-scroll": "^5.0.0-beta.13",
|
||||
"modujs": "^1.4.2",
|
||||
"modularload": "^1.2.6",
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<main data-module-example>
|
||||
<div class="o-container">
|
||||
<h1 class="c-heading -h1">Hello</h1>
|
||||
<button data-modal-toggler>Open modal</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -66,6 +67,18 @@
|
||||
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate"
|
||||
title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
|
||||
</footer>
|
||||
|
||||
<div class="c-modal" data-module-modal>
|
||||
<div class="c-modal_inner" data-modal-target>
|
||||
<button data-modal-toggler>Close</button>
|
||||
<h2>Modal</h2>
|
||||
<p>Content</p>
|
||||
<form action="">
|
||||
<input type="text">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user