Further modularisation

This commit is contained in:
Josh Johnson 2017-08-27 13:49:35 +01:00
parent 9c4f1f4963
commit 4c7cc047dc
5 changed files with 127 additions and 77 deletions

View file

@ -24,7 +24,6 @@ import {
isElement, isElement,
strToEl, strToEl,
extend, extend,
getWidthOfInput,
sortByAlpha, sortByAlpha,
sortByScore, sortByScore,
generateId, generateId,
@ -512,7 +511,7 @@ class Choices {
} }
const activeItems = this.store.getItemsFilteredByActive(); const activeItems = this.store.getItemsFilteredByActive();
const canAddItem = this._canAddItem(activeItems, this.input.element.value); const canAddItem = this._canAddItem(activeItems, this.input.getValue());
// If we have choices to show // If we have choices to show
if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) { if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
@ -940,7 +939,8 @@ class Choices {
// Add choices if passed // Add choices if passed
if (choices && choices.length) { if (choices && choices.length) {
this.containerOuter.element.classList.remove(this.config.classNames.loadingState); this.containerOuter.removeLoadingState();
choices.forEach((result) => { choices.forEach((result) => {
if (result.choices) { if (result.choices) {
this._addGroup( this._addGroup(
@ -1162,9 +1162,7 @@ class Choices {
// Focus input as without focus, a user cannot do anything with a // Focus input as without focus, a user cannot do anything with a
// highlighted item // highlighted item
if (document.activeElement !== this.input.element) { this.input.focus();
this.input.element.focus();
}
} }
} }
@ -1214,7 +1212,7 @@ class Choices {
// We wont to close the dropdown if we are dealing with a single select box // We wont to close the dropdown if we are dealing with a single select box
if (hasActiveDropdown && this.isSelectOneElement) { if (hasActiveDropdown && this.isSelectOneElement) {
this.hideDropdown(); this.hideDropdown();
this.containerOuter.element.focus(); this.containerOuter.focus();
} }
} }
@ -1232,7 +1230,7 @@ class Choices {
// If editing the last item is allowed and there are not other selected items, // If editing the last item is allowed and there are not other selected items,
// we can edit the item value. Otherwise if we can remove items, remove all selected items // we can edit the item value. Otherwise if we can remove items, remove all selected items
if (this.config.editItems && !hasHighlightedItems && lastItem) { if (this.config.editItems && !hasHighlightedItems && lastItem) {
this.input.element.value = lastItem.value; this.input.setValue(lastItem.value);
this.input.setWidth(); this.input.setWidth();
this._removeItem(lastItem); this._removeItem(lastItem);
this._triggerChange(lastItem.value); this._triggerChange(lastItem.value);
@ -1312,8 +1310,7 @@ class Choices {
_handleLoadingState(isLoading = true) { _handleLoadingState(isLoading = true) {
let placeholderItem = this.itemList.querySelector(`.${this.config.classNames.placeholder}`); let placeholderItem = this.itemList.querySelector(`.${this.config.classNames.placeholder}`);
if (isLoading) { if (isLoading) {
this.containerOuter.element.classList.add(this.config.classNames.loadingState); this.containerOuter.addLoadingState();
this.containerOuter.element.setAttribute('aria-busy', 'true');
if (this.isSelectOneElement) { if (this.isSelectOneElement) {
if (!placeholderItem) { if (!placeholderItem) {
placeholderItem = this._getTemplate('placeholder', this.config.loadingText); placeholderItem = this._getTemplate('placeholder', this.config.loadingText);
@ -1325,8 +1322,7 @@ class Choices {
this.input.setPlaceholder(this.config.loadingText); this.input.setPlaceholder(this.config.loadingText);
} }
} else { } else {
// Remove loading states/text this.containerOuter.removeLoadingState();
this.containerOuter.element.classList.remove(this.config.classNames.loadingState);
if (this.isSelectOneElement) { if (this.isSelectOneElement) {
placeholderItem.innerHTML = (this.placeholder || ''); placeholderItem.innerHTML = (this.placeholder || '');
@ -1382,8 +1378,6 @@ class Choices {
// No results, remove loading state // No results, remove loading state
this._handleLoadingState(false); this._handleLoadingState(false);
} }
this.containerOuter.element.removeAttribute('aria-busy');
}; };
} }
@ -1438,7 +1432,7 @@ class Choices {
const hasUnactiveChoices = choices.some(option => !option.active); const hasUnactiveChoices = choices.some(option => !option.active);
// Run callback if it is a function // Run callback if it is a function
if (this.input.element === document.activeElement) { if (this.input.isFocussed) {
// Check that we have a value to search and the input was an alphanumeric character // Check that we have a value to search and the input was an alphanumeric character
if (value && value.length >= this.config.searchFloor) { if (value && value.length >= this.config.searchFloor) {
let resultCount = 0; let resultCount = 0;
@ -1524,7 +1518,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.element === document.activeElement; const hasFocusedInput = this.input.isFocussed;
const hasActiveDropdown = this.dropdown.isActive; const hasActiveDropdown = this.dropdown.isActive;
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);
@ -1538,7 +1532,7 @@ class Choices {
const downKey = 40; const downKey = 40;
const pageUpKey = 33; const pageUpKey = 33;
const pageDownKey = 34; const pageDownKey = 34;
const ctrlDownKey = e.ctrlKey || e.metaKey; const ctrlDownKey = (e.ctrlKey || e.metaKey);
// If a user is typing and the dropdown is not active // If a user is typing and the dropdown is not active
if (!this.isTextElement && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) { if (!this.isTextElement && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) {
@ -1586,9 +1580,7 @@ class Choices {
if (hasActiveDropdown) { if (hasActiveDropdown) {
e.preventDefault(); e.preventDefault();
const highlighted = this.dropdown.element.querySelector( const highlighted = this.dropdown.getHighlightedChildren();
`.${this.config.classNames.highlightedState}`,
);
// If we have a highlighted choice // If we have a highlighted choice
if (highlighted) { if (highlighted) {
@ -1607,8 +1599,8 @@ class Choices {
const onEscapeKey = () => { const onEscapeKey = () => {
if (hasActiveDropdown) { if (hasActiveDropdown) {
this.dropdown.hide(); this.hideDropdown();
this.containerOuter.element.focus(); this.containerOuter.focus();
} }
}; };
@ -1762,7 +1754,7 @@ class Choices {
* @private * @private
*/ */
_onTouchEnd(e) { _onTouchEnd(e) {
const target = e.target || e.touches[0].target; const target = (e.target || e.touches[0].target);
const hasActiveDropdown = this.dropdown.isActive; const hasActiveDropdown = this.dropdown.isActive;
// If a user tapped within our container... // If a user tapped within our container...
@ -1773,10 +1765,8 @@ class Choices {
!this.isSelectOneElement !this.isSelectOneElement
) { ) {
if (this.isTextElement) { if (this.isTextElement) {
// If text element, we only want to focus the input (if it isn't already) // If text element, we only want to focus the input
if (document.activeElement !== this.input.element) { this.input.focus();
this.input.element.focus();
}
} else if (!hasActiveDropdown) { } else if (!hasActiveDropdown) {
// If a select box, we want to show the dropdown // If a select box, we want to show the dropdown
this.showDropdown(true); this.showDropdown(true);
@ -1836,21 +1826,16 @@ class Choices {
// If target is something that concerns us // If target is something that concerns us
if (this.containerOuter.element.contains(target)) { if (this.containerOuter.element.contains(target)) {
// Handle button delete
if (target.hasAttribute('data-button')) {
this._handleButtonAction(activeItems, target);
}
if (!hasActiveDropdown) { if (!hasActiveDropdown) {
if (this.isTextElement) { if (this.isTextElement) {
if (document.activeElement !== this.input.element) { if (document.activeElement !== this.input.element) {
this.input.element.focus(); this.input.focus();
} }
} else if (this.canSearch) { } else if (this.canSearch) {
this.showDropdown(true); this.showDropdown(true);
} else { } else {
this.showDropdown(); this.showDropdown();
this.containerOuter.element.focus(); this.containerOuter.focus();
} }
} else if ( } else if (
this.isSelectOneElement && this.isSelectOneElement &&
@ -1868,7 +1853,7 @@ class Choices {
} }
// Remove focus state // Remove focus state
this.containerOuter.blur(); this.containerOuter.removeFocusState();
// Close all other dropdowns // Close all other dropdowns
if (hasActiveDropdown) { if (hasActiveDropdown) {
@ -1907,24 +1892,21 @@ class Choices {
const focusActions = { const focusActions = {
text: () => { text: () => {
if (target === this.input.element) { if (target === this.input.element) {
this.containerOuter.focus(); this.containerOuter.addFocusState();
} }
}, },
'select-one': () => { 'select-one': () => {
this.containerOuter.focus(); this.containerOuter.addFocusState();
if (target === this.input.element) { if ((target === this.input.element) && !hasActiveDropdown) {
// Show dropdown if it isn't already showing // Show dropdown if it isn't already showing
if (!hasActiveDropdown) { this.showDropdown();
this.showDropdown();
}
} }
}, },
'select-multiple': () => { 'select-multiple': () => {
if (target === this.input.element) { if (target === this.input.element) {
// If element is a select box, the focused element is the container and the dropdown // If element is a select box, the focused element is the container and the dropdown
// isn't already open, focus and show dropdown // isn't already open, focus and show dropdown
this.containerOuter.focus(); this.containerOuter.addFocusState();
if (!hasActiveDropdown) { if (!hasActiveDropdown) {
this.showDropdown(true); this.showDropdown(true);
} }
@ -1953,7 +1935,7 @@ class Choices {
text: () => { text: () => {
if (target === this.input.element) { if (target === this.input.element) {
// Remove the focus state // Remove the focus state
this.containerOuter.blur(); this.containerOuter.removeFocusState();
// De-select any highlighted items // De-select any highlighted items
if (hasHighlightedItems) { if (hasHighlightedItems) {
this.unhighlightAll(); this.unhighlightAll();
@ -1965,7 +1947,7 @@ class Choices {
} }
}, },
'select-one': () => { 'select-one': () => {
this.containerOuter.blur(); this.containerOuter.removeFocusState();
if (target === this.containerOuter.element) { if (target === this.containerOuter.element) {
// Hide dropdown if it is showing // Hide dropdown if it is showing
if (hasActiveDropdown && !this.canSearch) { if (hasActiveDropdown && !this.canSearch) {
@ -1980,7 +1962,7 @@ class Choices {
'select-multiple': () => { 'select-multiple': () => {
if (target === this.input.element) { if (target === this.input.element) {
// Remove the focus state // Remove the focus state
this.containerOuter.blur(); this.containerOuter.removeFocusState();
// Hide dropdown if it is showing // Hide dropdown if it is showing
if (hasActiveDropdown) { if (hasActiveDropdown) {
this.hideDropdown(); this.hideDropdown();
@ -2110,8 +2092,8 @@ class Choices {
if (hasActiveDropdown) { if (hasActiveDropdown) {
// IE11 ignores aria-label and blocks virtual keyboard // IE11 ignores aria-label and blocks virtual keyboard
// if aria-activedescendant is set without a dropdown // if aria-activedescendant is set without a dropdown
this.input.element.setAttribute('aria-activedescendant', passedEl.id); this.input.setActiveDescendant(passedEl.id);
this.containerOuter.element.setAttribute('aria-activedescendant', passedEl.id); this.containerOuter.setActiveDescendant(passedEl.id);
} }
} }
} }
@ -2653,10 +2635,10 @@ class Choices {
wrap(this.containerInner.element, this.containerOuter.element); wrap(this.containerInner.element, this.containerOuter.element);
if (this.isSelectOneElement) { if (this.isSelectOneElement) {
this.input.element.placeholder = this.config.searchPlaceholderValue || ''; this.input.setPlaceholder(this.config.searchPlaceholderValue || '');
} else if (this.placeholder) { } else if (this.placeholder) {
this.input.element.placeholder = this.placeholder; this.input.setPlaceholder(this.placeholder);
this.input.element.style.width = getWidthOfInput(this.input.element); this.input.setWidth(true);
} }
if (!this.config.addItems) { if (!this.config.addItems) {

View file

@ -11,6 +11,26 @@ export default class Container {
this.isFlipped = false; this.isFlipped = false;
this.isFocussed = false; this.isFocussed = false;
this.isDisabled = false; this.isDisabled = false;
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
}
addEventListeners() {
this.element.addEventListener('focus', this.onFocus);
this.element.addEventListener('blur', this.onBlur);
}
removeEventListeners() {
this.element.removeEventListener('focus', this.onFocus);
this.element.removeEventListener('blur', this.onBlur);
}
onFocus() {
this.isFocussed = true;
}
onBlur() {
this.isFocussed = false;
} }
shouldFlip(dropdownPos) { shouldFlip(dropdownPos) {
@ -40,6 +60,14 @@ export default class Container {
return shouldFlip; return shouldFlip;
} }
setActiveDescendant(activeDescendant) {
this.element.setAttribute('aria-activedescendant', activeDescendant);
}
removeActiveDescendant() {
this.element.removeAttribute('aria-activedescendant');
}
open(dropdownPos) { open(dropdownPos) {
this.element.classList.add(this.classNames.openState); this.element.classList.add(this.classNames.openState);
this.element.setAttribute('aria-expanded', 'true'); this.element.setAttribute('aria-expanded', 'true');
@ -54,7 +82,7 @@ export default class Container {
close() { close() {
this.element.classList.remove(this.classNames.openState); this.element.classList.remove(this.classNames.openState);
this.element.setAttribute('aria-expanded', 'false'); this.element.setAttribute('aria-expanded', 'false');
this.element.removeAttribute('aria-activedescendant'); this.removeActiveDescendant();
this.isOpen = false; this.isOpen = false;
// A dropdown flips if it does not have space within the page // A dropdown flips if it does not have space within the page
@ -65,13 +93,17 @@ export default class Container {
} }
focus() { focus() {
this.element.classList.add(this.classNames.focusState); if (!this.isFocussed) {
this.isFocussed = true; this.element.focus();
}
} }
blur() { addFocusState() {
this.element.classList.add(this.classNames.focusState);
}
removeFocusState() {
this.element.classList.remove(this.classNames.focusState); this.element.classList.remove(this.classNames.focusState);
this.isFocussed = false;
} }
enable() { enable() {
@ -91,4 +123,14 @@ export default class Container {
} }
this.isDisabled = true; this.isDisabled = true;
} }
addLoadingState() {
this.element.classList.add(this.classNames.loadingState);
this.element.setAttribute('aria-busy', 'true');
}
removeLoadingState() {
this.element.classList.remove(this.classNames.loadingState);
this.element.removeAttribute('aria-busy');
}
} }

View file

@ -17,19 +17,10 @@ export default class Dropdown {
return this.position; return this.position;
} }
/** getHighlightedChildren() {
* Determine whether to hide or show dropdown based on its current state return this.element.querySelector(
* @return {Object} Class instance `.${this.classNames.highlightedState}`,
* @public );
*/
toggle() {
if (this.isActive) {
this.hide();
} else {
this.show();
}
return this.instance;
} }
/** /**

View file

@ -8,20 +8,27 @@ export default class Input {
this.instance = instance; this.instance = instance;
this.element = element; this.element = element;
this.classNames = classNames; this.classNames = classNames;
this.isFocussed = this.element === document.activeElement;
// Bind event listeners // Bind event listeners
this.onPaste = this.onPaste.bind(this); this.onPaste = this.onPaste.bind(this);
this.onInput = this.onInput.bind(this); this.onInput = this.onInput.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
} }
addEventListeners() { addEventListeners() {
this.element.addEventListener('input', this.onInput); this.element.addEventListener('input', this.onInput);
this.element.addEventListener('paste', this.onPaste); this.element.addEventListener('paste', this.onPaste);
this.element.addEventListener('focus', this.onFocus);
this.element.addEventListener('blur', this.onBlur);
} }
removeEventListeners() { removeEventListeners() {
this.element.removeEventListener('input', this.onInput); this.element.removeEventListener('input', this.onInput);
this.element.removeEventListener('paste', this.onPaste); this.element.removeEventListener('paste', this.onPaste);
this.element.removeEventListener('focus', this.onFocus);
this.element.removeEventListener('blur', this.onBlur);
} }
/** /**
@ -48,6 +55,14 @@ export default class Input {
} }
} }
onFocus() {
this.isFocussed = true;
}
onBlur() {
this.isFocussed = false;
}
activate(focusInput) { activate(focusInput) {
// Optionally focus the input if we have a search input // Optionally focus the input if we have a search input
if (focusInput && this.instance.canSearch && document.activeElement !== this.element) { if (focusInput && this.instance.canSearch && document.activeElement !== this.element) {
@ -56,10 +71,7 @@ export default class Input {
} }
deactivate(blurInput) { deactivate(blurInput) {
// IE11 ignores aria-label and blocks virtual keyboard this.removeActiveDescendant();
// if aria-activedescendant is set without a dropdown
this.element.removeAttribute('aria-activedescendant');
// Optionally blur the input if we have a search input // Optionally blur the input if we have a search input
if (blurInput && this.instance.canSearch && document.activeElement === this.element) { if (blurInput && this.instance.canSearch && document.activeElement === this.element) {
this.element.blur(); this.element.blur();
@ -74,6 +86,12 @@ export default class Input {
this.element.setAttribute('disabled', ''); this.element.setAttribute('disabled', '');
} }
focus() {
if (!this.isFocussed) {
this.element.focus();
}
}
/** /**
* Set value of input to blank * Set value of input to blank
* @return {Object} Class instance * @return {Object} Class instance
@ -96,13 +114,14 @@ export default class Input {
* value or input value * value or input value
* @return * @return
*/ */
setWidth() { setWidth(enforceWidth) {
if (this.instance.placeholder) { if (this.instance.placeholder) {
// If there is a placeholder, we only want to set the width of the input when it is a greater // If there is a placeholder, we only want to set the width of the input when it is a greater
// length than 75% of the placeholder. This stops the input jumping around. // length than 75% of the placeholder. This stops the input jumping around.
if ( if (
this.element.value && (this.element.value &&
this.element.value.length >= (this.instance.placeholder.length / 1.25) this.element.value.length >= (this.instance.placeholder.length / 1.25)) ||
enforceWidth
) { ) {
this.element.style.width = getWidthOfInput(this.element); this.element.style.width = getWidthOfInput(this.element);
} }
@ -115,4 +134,20 @@ export default class Input {
setPlaceholder(placeholder) { setPlaceholder(placeholder) {
this.element.placeholder = placeholder; this.element.placeholder = placeholder;
} }
setValue(value) {
this.element.value = value;
}
getValue() {
return this.element.value;
}
setActiveDescendant(activeDescendant) {
this.element.setAttribute('aria-activedescendant', activeDescendant);
}
removeActiveDescendant() {
this.element.removeAttribute('aria-activedescendant');
}
} }

View file

@ -780,7 +780,7 @@ describe('Choices', () => {
it('should handle toggleDropdown()', function() { it('should handle toggleDropdown()', function() {
spyOn(this.choices.dropdown, 'hide'); spyOn(this.choices.dropdown, 'hide');
this.choices.dropdown.show(); this.choices.dropdown.show();
this.choices.dropdown.toggle(); this.choices.toggleDropdown();
expect(this.choices.dropdown.hide).toHaveBeenCalled(); expect(this.choices.dropdown.hide).toHaveBeenCalled();
}); });