Fix remaning type issues

This commit is contained in:
Josh Johnson 2019-12-21 11:49:15 +00:00
parent 44666936a9
commit a80d42f5f6
25 changed files with 251 additions and 222 deletions

View file

@ -14,9 +14,14 @@ export interface AddChoiceAction {
keyCode: number; keyCode: number;
} }
export interface Result<T> {
item: T;
score: number;
}
export interface FilterChoicesAction { export interface FilterChoicesAction {
type: typeof ACTION_TYPES.FILTER_CHOICES; type: typeof ACTION_TYPES.FILTER_CHOICES;
results: Choice[]; results: Result<Choice>[];
} }
export interface ActivateChoicesAction { export interface ActivateChoicesAction {
@ -51,7 +56,9 @@ export const addChoice = ({
keyCode, keyCode,
}); });
export const filterChoices = (results: Choice[]): FilterChoicesAction => ({ export const filterChoices = (
results: Result<Choice>[],
): FilterChoicesAction => ({
type: ACTION_TYPES.FILTER_CHOICES, type: ACTION_TYPES.FILTER_CHOICES,
results, results,
}); });

View file

@ -5,7 +5,7 @@ describe('actions/groups', () => {
describe('addGroup action', () => { describe('addGroup action', () => {
it('returns ADD_GROUP action', () => { it('returns ADD_GROUP action', () => {
const value = 'test'; const value = 'test';
const id = 'test'; const id = 1;
const active = true; const active = true;
const disabled = false; const disabled = false;
const expectedAction = { const expectedAction = {

View file

@ -6,9 +6,9 @@ describe('actions/items', () => {
it('returns ADD_ITEM action', () => { it('returns ADD_ITEM action', () => {
const value = 'test'; const value = 'test';
const label = 'test'; const label = 'test';
const id = '1234'; const id = 1;
const choiceId = '1234'; const choiceId = 1;
const groupId = 'test'; const groupId = 1;
const customProperties = { test: true }; const customProperties = { test: true };
const placeholder = true; const placeholder = true;
const keyCode = 10; const keyCode = 10;
@ -42,8 +42,8 @@ describe('actions/items', () => {
describe('removeItem action', () => { describe('removeItem action', () => {
it('returns REMOVE_ITEM action', () => { it('returns REMOVE_ITEM action', () => {
const id = '1234'; const id = 1;
const choiceId = '1'; const choiceId = 1;
const expectedAction = { const expectedAction = {
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',
id, id,
@ -56,7 +56,7 @@ describe('actions/items', () => {
describe('highlightItem action', () => { describe('highlightItem action', () => {
it('returns HIGHLIGHT_ITEM action', () => { it('returns HIGHLIGHT_ITEM action', () => {
const id = '1234'; const id = 1;
const highlighted = true; const highlighted = true;
const expectedAction = { const expectedAction = {

View file

@ -14,7 +14,14 @@ describe('actions/misc', () => {
describe('resetTo action', () => { describe('resetTo action', () => {
it('returns RESET_TO action', () => { it('returns RESET_TO action', () => {
const state = { test: true }; const state = {
choices: [],
items: [],
groups: [],
general: {
loading: false,
},
};
const expectedAction = { const expectedAction = {
type: 'RESET_TO', type: 'RESET_TO',
state, state,

View file

@ -3,9 +3,11 @@ import { spy, stub } from 'sinon';
import sinonChai from 'sinon-chai'; import sinonChai from 'sinon-chai';
import Choices from './choices'; import Choices from './choices';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants'; import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants';
import { WrappedSelect, WrappedInput } from './components/index'; import { WrappedSelect, WrappedInput } from './components/index';
import { removeItem } from './actions/items'; import { removeItem } from './actions/items';
import { Item, Choice, Group } from './interfaces';
chai.use(sinonChai); chai.use(sinonChai);
@ -28,12 +30,6 @@ describe('choices', () => {
instance = null; instance = null;
}); });
const returnsInstance = () => {
it('returns this', () => {
expect(output).to.eql(instance);
});
};
describe('constructor', () => { describe('constructor', () => {
describe('config', () => { describe('config', () => {
describe('not passing config options', () => { describe('not passing config options', () => {
@ -88,7 +84,7 @@ describe('choices', () => {
`; `;
instance = new Choices('[data-choice]', { instance = new Choices('[data-choice]', {
renderSelectedChoices: 'test', renderSelectedChoices: 'test' as any,
}); });
expect(instance.config.renderSelectedChoices).to.equal('auto'); expect(instance.config.renderSelectedChoices).to.equal('auto');
@ -211,7 +207,7 @@ describe('choices', () => {
<input data-choice type="text" id="input-1" /> <input data-choice type="text" id="input-1" />
`; `;
instance = new Choices(document.querySelector('[data-choice]')); instance = new Choices('[data-choice]');
expect(instance.passedElement).to.be.an.instanceOf(WrappedInput); expect(instance.passedElement).to.be.an.instanceOf(WrappedInput);
}); });
@ -223,7 +219,7 @@ describe('choices', () => {
<select data-choice id="select-1"></select> <select data-choice id="select-1"></select>
`; `;
instance = new Choices(document.querySelector('[data-choice]')); instance = new Choices('[data-choice]');
expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect); expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect);
}); });
@ -423,7 +419,9 @@ describe('choices', () => {
output = instance.enable(); output = instance.enable();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(passedElementEnableSpy.called).to.equal(false); expect(passedElementEnableSpy.called).to.equal(false);
@ -481,7 +479,9 @@ describe('choices', () => {
output = instance.disable(); output = instance.disable();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(removeEventListenersSpy.called).to.equal(false); expect(removeEventListenersSpy.called).to.equal(false);
@ -638,7 +638,9 @@ describe('choices', () => {
output = instance.hideDropdown(); output = instance.hideDropdown();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(containerOuterCloseSpy.called).to.equal(false); expect(containerOuterCloseSpy.called).to.equal(false);
@ -735,7 +737,9 @@ describe('choices', () => {
output = instance.highlightItem(); output = instance.highlightItem();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
@ -745,7 +749,7 @@ describe('choices', () => {
}); });
describe('item passed', () => { describe('item passed', () => {
const item = { const item: Item = {
id: 1234, id: 1234,
value: 'Test', value: 'Test',
label: 'Test', label: 'Test',
@ -756,7 +760,9 @@ describe('choices', () => {
output = instance.highlightItem(item, true); output = instance.highlightItem(item, true);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches highlightItem action with correct arguments', () => { it('dispatches highlightItem action with correct arguments', () => {
expect(storeDispatchSpy.called).to.equal(true); expect(storeDispatchSpy.called).to.equal(true);
@ -817,7 +823,9 @@ describe('choices', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
}); });
}); });
}); });
@ -850,7 +858,9 @@ describe('choices', () => {
output = instance.unhighlightItem(); output = instance.unhighlightItem();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
@ -860,7 +870,7 @@ describe('choices', () => {
}); });
describe('item passed', () => { describe('item passed', () => {
const item = { const item: Item = {
id: 1234, id: 1234,
value: 'Test', value: 'Test',
label: 'Test', label: 'Test',
@ -871,7 +881,9 @@ describe('choices', () => {
output = instance.unhighlightItem(item, true); output = instance.unhighlightItem(item, true);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches highlightItem action with correct arguments', () => { it('dispatches highlightItem action with correct arguments', () => {
expect(storeDispatchSpy.called).to.equal(true); expect(storeDispatchSpy.called).to.equal(true);
@ -932,7 +944,9 @@ describe('choices', () => {
expect(passedElementTriggerEventStub.called).to.equal(false); expect(passedElementTriggerEventStub.called).to.equal(false);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
}); });
}); });
}); });
@ -966,7 +980,9 @@ describe('choices', () => {
storeGetItemsStub.reset(); storeGetItemsStub.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('highlights each item in store', () => { it('highlights each item in store', () => {
expect(highlightItemStub.callCount).to.equal(items.length); expect(highlightItemStub.callCount).to.equal(items.length);
@ -1004,7 +1020,9 @@ describe('choices', () => {
storeGetItemsStub.reset(); storeGetItemsStub.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('unhighlights each item in store', () => { it('unhighlights each item in store', () => {
expect(unhighlightItemStub.callCount).to.equal(items.length); expect(unhighlightItemStub.callCount).to.equal(items.length);
@ -1027,7 +1045,9 @@ describe('choices', () => {
instance._store.dispatch.reset(); instance._store.dispatch.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches clearChoices action', () => { it('dispatches clearChoices action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({ expect(storeDispatchStub.lastCall.args[0]).to.eql({
@ -1050,7 +1070,9 @@ describe('choices', () => {
instance._store.dispatch.reset(); instance._store.dispatch.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('dispatches clearAll action', () => { it('dispatches clearAll action', () => {
expect(storeDispatchStub.lastCall.args[0]).to.eql({ expect(storeDispatchStub.lastCall.args[0]).to.eql({
@ -1075,7 +1097,9 @@ describe('choices', () => {
instance._store.dispatch.reset(); instance._store.dispatch.reset();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
describe('text element', () => { describe('text element', () => {
beforeEach(() => { beforeEach(() => {
@ -1164,14 +1188,14 @@ describe('choices', () => {
const handleLoadingStateSpy = spy(choice, '_handleLoadingState'); const handleLoadingStateSpy = spy(choice, '_handleLoadingState');
let fetcherCalled = false; let fetcherCalled = false;
const fetcher = async inst => { const fetcher = async (inst): Promise<Choice[]> => {
expect(inst).to.eq(choice); expect(inst).to.eq(choice);
fetcherCalled = true; fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 800)); await new Promise(resolve => setTimeout(resolve, 800));
return [ return [
{ label: 'l1', value: 'v1', customProperties: 'prop1' }, { label: 'l1', value: 'v1', customProperties: { prop1: true } },
{ label: 'l2', value: 'v2', customProperties: 'prop2' }, { label: 'l2', value: 'v2', customProperties: { prop2: false } },
]; ];
}; };
expect(choice._store.choices.length).to.equal(0); expect(choice._store.choices.length).to.equal(0);
@ -1211,7 +1235,9 @@ describe('choices', () => {
output = instance.setValue(values); output = instance.setValue(values);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(setChoiceOrItemStub.called).to.equal(false); expect(setChoiceOrItemStub.called).to.equal(false);
@ -1224,7 +1250,9 @@ describe('choices', () => {
output = instance.setValue(values); output = instance.setValue(values);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each value', () => { it('sets each value', () => {
expect(setChoiceOrItemStub.callCount).to.equal(2); expect(setChoiceOrItemStub.callCount).to.equal(2);
@ -1252,7 +1280,9 @@ describe('choices', () => {
output = instance.setChoiceByValue([]); output = instance.setChoiceByValue([]);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('returns early', () => { it('returns early', () => {
expect(findAndSelectChoiceByValueStub.called).to.equal(false); expect(findAndSelectChoiceByValueStub.called).to.equal(false);
@ -1272,7 +1302,9 @@ describe('choices', () => {
output = instance.setChoiceByValue(value); output = instance.setChoiceByValue(value);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each choice with same value', () => { it('sets each choice with same value', () => {
expect(findAndSelectChoiceByValueStub.called).to.equal(true); expect(findAndSelectChoiceByValueStub.called).to.equal(true);
@ -1289,7 +1321,9 @@ describe('choices', () => {
output = instance.setChoiceByValue(values); output = instance.setChoiceByValue(values);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('sets each choice with same value', () => { it('sets each choice with same value', () => {
expect(findAndSelectChoiceByValueStub.callCount).to.equal(2); expect(findAndSelectChoiceByValueStub.callCount).to.equal(2);
@ -1509,7 +1543,9 @@ describe('choices', () => {
output = instance.removeHighlightedItems(); output = instance.removeHighlightedItems();
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('removes each highlighted item in store', () => { it('removes each highlighted item in store', () => {
expect(removeItemStub.callCount).to.equal(2); expect(removeItemStub.callCount).to.equal(2);
@ -1521,7 +1557,9 @@ describe('choices', () => {
output = instance.removeHighlightedItems(true); output = instance.removeHighlightedItems(true);
}); });
returnsInstance(output); it('returns this', () => {
expect(output).to.eql(instance);
});
it('triggers event with item value', () => { it('triggers event with item value', () => {
expect(triggerChangeStub.callCount).to.equal(2); expect(triggerChangeStub.callCount).to.equal(2);
@ -1538,7 +1576,7 @@ describe('choices', () => {
let containerOuterRemoveLoadingStateStub; let containerOuterRemoveLoadingStateStub;
const value = 'value'; const value = 'value';
const label = 'label'; const label = 'label';
const choices = [ const choices: Choice[] = [
{ {
id: 1, id: 1,
value: '1', value: '1',
@ -1550,7 +1588,7 @@ describe('choices', () => {
label: 'Test 2', label: 'Test 2',
}, },
]; ];
const groups = [ const groups: Group[] = [
{ {
...choices[0], ...choices[0],
choices, choices,
@ -1679,7 +1717,7 @@ describe('choices', () => {
describe('private methods', () => { describe('private methods', () => {
describe('_createGroupsFragment', () => { describe('_createGroupsFragment', () => {
let _createChoicesFragmentStub; let _createChoicesFragmentStub;
const choices = [ const choices: Choice[] = [
{ {
id: 1, id: 1,
selected: true, selected: true,
@ -1703,7 +1741,7 @@ describe('choices', () => {
}, },
]; ];
const groups = [ const groups: Group[] = [
{ {
id: 2, id: 2,
value: 'Group 2', value: 'Group 2',

View file

@ -19,12 +19,13 @@ import {
SELECT_ONE_TYPE, SELECT_ONE_TYPE,
SELECT_MULTIPLE_TYPE, SELECT_MULTIPLE_TYPE,
} from './constants'; } from './constants';
import { TEMPLATES } from './templates'; import templates from './templates';
import { import {
addChoice, addChoice,
filterChoices, filterChoices,
activateChoices, activateChoices,
clearChoices, clearChoices,
Result,
} from './actions/choices'; } from './actions/choices';
import { addItem, removeItem, highlightItem } from './actions/items'; import { addItem, removeItem, highlightItem } from './actions/items';
import { addGroup } from './actions/groups'; import { addGroup } from './actions/groups';
@ -41,7 +42,6 @@ import {
diff, diff,
} from './lib/utils'; } from './lib/utils';
import { import {
Templates,
Options, Options,
Choice, Choice,
Item, Item,
@ -67,14 +67,14 @@ const USER_DEFAULTS: Partial<Options> = {};
class Choices { class Choices {
static get defaults(): { static get defaults(): {
options: Partial<Options>; options: Partial<Options>;
templates: Templates; templates: typeof templates;
} { } {
return Object.preventExtensions({ return Object.preventExtensions({
get options(): Partial<Options> { get options(): Partial<Options> {
return USER_DEFAULTS; return USER_DEFAULTS;
}, },
get templates(): Templates { get templates(): typeof templates {
return TEMPLATES; return templates;
}, },
}); });
} }
@ -94,7 +94,7 @@ class Choices {
_isSelectMultipleElement: boolean; _isSelectMultipleElement: boolean;
_isSelectElement: boolean; _isSelectElement: boolean;
_store: Store; _store: Store;
_templates: Templates; _templates: typeof templates;
_initialState: State; _initialState: State;
_currentState: State; _currentState: State;
_prevState: State; _prevState: State;
@ -116,7 +116,11 @@ class Choices {
_presetItems: Item[] | string[]; _presetItems: Item[] | string[];
constructor( constructor(
element: string | HTMLInputElement | HTMLSelectElement = '[data-choice]', element:
| string
| Element
| HTMLInputElement
| HTMLSelectElement = '[data-choice]',
userConfig: Partial<Options> = {}, userConfig: Partial<Options> = {},
) { ) {
this.config = merge.all<Options>( this.config = merge.all<Options>(
@ -183,7 +187,7 @@ class Choices {
this.passedElement = new WrappedSelect({ this.passedElement = new WrappedSelect({
element: passedElement as HTMLSelectElement, element: passedElement as HTMLSelectElement,
classNames: this.config.classNames, classNames: this.config.classNames,
template: (data: object): HTMLOptionElement => template: (data: Item): HTMLOptionElement =>
this._templates.option(data), this._templates.option(data),
}); });
} }
@ -337,7 +341,7 @@ class Choices {
(this.passedElement as WrappedSelect).options = this._presetOptions; (this.passedElement as WrappedSelect).options = this._presetOptions;
} }
this._templates = TEMPLATES; this._templates = templates;
this.initialised = false; this.initialised = false;
} }
@ -627,7 +631,7 @@ class Choices {
if (typeof Promise === 'function' && fetcher instanceof Promise) { if (typeof Promise === 'function' && fetcher instanceof Promise) {
// that's a promise // that's a promise
// eslint-disable-next-line compat/compat // eslint-disable-next-line compat/compat
return new Promise(resolve => requestAnimationFrame(resolve)) return new Promise(resolve => requestAnimationFrame(resolve)) // eslint-disable-line compat/compat
.then(() => this._handleLoadingState(true)) .then(() => this._handleLoadingState(true))
.then(() => fetcher) .then(() => fetcher)
.then((data: Choice[]) => .then((data: Choice[]) =>
@ -1294,9 +1298,12 @@ class Choices {
const haystack = this._store.searchableChoices; const haystack = this._store.searchableChoices;
const needle = newValue; const needle = newValue;
const keys = [...this.config.searchFields]; const keys = [...this.config.searchFields];
const options = Object.assign(this.config.fuseOptions, { keys }); const options = Object.assign(this.config.fuseOptions, {
keys,
includeMatches: true,
});
const fuse = new Fuse(haystack, options); const fuse = new Fuse(haystack, options);
const results = fuse.search(needle); const results: Result<Choice>[] = fuse.search(needle) as any[]; // see https://github.com/krisk/Fuse/issues/303
this._currentValue = newValue; this._currentValue = newValue;
this._highlightPosition = 0; this._highlightPosition = 0;
@ -2091,7 +2098,7 @@ class Choices {
} }
} }
_getTemplate<K extends keyof Templates>(template: K, ...args: any): any { _getTemplate(template: string, ...args: any): any {
const { classNames } = this.config; const { classNames } = this.config;
return this._templates[template].call(this, classNames, ...args); return this._templates[template].call(this, classNames, ...args);
@ -2108,7 +2115,7 @@ class Choices {
userTemplates = callbackOnCreateTemplates.call(this, strToEl); userTemplates = callbackOnCreateTemplates.call(this, strToEl);
} }
this._templates = merge(TEMPLATES, userTemplates); this._templates = merge(templates, userTemplates);
} }
_createElements(): void { _createElements(): void {

View file

@ -13,7 +13,7 @@ describe('components/container', () => {
document.body.appendChild(element); document.body.appendChild(element);
instance = new Container({ instance = new Container({
element: document.getElementById('container'), element: document.getElementById('container') as HTMLElement,
classNames: DEFAULT_CLASSNAMES, classNames: DEFAULT_CLASSNAMES,
position: 'auto', position: 'auto',
type: 'text', type: 'text',
@ -383,7 +383,7 @@ describe('components/container', () => {
}); });
afterEach(() => { afterEach(() => {
document.getElementById('wrap-test').remove(); document.getElementById('wrap-test')!.remove();
}); });
it('wraps passed element inside element', () => { it('wraps passed element inside element', () => {
@ -406,7 +406,7 @@ describe('components/container', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(document.getElementById('unwrap-test')); document.body.removeChild(document.getElementById('unwrap-test') as Node);
}); });
it('moves wrapped element outside of element', () => { it('moves wrapped element outside of element', () => {

View file

@ -13,7 +13,6 @@ describe('components/input', () => {
element: choicesElement, element: choicesElement,
type: 'text', type: 'text',
classNames: DEFAULT_CLASSNAMES, classNames: DEFAULT_CLASSNAMES,
placeholderValue: null,
preventPaste: false, preventPaste: false,
}); });
}); });
@ -49,7 +48,7 @@ describe('components/input', () => {
expect(['input', 'paste', 'focus', 'blur']).to.have.members( expect(['input', 'paste', 'focus', 'blur']).to.have.members(
Array.from( Array.from(
{ length: addEventListenerStub.callCount }, { length: addEventListenerStub.callCount },
(v, i) => addEventListenerStub.getCall(i).args[0], (_, i) => addEventListenerStub.getCall(i).args[0],
), ),
); );
}); });

View file

@ -31,7 +31,7 @@ export default class List {
this.element.scrollTop = 0; this.element.scrollTop = 0;
} }
scrollToChildElement(element: Element, direction: 1 | -1): void { scrollToChildElement(element: HTMLElement, direction: 1 | -1): void {
if (!element) { if (!element) {
return; return;
} }

View file

@ -34,10 +34,12 @@ describe('components/wrappedInput', () => {
}); });
describe('inherited methods', () => { describe('inherited methods', () => {
['conceal', 'reveal', 'enable', 'disable'].forEach(method => { const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
describe(method, () => { describe(method, () => {
beforeEach(() => { beforeEach(() => {
stub(WrappedElement.prototype, method); stub(WrappedElement.prototype, method as keyof WrappedElement);
}); });
afterEach(() => { afterEach(() => {

View file

@ -1,5 +1,5 @@
import WrappedElement from './wrapped-element'; import WrappedElement from './wrapped-element';
import { ClassNames, Item } from '../interfaces'; import { ClassNames } from '../interfaces';
export default class WrappedInput extends WrappedElement { export default class WrappedInput extends WrappedElement {
element: HTMLInputElement; element: HTMLInputElement;

View file

@ -32,7 +32,7 @@ describe('components/wrappedSelect', () => {
document.body.appendChild(element); document.body.appendChild(element);
instance = new WrappedSelect({ instance = new WrappedSelect({
element: document.getElementById('target'), element: document.getElementById('target') as HTMLSelectElement,
classNames: DEFAULT_CLASSNAMES, classNames: DEFAULT_CLASSNAMES,
template: spy(Templates.option), template: spy(Templates.option),
}); });
@ -54,9 +54,11 @@ describe('components/wrappedSelect', () => {
}); });
describe('inherited methods', () => { describe('inherited methods', () => {
['conceal', 'reveal', 'enable', 'disable'].forEach(method => { const methods: string[] = ['conceal', 'reveal', 'enable', 'disable'];
methods.forEach(method => {
beforeEach(() => { beforeEach(() => {
stub(WrappedElement.prototype, method); stub(WrappedElement.prototype, method as keyof WrappedElement);
}); });
afterEach(() => { afterEach(() => {

View file

@ -1,5 +1,5 @@
import WrappedElement from './wrapped-element'; import WrappedElement from './wrapped-element';
import { ClassNames, Item, Choice } from '../interfaces'; import { ClassNames, Item } from '../interfaces';
export default class WrappedSelect extends WrappedElement { export default class WrappedSelect extends WrappedElement {
element: HTMLSelectElement; element: HTMLSelectElement;
@ -35,7 +35,7 @@ export default class WrappedSelect extends WrappedElement {
return Array.from(this.element.options); return Array.from(this.element.options);
} }
set options(options: Item[] | Choice[]): void { set options(options: Item[] | HTMLOptionElement[]) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const addOptionToFragment = (data): void => { const addOptionToFragment = (data): void => {
// Create a standard select option // Create a standard select option

View file

@ -21,7 +21,7 @@ export interface Choice {
customProperties?: Record<string, any>; customProperties?: Record<string, any>;
disabled?: boolean; disabled?: boolean;
active?: boolean; active?: boolean;
elementId?: string; elementId?: number;
groupId?: number; groupId?: number;
keyCode?: number; keyCode?: number;
label: string; label: string;
@ -183,64 +183,6 @@ export type ActionType =
| 'RESET_TO' | 'RESET_TO'
| 'SET_IS_LOADING'; | 'SET_IS_LOADING';
export interface Templates {
containerOuter: (
this: Choices,
classNames: ClassNames,
direction: HTMLElement['dir'],
isSelectElement: boolean,
isSelectOneElement: boolean,
searchEnabled: boolean,
passedElementType: PassedElement['type'],
) => HTMLElement;
containerInner: (this: Choices, classNames: ClassNames) => HTMLElement;
itemList: (
this: Choices,
classNames: ClassNames,
isSelectOneElement: boolean,
) => HTMLElement;
placeholder: (
this: Choices,
classNames: ClassNames,
value: string,
) => HTMLElement;
item: (
this: Choices,
classNames: ClassNames,
data: Choice,
removeItemButton: boolean,
) => HTMLElement;
choiceList: (
this: Choices,
classNames: ClassNames,
isSelectOneElement: boolean,
) => HTMLElement;
choiceGroup: (
this: Choices,
classNames: ClassNames,
data: Choice,
) => HTMLElement;
choice: (
this: Choices,
classNames: ClassNames,
data: Choice,
selectText: string,
) => HTMLElement;
input: (
this: Choices,
classNames: ClassNames,
placeholderValue: string,
) => HTMLInputElement;
dropdown: (this: Choices, classNames: ClassNames) => HTMLElement;
notice: (
this: Choices,
classNames: ClassNames,
label: string,
type: '' | 'no-results' | 'no-choices',
) => HTMLElement;
option: (data: object) => HTMLOptionElement;
}
/** Classes added to HTML generated by By default classnames follow the BEM notation. */ /** Classes added to HTML generated by By default classnames follow the BEM notation. */
export interface ClassNames { export interface ClassNames {
/** @default 'choices' */ /** @default 'choices' */
@ -795,9 +737,7 @@ export interface Options {
* *
* @default null * @default null
*/ */
callbackOnCreateTemplates: callbackOnCreateTemplates: ((template: Types.strToEl) => void) | null;
| ((template: Types.strToEl) => Partial<Templates>)
| null;
} }
export interface KeyDownAction { export interface KeyDownAction {
@ -815,9 +755,9 @@ export interface Notice {
} }
export interface State { export interface State {
choices: object[]; choices: Choice[];
groups: object[]; groups: Group[];
items: object[]; items: Item[];
general: { general: {
loading: boolean; loading: boolean;
}; };

View file

@ -1,3 +1,4 @@
/* eslint-disable no-new-wrappers */
import { expect } from 'chai'; import { expect } from 'chai';
import { stub } from 'sinon'; import { stub } from 'sinon';
import { import {
@ -140,19 +141,19 @@ describe('utils', () => {
it('sorts by label alphabetically', () => { it('sorts by label alphabetically', () => {
const values = [ const values = [
{ label: 'The Strokes' }, { value: '0', label: 'The Strokes' },
{ label: 'Arctic Monkeys' }, { value: '0', label: 'Arctic Monkeys' },
{ label: 'Oasis' }, { value: '0', label: 'Oasis' },
{ label: 'Tame Impala' }, { value: '0', label: 'Tame Impala' },
]; ];
const output = values.sort(sortByAlpha); const output = values.sort(sortByAlpha);
expect(output).to.eql([ expect(output).to.eql([
{ label: 'Arctic Monkeys' }, { value: '0', label: 'Arctic Monkeys' },
{ label: 'Oasis' }, { value: '0', label: 'Oasis' },
{ label: 'Tame Impala' }, { value: '0', label: 'Tame Impala' },
{ label: 'The Strokes' }, { value: '0', label: 'The Strokes' },
]); ]);
}); });
}); });
@ -185,12 +186,12 @@ describe('utils', () => {
const fakeElement = { const fakeElement = {
dispatchEvent: stub(), dispatchEvent: stub(),
}; };
const eventType = 'testEvent'; const eventType = 'addItem';
const customArgs = { const customArgs = {
testing: true, testing: true,
}; };
dispatchEvent(fakeElement, eventType, customArgs); dispatchEvent(fakeElement as any, eventType, customArgs);
expect(fakeElement.dispatchEvent.called).to.equal(true); expect(fakeElement.dispatchEvent.called).to.equal(true);
const event = fakeElement.dispatchEvent.lastCall.args[0]; const event = fakeElement.dispatchEvent.lastCall.args[0];

View file

@ -126,7 +126,10 @@ export const sortByAlpha = (
numeric: true, numeric: true,
}); });
export const sortByScore = (a: Choice, b: Choice): number => { export const sortByScore = (
a: Pick<Choice, 'score'>,
b: Pick<Choice, 'score'>,
): number => {
const { score: scoreA = 0 } = a; const { score: scoreA = 0 } = a;
const { score: scoreB = 0 } = b; const { score: scoreB = 0 } = b;

View file

@ -1,21 +1,22 @@
import { expect } from 'chai'; import { expect } from 'chai';
import choices, { defaultState } from './choices'; import choices, { defaultState } from './choices';
import { Choice } from '../interfaces';
describe('reducers/choices', () => { describe('reducers/choices', () => {
it('should return same state when no action matches', () => { it('should return same state when no action matches', () => {
expect(choices(defaultState, {})).to.equal(defaultState); expect(choices(defaultState, {} as any)).to.equal(defaultState);
}); });
describe('when choices do not exist', () => { describe('when choices do not exist', () => {
describe('ADD_CHOICE', () => { describe('ADD_CHOICE', () => {
const value = 'test'; const value = 'test';
const label = 'test'; const label = 'test';
const id = 'test'; const id = 1;
const groupId = 'test'; const groupId = 1;
const disabled = false; const disabled = false;
const elementId = 'test'; const elementId = 1;
const customProperties = 'test'; const customProperties = { test: true };
const placeholder = 'test'; const placeholder = true;
describe('passing expected values', () => { describe('passing expected values', () => {
it('adds choice', () => { it('adds choice', () => {
@ -75,7 +76,7 @@ describe('reducers/choices', () => {
const actualResponse = choices(undefined, { const actualResponse = choices(undefined, {
type: 'ADD_CHOICE', type: 'ADD_CHOICE',
value, value,
label: null, label: undefined,
id, id,
groupId, groupId,
disabled, disabled,
@ -110,7 +111,7 @@ describe('reducers/choices', () => {
const actualResponse = choices(undefined, { const actualResponse = choices(undefined, {
type: 'ADD_CHOICE', type: 'ADD_CHOICE',
value, value,
label: null, label: undefined,
id, id,
groupId, groupId,
disabled, disabled,
@ -178,9 +179,7 @@ describe('reducers/choices', () => {
type: 'FILTER_CHOICES', type: 'FILTER_CHOICES',
results: [ results: [
{ {
item: { item: { id } as Choice,
id,
},
score, score,
}, },
], ],
@ -248,11 +247,10 @@ describe('reducers/choices', () => {
expect(actualResponse).to.eql(expectedResponse); expect(actualResponse).to.eql(expectedResponse);
}); });
it('activates all choices if activateOptions flag passed', () => { it('activates all choices if active flag passed', () => {
const clonedState = state.slice(0); const clonedState = state.slice(0);
const actualResponse = choices(clonedState, { const actualResponse = choices(clonedState, {
type: 'ADD_ITEM', type: 'ADD_ITEM',
activateOptions: true,
active: true, active: true,
}); });
@ -265,7 +263,6 @@ describe('reducers/choices', () => {
const clonedState = state.slice(0); const clonedState = state.slice(0);
const actualResponse = choices(clonedState, { const actualResponse = choices(clonedState, {
type: 'ADD_ITEM', type: 'ADD_ITEM',
activateOptions: false,
choiceId: undefined, choiceId: undefined,
}); });

View file

@ -1,4 +1,4 @@
import { Choice, State } from '../interfaces'; import { Choice } from '../interfaces';
import { import {
AddChoiceAction, AddChoiceAction,
FilterChoicesAction, FilterChoicesAction,
@ -9,11 +9,6 @@ import { AddItemAction, RemoveItemAction } from '../actions/items';
export const defaultState = []; export const defaultState = [];
interface Result {
item: Choice;
score: number;
}
type ActionTypes = type ActionTypes =
| AddChoiceAction | AddChoiceAction
| FilterChoicesAction | FilterChoicesAction
@ -25,7 +20,7 @@ type ActionTypes =
export default function choices( export default function choices(
state: Choice[] = defaultState, state: Choice[] = defaultState,
action: ActionTypes, action: ActionTypes,
): State['choices'] { ): Choice[] {
switch (action.type) { switch (action.type) {
case 'ADD_CHOICE': { case 'ADD_CHOICE': {
const addChoiceAction = action as AddChoiceAction; const addChoiceAction = action as AddChoiceAction;
@ -41,7 +36,6 @@ export default function choices(
score: 9999, score: 9999,
customProperties: addChoiceAction.customProperties, customProperties: addChoiceAction.customProperties,
placeholder: addChoiceAction.placeholder || false, placeholder: addChoiceAction.placeholder || false,
keyCode: null,
}; };
/* /*
@ -49,7 +43,7 @@ export default function choices(
A selected choice has been added to the passed input's value (added as an item) A selected choice has been added to the passed input's value (added as an item)
An active choice appears within the choice dropdown An active choice appears within the choice dropdown
*/ */
return [...state, choice]; return [...state, choice as Choice];
} }
case 'ADD_ITEM': { case 'ADD_ITEM': {
@ -97,17 +91,15 @@ export default function choices(
const choice = obj; const choice = obj;
// Set active state based on whether choice is // Set active state based on whether choice is
// within filtered results // within filtered results
choice.active = filterChoicesAction.results.some( choice.active = filterChoicesAction.results.some(({ item, score }) => {
({ item, score }: Result) => { if (item.id === choice.id) {
if (item.id === choice.id) { choice.score = score;
choice.score = score;
return true; return true;
} }
return false; return false;
}, });
);
return choice; return choice;
}); });

View file

@ -3,7 +3,7 @@ import general, { defaultState } from './general';
describe('reducers/general', () => { describe('reducers/general', () => {
it('should return same state when no action matches', () => { it('should return same state when no action matches', () => {
expect(general(defaultState, {})).to.equal(defaultState); expect(general(defaultState, {} as any)).to.equal(defaultState);
}); });
describe('SET_IS_LOADING', () => { describe('SET_IS_LOADING', () => {

View file

@ -3,13 +3,13 @@ import groups, { defaultState } from './groups';
describe('reducers/groups', () => { describe('reducers/groups', () => {
it('should return same state when no action matches', () => { it('should return same state when no action matches', () => {
expect(groups(defaultState, {})).to.equal(defaultState); expect(groups(defaultState, {} as any)).to.equal(defaultState);
}); });
describe('when groups do not exist', () => { describe('when groups do not exist', () => {
describe('ADD_GROUP', () => { describe('ADD_GROUP', () => {
it('adds group', () => { it('adds group', () => {
const id = '1'; const id = 1;
const value = 'Group one'; const value = 'Group one';
const active = true; const active = true;
const disabled = false; const disabled = false;

View file

@ -11,9 +11,9 @@ describe('reducers/rootReducer', () => {
it('returns expected reducers', () => { it('returns expected reducers', () => {
const state = store.getState(); const state = store.getState();
expect(state.groups).to.equal(groups(undefined, {})); expect(state.groups).to.equal(groups(undefined, {} as any));
expect(state.choices).to.equal(choices(undefined, {})); expect(state.choices).to.equal(choices(undefined, {} as any));
expect(state.items).to.equal(items(undefined, {})); expect(state.items).to.equal(items(undefined, {} as any));
}); });
describe('CLEAR_ALL', () => { describe('CLEAR_ALL', () => {

View file

@ -1,9 +1,10 @@
import { expect } from 'chai'; import { expect } from 'chai';
import items, { defaultState } from './items'; import items, { defaultState } from './items';
import { RemoveItemAction } from '../actions/items';
describe('reducers/items', () => { describe('reducers/items', () => {
it('should return same state when no action matches', () => { it('should return same state when no action matches', () => {
expect(items(defaultState, {})).to.equal(defaultState); expect(items(defaultState, {} as any)).to.equal(defaultState);
}); });
describe('when items do not exist', () => { describe('when items do not exist', () => {
@ -148,7 +149,7 @@ describe('reducers/items', () => {
const actualResponse = items(clonedState, { const actualResponse = items(clonedState, {
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',
id, id,
}); } as RemoveItemAction);
expect(actualResponse).to.eql(expectedResponse); expect(actualResponse).to.eql(expectedResponse);
}); });

View file

@ -33,7 +33,7 @@ describe('reducers/store', () => {
describe('subscribe', () => { describe('subscribe', () => {
it('wraps redux subscribe method', () => { it('wraps redux subscribe method', () => {
const onChange = () => {}; const onChange = (): void => {};
expect(subscribeStub.callCount).to.equal(0); expect(subscribeStub.callCount).to.equal(0);
instance.subscribe(onChange); instance.subscribe(onChange);
expect(subscribeStub.callCount).to.equal(1); expect(subscribeStub.callCount).to.equal(1);

View file

@ -7,7 +7,7 @@ import { strToEl } from './lib/utils';
* @param {HTMLElement} element1 * @param {HTMLElement} element1
* @param {HTMLElement} element2 * @param {HTMLElement} element2
*/ */
function expectEqualElements(element1, element2) { function expectEqualElements(element1, element2): void {
expect(element1.tagName).to.equal(element2.tagName); expect(element1.tagName).to.equal(element2.tagName);
expect(element1.attributes.length).to.equal(element2.attributes.length); expect(element1.attributes.length).to.equal(element2.attributes.length);
expect(Object.keys(element1.dataset)).to.have.members( expect(Object.keys(element1.dataset)).to.have.members(
@ -516,11 +516,10 @@ describe('templates', () => {
}; };
it('returns expected html', () => { it('returns expected html', () => {
const value = 'test';
const expectedOutput = strToEl( const expectedOutput = strToEl(
`<div class="${classes.list} ${classes.listDropdown}" aria-expanded="false"></div>`, `<div class="${classes.list} ${classes.listDropdown}" aria-expanded="false"></div>`,
); );
const actualOutput = templates.dropdown(classes, value); const actualOutput = templates.dropdown(classes);
expectEqualElements(actualOutput, expectedOutput); expectEqualElements(actualOutput, expectedOutput);
}); });

View file

@ -1,26 +1,19 @@
import { import { ClassNames, Item, Choice, Group, PassedElement } from './interfaces';
Templates,
ClassNames,
Item,
Choice,
Group,
PassedElement,
} from './interfaces';
/** /**
* Helpers to create HTML elements used by Choices * Helpers to create HTML elements used by Choices
* Can be overridden by providing `callbackOnCreateTemplates` option * Can be overridden by providing `callbackOnCreateTemplates` option
*/ */
export const TEMPLATES: Templates = { const templates = {
containerOuter( containerOuter(
{ containerOuter }: ClassNames, { containerOuter }: Pick<ClassNames, 'containerOuter'>,
dir: HTMLElement['dir'], dir: HTMLElement['dir'],
isSelectElement: boolean, isSelectElement: boolean,
isSelectOneElement: boolean, isSelectOneElement: boolean,
searchEnabled: boolean, searchEnabled: boolean,
passedElementType: PassedElement['type'], passedElementType: PassedElement['type'],
) { ): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: containerOuter, className: containerOuter,
}); });
@ -48,22 +41,31 @@ export const TEMPLATES: Templates = {
return div; return div;
}, },
containerInner({ containerInner }: ClassNames) { containerInner({
containerInner,
}: Pick<ClassNames, 'containerInner'>): HTMLDivElement {
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: containerInner, className: containerInner,
}); });
}, },
itemList( itemList(
{ list, listSingle, listItems }: ClassNames, {
list,
listSingle,
listItems,
}: Pick<ClassNames, 'list' | 'listSingle' | 'listItems'>,
isSelectOneElement: boolean, isSelectOneElement: boolean,
) { ): HTMLDivElement {
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: `${list} ${isSelectOneElement ? listSingle : listItems}`, className: `${list} ${isSelectOneElement ? listSingle : listItems}`,
}); });
}, },
placeholder({ placeholder }: ClassNames, value: string) { placeholder(
{ placeholder }: Pick<ClassNames, 'placeholder'>,
value: string,
): HTMLDivElement {
return Object.assign(document.createElement('div'), { return Object.assign(document.createElement('div'), {
className: placeholder, className: placeholder,
innerHTML: value, innerHTML: value,
@ -71,7 +73,16 @@ export const TEMPLATES: Templates = {
}, },
item( item(
{ item, button, highlightedState, itemSelectable, placeholder }: ClassNames, {
item,
button,
highlightedState,
itemSelectable,
placeholder,
}: Pick<
ClassNames,
'item' | 'button' | 'highlightedState' | 'itemSelectable' | 'placeholder'
>,
{ {
id, id,
value, value,
@ -83,7 +94,7 @@ export const TEMPLATES: Templates = {
placeholder: isPlaceholder, placeholder: isPlaceholder,
}: Item, }: Item,
removeItemButton: boolean, removeItemButton: boolean,
) { ): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: item, className: item,
innerHTML: label, innerHTML: label,
@ -133,7 +144,10 @@ export const TEMPLATES: Templates = {
return div; return div;
}, },
choiceList({ list }: ClassNames, isSelectOneElement: boolean) { choiceList(
{ list }: Pick<ClassNames, 'list'>,
isSelectOneElement: boolean,
): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: list, className: list,
}); });
@ -147,9 +161,13 @@ export const TEMPLATES: Templates = {
}, },
choiceGroup( choiceGroup(
{ group, groupHeading, itemDisabled }: ClassNames, {
group,
groupHeading,
itemDisabled,
}: Pick<ClassNames, 'group' | 'groupHeading' | 'itemDisabled'>,
{ id, value, disabled }: Group, { id, value, disabled }: Group,
) { ): HTMLDivElement {
const div = Object.assign(document.createElement('div'), { const div = Object.assign(document.createElement('div'), {
className: `${group} ${disabled ? itemDisabled : ''}`, className: `${group} ${disabled ? itemDisabled : ''}`,
}); });
@ -184,7 +202,15 @@ export const TEMPLATES: Templates = {
selectedState, selectedState,
itemDisabled, itemDisabled,
placeholder, placeholder,
}: ClassNames, }: Pick<
ClassNames,
| 'item'
| 'itemChoice'
| 'itemSelectable'
| 'selectedState'
| 'itemDisabled'
| 'placeholder'
>,
{ {
id, id,
value, value,
@ -233,7 +259,7 @@ export const TEMPLATES: Templates = {
}, },
input( input(
{ input, inputCloned }: ClassNames, { input, inputCloned }: Pick<ClassNames, 'input' | 'inputCloned'>,
placeholderValue: string, placeholderValue: string,
): HTMLInputElement { ): HTMLInputElement {
const inp = Object.assign(document.createElement('input'), { const inp = Object.assign(document.createElement('input'), {
@ -251,7 +277,10 @@ export const TEMPLATES: Templates = {
return inp; return inp;
}, },
dropdown({ list, listDropdown }: ClassNames): HTMLDivElement { dropdown({
list,
listDropdown,
}: Pick<ClassNames, 'list' | 'listDropdown'>): HTMLDivElement {
const div = document.createElement('div'); const div = document.createElement('div');
div.classList.add(list, listDropdown); div.classList.add(list, listDropdown);
@ -261,7 +290,12 @@ export const TEMPLATES: Templates = {
}, },
notice( notice(
{ item, itemChoice, noResults, noChoices }: ClassNames, {
item,
itemChoice,
noResults,
noChoices,
}: Pick<ClassNames, 'item' | 'itemChoice' | 'noResults' | 'noChoices'>,
innerHTML: string, innerHTML: string,
type: 'no-choices' | 'no-results' | '' = '', type: 'no-choices' | 'no-results' | '' = '',
): HTMLDivElement { ): HTMLDivElement {
@ -298,4 +332,4 @@ export const TEMPLATES: Templates = {
}, },
}; };
export default TEMPLATES; export default templates;