Skip to content

Commit

Permalink
Merge branch 'erinsinger93-develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
jhchen committed Mar 12, 2018
2 parents 22d7d60 + 9e572fe commit 1b836b5
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 5 deletions.
125 changes: 121 additions & 4 deletions test/unit/ui/picker.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import Keyboard from '../../../modules/keyboard';
import Picker from '../../../ui/picker';

function createKeydownEvent(keyCode) {
let event;
if (typeof Event === 'function') {
event = new Event('keydown', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(event, 'keyCode', {'value': keyCode});
} else if (typeof Event === 'object') { // IE11
event = document.createEvent('Event');
Object.defineProperty(event, 'keyCode', {'value': keyCode});
event.initEvent('keydown', true, true);
}
return event;
}

describe('Picker', function() {
it('initialization', function() {
beforeEach(function () {
this.container.innerHTML = '<select><option selected>0</option><option value="1">1</option></select>';
let picker = new Picker(this.container.firstChild); // eslint-disable-line no-unused-vars
this.pickerSelectorInstance = new Picker(this.container.firstChild);
this.pickerSelector = this.container.querySelector('.ql-picker');
});

it('initialization', function() {
expect(this.container.querySelector('.ql-picker')).toBeTruthy();
expect(this.container.querySelector('.ql-active')).toBeFalsy();
expect(this.container.querySelector('.ql-picker-item.ql-selected').outerHTML).toEqualHTML('<span class="ql-picker-item ql-selected" data-label="0"></span>');
expect(this.container.querySelector('.ql-picker-item:not(.ql-selected)').outerHTML).toEqualHTML('<span class="ql-picker-item" data-value="1" data-label="1"></span>');
expect(this.container.querySelector('.ql-picker-item.ql-selected').outerHTML).toEqualHTML('<span tabindex="0" role="button" class="ql-picker-item ql-selected" data-label="0"></span>');
expect(this.container.querySelector('.ql-picker-item:not(.ql-selected)').outerHTML).toEqualHTML('<span tabindex="0" role="button" class="ql-picker-item" data-value="1" data-label="1"></span>');
});

it('escape charcters', function() {
Expand All @@ -20,4 +41,100 @@ describe('Picker', function() {
value = value.replace(/\"/g, '\\"');
expect(select.querySelector(`option[value="${value}"]`)).toEqual(option);
});

it('label is initialized with the correct aria attributes', function() {
expect(this.pickerSelector.querySelector('.ql-picker-label').getAttribute('aria-expanded')).toEqual('false');
const optionsId = this.pickerSelector.querySelector('.ql-picker-options').id;
expect(this.pickerSelector.querySelector('.ql-picker-label').getAttribute('aria-controls')).toEqual(optionsId);
});

it('options container is initialized with the correct aria attributes', function() {
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('true');

const ariaControlsLabel = this.pickerSelector.querySelector('.ql-picker-label').getAttribute('aria-controls');
expect(this.pickerSelector.querySelector('.ql-picker-options').id).toEqual(ariaControlsLabel);
expect(this.pickerSelector.querySelector('.ql-picker-options').tabIndex).toEqual(-1);
});

it('aria attributes toggle correctly when the picker is opened via enter key', function() {
const pickerLabel = this.pickerSelector.querySelector('.ql-picker-label');

// Select picker via enter key
const e = createKeydownEvent(Keyboard.keys.ENTER);
pickerLabel.dispatchEvent(e);

expect(pickerLabel.getAttribute('aria-expanded')).toEqual('true');
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('false');
});

it('aria attributes toggle correctly when the picker is opened via mousedown', function() {
const pickerLabel = this.pickerSelector.querySelector('.ql-picker-label');

// Select picker via enter key
let e;
if (typeof Event === 'function') {
e = new Event('mousedown', {
bubbles: true,
cancelable: true,
});
} else if (typeof Event === 'object') { // IE11
e = document.createEvent('Event');
e.initEvent('mousedown', true, true);
}

pickerLabel.dispatchEvent(e);

expect(pickerLabel.getAttribute('aria-expanded')).toEqual('true');
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('false');
});

it('aria attributes toggle correctly when an item is selected via click', function() {
const pickerLabel = this.pickerSelector.querySelector('.ql-picker-label');
pickerLabel.click();

const pickerItem = this.pickerSelector.querySelector('.ql-picker-item');
pickerItem.click();

expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('true');
expect(pickerLabel.textContent.trim()).toEqual(pickerItem.textContent.trim());
});

it('aria attributes toggle correctly when an item is selected via enter', function() {
const pickerLabel = this.pickerSelector.querySelector('.ql-picker-label');
pickerLabel.click();

const pickerItem = this.pickerSelector.querySelector('.ql-picker-item');
// Select picker item via enter key
const e = createKeydownEvent(Keyboard.keys.ENTER);
pickerItem.dispatchEvent(e);

expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('true');
expect(pickerLabel.textContent.trim()).toEqual(pickerItem.textContent.trim());
});

it('aria attributes toggle correctly when the picker is closed via clicking on the label again', function() {
const pickerLabel = this.pickerSelector.querySelector('.ql-picker-label');
pickerLabel.click();
pickerLabel.click();
expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('true');
});

it('aria attributes toggle correctly when the picker is closed via escaping out of it', function() {
const pickerLabel = this.pickerSelector.querySelector('.ql-picker-label');
pickerLabel.click();

// Escape out of the picker
const e = createKeydownEvent(Keyboard.keys.ESCAPE);
pickerLabel.dispatchEvent(e);

expect(pickerLabel.getAttribute('aria-expanded')).toEqual('false');
expect(this.pickerSelector.querySelector('.ql-picker-options').getAttribute('aria-hidden')).toEqual('true');
});

afterEach(function() {
this.pickerSelectorInstance = null;
});
});
76 changes: 75 additions & 1 deletion ui/picker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import Keyboard from '../modules/keyboard';
import DropdownIcon from '../assets/icons/dropdown.svg';

let optionsCounter = 0;

function toggleAriaAttribute(element, attribute) {
element.setAttribute(attribute, !(element.getAttribute(attribute) === 'true'));
}

class Picker {
constructor(select) {
Expand All @@ -8,14 +14,40 @@ class Picker {
this.buildPicker();
this.select.style.display = 'none';
this.select.parentNode.insertBefore(this.container, this.select);

this.label.addEventListener('mousedown', () => {
this.container.classList.toggle('ql-expanded');
this.togglePicker();
});
this.label.addEventListener('keydown', (event) => {
switch(event.keyCode) {
// Allows the "Enter" key to open the picker
case Keyboard.keys.ENTER:
this.togglePicker();
break;

// Allows the "Escape" key to close the picker
case Keyboard.keys.ESCAPE:
this.escape();
event.preventDefault();
break;
default:
}
});
this.select.addEventListener('change', this.update.bind(this));
}

togglePicker() {
this.container.classList.toggle('ql-expanded');
// Toggle aria-expanded and aria-hidden to make the picker accessible
toggleAriaAttribute(this.label, 'aria-expanded');
toggleAriaAttribute(this.options, 'aria-hidden');
}

buildItem(option) {
let item = document.createElement('span');
item.tabIndex = '0';
item.setAttribute('role', 'button');

item.classList.add('ql-picker-item');
if (option.hasAttribute('value')) {
item.setAttribute('data-value', option.getAttribute('value'));
Expand All @@ -26,20 +58,52 @@ class Picker {
item.addEventListener('click', () => {
this.selectItem(item, true);
});
item.addEventListener('keydown', (event) => {
switch(event.keyCode) {
// Allows the "Enter" key to select an item
case Keyboard.keys.ENTER:
this.selectItem(item, true);
event.preventDefault();
break;

// Allows the "Escape" key to close the picker
case Keyboard.keys.ESCAPE:
this.escape();
event.preventDefault();
break;
default:
}
});

return item;
}

buildLabel() {
let label = document.createElement('span');
label.classList.add('ql-picker-label');
label.innerHTML = DropdownIcon;
label.tabIndex = '0';
label.setAttribute('role', 'button');
label.setAttribute('aria-expanded', 'false');
this.container.appendChild(label);
return label;
}

buildOptions() {
let options = document.createElement('span');
options.classList.add('ql-picker-options');

// Don't want screen readers to read this until options are visible
options.setAttribute('aria-hidden', 'true');
options.tabIndex = '-1';

// Need a unique id for aria-controls
options.id = `ql-picker-options-${optionsCounter}`;
optionsCounter += 1;
this.label.setAttribute('aria-controls', options.id);

this.options = options;

[].slice.call(this.select.options).forEach((option) => {
let item = this.buildItem(option);
options.appendChild(item);
Expand All @@ -59,8 +123,18 @@ class Picker {
this.buildOptions();
}

escape() {
// Close menu and return focus to trigger label
this.close();
// Need setTimeout for accessibility to ensure that the browser executes
// focus on the next process thread and after any DOM content changes
setTimeout(() => this.label.focus(), 1);
}

close() {
this.container.classList.remove('ql-expanded');
this.label.setAttribute('aria-expanded', 'false');
this.options.setAttribute('aria-hidden', 'true');
}

selectItem(item, trigger = false) {
Expand Down

0 comments on commit 1b836b5

Please sign in to comment.