Browse Source

Update dependencies, fix linting issues, split interfaces and default objects to resolve dependency cycles

pull/977/head
Matt Triff 1 month ago
parent
commit
3d921621b7
  1. 15
      .eslintrc.json
  2. 82
      cypress/integration/select-multiple.spec.ts
  3. 35561
      package-lock.json
  4. 93
      package.json
  5. 3
      public/assets/styles/base.css
  6. 2
      public/assets/styles/base.min.css
  7. 5
      public/assets/styles/choices.css
  8. 2
      public/assets/styles/choices.min.css
  9. 2
      src/scripts/actions/choices.ts
  10. 2
      src/scripts/actions/misc.test.ts
  11. 2
      src/scripts/actions/misc.ts
  12. 45
      src/scripts/choices.test.ts
  13. 156
      src/scripts/choices.ts
  14. 2
      src/scripts/components/container.test.ts
  15. 20
      src/scripts/components/container.ts
  16. 2
      src/scripts/components/dropdown.test.ts
  17. 10
      src/scripts/components/dropdown.ts
  18. 2
      src/scripts/components/input.test.ts
  19. 12
      src/scripts/components/input.ts
  20. 2
      src/scripts/components/list.ts
  21. 4
      src/scripts/components/wrapped-element.test.ts
  22. 7
      src/scripts/components/wrapped-element.ts
  23. 4
      src/scripts/components/wrapped-input.test.ts
  24. 3
      src/scripts/components/wrapped-input.ts
  25. 8
      src/scripts/components/wrapped-select.test.ts
  26. 7
      src/scripts/components/wrapped-select.ts
  27. 12
      src/scripts/constants.test.ts
  28. 89
      src/scripts/constants.ts
  29. 79
      src/scripts/defaults.ts
  30. 12
      src/scripts/interfaces/action-type.ts
  31. 17
      src/scripts/interfaces/choice.ts
  32. 87
      src/scripts/interfaces/choices.ts
  33. 55
      src/scripts/interfaces/class-names.ts
  34. 11
      src/scripts/interfaces/event-type.ts
  35. 8
      src/scripts/interfaces/group.ts
  36. 6
      src/scripts/interfaces/item.ts
  37. 11
      src/scripts/interfaces/keycode-map.ts
  38. 5
      src/scripts/interfaces/notice.ts
  39. 295
      src/scripts/interfaces/options.ts
  40. 1
      src/scripts/interfaces/passed-element-type.ts
  41. 138
      src/scripts/interfaces/passed-element.ts
  42. 1
      src/scripts/interfaces/position-options-type.ts
  43. 10
      src/scripts/interfaces/state.ts
  44. 13
      src/scripts/interfaces/types.ts
  45. 2
      src/scripts/lib/utils.test.ts
  46. 20
      src/scripts/lib/utils.ts
  47. 4
      src/scripts/reducers/choices.test.ts
  48. 15
      src/scripts/reducers/choices.ts
  49. 7
      src/scripts/reducers/groups.ts
  50. 2
      src/scripts/reducers/items.test.ts
  51. 15
      src/scripts/reducers/items.ts
  52. 6
      src/scripts/reducers/loading.ts
  53. 20
      src/scripts/store/store.test.ts
  54. 27
      src/scripts/store/store.ts
  55. 10
      src/scripts/templates.ts

15
.eslintrc.json

