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);