Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add internal has-input-value-changed event and reset on programmatic value clear (CP: 14) #1044

Merged
merged 12 commits into from
May 8, 2023
29 changes: 28 additions & 1 deletion src/vaadin-combo-box-mixin.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@
notify: true
},

/**
* Whether the input element has a non-empty value.
* @protected
*/
_hasInputValue: {
type: Boolean,
value: false,
observer: '_hasInputValueChanged',
},

/**
* Path for label of the item. If `items` is an array of objects, the
* `itemLabelPath` is used to fetch the displayed string label for each
Expand Down Expand Up @@ -265,7 +275,8 @@
'_templateOrRendererChanged(_itemTemplate, renderer)',
'_loadingChanged(loading)',
'_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
'_toggleElementChanged(_toggleElement)'
'_toggleElementChanged(_toggleElement)',
'_inputElementValueChanged(_inputElementValue)'
];
}

Expand Down Expand Up @@ -636,6 +647,7 @@
this.selectedItem = null;

if (this.allowCustomValue) {
this._inputElementValue = '';
this.value = '';
}

Expand Down Expand Up @@ -767,6 +779,10 @@
}
}

_inputElementValueChanged(inputElementValue) {
this._hasInputValue = inputElementValue != null ? inputElementValue.length > 0 : false;
}

/** @private */
_filterFromInput(e) {
if (!this.opened && !e.__fromClearButton && !this.autoOpenDisabled) {
Expand Down Expand Up @@ -1093,6 +1109,17 @@
this._clear();
}

/**
* Observer to notify about the change of private property.
*
* @private
*/
_hasInputValueChanged(hasValue, oldHasValue) {
if (hasValue || oldHasValue) {
this.dispatchEvent(new CustomEvent('has-input-value-changed'));
}
}

/**
* @param {boolean} invalid
* @protected
Expand Down
152 changes: 152 additions & 0 deletions test/events.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<!doctype html>
<html>

<head>
<meta charset="UTF-8">
<title>vaadin-combo-box validation tests</title>
vursen marked this conversation as resolved.
Show resolved Hide resolved

<script src="../../web-component-tester/browser.js"></script>
<script src='../../webcomponentsjs/webcomponents-lite.js'></script>
vursen marked this conversation as resolved.
Show resolved Hide resolved
<link rel="import" href="../../test-fixture/test-fixture.html">
<script src="../../iron-test-helpers/mock-interactions.js"></script>
<link rel='import' href='common-imports.html'>
vursen marked this conversation as resolved.
Show resolved Hide resolved
<script src="common.js"></script>
</head>

<body>
<test-fixture id="combobox">
vursen marked this conversation as resolved.
Show resolved Hide resolved
<template>
<vaadin-combo-box></vaadin-combo-box>
</template>
</test-fixture>
<script>
(ios ? describe.skip : describe)('events', () => {
function inputChar(target, char) {
target.value += char;
MockInteractions.keyDownOn(target, char.charCodeAt(0));
target.dispatchEvent(new CustomEvent('input', {bubbles: true, composed: true}));
}

function inputText(target, text) {
for (var i = 0; i < text.length; i++) {
inputChar(target, text[i]);
}
}

function arrowDown(target) {
MockInteractions.keyDownOn(target, 40);
}

function enter(target) {
MockInteractions.pressEnter(target);
}

function space(target) {
MockInteractions.pressSpace(target);
}

function esc(target) {
MockInteractions.keyDownOn(target, 27, null, 'Escape');
}

describe('has-input-value-changed event', () => {
let comboBox, input, clearButton, hasInputValueChangedSpy, valueChangedSpy;

beforeEach(async() => {
hasInputValueChangedSpy = sinon.spy();
valueChangedSpy = sinon.spy();
comboBox = fixture('combobox');
vursen marked this conversation as resolved.
Show resolved Hide resolved
comboBox.allowCustomValue = true;
input = comboBox.inputElement;
vursen marked this conversation as resolved.
Show resolved Hide resolved
comboBox.addEventListener('has-input-value-changed', hasInputValueChangedSpy);
comboBox.addEventListener('value-changed', valueChangedSpy);
input.focus();
});

describe('without value', () => {

vursen marked this conversation as resolved.
Show resolved Hide resolved
describe('with user input', () => {
beforeEach(async() => {
inputText(input, 'foo');
hasInputValueChangedSpy.reset();
valueChangedSpy.reset();
});

it('should be fired when clearing the user input with Esc', async() => {
expect(comboBox.inputElement.value).to.equal('foo');
vursen marked this conversation as resolved.
Show resolved Hide resolved
// Clear selection in the dropdown.
esc(input);
// Clear the user input.
esc(input);
expect(comboBox.inputElement.value).to.be.empty;
vursen marked this conversation as resolved.
Show resolved Hide resolved
expect(hasInputValueChangedSpy.calledOnce).to.be.true;
});

it('should not fire the event when modifying input', async() => {
input.value = 'foobar';
input.dispatchEvent(new CustomEvent('input'));
expect(hasInputValueChangedSpy.called).to.be.false;
});

it('should fire the event once when removing input', async() => {
input.value = '';
input.dispatchEvent(new CustomEvent('input'));
expect(hasInputValueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledBefore(valueChangedSpy)).to.be.true;
});

it('should fire the event once on programmatic clear', async() => {
comboBox._clear();
expect(hasInputValueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledBefore(valueChangedSpy)).to.be.true;
});
});

describe('without user input', () => {
it('should fire the event once when entering input', async() => {
input.value = 'foo';
expect(hasInputValueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledBefore(valueChangedSpy)).to.be.true;
});

it('should not fire the event on programmatic clear', async() => {
comboBox._clear();
expect(hasInputValueChangedSpy.called).to.be.false;
});
});
});

describe('with value', () => {
beforeEach(async() => {
comboBox.clearButtonVisible = true;
clearButton = comboBox.inputElement.$.clearButton;
inputText(input, 'foo');
enter(input);
valueChangedSpy.reset();
hasInputValueChangedSpy.reset();
});

it('should be fired on clear button click', () => {
expect(comboBox.inputElement.value).to.equal('foo');
clearButton.click();
expect(comboBox.inputElement.value).to.be.empty;
expect(valueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledBefore(valueChangedSpy)).to.be.true;
});

it('should be fired when clearing the value with Esc', async() => {
esc(input);
expect(comboBox.inputElement.value).to.be.empty;
expect(valueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledOnce).to.be.true;
expect(hasInputValueChangedSpy.calledBefore(valueChangedSpy)).to.be.true;
});
});
});
});
</script>

vursen marked this conversation as resolved.
Show resolved Hide resolved
</body>

vursen marked this conversation as resolved.
Show resolved Hide resolved
</html>
3 changes: 2 additions & 1 deletion test/test-suites.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ window.VaadinComboBoxSuites = [
'item-template.html',
'vaadin-combo-box-dropdown.html',
'lazy-loading.html',
'validation.html'
'validation.html',
'events.html'
];

if (isPolymer2) {
Expand Down