[feedback]
~~~`, async () => {
- const lionField = await fixture(`<${tagString}>
+ const el = await fixture(`<${tagString}>
${inputSlotString}
Enter your Name
No name entered
${tagString}>
`);
- const nativeInput = lionField.$$slot('input');
+ const nativeInput = el.$$slot('input');
- expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${lionField._inputId}`);
- expect(nativeInput.getAttribute('aria-describedby')).to.contain(
- ` help-text-${lionField._inputId}`,
- );
- expect(nativeInput.getAttribute('aria-describedby')).to.contain(
- ` feedback-${lionField._inputId}`,
- );
+ expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${el._inputId}`);
+ expect(nativeInput.getAttribute('aria-describedby')).to.contain(` help-text-${el._inputId}`);
+ expect(nativeInput.getAttribute('aria-describedby')).to.contain(` feedback-${el._inputId}`);
});
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
(via attribute data-label) and in describedby (via attribute data-description)`, async () => {
- const lionField = await fixture(`<${tagString}>
+ const el = await fixture(`<${tagString}>
${inputSlotString}
[before]
[after]
@@ -217,12 +211,12 @@ describe('
', () => {
${tagString}>
`);
- const nativeInput = lionField.$$slot('input');
+ const nativeInput = el.$$slot('input');
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
- ` before-${lionField._inputId} after-${lionField._inputId}`,
+ ` before-${el._inputId} after-${el._inputId}`,
);
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
- ` prefix-${lionField._inputId} suffix-${lionField._inputId}`,
+ ` prefix-${el._inputId} suffix-${el._inputId}`,
);
});
@@ -291,31 +285,31 @@ describe('', () => {
function hasX(str) {
return { hasX: str.indexOf('x') > -1 };
}
- const lionField = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- const feedbackEl = lionField._feedbackElement;
+ const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
+ const feedbackEl = el._feedbackElement;
- lionField.modelValue = 'a@b.nl';
- lionField.errorValidators = [[hasX]];
+ el.modelValue = 'a@b.nl';
+ el.errorValidators = [[hasX]];
- expect(lionField.error.hasX).to.equal(true);
+ expect(el.error.hasX).to.equal(true);
expect(feedbackEl.innerText.trim()).to.equal(
'',
'shows no feedback, although the element has an error',
);
- lionField.dirty = true;
- lionField.touched = true;
- lionField.modelValue = 'ab@c.nl'; // retrigger validation
- await lionField.updateComplete;
+ el.dirty = true;
+ el.touched = true;
+ el.modelValue = 'ab@c.nl'; // retrigger validation
+ await el.updateComplete;
expect(feedbackEl.innerText.trim()).to.equal(
'This is error message for hasX',
'shows feedback, because touched=true and dirty=true',
);
- lionField.touched = false;
- lionField.dirty = false;
- lionField.prefilled = true;
- await lionField.updateComplete;
+ el.touched = false;
+ el.dirty = false;
+ el.prefilled = true;
+ await el.updateComplete;
expect(feedbackEl.innerText.trim()).to.equal(
'This is error message for hasX',
'shows feedback, because prefilled=true',
@@ -323,14 +317,14 @@ describe('', () => {
});
it('can be required', async () => {
- const lionField = await fixture(html`
+ const el = await fixture(html`
<${tag}
.errorValidators=${[['required']]}
>${inputSlot}${tag}>
`);
- expect(lionField.error.required).to.be.true;
- lionField.modelValue = 'cat';
- expect(lionField.error.required).to.be.undefined;
+ expect(el.error.required).to.be.true;
+ el.modelValue = 'cat';
+ expect(el.error.required).to.be.undefined;
});
it('will only update formattedValue when valid on `user-input-changed`', async () => {
@@ -338,7 +332,7 @@ describe('', () => {
function isBarValidator(value) {
return { isBar: value === 'bar' };
}
- const lionField = await fixture(html`
+ const el = await fixture(html`
<${tag}
.modelValue=${'init-string'}
.formatter=${formatterSpy}
@@ -347,21 +341,21 @@ describe('', () => {
`);
expect(formatterSpy.callCount).to.equal(0);
- expect(lionField.formattedValue).to.equal('init-string');
+ expect(el.formattedValue).to.equal('init-string');
- lionField.modelValue = 'bar';
+ el.modelValue = 'bar';
expect(formatterSpy.callCount).to.equal(1);
- expect(lionField.formattedValue).to.equal('foo: bar');
+ expect(el.formattedValue).to.equal('foo: bar');
- mimicUserInput(lionField, 'foo');
+ mimicUserInput(el, 'foo');
expect(formatterSpy.callCount).to.equal(1);
- expect(lionField.value).to.equal('foo');
+ expect(el.value).to.equal('foo');
});
});
describe(`Content projection${nameSuffix}`, () => {
it('renders correctly all slot elements in light DOM', async () => {
- const lionField = await fixture(`
+ const el = await fixture(`
<${tagString}>
${inputSlotString}
@@ -385,8 +379,8 @@ describe('', () => {
'feedback',
];
names.forEach(slotName => {
- lionField.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok');
- const slot = lionField.shadowRoot.querySelector(`slot[name="${slotName}"]`);
+ el.querySelector(`[slot="${slotName}"]`).setAttribute('test-me', 'ok');
+ const slot = el.shadowRoot.querySelector(`slot[name="${slotName}"]`);
const assignedNodes = slot.assignedNodes();
expect(assignedNodes.length).to.equal(1);
expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
@@ -395,12 +389,6 @@ describe('', () => {
});
describe(`Delegation${nameSuffix}`, () => {
- it('delegates attribute autofocus', async () => {
- const el = await fixture(`<${tagString} autofocus>${inputSlotString}${tagString}>`);
- expect(el.hasAttribute('autofocus')).to.be.false;
- expect(el.inputElement.hasAttribute('autofocus')).to.be.true;
- });
-
it('delegates property value', async () => {
const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
expect(el.inputElement.value).to.equal('');
@@ -409,51 +397,17 @@ describe('', () => {
expect(el.inputElement.value).to.equal('one');
});
- it('delegates property type', async () => {
- const el = await fixture(`<${tagString} type="text">${inputSlotString}${tagString}>`);
- const inputElemTag = el.inputElement.tagName.toLowerCase();
- if (inputElemTag === 'select') {
- // TODO: later on we might want to support multi select ?
- expect(el.inputElement.type).to.contain('select-one');
- } else if (inputElemTag === 'textarea') {
- expect(el.inputElement.type).to.contain('textarea');
- } else {
- // input or custom inputElement
- expect(el.inputElement.type).to.contain('text');
- el.type = 'password';
- expect(el.type).to.equal('password');
- expect(el.inputElement.type).to.equal('password');
- }
- });
-
- it('delegates property onfocus', async () => {
- const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- const cbFocusHost = sinon.spy();
- el.onfocus = cbFocusHost;
- await triggerFocusFor(el.inputElement);
- expect(cbFocusHost.callCount).to.equal(1);
- });
-
- it('delegates property onblur', async () => {
- const el = await fixture(`<${tagString}>${inputSlotString}${tagString}>`);
- const cbBlurHost = sinon.spy();
- el.onblur = cbBlurHost;
- await triggerFocusFor(el.inputElement);
- await triggerBlurFor(el.inputElement);
- expect(cbBlurHost.callCount).to.equal(1);
- });
-
it('delegates property selectionStart and selectionEnd', async () => {
- const lionField = await fixture(html`
+ const el = await fixture(html`
<${tag}
.modelValue=${'Some text to select'}
>${unsafeHTML(inputSlotString)}${tag}>
`);
- lionField.selectionStart = 5;
- lionField.selectionEnd = 12;
- expect(lionField.inputElement.selectionStart).to.equal(5);
- expect(lionField.inputElement.selectionEnd).to.equal(12);
+ el.selectionStart = 5;
+ el.selectionEnd = 12;
+ expect(el.inputElement.selectionStart).to.equal(5);
+ expect(el.inputElement.selectionEnd).to.equal(12);
});
});
});
diff --git a/packages/fieldset/src/LionFieldset.js b/packages/fieldset/src/LionFieldset.js
index 9adc8a3c1..c958f11f8 100644
--- a/packages/fieldset/src/LionFieldset.js
+++ b/packages/fieldset/src/LionFieldset.js
@@ -1,6 +1,6 @@
import { SlotMixin, html } from '@lion/core';
import { LionLitElement } from '@lion/core/src/LionLitElement.js';
-import { CssClassMixin } from '@lion/core/src/CssClassMixin.js';
+import { DisabledMixin } from '@lion/core/src/DisabledMixin.js';
import { ObserverMixin } from '@lion/core/src/ObserverMixin.js';
import { ValidateMixin } from '@lion/validate';
import { FormControlMixin, FormRegistrarMixin } from '@lion/field';
@@ -15,16 +15,10 @@ const pascalCase = str => str.charAt(0).toUpperCase() + str.slice(1);
* @extends LionLitElement
*/
export class LionFieldset extends FormRegistrarMixin(
- FormControlMixin(ValidateMixin(CssClassMixin(SlotMixin(ObserverMixin(LionLitElement))))),
+ FormControlMixin(ValidateMixin(DisabledMixin(SlotMixin(ObserverMixin(LionLitElement))))),
) {
static get properties() {
return {
- ...super.properties,
- disabled: {
- type: Boolean,
- reflect: true,
- nonEmptyToClass: 'state-disabled',
- },
name: {
type: String,
},
@@ -35,13 +29,6 @@ export class LionFieldset extends FormRegistrarMixin(
};
}
- static get asyncObservers() {
- return {
- ...super.asyncObservers,
- _onDisabledChanged: ['disabled'],
- };
- }
-
get inputElement() {
return this;
}
@@ -121,6 +108,36 @@ export class LionFieldset extends FormRegistrarMixin(
this.removeEventListener('dirty-changed', this._updateDirtyClass);
}
+ updated(changedProps) {
+ super.updated(changedProps);
+
+ if (changedProps.has('disabled')) {
+ if (this.disabled) {
+ this.__requestChildrenToBeDisabled();
+ this.classList.add('state-disabled'); // eslint-disable-line wc/no-self-class
+ } else {
+ this.__retractRequestChildrenToBeDisabled();
+ this.classList.remove('state-disabled'); // eslint-disable-line wc/no-self-class
+ }
+ }
+ }
+
+ __requestChildrenToBeDisabled() {
+ this.formElementsArray.forEach(child => {
+ if (child.makeRequestToBeDisabled) {
+ child.makeRequestToBeDisabled();
+ }
+ });
+ }
+
+ __retractRequestChildrenToBeDisabled() {
+ this.formElementsArray.forEach(child => {
+ if (child.retractRequestToBeDisabled) {
+ child.retractRequestToBeDisabled();
+ }
+ });
+ }
+
// eslint-disable-next-line class-methods-use-this
inputGroupTemplate() {
return html`
@@ -251,13 +268,6 @@ export class LionFieldset extends FormRegistrarMixin(
this.classList[this.dirty ? 'add' : 'remove']('state-dirty');
}
- _onDisabledChanged({ disabled }, { disabled: oldDisabled }) {
- // do not propagate/override inital disabled value on nested form elements
- if (typeof oldDisabled !== 'undefined') {
- this._setValueForAllFormElements('disabled', disabled);
- }
- }
-
_setRole(role) {
this.setAttribute('role', role || 'group');
}
@@ -303,7 +313,7 @@ export class LionFieldset extends FormRegistrarMixin(
if (this.disabled) {
// eslint-disable-next-line no-param-reassign
- child.disabled = true;
+ child.makeRequestToBeDisabled();
}
if (name.substr(-2) === '[]') {
if (!Array.isArray(this.formElements[name])) {
diff --git a/packages/fieldset/stories/index.stories.js b/packages/fieldset/stories/index.stories.js
index b84d58863..e4b291b93 100644
--- a/packages/fieldset/stories/index.stories.js
+++ b/packages/fieldset/stories/index.stories.js
@@ -31,6 +31,30 @@ storiesOf('Forms|Fieldset', module)
`,
)
+ .add('Disabled', () => {
+ function toggleDisabled() {
+ const fieldset = document.querySelector('#fieldset');
+ fieldset.disabled = !fieldset.disabled;
+ }
+ return html`
+
+
+
+
+
+
+
+
+
+ `;
+ })
.add(
'Sub Fieldsets Data',
() => html`
diff --git a/packages/fieldset/test/lion-fieldset.test.js b/packages/fieldset/test/lion-fieldset.test.js
index cd33424b4..ae9b25219 100644
--- a/packages/fieldset/test/lion-fieldset.test.js
+++ b/packages/fieldset/test/lion-fieldset.test.js
@@ -223,12 +223,11 @@ describe('', () => {
expect(el.formElements['hobbies[]'][1].disabled).to.equal(false);
});
- it('does not propagate/override inital disabled value on nested form elements', async () => {
+ it('does not propagate/override initial disabled value on nested form elements', async () => {
const el = await fixture(
`<${tagString}><${tagString} name="sub" disabled>${inputSlotString}${tagString}>${tagString}>`,
);
- await nextFrame();
-
+ await el.updateComplete;
expect(el.disabled).to.equal(false);
expect(el.formElements.sub.disabled).to.equal(true);
expect(el.formElements.sub.formElements.color.disabled).to.equal(true);
@@ -236,6 +235,16 @@ describe('', () => {
expect(el.formElements.sub.formElements['hobbies[]'][1].disabled).to.equal(true);
});
+ // classes are added only for backward compatibility - they are deprecated
+ it('sets a state-disabled class when disabled', async () => {
+ const el = await fixture(`<${tagString} disabled>${inputSlotString}${tagString}>`);
+ await nextFrame();
+ expect(el.classList.contains('state-disabled')).to.equal(true);
+ el.disabled = false;
+ await nextFrame();
+ expect(el.classList.contains('state-disabled')).to.equal(false);
+ });
+
describe('validation', () => {
it('validates on init', async () => {
function isCat(value) {
@@ -715,7 +724,7 @@ describe('', () => {
childAriaFixture = async (
msgSlotType = 'feedback', // eslint-disable-line no-shadow
) => {
- const dom = fixture(`
+ const dom = await fixture(`
@@ -750,14 +759,12 @@ describe('', () => {
`);
- await nextFrame();
return dom;
};
// eslint-disable-next-line no-shadow
childAriaTest = childAriaFixture => {
/* eslint-disable camelcase */
-
// Message elements: all elements pointed at by inputs
const msg_l1_g = childAriaFixture.querySelector('#msg_l1_g');
const msg_l1_fa = childAriaFixture.querySelector('#msg_l1_fa');
@@ -767,10 +774,10 @@ describe('', () => {
const msg_l2_fb = childAriaFixture.querySelector('#msg_l2_fb');
// Field elements: all inputs pointing to message elements
- const input_l1_fa = childAriaFixture.querySelector('[name=l1_fa]');
- const input_l1_fb = childAriaFixture.querySelector('[name=l1_fb]');
- const input_l2_fa = childAriaFixture.querySelector('[name=l2_fa]');
- const input_l2_fb = childAriaFixture.querySelector('[name=l2_fb]');
+ const input_l1_fa = childAriaFixture.querySelector('input[name=l1_fa]');
+ const input_l1_fb = childAriaFixture.querySelector('input[name=l1_fb]');
+ const input_l2_fa = childAriaFixture.querySelector('input[name=l2_fa]');
+ const input_l2_fb = childAriaFixture.querySelector('input[name=l2_fb]');
/* eslint-enable camelcase */
diff --git a/packages/form/src/LionForm.js b/packages/form/src/LionForm.js
index 442532091..9c3b084c6 100644
--- a/packages/form/src/LionForm.js
+++ b/packages/form/src/LionForm.js
@@ -1,4 +1,3 @@
-import { DelegateMixin } from '@lion/core';
import { LionFieldset } from '@lion/fieldset';
/**
@@ -8,50 +7,58 @@ import { LionFieldset } from '@lion/fieldset';
* @extends LionFieldset
*/
// eslint-disable-next-line no-unused-vars
-export class LionForm extends DelegateMixin(LionFieldset) {
- get delegations() {
- return {
- ...super.delegations,
- target: () => this.formElement,
- events: [...super.delegations.events, 'submit', 'reset'],
- methods: [...super.delegations.methods, 'submit', 'reset'],
- };
- }
-
- constructor() {
- super();
- this.__boundSubmit = this._submit.bind(this);
- this.__boundReset = this._reset.bind(this);
- }
-
+export class LionForm extends LionFieldset {
connectedCallback() {
- super.connectedCallback();
- this.addEventListener('submit', this.__boundSubmit);
- this.addEventListener('reset', this.__boundReset);
+ if (super.connectedCallback) {
+ super.connectedCallback();
+ }
+ this.__registerEventsForLionForm();
}
disconnectedCallback() {
- super.disconnectedCallback();
- this.removeEventListener('submit', this.__boundSubmit);
- this.removeEventListener('reset', this.__boundReset);
+ if (super.disconnectedCallback) {
+ super.disconnectedCallback();
+ }
+ this.__teardownEventsForLionForm();
}
get formElement() {
return this.querySelector('form');
}
+ submit() {
+ this.formElement.submit();
+ }
+
+ reset() {
+ this.formElement.reset();
+ }
+
/**
* As we use a native form there is no need for a role
*/
_setRole() {} // eslint-disable-line class-methods-use-this
- _submit(ev) {
- ev.preventDefault();
- this.submitGroup();
+ __registerEventsForLionForm() {
+ this._submit = ev => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.dispatchEvent(new Event('submit', { bubbles: true }));
+ this.submitGroup();
+ };
+ this.formElement.addEventListener('submit', this._submit);
+
+ this._reset = ev => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.dispatchEvent(new Event('reset', { bubbles: true }));
+ this.resetGroup();
+ };
+ this.formElement.addEventListener('reset', this._reset);
}
- _reset(ev) {
- ev.preventDefault();
- this.resetGroup();
+ __teardownEventsForLionForm() {
+ this.formElement.removeEventListener('submit', this._submit);
+ this.formElement.removeEventListener('rest', this._reset);
}
}
diff --git a/packages/form/test/lion-form.test.js b/packages/form/test/lion-form.test.js
index 4389976b0..f1edbdb99 100644
--- a/packages/form/test/lion-form.test.js
+++ b/packages/form/test/lion-form.test.js
@@ -1,4 +1,4 @@
-import { expect, fixture, html } from '@open-wc/testing';
+import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import { spy } from 'sinon';
import '@lion/input/lion-input.js';
@@ -7,7 +7,7 @@ import '@lion/fieldset/lion-fieldset.js';
import '../lion-form.js';
describe('', () => {
- it.skip('has a custom reset that gets triggered by native reset', async () => {
+ it('has a custom reset that gets triggered by native reset', async () => {
const withDefaults = await fixture(html`