diff --git a/.github/actions-scripts/__snapshots__/puppeteer-darwin.png b/.github/actions-scripts/__snapshots__/puppeteer-darwin.png old mode 100644 new mode 100755 index cc58dea..61d2554 Binary files a/.github/actions-scripts/__snapshots__/puppeteer-darwin.png and b/.github/actions-scripts/__snapshots__/puppeteer-darwin.png differ diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 5b74d19..0d888cb 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,6 +1,9 @@ name-template: 'Draft (next release)' tag-template: 'v$NEXT_PATCH_VERSION' sort-direction: descending +exclude-labels: + - 'skip-changelog' + - 'release' categories: - title: '🚨 Breaking changes' labels: diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 16132f3..4ae6bce 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -29,7 +29,7 @@ jobs: env: HUSKY_SKIP_INSTALL: true - - name: run Cypress CI + - name: run Cypress (with recording) run: npx run-p --race start cypress:ci env: CI: true @@ -41,3 +41,18 @@ jobs: COMMIT_INFO_BRANCH: ${{ github.head_ref }} COMMIT_INFO_AUTHOR: ${{ github.event.sender.login }} COMMIT_INFO_SHA: ${{ github.event.after }} + + # if we have ran out of free Cypress recordings, run Cypress with recording switched off + - name: run Cypress (without recording) + if: failure() + run: npx run-p --race start cypress:run + env: + CI: true + TERM: xterm-256color + NODE_ENV: production # prevent watching + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + DEBUG: commit-info,cypress:server:record + # https://docs.cypress.io/guides/guides/continuous-integration.html#Environment-variables + COMMIT_INFO_BRANCH: ${{ github.head_ref }} + COMMIT_INFO_AUTHOR: ${{ github.event.sender.login }} + COMMIT_INFO_SHA: ${{ github.event.after }} diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-drafter.yml similarity index 70% rename from .github/workflows/release-management.yml rename to .github/workflows/release-drafter.yml index f264373..0631111 100644 --- a/.github/workflows/release-management.yml +++ b/.github/workflows/release-drafter.yml @@ -1,4 +1,4 @@ -name: Release management +name: Release drafter on: push: @@ -9,6 +9,6 @@ jobs: update-draft-release: runs-on: ubuntu-latest steps: - - uses: toolmantim/release-drafter@v5.2.0 + - uses: toolmantim/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 3bce930..3b1a2eb 100644 --- a/README.md +++ b/README.md @@ -431,11 +431,11 @@ const example = new Choices(element, { **Type:** `Boolean` **Default:** `true` -**Input types affected:** `text`, `select-multiple` +**Input types affected:** `text` **Usage:** Whether the input should show a placeholder. Used in conjunction with `placeholderValue`. If `placeholder` is set to true and no value is passed to `placeholderValue`, the passed input's placeholder attribute will be used as the placeholder value. -**Note:** For single select boxes, the recommended way of adding a placeholder is as follows: +**Note:** For select boxes, the recommended way of adding a placeholder is as follows: ```html -
- +
+ +
+ +
+ + + > + +
@@ -373,10 +398,9 @@ searchFloor: 5, }); - new Choices('#choices-placeholder', { - placeholder: true, - placeholderValue: 'I am a placeholder', - }); + new Choices('#choices-placeholder-via-option-value'); + + new Choices('#choices-placeholder-via-option-attr'); new Choices('#choices-remote-data', { shouldSort: false, diff --git a/public/test/select-one.html b/public/test/select-one/index.html similarity index 86% rename from public/test/select-one.html rename to public/test/select-one/index.html index b4881b4..93e3ba5 100644 --- a/public/test/select-one.html +++ b/public/test/select-one/index.html @@ -15,43 +15,46 @@ - + - + - + - + @@ -178,13 +181,47 @@
+
+ + +
+ +
+ + +
+
+ > + +
@@ -359,6 +396,10 @@ searchFloor: 5, }); + new Choices('#choices-placeholder-via-option-value'); + + new Choices('#choices-placeholder-via-option-attr'); + new Choices('#choices-remote-data', { shouldSort: false, }).setChoices(async () => { diff --git a/public/test/text.html b/public/test/text/index.html similarity index 89% rename from public/test/text.html rename to public/test/text/index.html index 1040403..cb86435 100644 --- a/public/test/text.html +++ b/public/test/text/index.html @@ -15,43 +15,46 @@ - + - + - + - + diff --git a/src/scripts/choices.js b/src/scripts/choices.js index 1c16a01..166c90d 100644 --- a/src/scripts/choices.js +++ b/src/scripts/choices.js @@ -149,6 +149,7 @@ class Choices { * @type {HTMLElement['dir']} */ this._direction = this.passedElement.element.dir; + if (!this._direction) { const { direction: elementDirection } = window.getComputedStyle( this.passedElement.element, @@ -160,6 +161,7 @@ class Choices { this._direction = elementDirection; } } + this._idNames = { itemChoice: 'item-choice', }; @@ -849,7 +851,9 @@ class Choices { let choiceLimit = rendererableChoices.length; // Prepend placeholeder - const sortedChoices = [...placeholderChoices, ...normalChoices]; + const sortedChoices = this._isSelectOneElement + ? [...placeholderChoices, ...normalChoices] + : normalChoices; if (this._isSearching) { choiceLimit = searchResultLimit; @@ -2075,7 +2079,7 @@ class Choices { label: o.innerHTML, selected: o.selected, disabled: o.disabled || o.parentNode.disabled, - placeholder: o.hasAttribute('placeholder'), + placeholder: o.value === '' || o.hasAttribute('placeholder'), customProperties: o.getAttribute('data-custom-properties'), }); }); @@ -2224,14 +2228,28 @@ class Choices { } _generatePlaceholderValue() { - if (this._isSelectOneElement) { - return false; + if (this._isSelectElement) { + const { placeholderOption } = this.passedElement; + + return placeholderOption ? placeholderOption.text : false; } - return this.config.placeholder - ? this.config.placeholderValue || - this.passedElement.element.getAttribute('placeholder') - : false; + const { placeholder, placeholderValue } = this.config; + const { + element: { dataset }, + } = this.passedElement; + + if (placeholder) { + if (placeholderValue) { + return placeholderValue; + } + + if (dataset.placeholder) { + return dataset.placeholder; + } + } + + return false; } /* ===== End of Private functions ====== */ diff --git a/src/scripts/choices.test.js b/src/scripts/choices.test.js index cdf9522..1689b7d 100644 --- a/src/scripts/choices.test.js +++ b/src/scripts/choices.test.js @@ -1873,5 +1873,96 @@ describe('choices', () => { }); }); }); + + describe('_generatePlaceholderValue', () => { + describe('select element', () => { + describe('when a placeholder option is defined', () => { + it('returns the text value of the placeholder option', () => { + const placeholderValue = 'I am a placeholder'; + + instance._isSelectElement = true; + instance.passedElement.placeholderOption = { + text: placeholderValue, + }; + + const value = instance._generatePlaceholderValue(); + expect(value).to.equal(placeholderValue); + }); + }); + + describe('when a placeholder option is not defined', () => { + it('returns false', () => { + instance._isSelectElement = true; + instance.passedElement.placeholderOption = undefined; + + const value = instance._generatePlaceholderValue(); + expect(value).to.equal(false); + }); + }); + }); + + describe('text input', () => { + describe('when the placeholder config option is set to true', () => { + describe('when the placeholderValue config option is defined', () => { + it('returns placeholderValue', () => { + const placeholderValue = 'I am a placeholder'; + + instance._isSelectElement = false; + instance.config.placeholder = true; + instance.config.placeholderValue = placeholderValue; + + const value = instance._generatePlaceholderValue(); + expect(value).to.equal(placeholderValue); + }); + }); + + describe('when the placeholderValue config option is not defined', () => { + describe('when the placeholder attribute is defined on the passed element', () => { + it('returns the value of the placeholder attribute', () => { + const placeholderValue = 'I am a placeholder'; + + instance._isSelectElement = false; + instance.config.placeholder = true; + instance.config.placeholderValue = undefined; + instance.passedElement.element = { + dataset: { + placeholder: placeholderValue, + }, + }; + + const value = instance._generatePlaceholderValue(); + expect(value).to.equal(placeholderValue); + }); + }); + + describe('when the placeholder attribute is not defined on the passed element', () => { + it('returns false', () => { + instance._isSelectElement = false; + instance.config.placeholder = true; + instance.config.placeholderValue = undefined; + instance.passedElement.element = { + dataset: { + placeholder: undefined, + }, + }; + + const value = instance._generatePlaceholderValue(); + expect(value).to.equal(false); + }); + }); + }); + }); + + describe('when the placeholder config option is set to false', () => { + it('returns false', () => { + instance._isSelectElement = false; + instance.config.placeholder = false; + + const value = instance._generatePlaceholderValue(); + expect(value).to.equal(false); + }); + }); + }); + }); }); }); diff --git a/src/scripts/templates.js b/src/scripts/templates.js index a20788e..b955200 100644 --- a/src/scripts/templates.js +++ b/src/scripts/templates.js @@ -39,16 +39,19 @@ export const TEMPLATES = /** @type {Templates} */ ({ return div; }, + containerInner({ containerInner }) { return Object.assign(document.createElement('div'), { className: containerInner, }); }, + itemList({ list, listSingle, listItems }, isSelectOneElement) { return Object.assign(document.createElement('div'), { className: `${list} ${isSelectOneElement ? listSingle : listItems}`, }); }, + placeholder({ placeholder }, value) { return Object.assign(document.createElement('div'), { className: placeholder, @@ -93,6 +96,7 @@ export const TEMPLATES = /** @type {Templates} */ ({ if (isPlaceholder) { div.classList.add(placeholder); } + div.classList.add(highlighted ? highlightedState : itemSelectable); if (removeItemButton) { @@ -117,6 +121,7 @@ export const TEMPLATES = /** @type {Templates} */ ({ return div; }, + choiceList({ list }, isSelectOneElement) { const div = Object.assign(document.createElement('div'), { className: list, @@ -196,6 +201,7 @@ export const TEMPLATES = /** @type {Templates} */ ({ return div; }, + input({ input, inputCloned }, placeholderValue) { const inp = Object.assign(document.createElement('input'), { type: 'text', @@ -211,6 +217,7 @@ export const TEMPLATES = /** @type {Templates} */ ({ return inp; }, + dropdown({ list, listDropdown }) { const div = document.createElement('div'); @@ -219,6 +226,7 @@ export const TEMPLATES = /** @type {Templates} */ ({ return div; }, + notice({ item, itemChoice, noResults, noChoices }, innerHTML, type = '') { const classes = [item, itemChoice]; @@ -233,6 +241,7 @@ export const TEMPLATES = /** @type {Templates} */ ({ className: classes.join(' '), }); }, + option({ label, value, customProperties, active, disabled }) { const opt = new Option(label, value, false, active);