* Housekeeping

* Resolve placeholder bug + hide from choice list

* Restructure test folder

* Update cypress test to assert one placeholder

* Fix breaking e2e test

* Remove ability to pass placeholder via config for select boxes

* Add further e2e tests covering placeholders

* Add unit tests for _generatePlaceholderValue

* Display placeholder choice for select one

* Add further e2e test to assert on placeholder ordering

* Add labels to exclude from draft releases

* Add failure case to e2e test workflow

* Resolve broken e2e test

* Update puppeteer snapshot baseline
This commit is contained in:
Josh Johnson 2019-11-02 13:49:33 +00:00 committed by GitHub
parent 939a73b762
commit a0fe05f926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 398 additions and 70 deletions

BIN
.github/actions-scripts/__snapshots__/puppeteer-darwin.png vendored Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -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:

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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
<select>

View File

@ -1,6 +1,6 @@
describe('Choices - select multiple', () => {
beforeEach(() => {
cy.visit('/select-multiple.html');
cy.visit('/select-multiple');
});
describe('scenarios', () => {
@ -486,20 +486,42 @@ describe('Choices - select multiple', () => {
});
});
describe('placeholder', () => {
/*
{
placeholder: true,
placeholderValue: 'I am a placeholder',
}
*/
describe('placeholder via empty option value', () => {
describe('when no value has been inputted', () => {
it('displays a placeholder', () => {
cy.get('[data-test-hook=placeholder]')
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__input--cloned')
.should('have.attr', 'placeholder', 'I am a placeholder');
});
});
describe('when a value has been inputted', () => {
it('does not display a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__input--cloned')
.type('test')
.should('not.have.value', 'I am a placeholder');
});
});
});
describe('placeholder via option attribute', () => {
describe('when no value has been inputted', () => {
it('displays a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__input--cloned')
.should('have.attr', 'placeholder', 'I am a placeholder');
});
});
describe('when a value has been inputted', () => {
it('does not display a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__input--cloned')
.type('test')
.should('not.have.value', 'I am a placeholder');
});
});
});
describe('remote data', () => {

View File

@ -1,6 +1,6 @@
describe('Choices - select one', () => {
beforeEach(() => {
cy.visit('/select-one.html');
cy.visit('/select-one');
});
describe('scenarios', () => {
@ -448,6 +448,102 @@ describe('Choices - select one', () => {
});
});
describe('placeholder via empty option value', () => {
describe('when no choice has been selected', () => {
it('displays a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__list--single')
.children()
.first()
.should('have.class', 'choices__placeholder')
.and($placeholder => {
expect($placeholder).to.contain('I am a placeholder');
});
});
});
describe('when a choice has been selected', () => {
it('does not display a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__input--cloned')
.focus();
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.click();
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__input--cloned')
.should('not.have.value', 'I am a placeholder');
});
});
describe('when choice list is open', () => {
it('displays the placeholder choice first', () => {
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__input--cloned')
.focus();
cy.get('[data-test-hook=placeholder-via-option-value]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should('have.class', 'choices__placeholder')
.should('have.text', 'I am a placeholder');
});
});
});
describe('placeholder via option attribute', () => {
describe('when no choice has been selected', () => {
it('displays a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__list--single')
.children()
.first()
.should('have.class', 'choices__placeholder')
.and($placeholder => {
expect($placeholder).to.contain('I am a placeholder');
});
});
});
describe('when a choice has been selected', () => {
it('does not display a placeholder', () => {
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__input--cloned')
.focus();
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.click();
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__input--cloned')
.should('not.have.value', 'I am a placeholder');
});
});
describe('when choice list is open', () => {
it('displays the placeholder choice first', () => {
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__input--cloned')
.focus();
cy.get('[data-test-hook=placeholder-via-option-attr]')
.find('.choices__list--dropdown .choices__list')
.children()
.first()
.should('have.class', 'choices__placeholder')
.should('have.text', 'I am a placeholder');
});
});
});
describe('remote data', () => {
beforeEach(() => {
cy.reload(true);
@ -458,6 +554,7 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=remote-data]')
.find('.choices__list--single')
.children()
.should('have.length', 1)
.first()
.should('have.class', 'choices__placeholder')
.and($placeholder => {
@ -483,10 +580,14 @@ describe('Choices - select one', () => {
cy.get('[data-test-hook=remote-data]')
.find('.choices__list--dropdown .choices__list')
.children()
.should('have.length', 50)
.should('have.length', 51) // 50 choices + 1 placeholder choice
.each(($choice, index) => {
expect($choice.text().trim()).to.equal(`Label ${index + 1}`);
expect($choice.data('value')).to.equal(`Value ${index + 1}`);
if (index === 0) {
expect($choice.text().trim()).to.equal('I am a placeholder');
} else {
expect($choice.text().trim()).to.equal(`Label ${index}`);
expect($choice.data('value')).to.equal(`Value ${index}`);
}
});
});
});

View File

@ -1,6 +1,6 @@
describe('Choices - text element', () => {
beforeEach(() => {
cy.visit('/text.html');
cy.visit('/text');
});
describe('scenarios', () => {

View File

@ -218,7 +218,7 @@
}
.choices__list--dropdown {
display: none;
visibility: hidden;
z-index: 1;
position: absolute;
width: 100%;
@ -230,10 +230,11 @@
border-bottom-right-radius: 2.5px;
overflow: hidden;
word-break: break-all;
will-change: visibility;
}
.choices__list--dropdown.is-active {
display: block;
visibility: visible;
}
.is-open .choices__list--dropdown {

File diff suppressed because one or more lines are too long

View File

@ -15,43 +15,46 @@
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
href="../../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
href="../../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
href="../../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link rel="manifest" href="../../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
href="../../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<link rel="shortcut icon" href="../../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
content="../../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<link
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<!-- End ignore these -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
@ -194,14 +197,34 @@
</select>
</div>
<div data-test-hook="placeholder">
<label for="choices-placeholder">Placeholder</label>
<div data-test-hook="placeholder-via-option-value">
<label for="choices-placeholder-via-option-value"
>Placeholder via empty option value</label
>
<select
class="form-control"
name="choices-placeholder"
id="choices-placeholder"
name="choices-placeholder-via-option-value"
id="choices-placeholder-via-option-value"
multiple
>
<option value="">I am a placeholder</option>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
</select>
</div>
<div data-test-hook="placeholder-via-option-attr">
<label for="choices-placeholder-via-option-attr"
>Placeholder via option attribute</label
>
<select
class="form-control"
name="choices-placeholder-via-option-attr"
id="choices-placeholder-via-option-attr"
multiple
>
<option placeholder>I am a placeholder</option>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -215,7 +238,9 @@
name="choices-remote-data"
id="choices-remote-data"
multiple
></select>
>
<option value="">I am a placeholder</option>
</select>
</div>
<div data-test-hook="scrolling-dropdown">
@ -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,

View File

@ -15,43 +15,46 @@
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
href="../../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
href="../../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
href="../../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link rel="manifest" href="../../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
href="../../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<link rel="shortcut icon" href="../../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
content="../../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<link
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<!-- End ignore these -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
@ -178,13 +181,47 @@
</select>
</div>
<div data-test-hook="placeholder-via-option-value">
<label for="choices-placeholder-via-option-value"
>Placeholder via empty option value</label
>
<select
class="form-control"
name="choices-placeholder-via-option-value"
id="choices-placeholder-via-option-value"
>
<option value="">I am a placeholder</option>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
</select>
</div>
<div data-test-hook="placeholder-via-option-attr">
<label for="choices-placeholder-via-option-attr"
>Placeholder via option attribute</label
>
<select
class="form-control"
name="choices-placeholder-via-option-attr"
id="choices-placeholder-via-option-attr"
>
<option placeholder>I am a placeholder</option>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
</select>
</div>
<div data-test-hook="remote-data">
<label for="choices-remote-data">Remote data</label>
<select
class="form-control"
name="choices-remote-data"
id="choices-remote-data"
></select>
>
<option value="">I am a placeholder</option>
</select>
</div>
<div data-test-hook="scrolling-dropdown">
@ -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 () => {

View File

@ -15,43 +15,46 @@
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
href="../../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
href="../../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
href="../../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link rel="manifest" href="../../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
href="../../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<link rel="shortcut icon" href="../../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
content="../../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<link
rel="stylesheet"
href="../../assets/styles/base.min.css?version=6.0.3"
/>
<!-- End ignore these -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
href="../../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<script src="../../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>

View File

@ -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 ====== */

View File

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

View File

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