From 1231ff240e6913e5cb03b8c5b1cb5fc9f9637ce3 Mon Sep 17 00:00:00 2001 From: Mick Perkins Date: Thu, 6 Jan 2022 22:16:31 +0000 Subject: [PATCH 01/49] Initial commit --- packages/gallery/.npmignore | 7 + packages/gallery/README.md | 163 ++++++++++++ packages/gallery/example/src/favicon.ico | Bin 0 -> 1150 bytes packages/gallery/example/src/index.html | 34 +++ packages/gallery/example/src/js/index.js | 3 + packages/gallery/example/webpack.config.js | 37 +++ packages/gallery/jest.config.js | 11 + packages/gallery/package.json | 32 +++ packages/gallery/src/index.js | 27 ++ packages/gallery/src/lib/constants.js | 11 + packages/gallery/src/lib/defaults/index.js | 10 + .../gallery/src/lib/defaults/templates.js | 48 ++++ packages/gallery/src/lib/dom.js | 232 ++++++++++++++++++ packages/gallery/src/lib/factory.js | 25 ++ packages/gallery/src/lib/store.js | 25 ++ packages/gallery/src/lib/utils.js | 6 + 16 files changed, 671 insertions(+) create mode 100644 packages/gallery/.npmignore create mode 100644 packages/gallery/README.md create mode 100644 packages/gallery/example/src/favicon.ico create mode 100644 packages/gallery/example/src/index.html create mode 100644 packages/gallery/example/src/js/index.js create mode 100644 packages/gallery/example/webpack.config.js create mode 100644 packages/gallery/jest.config.js create mode 100644 packages/gallery/package.json create mode 100644 packages/gallery/src/index.js create mode 100644 packages/gallery/src/lib/constants.js create mode 100644 packages/gallery/src/lib/defaults/index.js create mode 100644 packages/gallery/src/lib/defaults/templates.js create mode 100644 packages/gallery/src/lib/dom.js create mode 100644 packages/gallery/src/lib/factory.js create mode 100644 packages/gallery/src/lib/store.js create mode 100644 packages/gallery/src/lib/utils.js diff --git a/packages/gallery/.npmignore b/packages/gallery/.npmignore new file mode 100644 index 00000000..97d59ca6 --- /dev/null +++ b/packages/gallery/.npmignore @@ -0,0 +1,7 @@ +.DS_Store +*.log +src +__tests__ +example +coverage +jest.config.js \ No newline at end of file diff --git a/packages/gallery/README.md b/packages/gallery/README.md new file mode 100644 index 00000000..fdcdd6ff --- /dev/null +++ b/packages/gallery/README.md @@ -0,0 +1,163 @@ +# Modal Gallery + +Accessible modal image gallery + +--- + +## Usage + +Install the package +``` +npm i -S @stormid/modal-gallery +``` + +A modal gallery can be created with DOM elements, or programmatically created from a JS Object. + +From HTML +``` + +``` + +Initialise the module +``` +import modalGallery from '@stormid/modal-gallery'; + +const [ gallery ] = modalGallery('.js-modal-gallery'); +``` + +Example MVP CSS +``` +.modal-gallery__outer { + display: none; + opacity: 0; + position: fixed; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; + background-color: rgba(0,0,0,.9); + transition: opacity 500ms ease; +} +.modal-gallery__outer.is--active { + display: block; + opacity: 1; +} +.modal-gallery__img-container { + text-align:center; +} +.modal-gallery__img { + margin:80px auto 0 auto; + max-width:80%; + max-height: 80vh; +} +.modal-gallery__item { + position: fixed; + top:0; + left:0; + right:0; + bottom:0; + opacity:0; + visibility:hidden; +} +.modal-gallery__item.is--active { + opacity:1; + visibility:visible; +} +.modal-gallery__next { + position: fixed; + bottom:50%; + right:25px; +} +.modal-gallery__previous { + position: fixed; + bottom:50%; + left:25px; +} +.modal-gallery__close { + position: fixed; + top:15px; + right:25px; +} +.modal-gallery__close:hover svg, +.modal-gallery__previous:hover svg, +.modal-gallery__next:hover svg{ + opacity:.8 +} +.modal-gallery__total { + position: absolute; + bottom:25px; + right:25px; + color:#fff +} +.modal-gallery__details { + position: fixed; + bottom:0; + left:120px; + right:120px; + padding:0 0 40px 0; + color:#fff; +} +``` + + +To create from a JavaScript Object +``` +import modalGallery from '@stormid/modal-gallery'; + +const [ gallery ] = modalGallery([ + { + src: '//placehold.it/500x500', + srcset:'//placehold.it/800x800 800w, //placehold.it/500x500 320w', + title: 'Image 1', + description: 'Description 1' + }, + { + src: '//placehold.it/300x800', + srcset:'//placehold.it/500x800 800w, //placehold.it/300x500 320w', + title: 'Image 2', + description: 'Description 2' + } +]); + +//e.g. Open the gallery at the second item (index 1) by clicking on a button with the className 'js-modal-gallery__trigger' +document.querySelector('.js-modal-gallery__trigger').addEventListener('click', () => gallery.open(1)); +``` + +## Options + +``` +{ + fullscreen: false, //show gallery in fullscreen + preload: false, //preload all images + totals: true, //show totals + scrollable: false, //modal is scrollable + single: false, //single image or gallery +} +``` + +## API + +modalGallery() returns an array of instances. Each instance exposes the interface +``` +{ + getState, a Function that returns the current state Object + open, a Function that opens the modal gallery +} +``` + +## Tests +``` +npm t +``` + +## License +MIT \ No newline at end of file diff --git a/packages/gallery/example/src/favicon.ico b/packages/gallery/example/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..23d3ceaa0662184438dc549f2066dab7b236eca7 GIT binary patch literal 1150 zcmaJN;U_LLhAhll4l=X}v<^qU-`SS)^4aCW;LEJmXdi9`a0LIHli|DSv~56*$=^~yQt zayhKmYmCQZ)M_=P(`mHZZS;CQXf&E*9O4|fA6&EYHtTx5j)#W_*le~Vmi@!w5UEs3 z<>Q{ZT&|xoztw6XpUFEhxuUEyV+5SG|n@lDwm&<3);c&owK1Zk1K_C!NarsT=JDpBd{+Ub$+wB&~WD*90 z;mBz=n@3y8mv>B_9s6Uk82bIb3L6XtpX*n6isl(kCX;va{Ipsv27>|0 + + + StormID + + + + + diff --git a/packages/gallery/example/src/js/index.js b/packages/gallery/example/src/js/index.js new file mode 100644 index 00000000..dec2ee07 --- /dev/null +++ b/packages/gallery/example/src/js/index.js @@ -0,0 +1,3 @@ +import gallery from '../../../src'; + +gallery('.js-gallery'); \ No newline at end of file diff --git a/packages/gallery/example/webpack.config.js b/packages/gallery/example/webpack.config.js new file mode 100644 index 00000000..c6b42ad2 --- /dev/null +++ b/packages/gallery/example/webpack.config.js @@ -0,0 +1,37 @@ +const path = require('path'); +const webpack = require('webpack'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const pkg = require('../package.json'); + +module.exports = { + entry: './example/src/js/index.js', + output: { + filename: 'app.js', + path: path.resolve(__dirname, './build') + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + new CleanWebpackPlugin(['./build']), + new HtmlWebpackPlugin({ + title: pkg.name, + template: './example/src/index.html', + filename: 'index.html' + }) + ], + module: { + rules: [{ + test: /\.js$/, + exclude: /(node_modules|bower_components)/, + use: { + loader: 'babel-loader', + } + }, + { + test: /\.(ico)$/, + use: { + loader: 'file-loader' + } + }] + } +}; diff --git a/packages/gallery/jest.config.js b/packages/gallery/jest.config.js new file mode 100644 index 00000000..7b7f035b --- /dev/null +++ b/packages/gallery/jest.config.js @@ -0,0 +1,11 @@ +const base = require('../../tools/jest/config.base.js'); +const pack = require('./package'); + +module.exports = { + ...base, + transform: { + '^.+\\.js$': '../../tools/jest/babel-jest-wrapper.js' + }, + displayName: pack.name, + name: pack.name +}; \ No newline at end of file diff --git a/packages/gallery/package.json b/packages/gallery/package.json new file mode 100644 index 00000000..5b541a0b --- /dev/null +++ b/packages/gallery/package.json @@ -0,0 +1,32 @@ +{ + "name": "@stormid/modal-gallery", + "version": "1.0.0-y.0", + "description": "Accessible modal image gallery", + "author": "stormid", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": "https://github.com/stormid/components/tree/master/packages/modal-gallery", + "private": false, + "main": "dist/index.js", + "source": "src/index.js", + "module": "dist/index.modern.mjs", + "unpkg": "dist/index.umd.js", + "browser": "dist/index.umd.js", + "keywords": [ + "stormid", + "component", + "ui", + "modal", + "gallery" + ], + "scripts": { + "build": "npx microbundle --name ModalGallery", + "test": "node_modules/.bin/jest --coverage", + "dev": "webpack-dev-server --config example/webpack.config.js --mode development", + "prod": "webpack --config example/webpack.config.js --mode production", + "prepublish": "npm run -s build" + }, + "gitHead": "9042db4005411c4360ba3e8188e03333c10098f2" +} diff --git a/packages/gallery/src/index.js b/packages/gallery/src/index.js new file mode 100644 index 00000000..60a5ed67 --- /dev/null +++ b/packages/gallery/src/index.js @@ -0,0 +1,27 @@ +import factory from './lib/factory'; + +export default (src, options) => { + if (!src.length) return void console.warn('Gallery cannot be initialised, no images found'); + + let items; + + if (typeof src === 'string'){ + const els = [].slice.call(document.querySelectorAll(src)); + + if (!els.length) return void console.warn('Gallery cannot be initialised, no images found'); + + items = els.map(el => ({ + trigger: el, + src: el.getAttribute('href'), + srcset: el.getAttribute('data-srcset') || null, + sizes: el.getAttribute('data-sizes') || null, + title: el.getAttribute('data-title') || '', + description: el.getAttribute('data-description') || '' + })); + } else items = src; + + return Object.create(factory({ + items, + settings: { ...defaults, ...options } + })); +}; \ No newline at end of file diff --git a/packages/gallery/src/lib/constants.js b/packages/gallery/src/lib/constants.js new file mode 100644 index 00000000..c6f1b11d --- /dev/null +++ b/packages/gallery/src/lib/constants.js @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +export const KEY_CODES = { + TAB: 9, + ESC: 27, + LEFT: 37, + RIGHT: 39 +}; + +export const TRIGGER_EVENTS = ['click', 'keydown' ]; + +export const FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex="-1"])']; \ No newline at end of file diff --git a/packages/gallery/src/lib/defaults/index.js b/packages/gallery/src/lib/defaults/index.js new file mode 100644 index 00000000..00bd624e --- /dev/null +++ b/packages/gallery/src/lib/defaults/index.js @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +import * as templates from './templates'; + +export default { + preload: false, + totals: true, + start: 0, + container: '.js-gallery__container', + templates +}; \ No newline at end of file diff --git a/packages/gallery/src/lib/defaults/templates.js b/packages/gallery/src/lib/defaults/templates.js new file mode 100644 index 00000000..16c1a65c --- /dev/null +++ b/packages/gallery/src/lib/defaults/templates.js @@ -0,0 +1,48 @@ +/* istanbul ignore file */ +import { sanitize } from '../utils'; + +export const container = () => { + const container = document.createElement('div'); + container.className = 'gallery__container'; + + return container; +}; + +export const overlayInner = (buttons, items) => ` + ${buttons} + + `; + +export const buttons = () => ` + `; + +export const item = items => (details, i) => ``; + +export const details = item => item.title || item.description + ? `` + : ''; \ No newline at end of file diff --git a/packages/gallery/src/lib/dom.js b/packages/gallery/src/lib/dom.js new file mode 100644 index 00000000..00244e07 --- /dev/null +++ b/packages/gallery/src/lib/dom.js @@ -0,0 +1,232 @@ +import { TRIGGER_EVENTS, KEY_CODES } from './constants'; +import { getFocusableChildren } from './utils'; + +export const initTriggers = Store => state => { + const { items, settings } = state; + + items.map((item, i) => { + if (!item.trigger) return; + TRIGGER_EVENTS.map(ev => { + item.trigger.addEventListener(ev, e => { + if ((e.keyCode && e.keyCode !== KEY_CODES.ENTER) || (e.which && e.which === 3)) return; + e.preventDefault(); + open(Store)(i); + }); + }); + }); + if (settings.preload) items.map(loadImage(Store)); +}; + +const loadImage = Store => (item, i) => { + try { + const img = new Image(); + const loaded = () => { + const { imageCache } = Store.getState(); + imageCache[i] = img; + Store.dispatch({ imageCache }); + writeImage(Store.getState(), i); + }; + img.onload = loaded; + img.src = item.src; + if (img.complete) loaded(); + } catch (e) { + console.warn(e); + } +}; + +const loadImages = Store => i => { + const { imageCache, items, dom } = Store.getState(); + const indexes = [i]; + + if (items.length > 1) indexes.push(i === 0 ? items.length - 1 : i - 1); + if (items.length > 2) indexes.push(i === items.length - 1 ? 0 : i + 1); + indexes.forEach(idx => { + if (imageCache[idx] === undefined) { + dom.items[idx].classList.add('loading'); + loadImage(Store)(items[idx], idx); + } + }); + +}; + +export const initUI = Store => state => { + const { settings, items, keyListener } = Store.getState(); + //write UI to target container + //persistent UI: + //buttons? + //menu? + //totals? + //list + //items with image container, title, description + const container = document.querySelector(settings.container); + if (!container) return void console.warn(`Gallery cannot be initialised, ${settings.container} not found`); + container.appendChild(settings.templates.container()); + + + + // Store.dispatch({ dom: { + // container, + // items, + // totals, + // focusableChildren: getFocusableChildren(container), + // lastFocused: document.activeElement + // } }, [ + // load(Store), + // initUIButtons(Store), + // writeTotals + // ]); +}; + +const load = Store => state => { + const { imageCache, items, current } = Store.getState(); + if (Object.keys(imageCache).length === items.length) imageCache.map((img, i) => { writeImage(state, i); }); + else loadImages(Store)(current); +}; + +const writeImage = (state, i) => { + const { dom, settings, items } = state; + if (!dom) return; + const imageContainer = dom.items[i].querySelector('.js-modal-gallery__img-container'); + const img = imageContainer.querySelector('.modal-gallery__img'); + if (img) return; + const imageClassName = settings.scrollable ? 'modal-gallery__img modal-gallery__img--scrollable' : 'modal-gallery__img'; + const srcsetAttribute = dom.items[i].srcset ? ` srcset="${dom.items[i].srcset}"` : ''; + const sizesAttribute = dom.items[i].sizes ? ` sizes="${dom.items[i].sizes}"` : ''; + + imageContainer.innerHTML = `${items[i].title}`; + dom.items[i].classList.remove('loading'); +}; + +const initUIButtons = Store => state => { + const { dom } = Store.getState(); + const closeBtn = dom.overlay.querySelector('.js-modal-gallery__close'); + TRIGGER_EVENTS.forEach(ev => { + closeBtn.addEventListener(ev, e => { + if ((e.keyCode && e.keyCode !== KEY_CODES.ENTER) || (e.which && e.which === 3)) return; + close(Store); + }); + }); + + const previousBtn = dom.overlay.querySelector('.js-modal-gallery__previous'); + const nextBtn = dom.overlay.querySelector('.js-modal-gallery__next'); + if (!previousBtn && !nextBtn) return; + + TRIGGER_EVENTS.forEach(ev => { + previousBtn && previousBtn.addEventListener(ev, e => { + if (e.keyCode && e.keyCode !== KEY_CODES.ENTER) return; + previous(Store); + }); + nextBtn && nextBtn.addEventListener(ev, e => { + if (e.keyCode && e.keyCode !== KEY_CODES.ENTER) return; + next(Store); + }); + }); +}; + +export const keyListener = Store => e => { + const { isOpen } = Store.getState(); + if (!isOpen) return; + switch (e.keyCode) { + case KEY_CODES.ESC: + close(Store); + break; + case KEY_CODES.TAB: + trapTab(Store, e); + break; + case KEY_CODES.LEFT: + previous(Store); + break; + case KEY_CODES.RIGHT: + next(Store); + break; + default: + break; + } +}; + +const trapTab = (Store, e) => { + const { dom } = Store.getState(); + const focusedIndex = dom.focusableChildren.indexOf(document.activeElement); + if (e.shiftKey && focusedIndex === 0) { + /* istanbul ignore next */ + e.preventDefault(); + dom.focusableChildren[dom.focusableChildren.length - 1].focus(); + } + /* istanbul ignore next */ + if (!e.shiftKey && focusedIndex === dom.focusableChildren.length - 1) { + e.preventDefault(); + dom.focusableChildren[0].focus(); + } +}; + +const toggle = Store => state => { + const { dom, current, isOpen, settings } = Store.getState(); + dom.overlay.classList.toggle('is--active'); + dom.overlay.setAttribute('aria-hidden', !isOpen); + dom.overlay.setAttribute('tabindex', isOpen ? '0' : '-1'); + isOpen !== null && dom.items[current].classList.add('is--active'); + if (dom.focusableChildren && dom.focusableChildren.length > 0) window.setTimeout(() => { dom.focusableChildren[0].focus(); }, 0); + + settings.fullscreen && toggleFullScreen(state); +}; + +const writeTotals = ({ dom, current, items, settings }) => { + if (settings.totals) dom.totals.innerHTML = `${current + 1}/${items.length}`; +}; + +const toggleFullScreen = ({ isOpen, dom }) => { + if (isOpen){ + dom.overlay.requestFullscreen && dom.overlay.requestFullscreen(); + /* istanbul ignore next */ + dom.overlay.webkitRequestFullscreen && dom.overlay.webkitRequestFullscreen(); + /* istanbul ignore next */ + dom.overlay.mozRequestFullScreen && dom.overlay.mozRequestFullScreen(); + } else { + /* istanbul ignore next */ + document.exitFullscreen && document.exitFullscreen(); + /* istanbul ignore next */ + document.mozCancelFullScreen && document.mozCancelFullScreen(); + /* istanbul ignore next */ + document.webkitExitFullscreen && document.webkitExitFullscreen(); + } +}; + +export const previous = Store => { + const { current, dom } = Store.getState(); + const next = current === 0 ? dom.items.length - 1 : current - 1; + Store.dispatch({ current: next }, [ + () => dom.items[current].classList.remove('is--active'), + () => dom.items[next].classList.add('is--active'), + load(Store), + writeTotals + ]); +}; + +export const next = Store => { + const { current, dom } = Store.getState(); + const next = current === dom.items.length - 1 ? 0 : current + 1; + Store.dispatch({ current: next }, [ + () => dom.items[current].classList.remove('is--active'), + () => dom.items[next].classList.add('is--active'), + load(Store), + writeTotals + ]); +}; + +export const close = Store => { + const { keyListener, lastFocused, dom } = Store.getState(); + Store.dispatch({ current: null, isOpen: false }, [ + () => document.removeEventListener('keydown', keyListener), + () => { if (lastFocused) lastFocused.focus(); }, + // () => dom.items[current].classList.remove('is--active'), + // toggle(Store), + () => dom.overlay.parentNode.removeChild(dom.overlay) + ]); +}; + +export const open = Store => (i = 0) => { + Store.dispatch( + { current: i, isOpen: true }, + [ initUI(Store) ] + ); +}; \ No newline at end of file diff --git a/packages/gallery/src/lib/factory.js b/packages/gallery/src/lib/factory.js new file mode 100644 index 00000000..5274f634 --- /dev/null +++ b/packages/gallery/src/lib/factory.js @@ -0,0 +1,25 @@ +import { createStore } from './store'; +import { initTriggers, keyListener, open } from './dom'; + +/* + * @param settings, Object, merged defaults + options passed in as instantiation config to module default + * @param items, HTMLElement, DOM node to be toggled + * + * @returns Object, Modal Gallery API + */ +export default ({ items, settings }) => { + const Store = createStore(); + + Store.dispatch({ + settings, + items, + imageCache: [], + current: settings.start, + keyListener: keyListener(Store) + }, [ initUI(Store)/*, initTriggers(Store)*/ ]); + + return { + getState: Store.getState, + open: open(Store) + }; +}; \ No newline at end of file diff --git a/packages/gallery/src/lib/store.js b/packages/gallery/src/lib/store.js new file mode 100644 index 00000000..a77d5d19 --- /dev/null +++ b/packages/gallery/src/lib/store.js @@ -0,0 +1,25 @@ +export const createStore = () => { + //shared centralised validator state + let state = {}; + + //state getter + const getState = () => state; + + /** + * Create next state by invoking reducer on current state + * + * Execute side effects of state update, as passed in the update + * + * @param type [String] + * @param nextState [Object] New slice of state to combine with current state to create next state + * @param effects [Array] Array of side effect functions to invoke after state update (DOM, operations, cmds...) + */ + const dispatch = (nextState, effects) => { + state = nextState ? ({ ...state, ...nextState }) : state; + + if (!effects) return; + effects.forEach(effect => effect(state)); + }; + + return { dispatch, getState }; +}; \ No newline at end of file diff --git a/packages/gallery/src/lib/utils.js b/packages/gallery/src/lib/utils.js new file mode 100644 index 00000000..2552908a --- /dev/null +++ b/packages/gallery/src/lib/utils.js @@ -0,0 +1,6 @@ +import { FOCUSABLE_ELEMENTS } from './constants'; + +export const sanitize = item => item.replace(/&/g, '&').replace(//g, '>'); + +export const getFocusableChildren = node => [].slice.call(node.querySelectorAll(FOCUSABLE_ELEMENTS.join(','))); + From 8640a1d672ba0694afa7b62c49246cc549a5143e Mon Sep 17 00:00:00 2001 From: Mick Perkins Date: Thu, 6 Jan 2022 23:18:16 +0000 Subject: [PATCH 02/49] Updates dom and initialisation --- packages/gallery/src/lib/constants.js | 12 +-- .../gallery/src/lib/defaults/templates.js | 48 ++--------- packages/gallery/src/lib/dom.js | 82 +++---------------- packages/gallery/src/lib/factory.js | 8 +- packages/gallery/src/lib/utils.js | 4 - 5 files changed, 24 insertions(+), 130 deletions(-) diff --git a/packages/gallery/src/lib/constants.js b/packages/gallery/src/lib/constants.js index c6f1b11d..713af1cc 100644 --- a/packages/gallery/src/lib/constants.js +++ b/packages/gallery/src/lib/constants.js @@ -1,11 +1 @@ -/* istanbul ignore file */ -export const KEY_CODES = { - TAB: 9, - ESC: 27, - LEFT: 37, - RIGHT: 39 -}; - -export const TRIGGER_EVENTS = ['click', 'keydown' ]; - -export const FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex="-1"])']; \ No newline at end of file +export const TRIGGER_EVENTS = ['click', 'keydown' ]; \ No newline at end of file diff --git a/packages/gallery/src/lib/defaults/templates.js b/packages/gallery/src/lib/defaults/templates.js index 16c1a65c..90c884e6 100644 --- a/packages/gallery/src/lib/defaults/templates.js +++ b/packages/gallery/src/lib/defaults/templates.js @@ -1,48 +1,18 @@ /* istanbul ignore file */ import { sanitize } from '../utils'; -export const container = () => { +export const ui = () => { const container = document.createElement('div'); container.className = 'gallery__container'; + // container.insertAdjacentHTML('beforeend', ); + return container; }; -export const overlayInner = (buttons, items) => ` - ${buttons} - - `; - -export const buttons = () => ` - `; - -export const item = items => (details, i) => ``; - -export const details = item => item.title || item.description - ? `` - : ''; \ No newline at end of file +// export const details = item => item.title || item.description +// ? `` +// : ''; \ No newline at end of file diff --git a/packages/gallery/src/lib/dom.js b/packages/gallery/src/lib/dom.js index 00244e07..c8af87c3 100644 --- a/packages/gallery/src/lib/dom.js +++ b/packages/gallery/src/lib/dom.js @@ -1,22 +1,3 @@ -import { TRIGGER_EVENTS, KEY_CODES } from './constants'; -import { getFocusableChildren } from './utils'; - -export const initTriggers = Store => state => { - const { items, settings } = state; - - items.map((item, i) => { - if (!item.trigger) return; - TRIGGER_EVENTS.map(ev => { - item.trigger.addEventListener(ev, e => { - if ((e.keyCode && e.keyCode !== KEY_CODES.ENTER) || (e.which && e.which === 3)) return; - e.preventDefault(); - open(Store)(i); - }); - }); - }); - if (settings.preload) items.map(loadImage(Store)); -}; - const loadImage = Store => (item, i) => { try { const img = new Image(); @@ -50,7 +31,7 @@ const loadImages = Store => i => { }; export const initUI = Store => state => { - const { settings, items, keyListener } = Store.getState(); + const { settings, items } = Store.getState(); //write UI to target container //persistent UI: //buttons? @@ -60,8 +41,10 @@ export const initUI = Store => state => { //items with image container, title, description const container = document.querySelector(settings.container); if (!container) return void console.warn(`Gallery cannot be initialised, ${settings.container} not found`); - container.appendChild(settings.templates.container()); + container.appendChild(settings.templates.ui()); + + //if (settings.preload) items.map(loadImage(Store)); // Store.dispatch({ dom: { @@ -123,64 +106,19 @@ const initUIButtons = Store => state => { }); }; -export const keyListener = Store => e => { - const { isOpen } = Store.getState(); - if (!isOpen) return; - switch (e.keyCode) { - case KEY_CODES.ESC: - close(Store); - break; - case KEY_CODES.TAB: - trapTab(Store, e); - break; - case KEY_CODES.LEFT: - previous(Store); - break; - case KEY_CODES.RIGHT: - next(Store); - break; - default: - break; - } -}; - -const trapTab = (Store, e) => { - const { dom } = Store.getState(); - const focusedIndex = dom.focusableChildren.indexOf(document.activeElement); - if (e.shiftKey && focusedIndex === 0) { - /* istanbul ignore next */ - e.preventDefault(); - dom.focusableChildren[dom.focusableChildren.length - 1].focus(); - } - /* istanbul ignore next */ - if (!e.shiftKey && focusedIndex === dom.focusableChildren.length - 1) { - e.preventDefault(); - dom.focusableChildren[0].focus(); - } -}; - -const toggle = Store => state => { - const { dom, current, isOpen, settings } = Store.getState(); - dom.overlay.classList.toggle('is--active'); - dom.overlay.setAttribute('aria-hidden', !isOpen); - dom.overlay.setAttribute('tabindex', isOpen ? '0' : '-1'); - isOpen !== null && dom.items[current].classList.add('is--active'); - if (dom.focusableChildren && dom.focusableChildren.length > 0) window.setTimeout(() => { dom.focusableChildren[0].focus(); }, 0); - - settings.fullscreen && toggleFullScreen(state); -}; const writeTotals = ({ dom, current, items, settings }) => { if (settings.totals) dom.totals.innerHTML = `${current + 1}/${items.length}`; }; -const toggleFullScreen = ({ isOpen, dom }) => { - if (isOpen){ - dom.overlay.requestFullscreen && dom.overlay.requestFullscreen(); +export const toggleFullScreen = Store => { + const { isFullScreen, container } = Store.getState(); + if (isFullScreen){ + container.requestFullscreen && container.requestFullscreen(); /* istanbul ignore next */ - dom.overlay.webkitRequestFullscreen && dom.overlay.webkitRequestFullscreen(); + container.webkitRequestFullscreen && container.webkitRequestFullscreen(); /* istanbul ignore next */ - dom.overlay.mozRequestFullScreen && dom.overlay.mozRequestFullScreen(); + container.mozRequestFullScreen && container.mozRequestFullScreen(); } else { /* istanbul ignore next */ document.exitFullscreen && document.exitFullscreen(); diff --git a/packages/gallery/src/lib/factory.js b/packages/gallery/src/lib/factory.js index 5274f634..00d183ec 100644 --- a/packages/gallery/src/lib/factory.js +++ b/packages/gallery/src/lib/factory.js @@ -1,5 +1,5 @@ import { createStore } from './store'; -import { initTriggers, keyListener, open } from './dom'; +import { initUI, toggleFullScreen } from './dom'; /* * @param settings, Object, merged defaults + options passed in as instantiation config to module default @@ -15,11 +15,11 @@ export default ({ items, settings }) => { items, imageCache: [], current: settings.start, - keyListener: keyListener(Store) - }, [ initUI(Store)/*, initTriggers(Store)*/ ]); + isFullScreen: false + }, [ initUI(Store) ]); return { getState: Store.getState, - open: open(Store) + toggleFullScreen: toggleFullScreen.bind(null, Store) }; }; \ No newline at end of file diff --git a/packages/gallery/src/lib/utils.js b/packages/gallery/src/lib/utils.js index 2552908a..764190df 100644 --- a/packages/gallery/src/lib/utils.js +++ b/packages/gallery/src/lib/utils.js @@ -1,6 +1,2 @@ -import { FOCUSABLE_ELEMENTS } from './constants'; - export const sanitize = item => item.replace(/&/g, '&').replace(//g, '>'); -export const getFocusableChildren = node => [].slice.call(node.querySelectorAll(FOCUSABLE_ELEMENTS.join(','))); - From ad57639edda3f3905dd3958fab7e9bc20ae46099 Mon Sep 17 00:00:00 2001 From: Mick Perkins Date: Fri, 7 Jan 2022 00:02:11 +0000 Subject: [PATCH 03/49] Starts tests outline; updates dom --- packages/gallery/__tests__/initialisation.js | 4 ++++ packages/gallery/package.json | 14 ++++++-------- packages/gallery/src/lib/defaults/index.js | 1 + packages/gallery/src/lib/defaults/templates.js | 15 +++++++++++++-- packages/gallery/src/lib/dom.js | 6 ++---- 5 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 packages/gallery/__tests__/initialisation.js diff --git a/packages/gallery/__tests__/initialisation.js b/packages/gallery/__tests__/initialisation.js new file mode 100644 index 00000000..6f8ea893 --- /dev/null +++ b/packages/gallery/__tests__/initialisation.js @@ -0,0 +1,4 @@ +//shoud return and Object of the expected shape, with expected API +//should console.warn if nothing found +//should generate DOM +//should return expected initial state diff --git a/packages/gallery/package.json b/packages/gallery/package.json index 5b541a0b..04b1efc3 100644 --- a/packages/gallery/package.json +++ b/packages/gallery/package.json @@ -1,13 +1,13 @@ { - "name": "@stormid/modal-gallery", - "version": "1.0.0-y.0", - "description": "Accessible modal image gallery", + "name": "@stormid/gallery", + "version": "1.0.0-rc.1", + "description": "Accessible image gallery", "author": "stormid", "license": "MIT", "publishConfig": { "access": "public" }, - "repository": "https://github.com/stormid/components/tree/master/packages/modal-gallery", + "repository": "https://github.com/stormid/components/tree/master/packages/gallery", "private": false, "main": "dist/index.js", "source": "src/index.js", @@ -18,15 +18,13 @@ "stormid", "component", "ui", - "modal", "gallery" ], "scripts": { - "build": "npx microbundle --name ModalGallery", + "build": "npx microbundle --name Gallery", "test": "node_modules/.bin/jest --coverage", "dev": "webpack-dev-server --config example/webpack.config.js --mode development", "prod": "webpack --config example/webpack.config.js --mode production", "prepublish": "npm run -s build" - }, - "gitHead": "9042db4005411c4360ba3e8188e03333c10098f2" + } } diff --git a/packages/gallery/src/lib/defaults/index.js b/packages/gallery/src/lib/defaults/index.js index 00bd624e..7a5627ab 100644 --- a/packages/gallery/src/lib/defaults/index.js +++ b/packages/gallery/src/lib/defaults/index.js @@ -5,6 +5,7 @@ export default { preload: false, totals: true, start: 0, + fullscreen: true, container: '.js-gallery__container', templates }; \ No newline at end of file diff --git a/packages/gallery/src/lib/defaults/templates.js b/packages/gallery/src/lib/defaults/templates.js index 90c884e6..739907a2 100644 --- a/packages/gallery/src/lib/defaults/templates.js +++ b/packages/gallery/src/lib/defaults/templates.js @@ -1,15 +1,26 @@ /* istanbul ignore file */ import { sanitize } from '../utils'; -export const ui = () => { +export const container = items => { const container = document.createElement('div'); container.className = 'gallery__container'; - // container.insertAdjacentHTML('beforeend', ); + container.insertAdjacentHTML('beforeend', `${header(items)}${main(items)}${footer()}`); return container; }; +export const header = (items, settings) => ``; + +export const main = items => ``; + +export const footer = items => ``; + // export const details = item => item.title || item.description // ? ` + + + + + + + `; + console.warn = jest.fn(); + gallery('.js-gallery'); + expect(console.warn).toHaveBeenCalledWith('A live region announcing current and total items is recommended for screen readers.'); + + }); + + it('Should update the live region when navigation occurs', () => { + + document.body.innerHTML = ``; + const [ instance ] = gallery('.js-gallery'); + + expect(instance.getState().dom.liveRegion.textContent).toEqual('1 of 3'); + instance.goTo(1); + expect(instance.getState().dom.liveRegion.textContent).toEqual('2 of 3'); + instance.getState().dom.next.click(); + expect(instance.getState().dom.liveRegion.textContent).toEqual('3 of 3'); + + }); + + it('Should render a custom announcement based on settings', () => { + + document.body.innerHTML = ``; + const [ instance ] = gallery('.js-gallery', { + announcement(current, total) { + return `Now viewing item ${current} of ${total}`; + } + }); + + expect(instance.getState().dom.liveRegion.textContent).toEqual('Now viewing item 1 of 3'); + instance.goTo(1); + expect(instance.getState().dom.liveRegion.textContent).toEqual('Now viewing item 2 of 3'); + instance.getState().dom.next.click(); + expect(instance.getState().dom.liveRegion.textContent).toEqual('Now viewing item 3 of 3'); + + }); + +}); \ No newline at end of file diff --git a/packages/gallery/__tests__/api.js b/packages/gallery/__tests__/api.js index f7cd6980..7902c7dc 100644 --- a/packages/gallery/__tests__/api.js +++ b/packages/gallery/__tests__/api.js @@ -7,7 +7,7 @@ beforeAll(() => { document.body.innerHTML = `