├── test ├── commons │ ├── fixture │ │ └── fixture.html │ ├── get-adjacent-item │ │ ├── snippet.html │ │ └── index.js │ ├── get-label │ │ ├── snippet.html │ │ └── index.js │ ├── dialog │ │ ├── selector │ │ │ └── index.js │ │ ├── sizer │ │ │ ├── index.js │ │ │ └── fixture.html │ │ ├── aria │ │ │ ├── fixture.html │ │ │ └── index.js │ │ ├── close │ │ │ ├── fixture.html │ │ │ └── index.js │ │ ├── open │ │ │ ├── fixture.html │ │ │ └── index.js │ │ └── trap-focus │ │ │ ├── fixture.html │ │ │ └── index.js │ ├── is-focusable │ │ ├── selector.js │ │ └── index.js │ ├── rndid │ │ └── index.js │ ├── is-selected │ │ └── index.js │ ├── is-outside │ │ └── index.js │ ├── query-all │ │ └── index.js │ ├── no-clobber │ │ └── index.js │ └── is-visible │ │ └── index.js ├── composites │ ├── landmarks-menu │ │ ├── snippet-existing-menu.html │ │ ├── selector.js │ │ ├── index.js │ │ ├── snippet-empty-menu.html │ │ ├── fix-existing.js │ │ ├── init.js │ │ ├── calulate-text.js │ │ └── create-landmark-menu.js │ ├── menu │ │ ├── utils │ │ │ ├── activate.js │ │ │ └── get-top-level-items.js │ │ ├── init.js │ │ ├── events │ │ │ ├── arrow.js │ │ │ └── resize.js │ │ ├── index.js │ │ └── snippet.html │ ├── alert │ │ ├── fixture.html │ │ └── index.js │ └── modals │ │ ├── fixture.html │ │ └── index.js ├── components │ ├── selects │ │ ├── index.js │ │ ├── validate.js │ │ ├── activate.js │ │ ├── snippet.html │ │ ├── open.js │ │ ├── arrow.js │ │ ├── search.js │ │ ├── init.js │ │ └── select.js │ ├── radio-buttons │ │ ├── index.js │ │ ├── snippet.html │ │ ├── get-selected-index.js │ │ ├── traverse.js │ │ └── set-selected.js │ ├── field-help │ │ ├── snippet.html │ │ ├── index.js │ │ ├── create-tooltip.js │ │ └── setup.js │ ├── checkboxes │ │ ├── snippet.html │ │ ├── index.js │ │ └── events.js │ └── option-menus │ │ └── snippet.html ├── global │ └── dialog │ │ ├── fixture.html │ │ └── index.js └── fixture.js ├── .npmignore ├── .gitignore ├── lib ├── commons │ ├── dialog │ │ ├── selector │ │ │ └── index.js │ │ ├── sizer │ │ │ └── index.js │ │ ├── open │ │ │ └── index.js │ │ ├── close │ │ │ └── index.js │ │ ├── aria │ │ │ └── index.js │ │ └── trap-focus │ │ │ └── index.js │ ├── is-selected │ │ └── index.js │ ├── is-focusable │ │ ├── index.js │ │ └── selector.js │ ├── rndid │ │ └── index.js │ ├── query-all │ │ └── index.js │ ├── flex.less │ ├── is-outside │ │ └── index.js │ ├── scrim.less │ ├── get-label │ │ └── index.js │ ├── no-clobber │ │ └── index.js │ ├── dropdown.less │ ├── get-adjacent-item │ │ └── index.js │ ├── animation │ │ └── animation.less │ ├── is-visible │ │ └── index.js │ └── forms.less ├── components │ ├── tabs │ │ ├── utils │ │ │ ├── get-panel.js │ │ │ └── activate-tab.js │ │ ├── index.js │ │ ├── attributes.js │ │ ├── events.js │ │ └── style.less │ ├── selects │ │ ├── index.js │ │ ├── validate.js │ │ ├── activate.js │ │ ├── open.js │ │ ├── arrow.js │ │ ├── select.js │ │ ├── search.js │ │ ├── init.js │ │ ├── style.less │ │ └── events.js │ ├── radio-buttons │ │ ├── index.js │ │ ├── get-selected-index.js │ │ ├── traverse.js │ │ └── set-selected.js │ ├── field-help │ │ ├── index.js │ │ ├── create-tooltip.js │ │ ├── style.less │ │ └── setup.js │ ├── checkboxes │ │ ├── index.js │ │ ├── events.js │ │ └── attributes.js │ ├── links │ │ └── style.less │ ├── first-time-point │ │ └── index.js │ ├── badges │ │ └── style.less │ ├── option-menus │ │ ├── style.less │ │ └── index.js │ ├── toasts │ │ └── style.less │ └── buttons │ │ └── style.less ├── composites │ ├── alert │ │ ├── index.js │ │ └── style.less │ ├── menu │ │ ├── utils │ │ │ ├── activate.js │ │ │ └── get-top-level-items.js │ │ ├── events │ │ │ ├── arrow.js │ │ │ └── resize.js │ │ ├── index.js │ │ └── init.js │ ├── landmarks-menu │ │ ├── selector.js │ │ ├── calculate-text.js │ │ ├── fix-existing.js │ │ ├── init.js │ │ ├── create-landmark-menu.js │ │ └── index.js │ ├── modals │ │ ├── index.js │ │ └── style.less │ └── tiles │ │ └── tiles.less ├── global │ ├── index.js │ └── dialog │ │ └── index.js ├── base.less └── layout.less ├── bower.json ├── .circleci └── config.yml ├── index.js ├── package.json ├── gulpfile.js └── README.md /test/commons/fixture/fixture.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | lib 3 | node_modules 4 | index.js 5 | gulpfile.js 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | dist/ 4 | playground/ 5 | html-report/ 6 | npm-debug.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /lib/commons/dialog/selector/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = [ 3 | '.dqpl-modal', 4 | '.dqpl-alert', 5 | '.dqpl-toast' 6 | ].join(', '); 7 | -------------------------------------------------------------------------------- /lib/components/tabs/utils/get-panel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (t) => document.getElementById(t.getAttribute('aria-controls')); 4 | -------------------------------------------------------------------------------- /test/commons/get-adjacent-item/snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
A
3 |
B
4 |
C
5 |
6 | -------------------------------------------------------------------------------- /lib/components/selects/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import init from './init'; 4 | 5 | module.exports = () => { 6 | document.addEventListener('dqpl:ready', init); 7 | init(); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/components/radio-buttons/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import setup from './setup'; 4 | 5 | module.exports = () => { 6 | document.addEventListener('dqpl:ready', setup); 7 | setup(); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/commons/is-selected/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Cl from 'classlist'; 4 | 5 | module.exports = (el) => { 6 | return Cl(el).contains('dqpl-selected') || el.getAttribute('aria-checked') === 'true'; 7 | }; 8 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/snippet-existing-menu.html: -------------------------------------------------------------------------------- 1 |
2 | Skip to main content 3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /test/commons/get-label/snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
Foo
3 | 4 |
5 |
Bar
6 |
7 |
8 | -------------------------------------------------------------------------------- /lib/composites/alert/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import trapFocus from '../../commons/dialog/trap-focus'; 4 | 5 | module.exports = () => { 6 | /** 7 | * Keydowns on alerts - trap focus 8 | */ 9 | trapFocus('.dqpl-alert'); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/composites/menu/utils/activate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Configures tabIndex and focus 5 | */ 6 | 7 | module.exports = (prevActive, newlyActive) => { 8 | prevActive.tabIndex = -1; 9 | newlyActive.tabIndex = 0; 10 | newlyActive.focus(); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/composites/landmarks-menu/selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Selector for landmark menu targets 5 | */ 6 | module.exports = [ 7 | '[role="main"]', 8 | '[role="banner"]', 9 | '[role="navigation"]', 10 | '[data-skip-target="true"]' 11 | ].join(', '); 12 | -------------------------------------------------------------------------------- /test/commons/dialog/selector/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | 5 | describe('commons/dialog/selector', () => { 6 | it('should export a string', () => { 7 | assert.equal('string', typeof require('../../../../lib/commons/dialog/selector')); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /lib/components/selects/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('dqpl:components:selects'); 4 | 5 | module.exports = (combobox, listbox) => { 6 | if (listbox.getAttribute('role') !== 'listbox') { 7 | debug('Listbox missing role="listbox" attribute: ', listbox); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /lib/global/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Invoke all globals here 5 | * (globals are shared pieces of code between multiple components/composites) 6 | */ 7 | 8 | module.exports = () => { 9 | /* 10 | * Dialog handles click listeners for the modal and alert. 11 | */ 12 | require('./dialog')(); 13 | }; 14 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const selector = require('../../../lib/composites/landmarks-menu/selector'); 5 | 6 | describe('composites/landmarks-menu/selector', () => { 7 | it('should be a string', () => assert.equal('string', typeof selector)); 8 | }); 9 | -------------------------------------------------------------------------------- /lib/commons/is-focusable/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import matches from 'dom-matches'; 4 | import SELECTOR from './selector'; 5 | 6 | /** 7 | * Checks if element is naturally focusable 8 | * @param {HTMLElement} el the element in question 9 | * @return {Boolean} 10 | */ 11 | module.exports = (el) => matches(el, SELECTOR); 12 | -------------------------------------------------------------------------------- /test/commons/is-focusable/selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const selector = require('../../../lib/commons/is-focusable/selector'); 5 | 6 | 7 | describe('commons/is-focusable/selector', () => { 8 | it('should export a string', () => assert.equal('string', typeof selector)); 9 | }); 10 | -------------------------------------------------------------------------------- /lib/commons/rndid/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import rndm from 'rndm'; 4 | 5 | /** 6 | * Returns a unique dom element id 7 | */ 8 | function rndid(len) { 9 | len = len || 8; 10 | const id = rndm(len); 11 | 12 | if (document.getElementById(id)) { 13 | return rndid(len); 14 | } 15 | 16 | return id; 17 | } 18 | 19 | module.exports = rndid; 20 | -------------------------------------------------------------------------------- /lib/components/field-help/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import setup from './setup'; 4 | 5 | const debug = require('debug')('dqpl:components:field-help'); 6 | 7 | module.exports = () => { 8 | document.addEventListener('dqpl:ready', () => { 9 | debug('dqpl:ready heard - reassessing field help'); 10 | setup(); 11 | }); 12 | 13 | setup(); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/composites/menu/utils/get-top-level-items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import isVisible from '../../../commons/is-visible'; 4 | 5 | module.exports = (ul, visible) => { 6 | if (!ul) { return []; } 7 | 8 | return Array.prototype.slice.call(ul.children) 9 | .filter((c) => c.getAttribute('role') === 'menuitem') 10 | .filter((c) => visible ? isVisible(c) : true); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/components/field-help/create-tooltip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | 5 | /** 6 | * Creates the dqpl tooltip element 7 | */ 8 | module.exports = (text) => { 9 | const tip = document.createElement('div'); 10 | tip.setAttribute('role', 'tooltip'); 11 | tip.innerHTML = text; 12 | Classlist(tip).add('dqpl-tooltip'); 13 | 14 | return tip; 15 | }; 16 | -------------------------------------------------------------------------------- /test/components/selects/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | 6 | describe('components/selects/index', () => { 7 | it('should call init', () => { 8 | let called = false; 9 | proxyquire('../../../lib/components/selects', { 10 | './init': () => called = true 11 | })(); 12 | 13 | assert.isTrue(called); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /lib/components/checkboxes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import attrs from './attributes'; 4 | import events from './events'; 5 | 6 | const debug = require('debug')('dqpl:components:checkboxes'); 7 | 8 | module.exports = () => { 9 | attrs(); 10 | events(); 11 | 12 | document.addEventListener('dqpl:ready', () => { 13 | debug('dqpl:ready heard - reassessing checkbox attributes'); 14 | attrs(); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/commons/query-all/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A querySelectorAll that returns a 5 | * normal array rather than live node list 6 | * @param {String} selector 7 | * @param {HTMLElement} context 8 | * @return {Array} 9 | */ 10 | module.exports = (selector, context) => { 11 | context = context || document; 12 | return Array.prototype.slice.call( 13 | context.querySelectorAll(selector) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | 6 | 7 | describe('composites/landmarks-menu', () => { 8 | it('should call init', () => { 9 | let called = false; 10 | proxyquire('../../../lib/composites/landmarks-menu', { 11 | './init': () => called = true 12 | })(); 13 | assert.isTrue(called); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/snippet-empty-menu.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /lib/commons/flex.less: -------------------------------------------------------------------------------- 1 | @import "../variables.less"; 2 | 3 | // convenience flex classes 4 | .flexr, @{prefix}flexr { 5 | .display-flex(); 6 | .align-items(center); 7 | } 8 | 9 | body .row { 10 | margin-right: auto; 11 | margin-left: auto; 12 | } 13 | 14 | .flex-1 { 15 | .flex(1 0 auto); 16 | } 17 | 18 | .flex-1-1 { 19 | .flex(1); 20 | } 21 | 22 | .flex-none { 23 | .flex(none); 24 | } 25 | 26 | .align-self-center { 27 | .align-self(center); 28 | } 29 | -------------------------------------------------------------------------------- /test/components/radio-buttons/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | 6 | describe('components/radio-buttons/', () => { 7 | it('should call setup', () => { 8 | let called = false; 9 | const entry = proxyquire('../../../lib/components/radio-buttons', { 10 | './setup': () => called = true 11 | }); 12 | 13 | entry(); 14 | 15 | assert.isTrue(called); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/composites/menu/events/arrow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import activate from '../utils/activate'; 4 | 5 | module.exports = (items, target, dir) => { 6 | const isNext = dir === 'next' || dir === 'down'; 7 | const currentIdx = items.indexOf(target); 8 | let adjacent = items[isNext ? currentIdx + 1 : currentIdx - 1]; 9 | // circularity 10 | if (!adjacent) { 11 | adjacent = items[isNext ? 0 : items.length - 1]; 12 | } 13 | 14 | activate(target, adjacent); 15 | }; 16 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deque-pattern-library", 3 | "description": "Deque Pattern Library", 4 | "main": "dist/js/pattern-library.js", 5 | "version": "7.0.0", 6 | "authors": [ 7 | "Harris Schneiderman " 8 | ], 9 | "license": "MPL-2.0", 10 | "homepage": "https://github.com/dequelabs/pattern-library", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "lib" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /lib/commons/dialog/sizer/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Adjusts the height of the dialog content based on window's height 5 | * @param {HTMLElement} el the dialog element 6 | */ 7 | module.exports = (el) => { 8 | if (!el || el.getAttribute('data-no-resize') === 'true') { 9 | return; 10 | } 11 | 12 | const content = el.querySelector('.dqpl-content'); 13 | 14 | if (content) { 15 | content.style.maxHeight = `${window.innerHeight - 205}px`; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/commons/is-focusable/selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Selector for naturally focusable elements 5 | */ 6 | module.exports = [ 7 | 'a[href]', 8 | 'button:not([disabled])', 9 | 'input:not([disabled])', 10 | 'select:not([disabled])', 11 | 'textarea:not([disabled])', 12 | 'area[href]', 13 | 'iframe', 14 | 'object', 15 | 'embed', 16 | '[tabindex="0"]', 17 | '[contenteditable]', 18 | 'audio[controls]', 19 | 'video[controls]', 20 | 'summary' 21 | ].join(', '); 22 | -------------------------------------------------------------------------------- /lib/components/tabs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * DQPL Tabs 5 | * TODO: This should become a proper component eventually 6 | */ 7 | 8 | import events from './events'; 9 | import attrs from './attributes'; 10 | 11 | const debug = require('debug')('dqpl:components:tabs'); 12 | 13 | module.exports = () => { 14 | events(); 15 | attrs(); 16 | 17 | document.addEventListener('dqpl:ready', () => { 18 | debug('dqpl:ready heard - reassessing tab attributes'); 19 | attrs(); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /test/commons/rndid/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-polyfill'); 4 | const assert = require('chai').assert; 5 | const rndid = require('../../../lib/commons/rndid'); 6 | 7 | describe('commons/rndid', () => { 8 | describe('len', () => { 9 | it('should default to 8', () => { 10 | assert.equal(rndid().length, 8); 11 | }); 12 | 13 | it('should accept len as the argument', () => { 14 | const LEN = 40; 15 | assert.equal(rndid(LEN).length, LEN); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/commons/is-outside/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import matches from 'dom-matches'; 4 | import closest from 'closest'; 5 | 6 | /** 7 | * Check if the target is outside of selector 8 | * @param {HTMLElement} target the element in question 9 | * @param {String} selector the selector in question 10 | * @return {Boolean} 11 | */ 12 | 13 | // TODO: closest has a third param - checkSelf...that could replace this 14 | module.exports = (target, selector) => !matches(target, selector) && !closest(target, selector); 15 | -------------------------------------------------------------------------------- /lib/components/radio-buttons/get-selected-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import isSelected from '../../commons/is-selected'; 4 | 5 | module.exports = (radios) => { 6 | const enableds = radios.filter((r) => r.getAttribute('aria-disabled') !== 'true'); 7 | const selecteds = radios.filter(isSelected); 8 | // attempt to use the selected radio (regardless of enabled state) 9 | // defaulting to the first enabled radio (if none in the group are selected) 10 | return radios.indexOf(selecteds.length ? selecteds[0] : enableds[0]); 11 | }; 12 | -------------------------------------------------------------------------------- /test/components/field-help/snippet.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /lib/components/links/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | @{prefix}link { 4 | text-decoration: none; 5 | color: @link; 6 | font-weight: @weight-medium; 7 | display: inline-block; 8 | padding: @space-quarter; 9 | 10 | &:hover { 11 | text-decoration: underline; 12 | color: @link; 13 | } 14 | 15 | &:focus { 16 | text-decoration: underline; 17 | outline-offset: 0; 18 | } 19 | } 20 | 21 | p { 22 | @{prefix}link { 23 | margin: 0 2px; 24 | display: inline; 25 | text-decoration: underline; 26 | color: @header-dark; 27 | font-weight: @weight-normal; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/composites/modals/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import delegate from 'delegate'; 4 | import close from '../../commons/dialog/close'; 5 | import trapFocus from '../../commons/dialog/trap-focus'; 6 | 7 | module.exports = () => { 8 | /** 9 | * Keydowns on modals 10 | * - trap focus 11 | * - escape => close 12 | */ 13 | trapFocus('.dqpl-modal'); 14 | delegate(document.body, '.dqpl-modal', 'keydown', (e) => { 15 | const modal = e.delegateTarget; 16 | const which = e.which; 17 | 18 | if (which === 27 && modal.getAttribute('data-force-action') !== 'true') { 19 | close(modal); 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/components/field-help/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | const fieldHelpEntry = require('../../../lib/components/field-help'); 6 | 7 | describe('components/field-help/index', () => { 8 | it('should be a function', () => { 9 | assert.equal('function', typeof fieldHelpEntry); 10 | }); 11 | 12 | it('should call field-help/setup', () => { 13 | let called = false; 14 | const f = proxyquire('../../../lib/components/field-help', { 15 | './setup': () => called = true 16 | }); 17 | 18 | f(); 19 | assert.isTrue(called); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /lib/composites/menu/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import init from './init'; 4 | 5 | const debug = require('debug')('dqpl:composites:menu'); 6 | 7 | function findTopBar() { 8 | const topBar = document.querySelector('.dqpl-top-bar'); 9 | if (topBar) { 10 | return init(topBar); 11 | } 12 | // no top bar present...wait for dqpl:ready before trying again 13 | document.addEventListener('dqpl:ready', onReadyFire); 14 | } 15 | 16 | function onReadyFire() { 17 | debug('dqpl:ready fired - querying DOM for top bar.'); 18 | document.removeEventListener('dqpl:ready', onReadyFire); 19 | findTopBar(); // try again 20 | } 21 | 22 | module.exports = findTopBar; 23 | -------------------------------------------------------------------------------- /lib/components/first-time-point/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import delegate from 'delegate'; 5 | 6 | module.exports = () => { 7 | delegate(document.body, '.dqpl-pointer-wrap', 'keydown', (e) => { 8 | const which = e.which; 9 | const wrap = e.delegateTarget; 10 | 11 | if (which === 27) { 12 | Classlist(wrap).add('dqpl-hidden'); 13 | } 14 | }); 15 | 16 | delegate(document.body, '.dqpl-ftpo-dismiss', 'click', (e) => { 17 | const closeBtn = e.delegateTarget; 18 | const pointer = closeBtn.closest('.dqpl-pointer-wrap'); 19 | 20 | Classlist(pointer).add('dqpl-hidden'); 21 | }); 22 | 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 2 | 3 | version: 2 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/node:10.11.0 8 | working_directory: ~/repo 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - v1-dependencies-{{ checksum "package.json" }} 14 | - v1-dependencies- 15 | - run: npm install 16 | - save_cache: 17 | paths: 18 | - node_modules 19 | key: v1-dependencies-{{ checksum "package.json" }} 20 | - run: sudo npm i -g gulp@3.9.1 21 | - run: npm run build 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /lib/commons/dialog/open/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import sizer from '../sizer'; 5 | import { hide } from '../aria'; 6 | 7 | module.exports = (trigger, el, focusEl) => { 8 | // Show the element 9 | Classlist(el).add('dqpl-dialog-show'); 10 | Classlist(document.body).add('dqpl-open'); 11 | let scrim = el.querySelector('.dqpl-screen'); 12 | 13 | if (!scrim) { 14 | scrim = document.createElement('div'); 15 | Classlist(scrim).add('dqpl-screen'); 16 | el.appendChild(scrim); 17 | } 18 | 19 | hide(el); 20 | sizer(el); 21 | 22 | if (focusEl) { 23 | focusEl.tabIndex = -1; 24 | focusEl.focus(); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/commons/dialog/close/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import { show } from '../aria'; 5 | 6 | const debug = require('debug')('dqpl:commons:close'); 7 | 8 | /** 9 | * Closes a dialog and returns focus to it's trigger 10 | * @param {HTMLElement} el the dialog element 11 | */ 12 | module.exports = (el) => { 13 | const trigger = document.querySelector(`[data-dialog-id="${el.id}"]`); 14 | 15 | Classlist(el).remove('dqpl-dialog-show'); 16 | Classlist(document.body).remove('dqpl-open'); 17 | 18 | show(el); 19 | 20 | if (trigger) { 21 | trigger.focus(); 22 | } else { 23 | debug('Unable to find trigger for el: ', el); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/composites/menu/utils/activate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const activate = require('../../../../lib/composites/menu/utils/activate'); 5 | 6 | describe('composites/menu/utils/activate', () => { 7 | it('should configure tabindex properly and focus the newly active', () => { 8 | const div1 = document.createElement('div'); 9 | const div2 = document.createElement('div'); 10 | 11 | document.body.appendChild(div1); 12 | document.body.appendChild(div2); 13 | 14 | activate(div1, div2); 15 | assert.equal(div1.tabIndex, -1); 16 | assert.equal(div2.tabIndex, 0); 17 | assert.equal(document.activeElement, div2); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/components/selects/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | 6 | describe('components/selects/validate', () => { 7 | it('should call debug if the listbox is missing the "listbox" role', () => { 8 | const c = document.createElement('div'); 9 | const l = document.createElement('div'); 10 | c.setAttribute('role', 'combobox'); 11 | let called = false; 12 | proxyquire('../../../lib/components/selects/validate', { 13 | 'debug': () => { 14 | return function () { 15 | called = true; 16 | }; 17 | } 18 | })(c, l); 19 | 20 | assert.isTrue(called); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/commons/scrim.less: -------------------------------------------------------------------------------- 1 | @import "../variables.less"; 2 | 3 | .scrim() { 4 | position: fixed; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | opacity: 0; 10 | display: none; 11 | z-index: @z-index-scrim; 12 | .animate(300ms opacity); 13 | } 14 | 15 | @{prefix}scrim { 16 | background-color: @scrim; 17 | .scrim(); 18 | } 19 | 20 | @{prefix}scrim-light { 21 | background-color: rgba(255, 255, 255, 0.75); 22 | .scrim(); 23 | z-index: @z-index-scrim-action-needed; 24 | } 25 | 26 | @{prefix}scrim, @{prefix}scrim-light { 27 | &@{prefix}scrim-show { 28 | display: block; 29 | } 30 | 31 | &@{prefix}scrim-fade-in { 32 | opacity: 1; 33 | .animate(300ms opacity); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/composites/menu/utils/get-top-level-items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const getTopLevels = require('../../../../lib/composites/menu/utils/get-top-level-items'); 5 | 6 | describe('composites/menu/utils/get-top-level-items', () => { 7 | it('should return the expected items', () => { 8 | const div = document.createElement('div'); 9 | div.innerHTML = [ 10 | '' 14 | ].join(''); 15 | document.body.appendChild(div); 16 | const tls = getTopLevels(div.querySelector('ul')); 17 | assert.equal(2, tls.length); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/components/checkboxes/snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
Foo
5 |
6 |
7 | 8 |
Bar
9 |
10 |
11 | 12 |
Baz (disabled)
13 |
14 |
15 | -------------------------------------------------------------------------------- /test/components/field-help/create-tooltip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Classlist = require('classlist'); 4 | const assert = require('chai').assert; 5 | const createTooltip = require('../../../lib/components/field-help/create-tooltip'); 6 | 7 | describe('lib/components/field-help/create-tooltip', () => { 8 | it('should be a function', () => { 9 | assert.equal(typeof createTooltip, 'function'); 10 | }); 11 | 12 | it('should create the tooltip (with proper class, text, and roles)', () => { 13 | const tip = createTooltip('BOOGNISH'); 14 | assert.isTrue(!!tip); 15 | assert.equal('tooltip', tip.getAttribute('role')); 16 | assert.isTrue(Classlist(tip).contains('dqpl-tooltip')); 17 | assert.equal(tip.innerText, 'BOOGNISH'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/commons/get-label/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import closest from 'closest'; 4 | 5 | /** 6 | * Find form field's label element 7 | * @param {HTMLElement} el the field element 8 | * @param {String} parentSelector the selector for the common parent of the field and label 9 | * @param {String} labelSelector the selector for the label element (qualified within the parent) 10 | * @return {HTMLElement} the label element 11 | */ 12 | module.exports = (el, parentSelector, labelSelector) => { 13 | const dataLabelId = el.getAttribute('data-label-id'); 14 | if (dataLabelId) { 15 | return document.getElementById(dataLabelId); 16 | } 17 | 18 | const parent = closest(el, parentSelector); 19 | return parent && parent.querySelector(labelSelector); 20 | }; 21 | -------------------------------------------------------------------------------- /test/components/radio-buttons/snippet.html: -------------------------------------------------------------------------------- 1 |
2 |

Do you link pizza?

3 |
4 | 5 |
Yes
6 |
7 |
8 | 9 |
No
10 |
11 |
12 | 13 | 14 |
Only on tuesdays
15 |
16 |
17 | -------------------------------------------------------------------------------- /test/composites/alert/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 |
Are you sure you want to delete that?
10 |
11 |
12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /test/commons/is-selected/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const isSelected = require('../../../lib/commons/is-selected'); 5 | const element = (tag) => document.createElement(tag || 'div'); 6 | 7 | describe('commons/is-selected', () => { 8 | it('should return true for an element with the "dqpl-selected" class', () => { 9 | const div = element(); 10 | div.className = 'dqpl-selected'; 11 | assert.isTrue(isSelected(div)); 12 | }); 13 | 14 | it('should return true for an element with aria-checked="true"', () => { 15 | const div = element(); 16 | div.setAttribute('aria-checked', 'true'); 17 | assert.isTrue(isSelected(div)); 18 | }); 19 | 20 | it('should return false for an element without selected class / attribute', () => { 21 | assert.isFalse(isSelected(element())); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /lib/commons/no-clobber/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import rndid from '../rndid'; 4 | 5 | /** 6 | * Handles not clobbering existing values in token list attributes 7 | * @param {HTMLElement} target the target element for the attr to be added 8 | * @param {HTMLElement} ref the element that target is being associated with 9 | * @param {String} attr the attr to be added (defaults to 'aria-describedby') 10 | */ 11 | module.exports = (target, ref, attr) => { 12 | attr = attr || 'aria-describedby'; 13 | ref.id = ref.id || rndid(); // ensure it has an id 14 | const existingVal = target.getAttribute(attr); 15 | const values = existingVal ? existingVal.split(' ') : []; 16 | // prevent duplicates 17 | if (values.indexOf(ref.id) > -1) { return; } 18 | 19 | values.push(ref.id); 20 | target.setAttribute(attr, values.join(' ').trim()); 21 | }; 22 | -------------------------------------------------------------------------------- /test/components/radio-buttons/get-selected-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const queryAll = require('../../../lib/commons/query-all'); 5 | const snippet = require('./snippet.html'); 6 | const Fixture = require('../../fixture'); 7 | const getSelectedIndex = require('../../../lib/components/radio-buttons/get-selected-index'); 8 | 9 | describe('components/radio-buttons/get-selected-index', () => { 10 | let fixture; 11 | before(() => { 12 | fixture = new Fixture(); 13 | fixture.create(snippet); 14 | }); 15 | after(() => { 16 | fixture.destroy().cleanUp(); 17 | }); 18 | 19 | it('should return the selected index', () => { 20 | const radios = queryAll('.dqpl-radio', fixture.element); 21 | radios[1].setAttribute('aria-checked', 'true'); 22 | assert.equal(1, getSelectedIndex(radios)); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/commons/is-outside/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const isOutside = require('../../../lib/commons/is-outside'); 5 | 6 | describe('commons/is-outside', () => { 7 | it('should properly check if the target is outside of the selector', () => { 8 | const div = () => document.createElement('div'); 9 | const container = div(); 10 | const other = div(); 11 | container.className = 'container'; 12 | const child1 = div(); 13 | const child2 = div(); 14 | 15 | container.appendChild(child1); 16 | container.appendChild(child2); 17 | 18 | document.body.appendChild(container); 19 | document.body.appendChild(other); 20 | 21 | assert.isFalse(isOutside(child1, '.container')); 22 | assert.isFalse(isOutside(child2, '.container')); 23 | // check self 24 | assert.isFalse(isOutside(container, '.container')); 25 | assert.isTrue(isOutside(other, '.container')); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/composites/landmarks-menu/calculate-text.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const idrefsText = (str) => { 4 | if (!str) { return ''; } 5 | var result = [], index, length; 6 | var idrefs = str.trim().replace(/\s{2,}/g, ' ').split(' '); 7 | for (index = 0, length = idrefs.length; index < length; index++) { 8 | result.push(document.getElementById(idrefs[index]).textContent); 9 | } 10 | return result.join(' '); 11 | }; 12 | const getLabel = (el) => { 13 | return el.getAttribute('aria-label') || idrefsText(el.getAttribute('aria-labelledby')); 14 | }; 15 | 16 | /** 17 | * Retrieves an element's label prioritized in the following order: 18 | * - it's data-skip-to-name attribute 19 | * - it's aria-label or the value of the element(s) 20 | * pointed to in the aria-labelledby attribute 21 | * = it's role 22 | * @param {HTMLElement} el 23 | * @return {String} 24 | */ 25 | module.exports = (el) => el.getAttribute('data-skip-to-name') || getLabel(el) || el.getAttribute('role'); 26 | -------------------------------------------------------------------------------- /lib/components/selects/activate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import queryAll from '../../commons/query-all'; 5 | const scrollIntoViewIfNeedBe = require('scroll-into-view-if-needed'); 6 | 7 | /** 8 | * Activates an option 9 | */ 10 | module.exports = (listbox, noScroll) => { 11 | // clean 12 | queryAll('[role="option"].dqpl-option-active', listbox) 13 | .forEach((o) => { 14 | Classlist(o).remove('dqpl-option-active'); 15 | }); 16 | 17 | queryAll('[aria-selected="true"]', listbox) 18 | .forEach((o) => o.removeAttribute('aria-selected')); 19 | 20 | const optionID = listbox.getAttribute('aria-activedescendant'); 21 | const active = optionID && document.getElementById(optionID); 22 | 23 | if (!active) { 24 | return; 25 | } 26 | 27 | Classlist(active).add('dqpl-option-active'); 28 | active.setAttribute('aria-selected', 'true'); 29 | 30 | if (!noScroll) { 31 | scrollIntoViewIfNeedBe(active, false); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /lib/components/selects/open.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import queryAll from '../../commons/query-all'; 5 | import activate from './activate'; 6 | 7 | /** 8 | * Opens the listbox 9 | */ 10 | module.exports = (listboxButton, listbox) => { 11 | const activeDesc = listbox.getAttribute('aria-activedescendant'); 12 | if (!activeDesc) { 13 | // theres no initially selected => default to the first 14 | const nonDisableds = queryAll('[role="option"]', listbox).filter((o) => { 15 | return o.getAttribute('aria-disabled') !== 'true'; 16 | }); 17 | 18 | if (nonDisableds.length) { 19 | listbox.setAttribute('aria-activedescendant', nonDisableds[0].id); 20 | } 21 | } 22 | 23 | Classlist(listbox).add('dqpl-listbox-show'); 24 | listboxButton.setAttribute('aria-expanded', 'true'); 25 | listbox.focus(); 26 | listbox.setAttribute('data-cached-selected', listbox.getAttribute('aria-activedescendant')); 27 | activate(listbox); 28 | }; 29 | -------------------------------------------------------------------------------- /test/components/checkboxes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | const checkboxEntry = require('../../../lib/components/checkboxes/'); 6 | 7 | describe('components/checkboxes/index', () => { 8 | it('should be a function', () => { 9 | assert.equal('function', typeof checkboxEntry); 10 | }); 11 | 12 | it('should call checkboxes/attributes', () => { 13 | let called = false; 14 | const cBox = proxyquire('../../../lib/components/checkboxes/', { 15 | './attributes': () => { 16 | called = true; 17 | } 18 | }); 19 | 20 | cBox(); 21 | assert.isTrue(called); 22 | }); 23 | 24 | it('should call checkboxes/events', () => { 25 | let called = false; 26 | const cBox = proxyquire('../../../lib/components/checkboxes/', { 27 | './events': () => { 28 | called = true; 29 | } 30 | }); 31 | 32 | cBox(); 33 | assert.isTrue(called); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/commons/dropdown.less: -------------------------------------------------------------------------------- 1 | @import "../variables.less"; 2 | 3 | @{prefix}dropdown { 4 | position: absolute; 5 | padding: 8px; 6 | background-color: @tile-bg; 7 | .ambient-light(); 8 | border: 1px solid @field-border; 9 | color: @text-base; 10 | display: none; 11 | list-style: none; 12 | 13 | &@{prefix}dropdown-active { 14 | display: block; 15 | } 16 | 17 | &:focus { 18 | outline: 0; 19 | border: 1px solid @text-base; 20 | } 21 | 22 | [role="menuitem"] { 23 | font-size: @text-small; 24 | font-weight: @weight-normal; 25 | padding: 2px @space-smallest; 26 | cursor: default; 27 | 28 | &:hover, &:focus { 29 | background-color: @link-light; 30 | outline: 0; 31 | } 32 | 33 | &[aria-disabled="true"] { 34 | color: @disabled; 35 | 36 | &:hover { 37 | background-color: transparent; 38 | } 39 | } 40 | } 41 | } 42 | 43 | @{prefix}top-bar { 44 | @{prefix}dropdown { 45 | top: (@top-bar-height - 8px); 46 | right: 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/commons/dialog/sizer/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const snippet = require('./fixture.html'); 5 | const Fixture = require('../../../fixture'); 6 | const sizer = require('../../../../lib/commons/dialog/sizer'); 7 | 8 | describe('commons/sizer', () => { 9 | let fixture, element; 10 | before(() => fixture = new Fixture()); 11 | 12 | beforeEach(() => { 13 | fixture.create(snippet); 14 | const el = fixture.element; 15 | element = el.querySelector('.dqpl-modal'); 16 | }); 17 | 18 | afterEach(() => fixture.destroy()); 19 | after(() => fixture.cleanUp()); 20 | 21 | it('should set modal content maxHeight', () => { 22 | sizer(element); 23 | assert.isNotNull(element.querySelector('.dqpl-content').style.maxHeight); 24 | }); 25 | 26 | it('should not set maxHeight if data-no-resize="true" is set', () => { 27 | element.setAttribute('data-no-resize', 'true'); 28 | sizer(element); 29 | const maxHeight = element.querySelector('.dqpl-content').style.maxHeight; 30 | assert.strictEqual(maxHeight, ''); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /lib/commons/get-adjacent-item/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import closest from 'closest'; 4 | import isVisible from '../is-visible'; 5 | import queryAll from '../query-all'; 6 | 7 | const isEnabled = (el) => el.getAttribute('aria-disabled') !== 'true'; 8 | 9 | /** 10 | * Finds adjacent menu item 11 | * @param {HTMLElement} target the base item 12 | * @param {String} dir direction of desired adjacent item ("next" or "prev") 13 | * @return {HTMLElement} the adjacent item 14 | */ 15 | module.exports = (target, dir) => { 16 | const isDown = dir === 'down'; 17 | const menu = closest(target, '[role="menu"]'); // TODO: Open this up to more than just menus? 18 | const items = menu && queryAll('[role="menuitem"]', menu).filter(isVisible).filter(isEnabled); 19 | 20 | if (!items || !items.length) { return; } 21 | 22 | const currentIndex = items.indexOf(target); 23 | const adjacentIndex = isDown ? currentIndex + 1 : currentIndex - 1; 24 | let item = items[adjacentIndex]; 25 | 26 | if (!item) { 27 | item = items[isDown ? 0 : items.length - 1]; 28 | } 29 | 30 | return item; 31 | }; 32 | -------------------------------------------------------------------------------- /test/composites/modals/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /test/commons/get-label/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const snippet = require('./snippet.html'); 5 | const Fixture = require('../../fixture'); 6 | const getLabel = require('../../../lib/commons/get-label/'); 7 | 8 | describe('commons/get-adjacent-item', () => { 9 | let fixture, target1, target2, label1, label2; 10 | 11 | before(() => fixture = new Fixture()); 12 | beforeEach(() => { 13 | fixture.create(snippet); 14 | target1 = document.getElementById('target-1'); 15 | target2 = document.getElementById('target-2'); 16 | 17 | label1 = document.getElementById('label-1'); 18 | label2 = document.getElementById('label-2'); 19 | }); 20 | 21 | afterEach(() => fixture.destroy()); 22 | after(() => fixture.cleanUp()); 23 | 24 | it('should handle data-label-id', () => { 25 | const label = getLabel(target1); 26 | assert.equal(label, label1); 27 | }); 28 | 29 | it('should handle finding label by parent/label selector', () => { 30 | const label = getLabel(target2, '.parentClass', '.labelClass'); 31 | assert.equal(label, label2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/commons/dialog/sizer/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /test/global/dialog/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /test/commons/dialog/aria/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /test/commons/dialog/close/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /test/commons/dialog/open/fixture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /lib/components/radio-buttons/traverse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import setSelected from './set-selected'; 4 | import closest from 'closest'; 5 | import queryAll from '../../commons/query-all'; 6 | const enabled = (r) => r.getAttribute('aria-disabled') !== 'true'; 7 | 8 | /** 9 | * Traverses to adjacent radio button 10 | * @param {HTMLElement} radio the starting point element 11 | * @param {Array} radios the radio buttons in the group 12 | * @param {String} dir "next" or "prev" - the direction to traverse 13 | */ 14 | module.exports = (radio, radios, dir) => { 15 | const group = closest(radio, '[role="radiogroup"]'); 16 | const _radios = queryAll('[role="radio"]', group); 17 | const enableds = _radios.filter(enabled); 18 | if (enableds.length <= 1) { return; } 19 | const isNext = dir === 'next'; 20 | const currentIndex = enableds.indexOf(radio); 21 | let adjacentIndex = isNext ? currentIndex + 1 : currentIndex - 1; 22 | if (!enableds[adjacentIndex]) { 23 | adjacentIndex = isNext ? 0 : enableds.length - 1; 24 | } 25 | // configure selected/unselected state and focus 26 | setSelected(_radios, enableds[adjacentIndex], true); 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /lib/commons/animation/animation.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | // slide down/up 4 | // going from display none (presumably) 5 | // to display block w/ 0 height breifly 6 | // 7 | // NOTE: CSS slide down animation requires use of max-height 8 | // rather than height and the final value (see dqpl-slidedown 9 | // below) needs to be a real value (not "auto") 10 | @{prefix}slidedown-setup { 11 | display: block; 12 | max-height: 0; 13 | overflow: hidden; 14 | .animate(max-height 400ms); 15 | } 16 | 17 | @{prefix}slidedown { 18 | max-height: 1000px; // TODO: this is dangerous 19 | overflow: auto; 20 | .animate(max-height 400ms); 21 | } 22 | 23 | 24 | // fade in/out 25 | @{prefix}fadein-setup { 26 | display: block; 27 | opacity: 0; 28 | .animate(opacity 400ms); 29 | } 30 | 31 | @{prefix}fadein-flex { 32 | .display-flex(); 33 | opacity: 0; 34 | .animate(opacity 400ms); 35 | } 36 | 37 | @{prefix}fadein { 38 | opacity: 1; 39 | .animate(opacity 400ms); 40 | } 41 | 42 | @{prefix}fadein-fast-setup { 43 | display: block; 44 | opacity: 0; 45 | .animate(opacity 250ms); 46 | } 47 | 48 | @{prefix}fadein-fast { 49 | opacity: 1; 50 | .animate(opacity 250ms); 51 | } 52 | -------------------------------------------------------------------------------- /lib/components/selects/arrow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import queryAll from '../../commons/query-all'; 4 | import activate from './activate'; 5 | 6 | /** 7 | * Handles arrow keyboard logic 8 | * @param {Object} data Object containing the following properties: 9 | * @prop {Number} key keydown keycode 10 | * @prop {HTMLElement} listbox the listbox element 11 | */ 12 | module.exports = (data) => { 13 | const isNext = data.key === 40; 14 | const activeID = data.listbox.getAttribute('aria-activedescendant'); 15 | const selectedOption = activeID && document.getElementById(activeID); 16 | 17 | if (!selectedOption) { 18 | return; 19 | } 20 | 21 | const options = queryAll('[role="option"]', data.listbox).filter((o) => { 22 | return o.getAttribute('aria-disabled') !== 'true'; 23 | }); 24 | const index = options.indexOf(selectedOption); 25 | const adjacentIndex = isNext ? index + 1 : index - 1; 26 | 27 | if (adjacentIndex !== -1 && adjacentIndex !== options.length) { 28 | const adjacentOption = options[adjacentIndex]; 29 | data.listbox.setAttribute('aria-activedescendant', adjacentOption.id); 30 | 31 | // set active class/scroll 32 | activate(data.listbox); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import ready from 'document-ready'; 4 | 5 | /** 6 | * Deque Pattern Library entry 7 | */ 8 | 9 | const debug = require('debug')('dqpl:entry'); 10 | 11 | ready(() => { 12 | debug('document ready'); 13 | 14 | // TODO: think about exposing a global equipped 15 | // with an emitter so we can do stuff like: 16 | // window.dqpl.checkboxes.on('select', (box) => console.log('selected: ', box)); 17 | 18 | /** 19 | * Components 20 | */ 21 | 22 | require('./lib/components/tabs')(); 23 | require('./lib/components/checkboxes')(); 24 | require('./lib/components/radio-buttons')(); 25 | require('./lib/components/field-help')(); 26 | require('./lib/components/option-menus')(); 27 | require('./lib/components/selects')(); 28 | require('./lib/components/first-time-point')(); 29 | 30 | /** 31 | * Composites 32 | */ 33 | 34 | require('./lib/composites/landmarks-menu')(); 35 | require('./lib/composites/modals')(); 36 | require('./lib/composites/menu')(); 37 | require('./lib/composites/alert')(); 38 | 39 | /** 40 | * Globals 41 | * Only need to call index (calls to specific globals should be added to lib/global/index.js) 42 | */ 43 | 44 | require('./lib/global')(); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/components/checkboxes/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import delegate from 'delegate'; 4 | import Classlist from 'classlist'; 5 | import closest from 'closest'; 6 | 7 | const SELECTOR = '.dqpl-checkbox:not(.dqpl-overlay-checkbox)'; 8 | const toggleSelected = (box) => { 9 | if (box.getAttribute('aria-disabled') === 'true') { return; } 10 | const wasSelected = box.getAttribute('aria-checked') === 'true'; 11 | 12 | box.setAttribute('aria-checked', wasSelected ? 'false' : 'true'); 13 | const inner = box.querySelector('.dqpl-inner-checkbox'); 14 | Classlist(inner) 15 | .remove('fa-check-square') 16 | .remove('fa-square-o') 17 | .add(wasSelected ? 'fa-square-o' : 'fa-check-square'); 18 | }; 19 | 20 | /** 21 | * Keyboard/mouse events for checkboxes 22 | */ 23 | module.exports = () => { 24 | delegate(document.body, SELECTOR, 'click', (e) => { 25 | toggleSelected(e.delegateTarget); 26 | }); 27 | 28 | delegate(document.body, SELECTOR, 'keydown', (e) => { 29 | const which = e.which; 30 | if (which === 32) { 31 | e.preventDefault(); 32 | toggleSelected(e.target); 33 | } else if (which === 13) { 34 | const form = closest(e.target, 'form'); 35 | if (form) { 36 | form.submit(); 37 | } 38 | } 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /lib/commons/is-visible/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Determine whether an element is visible 5 | * 6 | * @param {HTMLElement} el The HTMLElement 7 | * @param {Boolean} screenReader When provided, will evaluate visibility from the perspective of a screen reader 8 | * @return {Boolean} The element's visibilty status 9 | */ 10 | module.exports = isVisible; 11 | 12 | function isVisible(el, screenReader, recursed) { 13 | let style; 14 | const nodeName = el.nodeName.toUpperCase(); 15 | const parent = el.parentNode; 16 | 17 | // 9 === Node.DOCUMENT 18 | if (el.nodeType === 9) { return true; } 19 | 20 | style = window.getComputedStyle(el, null); 21 | if (style === null) { return false; } 22 | 23 | const isDisplayNone = style.getPropertyValue('display') === 'none'; 24 | const isInvisibleTag = nodeName.toUpperCase() === 'STYLE' || nodeName.toUpperCase() === 'SCRIPT'; 25 | const srHidden = screenReader && el.getAttribute('aria-hidden') === 'true'; 26 | const isInvisible = !recursed && style.getPropertyValue('visibility') === 'hidden'; 27 | 28 | if (isDisplayNone || isInvisibleTag || srHidden || isInvisible) { 29 | return false; 30 | } 31 | 32 | if (parent) { 33 | return isVisible(parent, screenReader, true); 34 | } 35 | 36 | return false; 37 | } 38 | -------------------------------------------------------------------------------- /lib/components/badges/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | .badge-styles() { 4 | font-size: @text-smallest; 5 | font-weight: @weight-normal; 6 | padding: 0 @space-smallest; 7 | margin: 0 @space-smallest; 8 | text-transform: lowercase; 9 | display: inline-block; 10 | .border-radius(15px); 11 | 12 | @{prefix}icon { 13 | font-size: 12px; 14 | color: @header-dark; 15 | display: inline-block; 16 | vertical-align: middle; 17 | margin-right: @space-half; 18 | } 19 | 20 | @{prefix}badge-text { 21 | font-size: 12px; 22 | display: inline-block; 23 | vertical-align: middle; 24 | color: @header-dark; 25 | } 26 | } 27 | 28 | @{prefix}badge-tag { 29 | .badge-styles(); 30 | background-color: @button-secondary; 31 | border: 1px solid @text-base; 32 | } 33 | 34 | @{prefix}badge-error { 35 | .badge-styles(); 36 | border: 1px solid @top-bar-accent-error; 37 | background-color: @top-bar-accent-error; 38 | } 39 | 40 | @{prefix}badge-success { 41 | .badge-styles(); 42 | border: 1px solid @top-bar-accent-success; 43 | background-color: @top-bar-accent-success; 44 | } 45 | 46 | @{prefix}badge-warning { 47 | .badge-styles(); 48 | border: 1px solid @top-bar-accent-warning; 49 | background-color: @top-bar-accent-warning; 50 | } 51 | -------------------------------------------------------------------------------- /lib/components/tabs/attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import queryAll from '../../commons/query-all'; 5 | 6 | const debug = require('debug')('dqpl:components:tabs'); 7 | 8 | module.exports = () => { 9 | const tabLists = queryAll('.dqpl-tablist'); 10 | 11 | tabLists.forEach((container) => { 12 | const tabs = queryAll('.dqpl-tab', container); 13 | // find the initially active tab (defaults to first) 14 | const activeTab = container.querySelector('.dqpl-tab-active') || tabs[0]; 15 | 16 | if (!activeTab) { 17 | return debug('unable to find active tab for tablist', container); 18 | } 19 | 20 | Classlist(activeTab).add('dqpl-tab-active'); 21 | 22 | // Set initial tabindex / aria-selected 23 | tabs.forEach((t) => { 24 | const isActive = t === activeTab; 25 | t.tabIndex = isActive ? 0 : -1; 26 | t.setAttribute('aria-selected', isActive ? 'true' : 'false'); 27 | 28 | // validate aria-controls presence 29 | if (!t.getAttribute('aria-controls')) { 30 | debug('aria-controls attribute missing on tab', t); 31 | } 32 | // validate role 33 | if (t.getAttribute('role') !== 'tab') { 34 | debug('role="tab" missing on tab', t); 35 | } 36 | }); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /test/commons/is-focusable/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const isFocusable = require('../../../lib/commons/is-focusable'); 5 | 6 | describe('commons/is-focusable', () => { 7 | it('should properly determine if the element is focusable', () => { 8 | const a = document.createElement('a'); 9 | a.href = '#'; 10 | const aNoHref = document.createElement('a'); 11 | const enabledButton = document.createElement('button'); 12 | enabledButton.type = 'button'; 13 | const disabledButton = document.createElement('button'); 14 | disabledButton.disabled = true; 15 | const enabledInput = document.createElement('input'); 16 | enabledInput.type = 'text'; 17 | const disabledInput = document.createElement('input'); 18 | disabledInput.type = 'text'; 19 | disabledInput.disabled = true; 20 | const focusableDiv = document.createElement('div'); 21 | focusableDiv.tabIndex = 0; 22 | const nonFocusableDiv = document.createElement('div'); 23 | 24 | [enabledButton, enabledInput, focusableDiv].forEach((el) => { 25 | assert.isTrue(isFocusable(el)); 26 | }); 27 | 28 | [aNoHref, disabledButton, disabledInput, nonFocusableDiv].forEach((el) => { 29 | assert.isFalse(isFocusable(el)); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/fixture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cauldronCSS = require('../dist/css/pattern-library.min.css'); 4 | 5 | module.exports = class Fixture { 6 | constructor() { 7 | this.addCauldron(); 8 | } 9 | 10 | /** 11 | * Adds cauldron assets (css) 12 | */ 13 | 14 | addCauldron() { 15 | this.style = document.createElement('style'); 16 | this.style.innerHTML = cauldronCSS; 17 | document.body.appendChild(this.style); 18 | return this; 19 | } 20 | 21 | /** 22 | * Creates a fixture element with provided markup 23 | * @param {String} markup A string of html to be added to the fixture 24 | */ 25 | 26 | create(markup) { 27 | markup = markup || ''; 28 | this.element = document.createElement('div'); 29 | this.element.id = 'fixture'; 30 | this.element.innerHTML = markup; 31 | 32 | document.body.appendChild(this.element); 33 | return this; 34 | } 35 | 36 | /** 37 | * Removes the fixture from the DOM 38 | */ 39 | 40 | destroy() { 41 | if (this.element) { 42 | document.body.removeChild(this.element); 43 | } 44 | 45 | return this; 46 | } 47 | 48 | /** 49 | * Removes the assets from the DOM 50 | */ 51 | 52 | cleanUp() { 53 | document.body.removeChild(this.style); 54 | return this; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /test/components/selects/activate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const Classlist = require('classlist'); 5 | const proxyquire = require('proxyquire'); 6 | const snippet = require('./snippet.html'); 7 | const queryAll = require('../../../lib/commons/query-all'); 8 | const Fixture = require('../../fixture'); 9 | 10 | describe('components/selects/activate', () => { 11 | let fixture, list; 12 | 13 | before(() => fixture = new Fixture()); 14 | 15 | beforeEach(() => { 16 | fixture.create(snippet); 17 | list = fixture.element.querySelector('.dqpl-listbox'); 18 | }); 19 | 20 | afterEach(() => fixture.destroy()); 21 | after(() => fixture.cleanUp()); 22 | 23 | it('should activate the option', () => { 24 | const options = queryAll('[role="option"]', list); 25 | options[2].id = 'foo'; 26 | list.setAttribute('aria-activedescendant', options[2].id); 27 | proxyquire('../../../lib/components/selects/activate', { 28 | '../../commons/is-scrolled-in-view': () => false 29 | })(list, false); 30 | // ensure the active class is added 31 | assert.isTrue(Classlist(options[2]).contains('dqpl-option-active')); 32 | assert.equal(queryAll('.dqpl-option-active', list).length, 1); 33 | assert.equal(queryAll('[aria-selected="true"]', list).length, 1); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/composites/landmarks-menu/fix-existing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import delegate from 'delegate'; 4 | 5 | const debug = require('debug')('dqpl:composites:landmarks-menu'); 6 | 7 | /** 8 | * Configures an existing skip list by ensuring the target of each link 9 | * is focusable optionally removing tabindex when the target is blurred 10 | */ 11 | module.exports = (shouldRemove) => { 12 | delegate(document.body, '.dqpl-skip-link', 'click', (e) => { 13 | e.preventDefault(); 14 | const link = e.delegateTarget; 15 | // prevent doing stuff with skip links generated by `./create-landmark-menu` 16 | if (link.getAttribute('data-dqpl-created') === 'true') { 17 | return; 18 | } 19 | 20 | const href = link.getAttribute('href'); 21 | const landing = href && document.querySelector(href); 22 | 23 | if (!landing) { 24 | return debug('Unable to calculate landing for skip link: ', link); 25 | } 26 | 27 | // ensure focusability 28 | landing.tabIndex = landing.tabIndex === 0 ? 0 : -1; 29 | // focus it 30 | landing.focus(); 31 | // tabindex cleanup 32 | if (shouldRemove) { 33 | landing.addEventListener('blur', onLandingBlur); 34 | } 35 | 36 | function onLandingBlur() { 37 | landing.removeAttribute('tabindex'); 38 | landing.removeEventListener('blur', onLandingBlur); 39 | } 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/components/tabs/utils/activate-tab.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import closest from 'closest'; 5 | import queryAll from '../../../commons/query-all'; 6 | import getPanel from './get-panel'; 7 | 8 | const debug = require('debug')('dqpl:components:tabs'); 9 | 10 | module.exports = (tab, dir) => { 11 | // configure active state of tabs/panels in tablist 12 | const tabList = closest(tab, '.dqpl-tablist'); 13 | const tabs = tabList ? queryAll('.dqpl-tab', tabList) : []; 14 | 15 | if (dir) { 16 | const idx = tabs.indexOf(tab); 17 | tab = tabs[dir == 'prev' ? (idx - 1) : (idx + 1)]; 18 | 19 | if (!tab) { // circularity 20 | tab = tabs[dir === 'prev' ? tabs.length - 1 : 0]; 21 | } 22 | } 23 | 24 | debug('activating tab: ', tab); 25 | 26 | tabs.forEach((t) => { 27 | const isActive = t === tab; 28 | const panel = getPanel(t); 29 | if (!panel) { 30 | return debug('unable to find panel for tab', t); 31 | } 32 | 33 | t.setAttribute('aria-selected', isActive ? 'true' : 'false'); 34 | t.tabIndex = isActive ? 0 : -1; 35 | Classlist(t)[isActive ? 'add' : 'remove']('dqpl-tab-active'); 36 | Classlist(panel)[isActive ? 'remove' : 'add']('dqpl-hidden'); 37 | panel.setAttribute('aria-hidden', isActive ? 'false' : 'true'); 38 | 39 | if (isActive && dir) { t.focus(); } 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/components/radio-buttons/set-selected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import closest from 'closest'; 5 | import queryAll from '../../commons/query-all'; 6 | 7 | /** 8 | * Cleans up previously selected / configures newly selected radio 9 | * @param {Array} radios Array of radio buttons in group 10 | * @param {HTMLElement} newlySelected Radio button to be selected 11 | * @param {Boolean} focus If the newlySelected radio should be focused 12 | */ 13 | module.exports = (radios, newlySelected, focus) => { 14 | const group = closest(newlySelected, '[role="radiogroup"]'); 15 | const _radios = queryAll('[role="radio"]', group); 16 | _radios.forEach((radio) => { 17 | const isNewlySelected = radio === newlySelected; 18 | // set attributes / properties / classes 19 | radio.tabIndex = isNewlySelected ? 0 : -1; 20 | radio.setAttribute('aria-checked', isNewlySelected ? 'true' : 'false'); 21 | Classlist(radio).toggle('dqpl-selected'); 22 | if (isNewlySelected && focus) { newlySelected.focus(); } 23 | 24 | // icon state 25 | const inner = radio.querySelector('.dqpl-inner-radio'); 26 | if (inner) { 27 | Classlist(inner) 28 | .remove(isNewlySelected ? 'fa-circle-o' : 'fa-dot-circle-o') 29 | .add(isNewlySelected ? 'fa-dot-circle-o' : 'fa-circle-o'); 30 | } 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/components/selects/select.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import CustomEvent from 'custom-event'; 5 | 6 | /** 7 | * Cleans up previously selected / sets new selection 8 | */ 9 | module.exports = (listboxButton, listbox, noHide) => { 10 | // clear selected state 11 | const prevSelected = listbox.querySelector('.dqpl-option-selected'); 12 | if (prevSelected) { 13 | prevSelected.removeAttribute('aria-selected'); 14 | Classlist(prevSelected).remove('dqpl-option-selected'); 15 | } 16 | 17 | const active = listbox.querySelector('.dqpl-option-active'); 18 | if (active) { 19 | active.setAttribute('aria-selected', 'true'); 20 | Classlist(active).add('dqpl-option-selected'); 21 | // fire a custom change event 22 | const onChange = new CustomEvent('dqpl:select:change', { 23 | bubbles: true, 24 | detail: { 25 | value: active.innerHTML 26 | } 27 | }); 28 | listboxButton.dispatchEvent(onChange); 29 | } 30 | 31 | if (!noHide) { 32 | // hide the list 33 | Classlist(listbox).remove('dqpl-listbox-show'); 34 | listboxButton.setAttribute('aria-expanded', 'false'); 35 | listboxButton.focus(); 36 | } 37 | 38 | // set pseudoVal 39 | const pseudoVal = active && listboxButton.querySelector('.dqpl-pseudo-value'); 40 | if (pseudoVal) { pseudoVal.innerHTML = active.innerHTML; } 41 | }; 42 | -------------------------------------------------------------------------------- /test/components/selects/snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
Age group (with default value selected)
3 |
4 | 16 |
    17 |
  • 18 - 25
  • 18 |
  • 26 - 39
  • 19 |
  • 40 - 55
  • 20 |
  • 55 - 99
  • 21 |
22 |
23 |
24 |
25 |
Do you like pizza?
26 |
27 | 28 |
    29 |
  • Yes
  • 30 |
  • No
  • 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /test/components/radio-buttons/traverse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | const queryAll = require('../../../lib/commons/query-all'); 6 | const snippet = require('./snippet.html'); 7 | const Fixture = require('../../fixture'); 8 | 9 | describe('components/radio-buttons/traverse', () => { 10 | let fixture, radios; 11 | before(() => { 12 | fixture = new Fixture(); 13 | fixture.create(snippet); 14 | radios = queryAll('.dqpl-radio', fixture.element); 15 | }); 16 | after(() => { 17 | fixture.destroy().cleanUp(); 18 | }); 19 | 20 | describe('given a dir of "next"', () => { 21 | it('should call setSelected with the proper params', () => { 22 | const t = proxyquire('../../../lib/components/radio-buttons/traverse', { 23 | './set-selected': (radioButtons, enabled) => { 24 | assert.equal(enabled, radioButtons[1]); 25 | } 26 | }); 27 | 28 | t(radios[0], radios, 'next'); 29 | }); 30 | }); 31 | 32 | describe('given a dir of "prev"', () => { 33 | it('should call setSelected with the proper params', () => { 34 | const t = proxyquire('../../../lib/components/radio-buttons/traverse', { 35 | './set-selected': (radioButtons, enabled) => { 36 | assert.equal(enabled, radioButtons[0]); 37 | } 38 | }); 39 | 40 | t(radios[1], radios, 'prev'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/fix-existing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const fire = require('simulant').fire; 5 | const Fixture = require('../../fixture'); 6 | const snippet = require('./snippet-existing-menu.html'); 7 | const fix = require('../../../lib/composites/landmarks-menu/fix-existing'); 8 | 9 | describe('composites/landmarks-menu/fix-existing', () => { 10 | describe('when a skip link is clicked', () => { 11 | let fixture, link, landing; 12 | 13 | before(() => { 14 | fixture = new Fixture(); 15 | fix(true); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture.create(snippet); 20 | link = fixture.element.querySelector('.dqpl-skip-link'); 21 | landing = fixture.element.querySelector('#main'); 22 | fire(link, 'click'); 23 | }); 24 | 25 | afterEach(() => fixture.destroy()); 26 | after(() => fixture.cleanUp()); 27 | 28 | it('should add tabindex to ensure the target is focusable', () => { 29 | assert.equal(-1, landing.tabIndex); 30 | }); 31 | 32 | it('should focus the target', () => { 33 | assert.equal(document.activeElement, landing); 34 | }); 35 | 36 | it('should remove tabindex on blur if "shouldRemove" param is truthy', () => { 37 | assert.equal(landing.getAttribute('tabindex'), '-1'); 38 | fire(landing, 'blur'); 39 | assert.isFalse(!!landing.getAttribute('tabindex')); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /lib/components/field-help/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | @{prefix}tooltip { 4 | width: 145px; 5 | .ambient-light(); 6 | .box-sizing(border-box); 7 | border: 1px solid #999999; 8 | .border-radius(2px); 9 | font-size: @text-smallest; 10 | position: absolute; 11 | display: none; 12 | background: @button-secondary; 13 | padding: @space-smallest; 14 | bottom: 45px; 15 | right: -28px; 16 | z-index: @z-index-tooltip; 17 | pointer-events: none; 18 | 19 | &:after { 20 | position: absolute; 21 | bottom: -14px; 22 | right: 32px; 23 | content: ''; 24 | width: 0; 25 | height: 0; 26 | border-style: solid; 27 | border-width: 15.6px 9px 0 9px; 28 | border-color: @button-secondary transparent transparent transparent; 29 | } 30 | 31 | &:before { 32 | position: absolute; 33 | bottom: -16px; 34 | right: 32px; 35 | content: ''; 36 | width: 0; 37 | height: 0; 38 | border-style: solid; 39 | border-width: 15.6px 9px 0 9px; 40 | border-color: @header transparent transparent transparent; 41 | } 42 | 43 | &@{prefix}tip-active { 44 | display: block; 45 | } 46 | } 47 | 48 | @{prefix}definition-button-wrap { 49 | @{prefix}tooltip { 50 | bottom: 30px; 51 | right: 50%; 52 | margin-right: -72.5px; // half the width of the tooltip 53 | 54 | &:before, &:after { 55 | right: 50%; 56 | margin-right: -9px; // half the width of the triangle 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/commons/dialog/aria/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import queryAll from '../../query-all'; 4 | 5 | /** 6 | * Aria-hides everything except the 7 | * given element and direct parents of it 8 | */ 9 | exports.hide = (el) => { 10 | let parent = el.parentNode; 11 | 12 | while (parent && parent.nodeName !== 'HTML') { 13 | Array.prototype.slice.call(parent.children).forEach(childHandler); 14 | parent = parent.parentNode; 15 | } 16 | 17 | function childHandler(child) { 18 | if (child !== el && !child.contains(el)) { 19 | setHidden(child); 20 | } 21 | } 22 | 23 | /** 24 | * Sets aria-hidden="true" and sets data 25 | * attribute if it was originally hidden 26 | */ 27 | function setHidden(child) { 28 | if (child.getAttribute('aria-hidden') === 'true') { 29 | return child.setAttribute('data-already-aria-hidden', 'true'); 30 | } 31 | 32 | child.setAttribute('data-dqpl-aria-hidden', 'true'); 33 | child.setAttribute('aria-hidden', 'true'); 34 | } 35 | }; 36 | 37 | exports.show = (dialog) => { 38 | // revert all of the aria-hiddens added by us 39 | queryAll('[data-dqpl-aria-hidden="true"][aria-hidden="true"]').forEach((el) => { 40 | const within = dialog.contains(el) && dialog !== el; // contains returns true for self 41 | if (el.getAttribute('data-already-aria-hidden') !== 'true' && !within) { 42 | el.removeAttribute('aria-hidden'); 43 | el.removeAttribute('data-dqpl-aria-hidden'); 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/components/tabs/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import delegate from 'delegate'; 4 | import activateTab from './utils/activate-tab'; 5 | import getPanel from './utils/get-panel'; 6 | 7 | module.exports = () => { 8 | /** 9 | * Clicks on tabs 10 | */ 11 | 12 | delegate(document.body, '.dqpl-tablist .dqpl-tab', 'click', (e) => { 13 | activateTab(e.delegateTarget); 14 | }); 15 | 16 | /** 17 | * Keydowns on tabs 18 | */ 19 | 20 | delegate(document.body, '.dqpl-tablist .dqpl-tab', 'keydown', (e) => { 21 | const which = e.which; 22 | const tab = e.target; 23 | 24 | switch (which) { 25 | case 37: 26 | case 38: 27 | e.preventDefault(); 28 | activateTab(tab, 'prev'); 29 | break; 30 | case 39: 31 | case 40: 32 | e.preventDefault(); 33 | activateTab(tab, 'next'); 34 | break; 35 | case 34: // page down 36 | e.preventDefault(); 37 | const panel = getPanel(e.target); 38 | if (panel) { 39 | panel.tabIndex = -1; // ensure its focusable 40 | panel.focus(); 41 | } 42 | break; 43 | } 44 | }); 45 | 46 | /** 47 | * Keydowns on panel 48 | * shortcut for page up to focus panel's tab 49 | */ 50 | 51 | delegate(document.body, '.dqpl-panel', 'keydown', (e) => { 52 | if (e.which !== 33) { return; } 53 | const panel = e.target; 54 | const tab = document.querySelector(`[aria-controls="${panel.id}"]`); 55 | if (tab) { tab.focus(); } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /lib/composites/menu/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import resize from './events/resize'; 5 | import attachMainEvents from './events/main'; 6 | import getTopLevels from './utils/get-top-level-items'; 7 | 8 | module.exports = (topBar) => { 9 | const elements = { 10 | topBar: topBar, 11 | trigger: topBar.querySelector('.dqpl-menu-trigger'), 12 | menu: document.querySelector('.dqpl-side-bar'), 13 | scrim: document.getElementById('dqpl-side-bar-scrim'), 14 | topBarItems: getTopLevels(topBar.querySelector('[role="menubar"]'), true) 15 | }; 16 | const updateTopBarItems = (items) => elements.topBarItems = items; 17 | 18 | // Configure the menu based on size of window 19 | resize(elements, updateTopBarItems); 20 | 21 | // attach all of the top/side bar click and keydown events 22 | attachMainEvents(elements, updateTopBarItems); 23 | 24 | // update top bar element references on dqpl:refresh 25 | elements.topBar.addEventListener('dqpl:refresh', () => { 26 | const topLevels = getTopLevels(topBar.querySelector('[role="menubar"]'), true); 27 | updateTopBarItems(topLevels); 28 | 29 | const activeOnes = topLevels.filter((t) => t.tabIndex === 0); 30 | 31 | // default to the first item if no active item exists 32 | if (!activeOnes.length) { topLevels[0].tabIndex = 0; } 33 | }); 34 | 35 | // ensure the "dqpl-no-sidebar" class is present since there is no menu (sidebar) 36 | if (!elements.menu) { 37 | Classlist(document.body).add('dqpl-no-sidebar'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /test/components/selects/open.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const Classlist = require('classlist'); 5 | const proxyquire = require('proxyquire'); 6 | const snippet = require('./snippet.html'); 7 | const queryAll = require('../../../lib/commons/query-all'); 8 | const Fixture = require('../../fixture'); 9 | const open = require('../../../lib/components/selects/open'); 10 | 11 | describe('components/selects/open', () => { 12 | let fixture, selects, lists; 13 | 14 | before(() => fixture = new Fixture()); 15 | 16 | beforeEach(() => { 17 | fixture.create(snippet); 18 | selects = queryAll('.dqpl-listbox-button', fixture.element); 19 | lists = queryAll('.dqpl-listbox', fixture.element); 20 | }); 21 | 22 | afterEach(() => fixture.destroy()); 23 | after(() => fixture.cleanUp()); 24 | 25 | it('should default to the first option when no default selection exists', () => { 26 | const firstOpt = lists[1].querySelector('[role="option"]'); 27 | open(selects[1], lists[1]); 28 | assert.equal(firstOpt.id, lists[1].getAttribute('aria-activedescendant')); 29 | }); 30 | 31 | it('should open the list and call activate', () => { 32 | let called = false; 33 | proxyquire('../../../lib/components/selects/open', { 34 | './activate': () => called = true 35 | })(selects[0], lists[0]); 36 | 37 | assert.isTrue(called); 38 | assert.equal('true', selects[0].getAttribute('aria-expanded')); 39 | assert.isTrue(Classlist(lists[0]).contains('dqpl-listbox-show')); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/composites/menu/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomEvent = require('custom-event'); 4 | const proxyquire = require('proxyquire'); 5 | const assert = require('chai').assert; 6 | const snippet = require('./snippet.html'); 7 | const Fixture = require('../../fixture'); 8 | const queryAll = require('../../../lib/commons/query-all'); 9 | const init = require('../../../lib/composites/menu/init'); 10 | 11 | describe('composites/menu/init', () => { 12 | let fixture; 13 | 14 | before(() => fixture = new Fixture()); 15 | beforeEach(() => fixture.create(snippet)); 16 | afterEach(() => fixture.destroy()); 17 | after(() => fixture.cleanUp()); 18 | 19 | it('should call resize and main events', () => { 20 | let resized = false, mained = false; 21 | 22 | proxyquire('../../../lib/composites/menu/init', { 23 | './events/resize': () => resized = true, 24 | './events/main': () => mained = true 25 | })(fixture.element.querySelector('.dqpl-top-bar')); 26 | 27 | assert.isTrue(resized); 28 | assert.isTrue(mained); 29 | }); 30 | 31 | it('should attach dqpl:refresh listener and configure tabindex properly when fired', () => { 32 | const topBar = fixture.element.querySelector('.dqpl-top-bar'); 33 | init(topBar); 34 | // make all of the topbar items tabindex="-1" 35 | queryAll('[role="menuitem"]', topBar).forEach((m) => m.tabIndex = -1); 36 | const e = new CustomEvent('dqpl:refresh'); 37 | topBar.dispatchEvent(e); 38 | assert.isTrue(!!topBar.querySelector('[tabindex="0"]')); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /lib/composites/alert/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | @{prefix}alert { 4 | display: none; 5 | width: 100%; 6 | .box-sizing(border-box); 7 | 8 | &@{prefix}dialog-show { 9 | display: block; 10 | } 11 | 12 | @{prefix}dialog-inner { 13 | width: 399px; 14 | padding: 24px; 15 | left: 50%; 16 | top: 100px; 17 | position: fixed; 18 | text-align: center; 19 | z-index: @z-index-modal; 20 | .transform(translate(-50%, 0)); 21 | .border-radius(3px); 22 | background: @tile-bg; 23 | .modal-drop(); 24 | 25 | @{prefix}content { 26 | border-left: 3px solid transparent; 27 | 28 | p { 29 | margin: 0; 30 | } 31 | } 32 | 33 | &:focus { 34 | outline: none; 35 | 36 | @{prefix}content { 37 | border-left: 3px solid @text-base; 38 | } 39 | } 40 | 41 | @{prefix}buttons { 42 | margin-top: 24px; 43 | .display-flex(); 44 | .flex-direction(); 45 | .flex-wrap(wrap); 46 | .justify-content(); 47 | .align-items(center); 48 | 49 | button { 50 | margin: 0 16px 0 0; 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Loader Animation 57 | */ 58 | @{prefix}loader { 59 | display: none; 60 | .loader(#fff, @text-base); 61 | } 62 | 63 | /* 64 | * Alert Scrim 65 | */ 66 | @{prefix}screen { 67 | top: 0; 68 | left: 0; 69 | right: 0; 70 | bottom: 0; 71 | background: @scrim; 72 | position: fixed; 73 | z-index: @z-index-modal-scrim; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/composites/landmarks-menu/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import closest from 'closest'; 4 | import Classlist from 'classlist'; 5 | import fixExisting from './fix-existing'; 6 | import createLandmarkMenu from './create-landmark-menu'; 7 | 8 | module.exports = () => { 9 | const skipContainer = document.querySelector('.dqpl-skip-container'); 10 | if (!skipContainer) { return; } 11 | const shouldRemove = skipContainer.getAttribute('data-remove-tabindex-on-blur') === 'true'; 12 | 13 | // focus management 14 | skipContainer.addEventListener('focusin', (e) => { 15 | const target = e.target; 16 | const list = Classlist(skipContainer); 17 | 18 | if (closest(target, 'ul')) { 19 | list.add('dqpl-child-focused'); 20 | } 21 | 22 | list.add('dqpl-skip-container-active'); 23 | setTimeout(() => list.add('dqpl-skip-fade')); 24 | }); 25 | 26 | skipContainer.addEventListener('focusout', (e) => { 27 | const target = e.target; 28 | const list = Classlist(skipContainer); 29 | 30 | setTimeout(() => { 31 | const activeEl = document.activeElement; 32 | if (closest(activeEl, '.dqpl-skip-container')) { return; } 33 | 34 | if (closest(target, 'ul')) { 35 | list.remove('dqpl-child-focused'); 36 | } 37 | 38 | list.remove('dqpl-skip-container-active'); 39 | setTimeout(() => list.remove('dqpl-skip-fade')); 40 | }); 41 | }); 42 | 43 | if (skipContainer.childElementCount) { 44 | return fixExisting(shouldRemove); 45 | } 46 | 47 | createLandmarkMenu(shouldRemove, skipContainer); 48 | }; 49 | -------------------------------------------------------------------------------- /test/commons/get-adjacent-item/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const snippet = require('./snippet.html'); 5 | const Fixture = require('../../fixture'); 6 | const queryAll = require('../../../lib/commons/query-all'); 7 | const getAdjacentItem = require('../../../lib/commons/get-adjacent-item/'); 8 | 9 | describe('commons/get-adjacent-item', () => { 10 | let fixture, menu, items; 11 | 12 | before(() => fixture = new Fixture()); 13 | beforeEach(() => { 14 | fixture.create(snippet); 15 | menu = fixture.element.querySelector('[role="menu"]'); 16 | items = queryAll('[role="menuitem"]', menu); 17 | }); 18 | 19 | afterEach(() => fixture.destroy()); 20 | after(() => fixture.cleanUp()); 21 | 22 | it('should return undefined if it cant find items', () => { 23 | // passing in the menu (rather than a menuitem) 24 | // so it wont be able to find any menu items 25 | const adjacentItem = getAdjacentItem(menu); 26 | assert.isUndefined(adjacentItem); 27 | }); 28 | 29 | it('should handle a direction of "down" properly', () => { 30 | const adjacentItem = getAdjacentItem(items[0], 'down'); 31 | assert.equal(adjacentItem, items[1]); 32 | }); 33 | 34 | it('should handle a direction of "up" properly', () => { 35 | const adjacentItem = getAdjacentItem(items[1], 'up'); 36 | assert.equal(adjacentItem, items[0]); 37 | }); 38 | 39 | it('should be circular', () => { 40 | const adjacentItem = getAdjacentItem(items[0], 'up'); 41 | assert.equal(adjacentItem, items[items.length -1]); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/commons/query-all/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const queryAll = require('../../../lib/commons/query-all'); 5 | 6 | describe('commons/query-all', () => { 7 | it('should return an array (NOT a NodeList)', () => { 8 | const things = queryAll('.asdf'); 9 | assert.isTrue(Array.isArray(things)); 10 | assert.isTrue(things instanceof Array); 11 | assert.isFalse(things instanceof NodeList); 12 | }); 13 | 14 | describe('context', () => { 15 | it('should default to the document', () => { 16 | const foo = document.createElement('div'); 17 | foo.className = 'foo'; 18 | const otherFoo = document.createElement('div'); 19 | otherFoo.className = 'foo'; 20 | 21 | document.body.appendChild(foo); 22 | document.body.appendChild(otherFoo); 23 | 24 | const foos = queryAll('.foo'); 25 | assert.equal(foos.length, 2); 26 | }); 27 | 28 | it('should query within context passed in', () => { 29 | const foo = document.createElement('div'); 30 | foo.className = 'foo'; 31 | const otherFoo = document.createElement('div'); 32 | otherFoo.className = 'foo'; 33 | const wrapper = document.createElement('div'); 34 | wrapper.className = 'wrapper'; 35 | 36 | wrapper.appendChild(foo); 37 | document.body.appendChild(wrapper); 38 | document.body.appendChild(otherFoo); 39 | 40 | const wrapperFoos = queryAll('.foo', wrapper); 41 | assert.equal(wrapperFoos.length, 1); 42 | assert.equal(wrapperFoos[0], foo); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/components/selects/arrow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | const snippet = require('./snippet.html'); 6 | const Fixture = require('../../fixture'); 7 | 8 | describe('components/selects/arrow', () => { 9 | let fixture, listbox, called = false; 10 | const arrow = proxyquire('../../../lib/components/selects/arrow', { 11 | './activate': () => called = true 12 | }); 13 | 14 | before(() => fixture = new Fixture()); 15 | 16 | beforeEach(() => { 17 | fixture.create(snippet); 18 | listbox = fixture.element.querySelector('.dqpl-listbox'); 19 | }); 20 | 21 | afterEach(() => fixture.destroy()); 22 | after(() => fixture.cleanUp()); 23 | 24 | it('should call activate when there is an option in the provided direction', () => { 25 | called = false; 26 | listbox.setAttribute('aria-activedescendant', 'default'); 27 | arrow({ 28 | key: 40, 29 | listbox 30 | }); 31 | 32 | assert.isTrue(called); 33 | }); 34 | 35 | it('should do nothing if there is no selected option', () => { 36 | called = false; 37 | listbox.removeAttribute('aria-activedescendant'); 38 | arrow({ 39 | key: 40, 40 | listbox 41 | }); 42 | 43 | assert.isFalse(called); 44 | }); 45 | 46 | it('should do nothing if there is NOT an option in the provided direction', () => { 47 | called = false; 48 | listbox.setAttribute('aria-activedescendant', 'default'); 49 | arrow({ 50 | key: 38, 51 | listbox 52 | }); 53 | 54 | assert.isFalse(called); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /lib/components/option-menus/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | @{prefix}options-menu-wrap { 4 | position: relative; 5 | width: 34px; // same as trigger 6 | color: @text-base; 7 | 8 | @{prefix}options-menu-trigger { 9 | font-size: @text-small-medium; 10 | color: @header-dark; 11 | .border-radius(3px); 12 | background-color: @tile-bg; 13 | border: 1px solid @field-border; 14 | .border-box(); 15 | height: 34px; 16 | width: 34px; 17 | text-align: center; 18 | } 19 | 20 | @{prefix}options-menu { 21 | display: none; 22 | position: absolute; 23 | list-style-type: none; 24 | margin: 0; 25 | padding: 0; 26 | background-color: @tile-bg; 27 | top: 33px; // one less than the height of the trigger 28 | right: 0; // default: right aligned dropdowns 29 | left: auto; 30 | border: 1px solid @field-border; 31 | z-index: @z-index-listbox; 32 | min-width: 150px; 33 | max-height: 140px; 34 | overflow-y: auto; 35 | 36 | &[aria-expanded="true"] { 37 | display: block; 38 | } 39 | 40 | @{prefix}options-menuitem { 41 | font-size: @text-small; 42 | font-weight: @weight-normal; 43 | padding: 2px @space-smallest; 44 | cursor: default; 45 | 46 | &:hover, &:focus { 47 | background-color: @link-light; 48 | outline: 0; 49 | } 50 | 51 | &[aria-disabled="true"] { 52 | color: @disabled; 53 | 54 | &:hover { 55 | background-color: transparent; 56 | } 57 | } 58 | } 59 | } 60 | // left aligned ones 61 | &@{prefix}align-left { 62 | @{prefix}options-menu { 63 | left: 0; 64 | right: auto; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/components/toasts/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | @{prefix}toast { 4 | top: @top-bar-height; 5 | position: fixed; 6 | color: @top-bar-bg-active; 7 | font-size: @text-small; 8 | z-index: @z-index-toast; 9 | .align-items(center); 10 | .border-box(); 11 | .display-flex(); 12 | padding: 7.5px 15px; 13 | right: 0; 14 | left: 0; 15 | 16 | &@{prefix}toast-success { 17 | background-color: rgba(209, 255, 164, 0.95); 18 | } 19 | 20 | &@{prefix}toast-warning { 21 | background-color: rgba(255, 230, 159, 0.95); 22 | } 23 | 24 | &@{prefix}toast-error { 25 | background-color: rgba(255, 161, 161, 0.95); 26 | z-index: @z-index-toast-action-needed; 27 | } 28 | 29 | // Dismiss button 30 | @{prefix}toast-dismiss { 31 | background: transparent; 32 | border: 0; 33 | 34 | &:focus { 35 | outline: 2px solid; 36 | } 37 | } 38 | 39 | @{prefix}toast-message { 40 | margin: 0 auto; 41 | 42 | &:before { 43 | font-family: 'FontAwesome'; 44 | content: "\f0da"; 45 | margin: 0 2px; 46 | display: inline-block; 47 | color: transparent; 48 | margin-right: @space-smallest; 49 | } 50 | 51 | .fa { // space between toast's icon and toast's messsage 52 | margin-right: 3px; 53 | } 54 | 55 | @{prefix}link { 56 | color: @top-bar-bg-active; 57 | font-size: @text-small; 58 | font-weight: @weight-light; 59 | text-decoration: underline; 60 | 61 | &:focus { 62 | .focusRingInherit(); 63 | } 64 | } 65 | } 66 | 67 | &:focus { 68 | outline: 0; 69 | @{prefix}toast-message { 70 | &:before { 71 | color: @top-bar-bg-active; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/composites/menu/events/arrow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const proxyquire = require('proxyquire'); 4 | const assert = require('chai').assert; 5 | const snippet = require('../snippet.html'); 6 | const Fixture = require('../../../fixture'); 7 | const queryAll = require('../../../../lib/commons/query-all'); 8 | 9 | describe('composites/menu/events/arrow', () => { 10 | let fixture; 11 | before(() => fixture = new Fixture()); 12 | beforeEach(() => fixture.create(snippet)); 13 | afterEach(() => fixture.destroy()); 14 | after(() => fixture.cleanUp()); 15 | 16 | it('should call activate on the proper element', () => { 17 | const topBar = fixture.element.querySelector('.dqpl-top-bar'); 18 | const target = topBar.querySelector('[role="menuitem"]'); 19 | const expected = topBar.querySelector('.second-item'); 20 | 21 | proxyquire('../../../../lib/composites/menu/events/arrow', { 22 | '../utils/activate': (t, adjacent) => { 23 | assert.equal(target, t); 24 | assert.equal(expected, adjacent); 25 | } 26 | })(queryAll('[role="menuitem"]', topBar), target, 'next'); 27 | }); 28 | 29 | it('should handle circularity', () => { 30 | const topBar = fixture.element.querySelector('.dqpl-top-bar'); 31 | const target = topBar.querySelector('[role="menuitem"]'); 32 | const expected = topBar.querySelector('.dd-trig-2'); 33 | 34 | proxyquire('../../../../lib/composites/menu/events/arrow', { 35 | '../utils/activate': (t, adjacent) => { 36 | assert.equal(target, t); 37 | assert.equal(expected, adjacent); 38 | } 39 | })( 40 | Array.prototype.slice.call(topBar.querySelector('[role="menubar"]').children), 41 | target, 42 | 'prev' 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/composites/menu/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomEvent = require('custom-event'); 4 | const proxyquire = require('proxyquire'); 5 | const assert = require('chai').assert; 6 | const snippet = require('./snippet.html'); 7 | const Fixture = require('../../fixture'); 8 | 9 | describe('composites/menu', () => { 10 | let fixture; 11 | 12 | before(() => fixture = new Fixture()); 13 | beforeEach(() => fixture.create(snippet)); 14 | afterEach(() => fixture.destroy()); 15 | after(() => fixture.cleanUp()); 16 | 17 | describe('given a top-bar that is present', () => { 18 | it('should call init', () => { 19 | let called = false; 20 | proxyquire('../../../lib/composites/menu/', { 21 | './init': () => called = true 22 | })(); 23 | assert.isTrue(called); 24 | }); 25 | }); 26 | 27 | describe('given no present top-bar initially', () => { 28 | it('should attach dqpl:ready event and, when fired call init if top bar is now present', () => { 29 | let called = false; 30 | const topBar = fixture.element.querySelector('.dqpl-top-bar'); 31 | // remove the top bar so the dqpl:ready event gets attacahed 32 | fixture.element.removeChild(topBar); 33 | 34 | proxyquire('../../../lib/composites/menu/', { 35 | './init': () => called = true 36 | })(); 37 | 38 | // ensure it hasn't been called 39 | assert.isFalse(called); 40 | 41 | const newTopBar = document.createElement('div'); 42 | newTopBar.className = 'dqpl-top-bar'; 43 | fixture.element.appendChild(newTopBar); 44 | 45 | // fire dqpl:ready on the document 46 | const e = new CustomEvent('dqpl:ready'); 47 | document.dispatchEvent(e); 48 | 49 | assert.isTrue(called); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/commons/dialog/trap-focus/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import createDebug from 'debug'; 4 | import delegate from 'delegate'; 5 | import closest from 'closest'; 6 | import focusableSelector from '../../is-focusable/selector'; 7 | import isVisible from '../../is-visible'; 8 | import queryAll from '../../query-all'; 9 | 10 | const debug = createDebug('dqpl:commons:dialog:trap-focus'); 11 | 12 | /** 13 | * Force keeping focus inside given element. 14 | * 15 | */ 16 | module.exports = (selector) => { 17 | delegate(document.body, selector, 'keydown', (e) => { 18 | if (e.which !== 9) { return; } 19 | 20 | const target = e.target; 21 | const container = e.delegateTarget; 22 | const focusables = queryAll(focusableSelector, container).filter((el) => { 23 | return isVisible(el); 24 | }); 25 | 26 | debug('focusables: ', focusables); 27 | 28 | if (!focusables.length) { return; } 29 | 30 | const first = focusables[0]; 31 | const last = focusables[focusables.length - 1]; 32 | 33 | if (e.shiftKey && target === first) { // first to last 34 | e.preventDefault(); 35 | last.focus(); 36 | } else if (!e.shiftKey && target === last) { 37 | e.preventDefault(); 38 | first.focus(); 39 | } 40 | }); 41 | 42 | 43 | /** 44 | * Prevents shift + tab from leaving alert 45 | */ 46 | delegate(document.body, '.dqpl-alert .dqpl-dialog-inner, .dqpl-modal-header h2', 'keydown', (e) => { 47 | if (e.which === 9 && e.shiftKey) { 48 | e.preventDefault(); 49 | const openElement = closest(e.delegateTarget, '.dqpl-alert, .dqpl-modal'); 50 | const focusEls = queryAll(focusableSelector, openElement); 51 | 52 | if (focusEls.length) { 53 | focusEls[focusEls.length - 1].focus(); 54 | } 55 | } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /test/components/option-menus/snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sample actions 1

4 |

(with left aligned dropdown)

5 |
6 | 9 | 16 |
17 |
18 |
19 |

Sample actions 2

20 |

(with right aligned dropdown)

21 |
22 | 25 | 29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const CustomEvent = require('custom-event'); 5 | const Classlist = require('classlist'); 6 | const Fixture = require('../../fixture'); 7 | const snippet = require('./snippet-empty-menu.html'); 8 | const init = require('../../../lib/composites/landmarks-menu/init'); 9 | 10 | describe('composites/landmarks-menu/init', () => { 11 | let fixture, container; 12 | 13 | before(() => fixture = new Fixture()); 14 | 15 | beforeEach(() => { 16 | fixture.create(snippet); 17 | container = fixture.element.querySelector('.dqpl-skip-container'); 18 | init(); 19 | }); 20 | 21 | afterEach(() => fixture.destroy()); 22 | after(() => fixture.cleanUp()); 23 | 24 | describe('focusin', () => { 25 | it('should add the proper classes', (done) => { 26 | Classlist(container) 27 | .remove('dqpl-skip-container-active') 28 | .remove('dqpl-skip-fade'); 29 | const e = new CustomEvent('focusin'); 30 | container.dispatchEvent(e); 31 | setTimeout(() => { 32 | assert.isTrue(Classlist(container).contains('dqpl-skip-container-active')); 33 | assert.isTrue(Classlist(container).contains('dqpl-skip-fade')); 34 | done(); 35 | }, 100); 36 | }); 37 | }); 38 | 39 | describe('focusout', () => { 40 | it('should remove the proper classes', (done) => { 41 | Classlist(container) 42 | .add('dqpl-skip-container-active') 43 | .add('dqpl-skip-fade'); 44 | const e = new CustomEvent('focusout'); 45 | container.dispatchEvent(e); 46 | setTimeout(() => { 47 | assert.isFalse(Classlist(container).contains('dqpl-skip-container-active')); 48 | assert.isFalse(Classlist(container).contains('dqpl-skip-fade')); 49 | done(); 50 | }, 100); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/components/tabs/style.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | .dqpl-tablist { 4 | list-style-type: none; 5 | .border-box(); 6 | .display-flex(); 7 | .flex-direction(row); 8 | .align-items(center); 9 | background: @tile-bg; 10 | width: 100%; 11 | border: 1px solid @field-border; 12 | 13 | .dqpl-tab { 14 | font-size: @text-small; 15 | font-weight: @weight-light; 16 | color: @text-base; 17 | .border-box(); 18 | height: 48px; 19 | .display-flex(); 20 | .align-items(center); 21 | text-align: center; 22 | width: 150px; 23 | border-bottom: 4px solid transparent; 24 | position: relative; 25 | cursor: pointer; 26 | 27 | .dqpl-tab-content { 28 | display: block; 29 | text-align: center; 30 | width: 100%; 31 | padding: 4px; 32 | border-bottom: 2px solid transparent; 33 | } 34 | 35 | &:hover, &:focus { 36 | border-bottom: 2px solid @text-base; 37 | outline: 0; 38 | 39 | .dqpl-tab-content { 40 | margin-bottom: 2px; 41 | } 42 | } 43 | 44 | &.dqpl-tab-active { 45 | font-weight: @weight-medium; 46 | color: @header-dark; 47 | border-bottom: 4px solid @header-dark; 48 | 49 | &:hover, &:focus { 50 | border-bottom: 4px solid @header-dark; 51 | 52 | .dqpl-tab-content { 53 | margin-bottom: 0; 54 | } 55 | } 56 | 57 | &:focus { 58 | background-color: @button-secondary; 59 | outline: 0; 60 | } 61 | } 62 | } 63 | } 64 | 65 | .dqpl-panel { 66 | width: 100%; 67 | border-bottom: 1px solid @field-border; 68 | border-left: 1px solid @field-border; 69 | border-right: 1px solid @field-border; 70 | .border-box(); 71 | overflow: auto; 72 | font-size: 16px; 73 | 74 | &.dqpl-hidden { 75 | display: none; 76 | } 77 | 78 | pre { 79 | margin: 0; 80 | .box-shadow(none); 81 | border-left: none; 82 | 83 | 84 | &:before, &:after { 85 | .box-shadow(none); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/composites/alert/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const Classlist = require('classlist'); 5 | const fire = require('simulant').fire; 6 | const snippet = require('./fixture.html'); 7 | const Fixture = require('../../fixture'); 8 | const global = require('../../../lib/global'); 9 | const index = require('../../../lib/composites/alert'); 10 | 11 | describe('composites/alert', () => { 12 | let fixture, element, trigger; 13 | before(() => { 14 | fixture = new Fixture(); 15 | // NOTE: only call this once so delegated 16 | // events don't get attached multiple times 17 | global(); 18 | index(); 19 | }); 20 | 21 | beforeEach(() => { 22 | fixture.create(snippet); 23 | const el = fixture.element; 24 | element = el.querySelector('.dqpl-alert'); 25 | trigger = document.querySelector(`[data-dialog-id="${element.id}"]`); 26 | }); 27 | 28 | afterEach(() => fixture.destroy()); 29 | after(() => fixture.cleanUp()); 30 | 31 | describe('clicking a trigger', () => { 32 | it('should show the alert', () => { 33 | fire(trigger, 'click'); 34 | assert.isTrue(Classlist(element).contains('dqpl-dialog-show')); 35 | }); 36 | 37 | it('should not attempt to open the alert if it cannot be found', () => { 38 | trigger.removeAttribute('data-dialog-id'); 39 | fire(trigger, 'click'); 40 | assert.isFalse(Classlist(element).contains('dqpl-dialog-show')); 41 | }); 42 | }); 43 | 44 | describe('keydowns on modals', () => { 45 | describe('escape', () => { 46 | it('should NOT call close', () => { 47 | fire(trigger, 'click'); // Shows the alert 48 | assert.isTrue(Classlist(element).contains('dqpl-dialog-show')); 49 | fire(element, 'keydown', { which: 27 }); 50 | assert.isTrue(Classlist(element).contains('dqpl-dialog-show')); 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /lib/components/selects/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import queryAll from '../../commons/query-all'; 4 | import activate from './activate'; 5 | 6 | const TYPE_TIME = 600; 7 | let timer; 8 | let keys = []; 9 | 10 | /** 11 | * Handles searching the options / jumping to option based on characters typed 12 | */ 13 | module.exports = (which, listbox) => { 14 | const searchSelect = (matches) => { 15 | if (!matches.length) { return; } 16 | const current = listbox.querySelector('.dqpl-option-active'); 17 | const currentIndex = matches.indexOf(current); 18 | const nextIndex = currentIndex + 1; 19 | const toBeSelected = matches[nextIndex] || matches[0]; 20 | 21 | if (toBeSelected === current) { return; } 22 | listbox.setAttribute('aria-activedescendant', toBeSelected.id); 23 | activate(listbox); 24 | }; 25 | 26 | clearTimeout(timer); 27 | 28 | let key = String.fromCharCode(which); 29 | if (!key || !key.trim().length) { return; } 30 | 31 | const options = queryAll('[role="option"]', listbox).filter((o) => { 32 | return o.getAttribute('aria-disabled') !== 'true'; 33 | }); 34 | 35 | key = key.toLowerCase(); 36 | keys.push(key); 37 | 38 | // find the FIRST option that most closely matches our keys 39 | // if that first one is already selected, go to NEXT option 40 | const stringMatch = keys.join(''); 41 | // attempt an exact match 42 | const deepMatches = options.filter((o) => { 43 | return o.innerText.toLowerCase().indexOf(stringMatch) === 0; 44 | }); 45 | 46 | if (deepMatches.length) { 47 | searchSelect(deepMatches); 48 | } else { 49 | // plan b - first character match 50 | const firstChar = stringMatch[0]; 51 | searchSelect(options.filter((o) => { 52 | return o.innerText.toLowerCase().indexOf(firstChar) === 0; 53 | })); 54 | } 55 | 56 | timer = setTimeout(() => { 57 | // reset 58 | keys = []; 59 | }, TYPE_TIME); 60 | }; 61 | -------------------------------------------------------------------------------- /test/composites/menu/events/resize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Classlist = require('classlist'); 4 | const assert = require('chai').assert; 5 | const snippet = require('../snippet.html'); 6 | const Fixture = require('../../../fixture'); 7 | const resize = require('../../../../lib/composites/menu/events/resize'); 8 | const getTopLevels = require('../../../../lib/composites/menu/utils/get-top-level-items'); 9 | 10 | describe('composites/menu/events/resize', () => { 11 | let fixture, elements; 12 | 13 | before(() => { 14 | fixture = new Fixture(); 15 | fixture.create(snippet); 16 | const topBar = fixture.element.querySelector('.dqpl-top-bar'); 17 | elements = { 18 | menu: fixture.element.querySelector('.dqpl-side-bar'), 19 | topBar: topBar, 20 | topBarItems: getTopLevels(topBar.querySelector('[role="menubar"]'), true), 21 | trigger: topBar.querySelector('.dqpl-menu-trigger'), 22 | scrim: document.getElementById('dqpl-side-bar-scrim'), 23 | dropdown: topBar.querySelector('.dqpl-dropdown') 24 | }; 25 | resize(elements, () => {}); 26 | }); 27 | 28 | beforeEach(() => { 29 | // reset everything 30 | Classlist(elements.trigger).remove('dqpl-active'); 31 | Classlist(elements.menu).remove('dqpl-active').remove('dqpl-dialog-show'); 32 | elements.menu.setAttribute('aria-expanded', 'false'); 33 | Classlist(elements.scrim).remove('dqpl-scrim-show').remove('dqpl-scrim-fade-in'); 34 | Classlist(elements.dropdown).remove('dqpl-dropdown-active'); 35 | elements.dropdown.setAttribute('aria-expanded', 'false'); 36 | }); 37 | 38 | after(() => fixture.destroy().cleanUp()); 39 | 40 | // TODO: figure out a way to resize window in phantomjs on the fly 41 | it('should configure the menu state properly', () => { 42 | const isWide = window.innerWidth >= 1024; 43 | assert.equal(elements.menu.getAttribute('data-locked'), isWide ? 'true' : 'false'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/components/selects/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const search = require('../../../lib/components/selects/search'); 5 | const queryAll = require('../../../lib/commons/query-all'); 6 | const snippet = require('./snippet.html'); 7 | const Fixture = require('../../fixture'); 8 | 9 | 10 | describe('components/selects/search', () => { 11 | let fixture, lists; 12 | 13 | before(() => fixture = new Fixture()); 14 | beforeEach(() => { 15 | fixture.create(snippet); 16 | const newOpt = document.createElement('div'); 17 | newOpt.className = 'dqpl-option foo'; 18 | newOpt.setAttribute('role', 'option'); 19 | newOpt.innerHTML = '54 - cats'; 20 | fixture.element.querySelector('.dqpl-listbox').appendChild(newOpt); 21 | lists = queryAll('.dqpl-listbox', fixture.element); 22 | }); 23 | 24 | afterEach((done) => { 25 | fixture.destroy(); 26 | setTimeout(done, 600); // allow the search timer to reset... 27 | }); 28 | after(() => fixture.cleanUp()); 29 | 30 | describe('exact match', () => { 31 | it('should activate the expected option', () => { 32 | // search for "5", then for "4" 33 | search(53, lists[0]); 34 | search(52, lists[0]); 35 | assert.equal(lists[0].querySelector('.foo').id, lists[0].getAttribute('aria-activedescendant')); 36 | }); 37 | }); 38 | 39 | describe('first character match (plan b)', () => { 40 | it('should activate the expected option', () => { 41 | // search with "5" character (53) 42 | search(53, lists[0]); 43 | assert.equal(lists[0].querySelector('.last').id, lists[0].getAttribute('aria-activedescendant')); 44 | }); 45 | }); 46 | 47 | describe('no match', () => { 48 | it('should not update aria-activedescendant', () => { 49 | // search "x" (no options contain "x") 50 | search(0, lists[0]); 51 | assert.equal(lists[0].getAttribute('aria-activedescendant'), 'default'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/commons/dialog/close/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | const Classlist = require('classlist'); 6 | const snippet = require('./fixture.html'); 7 | const Fixture = require('../../../fixture'); 8 | const close = require('../../../../lib/commons/dialog/close'); 9 | 10 | 11 | describe('commons/dialog/close', () => { 12 | let fixture, element, trigger; 13 | before(() => fixture = new Fixture()); 14 | 15 | beforeEach(() => { 16 | fixture.create(snippet); 17 | const el = fixture.element; 18 | element = el.querySelector('.dqpl-modal'); 19 | trigger = document.querySelector(`[data-dialog-id="${element.id}"]`); 20 | }); 21 | 22 | afterEach(() => fixture.destroy()); 23 | after(() => fixture.cleanUp()); 24 | 25 | it('should add the proper classes', () => { 26 | // throw the show classes on the modal/body 27 | Classlist(element).add('dqpl-dialog-show'); 28 | Classlist(document.body).add('dqpl-open'); 29 | close(element); 30 | assert.isFalse(Classlist(element).contains('dqpl-dialog-show')); 31 | assert.isFalse(Classlist(document.body).contains('dqpl-open')); 32 | }); 33 | 34 | it('should call aria-show', () => { 35 | let called = false; 36 | proxyquire('../../../../lib/commons/dialog/close', { 37 | '../aria': { 38 | show: () => called = true 39 | } 40 | })(element); 41 | 42 | assert.isTrue(called); 43 | }); 44 | 45 | it('should focus the trigger', () => { 46 | close(element); 47 | assert.equal(trigger, document.activeElement); 48 | }); 49 | 50 | it('should call debug if trigger is not found', () => { 51 | let called = false; 52 | trigger.parentNode.removeChild(trigger); 53 | proxyquire('../../../../lib/commons/dialog/close', { 54 | 'debug': () => { 55 | return function () { called = true; }; 56 | } 57 | })(element); 58 | 59 | assert.isTrue(called); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/commons/dialog/open/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire'); 5 | const Classlist = require('classlist'); 6 | const snippet = require('./fixture.html'); 7 | const Fixture = require('../../../fixture'); 8 | const open = require('../../../../lib/commons/dialog/open'); 9 | 10 | describe('commons/dialog/open', () => { 11 | let fixture, element, trigger, heading; 12 | 13 | before(() => fixture = new Fixture()); 14 | 15 | beforeEach(() => { 16 | fixture.create(snippet); 17 | const el = fixture.element; 18 | element = el.querySelector('.dqpl-modal'); 19 | heading = element.querySelector('h2'); 20 | trigger = document.querySelector(`[data-dialog-id="${element.id}"]`); 21 | }); 22 | 23 | afterEach(() => fixture.destroy()); 24 | after(() => fixture.cleanUp()); 25 | 26 | it('should add the "dqpl-dialog-show" class to the modal', () => { 27 | open(trigger, element); 28 | assert.isTrue(Classlist(element).contains('dqpl-dialog-show')); 29 | }); 30 | 31 | it('should add the "dqpl-open" class to the body', () => { 32 | open(trigger, element); 33 | assert.isTrue(Classlist(document.body).contains('dqpl-open')); 34 | }); 35 | 36 | it('should create a modal scrim if one doesn\'t exist', () => { 37 | open(trigger, element); 38 | assert.isTrue(!!element.querySelector('.dqpl-screen')); 39 | }); 40 | 41 | it('should call ariaHide and sizer', () => { 42 | let ariaHideCalled = false, sizerCalled = false; 43 | proxyquire('../../../../lib/commons/dialog/open', { 44 | '../aria': { 45 | hide: () => ariaHideCalled = true 46 | }, 47 | '../sizer': () => sizerCalled = true 48 | })(trigger, element); 49 | 50 | assert.isTrue(ariaHideCalled); 51 | assert.isTrue(sizerCalled); 52 | }); 53 | 54 | describe('given a focusEl', () => { 55 | it('should focus the element', () => { 56 | open(trigger, element, heading); 57 | assert.equal(document.activeElement, heading); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /lib/components/field-help/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import closest from 'closest'; 5 | import queryAll from '../../commons/query-all'; 6 | import noClobber from '../../commons/no-clobber'; 7 | import createTooltip from './create-tooltip'; 8 | 9 | const debug = require('debug')('dqpl:components:field-help'); 10 | const cached = []; 11 | 12 | /** 13 | * Setup field help (tooltip attrs/classes/events) 14 | */ 15 | module.exports = () => { 16 | queryAll('.dqpl-help-button, .dqpl-button-definition') 17 | .forEach((button) => { 18 | // avoid setting up the same button twice 19 | if (cached.indexOf(button) > -1) { return; } 20 | cached.push(button); 21 | const tipText = button.getAttribute('data-help-text'); 22 | const tip = createTooltip(tipText); 23 | // find the wrapper 24 | const wrap = closest(button, '.dqpl-help-button-wrap, .dqpl-definition-button-wrap'); 25 | // don't continue if no wrapper found 26 | if (!wrap) { 27 | const expected = Classlist(button).contains('dqpl-help-button') ? 28 | '.dqpl-help-button-wrap' : 29 | '.dqpl-definition-button-wrap'; 30 | debug(`Unable to generate tooltip without a "${expected}" wrapper for: `, button); 31 | return; 32 | } 33 | 34 | // insert tip into DOM 35 | wrap.appendChild(tip); 36 | // associate trigger with tip via aria-describedby 37 | noClobber(button, tip); 38 | 39 | const list = Classlist(tip); 40 | const showTip = () => { 41 | list.add('dqpl-tip-active'); 42 | const updatedText = button.getAttribute('data-help-text'); 43 | tip.innerHTML = updatedText; 44 | }; 45 | const hideTip = () => list.remove('dqpl-tip-active'); 46 | // focus/blur / mouseover/mouseout events (to show/hide) 47 | button.addEventListener('focus', showTip); 48 | button.addEventListener('mouseover', showTip); 49 | button.addEventListener('blur', hideTip); 50 | button.addEventListener('mouseout', hideTip); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /test/composites/landmarks-menu/calulate-text.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const calc = require('../../../lib/composites/landmarks-menu/calculate-text'); 5 | 6 | describe('composites/landmarks-menu/calculate-text', () => { 7 | describe('given a "data-skip-to-name" attribute', () => { 8 | it('should return the proper value', () => { 9 | const expected = 'BOOGNISH'; 10 | const element = document.createElement('div'); 11 | element.setAttribute('data-skip-to-name', expected); 12 | const val = calc(element); 13 | assert.equal(val, expected); 14 | }); 15 | }); 16 | 17 | describe('given no "data-skip-to-name" attirubte and an aria-label', () => { 18 | it('should return the element\'s aria-label', () => { 19 | const expected = 'BOOGNISH'; 20 | const element = document.createElement('div'); 21 | element.setAttribute('aria-label', expected); 22 | const val = calc(element); 23 | assert.equal(val, expected); 24 | }); 25 | 26 | it('should return the element\'s aria-labelledby referenced element\'s text', () => { 27 | // for this one we actually need to append some elements to the body 28 | const label1 = document.createElement('div'); 29 | const label2 = document.createElement('div'); 30 | label1.innerHTML = 'mighty'; 31 | label2.innerHTML = 'boognish'; 32 | label1.id = 'foo'; 33 | label2.id = 'bar'; 34 | const element = document.createElement('div'); 35 | element.setAttribute('aria-labelledby', 'foo bar'); 36 | document.body.appendChild(label1); 37 | document.body.appendChild(label2); 38 | const val = calc(element); 39 | assert.equal(val, 'mighty boognish'); 40 | }); 41 | }); 42 | 43 | describe('given no "data-skip-to-name" and no aria-label(ledby)', () => { 44 | it('should return the element\'s role', () => { 45 | const element = document.createElement('div'); 46 | element.setAttribute('role', 'boognish'); 47 | const val = calc(element); 48 | assert.equal(val, 'boognish'); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/components/checkboxes/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const simulant = require('simulant'); 5 | const attrs = require('../../../lib/components/checkboxes/attributes'); 6 | const events = require('../../../lib/components/checkboxes/events'); 7 | const snippet = require('./snippet.html'); 8 | const Fixture = require('../../fixture'); 9 | const queryAll = require('../../../lib/commons/query-all'); 10 | 11 | describe('components/checkboxes/events', () => { 12 | let fixture, checkboxes; 13 | 14 | before(() => { 15 | fixture = new Fixture(); 16 | events(); // only invoke the delegated events once... 17 | }); 18 | 19 | beforeEach(() => { 20 | fixture.create(snippet); 21 | checkboxes = queryAll('.dqpl-checkbox', fixture.element); 22 | attrs(); 23 | }); 24 | 25 | afterEach(() => fixture.destroy()); 26 | after(() => fixture.cleanUp()); 27 | 28 | it('should be a function', () => { 29 | assert.equal(typeof events, 'function'); 30 | }); 31 | 32 | it('should toggle the selected state of a checkbox when clicked', () => { 33 | const box = checkboxes[0]; // the first checkbox is initially selected (see snippet.html) 34 | assert.equal(box.getAttribute('aria-checked'), 'true'); 35 | simulant.fire(box, 'click'); 36 | assert.equal(box.getAttribute('aria-checked'), 'false'); 37 | }); 38 | 39 | it('should toggle the selected state of a checkbox when space is pressed', () => { 40 | const box = checkboxes[0]; // the first checkbox is initially selected (see snippet.html) 41 | assert.equal(box.getAttribute('aria-checked'), 'true'); 42 | simulant.fire(box, 'keydown', { which: 32 }); 43 | assert.equal(box.getAttribute('aria-checked'), 'false'); 44 | }); 45 | 46 | it('should submit the form on enter press if the checkbox is in a form', () => { 47 | const form = document.getElementById('foo'); 48 | let submitted = false; 49 | 50 | form.submit = () => submitted = true; 51 | 52 | const box = checkboxes[0]; 53 | simulant.fire(box, 'keydown', { which: 13 }); 54 | assert.isTrue(submitted); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /lib/components/selects/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import noClobber from '../../commons/no-clobber'; 5 | import closest from 'closest'; 6 | import rndid from '../../commons/rndid'; 7 | import queryAll from '../../commons/query-all'; 8 | import validate from './validate'; 9 | import attachEvents from './events'; 10 | 11 | const boxCache = []; 12 | const debug = require('debug')('dqpl:components:selects'); 13 | 14 | module.exports = () => { 15 | queryAll('[aria-haspopup="listbox"]') 16 | .forEach((listboxButton) => { 17 | if (boxCache.indexOf(listboxButton) > -1) { return; } 18 | boxCache.push(listboxButton); 19 | 20 | const wrapper = closest(listboxButton, '.dqpl-select'); 21 | const listbox = wrapper.querySelector('[role="listbox"]'); 22 | 23 | if (!listbox) { 24 | return debug('Unable to find listbox using aria-owns attribute for: ', listboxButton); 25 | } 26 | 27 | listbox.tabIndex = -1; 28 | 29 | // ensure pseudo value element is present 30 | let pseudoVal = listboxButton.querySelector('.dqpl-pseudo-value'); 31 | if (!pseudoVal) { 32 | pseudoVal = document.createElement('div'); 33 | Classlist(pseudoVal).add('dqpl-pseudo-value'); 34 | listboxButton.appendChild(pseudoVal); 35 | } 36 | 37 | noClobber(listboxButton, pseudoVal, 'aria-labelledby'); 38 | 39 | // ensure all options have an id 40 | queryAll('[role="option"]', listbox).forEach((o) => o.id = o.id || rndid()); 41 | 42 | // check if there is a default selected and ensure it has the right attrs/classes 43 | const activeId = listbox.getAttribute('aria-activedescendant'); 44 | const active = activeId && document.getElementById(activeId); 45 | 46 | if (active) { 47 | active.setAttribute('aria-selected', 'true'); 48 | Classlist(active).add('dqpl-option-selected'); 49 | Classlist(active).add('dqpl-option-active'); 50 | } 51 | 52 | // attach native click-label-focus-control behavior 53 | validate(listboxButton, listbox); 54 | attachEvents(listboxButton, listbox); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /test/components/radio-buttons/set-selected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const Classlist = require('classlist'); 5 | const queryAll = require('../../../lib/commons/query-all'); 6 | const Fixture = require('../../fixture'); 7 | const snippet = require('./snippet.html'); 8 | const setSelected = require('../../../lib/components/radio-buttons/set-selected'); 9 | 10 | describe('components/radio-buttons/set-selected', () => { 11 | let fixture, radios; 12 | 13 | before(() => fixture = new Fixture()); 14 | 15 | beforeEach(() => { 16 | fixture.create(snippet); 17 | radios = queryAll('.dqpl-radio', fixture.element); 18 | }); 19 | 20 | afterEach(() => fixture.destroy()); 21 | after(() => fixture.cleanUp()); 22 | 23 | it('should properly configure selected state', () => { 24 | // do what setup does 25 | radios.forEach((r) => { 26 | const inner = document.createElement('div'); 27 | Classlist(inner).add('dqpl-inner-radio'); 28 | r.appendChild(inner); 29 | }); 30 | 31 | setSelected(radios, radios[1], true); 32 | radios.forEach((r, i) => { 33 | const isChecked = i === 1; 34 | const inner = r.querySelector('.dqpl-inner-radio'); 35 | 36 | assert.equal(r.getAttribute('aria-checked'), isChecked ? 'true' : 'false'); 37 | 38 | if (isChecked) { 39 | assert.isTrue(Classlist(r).contains('dqpl-selected')); 40 | } 41 | 42 | assert.isTrue( 43 | Classlist(inner).contains(isChecked ? 'fa-dot-circle-o' : 'fa-circle-o') 44 | ); 45 | }); 46 | 47 | assert.equal(radios[1], document.activeElement); 48 | }); 49 | 50 | it('should compare a live collection of element references', () => { 51 | const radioWraps = queryAll('.dqpl-radio-wrap', fixture.element); 52 | const radioWrap = radioWraps[2]; 53 | const cloneWrap = radioWrap.cloneNode(true); 54 | radioWrap.parentElement.replaceChild(cloneWrap, radioWrap); 55 | const clone = cloneWrap.querySelector('[role="radio"]'); 56 | assert.notEqual(clone, document.activeElement); 57 | setSelected(null, clone, true); 58 | assert.equal(clone, document.activeElement); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/commons/dialog/trap-focus/fixture.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |
8 |
Are you sure you want to delete that?
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 34 | 51 | 52 | -------------------------------------------------------------------------------- /lib/global/dialog/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import delegate from 'delegate'; 4 | import open from '../../commons/dialog/open'; 5 | import close from '../../commons/dialog/close'; 6 | import closest from 'closest'; 7 | import sizer from '../../commons/dialog/sizer'; 8 | import selector from '../../commons/dialog/selector'; 9 | import { show, hide } from '../../commons/dialog/aria'; 10 | 11 | const debug = require('debug')('dqpl:composites:alert'); 12 | 13 | module.exports = () => { 14 | /** 15 | * Handle clicks on triggers 16 | */ 17 | 18 | delegate(document.body, '[data-dialog-id]', 'click', (e) => { 19 | const trigger = e.delegateTarget; 20 | const elID = trigger.getAttribute('data-dialog-id'); 21 | const el = document.getElementById(elID); 22 | const container = el.querySelector('.dqpl-dialog-inner'); 23 | const h2 = el.querySelector('.dqpl-modal-header h2'); 24 | 25 | if (!alert) { 26 | return debug('No alert found with id: ', elID); 27 | } 28 | 29 | const focusEl = h2 || container; 30 | open(trigger, el, focusEl); 31 | window.addEventListener('resize', onWindowResize); 32 | }); 33 | 34 | /** 35 | * Handle clicks on cancel/close buttons 36 | */ 37 | 38 | delegate(document.body, '.dqpl-close, .dqpl-cancel', 'click', (e) => { 39 | const button = e.delegateTarget; 40 | const modal = closest(button, '.dqpl-modal, .dqpl-alert'); 41 | close(modal); 42 | window.removeEventListener('resize', onWindowResize); 43 | }); 44 | 45 | function onWindowResize() { 46 | sizer(document.querySelector('.dqpl-dialog-show')); 47 | } 48 | 49 | /** 50 | * Open up custom events to call aria hide/show on a dialog 51 | * 52 | * example call: 53 | * ``` 54 | * const e = new CustomEvent('dqpl:dialog:aria-hide', { 55 | * bubbles: true, // IMPORTANT 56 | * cancelable: false // IMPORTANT 57 | * }); 58 | * 59 | * dialog.dispatchEvent(e); 60 | * ``` 61 | */ 62 | 63 | delegate(document.body, `${selector}`, 'dqpl:dialog:aria-hide', (e) => { 64 | hide(e.delegateTarget); 65 | }); 66 | 67 | delegate(document.body, `${selector}`, 'dqpl:dialog:aria-show', (e) => { 68 | show(e.delegateTarget); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/composites/tiles/tiles.less: -------------------------------------------------------------------------------- 1 | @import "../../variables.less"; 2 | 3 | @{prefix}tile { 4 | .tile-drop(); 5 | max-width: 476px; 6 | 7 | @{prefix}row { 8 | .align-items(center); 9 | } 10 | 11 | @{prefix}tile-header { 12 | color: @tile-bg; 13 | background-color: @top-bar-bg; 14 | padding: @space-three-quarters @space-three-quarters @space-three-quarters @space-large; 15 | 16 | h2, h3, h4, h5, h6 { 17 | font-size: @text-small-medium; 18 | font-weight: @weight-light; 19 | color: @tile-bg; 20 | margin: 0; 21 | } 22 | 23 | // option menus 24 | @{prefix}options-menu-wrap { 25 | margin-left: auto; 26 | 27 | @{prefix}options-menu-trigger { 28 | background: @top-bar-bg; 29 | color: @tile-bg; 30 | border: none; 31 | height: auto; 32 | } 33 | } 34 | 35 | // filters (just a flavor of the select component) 36 | @{prefix}label { 37 | font-size: @text-smaller; 38 | font-weight: @weight-light; 39 | color: @tile-bg; 40 | margin-left: 0; 41 | margin-bottom: 0; 42 | padding: 0 0 5px 0; 43 | } 44 | 45 | @{prefix}select { 46 | @{prefix}combobox { 47 | width: auto; 48 | background: transparent; 49 | font-size: @text-smaller; 50 | color: @tile-bg; 51 | border: none; 52 | margin: 0 0 0 2px; 53 | padding: 0 16px 5px 4px; 54 | height: auto; 55 | cursor: default; 56 | 57 | &:focus { 58 | .box-shadow(none); 59 | .focusRing(); 60 | } 61 | 62 | @{prefix}pseudo-value { 63 | font-size: @text-smaller; 64 | min-height: 18px; 65 | font-weight: @weight-normal; 66 | } 67 | 68 | &:after { 69 | top: -1px; 70 | right: 1px; 71 | color: @tile-bg; 72 | } 73 | } 74 | 75 | @{prefix}listbox { 76 | color: @text-base; 77 | } 78 | } 79 | } 80 | 81 | @{prefix}tile-content { 82 | ul { 83 | .border-box(); 84 | li { 85 | border-bottom: 1px solid @list-separator; 86 | } 87 | } 88 | } 89 | 90 | @{prefix}tile-footer { 91 | padding: @space-small; 92 | text-align: center; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/commons/no-clobber/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-polyfill'); 4 | const assert = require('chai').assert; 5 | const noClobber = require('../../../lib/commons/no-clobber'); 6 | const element = (tag) => document.createElement(tag || 'div'); 7 | 8 | describe('commons/no-clobber', () => { 9 | let target, ref; 10 | 11 | beforeEach(() => { 12 | target = element(); 13 | ref = element(); 14 | document.body.appendChild(target); 15 | document.body.appendChild(ref); 16 | }); 17 | 18 | afterEach(() => { 19 | target.parentNode.removeChild(target); 20 | ref.parentNode.removeChild(ref); 21 | }); 22 | 23 | describe('attr param', () => { 24 | it('should default to aria-describedby if no attr is provided', () => { 25 | noClobber(target, ref); 26 | 27 | assert.equal(target.getAttribute('aria-describedby'), ref.id); 28 | }); 29 | 30 | it('should set the provided attr', () => { 31 | noClobber(target, ref, 'aria-boognish'); 32 | assert.equal(target.getAttribute('aria-boognish'), ref.id); 33 | }); 34 | }); 35 | 36 | describe('id', () => { 37 | describe('given a ref without an id', () => { 38 | it('should assign an id to ref', () => { 39 | assert.isFalse(!!ref.id); 40 | noClobber(target, ref); 41 | assert.isTrue(!!ref.id); 42 | }); 43 | }); 44 | 45 | describe('given a ref with an id', () => { 46 | it('should not clobber the existing id', () => { 47 | const expectedID = 'foo'; 48 | ref.id = expectedID; 49 | noClobber(target, ref); 50 | assert.equal(ref.id, expectedID); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('given an existing value', () => { 56 | it('should no clobber the existing value', () => { 57 | target.setAttribute('aria-labelledby', 'cats dogs'); 58 | ref.id = 'rain'; 59 | noClobber(target, ref, 'aria-labelledby'); 60 | assert.equal(target.getAttribute('aria-labelledby'), 'cats dogs rain'); 61 | }); 62 | }); 63 | 64 | describe('given a duplicate', () => { 65 | it('should not include the duplicate token value', () => { 66 | target.setAttribute('aria-labelledby', 'cats dogs'); 67 | ref.id = 'cats'; 68 | noClobber(target, ref, 'aria-labelledby'); 69 | assert.equal(target.getAttribute('aria-labelledby'), 'cats dogs'); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /lib/composites/menu/events/resize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Classlist from 'classlist'; 4 | import getTopLevels from '../utils/get-top-level-items'; 5 | 6 | let lastSize; 7 | 8 | module.exports = (elements, update) => { 9 | const menu = elements.menu; 10 | const trigger = elements.trigger; 11 | const scrim = elements.scrim; 12 | const topBar = elements.topBar; 13 | 14 | if (!menu || !trigger) { return; } 15 | 16 | /** 17 | * The menu is locked into visibility above 1024px viewport... 18 | * - ensure aria-expanded is removed/readded properly 19 | * - ensure the topbar menu isn't thrown off (in case the hamburger was the "active" item) 20 | */ 21 | 22 | function onResize() { 23 | const width = window.innerWidth; 24 | if (width >= 1024) { 25 | if (!lastSize || lastSize === 'narrow') { 26 | lastSize = 'wide'; 27 | 28 | const expandedState = menu.getAttribute('aria-expanded'); 29 | if (expandedState) { 30 | menu.setAttribute('data-prev-expanded', expandedState); 31 | } 32 | 33 | menu.removeAttribute('aria-expanded'); 34 | if (scrim) { 35 | Classlist(scrim) 36 | .remove('dqpl-scrim-show') 37 | .remove('dqpl-scrim-fade-in'); 38 | } 39 | 40 | if (trigger.tabIndex === 0) { 41 | // since `$trigger` gets hidden (via css hook) 42 | // "activate" something else in the menubar 43 | const topBarMenuItems = getTopLevels(topBar.querySelector('[role="menubar"]'), true); 44 | update(topBarMenuItems); 45 | topBarMenuItems.forEach((item, i) => item.tabIndex = i === 0 ? 0 : -1); 46 | } 47 | menu.setAttribute('data-locked', 'true'); 48 | } 49 | } else { 50 | if (!lastSize || lastSize === 'wide') { 51 | lastSize = 'narrow'; 52 | const wasExpanded = menu.getAttribute('data-prev-expanded') === 'true'; 53 | menu.setAttribute('aria-expanded', wasExpanded ? 'true' : 'false'); 54 | update(getTopLevels(topBar.querySelector('ul'), true)); 55 | menu.setAttribute('data-locked', 'false'); 56 | 57 | if (wasExpanded === 'true' && scrim) { 58 | Classlist(scrim) 59 | .add('dqpl-scrim-show') 60 | .add('dqpl-scrim-fade-in'); 61 | } 62 | } 63 | } 64 | } 65 | 66 | onResize(); 67 | // TODO: Throttle this for better performance 68 | window.addEventListener('resize', onResize); 69 | }; 70 | -------------------------------------------------------------------------------- /test/commons/is-visible/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const isVisible = require('../../../lib/commons/is-visible'); 5 | const element = (tag, props) => { 6 | const el = document.createElement(tag || 'div'); 7 | if (props) { 8 | Object.keys(props).forEach((prop) => { 9 | el[prop] = props[prop]; 10 | }); 11 | } 12 | return el; 13 | }; 14 | 15 | describe('commons/is-visible', () => { 16 | let el; 17 | 18 | afterEach(() => { // clean up 19 | if (el && el.parentNode) { el.parentNode.removeChild(el); } 20 | }); 21 | 22 | it('should return false for `display: none;` elements', () => { 23 | el = element('div', { innerHTML: 'foo' }); 24 | el.style.display = 'none'; 25 | document.body.appendChild(el); 26 | assert.isFalse(isVisible(el)); 27 | }); 28 | 29 | it('should return false for `visibility: hidden;` elements', () => { 30 | el = element('div', { innerHTML: 'foo' }); 31 | el.style.visibility = 'hidden'; 32 | document.body.appendChild(el); 33 | assert.isFalse(isVisible(el)); 34 | }); 35 | 36 | it('should return false for