Merge pull request #647 from ing-bank/fix/model-value-event-consistent
Fix/model value event consistent
This commit is contained in:
commit
5879e499fa
21 changed files with 746 additions and 58 deletions
|
|
@ -55,20 +55,7 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
constructor() {
|
||||
super();
|
||||
this.multipleChoice = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.multipleChoice) {
|
||||
this.addEventListener('model-value-changed', this._checkSingleChoiceElements);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (!this.multipleChoice) {
|
||||
this.removeEventListener('model-value-changed', this._checkSingleChoiceElements);
|
||||
}
|
||||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -157,9 +144,10 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
}
|
||||
}
|
||||
|
||||
__triggerCheckedValueChanged() {
|
||||
__setChoiceGroupTouched() {
|
||||
const value = this.modelValue;
|
||||
if (value != null && value !== this.__previousCheckedValue) {
|
||||
// TODO: what happens here exactly? Needs to be based on user interaction (?)
|
||||
this.touched = true;
|
||||
this.__previousCheckedValue = value;
|
||||
}
|
||||
|
|
@ -179,5 +167,24 @@ export const ChoiceGroupMixin = dedupeMixin(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override FormControlMixin
|
||||
*/
|
||||
_onBeforeRepropagateChildrenValues(ev) {
|
||||
// Normalize target, since we might receive 'portal events' (from children in a modal,
|
||||
// see select-rich)
|
||||
const target = (ev.detail && ev.detail.element) || ev.target;
|
||||
if (this.multipleChoice || !target.checked) {
|
||||
return;
|
||||
}
|
||||
this.formElements.forEach(option => {
|
||||
if (target.choiceValue !== option.choiceValue) {
|
||||
option.checked = false; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
});
|
||||
this.__setChoiceGroupTouched();
|
||||
this.requestUpdate('modelValue');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { FormGroupMixin } from '@lion/fieldset';
|
|||
import '@lion/fieldset/lion-fieldset.js';
|
||||
import { LionInput } from '@lion/input';
|
||||
import { Required } from '@lion/validate';
|
||||
import { expect, fixture, nextFrame } from '@open-wc/testing';
|
||||
import { expect, nextFrame } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/field/test-helpers.js';
|
||||
import { ChoiceGroupMixin } from '../src/ChoiceGroupMixin.js';
|
||||
import { ChoiceInputMixin } from '../src/ChoiceInputMixin.js';
|
||||
|
||||
|
|
@ -194,21 +195,21 @@ describe('ChoiceGroupMixin', () => {
|
|||
counter = 0; // reset after setup which may result in different results
|
||||
|
||||
el.formElements[0].checked = true;
|
||||
expect(counter).to.equal(2); // male becomes checked, female becomes unchecked
|
||||
expect(counter).to.equal(1); // male becomes checked, female becomes unchecked
|
||||
|
||||
// not changed values trigger no event
|
||||
el.formElements[0].checked = true;
|
||||
expect(counter).to.equal(2);
|
||||
expect(counter).to.equal(1);
|
||||
|
||||
el.formElements[2].checked = true;
|
||||
expect(counter).to.equal(4); // other becomes checked, male becomes unchecked
|
||||
expect(counter).to.equal(2); // other becomes checked, male becomes unchecked
|
||||
|
||||
// not found values trigger no event
|
||||
el.modelValue = 'foo';
|
||||
expect(counter).to.equal(4);
|
||||
expect(counter).to.equal(2);
|
||||
|
||||
el.modelValue = 'male';
|
||||
expect(counter).to.equal(6); // male becomes checked, other becomes unchecked
|
||||
expect(counter).to.equal(3); // male becomes checked, other becomes unchecked
|
||||
});
|
||||
|
||||
it('can be required', async () => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"stories",
|
||||
"test",
|
||||
"test-suites",
|
||||
"test-helpers",
|
||||
"translations",
|
||||
"*.js"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -55,6 +55,22 @@ export const FormControlMixin = dedupeMixin(
|
|||
* Contains all elements that should end up in aria-describedby of `._inputNode`
|
||||
*/
|
||||
_ariaDescribedNodes: Array,
|
||||
/**
|
||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||
* @type {'child'|'fieldset'|'choice-group'}
|
||||
*/
|
||||
_repropagationRole: String,
|
||||
/**
|
||||
* By default, a field with _repropagationRole 'choice-group' will act as an
|
||||
* 'endpoint'. This means it will be considered as an individual field: for
|
||||
* a select, individual options will not be part of the formPath. They
|
||||
* will.
|
||||
* Similarly, components that (a11y wise) need to be fieldsets, but 'interaction wise'
|
||||
* (from Application Developer perspective) need to be more like fields
|
||||
* (think of an amount-input with a currency select box next to it), can set this
|
||||
* to true to hide private internals in the formPath.
|
||||
*/
|
||||
_isRepropagationEndpoint: Boolean,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +167,8 @@ export const FormControlMixin = dedupeMixin(
|
|||
this._inputId = uuid(this.localName);
|
||||
this._ariaLabelledNodes = [];
|
||||
this._ariaDescribedNodes = [];
|
||||
this._repropagationRole = 'child';
|
||||
this.addEventListener('model-value-changed', this.__repropagateChildrenValues);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -553,5 +571,95 @@ export const FormControlMixin = dedupeMixin(
|
|||
__getDirectSlotChild(slotName) {
|
||||
return [...this.children].find(el => el.slot === slotName);
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__dispatchInitialModelValueChangedEvent();
|
||||
}
|
||||
|
||||
async __dispatchInitialModelValueChangedEvent() {
|
||||
// When we are not a fieldset / choice-group, we don't need to wait for our children
|
||||
// to send a unified event
|
||||
if (this._repropagationRole === 'child') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.registrationComplete;
|
||||
// Initially we don't repropagate model-value-changed events coming
|
||||
// from children. On firstUpdated we re-dispatch this event to maintain
|
||||
// 'count consistency' (to not confuse the application developer with a
|
||||
// large number of initial events). Initially the source field will not
|
||||
// be part of the formPath but afterwards it will.
|
||||
this.__repropagateChildrenInitialized = true;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('model-value-changed', {
|
||||
bubbles: true,
|
||||
detail: { formPath: [this], initialize: true },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
||||
_onBeforeRepropagateChildrenValues(ev) {}
|
||||
|
||||
__repropagateChildrenValues(ev) {
|
||||
// Allows sub classes to internally listen to the children change events
|
||||
// (before stopImmediatePropagation is called below).
|
||||
this._onBeforeRepropagateChildrenValues(ev);
|
||||
// Normalize target, we also might get it from 'portals' (rich select)
|
||||
const target = (ev.detail && ev.detail.element) || ev.target;
|
||||
const isEndpoint =
|
||||
this._isRepropagationEndpoint || this._repropagationRole === 'choice-group';
|
||||
|
||||
// Prevent eternal loops after we sent the event below.
|
||||
if (target === this) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A. Stop sibling handlers
|
||||
//
|
||||
// Make sure our sibling event listeners (added by Application developers) will not get
|
||||
// the child model-value-changed event, but the repropagated one at the bottom of this
|
||||
// method
|
||||
ev.stopImmediatePropagation();
|
||||
|
||||
// B1. Are we still initializing? If so, halt...
|
||||
//
|
||||
// Stop repropagating children events before firstUpdated and make sure we de not
|
||||
// repropagate init events of our children (we already sent our own
|
||||
// initial model-value-change event in firstUpdated)
|
||||
const isGroup = this._repropagationRole !== 'child'; // => fieldset or choice-group
|
||||
const isSelfInitializing = isGroup && !this.__repropagateChildrenInitialized;
|
||||
const isChildGroupInitializing = ev.detail && ev.detail.initialize;
|
||||
if (isSelfInitializing || isChildGroupInitializing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// B2. Are we a single choice choice-group? If so, halt when unchecked
|
||||
//
|
||||
// We only send the checked changed up (not the unchecked). In this way a choice group
|
||||
// (radio-group, checkbox-group, select/listbox) acts as an 'endpoint' (a single Field)
|
||||
// just like the native <select>
|
||||
if (this._repropagationRole === 'choice-group' && !this.multipleChoice && !target.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// C1. We are ready to dispatch. Create a formPath
|
||||
//
|
||||
// Compute the formPath. Choice groups are regarded 'end points'
|
||||
let parentFormPath = [];
|
||||
if (!isEndpoint) {
|
||||
parentFormPath = (ev.detail && ev.detail.formPath) || [target];
|
||||
}
|
||||
const formPath = [...parentFormPath, this];
|
||||
|
||||
// C2. Finally, redispatch a fresh model-value-changed event from our host, consumable
|
||||
// for an Application Developer
|
||||
//
|
||||
// Since for a11y everything needs to be in lightdom, we don't add 'composed:true'
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('model-value-changed', { bubbles: true, detail: { formPath } }),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -289,7 +289,10 @@ export const FormatMixin = dedupeMixin(
|
|||
_dispatchModelValueChangedEvent() {
|
||||
/** @event model-value-changed */
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
|
||||
new CustomEvent('model-value-changed', {
|
||||
bubbles: true,
|
||||
detail: { formPath: [this] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ export const FormRegistrarMixin = dedupeMixin(
|
|||
this.registrationReady = new Promise(resolve => {
|
||||
this.__resolveRegistrationReady = resolve;
|
||||
});
|
||||
this.registrationComplete = new Promise(resolve => {
|
||||
this.__resolveRegistrationComplete = resolve;
|
||||
});
|
||||
|
||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||
|
|
@ -76,6 +79,16 @@ export const FormRegistrarMixin = dedupeMixin(
|
|||
super.firstUpdated(changedProperties);
|
||||
this.__resolveRegistrationReady();
|
||||
this.__readyForRegistration = true;
|
||||
|
||||
// After we allow our children to register, we need to wait one tick before they
|
||||
// all sent their 'form-element-register' event.
|
||||
// TODO: allow developer to delay this moment, similar to LitElement.performUpdate can be
|
||||
// delayed.
|
||||
setTimeout(() => {
|
||||
this.registrationHasCompleted = true;
|
||||
this.__resolveRegistrationComplete();
|
||||
});
|
||||
|
||||
formRegistrarManager.becomesReady();
|
||||
this.__hasBeenRendered = true;
|
||||
}
|
||||
|
|
|
|||
1
packages/field/test-helpers.js
Normal file
1
packages/field/test-helpers.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { formFixture } from './test-helpers/formFixture.js';
|
||||
11
packages/field/test-helpers/formFixture.js
Normal file
11
packages/field/test-helpers/formFixture.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { fixture, aTimeout } from '@open-wc/testing';
|
||||
|
||||
export async function formFixture(...args) {
|
||||
const el = await fixture(...args);
|
||||
if (el.registrationComplete) {
|
||||
await el.registrationComplete;
|
||||
await aTimeout();
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
|
||||
import { expect, html, defineCE, unsafeStatic } from '@open-wc/testing';
|
||||
import { LitElement, SlotMixin } from '@lion/core';
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { formFixture as fixture } from '../test-helpers/formFixture.js';
|
||||
import { FormControlMixin } from '../src/FormControlMixin.js';
|
||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||
|
||||
describe('FormControlMixin', () => {
|
||||
const inputSlot = '<input slot="input" />';
|
||||
|
|
@ -179,4 +181,113 @@ describe('FormControlMixin', () => {
|
|||
.getAttribute('aria-live'),
|
||||
).to.equal('polite');
|
||||
});
|
||||
|
||||
describe('Model-value-changed event propagation', () => {
|
||||
const FormControlWithRegistrarMixinClass = class extends FormControlMixin(
|
||||
FormRegistrarMixin(SlotMixin(LitElement)),
|
||||
) {
|
||||
static get properties() {
|
||||
return {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const groupElem = defineCE(FormControlWithRegistrarMixinClass);
|
||||
const groupTag = unsafeStatic(groupElem);
|
||||
|
||||
describe('On initialization', () => {
|
||||
it('redispatches one event from host', async () => {
|
||||
const formSpy = sinon.spy();
|
||||
const fieldsetSpy = sinon.spy();
|
||||
const formEl = await fixture(html`
|
||||
<${groupTag} name="form" ._repropagationRole=${'form-group'} @model-value-changed=${formSpy}>
|
||||
<${groupTag} name="fieldset" ._repropagationRole=${'form-group'} @model-value-changed=${fieldsetSpy}>
|
||||
<${tag} name="field"></${tag}>
|
||||
</${groupTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
const fieldsetEl = formEl.querySelector('[name=fieldset]');
|
||||
|
||||
expect(fieldsetSpy.callCount).to.equal(1);
|
||||
const fieldsetEv = fieldsetSpy.firstCall.args[0];
|
||||
expect(fieldsetEv.target).to.equal(fieldsetEl);
|
||||
expect(fieldsetEv.detail.formPath).to.eql([fieldsetEl]);
|
||||
|
||||
expect(formSpy.callCount).to.equal(1);
|
||||
const formEv = formSpy.firstCall.args[0];
|
||||
expect(formEv.target).to.equal(formEl);
|
||||
expect(formEv.detail.formPath).to.eql([formEl]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After initialization', () => {
|
||||
it('redispatches one event from host and keeps formPath history', async () => {
|
||||
const formSpy = sinon.spy();
|
||||
const fieldsetSpy = sinon.spy();
|
||||
const fieldSpy = sinon.spy();
|
||||
const formEl = await fixture(html`
|
||||
<${groupTag} name="form">
|
||||
<${groupTag} name="fieldset">
|
||||
<${tag} name="field"></${tag}>
|
||||
</${groupTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
const fieldEl = formEl.querySelector('[name=field]');
|
||||
const fieldsetEl = formEl.querySelector('[name=fieldset]');
|
||||
|
||||
formEl.addEventListener('model-value-changed', formSpy);
|
||||
fieldsetEl.addEventListener('model-value-changed', fieldsetSpy);
|
||||
fieldEl.addEventListener('model-value-changed', fieldSpy);
|
||||
|
||||
fieldEl.dispatchEvent(new Event('model-value-changed', { bubbles: true }));
|
||||
|
||||
expect(fieldsetSpy.callCount).to.equal(1);
|
||||
const fieldsetEv = fieldsetSpy.firstCall.args[0];
|
||||
expect(fieldsetEv.target).to.equal(fieldsetEl);
|
||||
expect(fieldsetEv.detail.formPath).to.eql([fieldEl, fieldsetEl]);
|
||||
|
||||
expect(formSpy.callCount).to.equal(1);
|
||||
const formEv = formSpy.firstCall.args[0];
|
||||
expect(formEv.target).to.equal(formEl);
|
||||
expect(formEv.detail.formPath).to.eql([fieldEl, fieldsetEl, formEl]);
|
||||
});
|
||||
|
||||
it('sends one event for single select choice-groups', async () => {
|
||||
const formSpy = sinon.spy();
|
||||
const choiceGroupSpy = sinon.spy();
|
||||
const formEl = await fixture(html`
|
||||
<${groupTag} name="form">
|
||||
<${groupTag} name="choice-group" ._repropagationRole=${'choice-group'}>
|
||||
<${tag} name="choice-group" id="option1" .checked=${true}></${tag}>
|
||||
<${tag} name="choice-group" id="option2"></${tag}>
|
||||
</${groupTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
const choiceGroupEl = formEl.querySelector('[name=choice-group]');
|
||||
const option1El = formEl.querySelector('#option1');
|
||||
const option2El = formEl.querySelector('#option2');
|
||||
formEl.addEventListener('model-value-changed', formSpy);
|
||||
choiceGroupEl.addEventListener('model-value-changed', choiceGroupSpy);
|
||||
|
||||
// Simulate check
|
||||
option2El.checked = true;
|
||||
option2El.dispatchEvent(new Event('model-value-changed', { bubbles: true }));
|
||||
option1El.checked = false;
|
||||
option1El.dispatchEvent(new Event('model-value-changed', { bubbles: true }));
|
||||
|
||||
expect(choiceGroupSpy.callCount).to.equal(1);
|
||||
const choiceGroupEv = choiceGroupSpy.firstCall.args[0];
|
||||
expect(choiceGroupEv.target).to.equal(choiceGroupEl);
|
||||
expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]);
|
||||
|
||||
expect(formSpy.callCount).to.equal(1);
|
||||
const formEv = formSpy.firstCall.args[0];
|
||||
expect(formEv.target).to.equal(formEl);
|
||||
expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -135,8 +135,8 @@ export const FormGroupMixin = dedupeMixin(
|
|||
}
|
||||
|
||||
async __initInteractionStates() {
|
||||
if (!this.__readyForRegistration) {
|
||||
await this.registrationReady;
|
||||
if (!this.registrationHasCompleted) {
|
||||
await this.registrationComplete;
|
||||
}
|
||||
this.formElements.forEach(el => {
|
||||
if (typeof el.initInteractionState === 'function') {
|
||||
|
|
|
|||
|
|
@ -25,5 +25,6 @@ export class LionFieldset extends FormGroupMixin(LitElement) {
|
|||
super();
|
||||
/** @override from FormRegistrarMixin */
|
||||
this._isFormOrFieldset = true;
|
||||
this._repropagationRole = 'fieldset'; // configures FormControlMixin
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
expect,
|
||||
fixture,
|
||||
fixtureSync,
|
||||
html,
|
||||
unsafeStatic,
|
||||
|
|
@ -8,6 +7,7 @@ import {
|
|||
nextFrame,
|
||||
defineCE,
|
||||
} from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/field/test-helpers.js';
|
||||
import sinon from 'sinon';
|
||||
import { Validator, IsNumber } from '@lion/validate';
|
||||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"@lion/input": "0.5.18",
|
||||
"@lion/input-amount": "0.5.18",
|
||||
"@lion/input-date": "0.5.19",
|
||||
"@lion/input-datepicker": "0.10.1",
|
||||
"@lion/input-email": "0.6.3",
|
||||
"@lion/input-iban": "0.6.3",
|
||||
"@lion/input-range": "0.2.18",
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ For usage and installation please see the appropriate packages.
|
|||
{() => {
|
||||
Required.getMessage = () => 'Please enter a value';
|
||||
return html`
|
||||
<lion-form>
|
||||
<lion-form @model-value-changed="${(ev) => { console.log('lion-form::', ev.target.name, ':', ev.detail.formPath) }}">
|
||||
<form>
|
||||
<lion-fieldset name="full_name">
|
||||
<lion-fieldset name="full_name" @model-value-changed="${(ev) => { console.log('lion-fieldset::', ev.target.name, ':', ev.detail.formPath) }}">
|
||||
<lion-input
|
||||
name="first_name"
|
||||
label="First Name"
|
||||
|
|
@ -69,6 +69,7 @@ For usage and installation please see the appropriate packages.
|
|||
<lion-input-iban name="iban" label="Iban"></lion-input-iban>
|
||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||
<lion-checkbox-group
|
||||
@model-value-changed="${(ev) => { console.log('lion-cb-group::', ev.target.name, ':', ev.detail.formPath) }}"
|
||||
label="What do you like?"
|
||||
name="checkers"
|
||||
.validators="${[new Required()]}"
|
||||
|
|
@ -78,6 +79,7 @@ For usage and installation please see the appropriate packages.
|
|||
<lion-checkbox .choiceValue=${'baz'} label="I like baz"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-radio-group
|
||||
@model-value-changed="${(ev) => { console.log('lion-radio-group::', ev.target.name, ':', ev.detail.formPath) }}"
|
||||
name="dinosaurs"
|
||||
label="Favorite dinosaur"
|
||||
.validators="${[new Required()]}"
|
||||
|
|
|
|||
274
packages/form-system/test/model-value-consistency.test.js
Normal file
274
packages/form-system/test/model-value-consistency.test.js
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { expect, html, unsafeStatic } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/field/test-helpers.js';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/input-amount/lion-input-amount.js';
|
||||
import '@lion/input-date/lion-input-date.js';
|
||||
import '@lion/input-datepicker/lion-input-datepicker.js';
|
||||
import '@lion/input-email/lion-input-email.js';
|
||||
import '@lion/input-iban/lion-input-iban.js';
|
||||
import '@lion/input-range/lion-input-range.js';
|
||||
import '@lion/textarea/lion-textarea.js';
|
||||
|
||||
import '@lion/checkbox-group/lion-checkbox-group.js';
|
||||
import '@lion/checkbox-group/lion-checkbox.js';
|
||||
|
||||
import '@lion/radio-group/lion-radio-group.js';
|
||||
import '@lion/radio-group/lion-radio.js';
|
||||
|
||||
import '@lion/select/lion-select.js';
|
||||
|
||||
import '@lion/select-rich/lion-select-rich.js';
|
||||
import '@lion/select-rich/lion-options.js';
|
||||
import '@lion/select-rich/lion-option.js';
|
||||
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
|
||||
const featureName = 'model value';
|
||||
|
||||
const getFirstPaintTitle = count => `should dispatch ${count} time(s) on first paint`;
|
||||
const getInteractionTitle = count => `should dispatch ${count} time(s) on interaction`;
|
||||
|
||||
const firstStampCount = 1;
|
||||
const interactionCount = 1;
|
||||
|
||||
const fieldDispatchesCountOnFirstPaint = (tagname, count) => {
|
||||
const tag = unsafeStatic(tagname);
|
||||
const spy = sinon.spy();
|
||||
it(getFirstPaintTitle(count), async () => {
|
||||
await fixture(html`<${tag} @model-value-changed="${spy}"></${tag}>`);
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
||||
const fieldDispatchesCountOnInteraction = (tagname, count) => {
|
||||
const tag = unsafeStatic(tagname);
|
||||
const spy = sinon.spy();
|
||||
it(getInteractionTitle(count), async () => {
|
||||
const el = await fixture(html`<${tag}></${tag}>`);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
// TODO: discuss if this is the "correct" way to interact with component
|
||||
el.modelValue = 'foo';
|
||||
await el.updateComplete;
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
||||
const choiceDispatchesCountOnFirstPaint = (tagname, count) => {
|
||||
const tag = unsafeStatic(tagname);
|
||||
const spy = sinon.spy();
|
||||
it(getFirstPaintTitle(count), async () => {
|
||||
await fixture(html`<${tag} @model-value-changed="${spy}" .choiceValue="${'option'}"></${tag}>`);
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
||||
const choiceDispatchesCountOnInteraction = (tagname, count) => {
|
||||
const tag = unsafeStatic(tagname);
|
||||
const spy = sinon.spy();
|
||||
it(getInteractionTitle(count), async () => {
|
||||
const el = await fixture(html`<${tag} .choiceValue="${'option'}"></${tag}>`);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
el.checked = true;
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
||||
const choiceGroupDispatchesCountOnFirstPaint = (groupTagname, itemTagname, count) => {
|
||||
const groupTag = unsafeStatic(groupTagname);
|
||||
const itemTag = unsafeStatic(itemTagname);
|
||||
it(getFirstPaintTitle(count), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
<${groupTag} @model-value-changed="${spy}">
|
||||
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
||||
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
||||
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
||||
const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, count) => {
|
||||
const groupTag = unsafeStatic(groupTagname);
|
||||
const itemTag = unsafeStatic(itemTagname);
|
||||
it(getInteractionTitle(count), async () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
<${groupTag}>
|
||||
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
||||
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
||||
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
||||
</${groupTag}>
|
||||
`);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const option2 = el.querySelector(`${itemTagname}:nth-child(2)`);
|
||||
option2.checked = true;
|
||||
expect(spy.callCount).to.equal(count);
|
||||
|
||||
spy.resetHistory();
|
||||
|
||||
const option3 = el.querySelector(`${itemTagname}:nth-child(3)`);
|
||||
option3.checked = true;
|
||||
expect(spy.callCount).to.equal(count);
|
||||
});
|
||||
};
|
||||
|
||||
[
|
||||
'input',
|
||||
'input-amount',
|
||||
'input-date',
|
||||
'input-datepicker',
|
||||
'input-email',
|
||||
'input-iban',
|
||||
'input-range',
|
||||
'textarea',
|
||||
].forEach(chunk => {
|
||||
const tagname = `lion-${chunk}`;
|
||||
describe(`${tagname}`, () => {
|
||||
describe(featureName, () => {
|
||||
fieldDispatchesCountOnFirstPaint(tagname, firstStampCount);
|
||||
fieldDispatchesCountOnInteraction(tagname, interactionCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
['checkbox', 'radio'].forEach(chunk => {
|
||||
const groupTagname = `lion-${chunk}-group`;
|
||||
const itemTagname = `lion-${chunk}`;
|
||||
|
||||
describe(`${itemTagname}`, () => {
|
||||
describe(featureName, () => {
|
||||
choiceDispatchesCountOnFirstPaint(itemTagname, firstStampCount);
|
||||
choiceDispatchesCountOnInteraction(itemTagname, interactionCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${groupTagname}`, () => {
|
||||
describe(featureName, () => {
|
||||
choiceGroupDispatchesCountOnFirstPaint(groupTagname, itemTagname, firstStampCount);
|
||||
choiceGroupDispatchesCountOnInteraction(groupTagname, itemTagname, interactionCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lion-select', () => {
|
||||
describe(featureName, () => {
|
||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
<lion-select @model-value-changed="${spy}">
|
||||
<select slot="input">
|
||||
<option value="option1"></option>
|
||||
<option value="option2"></option>
|
||||
<option value="option3"></option>
|
||||
</select>
|
||||
</lion-select>
|
||||
`);
|
||||
expect(spy.callCount).to.equal(firstStampCount);
|
||||
});
|
||||
|
||||
it(getInteractionTitle(interactionCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
<lion-select>
|
||||
<select slot="input">
|
||||
<option value="option1"></option>
|
||||
<option value="option2"></option>
|
||||
<option value="option3"></option>
|
||||
</select>
|
||||
</lion-select>
|
||||
`);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const option2 = el.querySelector('option:nth-child(2)');
|
||||
|
||||
// mimic user input
|
||||
option2.selected = true;
|
||||
el._inputNode.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
expect(spy.callCount).to.equal(interactionCount);
|
||||
|
||||
spy.resetHistory();
|
||||
|
||||
const option3 = el.querySelector('option:nth-child(3)');
|
||||
|
||||
// mimic user input
|
||||
option3.selected = true;
|
||||
el._inputNode.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
expect(spy.callCount).to.equal(interactionCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lion-select-rich', () => {
|
||||
describe(featureName, () => {
|
||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
<lion-select-rich @model-value-changed="${spy}">
|
||||
<lion-options slot="input">
|
||||
<lion-option .choiceValue="${'option1'}"></lion-option>
|
||||
<lion-option .choiceValue="${'option2'}"></lion-option>
|
||||
<lion-option .choiceValue="${'option3'}"></lion-option>
|
||||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
expect(spy.callCount).to.equal(firstStampCount);
|
||||
});
|
||||
|
||||
it(getInteractionTitle(interactionCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
<lion-select-rich>
|
||||
<lion-options slot="input">
|
||||
<lion-option .choiceValue="${'option1'}"></lion-option>
|
||||
<lion-option .choiceValue="${'option2'}"></lion-option>
|
||||
<lion-option .choiceValue="${'option3'}"></lion-option>
|
||||
</lion-options>
|
||||
</lion-select-rich>
|
||||
`);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const option2 = el.querySelector('lion-option:nth-child(2)');
|
||||
option2.checked = true;
|
||||
expect(spy.callCount).to.equal(interactionCount);
|
||||
|
||||
spy.resetHistory();
|
||||
|
||||
const option3 = el.querySelector('lion-option:nth-child(3)');
|
||||
option3.checked = true;
|
||||
expect(spy.callCount).to.equal(interactionCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lion-fieldset', () => {
|
||||
describe(featureName, () => {
|
||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
await fixture(html`
|
||||
<lion-fieldset name="parent" @model-value-changed="${spy}">
|
||||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
expect(spy.callCount).to.equal(firstStampCount);
|
||||
});
|
||||
|
||||
it(getInteractionTitle(interactionCount), async () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
<lion-fieldset name="parent">
|
||||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
const input = el.querySelector('lion-input');
|
||||
input.modelValue = 'foo';
|
||||
expect(spy.callCount).to.equal(interactionCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
144
packages/form-system/test/model-value-event.test.js
Normal file
144
packages/form-system/test/model-value-event.test.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { expect, html } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/field/test-helpers.js';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '@lion/input/lion-input.js';
|
||||
import '@lion/fieldset/lion-fieldset.js';
|
||||
|
||||
describe('model value event', () => {
|
||||
describe('form path', () => {
|
||||
it('should be property', async () => {
|
||||
const spy = sinon.spy();
|
||||
const input = await fixture(html`
|
||||
<lion-input></lion-input>
|
||||
`);
|
||||
input.addEventListener('model-value-changed', spy);
|
||||
input.modelValue = 'woof';
|
||||
const e = spy.firstCall.args[0];
|
||||
expect(e.detail).to.have.a.property('formPath');
|
||||
});
|
||||
|
||||
it('should contain dispatching field', async () => {
|
||||
const spy = sinon.spy();
|
||||
const input = await fixture(html`
|
||||
<lion-input></lion-input>
|
||||
`);
|
||||
input.addEventListener('model-value-changed', spy);
|
||||
input.modelValue = 'foo';
|
||||
const e = spy.firstCall.args[0];
|
||||
expect(e.detail.formPath).to.eql([input]);
|
||||
});
|
||||
|
||||
it('should contain field and group', async () => {
|
||||
const spy = sinon.spy();
|
||||
const fieldset = await fixture(html`
|
||||
<lion-fieldset name="fieldset">
|
||||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
fieldset.addEventListener('model-value-changed', spy);
|
||||
const input = fieldset.querySelector('lion-input');
|
||||
input.modelValue = 'foo';
|
||||
const e = spy.firstCall.args[0];
|
||||
expect(e.detail.formPath).to.eql([input, fieldset]);
|
||||
});
|
||||
|
||||
it('should contain deep elements', async () => {
|
||||
const spy = sinon.spy();
|
||||
const grandparent = await fixture(html`
|
||||
<lion-fieldset name="grandparent">
|
||||
<lion-fieldset name="parent">
|
||||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
const parent = grandparent.querySelector('[name=parent]');
|
||||
const input = grandparent.querySelector('[name=input]');
|
||||
grandparent.addEventListener('model-value-changed', spy);
|
||||
input.modelValue = 'foo';
|
||||
const e = spy.firstCall.args[0];
|
||||
expect(e.detail.formPath).to.eql([input, parent, grandparent]);
|
||||
});
|
||||
|
||||
it('should ignore elements that are not fields or fieldsets', async () => {
|
||||
const spy = sinon.spy();
|
||||
const grandparent = await fixture(html`
|
||||
<lion-fieldset name="grandparent">
|
||||
<div>
|
||||
<lion-fieldset name="parent">
|
||||
<div>
|
||||
<div>
|
||||
<lion-input name="input"></lion-input>
|
||||
</div>
|
||||
</div>
|
||||
</lion-fieldset>
|
||||
</div>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
const parent = grandparent.querySelector('[name=parent]');
|
||||
const input = grandparent.querySelector('[name=input]');
|
||||
grandparent.addEventListener('model-value-changed', spy);
|
||||
input.modelValue = 'foo';
|
||||
const e = spy.firstCall.args[0];
|
||||
expect(e.detail.formPath).to.eql([input, parent, grandparent]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signature', () => {
|
||||
let e;
|
||||
beforeEach(async () => {
|
||||
const spy = sinon.spy();
|
||||
const el = await fixture(
|
||||
html`
|
||||
<lion-input></lion-input>
|
||||
`,
|
||||
);
|
||||
el.addEventListener('model-value-changed', spy);
|
||||
el.modelValue = 'foo';
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
e = spy.firstCall.args[0];
|
||||
});
|
||||
|
||||
// TODO: Re-enable at some point...
|
||||
// In my opinion (@CubLion) we should not bubble events.
|
||||
// Instead each parent should explicitly listen to children events,
|
||||
// and then re-dispatch on themselves.
|
||||
it.skip('should not bubble', async () => {
|
||||
expect(e.bubbles).to.be.false;
|
||||
});
|
||||
|
||||
it('should not leave shadow boundary', async () => {
|
||||
expect(e.composed).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('propagation', () => {
|
||||
it('should dispatch different event at each level', async () => {
|
||||
const grandparent = await fixture(html`
|
||||
<lion-fieldset name="grandparent">
|
||||
<lion-fieldset name="parent">
|
||||
<lion-input name="input"></lion-input>
|
||||
</lion-fieldset>
|
||||
</lion-fieldset>
|
||||
`);
|
||||
const parent = grandparent.querySelector('[name="parent"]');
|
||||
const input = grandparent.querySelector('[name="input"]');
|
||||
const spies = [];
|
||||
[grandparent, parent, input].forEach(element => {
|
||||
const spy = sinon.spy();
|
||||
spies.push(spy);
|
||||
element.addEventListener('model-value-changed', spy);
|
||||
});
|
||||
input.modelValue = 'foo';
|
||||
spies.forEach((spy, index) => {
|
||||
const currentEvent = spy.firstCall.args[0];
|
||||
for (let i = index + 1; i < spies.length; i += 1) {
|
||||
const nextEvent = spies[i].firstCall.args[0];
|
||||
expect(currentEvent).not.to.eql(nextEvent);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
export const withDropdownConfig = () => ({
|
||||
placementMode: 'local',
|
||||
|
||||
inheritsReferenceWidth: true,
|
||||
inheritsReferenceWidth: 'full',
|
||||
hidesOnOutsideClick: true,
|
||||
popperConfig: {
|
||||
placement: 'bottom-start',
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
this.__cachedUserSetModelValue = value;
|
||||
}
|
||||
|
||||
this.__syncInvokerElement();
|
||||
this.requestUpdate('modelValue');
|
||||
}
|
||||
|
||||
|
|
@ -199,8 +198,9 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
// for interaction states
|
||||
this._listboxActiveDescendant = null;
|
||||
this.__hasInitialSelectedFormElement = false;
|
||||
|
||||
this._repropagationRole = 'choice-group'; // configures FormControlMixin
|
||||
this.__setupEventListeners();
|
||||
this.__initInteractionStates();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -250,6 +250,15 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
}
|
||||
}
|
||||
|
||||
async __initInteractionStates() {
|
||||
await this.registrationComplete;
|
||||
// This timeout is here, so that we know we handle after the initial model-value
|
||||
// event (see firstUpdated method FormConrtolMixin) has fired.
|
||||
setTimeout(() => {
|
||||
this.initInteractionState();
|
||||
});
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
// In FormControl, we get direct child [slot="input"]. This doesn't work, because the overlay
|
||||
// system wraps it in [slot="_overlay-shadow-outlet"]
|
||||
|
|
@ -298,6 +307,10 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
this._invokerNode.setAttribute('aria-invalid', this._hasFeedbackVisibleFor('error'));
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('modelValue')) {
|
||||
this.__syncInvokerElement();
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
|
@ -345,24 +358,27 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
this.__setAttributeForAllFormElements('aria-setsize', this.formElements.length);
|
||||
child.setAttribute('aria-posinset', this.formElements.length);
|
||||
|
||||
this.__onChildModelValueChanged({ target: child });
|
||||
this.__proxyChildModelValueChanged({ target: child });
|
||||
this.resetInteractionState();
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
__setupEventListeners() {
|
||||
this.__onChildActiveChanged = this.__onChildActiveChanged.bind(this);
|
||||
this.__onChildModelValueChanged = this.__onChildModelValueChanged.bind(this);
|
||||
this.__proxyChildModelValueChanged = this.__proxyChildModelValueChanged.bind(this);
|
||||
this.__onKeyUp = this.__onKeyUp.bind(this);
|
||||
|
||||
this._listboxNode.addEventListener('active-changed', this.__onChildActiveChanged);
|
||||
this._listboxNode.addEventListener('model-value-changed', this.__onChildModelValueChanged);
|
||||
this._listboxNode.addEventListener('model-value-changed', this.__proxyChildModelValueChanged);
|
||||
this.addEventListener('keyup', this.__onKeyUp);
|
||||
}
|
||||
|
||||
__teardownEventListeners() {
|
||||
this._listboxNode.removeEventListener('active-changed', this.__onChildActiveChanged);
|
||||
this._listboxNode.removeEventListener('model-value-changed', this.__onChildModelValueChanged);
|
||||
this._listboxNode.removeEventListener(
|
||||
'model-value-changed',
|
||||
this.__proxyChildModelValueChanged,
|
||||
);
|
||||
this._listboxNode.removeEventListener('keyup', this.__onKeyUp);
|
||||
}
|
||||
|
||||
|
|
@ -391,16 +407,14 @@ export class LionSelectRich extends ScopedElementsMixin(
|
|||
});
|
||||
}
|
||||
|
||||
__onChildModelValueChanged({ target }) {
|
||||
if (target.checked) {
|
||||
this.formElements.forEach(formElement => {
|
||||
if (formElement !== target) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
formElement.checked = false;
|
||||
}
|
||||
});
|
||||
this.modelValue = target.value;
|
||||
__proxyChildModelValueChanged(ev) {
|
||||
// We need to redispatch the model-value-changed event on 'this', so it will
|
||||
// align with FormControl.__repropagateChildrenValues method. Also, this makes
|
||||
// it act like a portal, in case the listbox is put in a modal overlay on body level.
|
||||
if (ev.stopPropagation) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('model-value-changed', { detail: { element: ev.target } }));
|
||||
}
|
||||
|
||||
__syncInvokerElement() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Required } from '@lion/validate';
|
||||
import { expect, fixture, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing';
|
||||
import { expect, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/field/test-helpers.js';
|
||||
|
||||
import '../lion-option.js';
|
||||
import '../lion-options.js';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { OverlayController } from '@lion/overlays';
|
||||
import { Required } from '@lion/validate';
|
||||
import {
|
||||
aTimeout,
|
||||
defineCE,
|
||||
expect,
|
||||
fixture,
|
||||
html,
|
||||
nextFrame,
|
||||
unsafeStatic,
|
||||
} from '@open-wc/testing';
|
||||
import { aTimeout, defineCE, expect, html, nextFrame, unsafeStatic } from '@open-wc/testing';
|
||||
import { formFixture as fixture } from '@lion/field/test-helpers.js';
|
||||
|
||||
import { LionSelectRich } from '../index.js';
|
||||
import '../lion-option.js';
|
||||
import '../lion-options.js';
|
||||
|
|
@ -246,6 +240,7 @@ describe('lion-select-rich', () => {
|
|||
expect(el._invokerNode.selectedElement).dom.to.equal(options[1]);
|
||||
|
||||
el.checkedIndex = 0;
|
||||
await el.updateComplete;
|
||||
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -32,16 +32,16 @@ import { LionField } from '@lion/field';
|
|||
export class LionSelect extends LionField {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('change', this._proxyChangeEvent);
|
||||
this._inputNode.addEventListener('change', this._proxyChangeEvent);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('change', this._proxyChangeEvent);
|
||||
this._inputNode.removeEventListener('change', this._proxyChangeEvent);
|
||||
}
|
||||
|
||||
_proxyChangeEvent() {
|
||||
this._inputNode.dispatchEvent(
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue