...
13 | * The target element is a role="navigation" and the data-skip-to-text is
14 | * "Skip to" so the link's text will be "Skip to navigation"
15 | *
16 | * Additional example (using label)
17 | *
26 | * which would result in a skip link's text: "Skip to Main Navigation"
27 | *
28 | * 3) The 3rd option is much different than the above... It lets you have
29 | * complete control of the link's text. You can create your own skip links
30 | * within the "dqpl-skip-container" element in which you just have to create
31 | * links with the class "dqpl-skip-link" and have the href attribute point to
32 | * the id of the target of the skip link
33 | * example:
34 | *
41 | *
42 | * I am the target of the first skip link "Skip to main content"
43 | *
44 | *
45 | * I am the target of the second skip link "Jump to side bar"
46 | *
47 | *
48 | * I am the target of the third skip link "Hop to other thing"
49 | *
50 | *
51 | *
52 | * NOTE: add `data-remove-tabindex-on-blur="true"` to the skip container
53 | * if you want tabindex to be removed from a skip target on blur (so when
54 | * you click inside of a container the focus ring doesn't show up)
55 | */
56 |
57 | module.exports = () => {
58 | document.addEventListener('dqpl:ready', init);
59 | init();
60 | };
61 |
--------------------------------------------------------------------------------
/test/global/dialog/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('chai').assert;
4 | const proxyquire = require('proxyquire');
5 | const snippet = require('./fixture.html');
6 | const fire = require('simulant').fire;
7 | const Fixture = require('../../fixture');
8 | const global = require('../../../lib/global');
9 |
10 | describe('global/dialog', () => {
11 | let fixture, element, trigger;
12 |
13 | before(() => {
14 | fixture = new Fixture();
15 | global(); // NOTE: Doing this just once because we don't want to attach multiple delegated listeners
16 | });
17 |
18 | beforeEach(() => {
19 | fixture.create(snippet);
20 | const el = fixture.element;
21 | element = el.querySelector('.dqpl-modal');
22 | trigger = document.querySelector(`[data-dialog-id="${element.id}"]`);
23 | });
24 |
25 | afterEach(() => fixture.destroy());
26 | after(() => fixture.cleanUp());
27 |
28 | describe('clicks on triggers', () => {
29 | it('should call open', () => {
30 | let openCalled = false;
31 | proxyquire('../../../lib/global/dialog', {
32 | '../../commons/dialog/open': () => openCalled = true
33 | })();
34 | fire(trigger, 'click');
35 |
36 | assert.isTrue(openCalled);
37 | });
38 | });
39 |
40 | describe('clicks on close/cancel', () => {
41 | it('should call close', () => {
42 | let closeCalled = false;
43 | const cancelBtn = document.querySelector('.dqpl-close');
44 | proxyquire('../../../lib/global/dialog', {
45 | '../../commons/dialog/close': () => closeCalled = true
46 | })();
47 |
48 | fire(trigger, 'click');
49 | fire(cancelBtn, 'click');
50 |
51 | assert.isTrue(closeCalled);
52 | });
53 | });
54 |
55 | describe('dqpl:dialog:aria-hide', () => {
56 | it('should call hide', () => {
57 | let called = false;
58 | proxyquire('../../../lib/global/dialog', {
59 | '../../commons/dialog/aria': {
60 | hide: () => called = true
61 | }
62 | })();
63 |
64 | const e = new CustomEvent('dqpl:dialog:aria-hide', {
65 | bubbles: true,
66 | cancelable: false
67 | });
68 |
69 | element.dispatchEvent(e);
70 | assert.isTrue(called);
71 | });
72 | });
73 |
74 | describe('dqpl:dialog:aria-show', () => {
75 | it('should call show', () => {
76 | let called = false;
77 | proxyquire('../../../lib/global/dialog', {
78 | '../../commons/dialog/aria': {
79 | show: () => called = true
80 | }
81 | })();
82 |
83 | const e = new CustomEvent('dqpl:dialog:aria-show', {
84 | bubbles: true,
85 | cancelable: false
86 | });
87 |
88 | element.dispatchEvent(e);
89 | assert.isTrue(called);
90 | });
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/lib/composites/modals/style.less:
--------------------------------------------------------------------------------
1 | @import "../../variables.less";
2 |
3 | @{prefix}modal {
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: 450px;
14 | left: 50%;
15 | top: 100px;
16 | position: fixed;
17 | z-index: @z-index-modal;
18 | .transform(translate(-50%, 0));
19 | .border-radius(3px);
20 | background: @tile-bg;
21 | .modal-drop();
22 |
23 | @{prefix}modal-header {
24 | background-color: @button-secondary;
25 | position: relative;
26 | .display-flex();
27 | height: 48px;
28 | border-top-left-radius: 3px;
29 | border-top-right-radius: 3px;
30 |
31 | h1,
32 | h2,
33 | h3,
34 | h4,
35 | h5,
36 | h6,
37 | @{prefix}close,
38 | @{prefix}header-item {
39 | border-bottom: 4px solid transparent;
40 |
41 | &:focus {
42 | outline: 0;
43 | border-bottom: 4px solid @text-base;
44 | }
45 | }
46 |
47 | h2 {
48 | font-size: @text-normal;
49 | color: @text-base;
50 | font-weight: @weight-normal;
51 | .align-self(center);
52 | padding: 8px 24px;
53 | border-bottom: 4px solid transparent;
54 | }
55 |
56 | @{prefix}close {
57 | background-color: transparent;
58 | color: @text-base;
59 | font-size: @text-normal;
60 | .align-self(center);
61 | margin-left: auto;
62 | padding: 8px 10px;
63 | height: 100%;
64 | }
65 | }
66 |
67 | @{prefix}content {
68 | padding: 0 @space-large 0 @space-large;
69 | margin-bottom: 60px; // the height of the footer
70 | overflow-y: auto;
71 |
72 | @{prefix}text-input,
73 | @{prefix}textarea {
74 | width: 100%;
75 | }
76 |
77 | h3 {
78 | font-weight: @weight-light;
79 | margin: @space-large 0 @space-large/2 0;
80 | padding: 0;
81 | }
82 | }
83 |
84 | @{prefix}modal-footer {
85 | position: absolute;
86 | width: 100%;
87 | bottom: 0;
88 | height: 60px;
89 | .display-flex();
90 | .align-items(center);
91 | border-top: 1px solid @field-disabled;
92 | padding: 0 @space-large;
93 | .box-sizing(border-box);
94 | background: @tile-bg;
95 |
96 | button {
97 | margin: 0 @space-small 0 0;
98 | }
99 | }
100 | }
101 |
102 | @{prefix}screen {
103 | top: 0;
104 | left: 0;
105 | right: 0;
106 | bottom: 0;
107 | background: @scrim;
108 | position: fixed;
109 | z-index: @z-index-modal-scrim;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/lib/components/checkboxes/attributes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Classlist from 'classlist';
4 | import queryAll from '../../commons/query-all';
5 | import isSelected from '../../commons/is-selected';
6 | import getLabel from '../../commons/get-label';
7 |
8 | const debug = require('debug')('dqpl:components:checkboxes');
9 | let cached = [];
10 |
11 | /**
12 | * Sets attributes/classes/disable-enable event hooks for checkboxes
13 | */
14 | module.exports = () => {
15 | const checkboxes = queryAll('.dqpl-checkbox:not(.dqpl-overlay-checkbox)');
16 |
17 | checkboxes.forEach((box) => {
18 | // only update ones we haven't already touched
19 | if (cached.indexOf(box) > -1) { return; }
20 | cached.push(box);
21 |
22 | const isSel = isSelected(box);
23 | const isDis = box.getAttribute('aria-disabled') === 'true';
24 |
25 | if (box.getAttribute('role') !== 'checkbox') {
26 | debug('role="checkbox" missing from checkbox: ', box);
27 | }
28 |
29 | const iconClass = isSel ? 'fa-check-square' : 'fa-square-o';
30 | // create the inner checkbox element for the icon
31 | const inner = document.createElement('div');
32 | Classlist(inner)
33 | .add('dqpl-inner-checkbox')
34 | .add('fa')
35 | .add(isDis ? 'fa-square' : iconClass);
36 | box.appendChild(inner);
37 |
38 | box.tabIndex = 0;
39 | box.setAttribute('aria-checked', isSel ? 'true' : 'false');
40 |
41 |
42 | const label = getLabel(
43 | box,
44 | '.dqpl-field-wrap, .dqpl-checkbox-wrap',
45 | '.dqpl-label, .dqpl-label-inline'
46 | );
47 |
48 | if (label) {
49 | if (isDis) { Classlist(label).add('dqpl-label-disabled'); }
50 | label.addEventListener('click', () => {
51 | box.click();
52 | box.focus();
53 | });
54 | }
55 |
56 | /**
57 | * Enable / disable events
58 | */
59 |
60 | box.addEventListener('dqpl:checkbox:disable', () => {
61 | debug('dqpl:checkbox:disable fired - disabling: ', box);
62 | box.setAttribute('aria-disabled', 'true');
63 |
64 | Classlist(inner)
65 | .remove('fa-check-square')
66 | .remove('fa-square-o')
67 | .remove('fa-square')
68 | .add(isSelected(box) ? 'fa-check-square' : 'fa-square');
69 |
70 | if (label) { Classlist(label).add('dqpl-label-disabled'); }
71 | });
72 |
73 | box.addEventListener('dqpl:checkbox:enable', () => {
74 | debug('dqpl:checkbox:enable fired - enabling: ', box);
75 | box.removeAttribute('aria-disabled');
76 |
77 | Classlist(inner)
78 | .remove('fa-check-square')
79 | .remove('fa-square-o')
80 | .remove('fa-square')
81 | .add(isSelected(box) ? 'fa-check-square' : 'fa-square-o');
82 |
83 | if (label) { Classlist(label).remove('dqpl-label-disabled'); }
84 | });
85 | });
86 | };
87 |
--------------------------------------------------------------------------------
/test/composites/menu/snippet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
9 |
10 | Foo
11 |
12 |
Hello
13 |
World
14 |
15 |
16 |
17 | Bar
18 |
19 |
Hello Hello
20 |
World World
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Styleguide
29 |
30 | Overview
31 |
32 | Components
33 |
38 |
39 |
40 | Composites
41 |
45 |
46 | Yo
47 |
48 |
--------------------------------------------------------------------------------
/lib/base.less:
--------------------------------------------------------------------------------
1 | @import "variables.less";
2 |
3 | body {
4 | font-family: 'Roboto', Helvetica, Arial, sans-serif;
5 | font-style: normal;
6 | font-weight: 400;
7 | color: @text-base;
8 | margin: 0;
9 | padding: 0;
10 | background-color: @workspace-bg;
11 | font-size: @text-small;
12 |
13 | &@{prefix}modal-open {
14 | overflow: hidden;
15 | }
16 |
17 | :focus {
18 | .focusRing();
19 | }
20 |
21 | h1 {
22 | margin: 0;
23 | padding: 18px 0;
24 | }
25 |
26 | ul {
27 | margin: 0;
28 | padding: 0;
29 | }
30 |
31 | button, [role="button"] {
32 | display: inline-block;
33 | zoom: 1;
34 | line-height: normal;
35 | white-space: nowrap;
36 | vertical-align: middle;
37 | text-align: center;
38 | cursor: pointer;
39 | .vendor(user-select, none);
40 | -webkit-user-drag: none;
41 | .box-sizing();
42 | border: 0;
43 | }
44 |
45 | a {
46 | color: @link;
47 | font-weight: @weight-medium;
48 | }
49 |
50 | p {
51 | line-height: 1.618;
52 | }
53 |
54 | // Font sizes
55 | @{prefix}text-largest {
56 | font-size: @text-largest;
57 | }
58 |
59 | @{prefix}text-larger {
60 | font-size: @text-larger;
61 | }
62 |
63 | @{prefix}text-large {
64 | font-size: @text-large;
65 | }
66 |
67 | @{prefix}text-large-medium {
68 | font-size: @text-large-medium;
69 | }
70 |
71 | @{prefix}text-medium {
72 | font-size: @text-medium;
73 | }
74 |
75 | @{prefix}text-normal {
76 | font-size: @text-normal;
77 | }
78 |
79 | @{prefix}text-small-medium {
80 | font-size: @text-small-medium;
81 | }
82 |
83 | @{prefix}text-small {
84 | font-size: @text-small;
85 | }
86 |
87 | @{prefix}text-smaller {
88 | font-size: @text-smaller;
89 | }
90 |
91 | @{prefix}text-smallest {
92 | font-size: @text-smallest;
93 | }
94 |
95 | // Font weights
96 | @{prefix}weight-thin {
97 | font-weight: @weight-thin;
98 | }
99 |
100 | @{prefix}weight-light {
101 | font-weight: @weight-light;
102 | }
103 |
104 | @{prefix}weight-normal {
105 | font-weight: @weight-normal;
106 | }
107 |
108 | @{prefix}weight-medium {
109 | font-weight: @weight-medium;
110 | }
111 |
112 | @{prefix}weight-bold {
113 | font-weight: @weight-bold;
114 | }
115 |
116 | @{prefix}weight-ultra-bold {
117 | font-weight: @weight-ultra-bold;
118 | }
119 |
120 | @{prefix}inline {
121 | display: inline;
122 | }
123 |
124 | @{prefix}hide {
125 | display: none;
126 | }
127 |
128 | @{prefix}pointer {
129 | cursor: pointer;
130 | }
131 |
132 | @{prefix}hidden {
133 | display: none;
134 | }
135 | }
136 |
137 | @{prefix}offscreen {
138 | .offscreen();
139 | }
140 |
141 | @{prefix}loader {
142 | .loader(@workspace-bg, @text-base);
143 | }
144 |
--------------------------------------------------------------------------------
/test/components/selects/init.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('chai').assert;
4 | const Classlist = require('classlist');
5 | const proxyquire = require('proxyquire');
6 | const queryAll = require('../../../lib/commons/query-all');
7 | const Fixture = require('../../fixture');
8 | const snippet = require('./snippet.html');
9 | const init = require('../../../lib/components/selects/init');
10 |
11 | describe('components/selects/init', () => {
12 | let fixture, selects, listboxes;
13 |
14 | before(() => fixture = new Fixture());
15 |
16 | beforeEach(() => {
17 | fixture.create(snippet);
18 | selects = queryAll('.dqpl-listbox-button', fixture.element);
19 | listboxes = queryAll('[role="listbox"]', fixture.element);
20 | });
21 |
22 | afterEach(() => fixture.destroy());
23 | after(() => fixture.cleanUp());
24 |
25 | it('should warn if a select\'s listbox cannot be found', () => {
26 | let called = false;
27 | listboxes[0].parentNode.removeChild(listboxes[0]);
28 | proxyquire('../../../lib/components/selects/init', {
29 | 'debug': () => {
30 | return function () { called = true; };
31 | }
32 | })();
33 |
34 | assert.isTrue(called);
35 | });
36 |
37 | it('should add the dqpl-pseudo-value element if its not present', () => {
38 | const theOneWithout = selects[1];
39 | init();
40 | assert.isTrue(!!theOneWithout.querySelector('.dqpl-pseudo-value'));
41 | });
42 |
43 | it('should associate the combobox with the pseudoVal via aria-labelledby', () => {
44 | const select = selects[0];
45 | const pseudoVal = select.querySelector('.dqpl-pseudo-value');
46 | init();
47 |
48 | assert.isTrue(select.getAttribute('aria-labelledby').indexOf(pseudoVal.id) > -1);
49 | });
50 |
51 | it('should assign an id to each option', () => {
52 | const select = selects[0];
53 | const listbox = document.getElementById(select.getAttribute('aria-owns'));
54 | init();
55 | queryAll('[role="option"]', listbox)
56 | .forEach((opt) => assert.isTrue(!!opt.id));
57 | });
58 |
59 | it('should call validate', () => {
60 | let called = false;
61 | proxyquire('../../../lib/components/selects/init', {
62 | './validate': () => called = true
63 | })();
64 | assert.isTrue(called);
65 | });
66 |
67 | it('should call attachEvents', () => {
68 | let called = false;
69 | proxyquire('../../../lib/components/selects/init', {
70 | './events': () => called = true
71 | })();
72 | assert.isTrue(called);
73 | });
74 |
75 | it('should set aria-selected=true and add the "dqpl-option-active" class to the intially selected option', () => {
76 | const defaultSelected = document.getElementById(listboxes[0].getAttribute('aria-activedescendant'));
77 | init();
78 |
79 | assert.isTrue(Classlist(defaultSelected).contains('dqpl-option-active'));
80 | assert.equal(defaultSelected.getAttribute('aria-selected'), 'true');
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/composites/landmarks-menu/create-landmark-menu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('chai').assert;
4 | const proxyquire = require('proxyquire');
5 | const fire = require('simulant').fire;
6 | const Fixture = require('../../fixture');
7 | const snippet = require('./snippet-empty-menu.html');
8 | const create = require('../../../lib/composites/landmarks-menu/create-landmark-menu');
9 |
10 | describe('composites/landmarks-menu/create-landmark-menu', () => {
11 | let fixture, container, main, nav, banner, target;
12 |
13 | before(() => fixture = new Fixture());
14 |
15 | beforeEach(() => {
16 | fixture.create(snippet);
17 | container = fixture.element.querySelector('.dqpl-skip-container');
18 | main = fixture.element.querySelector('[role="main"]');
19 | nav = fixture.element.querySelector('[role="navigation"]');
20 | banner = fixture.element.querySelector('[role="banner"]');
21 | target = fixture.element.querySelector('[data-skip-target="true"]');
22 | });
23 |
24 | afterEach(() => fixture.destroy());
25 | after(() => fixture.cleanUp());
26 |
27 | it('should debug if a skip target\'s text can not be calculated', () => {
28 | let called = false;
29 | proxyquire('../../../lib/composites/landmarks-menu/create-landmark-menu', {
30 | 'debug': () => {
31 | return function () { called = true; };
32 | }
33 | })(null, container);
34 |
35 | assert.isTrue(called);
36 | });
37 |
38 | it('should create/append 1 link per valid skip target', () => {
39 | create(null, container);
40 | assert.equal(container.querySelectorAll('.dqpl-skip-link').length, 3);
41 | });
42 |
43 | it('should create a ul if there are more than 1 valid skip targets', () => {
44 | create(null, container);
45 | assert.isTrue(!!container.querySelector('ul'));
46 | });
47 |
48 | it('should not create the ul if there is just 1 valid skip target', () => {
49 | nav.parentNode.removeChild(nav);
50 | banner.parentNode.removeChild(banner);
51 | target.parentNode.removeChild(target);
52 | create(null, container);
53 | assert.isFalse(!!container.querySelector('ul'));
54 | });
55 |
56 | it('should focus the skip target when a skip link is clicked', () => {
57 | nav.parentNode.removeChild(nav);
58 | banner.parentNode.removeChild(banner);
59 | target.parentNode.removeChild(target);
60 | create(null, container);
61 | const skipLink = container.querySelector('.dqpl-skip-link');
62 | fire(skipLink, 'click');
63 | assert.equal(document.activeElement, main);
64 | });
65 |
66 | it('should remove tabindex on blur if param is provided', () => {
67 | main.tabIndex = 0;
68 | nav.parentNode.removeChild(nav);
69 | banner.parentNode.removeChild(banner);
70 | target.parentNode.removeChild(target);
71 | create(true, container);
72 | const skipLink = container.querySelector('.dqpl-skip-link');
73 | fire(skipLink, 'click');
74 | fire(main, 'blur');
75 | assert.isNull(main.getAttribute('tabIndex'));
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const gulp = require('gulp');
5 | const less = require('gulp-less');
6 | const concat = require('gulp-concat');
7 | const uglify = require('gulp-uglify');
8 | const rename = require('gulp-rename');
9 | const browserify = require('browserify');
10 | const source = require('vinyl-source-stream');
11 | const cleanCSS = require('gulp-clean-css');
12 | const del = require('del');
13 | const DIST = './dist/';
14 |
15 | gulp.task('default', [
16 | 'css',
17 | 'bundle',
18 | 'variables',
19 | 'minify-css',
20 | 'minify-js',
21 | 'extras',
22 | 'individual-css'
23 | ]);
24 |
25 | // Empties out dist/
26 | gulp.task('clean', () => {
27 | return del(['dist/**/*']);
28 | });
29 |
30 | /**
31 | * Styles
32 | */
33 |
34 | gulp.task('css', () => {
35 | return gulp.src([
36 | './node_modules/prismjs/themes/prism-coy.css', // prismjs coy theme (syntax highlighting)
37 | './node_modules/flexboxgrid/dist/flexboxgrid.min.css', // flexbox grid system
38 | './lib/**/*.less'
39 | ])
40 | .pipe(less())
41 | .pipe(concat('pattern-library.css'))
42 | .pipe(gulp.dest(path.join(DIST, 'css')));
43 | });
44 |
45 | gulp.task('individual-css', () => {
46 | return gulp.src([
47 | './lib/**/*.less'
48 | ])
49 | .pipe(less())
50 | .pipe(gulp.dest(path.join(DIST, 'css')));
51 | });
52 |
53 | /**
54 | * Minify `pattern-library.css`
55 | */
56 |
57 | gulp.task('minify-css', ['css'], () => {
58 | return gulp.src(path.join(DIST, 'css', 'pattern-library.css'))
59 | .pipe(cleanCSS())
60 | .pipe(rename('pattern-library.min.css'))
61 | .pipe(gulp.dest(path.join(DIST, 'css')));
62 | });
63 |
64 |
65 | /**
66 | * Variables
67 | * (to be included in the release)
68 | */
69 |
70 | gulp.task('variables', () => {
71 | return gulp.src(['./lib/variables.less'])
72 | .pipe(gulp.dest(path.join(DIST, 'less')));
73 | });
74 |
75 | /**
76 | * Scripts bundle
77 | */
78 |
79 | gulp.task('bundle', () => {
80 | return browserify('./index.js')
81 | .transform('babelify', {
82 | presets: ['env']
83 | })
84 | .bundle()
85 | .pipe(source('pattern-library.js'))
86 | .pipe(gulp.dest(path.join(DIST, 'js')));
87 | });
88 |
89 | gulp.task('extras', ['bundle'], () => {
90 | return gulp.src([
91 | './dist/js/pattern-library.js',
92 | './node_modules/prismjs/prism.js',
93 | './node_modules/prismjs/components/prism-jade.min.js'
94 | ])
95 | .pipe(concat('pattern-library.js'))
96 | .pipe(gulp.dest(path.join(DIST, 'js')));
97 | });
98 |
99 | /**
100 | * Minify pattern-library.js
101 | */
102 |
103 | gulp.task('minify-js', ['bundle', 'extras'], () => {
104 | return gulp.src(path.join(DIST, 'js', 'pattern-library.js'))
105 | .pipe(uglify())
106 | .pipe(rename('pattern-library.min.js'))
107 | .pipe(gulp.dest(path.join(DIST, 'js')));
108 | });
109 |
110 | /**
111 | * Watcher
112 | */
113 |
114 | gulp.task('watch', () => {
115 | gulp.watch(['./lib/**/*.less'], ['css']);
116 | gulp.watch(['./lib/**/*.js', './index.js'], ['bundle', 'extras', 'minify-js']);
117 | gulp.watch(['./lib/variables.less'], ['variables']);
118 | });
119 |
--------------------------------------------------------------------------------
/test/components/selects/select.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('chai').assert;
4 | const Classlist = require('classlist');
5 | const select = require('../../../lib/components/selects/select');
6 | const queryAll = require('../../../lib/commons/query-all');
7 | const snippet = require('./snippet.html');
8 | const Fixture = require('../../fixture');
9 |
10 | describe('components/selects/select', () => {
11 | let fixture, combo, list, options;
12 |
13 | before(() => fixture = new Fixture());
14 |
15 | beforeEach(() => {
16 | fixture.create(snippet);
17 | combo = fixture.element.querySelector('.dqpl-listbox-button');
18 | list = fixture.element.querySelector('.dqpl-listbox');
19 | options = queryAll('.dqpl-option', list);
20 | });
21 |
22 | afterEach(() => fixture.destroy());
23 | after(() => fixture.cleanUp());
24 |
25 | it('should remove aria-selected from the previously selected option', () => {
26 | const prevActive = options[2];
27 | const newActive = options[1];
28 | // mock up some attrs/classes
29 | prevActive.setAttribute('aria-selected', 'true');
30 | prevActive.classList.add('dqpl-option-selected');
31 | Classlist(newActive).add('dqpl-option-active');
32 |
33 | select(combo, list);
34 |
35 | assert.isFalse(!!prevActive.getAttribute('aria-selected'));
36 | });
37 |
38 | it('should set aria-selected="true" to the option that has the "dqpl-option-active" class', () => {
39 | const prevActive = options[2];
40 | const newActive = options[1];
41 | // mock up some attrs/classes
42 | prevActive.setAttribute('aria-selected', 'true');
43 | Classlist(newActive).add('dqpl-option-active');
44 |
45 | select(combo, list);
46 |
47 | assert.equal(newActive.getAttribute('aria-selected'), 'true');
48 | });
49 |
50 | it('should hide the list given a falsey "noHide" param', () => {
51 | Classlist(options[1]).add('dqpl-option-active');
52 | Classlist(list).add('dqpl-listbox-show');
53 | combo.setAttribute('aria-expanded', 'true');
54 | select(combo, list, false);
55 | assert.equal(combo.getAttribute('aria-expanded'), 'false');
56 | assert.isFalse(Classlist(list).contains('dqpl-listbox-show'));
57 | });
58 |
59 | it('should NOT hide the list given a truthy "noHide" param', () => {
60 | Classlist(options[1]).add('dqpl-option-active');
61 | Classlist(list).add('dqpl-listbox-show');
62 | combo.setAttribute('aria-expanded', 'true');
63 | select(combo, list, true);
64 | assert.equal(combo.getAttribute('aria-expanded'), 'true');
65 | assert.isTrue(Classlist(list).contains('dqpl-listbox-show'));
66 | });
67 |
68 | it('should set the pseuoVal properly', () => {
69 | const prevActive = options[2];
70 | const newActive = options[1];
71 | // mock up some attrs/classes
72 | prevActive.setAttribute('aria-selected', 'true');
73 | Classlist(newActive).add('dqpl-option-active');
74 |
75 | select(combo, list);
76 |
77 | assert.equal(combo.querySelector('.dqpl-pseudo-value').innerText, newActive.innerText);
78 | });
79 |
80 | it('fires "dqpl:select:change" custom event', (done) => {
81 | const opt = options[1];
82 | combo.addEventListener('dqpl:select:change', ({ detail }) => {
83 | assert.equal(detail.value, opt.innerHTML);
84 | done();
85 | });
86 |
87 | Classlist(opt).add('dqpl-option-active');
88 | select(combo, list);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/lib/layout.less:
--------------------------------------------------------------------------------
1 | @import './variables.less';
2 |
3 | .contentFocus() {
4 | outline: 0;
5 | border-left-color: @top-bar-bg;
6 | }
7 |
8 | @adjusted-padding: @layout-padding - @border-width;
9 |
10 | @{prefix}layout {
11 | top: @top-bar-height;
12 | padding: @layout-padding @layout-padding 0 @layout-padding;
13 | position: fixed;
14 | overflow-x: hidden;
15 | overflow-y: scroll;
16 | -webkit-overflow-scrolling: touch;
17 | right: 0;
18 | bottom: 0;
19 | left: 0;
20 | .display-flex();
21 | .flex-direction(column);
22 |
23 | @{prefix}main-content {
24 | padding: @layout-padding @layout-padding 0 @layout-padding;
25 | border-left: @border-width solid transparent;
26 |
27 | &:focus {
28 | outline: 0;
29 | border-left-color: @top-bar-bg;
30 | }
31 |
32 | &@{prefix}collapsed {
33 | margin-left: 0;
34 | }
35 |
36 | // Content header
37 | @{prefix}content-header {
38 | .display-flex();
39 | .align-items(baseline);
40 | .flex-wrap(wrap);
41 | padding-bottom: 10px;
42 | border-bottom: 1px solid @field-disabled;
43 | margin-bottom: @space-small;
44 |
45 | h1 {
46 | margin: 0 20px 0 0;
47 | padding: 0;
48 | font-size: @text-larger;
49 | color: @top-bar-bg-active;
50 | font-weight: @weight-light;
51 | }
52 |
53 | h2, h3 {
54 | padding: 0;
55 | margin: 0;
56 | color: @text-base;
57 | font-weight: @weight-light;
58 | font-size: @text-large-medium;
59 | }
60 | }
61 |
62 | // Content body
63 | @{prefix}content-body {
64 | max-width: @content-max-width;
65 | }
66 |
67 | @{prefix}row {
68 | .display-flex();
69 | .flex-direction(row);
70 | }
71 |
72 | @{prefix}column {
73 | .display-flex();
74 | .flex-direction(column);
75 | }
76 | }
77 | }
78 |
79 | @{prefix}content-block, [data-skip-target="true"] {
80 | position: relative;
81 | &::before {
82 | content: '';
83 | position: absolute;
84 | left: -(@layout-padding + @border-width);
85 | background-color: transparent;
86 | top: 0;
87 | bottom: 0;
88 | width: @border-width;
89 | }
90 |
91 | &:focus {
92 | outline: 0;
93 | &::before {
94 | background-color: @top-bar-bg;
95 | }
96 | }
97 | }
98 |
99 | @media (min-width: 1024px) {
100 | body {
101 | @{prefix}side-bar {
102 | display: block;
103 | left: 0;
104 | .animate(left .3s);
105 |
106 | }
107 | @{prefix}top-bar {
108 | @{prefix}menu-trigger {
109 | display: none;
110 | }
111 | }
112 | }
113 |
114 | body:not(@{prefix}no-sidebar) {
115 | @{prefix}layout {
116 | left: @menu-width;
117 | }
118 |
119 | @{prefix}toast {
120 | left: @menu-width;
121 | }
122 | }
123 | }
124 |
125 | @media (max-width: 768px) {
126 | @{prefix}top-bar {
127 | > ul {
128 | > li[role='menuitem'] {
129 | &@{prefix}account-item {
130 | @{prefix}avatar {
131 | margin-right: 0 !important;
132 | }
133 |
134 | @{prefix}account-info {
135 | display: none;
136 | }
137 | }
138 | }
139 | }
140 | }
141 | }
142 |
143 | @media (max-width: 460px) {
144 | body {
145 | @{prefix}modal {
146 | @{prefix}modal-inner {
147 | width: 301px;
148 | }
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/test/commons/dialog/trap-focus/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('chai').assert;
4 | const fire = require('simulant').fire;
5 | const snippet = require('./fixture.html');
6 | const Fixture = require('../../../fixture');
7 | const global = require('../../../../lib/global');
8 | const trapFocus = require('../../../../lib/commons/dialog/trap-focus');
9 |
10 | describe('commons/dialog/trap-focus', () => {
11 | let fixture, element, trigger;
12 |
13 | before(() => {
14 | fixture = new Fixture();
15 | global();
16 | });
17 |
18 | beforeEach(() => {
19 | fixture.create(snippet);
20 | const el = fixture.element;
21 | element = el.querySelector('.dqpl-alert');
22 | trigger = document.querySelector(`[data-dialog-id="${element.id}"]`);
23 | });
24 |
25 | afterEach(() => fixture.destroy());
26 | after(() => fixture.cleanUp());
27 |
28 | describe('shift+tab', () => {
29 | it('should focus the last focusable element', () => {
30 | fire(trigger, 'click');
31 | trapFocus('.dqpl-alert');
32 |
33 | const lastFocusable = element.querySelector('.dqpl-buttons .cancel');
34 | const firstFocusable = element.querySelector('.dqpl-buttons .set');
35 |
36 | fire(firstFocusable, 'keydown', { which: 9, shiftKey: true });
37 | assert.equal(document.activeElement, lastFocusable);
38 | });
39 |
40 | it('should focus from a modal’s h2 to the last focusable', () => {
41 | const trig = fixture.element.querySelector('[data-dialog-id="demo-3"]');
42 | const h2 = document.getElementById('text-heading');
43 | const lastFocusable = document.querySelector('.dqpl-cancel');
44 | fire(trig, 'click');
45 | trapFocus('#demo-3');
46 | assert.equal(document.activeElement, h2);
47 | fire(h2, 'keydown', { which: 9, shiftKey: true });
48 | assert.equal(document.activeElement, lastFocusable);
49 | });
50 |
51 | it('should focus from an alerts content to the last focusable', () => {
52 | const content = element.querySelector('.dqpl-dialog-inner');
53 | const lastFocusable = element.querySelector('.dqpl-buttons .cancel');
54 | fire(trigger, 'click');
55 | trapFocus('.dqpl-alert');
56 | assert.equal(document.activeElement, content);
57 | fire(content, 'keydown', { which: 9, shiftKey: true });
58 | assert.equal(document.activeElement, lastFocusable);
59 | });
60 | });
61 |
62 | describe('tab', () => {
63 | it('should focus the first focusable element', () => {
64 | fire(trigger, 'click');
65 | trapFocus('.dqpl-alert');
66 |
67 | const lastFocusable = element.querySelector('.dqpl-buttons .cancel');
68 | const firstFocusable = element.querySelector('.dqpl-buttons .set');
69 |
70 | fire(lastFocusable, 'keydown', { which: 9, shiftKey: false });
71 | assert.equal(document.activeElement, firstFocusable);
72 | });
73 | });
74 |
75 | describe('focusables', () => {
76 | it('should ignore hidden focusables', () => {
77 | const trig = fixture.element.querySelector('[data-dialog-id="demo-2"]');
78 | fire(trig, 'click');
79 | trapFocus('.dqpl-modal');
80 |
81 | const first = document.getElementById('first-focusable');
82 | const last = document.getElementById('last-visible-focusable');
83 |
84 | fire(last, 'keydown', { which: 9, shiftKey: false});
85 | assert.equal(document.activeElement, first);
86 |
87 | fire(first, 'keydown', { which: 9, shiftKey: true });
88 | assert.equal(document.activeElement, last);
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/lib/components/buttons/style.less:
--------------------------------------------------------------------------------
1 | @import "../../variables.less";
2 |
3 | @{prefix}button-primary,
4 | @{prefix}button-secondary,
5 | @{prefix}button-clear,
6 | @{prefix}button-error {
7 | min-width: 110px;
8 | border-radius: 18px;
9 | border: none;
10 | padding: @space-smallest;
11 | font-size: @text-small;
12 | .border-box();
13 | padding: @space-smallest @space-medium;
14 | .ambient-light();
15 | position: relative;
16 | margin: 0 auto;
17 | text-align: center;
18 | height: 36px;
19 | position: relative;
20 | text-transform: uppercase;
21 |
22 | &:before {
23 | content: '';
24 | position: absolute;
25 | width: 30px;
26 | height: 30px;
27 | .border-radius(50%);
28 | .box-shadow(-4px 0 transparent);
29 | top: 3px;
30 | left: 8px;
31 | }
32 |
33 | &:after {
34 | content: '';
35 | position: absolute;
36 | width: 30px;
37 | height: 30px;
38 | .border-radius(50%);
39 | .box-shadow(4px 0 transparent);
40 | top: 3px;
41 | right: 8px;
42 | }
43 |
44 | &:hover {
45 | .key-light();
46 | }
47 |
48 | &:focus {
49 | outline: 0;
50 | }
51 |
52 | &:active {
53 | .active-light();
54 | }
55 | }
56 |
57 | @{prefix}button-primary {
58 | background-color: @button-primary;
59 | color: @button-text-light;
60 |
61 | &@{prefix}disabled,
62 | &[aria-disabled="true"],
63 | &[disabled] {
64 | color: @text-light-disabled;
65 | }
66 |
67 | &:focus {
68 | &:after {
69 | .box-shadow(4px 0 @text-light);
70 | }
71 |
72 | &:before {
73 | .box-shadow(-4px 0 @text-light);
74 | }
75 | }
76 | }
77 |
78 | @{prefix}button-secondary {
79 | background-color: @button-secondary;
80 | color: @button-text-dark;
81 |
82 | &@{prefix}disabled,
83 | &[aria-disabled="true"],
84 | &[disabled] {
85 | color: @disabled;
86 | }
87 |
88 | &:focus {
89 | &:after {
90 | .box-shadow(4px 0 @button-text-dark);
91 | }
92 |
93 | &:before {
94 | .box-shadow(-4px 0 @button-text-dark);
95 | }
96 | }
97 | }
98 |
99 | @{prefix}button-error {
100 | background-color: @error;
101 | .box-shadow(none);
102 | color: @button-text-light;
103 |
104 | &@{prefix}disabled,
105 | &[aria-disabled="true"],
106 | &[disabled] {
107 | color: @text-light-disabled;
108 | }
109 |
110 | &:hover {
111 | .key-light();
112 | }
113 |
114 | &:focus {
115 | outline: 0;
116 | }
117 |
118 | &:active {
119 | .active-light();
120 | }
121 |
122 | &:focus {
123 | &:after {
124 | .box-shadow(4px 0 @text-light);
125 | }
126 |
127 | &:before {
128 | .box-shadow(-4px 0 @text-light);
129 | }
130 | }
131 | }
132 |
133 | @{prefix}button-clear {
134 | background-color: transparent;
135 | .box-shadow(none);
136 |
137 | &:hover {
138 | background-color: @button-secondary;
139 | .box-shadow(none);
140 | }
141 |
142 | &:focus {
143 | background-color: @button-secondary;
144 | &:after {
145 | .box-shadow(4px 0 @button-text-dark);
146 | }
147 |
148 | &:before {
149 | .box-shadow(-4px 0 @button-text-dark);
150 | }
151 | }
152 | }
153 |
154 | @{prefix}buttons-inline {
155 | .display-flex();
156 | .flex-direction(row);
157 | margin: 0 auto;
158 |
159 | button {
160 | margin: 0 0 0 10px;
161 | }
162 | }
163 |
164 | @{prefix}definition-button-wrap {
165 | display: inline;
166 | vertical-align: baseline;
167 | position: relative;
168 | }
169 |
170 | @{prefix}button-definition {
171 | background-color: transparent;
172 | color: @text-base;
173 | font-weight: @weight-normal;
174 | border-bottom: 1px dotted;
175 | display: inline-block;
176 | margin: 0 2px;
177 | padding: 0;
178 | font-size: inherit;
179 | vertical-align: baseline;
180 | cursor: auto;
181 | .vendor(user-select, text);
182 | }
183 |
--------------------------------------------------------------------------------
/lib/components/option-menus/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import closest from 'closest';
4 | import delegate from 'delegate';
5 | import queryAll from '../../commons/query-all';
6 | import isOutside from '../../commons/is-outside';
7 | import getAdjacentItem from '../../commons/get-adjacent-item';
8 |
9 | const debug = require('debug')('dqpl:components:option-menus');
10 |
11 | module.exports = () => {
12 | /**
13 | * Clicks on the document (outside of a dropdown or
14 | * trigger) should close all expanded options menus
15 | */
16 |
17 | document.addEventListener('click', (e) => {
18 | const target = e.target;
19 | const selector = '.dqpl-options-menu, .dqpl-options-menu-trigger';
20 | if (isOutside(target, selector)) {
21 | // collapse all menus
22 | queryAll(selector).forEach((m) => m.setAttribute('aria-expanded', 'false'));
23 | }
24 | });
25 |
26 | /**
27 | * Clicks on triggers
28 | */
29 |
30 | delegate(document.body, '.dqpl-options-menu-trigger', 'click', (e) => {
31 | const trigger = e.delegateTarget;
32 | const dropdownID = trigger.getAttribute('aria-controls');
33 | const wasExpanded = trigger.getAttribute('aria-expanded') === 'true';
34 | const dropdown = document.getElementById(dropdownID);
35 | if (!dropdown) { return debug('Unable to find option menu for trigger: ', trigger); }
36 | // clean up the others
37 | queryAll('.dqpl-options-menu, .dqpl-options-menu-trigger')
38 | .filter((el) => el !== trigger && el !== dropdown)
39 | .forEach((el) => el.setAttribute('aria-expanded', 'false'));
40 | // toggle expanded
41 | trigger.setAttribute('aria-expanded', wasExpanded ? 'false' : 'true');
42 | dropdown.setAttribute('aria-expanded', wasExpanded ? 'false' : 'true');
43 |
44 | if (!wasExpanded) {
45 | // focus the first item...
46 | const firstItem = dropdown.querySelector('[role="menuitem"]');
47 | if (firstItem) { firstItem.focus(); }
48 | }
49 | });
50 |
51 | /**
52 | * Keydowns on triggers
53 | */
54 |
55 | delegate(document.body, '.dqpl-options-menu-trigger', 'keydown', (e) => {
56 | if (e.which === 40) { // down
57 | e.preventDefault();
58 | e.delegateTarget.click();
59 | }
60 | });
61 |
62 | /**
63 | * Keydowns on options menuitems
64 | */
65 |
66 | delegate(document.body, '.dqpl-options-menu [role="menuitem"]', 'keydown', (e) => {
67 | const which = e.which;
68 | const target = e.delegateTarget;
69 | const dropdown = closest(target, '[role="menu"]');
70 | const id = dropdown && dropdown.id;
71 | const trigger = id && document.querySelector(`.dqpl-options-menu-trigger[aria-controls="${id}"]`);
72 | const menuOpen = trigger && (trigger.getAttribute('aria-expanded') === 'true');
73 |
74 | if (which === 38 || which === 40) { // up or down
75 | e.preventDefault();
76 | const toFocus = getAdjacentItem(target, which === 38 ? 'up' : 'down');
77 | if (toFocus) { toFocus.focus(); }
78 | } else if (which === 27) { // escape
79 | e.preventDefault();
80 | if (trigger) {
81 | trigger.click();
82 | trigger.focus();
83 | }
84 | } else if (which === 13 || which === 32) {
85 | e.preventDefault();
86 | target.click();
87 | }
88 |
89 | if (which === 9 && menuOpen) {
90 | trigger.click();
91 | }
92 | });
93 |
94 | /**
95 | * Clicks on options menuitems
96 | *
97 | * (If theres a link in it, click it)
98 | */
99 |
100 | delegate(document.body, '.dqpl-options-menu [role="menuitem"]', 'click', (e) => {
101 | const link = e.delegateTarget.querySelector('a');
102 | if (link) { link.click(); }
103 | });
104 |
105 | /**
106 | * Clicks on links within options menuitems
107 | *
108 | * In case its for whatever reason an internal link - prevent inifinite loop
109 | * (This click would bubble up to the menuitem which would trigger a click
110 | * on this link which would bubble up and repeat itself infinitely)
111 | */
112 |
113 | delegate(document.body, '.dqpl-options-menu [role="menuitem"] a', (e) => {
114 | e.stopPropagation();
115 | });
116 | };
117 |
--------------------------------------------------------------------------------
/test/components/field-help/setup.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Classlist = require('classlist');
4 | const assert = require('chai').assert;
5 | const closest = require('closest');
6 | const simulant = require('simulant');
7 | const setup = require('../../../lib/components/field-help/setup');
8 | const queryAll = require('../../../lib/commons/query-all');
9 | const proxyquire = require('proxyquire');
10 | const Fixture = require('../../fixture');
11 | const snippet = require('./snippet.html');
12 |
13 | describe('components/field-help/setup', () => {
14 | let fixture, helps;
15 |
16 | before(() => {
17 | fixture = new Fixture();
18 | });
19 |
20 | beforeEach(() => {
21 | fixture.create(snippet);
22 | helps = queryAll('.dqpl-help-button, .dqpl-button-definition', fixture.element);
23 | });
24 |
25 | afterEach(() => fixture.destroy());
26 | after(() => fixture.cleanUp());
27 |
28 | it('should warn if button is not in a proper wrapper', () => {
29 | let called = false;
30 | const funk = () => {
31 | return function () {
32 | called = true;
33 | };
34 | };
35 | const set = proxyquire('../../../lib/components/field-help/setup', {
36 | 'debug': funk
37 | });
38 |
39 | Classlist(fixture.element.querySelector('.dqpl-help-button-wrap'))
40 | .remove('dqpl-help-button-wrap');
41 |
42 | set();
43 | assert.isTrue(called);
44 | });
45 |
46 | it('should create/append the tooltip', () => {
47 | setup();
48 | const wrapper = closest(helps[0], '.dqpl-help-button-wrap');
49 | const tip = wrapper.querySelector('.dqpl-tooltip');
50 | assert.isTrue(!!tip);
51 | });
52 |
53 | it('should associate the tip with the button via aria-describedby', () => {
54 | setup();
55 | const button = helps[0];
56 | const wrapper = closest(button, '.dqpl-help-button-wrap');
57 | const tip = wrapper.querySelector('.dqpl-tooltip');
58 | assert.equal(button.getAttribute('aria-describedby'), tip.id);
59 | });
60 |
61 | it('should add the active class on focus', () => {
62 | setup();
63 | const button = helps[0];
64 | const wrapper = closest(button, '.dqpl-help-button-wrap');
65 | const tip = wrapper.querySelector('.dqpl-tooltip');
66 | simulant.fire(button, 'focus');
67 | assert.isTrue(Classlist(tip).contains('dqpl-tip-active'));
68 | });
69 |
70 | it('should add the active class on mouseover', () => {
71 | setup();
72 | const button = helps[0];
73 | const wrapper = closest(button, '.dqpl-help-button-wrap');
74 | const tip = wrapper.querySelector('.dqpl-tooltip');
75 | simulant.fire(button, 'mouseover');
76 | assert.isTrue(Classlist(tip).contains('dqpl-tip-active'));
77 | });
78 |
79 | it('should remove the active class on blur', () => {
80 | setup();
81 | const button = helps[0];
82 | const wrapper = closest(button, '.dqpl-help-button-wrap');
83 | const tip = wrapper.querySelector('.dqpl-tooltip');
84 | simulant.fire(button, 'focus');
85 | assert.isTrue(Classlist(tip).contains('dqpl-tip-active'));
86 | simulant.fire(button, 'blur');
87 | assert.isFalse(Classlist(tip).contains('dqpl-tip-active'));
88 | });
89 |
90 | it('should remove the active class on mouseout', () => {
91 | setup();
92 | const button = helps[0];
93 | const wrapper = closest(button, '.dqpl-help-button-wrap');
94 | const tip = wrapper.querySelector('.dqpl-tooltip');
95 | simulant.fire(button, 'mouseover');
96 | assert.isTrue(Classlist(tip).contains('dqpl-tip-active'));
97 | simulant.fire(button, 'mouseout');
98 | assert.isFalse(Classlist(tip).contains('dqpl-tip-active'));
99 | });
100 |
101 | it('should update the text if the data-help-text gets updated', () => {
102 | setup();
103 | const button = helps[0];
104 | const wrapper = closest(button, '.dqpl-help-button-wrap');
105 | const tip = wrapper.querySelector('.dqpl-tooltip');
106 | const tipText = tip.textContent;
107 | assert.equal(tipText, 'Your first name is the name that comes before your middle and last names.');
108 | button.setAttribute('data-help-text', 'food bar');
109 | simulant.fire(button, 'mouseover');
110 | assert.equal(tip.textContent, 'food bar');
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/lib/components/selects/events.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import delegate from 'delegate';
4 | import Classlist from 'classlist';
5 | import getLabel from '../../commons/get-label';
6 | import open from './open';
7 | import arrow from './arrow';
8 | import select from './select';
9 | import search from './search';
10 | import activate from './activate';
11 |
12 | const isLetterOrNum = (key) => {
13 | var isLetter = key >= 65 && key <= 90;
14 | var isNumber = key >= 48 && key <= 57;
15 | return isLetter || isNumber;
16 | };
17 |
18 | module.exports = (listboxButton, listbox) => {
19 | const restoreSelected = () => {
20 | const cachedSelected = listbox.getAttribute('data-cached-selected');
21 | listbox.setAttribute('aria-activedescendant', cachedSelected);
22 | };
23 | /**
24 | * attach native label click to focus control behavior
25 | */
26 |
27 | const label = getLabel(listboxButton, '.dqpl-field-wrap', '.dqpl-label, .dqpl-label-inline');
28 | if (label) {
29 | label.addEventListener('click', () => listboxButton.focus());
30 | }
31 |
32 | /**
33 | * listboxButton events
34 | */
35 |
36 | listboxButton.addEventListener('click', () => {
37 | Classlist(listbox).toggle('dqpl-listbox-show');
38 | const hasShowClass = Classlist(listbox).contains('dqpl-listbox-show');
39 | // set expanded state
40 | listboxButton.setAttribute('aria-expanded', hasShowClass ? 'true' : 'false');
41 | if (hasShowClass) { open(listboxButton, listbox); }
42 | });
43 |
44 | listboxButton.addEventListener('keydown', (e) => {
45 | if (e.which !== 40) {
46 | return;
47 | }
48 |
49 | e.preventDefault();
50 | open(listboxButton, listbox);
51 | });
52 |
53 | listbox.addEventListener('keydown', (e) => {
54 | const which = e.which;
55 |
56 | switch (which) {
57 | case 9:
58 | // restore selected
59 | restoreSelected();
60 | break;
61 | case 38: // up
62 | case 40: // down
63 | e.preventDefault();
64 | arrow({
65 | key: which,
66 | listbox
67 | });
68 | break;
69 | case 13: // enter
70 | case 32: // space
71 | e.preventDefault();
72 | select(listboxButton, listbox);
73 | break;
74 | case 27: // escape
75 | // restore previously selected
76 | restoreSelected();
77 | Classlist(listbox).remove('dqpl-listbox-show');
78 | listboxButton.setAttribute('aria-expanded', 'false');
79 | listboxButton.focus();
80 | break;
81 | default:
82 | // TODO: letters / numbers might not cut it...should probably allow any character
83 | if (isLetterOrNum(which)) {
84 | search(which, listbox);
85 | }
86 | }
87 | });
88 |
89 | listbox.addEventListener('blur', onListboxBlur);
90 |
91 | function onListboxBlur() {
92 | Classlist(listbox).remove('dqpl-listbox-show');
93 | listboxButton.setAttribute('aria-expanded', 'false');
94 | }
95 |
96 | /**
97 | * Listbox events
98 | */
99 |
100 | delegate(listbox, '[role="option"]', 'mousedown', (e) => {
101 | const option = e.delegateTarget;
102 | // detach blur events so the list doesn't close
103 | listbox.removeEventListener('blur', onListboxBlur);
104 |
105 | if (option.getAttribute('aria-disabled') === 'true') {
106 | return setTimeout(() => {
107 | listboxButton.focus();
108 | // re-attach blur events so the list closes on blur again
109 | listbox.addEventListener('blur', onListboxBlur);
110 | });
111 | }
112 |
113 | listbox.setAttribute('aria-activedescendant', option.id);
114 |
115 | activate(listbox, true);
116 | select(listboxButton, listbox, true);
117 |
118 | document.removeEventListener('mouseup', onMouseUp);
119 | document.addEventListener('mouseup', onMouseUp);
120 | });
121 |
122 | function onMouseUp() {
123 | listboxButton.focus();
124 | Classlist(listbox).remove('dqpl-listbox-show');
125 | listboxButton.setAttribute('aria-expanded', 'false');
126 | document.removeEventListener('mouseup', onMouseUp);
127 | // re-attach blur events so the list closes on blur again
128 | listbox.addEventListener('blur', onListboxBlur);
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/lib/commons/forms.less:
--------------------------------------------------------------------------------
1 | @import "../variables.less";
2 |
3 | input,
4 | select,
5 | textarea,
6 | [role="menuitemcheckbox"],
7 | [role="menuitemradio"],
8 | [role="textbox"],
9 | [aria-haspopup="listbox"],
10 | [role="listbox"],
11 | [role="spinbutton"] {
12 | border: 1px solid @field-border;
13 | margin-bottom: @space-half;
14 |
15 | &:focus {
16 | outline: 0;
17 | border: 1px solid @link;
18 | .field-focus();
19 | }
20 |
21 | &:hover {
22 | border: 1px solid @field-hover-border;
23 | }
24 |
25 | &@{prefix}error {
26 | border: 1px solid @field-error-border;
27 | outline: 2px solid @field-error-border;
28 |
29 | &:focus {
30 | .field-focus-error();
31 | outline: 0;
32 | }
33 |
34 | &:hover {
35 | border: 1px solid @field-error-hover-border;
36 | }
37 | }
38 | }
39 |
40 | @{prefix}error-wrap {
41 | min-height: @space-large;
42 | color: @error;
43 | text-align: left;
44 | margin-bottom: @space-smallest;
45 | font-size: @text-smallest;
46 | font-weight: @weight-normal;
47 | padding-left: @space-half;
48 | }
49 |
50 | @{prefix}field-wrap {
51 | position: relative;
52 | .border-box();
53 | margin-bottom: @space-large;
54 |
55 | &.flexr {
56 | .align-items(center);
57 |
58 | @{prefix}label {
59 | margin-bottom: 0;
60 | }
61 | }
62 | }
63 |
64 | @{prefix}checkbox-wrap, @{prefix}radio-wrap {
65 | .border-box();
66 | position: relative;
67 | margin-bottom: 0;
68 |
69 | @{prefix}label {
70 | margin-bottom: 0;
71 | }
72 | }
73 |
74 | @{prefix}label {
75 | display: block;
76 | text-align: left;
77 | font-size: @text-smaller;
78 | font-weight: @weight-light;
79 | margin-bottom: @space-half;
80 | cursor: default;
81 |
82 | &@{prefix}label-disabled {
83 | color: @disabled;
84 | }
85 |
86 | &@{prefix}required {
87 | @{prefix}required-text {
88 | display: inline-block;
89 | margin-left: @space-large;
90 | font-style: italic;
91 | font-weight: @weight-normal;
92 |
93 | &::before {
94 | content: ' ';
95 | }
96 | }
97 | &@{prefix}error {
98 | @{prefix}required-text {
99 | color: @error;
100 | }
101 | }
102 | }
103 | }
104 |
105 | @{prefix}label-inline {
106 | display: inline-block;
107 | text-align: left;
108 | font-size: @text-smaller;
109 | }
110 |
111 | @{prefix}text-input {
112 | padding: @space-half;
113 | .border-box();
114 | font-size: @text-small;
115 | color: @text-base;
116 | min-width: @input-min-width;
117 |
118 | &[disabled], &[aria-disabled="true"] {
119 | background-color: @field-disabled;
120 | border: 1px solid @field-disabled;
121 | }
122 | }
123 |
124 | @{prefix}textarea {
125 | display: block;
126 | min-height: 56px;
127 | font-size: @text-smaller;
128 | min-width: @input-min-width;
129 | padding: @space-half;
130 | max-width: 500px;
131 | }
132 |
133 | // custom radios
134 | @{prefix}radio,
135 | @{prefix}checkbox {
136 | width: 36px;
137 | height: 36px;
138 | border-radius: 50%;
139 | border: 1px solid transparent;
140 | .box-sizing(border-box);
141 | .display-flex();
142 | .justify-content(center);
143 | .align-items(center);
144 | cursor: pointer;
145 |
146 | @{prefix}inner-radio,
147 | @{prefix}inner-checkbox {
148 | font-size: 17px;
149 | }
150 |
151 | &[aria-checked="true"] {
152 | color: @link;
153 | }
154 |
155 | &[aria-disabled="true"], &[disabled] {
156 | @{prefix}inner-checkbox, @{prefix}inner-radio {
157 | color: @disabled;
158 | }
159 | }
160 |
161 | &:focus {
162 | outline: 0;
163 | border: 1px solid @focus;
164 | .box-shadow(none);
165 | }
166 |
167 | &:active:not([aria-disabled="true"]) {
168 | background-color: @focus-active;
169 | }
170 | }
171 |
172 |
173 | // help
174 | @{prefix}field-help {
175 | .display-flex();
176 | position: relative;
177 |
178 | @{prefix}text-input {
179 | width: (@input-min-width - 30px);
180 | }
181 |
182 | @{prefix}help-button-wrap {
183 | position: relative;
184 | @{prefix}help-button {
185 | padding: @space-quarter @space-smallest @space-smallest @space-smallest;
186 | border: 0;
187 | margin: 0;
188 | color: @link;
189 | font-size: @text-small;
190 | background-color: transparent;
191 | .border-box();
192 | position: relative;
193 |
194 | &:focus {
195 | outline-offset: 0;
196 | }
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | # :warning: DEPRECATION NOTICE :warning:
3 |
4 | This repo is deprecated. Please go to https://github.com/dequelabs/cauldron
5 | ---
6 |
7 | # Pattern Library [](https://circleci.com/gh/dequelabs/pattern-library)
8 |
9 | ## Installation
10 |
11 | ### NPM
12 |
13 | ```bash
14 | $ npm install deque-pattern-library
15 | ```
16 |
17 | ### Bower
18 |
19 | ```bash
20 | $ bower install deque-pattern-library
21 | ```
22 |
23 | ## CDN
24 | Thanks to [unpkg](https://unpkg.com), you can link directly to deque pattern library files.
25 |
26 | ```html
27 |
28 |
29 |
30 | ```
31 |
32 | ```html
33 |
34 |
35 |
36 | ```
37 |
38 | ## Fonts
39 |
40 | ### Including font awesome (v4)
41 |
42 | The pattern library relies on font awesome meaning including it is **required**.
43 |
44 | ### CDN
45 |
46 | You can include it using a CDN (see [font awesome getting started docs](https://fontawesome.com/v4.7.0/get-started/))
47 |
48 | ### npm
49 |
50 | ```sh
51 | $ npm install font-awesome --save
52 | ```
53 |
54 |
55 | ### Including Roboto
56 |
57 | The patterns look best when the roboto font is available.
58 |
59 | #### CDN
60 |
61 | You can include it using a CDN, like so:
62 |
63 | ```html
64 |
65 | ```
66 |
67 | #### npm
68 |
69 | install it:
70 | ```sh
71 | $ npm install typeface-roboto --save
72 | ```
73 |
74 | include it (in your entry-point):
75 | ```
76 | import 'typeface-roboto';
77 | ```
78 |
79 | ## Usage
80 |
81 | Just drop the css and js into your page:
82 |
83 | ```html
84 |
85 |
86 | ...
87 |
88 |
89 |
90 | ...
91 |
92 |
93 |
94 | ```
95 |
96 | ## What is included?
97 |
98 | * css
99 | * `pattern-library.css`
100 | * `pattern-library.min.css`
101 | * js
102 | * `pattern-library.js`
103 | * `pattern-library.min.js`
104 | * less
105 | * `variables.less`: All of the pattern library's colors and mixins
106 |
107 | ## Getting started
108 |
109 | Please refer to the [wiki](https://github.com/dequelabs/pattern-library/wiki)
110 |
111 | ## Adding new components/composites
112 |
113 | All additions must be approved by our UX team so before working on anything, please create an [Issue](https://github.com/dequelabs/pattern-library/issues) including a detailed description on the requested pattern and several use cases for it.
114 |
115 | ## Development
116 |
117 | - `npm install`
118 | - `npm run build` or for development - `npm run dev` which will rebuild when files are changed
119 |
120 | __NOTE__: if a new component or composite is added, remember to create a quick [wiki](https://github.com/dequelabs/pattern-library/wiki) entry explaining what is absolutely necessary in using this widget.
121 |
122 | ### Testing
123 | Testing is done using mochify along with the 'chai' assertion library (`assert.isFalse(!!0)`). The `test/` directory structure matches the `lib/` directory. This means that if you're testing `lib/components/foo/index.js`, you would create a test in `test/components/foo/index.js`. See the `test/` directory for examples. The tests are browserified and transpiled before running in the phantomjs headless browser so you can `require` / `import` stuff and use ES6 syntax in the tests.
124 |
125 | ```bash
126 | $ npm test
127 | ```
128 |
129 | or to have a watcher re-run tests every time you add a new test:
130 |
131 | ```bash
132 | $ npm run test:dev
133 | ```
134 |
135 | ### Debugging
136 | The pattern library uses the [debug](https://www.npmjs.com/package/debug) module. To turn all debugging on, execute: `localStorage.debug = 'dqpl:*'` and refresh the page. The directory structure of lib is used as the debug naming convention. For example, to specifically debug the "selects" component, execute: `localStorage.debug = 'dqpl:components:selects'`.
137 |
--------------------------------------------------------------------------------