@ -3,6 +3,7 @@
"plugins": ["@typescript-eslint", "prettier", "sort-class-members"],
"extends": [
"airbnb-base",
"airbnb-typescript",
"plugin:prettier/recommended",
"plugin:compat/recommended",
"plugin:@typescript-eslint/recommended"
@ -61,7 +62,8 @@
}
],
"lines-between-class-members": "off",
"@typescript-eslint/no-namespace": "off"
"@typescript-eslint/no-namespace": "off",
"react/jsx-filename-extension": [0]
},
"overrides": [
{
@ -75,7 +77,16 @@
"no-new": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "default",
"format": ["camelCase", "PascalCase", "UPPER_CASE"],
"leadingUnderscore": "allow"
}
]
}
},
{

82
cypress/integration/select-multiple.spec.ts

@ -62,7 +62,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.then($choice => {
.then(($choice) => {
selectedChoiceText = $choice.text().trim();
})
.click();
@ -72,7 +72,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--multiple .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain(selectedChoiceText);
});
});
@ -80,7 +80,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
expect($select.val()).to.contain(selectedChoiceText);
});
});
@ -89,7 +89,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown .choices__list')
.children()
.each($choice => {
.each(($choice) => {
expect($choice.text().trim()).to.not.equal(selectedChoiceText);
});
});
@ -114,7 +114,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal('No choices to choose from');
});
@ -130,7 +130,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.last()
.then($choice => {
.then(($choice) => {
removedChoiceText = $choice.text().trim();
})
.click();
@ -151,7 +151,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
const val = $select.val() || [];
expect(val).to.not.contain(removedChoiceText);
});
@ -171,7 +171,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 2');
});
});
@ -187,7 +187,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 3');
});
});
@ -202,7 +202,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=basic]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal('No results found');
});
@ -346,10 +346,10 @@ describe('Choices - select multiple', () => {
describe('selection limit', () => {
/*
{
maxItemCount: 5,
}
*/
{
maxItemCount: 5,
}
*/
const selectionLimit = 5;
beforeEach(() => {
@ -370,7 +370,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=selection-limit]')
.find('.choices__list--dropdown')
.should('be.visible')
.should($dropdown => {
.should(($dropdown) => {
const dropdownText = $dropdown.text().trim();
expect(dropdownText).to.equal(
`Only ${selectionLimit} values can be added`,
@ -397,7 +397,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.last()
.then($choice => {
.then(($choice) => {
selectedChoiceText = $choice.text().trim();
})
.click();
@ -407,7 +407,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.data('value')).to.equal(
`before-${selectedChoiceText}-after`,
);
@ -418,7 +418,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=prepend-append]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text()).to.not.contain(
`before-${selectedChoiceText}-after`,
);
@ -460,7 +460,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.not.contain(searchTerm);
});
});
@ -478,7 +478,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.contain(searchTerm);
});
});
@ -565,13 +565,15 @@ describe('Choices - select multiple', () => {
});
describe('dropdown scrolling', () => {
let choicesCount;
let choicesCount: number;
// let choicesItems: number[];
beforeEach(() => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .choices__item')
.then($choices => {
.then(($choices) => {
choicesCount = $choices.length;
// choicesItems = Array.from({ length: 10 }, (_, i) => i + 1);
});
cy.get('[data-test-hook=scrolling-dropdown]')
@ -582,7 +584,7 @@ describe('Choices - select multiple', () => {
it('highlights first choice on dropdown open', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('Choice 1');
});
});
@ -593,7 +595,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(`Choice ${index + 1}`);
});
@ -617,7 +619,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=scrolling-dropdown]')
.find('.choices__list--dropdown .choices__list .is-highlighted')
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(`Choice ${index}`);
});
@ -636,7 +638,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.then($group => {
.then(($group) => {
groupValue = $group.text().trim();
});
});
@ -657,7 +659,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.should($group => {
.should(($group) => {
expect($group.text().trim()).to.not.equal(groupValue);
});
});
@ -688,7 +690,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=groups]')
.find('.choices__list--dropdown .choices__list .choices__group')
.first()
.should($group => {
.should(($group) => {
expect($group.text().trim()).to.equal(groupValue);
});
});
@ -728,7 +730,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(city);
});
@ -742,9 +744,7 @@ describe('Choices - select multiple', () => {
describe('non-string values', () => {
beforeEach(() => {
cy.get('[data-test-hook=non-string-values]')
.find('.choices')
.click();
cy.get('[data-test-hook=non-string-values]').find('.choices').click();
});
it('displays expected amount of choices in dropdown', () => {
@ -760,7 +760,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.then($choice => {
.then(($choice) => {
$selectedChoice = $choice;
})
.click();
@ -768,7 +768,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=non-string-values]')
.find('.choices__list--single .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item.text().trim()).to.equal($selectedChoice.text().trim());
});
});
@ -778,7 +778,7 @@ describe('Choices - select multiple', () => {
describe('selecting choice', () => {
describe('on enter key', () => {
it('selects choice', () => {
cy.get('[data-test-hook=within-form] form').then($form => {
cy.get('[data-test-hook=within-form] form').then(($form) => {
$form.submit(() => {
// this will fail the test if the form submits
throw new Error('Form submitted');
@ -793,7 +793,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=within-form]')
.find('.choices__list--multiple .choices__item')
.last()
.should($item => {
.should(($item) => {
expect($item).to.contain('Choice 1');
});
});
@ -808,7 +808,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__list--multiple .choices__item')
.last()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal(
dynamicallySelectedChoiceValue,
);
@ -819,7 +819,7 @@ describe('Choices - select multiple', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__list--dropdown .choices__list')
.children()
.each($choice => {
.each(($choice) => {
expect($choice.text().trim()).to.not.equal(
dynamicallySelectedChoiceValue,
);
@ -829,7 +829,7 @@ describe('Choices - select multiple', () => {
it('updates the value of the original input', () => {
cy.get('[data-test-hook=set-choice-by-value]')
.find('.choices__input[hidden]')
.should($select => {
.should(($select) => {
const val = $select.val() || [];
expect(val).to.contain(dynamicallySelectedChoiceValue);
});
@ -846,7 +846,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('No results found');
});
});
@ -860,7 +860,7 @@ describe('Choices - select multiple', () => {
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should($choice => {
.should(($choice) => {
expect($choice.text().trim()).to.equal('label1');
});
});

35561
package-lock.json

File diff suppressed because it is too large

93
package.json

@ -13,7 +13,7 @@
"cypress:open": "cypress open",
"cypress:ci": "cypress run --record --group $GITHUB_REF --ci-build-id $GITHUB_SHA",
"test": "run-s test:unit test:e2e",
"test:unit": "TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha",
"test:unit": "cross-env TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha",
"test:unit:watch": "npm run test:unit -- --watch --inspect=5556",
"test:unit:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha",
"test:e2e": "run-p --race start cypress:run",
@ -53,56 +53,57 @@
"js"
],
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"@babel/register": "^7.6.2",
"@types/chai": "^4.2.7",
"@types/mocha": "^5.2.7",
"@types/sinon": "^7.5.1",
"@types/sinon-chai": "^3.2.3",
"@typescript-eslint/eslint-plugin": "^2.11.0",
"@typescript-eslint/parser": "^2.11.0",
"autoprefixer": "^9.6.5",
"babel-loader": "^8.0.6",
"bundlesize": "^0.18.0",
"chai": "^4.2.0",
"cross-env": "^6.0.3",
"@babel/core": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@babel/register": "^7.16.5",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/sinon": "^10.0.6",
"@types/sinon-chai": "^3.2.6",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"autoprefixer": "^10.4.0",
"babel-loader": "^8.2.3",
"bundlesize": "^0.18.1",
"chai": "^4.3.4",
"cross-env": "^7.0.3",
"csso-cli": "^3.0.0",
"cypress": "3.6.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.5.0",
"cypress": "9.1.1",
"eslint": "^8.4.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.3.0",
"eslint-loader": "^3.0.2",
"eslint-plugin-compat": "3.3.0",
"eslint-plugin-cypress": "^2.8.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-sort-class-members": "^1.6.0",
"express": "^4.16.4",
"husky": "^3.0.9",
"jsdom": "^15.2.0",
"lint-staged": "^9.4.2",
"mocha": "^6.2.2",
"node-sass": "^4.12.0",
"nodemon": "^1.18.10",
"eslint-plugin-compat": "4.0.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-sort-class-members": "^1.14.1",
"express": "^4.17.1",
"husky": "^7.0.4",
"jsdom": "^19.0.0",
"lint-staged": "^12.1.2",
"mocha": "^9.1.3",
"node-sass": "^7.0.0",
"nodemon": "^2.0.15",
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"postcss-cli": "^6.1.3",
"prettier": "^1.19.1",
"sinon": "^7.5.0",
"sinon-chai": "^3.3.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.4",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.9",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0"
"nyc": "^15.1.0",
"postcss-cli": "^9.1.0",
"prettier": "^2.5.1",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"typescript": "^4.5.4",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-dev-middleware": "^5.2.2",
"webpack-hot-middleware": "^2.25.1"
},
"dependencies": {
"deepmerge": "^4.2.0",
"deepmerge": "^4.2.2",
"fuse.js": "^3.4.6",
"redux": "^4.0.4"
"redux": "^4.1.2"
},
"npmName": "choices.js",
"npmFileMap": [
@ -125,7 +126,7 @@
"bundlesize": [
{
"path": "public/assets/scripts/choices.min.js",
"maxSize": "20 kB"
"maxSize": "25 kB"
},
{
"path": "public/assets/styles/choices.min.css",

3
public/assets/styles/base.css

@ -78,8 +78,7 @@ a:focus {
border-radius: 2.5px;
font-size: 14px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
appearance: none;
margin-bottom: 24px;
}

2
public/assets/styles/base.min.css

@ -1 +1 @@
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:'Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}label+p{margin-top:-4px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.push-bottom{margin-bottom:24px}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}.text-center{text-align:center}[data-test-hook]{margin-bottom:24px}
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:'Helvetica Neue',Helvetica,Arial,'Lucida Grande',sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label,p{margin-bottom:8px}label{font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:30px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}label+p{margin-top:-4px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.push-bottom{margin-bottom:24px}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}.text-center{text-align:center}[data-test-hook]{margin-bottom:24px}

5
public/assets/styles/choices.css

@ -25,7 +25,6 @@
background-color: #eaeaea;
cursor: not-allowed;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
@ -320,7 +319,6 @@
.choices__item--disabled {
cursor: not-allowed;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: 0.5;
}
@ -336,8 +334,7 @@
.choices__button {
text-indent: -9999px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
appearance: none;
border: 0;
background-color: transparent;
background-repeat: no-repeat;

2
public/assets/styles/choices.min.css

File diff suppressed because one or more lines are too long

2
src/scripts/actions/choices.ts

@ -1,5 +1,5 @@
import { ACTION_TYPES } from '../constants';
import { Choice } from '../interfaces';
import { Choice } from '../interfaces/choice';
export interface AddChoiceAction {
type: typeof ACTION_TYPES.ADD_CHOICE;

2
src/scripts/actions/misc.test.ts

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { State } from '../interfaces/state';
import * as actions from './misc';
import { State } from '../interfaces';
describe('actions/misc', () => {
describe('clearAll action', () => {

2
src/scripts/actions/misc.ts

@ -1,5 +1,5 @@
import { State } from '../interfaces';
import { ACTION_TYPES } from '../constants';
import { State } from '../interfaces/state';
export interface ClearAllAction {
type: typeof ACTION_TYPES.CLEAR_ALL;

45
src/scripts/choices.test.ts

@ -4,11 +4,14 @@ import sinonChai from 'sinon-chai';
import Choices from './choices';
import { EVENTS, ACTION_TYPES, DEFAULT_CONFIG, KEY_CODES } from './constants';
import { EVENTS, ACTION_TYPES, KEY_CODES } from './constants';
import { WrappedSelect, WrappedInput } from './components/index';
import { removeItem } from './actions/items';
import { Item, Choice, Group } from './interfaces';
import templates from './templates';
import { Choice } from './interfaces/choice';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { DEFAULT_CONFIG } from './defaults';
chai.use(sinonChai);
@ -563,21 +566,21 @@ describe('choices', () => {
expect(output).to.eql(instance);
});
it('opens containerOuter', done => {
it('opens containerOuter', (done) => {
requestAnimationFrame(() => {
expect(containerOuterOpenSpy.called).to.equal(true);
done();
});
});
it('shows dropdown with blurInput flag', done => {
it('shows dropdown with blurInput flag', (done) => {
requestAnimationFrame(() => {
expect(dropdownShowSpy.called).to.equal(true);
done();
});
});
it('triggers event on passedElement', done => {
it('triggers event on passedElement', (done) => {
requestAnimationFrame(() => {
expect(passedElementTriggerEventStub.called).to.equal(true);
expect(passedElementTriggerEventStub.lastCall.args[0]).to.eql(
@ -595,7 +598,7 @@ describe('choices', () => {
output = instance.showDropdown(true);
});
it('focuses input', done => {
it('focuses input', (done) => {
requestAnimationFrame(() => {
expect(inputFocusSpy.called).to.equal(true);
done();
@ -661,21 +664,21 @@ describe('choices', () => {
expect(output).to.eql(instance);
});
it('closes containerOuter', done => {
it('closes containerOuter', (done) => {
requestAnimationFrame(() => {
expect(containerOuterCloseSpy.called).to.equal(true);
done();
});
});
it('hides dropdown with blurInput flag', done => {
it('hides dropdown with blurInput flag', (done) => {
requestAnimationFrame(() => {
expect(dropdownHideSpy.called).to.equal(true);
done();
});
});
it('triggers event on passedElement', done => {
it('triggers event on passedElement', (done) => {
requestAnimationFrame(() => {
expect(passedElementTriggerEventStub.called).to.equal(true);
expect(passedElementTriggerEventStub.lastCall.args[0]).to.eql(
@ -693,14 +696,14 @@ describe('choices', () => {
output = instance.hideDropdown(true);
});
it('removes active descendants', done => {
it('removes active descendants', (done) => {
requestAnimationFrame(() => {
expect(inputRemoveActiveDescendantSpy.called).to.equal(true);
done();
});
});
it('blurs input', done => {
it('blurs input', (done) => {
requestAnimationFrame(() => {
expect(inputBlurSpy.called).to.equal(true);
done();
@ -1192,7 +1195,8 @@ describe('choices', () => {
const fetcher = async (inst): Promise<Choice[]> => {
expect(inst).to.eq(choice);
fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 800));
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 800));
return [
{ label: 'l1', value: 'v1', customProperties: { prop1: true } },
@ -1381,7 +1385,7 @@ describe('choices', () => {
});
it('returns all active item values', () => {
expect(output).to.eql(items.map(item => item.value));
expect(output).to.eql(items.map((item) => item.value));
});
});
});
@ -1612,7 +1616,8 @@ describe('choices', () => {
instance.clearChoices = clearChoicesStub;
instance._addGroup = addGroupStub;
instance._addChoice = addChoiceStub;
instance.containerOuter.removeLoadingState = containerOuterRemoveLoadingStateStub;
instance.containerOuter.removeLoadingState =
containerOuterRemoveLoadingStateStub;
});
afterEach(() => {
@ -2093,7 +2098,7 @@ describe('choices', () => {
KEY_CODES.PAGE_DOWN_KEY,
];
keyCodes.forEach(keyCode => {
keyCodes.forEach((keyCode) => {
it(`calls _onDirectionKey with the expected arguments`, () => {
const event = {
keyCode,
@ -2143,7 +2148,7 @@ describe('choices', () => {
describe('delete key', () => {
const keyCodes = [KEY_CODES.DELETE_KEY, KEY_CODES.BACK_KEY];
keyCodes.forEach(keyCode => {
keyCodes.forEach((keyCode) => {
it(`calls _onDeleteKey with the expected arguments`, () => {
const event = {
keyCode,
@ -2188,10 +2193,10 @@ describe('choices', () => {
);
});
it('triggers a REMOVE_ITEM event on the passed element', done => {
it('triggers a REMOVE_ITEM event on the passed element', (done) => {
passedElement.addEventListener(
'removeItem',
event => {
(event) => {
expect(event.detail).to.eql({
id: item.id,
value: item.value,
@ -2226,10 +2231,10 @@ describe('choices', () => {
instance._store.getGroupById.reset();
});
it("includes the group's value in the triggered event", done => {
it("includes the group's value in the triggered event", (done) => {
passedElement.addEventListener(
'removeItem',
event => {
(event) => {
expect(event.detail).to.eql({
id: itemWithGroup.id,
value: itemWithGroup.value,

156
src/scripts/choices.ts

@ -1,56 +1,55 @@
import merge from 'deepmerge';
/* eslint-disable @typescript-eslint/no-explicit-any */
import Fuse from 'fuse.js';
import merge from 'deepmerge';
import Store from './store/store';
import {
Dropdown,
activateChoices,
addChoice,
clearChoices,
filterChoices,
Result,
} from './actions/choices';
import { addGroup } from './actions/groups';
import { addItem, highlightItem, removeItem } from './actions/items';
import { clearAll, resetTo, setIsLoading } from './actions/misc';
import {
Container,
Dropdown,
Input,
List,
WrappedInput,
WrappedSelect,
} from './components';
import {
DEFAULT_CONFIG,
EVENTS,
KEY_CODES,
TEXT_TYPE,
SELECT_ONE_TYPE,
SELECT_MULTIPLE_TYPE,
SELECT_ONE_TYPE,
TEXT_TYPE,
} from './constants';
import templates from './templates';
import {
addChoice,
filterChoices,
activateChoices,
clearChoices,
Result,
} from './actions/choices';
import { addItem, removeItem, highlightItem } from './actions/items';
import { addGroup } from './actions/groups';
import { clearAll, resetTo, setIsLoading } from './actions/misc';
import { DEFAULT_CONFIG } from './defaults';
import { Choice } from './interfaces/choice';
import { Group } from './interfaces/group';
import { Item } from './interfaces/item';
import { Notice } from './interfaces/notice';
import { Options } from './interfaces/options';
import { PassedElement } from './interfaces/passed-element';
import { State } from './interfaces/state';
import {
isScrolledIntoView,
diff,
existsInArray,
generateId,
getAdjacentEl,
getType,
isScrolledIntoView,
isType,
strToEl,
sortByScore,
generateId,
existsInArray,
diff,
strToEl,
} from './lib/utils';
import {
Options,
Choice,
Item,
Group,
Notice,
State,
PassedElement,
} from './interfaces';
import { defaultState } from './reducers';
import Store from './store/store';
import templates from './templates';
/** @see {@link http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c} */
const IS_IE11 =
@ -63,7 +62,7 @@ const USER_DEFAULTS: Partial<Options> = {};
* Choices
* @author Josh Johnson<josh@joshuajohnson.co.uk>
*/
class Choices {
class Choices implements Choices {
static get defaults(): {
options: Partial<Options>;
templates: typeof templates;
@ -79,39 +78,69 @@ class Choices {
}
initialised: boolean;
config: Options;
passedElement: WrappedInput | WrappedSelect;
containerOuter: Container;
containerInner: Container;
choiceList: List;
itemList: List;
input: Input;
dropdown: Dropdown;
_isTextElement: boolean;
_isSelectOneElement: boolean;
_isSelectMultipleElement: boolean;
_isSelectElement: boolean;
_store: Store;
_templates: typeof templates;
_initialState: State;
_currentState: State;
_prevState: State;
_currentValue: string;
_canSearch: boolean;
_isScrollingOnIe: boolean;
_highlightPosition: number;
_wasTap: boolean;
_isSearching: boolean;
_placeholderValue: string | null;
_baseId: string;
_direction: HTMLElement['dir'];
_idNames: {
itemChoice: string;
};
_presetGroups: Group[] | HTMLOptGroupElement[] | Element[];
_presetOptions: Item[] | HTMLOptionElement[];
_presetChoices: Partial<Choice>[];
_presetItems: Item[] | string[];
constructor(
@ -247,7 +276,7 @@ class Choices {
}
// Create array of choices from option elements
if ((this.passedElement as WrappedSelect).options) {
(this.passedElement as WrappedSelect).options.forEach(option => {
(this.passedElement as WrappedSelect).options.forEach((option) => {
this._presetChoices.push({
value: option.value,
label: option.innerHTML,
@ -415,21 +444,21 @@ class Choices {
}
highlightAll(): this {
this._store.items.forEach(item => this.highlightItem(item));
this._store.items.forEach((item) => this.highlightItem(item));
return this;
}
unhighlightAll(): this {
this._store.items.forEach(item => this.unhighlightItem(item));
this._store.items.forEach((item) => this.unhighlightItem(item));
return this;
}
removeActiveItemsByValue(value: string): this {
this._store.activeItems
.filter(item => item.value === value)
.forEach(item => this._removeItem(item));
.filter((item) => item.value === value)
.forEach((item) => this._removeItem(item));
return this;
}
@ -437,13 +466,13 @@ class Choices {
removeActiveItems(excludedId: number): this {
this._store.activeItems
.filter(({ id }) => id !== excludedId)
.forEach(item => this._removeItem(item));
.forEach((item) => this._removeItem(item));
return this;
}
removeHighlightedItems(runEvent = false): this {
this._store.highlightedActiveItems.forEach(item => {
this._store.highlightedActiveItems.forEach((item) => {
this._removeItem(item);
// If this action was performed by the user
// trigger the event
@ -513,7 +542,7 @@ class Choices {
return this;
}
items.forEach(value => this._setChoiceOrItem(value));
items.forEach((value) => this._setChoiceOrItem(value));
return this;
}
@ -527,7 +556,7 @@ class Choices {
const choiceValue = Array.isArray(value) ? value : [value];
// Loop through each value and
choiceValue.forEach(val => this._findAndSelectChoiceByValue(val));
choiceValue.forEach((val) => this._findAndSelectChoiceByValue(val));
return this;
}
@ -630,14 +659,14 @@ class Choices {
if (typeof Promise === 'function' && fetcher instanceof Promise) {
// that's a promise
// eslint-disable-next-line compat/compat
return new Promise(resolve => requestAnimationFrame(resolve)) // eslint-disable-line compat/compat
// eslint-disable-next-line no-promise-executor-return
return new Promise((resolve) => requestAnimationFrame(resolve))
.then(() => this._handleLoadingState(true))
.then(() => fetcher)
.then((data: Choice[]) =>
this.setChoices(data, value, label, replaceChoices),
)
.catch(err => {
.catch((err) => {
if (!this.config.silent) {
console.error(err);
}
@ -766,7 +795,7 @@ class Choices {
if (activeGroups.length >= 1 && !this._isSearching) {
// If we have a placeholder choice along with groups
const activePlaceholders = activeChoices.filter(
activeChoice =>
(activeChoice) =>
activeChoice.placeholder === true && activeChoice.groupId === -1,
);
if (activePlaceholders.length >= 1) {
@ -849,7 +878,7 @@ class Choices {
fragment: DocumentFragment = document.createDocumentFragment(),
): DocumentFragment {
const getGroupChoices = (group): Choice[] =>
choices.filter(choice => {
choices.filter((choice) => {
if (this._isSelectOneElement) {
return choice.groupId === group.id;
}
@ -865,7 +894,7 @@ class Choices {
groups.sort(this.config.sorter);
}
groups.forEach(group => {
groups.forEach((group) => {
const groupChoices = getGroupChoices(group);
if (groupChoices.length >= 1) {
const dropdownGroup = this._getTemplate('choiceGroup', group);
@ -883,11 +912,8 @@ class Choices {
withinGroup = false,
): DocumentFragment {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const {
renderSelectedChoices,
searchResultLimit,
renderChoiceLimit,
} = this.config;
const { renderSelectedChoices, searchResultLimit, renderChoiceLimit } =
this.config;
const filter = this._isSearching ? sortByScore : this.config.sorter;
const appendChoice = (choice: Choice): void => {
const shouldRender =
@ -909,7 +935,7 @@ class Choices {
let rendererableChoices = choices;
if (renderSelectedChoices === 'auto' && !this._isSelectOneElement) {
rendererableChoices = choices.filter(choice => !choice.selected);
rendererableChoices = choices.filter((choice) => !choice.selected);
}
// Split array into placeholders and "normal" choices
@ -1027,7 +1053,7 @@ class Choices {
const itemId =
element.parentNode && (element.parentNode as HTMLElement).dataset.id;
const itemToRemove =
itemId && activeItems.find(item => item.id === parseInt(itemId, 10));
itemId && activeItems.find((item) => item.id === parseInt(itemId, 10));
if (!itemToRemove) {
return;
@ -1061,7 +1087,7 @@ class Choices {
// We only want to select one item with a click
// so we deselect any items that aren't the target
// unless shift is being pressed
activeItems.forEach(item => {
activeItems.forEach((item) => {
if (item.id === parseInt(`${passedId}`, 10) && !item.highlighted) {
this.highlightItem(item);
} else if (!hasShiftKey && item.highlighted) {
@ -1132,7 +1158,7 @@ class Choices {
}
const lastItem = activeItems[activeItems.length - 1];
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const hasHighlightedItems = activeItems.some((item) => item.highlighted);
// 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
@ -1204,7 +1230,7 @@ class Choices {
const { choices } = this._store;
const { searchFloor, searchChoices } = this.config;
const hasUnactiveChoices = choices.some(option => !option.active);
const hasUnactiveChoices = choices.some((option) => !option.active);
// Check that we have a value to search and the input was an alphanumeric character
if (value && value.length >= searchFloor) {
@ -1800,7 +1826,7 @@ class Choices {
if (blurWasWithinContainer && !this._isScrollingOnIe) {
const { activeItems } = this._store;
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const hasHighlightedItems = activeItems.some((item) => item.highlighted);
const blurActions = {
[TEXT_TYPE]: (): void => {
if (target === this.input.element) {
@ -1862,7 +1888,7 @@ class Choices {
);
// Remove any highlighted choices
highlightedChoices.forEach(choice => {
highlightedChoices.forEach((choice) => {
choice.classList.remove(this.config.classNames.highlightedState);
choice.setAttribute('aria-selected', 'false');
});
@ -2214,7 +2240,7 @@ class Choices {
});
}
groups.forEach(group =>
groups.forEach((group) =>
this._addGroup({
group,
id: group.id || null,
@ -2228,9 +2254,9 @@ class Choices {
choices.sort(this.config.sorter);
}
const hasSelectedChoice = choices.some(choice => choice.selected);
const hasSelectedChoice = choices.some((choice) => choice.selected);
const firstEnabledChoiceIndex = choices.findIndex(
choice => choice.disabled === undefined || !choice.disabled,
(choice) => choice.disabled === undefined || !choice.disabled,
);
choices.forEach((choice, index) => {
@ -2258,8 +2284,6 @@ class Choices {
const isSelected = shouldPreselect ? true : choice.selected;
const isDisabled = choice.disabled;
console.log(isDisabled, choice);
this._addChoice({
value,
label,
@ -2283,7 +2307,7 @@ class Choices {
}
_addPredefinedItems(items: Item[] | string[]): void {
items.forEach(item => {
items.forEach((item) => {
if (typeof item === 'object' && item.value) {
this._addItem({
value: item.value,
@ -2353,7 +2377,7 @@ class Choices {
_findAndSelectChoiceByValue(value: string): void {
const { choices } = this._store;
// Check 'value' property exists and the choice isn't already selected
const foundChoice = choices.find(choice =>
const foundChoice = choices.find((choice) =>
this.config.valueComparer(choice.value, value),
);

2
src/scripts/components/container.test.ts

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import Container from './container';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/container', () => {
let instance;

20
src/scripts/components/container.ts

@ -1,16 +1,26 @@
import { wrap } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
import { PassedElement, ClassNames, Options } from '../interfaces';
import { ClassNames } from '../interfaces/class-names';
import { PositionOptionsType } from '../interfaces/position-options-type';
import { PassedElementType } from '../interfaces/passed-element-type';
export default class Container {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
position: Options['position'];
position: PositionOptionsType;
isOpen: boolean;
isFlipped: boolean;
isFocussed: boolean;
isDisabled: boolean;
isLoading: boolean;
constructor({
@ -20,9 +30,9 @@ export default class Container {
position,
}: {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
position: Options['position'];
position: PositionOptionsType;
}) {
this.element = element;
this.classNames = classNames;

2
src/scripts/components/dropdown.test.ts

@ -1,7 +1,7 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import Dropdown from './dropdown';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/dropdown', () => {
let instance;

10
src/scripts/components/dropdown.ts

@ -1,9 +1,13 @@
import { PassedElement, ClassNames } from '../interfaces';
import { ClassNames } from '../interfaces/class-names';
import { PassedElementType } from '../interfaces/passed-element-type';
export default class Dropdown {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
isActive: boolean;
constructor({
@ -12,7 +16,7 @@ export default class Dropdown {
classNames,
}: {
element: HTMLElement;
type: PassedElement['type'];
type: PassedElementType;
classNames: ClassNames;
}) {
this.element = element;

2
src/scripts/components/input.test.ts

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import { DEFAULT_CLASSNAMES } from '../defaults';
import Input from './input';
import { DEFAULT_CLASSNAMES } from '../constants';
describe('components/input', () => {
let instance;

12
src/scripts/components/input.ts

@ -1,13 +1,19 @@
import { sanitise } from '../lib/utils';
import { SELECT_ONE_TYPE } from '../constants';
import { PassedElement, ClassNames } from '../interfaces';
import { ClassNames } from '../interfaces/class-names';
import { PassedElementType } from '../interfaces/passed-element-type';
export default class Input {
element: HTMLInputElement;
type: PassedElement['type'];