diff --git a/README.md b/README.md index 4f49648..12a6160 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Or include Choices directly: choices: [], renderChoiceLimit: -1, maxItemCount: -1, + addChoices: false, addItems: true, addItemFilter: null, removeItems: true, @@ -304,6 +305,15 @@ Pass an array of objects: **Usage:** The amount of items a user can input/select ("-1" indicates no limit). +### addChoices +**Type**: `Boolean` **Default:** `false` + +**Input types affected:** `select-multiple`, `select-one` + +**Usage:** Whether a user can add choices + +**Note:** `addItems` must also be `true` + ### addItems **Type:** `Boolean` **Default:** `true` diff --git a/cypress/e2e/select-multiple.spec.ts b/cypress/e2e/select-multiple.spec.ts index c35f781..bade601 100644 --- a/cypress/e2e/select-multiple.spec.ts +++ b/cypress/e2e/select-multiple.spec.ts @@ -1006,6 +1006,24 @@ describe('Choices - select multiple', () => { }); }); }); + + describe('adding user-created choices', () => { + it('allows the user to add choices', () => { + const newChoice = 'New Choice'; + + cy.get('[data-test-hook=add-choices]') + .find('.choices__input--cloned') + .type(newChoice) + .type('{enter}'); + + cy.get('[data-test-hook=add-choices]') + .find('.choices__list--multiple') + .last() + .should($el => { + expect($el).to.contain(newChoice); + }); + }); + }); }); }); }); diff --git a/cypress/e2e/select-one.spec.ts b/cypress/e2e/select-one.spec.ts index 6809dd8..6d5b7cf 100644 --- a/cypress/e2e/select-one.spec.ts +++ b/cypress/e2e/select-one.spec.ts @@ -1145,5 +1145,26 @@ describe('Choices - select one', () => { .should('have.length', 3); }); }); + + describe('adding user-created choices', () => { + beforeEach(() => { + cy.get('[data-test-hook=add-choices]').find('.choices').click(); + }); + + it('allows the user to add choices', () => { + const newChoice = 'New Choice'; + + cy.get('[data-test-hook=add-choices]') + .find('.choices__input--cloned') + .type(newChoice) + .type('{enter}'); + + cy.get('[data-test-hook=add-choices]') + .find('.choices__list--single .choices__item') + .should(($el) => { + expect($el).to.contain(newChoice); + }); + }); + }); }); }); diff --git a/public/test/select-multiple/index.html b/public/test/select-multiple/index.html index e4091e5..5057019 100644 --- a/public/test/select-multiple/index.html +++ b/public/test/select-multiple/index.html @@ -397,6 +397,15 @@ multiple > + +
+ + +
diff --git a/public/test/select-one/index.html b/public/test/select-one/index.html index c0aea2d..020096b 100644 --- a/public/test/select-one/index.html +++ b/public/test/select-one/index.html @@ -417,6 +417,16 @@ + +
+ + +
+ diff --git a/src/scripts/choices.ts b/src/scripts/choices.ts index 36a51da..26b2a4b 100644 --- a/src/scripts/choices.ts +++ b/src/scripts/choices.ts @@ -825,12 +825,12 @@ class Choices implements Choices { ); } - // If we have choices to show + const { activeItems } = this._store; // If we have choices to show + if ( choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0 ) { - const { activeItems } = this._store; const canAddItem = this._canAddItem(activeItems, this.input.value); // ...and we can select them @@ -844,18 +844,21 @@ class Choices implements Choices { } } else { // Otherwise show a notice - let dropdownItem; - let notice; + const canAddChoice = this._canAddChoice(activeItems, this.input.value); - if (this._isSearching) { - notice = + let dropdownItem; + + if (canAddChoice.response) { + dropdownItem = this._getTemplate('notice', canAddChoice.notice); + } else if (this._isSearching) { + const notice = typeof this.config.noResultsText === 'function' ? this.config.noResultsText() : this.config.noResultsText; dropdownItem = this._getTemplate('notice', notice, 'no-results'); } else { - notice = + const notice = typeof this.config.noChoicesText === 'function' ? this.config.noChoicesText() : this.config.noChoicesText; @@ -1260,6 +1263,14 @@ class Choices implements Choices { } } + _canAddChoice(activeItems: Item[], value: string): Notice { + const canAddItem = this._canAddItem(activeItems, value); + + canAddItem.response = this.config.addChoices && canAddItem.response; + + return canAddItem; + } + _canAddItem(activeItems: Item[], value: string): Notice { let canAddItem = true; let notice = @@ -1555,16 +1566,22 @@ class Choices implements Choices { const { ENTER_KEY: enterKey } = KEY_CODES; const targetWasButton = target && (target as HTMLElement).hasAttribute('data-button'); + let addedItem = false; - if (this._isTextElement && target && (target as HTMLInputElement).value) { + if (target && (target as HTMLInputElement).value) { const { value } = this.input; const canAddItem = this._canAddItem(activeItems, value); + const canAddChoice = this._canAddChoice(activeItems, value); - if (canAddItem.response) { + if ( + (this._isTextElement && canAddItem.response) || + (!this._isTextElement && canAddChoice.response) + ) { this.hideDropdown(true); this._addItem({ value }); this._triggerChange(value); this.clearInput(); + addedItem = true; } } @@ -1579,11 +1596,15 @@ class Choices implements Choices { ); if (highlightedChoice) { - // add enter keyCode value - if (activeItems[0]) { - activeItems[0].keyCode = enterKey; // eslint-disable-line no-param-reassign + if (addedItem) { + this.unhighlightAll(); + } else { + if (activeItems[0]) { + // add enter keyCode value + activeItems[0].keyCode = enterKey; // eslint-disable-line no-param-reassign + } + this._handleChoiceAction(activeItems, highlightedChoice); } - this._handleChoiceAction(activeItems, highlightedChoice); } event.preventDefault(); diff --git a/src/scripts/defaults.ts b/src/scripts/defaults.ts index 488cb22..c3b37fb 100644 --- a/src/scripts/defaults.ts +++ b/src/scripts/defaults.ts @@ -37,6 +37,7 @@ export const DEFAULT_CONFIG: Options = { silent: false, renderChoiceLimit: -1, maxItemCount: -1, + addChoices: false, addItems: true, addItemFilter: null, removeItems: true, diff --git a/src/scripts/interfaces/options.ts b/src/scripts/interfaces/options.ts index 4a4e22f..f3bba0a 100644 --- a/src/scripts/interfaces/options.ts +++ b/src/scripts/interfaces/options.ts @@ -102,6 +102,15 @@ export interface Options { */ maxItemCount: number; + /** + * Whether a user can add choices dynamically. + * + * **Input types affected:** select-one, select-multiple + * + * @default false + */ + addChoices: boolean; + /** * Whether a user can add items. *