Begin extracting dropdown from core class

This commit is contained in:
Josh Johnson 2017-08-16 12:40:09 +01:00
parent 0f81a03e19
commit b86b8f0ed0
3 changed files with 79 additions and 32 deletions

View file

@ -1,6 +1,7 @@
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import classNames from 'classnames'; import classNames from 'classnames';
import Store from './store/index'; import Store from './store/index';
import Dropdown from './components/dropdown';
import { import {
addItem, addItem,
removeItem, removeItem,
@ -11,8 +12,7 @@ import {
addGroup, addGroup,
clearAll, clearAll,
clearChoices, clearChoices,
} } from './actions/index';
from './actions/index';
import { import {
isScrolledIntoView, isScrolledIntoView,
getAdjacentEl, getAdjacentEl,
@ -29,8 +29,7 @@ import {
triggerEvent, triggerEvent,
findAncestorByAttrName, findAncestorByAttrName,
regexFilter, regexFilter,
} } from './lib/utils';
from './lib/utils';
import './lib/polyfills'; import './lib/polyfills';
/** /**
@ -755,10 +754,10 @@ class Choices {
this.containerOuter.classList.add(this.config.classNames.openState); this.containerOuter.classList.add(this.config.classNames.openState);
this.containerOuter.setAttribute('aria-expanded', 'true'); this.containerOuter.setAttribute('aria-expanded', 'true');
this.dropdown.classList.add(this.config.classNames.activeState); this.dropdown.element.classList.add(this.config.classNames.activeState);
this.dropdown.setAttribute('aria-expanded', 'true'); this.dropdown.element.setAttribute('aria-expanded', 'true');
const dimensions = this.dropdown.getBoundingClientRect(); const dimensions = this.dropdown.element.getBoundingClientRect();
const dropdownPos = Math.ceil(dimensions.top + window.scrollY + this.dropdown.offsetHeight); const dropdownPos = Math.ceil(dimensions.top + window.scrollY + this.dropdown.offsetHeight);
// If flip is enabled and the dropdown bottom position is // If flip is enabled and the dropdown bottom position is
@ -795,8 +794,8 @@ class Choices {
this.containerOuter.classList.remove(this.config.classNames.openState); this.containerOuter.classList.remove(this.config.classNames.openState);
this.containerOuter.setAttribute('aria-expanded', 'false'); this.containerOuter.setAttribute('aria-expanded', 'false');
this.dropdown.classList.remove(this.config.classNames.activeState); this.dropdown.element.classList.remove(this.config.classNames.activeState);
this.dropdown.setAttribute('aria-expanded', 'false'); this.dropdown.element.setAttribute('aria-expanded', 'false');
if (isFlipped) { if (isFlipped) {
this.containerOuter.classList.remove(this.config.classNames.flippedState); this.containerOuter.classList.remove(this.config.classNames.flippedState);
@ -818,7 +817,7 @@ class Choices {
* @public * @public
*/ */
toggleDropdown() { toggleDropdown() {
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); const hasActiveDropdown = this.dropdown.element.classList.contains(this.config.classNames.activeState);
if (hasActiveDropdown) { if (hasActiveDropdown) {
this.hideDropdown(); this.hideDropdown();
} else { } else {
@ -1247,7 +1246,7 @@ class Choices {
const id = element.getAttribute('data-id'); const id = element.getAttribute('data-id');
const choice = this.store.getChoiceById(id); const choice = this.store.getChoiceById(id);
const passedKeyCode = activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null; const passedKeyCode = activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null;
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); const hasActiveDropdown = this.dropdown.element.classList.contains(this.config.classNames.activeState);
// Update choice keyCode // Update choice keyCode
choice.keyCode = passedKeyCode; choice.keyCode = passedKeyCode;
@ -1603,7 +1602,7 @@ class Choices {
const target = e.target; const target = e.target;
const activeItems = this.store.getItemsFilteredByActive(); const activeItems = this.store.getItemsFilteredByActive();
const hasFocusedInput = this.input === document.activeElement; const hasFocusedInput = this.input === document.activeElement;
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); const hasActiveDropdown = this.dropdown.element.classList.contains(this.config.classNames.activeState);
const hasItems = this.itemList && this.itemList.children; const hasItems = this.itemList && this.itemList.children;
const keyString = String.fromCharCode(e.keyCode); const keyString = String.fromCharCode(e.keyCode);
@ -1660,7 +1659,7 @@ class Choices {
if (hasActiveDropdown) { if (hasActiveDropdown) {
e.preventDefault(); e.preventDefault();
const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`); const highlighted = this.dropdown.element.querySelector(`.${this.config.classNames.highlightedState}`);
// If we have a highlighted choice // If we have a highlighted choice
if (highlighted) { if (highlighted) {
@ -1700,16 +1699,16 @@ class Choices {
let nextEl; let nextEl;
if (skipKey) { if (skipKey) {
if (directionInt > 0) { if (directionInt > 0) {
nextEl = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]')).pop(); nextEl = Array.from(this.dropdown.element.querySelectorAll('[data-choice-selectable]')).pop();
} else { } else {
nextEl = this.dropdown.querySelector('[data-choice-selectable]'); nextEl = this.dropdown.element.querySelector('[data-choice-selectable]');
} }
} else { } else {
const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`); const currentEl = this.dropdown.element.querySelector(`.${this.config.classNames.highlightedState}`);
if (currentEl) { if (currentEl) {
nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt); nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt);
} else { } else {
nextEl = this.dropdown.querySelector('[data-choice-selectable]'); nextEl = this.dropdown.element.querySelector('[data-choice-selectable]');
} }
} }
@ -1773,7 +1772,7 @@ class Choices {
// We are typing into a text input and have a value, we want to show a dropdown // We are typing into a text input and have a value, we want to show a dropdown
// notice. Otherwise hide the dropdown // notice. Otherwise hide the dropdown
if (this.isTextElement) { if (this.isTextElement) {
const hasActiveDropdown = this.dropdown.classList.contains( const hasActiveDropdown = this.dropdown.element.classList.contains(
this.config.classNames.activeState, this.config.classNames.activeState,
); );
@ -1844,7 +1843,7 @@ class Choices {
*/ */
_onTouchEnd(e) { _onTouchEnd(e) {
const target = e.target || e.touches[0].target; const target = e.target || e.touches[0].target;
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); const hasActiveDropdown = this.dropdown.element.classList.contains(this.config.classNames.activeState);
// If a user tapped within our container... // If a user tapped within our container...
if (this.wasTap === true && this.containerOuter.contains(target)) { if (this.wasTap === true && this.containerOuter.contains(target)) {
@ -1912,7 +1911,7 @@ class Choices {
*/ */
_onClick(e) { _onClick(e) {
const target = e.target; const target = e.target;
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); const hasActiveDropdown = this.dropdown.element.classList.contains(this.config.classNames.activeState);
const activeItems = this.store.getItemsFilteredByActive(); const activeItems = this.store.getItemsFilteredByActive();
// If target is something that concerns us // If target is something that concerns us
@ -1936,7 +1935,7 @@ class Choices {
} else if ( } else if (
this.isSelectOneElement && this.isSelectOneElement &&
target !== this.input && target !== this.input &&
!this.dropdown.contains(target) !this.dropdown.element.contains(target)
) { ) {
this.hideDropdown(true); this.hideDropdown(true);
} }
@ -1966,7 +1965,7 @@ class Choices {
*/ */
_onMouseOver(e) { _onMouseOver(e) {
// If the dropdown is either the target or one of its children is the target // If the dropdown is either the target or one of its children is the target
if (e.target === this.dropdown || this.dropdown.contains(e.target)) { if (e.target === this.dropdown || this.dropdown.element.contains(e.target)) {
if (e.target.hasAttribute('data-choice')) this._highlightChoice(e.target); if (e.target.hasAttribute('data-choice')) this._highlightChoice(e.target);
} }
} }
@ -1994,7 +1993,7 @@ class Choices {
const target = e.target; const target = e.target;
// If target is something that concerns us // If target is something that concerns us
if (this.containerOuter.contains(target)) { if (this.containerOuter.contains(target)) {
const hasActiveDropdown = this.dropdown.classList.contains( const hasActiveDropdown = this.dropdown.element.classList.contains(
this.config.classNames.activeState, this.config.classNames.activeState,
); );
const focusActions = { const focusActions = {
@ -2040,7 +2039,7 @@ class Choices {
// If target is something that concerns us // If target is something that concerns us
if (this.containerOuter.contains(target) && !this.isScrollingOnIe) { if (this.containerOuter.contains(target) && !this.isScrollingOnIe) {
const activeItems = this.store.getItemsFilteredByActive(); const activeItems = this.store.getItemsFilteredByActive();
const hasActiveDropdown = this.dropdown.classList.contains( const hasActiveDropdown = this.dropdown.element.classList.contains(
this.config.classNames.activeState, this.config.classNames.activeState,
); );
const hasHighlightedItems = activeItems.some(item => item.highlighted); const hasHighlightedItems = activeItems.some(item => item.highlighted);
@ -2166,11 +2165,11 @@ class Choices {
*/ */
_highlightChoice(el = null) { _highlightChoice(el = null) {
// Highlight first element in dropdown // Highlight first element in dropdown
const choices = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]')); const choices = Array.from(this.dropdown.element.querySelectorAll('[data-choice-selectable]'));
let passedEl = el; let passedEl = el;
if (choices && choices.length) { if (choices && choices.length) {
const highlightedChoices = Array.from(this.dropdown.querySelectorAll(`.${this.config.classNames.highlightedState}`)); const highlightedChoices = Array.from(this.dropdown.element.querySelectorAll(`.${this.config.classNames.highlightedState}`));
// Remove any highlighted choices // Remove any highlighted choices
highlightedChoices.forEach((choice) => { highlightedChoices.forEach((choice) => {
@ -2708,7 +2707,7 @@ class Choices {
this.input = input; this.input = input;
this.choiceList = choiceList; this.choiceList = choiceList;
this.itemList = itemList; this.itemList = itemList;
this.dropdown = dropdown; this.dropdown = new Dropdown(this, dropdown);
// Hide passed input // Hide passed input
this.passedElement.classList.add( this.passedElement.classList.add(
@ -2748,7 +2747,7 @@ class Choices {
} }
containerOuter.appendChild(containerInner); containerOuter.appendChild(containerInner);
containerOuter.appendChild(dropdown); containerOuter.appendChild(this.dropdown.element);
containerInner.appendChild(itemList); containerInner.appendChild(itemList);
if (!this.isTextElement) { if (!this.isTextElement) {

View file

@ -0,0 +1,48 @@
export default class Dropdown {
constructor(instance, element) {
this.instance = instance;
this.element = element;
this.dimensions = this.element.getBoundingClientRect();
this.position = Math.ceil(this.dimensions.top + window.scrollY + this.element.offsetHeight);
this.isActive = false;
}
/**
* Determine whether to hide or show dropdown based on its current state
* @return {Object} Class instance
* @public
*/
toggle() {
if (this.isActive) {
this.hideDropdown();
} else {
this.showDropdown();
}
return this.instance;
}
/**
* Show dropdown to user by adding active state class
* @return {Object} Class instance
* @public
*/
show() {
this.dropdown.classList.add(this.config.classNames.activeState);
this.dropdown.setAttribute('aria-expanded', 'true');
this.isActive = true;
return this.instance;
}
/**
* Hide dropdown from user
* @return {Object} Class instance
* @public
*/
hide() {
this.element.classList.remove(this.config.classNames.activeState);
this.element.setAttribute('aria-expanded', 'false');
this.isActive = false;
return this.instance;
}
}

View file

@ -135,7 +135,7 @@ describe('Choices', () => {
}); });
it('should create a dropdown', function() { it('should create a dropdown', function() {
expect(this.choices.dropdown).toEqual(jasmine.any(HTMLElement)); expect(this.choices.dropdown.element).toEqual(jasmine.any(HTMLElement));
}); });
it('should backup and recover original styles', function () { it('should backup and recover original styles', function () {
@ -312,7 +312,7 @@ describe('Choices', () => {
it('should open the choice list on focusing', function() { it('should open the choice list on focusing', function() {
this.choices = new Choices(this.input); this.choices = new Choices(this.input);
this.choices.input.focus(); this.choices.input.focus();
expect(this.choices.dropdown.classList).toContain(this.choices.config.classNames.activeState); expect(this.choices.dropdown.element.classList).toContain(this.choices.config.classNames.activeState);
}); });
it('should select the first choice', function() { it('should select the first choice', function() {
@ -748,7 +748,7 @@ describe('Choices', () => {
this.choices.showDropdown(); this.choices.showDropdown();
const hasOpenState = this.choices.containerOuter.classList.contains(this.choices.config.classNames.openState); const hasOpenState = this.choices.containerOuter.classList.contains(this.choices.config.classNames.openState);
const hasAttr = this.choices.containerOuter.getAttribute('aria-expanded') === 'true'; const hasAttr = this.choices.containerOuter.getAttribute('aria-expanded') === 'true';
const hasActiveState = this.choices.dropdown.classList.contains(this.choices.config.classNames.activeState); const hasActiveState = this.choices.dropdown.element.classList.contains(this.choices.config.classNames.activeState);
expect(hasOpenState && hasAttr && hasActiveState).toBe(true); expect(hasOpenState && hasAttr && hasActiveState).toBe(true);
}); });
@ -757,7 +757,7 @@ describe('Choices', () => {
this.choices.hideDropdown(); this.choices.hideDropdown();
const hasOpenState = this.choices.containerOuter.classList.contains(this.choices.config.classNames.openState); const hasOpenState = this.choices.containerOuter.classList.contains(this.choices.config.classNames.openState);
const hasAttr = this.choices.containerOuter.getAttribute('aria-expanded') === 'true'; const hasAttr = this.choices.containerOuter.getAttribute('aria-expanded') === 'true';
const hasActiveState = this.choices.dropdown.classList.contains(this.choices.config.classNames.activeState); const hasActiveState = this.choices.dropdown.element.classList.contains(this.choices.config.classNames.activeState);
expect(hasOpenState && hasAttr && hasActiveState).toBe(false); expect(hasOpenState && hasAttr && hasActiveState).toBe(false);
}); });