1
0
mirror of https://github.com/locomotivemtl/locomotive-boilerplate.git synced 2026-01-15 00:55:08 +08:00

3 Commits

Author SHA1 Message Date
Lucas Bigot
344f4bc98c Add default Modal component 2024-08-02 12:41:22 -04:00
Chauncey McAskill
d593fe5409 Update to NPM v10 + Update dependencies (#179)
* Update NPM constraint and dependencies

Requied:
- NPM v8 → v10

Changed:
- Fixed Node/NPM requirements in README.
- Fixed dependency vulenerabilities.
- Updated dependencies.
- Removed obsolete Node flag `--experimental-json-modules`.
- Replaced obsolete import `assert` keyword with `with`.

* Update gitignore of assets to use wildcards

---------

Co-authored-by: Deven Caron <devencaron@gmail.com>
2024-07-04 13:12:54 -04:00
Chauncey McAskill
f8a46043a6 Remove babel-polyfill (#178)
All features are suported by modern browsers.

Can be restored if ever needed to support older browsers.
2024-07-04 13:12:18 -04:00
16 changed files with 1411 additions and 3111 deletions

12
.gitignore vendored
View File

@@ -5,13 +5,9 @@ loconfig.*.json
!loconfig.example.json
.prettierrc
www/assets/scripts/app.js
www/assets/scripts/app.js.map
www/assets/scripts/vendors.js
www/assets/styles/main.css
www/assets/styles/main.css.map
www/assets/styles/critical.css
www/assets/styles/critical.css.map
www/assets/scripts/*
!www/assets/scripts/.gitkeep
www/assets/styles/*
!www/assets/styles/.gitkeep
assets.json

2
.nvmrc
View File

@@ -1 +1 @@
v20.10
v20

View File

@@ -23,8 +23,8 @@ Learn more about [languages and technologies](docs/technologies.md).
Make sure you have the following installed:
* [Node] — at least 17.9, the latest LTS is recommended.
* [NPM] — at least 8.0, the latest LTS is recommended.
* [Node] — at least 20, the latest LTS is recommended.
* [NPM] — at least 10, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.

View File

@@ -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',
// ...
})

View File

@@ -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';

View File

@@ -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');

View 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)
}
}

View 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);
}

View File

@@ -57,6 +57,7 @@
@import "components/text";
@import "components/button";
@import "components/form";
@import "components/modal";
// Utilities
// ==========================================================================

View File

@@ -2,14 +2,14 @@
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json' assert { type: 'json' };
import loconfig from '../../loconfig.json' with { type: 'json' };
import { merge } from '../utils/index.js';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json', {
assert: { type: 'json' },
with: { type: 'json' },
});
usrconfig = usrconfig.default;

4188
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,39 +6,44 @@
"author": "Locomotive <info@locomotive.ca>",
"type": "module",
"engines": {
"node": "20.x",
"npm": ">=8.0"
"node": ">=20",
"npm": ">=10"
},
"scripts": {
"start": "node --experimental-json-modules --no-warnings build/watch.js",
"build": "node --experimental-json-modules --no-warnings build/build.js"
"start": "node --no-warnings build/watch.js",
"build": "node --no-warnings build/build.js"
},
"dependencies": {
"locomotive-scroll": "^5.0.0-beta.11",
"focus-trap": "^7.5.4",
"locomotive-scroll": "^5.0.0-beta.13",
"modujs": "^1.4.2",
"modularload": "^1.2.6",
"svg4everybody": "^2.1.9"
},
"devDependencies": {
"autoprefixer": "^10.4.17",
"autoprefixer": "^10.4.19",
"browser-sync": "^3.0.2",
"common-path": "^1.0.1",
"concat": "^1.0.3",
"esbuild": "^0.20.0",
"esbuild": "^0.21.5",
"kleur": "^4.1.5",
"node-notifier": "^10.0.1",
"postcss": "^8.4.21",
"purgecss": "^5.0.0",
"sass": "^1.70.0",
"svg-mixer": "~2.3.14",
"postcss": "^8.4.38",
"purgecss": "^6.0.0",
"sass": "^1.77.6",
"svg-mixer": "^2.3.14",
"tiny-glob": "^0.2.9"
},
"overrides": {
"browser-sync": {
"ua-parser-js": "~1.0.33"
"ua-parser-js": "^1.0.33"
},
"svg-mixer": {
"postcss": "^8.4.20"
"micromatch": "^4.0.4",
"postcss": "^8.4.38"
},
"svg-mixer-utils": {
"anymatch": "^3.1.3"
}
}
}

View File

@@ -96,8 +96,6 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js" crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>

View File

@@ -83,9 +83,6 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"
crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>

View File

@@ -122,8 +122,6 @@
</div>
</div>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js" crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>

View File

@@ -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,12 +67,21 @@
<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>
<script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"
crossorigin="anonymous"></script>
<script src="assets/scripts/vendors.js" defer></script>
<script src="assets/scripts/app.js" defer></script>
</body>