fix: assure proper typings _inputNode across lion fields
This commit is contained in:
parent
c03ebde5b5
commit
0aa4480e0c
16 changed files with 535 additions and 434 deletions
10
.changeset/small-pens-applaud.md
Normal file
10
.changeset/small-pens-applaud.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
'@lion/fieldset': patch
|
||||
'@lion/form-core': patch
|
||||
'@lion/input': patch
|
||||
'@lion/select': patch
|
||||
'@lion/switch': patch
|
||||
'@lion/textarea': patch
|
||||
---
|
||||
|
||||
Refactor of some fields to ensure that \_inputNode has the right type. It starts as HTMLElement for LionField, and all HTMLInputElement, HTMLSelectElement and HTMLTextAreaElement logic, are moved to the right places.
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { runFormGroupMixinSuite } from '@lion/form-core/test-suites/form-group/FormGroupMixin.suite.js';
|
||||
import { runFormGroupMixinInputSuite } from '@lion/form-core/test-suites/form-group/FormGroupMixin-input.suite.js';
|
||||
import '../lion-fieldset.js';
|
||||
|
||||
runFormGroupMixinSuite({ tagString: 'lion-fieldset' });
|
||||
runFormGroupMixinInputSuite({ tagString: 'lion-fieldset' });
|
||||
|
|
|
|||
|
|
@ -39,56 +39,7 @@ export class LionField extends FormControlMixin(
|
|||
}
|
||||
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get selectionStart() {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionStart) {
|
||||
return native.selectionStart;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
set selectionStart(value) {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionStart) {
|
||||
native.selectionStart = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get selectionEnd() {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionEnd) {
|
||||
return native.selectionEnd;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
set selectionEnd(value) {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionEnd) {
|
||||
native.selectionEnd = value;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
|
||||
/** @type {string} */
|
||||
set value(value) {
|
||||
// if not yet connected to dom can't change the value
|
||||
if (this._inputNode) {
|
||||
this._setValueAndPreserveCaret(value);
|
||||
/** @type {string | undefined} */
|
||||
this.__value = undefined;
|
||||
} else {
|
||||
this.__value = value;
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
return (this._inputNode && this._inputNode.value) || this.__value || '';
|
||||
return /** @type {HTMLElement} */ (super._inputNode); // casts type
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
|
@ -119,26 +70,6 @@ export class LionField extends FormControlMixin(
|
|||
this._inputNode.removeEventListener('change', this._onChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this._inputNode.disabled = this.disabled;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
if (changedProperties.has('name')) {
|
||||
this._inputNode.name = this.name;
|
||||
}
|
||||
|
||||
if (changedProperties.has('autocomplete')) {
|
||||
this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete);
|
||||
}
|
||||
}
|
||||
|
||||
resetInteractionState() {
|
||||
super.resetInteractionState();
|
||||
this.submitted = false;
|
||||
|
|
@ -164,30 +95,4 @@ export class LionField extends FormControlMixin(
|
|||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the cursor to its original position after updating the value.
|
||||
* @param {string} newValue The value that should be saved.
|
||||
*/
|
||||
_setValueAndPreserveCaret(newValue) {
|
||||
// Only preserve caret if focused (changing selectionStart will move focus in Safari)
|
||||
if (this.focused) {
|
||||
// Not all elements might have selection, and even if they have the
|
||||
// right properties, accessing them might throw an exception (like for
|
||||
// <input type=number>)
|
||||
try {
|
||||
const start = this._inputNode.selectionStart;
|
||||
this._inputNode.value = newValue;
|
||||
// The cursor automatically jumps to the end after re-setting the value,
|
||||
// so restore it to its original position.
|
||||
this._inputNode.selectionStart = start;
|
||||
this._inputNode.selectionEnd = start;
|
||||
} catch (error) {
|
||||
// Just set the value and give up on the caret.
|
||||
this._inputNode.value = newValue;
|
||||
}
|
||||
} else {
|
||||
this._inputNode.value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
packages/form-core/src/ValueMixin.js
Normal file
57
packages/form-core/src/ValueMixin.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { dedupeMixin } from '@lion/core';
|
||||
|
||||
/**
|
||||
* @typedef {import('../types/ValueMixinTypes').ValueMixin} ValueMixin
|
||||
* @type {ValueMixin}
|
||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('../types/ValueMixinTypes').LionFieldWithValue>} superclass} superclass
|
||||
*/
|
||||
const ValueMixinImplementation = superclass =>
|
||||
class ValueMixin extends superclass {
|
||||
get value() {
|
||||
return (this._inputNode && this._inputNode.value) || this.__value || '';
|
||||
}
|
||||
|
||||
// We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret
|
||||
/** @type {string} */
|
||||
set value(value) {
|
||||
// if not yet connected to dom can't change the value
|
||||
if (this._inputNode) {
|
||||
this._setValueAndPreserveCaret(value);
|
||||
/** @type {string | undefined} */
|
||||
this.__value = undefined;
|
||||
} else {
|
||||
this.__value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the cursor to its original position after updating the value.
|
||||
* @param {string} newValue The value that should be saved.
|
||||
*/
|
||||
_setValueAndPreserveCaret(newValue) {
|
||||
// Only preserve caret if focused (changing selectionStart will move focus in Safari)
|
||||
if (this.focused) {
|
||||
// Not all elements might have selection, and even if they have the
|
||||
// right properties, accessing them might throw an exception (like for
|
||||
// <input type=number>)
|
||||
try {
|
||||
// SelectElement doesn't have selectionStart/selectionEnd
|
||||
if (!(this._inputNode instanceof HTMLSelectElement)) {
|
||||
const start = this._inputNode.selectionStart;
|
||||
this._inputNode.value = newValue;
|
||||
// The cursor automatically jumps to the end after re-setting the value,
|
||||
// so restore it to its original position.
|
||||
this._inputNode.selectionStart = start;
|
||||
this._inputNode.selectionEnd = start;
|
||||
}
|
||||
} catch (error) {
|
||||
// Just set the value and give up on the caret.
|
||||
this._inputNode.value = newValue;
|
||||
}
|
||||
} else {
|
||||
this._inputNode.value = newValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ValueMixin = dedupeMixin(ValueMixinImplementation);
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||
import { defineCE, expect, html, unsafeStatic, fixture } from '@open-wc/testing';
|
||||
import { LionInput } from '@lion/input';
|
||||
import '@lion/form-core/lion-field.js';
|
||||
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
||||
|
||||
/**
|
||||
* @param {{ tagString?: string, childTagString?:string }} [cfg]
|
||||
*/
|
||||
export function runFormGroupMixinInputSuite(cfg = {}) {
|
||||
const FormChild = class extends LionInput {
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
input: () => document.createElement('input'),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const childTagString = cfg.childTagString || defineCE(FormChild);
|
||||
|
||||
// @ts-expect-error
|
||||
const FormGroup = class extends FormGroupMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
/** @override from FormRegistrarMixin */
|
||||
this._isFormOrFieldset = true;
|
||||
this._repropagationRole = 'fieldset'; // configures FormControlMixin
|
||||
}
|
||||
};
|
||||
|
||||
const tagString = cfg.tagString || defineCE(FormGroup);
|
||||
const tag = unsafeStatic(tagString);
|
||||
const childTag = unsafeStatic(childTagString);
|
||||
|
||||
beforeEach(() => {
|
||||
localizeTearDown();
|
||||
});
|
||||
|
||||
describe('FormGroupMixin with LionInput', () => {
|
||||
it('serializes undefined values as "" (nb radios/checkboxes are always serialized)', async () => {
|
||||
const fieldset = /** @type {FormGroup} */ (await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
</${tag}>
|
||||
`));
|
||||
console.log(fieldset.formElements['custom[]'][1]);
|
||||
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
|
||||
fieldset.formElements['custom[]'][1].modelValue = undefined;
|
||||
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
'custom[]': ['custom 1', ''],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Screen reader relations (aria-describedby) for child inputs and fieldsets', () => {
|
||||
/** @type {Function} */
|
||||
let childAriaFixture;
|
||||
/** @type {Function} */
|
||||
let childAriaTest;
|
||||
|
||||
before(() => {
|
||||
// Legend:
|
||||
// - l1 means level 1 (outer) fieldset
|
||||
// - l2 means level 2 (inner) fieldset
|
||||
// - g means group: the help-text or feedback belongs to group
|
||||
// - f means field(lion-input in fixture below): the help-text or feedback belongs to field
|
||||
// - 'a' or 'b' behind 'f' indicate which field in a fieldset is meant (a: first, b: second)
|
||||
|
||||
childAriaFixture = async (
|
||||
msgSlotType = 'feedback', // eslint-disable-line no-shadow
|
||||
) => {
|
||||
const dom = /** @type {FormGroup} */ (await fixture(html`
|
||||
<${tag} name="l1_g">
|
||||
<${childTag} name="l1_fa">
|
||||
<div slot="${msgSlotType}" id="msg_l1_fa"></div>
|
||||
<!-- field referred by: #msg_l1_fa (local), #msg_l1_g (parent/group) -->
|
||||
</${childTag}>
|
||||
|
||||
<${childTag} name="l1_fb">
|
||||
<div slot="${msgSlotType}" id="msg_l1_fb"></div>
|
||||
<!-- field referred by: #msg_l1_fb (local), #msg_l1_g (parent/group) -->
|
||||
</${childTag}>
|
||||
|
||||
<!-- [ INNER FIELDSET ] -->
|
||||
|
||||
<${tag} name="l2_g">
|
||||
<${childTag} name="l2_fa">
|
||||
<div slot="${msgSlotType}" id="msg_l2_fa"></div>
|
||||
<!-- field referred by: #msg_l2_fa (local), #msg_l2_g (parent/group), #msg_l1_g (grandparent/group.group) -->
|
||||
</${childTag}>
|
||||
|
||||
<${childTag} name="l2_fb">
|
||||
<div slot="${msgSlotType}" id="msg_l2_fb"></div>
|
||||
<!-- field referred by: #msg_l2_fb (local), #msg_l2_g (parent/group), #msg_l1_g (grandparent/group.group) -->
|
||||
</${childTag}>
|
||||
|
||||
<div slot="${msgSlotType}" id="msg_l2_g"></div>
|
||||
<!-- group referred by: #msg_l2_g (local), #msg_l1_g (parent/group) -->
|
||||
</${tag}>
|
||||
|
||||
<!-- [ / INNER FIELDSET ] -->
|
||||
|
||||
<div slot="${msgSlotType}" id="msg_l1_g"></div>
|
||||
<!-- group referred by: #msg_l1_g (local) -->
|
||||
</${tag}>
|
||||
`));
|
||||
return dom;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
childAriaTest = (/** @type {FormGroup} */ childAriaFixture) => {
|
||||
/* eslint-disable camelcase */
|
||||
// Message elements: all elements pointed at by inputs
|
||||
const msg_l1_g = /** @type {FormGroup} */ (childAriaFixture.querySelector('#msg_l1_g'));
|
||||
const msg_l1_fa = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l1_fa'));
|
||||
const msg_l1_fb = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l1_fb'));
|
||||
const msg_l2_g = /** @type {FormGroup} */ (childAriaFixture.querySelector('#msg_l2_g'));
|
||||
const msg_l2_fa = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l2_fa'));
|
||||
const msg_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector('#msg_l2_fb'));
|
||||
|
||||
// Field elements: all inputs pointing to message elements
|
||||
const input_l1_fa = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l1_fa]',
|
||||
));
|
||||
const input_l1_fb = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l1_fb]',
|
||||
));
|
||||
const input_l2_fa = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l2_fa]',
|
||||
));
|
||||
const input_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l2_fb]',
|
||||
));
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
// 'L1' fields (inside lion-fieldset[name="l1_g"]) should point to l1(group) msg
|
||||
expect(input_l1_fa.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l1 input(a) refers parent/group',
|
||||
);
|
||||
expect(input_l1_fb.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l1 input(b) refers parent/group',
|
||||
);
|
||||
|
||||
// Also check that aria-describedby of the inputs are not overridden (this relation was
|
||||
// put there in lion-input(using lion-field)).
|
||||
expect(input_l1_fa.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_fa.id,
|
||||
'l1 input(a) refers local field',
|
||||
);
|
||||
expect(input_l1_fb.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_fb.id,
|
||||
'l1 input(b) refers local field',
|
||||
);
|
||||
|
||||
// Also make feedback element point to nested fieldset inputs
|
||||
expect(input_l2_fa.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l2 input(a) refers grandparent/group.group',
|
||||
);
|
||||
expect(input_l2_fb.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l2 input(b) refers grandparent/group.group',
|
||||
);
|
||||
|
||||
// Check order: the nearest ('dom wise': so 1. local, 2. parent, 3. grandparent) message
|
||||
// should be read first by screen reader
|
||||
const dA = /** @type {string} */ (input_l2_fa.getAttribute('aria-describedby'));
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
dA.indexOf(msg_l2_fa.id) < dA.indexOf(msg_l2_g.id) < dA.indexOf(msg_l1_g.id),
|
||||
).to.equal(true, 'order of ids');
|
||||
const dB = input_l2_fb.getAttribute('aria-describedby');
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
dB.indexOf(msg_l2_fb.id) < dB.indexOf(msg_l2_g.id) < dB.indexOf(msg_l1_g.id),
|
||||
).to.equal(true, 'order of ids');
|
||||
};
|
||||
|
||||
it(`reads feedback message belonging to fieldset when child input is focused
|
||||
(via aria-describedby)`, async () => {
|
||||
childAriaTest(await childAriaFixture('feedback'));
|
||||
});
|
||||
|
||||
it(`reads help-text message belonging to fieldset when child input is focused
|
||||
(via aria-describedby)`, async () => {
|
||||
childAriaTest(await childAriaFixture('help-text'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
aTimeout,
|
||||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { LionField, IsNumber, Validator } from '@lion/form-core';
|
||||
import { IsNumber, Validator, LionField } from '@lion/form-core';
|
||||
import '@lion/form-core/lion-field.js';
|
||||
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
||||
|
||||
|
|
@ -710,21 +710,6 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
expect(fieldset.serializedValue).to.deep.equal({ price: 0 });
|
||||
});
|
||||
|
||||
it('serializes undefined values as ""(nb radios/checkboxes are always serialized)', async () => {
|
||||
const fieldset = /** @type {FormGroup} */ (await fixture(html`
|
||||
<${tag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
<${childTag} name="custom[]"></${childTag}>
|
||||
</${tag}>
|
||||
`));
|
||||
fieldset.formElements['custom[]'][0].modelValue = 'custom 1';
|
||||
fieldset.formElements['custom[]'][1].modelValue = undefined;
|
||||
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
'custom[]': ['custom 1', ''],
|
||||
});
|
||||
});
|
||||
|
||||
it('allows for nested fieldsets', async () => {
|
||||
const fieldset = /** @type {FormGroup} */ (await fixture(html`
|
||||
<${tag} name="userData">
|
||||
|
|
@ -1119,153 +1104,6 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
expect(el.hasAttribute('aria-labelledby')).to.equal(true);
|
||||
expect(el.getAttribute('aria-labelledby')).contains(label.id);
|
||||
});
|
||||
|
||||
describe('Screen reader relations (aria-describedby) for child fields and fieldsets', () => {
|
||||
/** @type {Function} */
|
||||
let childAriaFixture;
|
||||
/** @type {Function} */
|
||||
let childAriaTest;
|
||||
|
||||
before(() => {
|
||||
// Legend:
|
||||
// - l1 means level 1 (outer) fieldset
|
||||
// - l2 means level 2 (inner) fieldset
|
||||
// - g means group: the help-text or feedback belongs to group
|
||||
// - f means field(lion-input in fixture below): the help-text or feedback belongs to field
|
||||
// - 'a' or 'b' behind 'f' indicate which field in a fieldset is meant (a: first, b: second)
|
||||
|
||||
childAriaFixture = async (
|
||||
msgSlotType = 'feedback', // eslint-disable-line no-shadow
|
||||
) => {
|
||||
const dom = /** @type {FormGroup} */ (await fixture(html`
|
||||
<${tag} name="l1_g">
|
||||
<${childTag} name="l1_fa">
|
||||
<div slot="${msgSlotType}" id="msg_l1_fa"></div>
|
||||
<!-- field referred by: #msg_l1_fa (local), #msg_l1_g (parent/group) -->
|
||||
</${childTag}>
|
||||
|
||||
<${childTag} name="l1_fb">
|
||||
<div slot="${msgSlotType}" id="msg_l1_fb"></div>
|
||||
<!-- field referred by: #msg_l1_fb (local), #msg_l1_g (parent/group) -->
|
||||
</${childTag}>
|
||||
|
||||
<!-- [ INNER FIELDSET ] -->
|
||||
|
||||
<${tag} name="l2_g">
|
||||
<${childTag} name="l2_fa">
|
||||
<div slot="${msgSlotType}" id="msg_l2_fa"></div>
|
||||
<!-- field referred by: #msg_l2_fa (local), #msg_l2_g (parent/group), #msg_l1_g (grandparent/group.group) -->
|
||||
</${childTag}>
|
||||
|
||||
<${childTag} name="l2_fb">
|
||||
<div slot="${msgSlotType}" id="msg_l2_fb"></div>
|
||||
<!-- field referred by: #msg_l2_fb (local), #msg_l2_g (parent/group), #msg_l1_g (grandparent/group.group) -->
|
||||
</${childTag}>
|
||||
|
||||
<div slot="${msgSlotType}" id="msg_l2_g"></div>
|
||||
<!-- group referred by: #msg_l2_g (local), #msg_l1_g (parent/group) -->
|
||||
</${tag}>
|
||||
|
||||
<!-- [ / INNER FIELDSET ] -->
|
||||
|
||||
<div slot="${msgSlotType}" id="msg_l1_g"></div>
|
||||
<!-- group referred by: #msg_l1_g (local) -->
|
||||
</${tag}>
|
||||
`));
|
||||
return dom;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
childAriaTest = (/** @type {FormGroup} */ childAriaFixture) => {
|
||||
/* eslint-disable camelcase */
|
||||
// Message elements: all elements pointed at by inputs
|
||||
const msg_l1_g = /** @type {FormGroup} */ (childAriaFixture.querySelector('#msg_l1_g'));
|
||||
const msg_l1_fa = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'#msg_l1_fa',
|
||||
));
|
||||
const msg_l1_fb = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'#msg_l1_fb',
|
||||
));
|
||||
const msg_l2_g = /** @type {FormGroup} */ (childAriaFixture.querySelector('#msg_l2_g'));
|
||||
const msg_l2_fa = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'#msg_l2_fa',
|
||||
));
|
||||
const msg_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'#msg_l2_fb',
|
||||
));
|
||||
|
||||
// Field elements: all inputs pointing to message elements
|
||||
const input_l1_fa = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l1_fa]',
|
||||
));
|
||||
const input_l1_fb = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l1_fb]',
|
||||
));
|
||||
const input_l2_fa = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l2_fa]',
|
||||
));
|
||||
const input_l2_fb = /** @type {FormChild} */ (childAriaFixture.querySelector(
|
||||
'input[name=l2_fb]',
|
||||
));
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
// 'L1' fields (inside lion-fieldset[name="l1_g"]) should point to l1(group) msg
|
||||
expect(input_l1_fa.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l1 input(a) refers parent/group',
|
||||
);
|
||||
expect(input_l1_fb.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l1 input(b) refers parent/group',
|
||||
);
|
||||
|
||||
// Also check that aria-describedby of the inputs are not overridden (this relation was
|
||||
// put there in lion-input(using lion-field)).
|
||||
expect(input_l1_fa.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_fa.id,
|
||||
'l1 input(a) refers local field',
|
||||
);
|
||||
expect(input_l1_fb.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_fb.id,
|
||||
'l1 input(b) refers local field',
|
||||
);
|
||||
|
||||
// Also make feedback element point to nested fieldset inputs
|
||||
expect(input_l2_fa.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l2 input(a) refers grandparent/group.group',
|
||||
);
|
||||
expect(input_l2_fb.getAttribute('aria-describedby')).to.contain(
|
||||
msg_l1_g.id,
|
||||
'l2 input(b) refers grandparent/group.group',
|
||||
);
|
||||
|
||||
// Check order: the nearest ('dom wise': so 1. local, 2. parent, 3. grandparent) message
|
||||
// should be read first by screen reader
|
||||
const dA = /** @type {string} */ (input_l2_fa.getAttribute('aria-describedby'));
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
dA.indexOf(msg_l2_fa.id) < dA.indexOf(msg_l2_g.id) < dA.indexOf(msg_l1_g.id),
|
||||
).to.equal(true, 'order of ids');
|
||||
const dB = input_l2_fb.getAttribute('aria-describedby');
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
dB.indexOf(msg_l2_fb.id) < dB.indexOf(msg_l2_g.id) < dB.indexOf(msg_l1_g.id),
|
||||
).to.equal(true, 'order of ids');
|
||||
};
|
||||
});
|
||||
|
||||
it(`reads feedback message belonging to fieldset when child input is focused
|
||||
(via aria-describedby)`, async () => {
|
||||
childAriaTest(await childAriaFixture('feedback'));
|
||||
});
|
||||
|
||||
it(`reads help-text message belonging to fieldset when child input is focused
|
||||
(via aria-describedby)`, async () => {
|
||||
childAriaTest(await childAriaFixture('help-text'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { defineCE } from '@open-wc/testing';
|
||||
import { runInteractionStateMixinSuite } from '../test-suites/InteractionStateMixin.suite.js';
|
||||
import { LionField } from '../src/LionField.js';
|
||||
import { runFormatMixinSuite } from '../test-suites/FormatMixin.suite.js';
|
||||
|
||||
const fieldTagString = defineCE(
|
||||
class extends LionField {
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
// LionField needs to have an _inputNode defined in order to work...
|
||||
input: () => document.createElement('input'),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe('<lion-field> integrations', () => {
|
||||
runInteractionStateMixinSuite({
|
||||
tagString: fieldTagString,
|
||||
});
|
||||
|
||||
runFormatMixinSuite({
|
||||
tagString: fieldTagString,
|
||||
});
|
||||
});
|
||||
|
|
@ -3,7 +3,6 @@ import { localize } from '@lion/localize';
|
|||
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||
import { Required, Validator } from '@lion/form-core';
|
||||
import {
|
||||
aTimeout,
|
||||
expect,
|
||||
fixture,
|
||||
html,
|
||||
|
|
@ -123,21 +122,6 @@ describe('<lion-field>', () => {
|
|||
expect(el.focused).to.equal(false);
|
||||
});
|
||||
|
||||
it('can be disabled via attribute', async () => {
|
||||
const elDisabled = /** @type {LionField} */ (await fixture(
|
||||
html`<${tag} disabled>${inputSlot}</${tag}>`,
|
||||
));
|
||||
expect(elDisabled.disabled).to.equal(true);
|
||||
expect(elDisabled._inputNode.disabled).to.equal(true);
|
||||
});
|
||||
|
||||
it('can be disabled via property', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.disabled).to.equal(true);
|
||||
});
|
||||
|
||||
it('can be cleared which erases value, validation and interaction states', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(
|
||||
html`<${tag} value="Some value from attribute">${inputSlot}</${tag}>`,
|
||||
|
|
@ -161,60 +145,6 @@ describe('<lion-field>', () => {
|
|||
expect(el.modelValue).to.equal('foo');
|
||||
});
|
||||
|
||||
it('reads initial value from attribute value', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(
|
||||
html`<${tag} value="one">${inputSlot}</${tag}>`,
|
||||
));
|
||||
expect(getSlot(el, 'input').value).to.equal('one');
|
||||
});
|
||||
|
||||
it('delegates value property', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||
expect(getSlot(el, 'input').value).to.equal('');
|
||||
el.value = 'one';
|
||||
expect(el.value).to.equal('one');
|
||||
expect(getSlot(el, 'input').value).to.equal('one');
|
||||
});
|
||||
|
||||
// This is necessary for security, so that _inputNodes autocomplete can be set to 'off'
|
||||
it('delegates autocomplete property', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||
expect(el._inputNode.autocomplete).to.equal('');
|
||||
expect(el._inputNode.hasAttribute('autocomplete')).to.be.false;
|
||||
el.autocomplete = 'off';
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.autocomplete).to.equal('off');
|
||||
expect(el._inputNode.getAttribute('autocomplete')).to.equal('off');
|
||||
});
|
||||
|
||||
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||
await triggerFocusFor(el);
|
||||
await el.updateComplete;
|
||||
el._inputNode.value = 'hello world';
|
||||
el._inputNode.selectionStart = 2;
|
||||
el._inputNode.selectionEnd = 2;
|
||||
el.value = 'hey there universe';
|
||||
expect(el._inputNode.selectionStart).to.equal(2);
|
||||
expect(el._inputNode.selectionEnd).to.equal(2);
|
||||
});
|
||||
|
||||
// TODO: Add test that css pointerEvents is none if disabled.
|
||||
it('is disabled when disabled property is passed', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(false);
|
||||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
await aTimeout(0);
|
||||
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
|
||||
const disabledel = /** @type {LionField} */ (await fixture(
|
||||
html`<${tag} disabled>${inputSlot}</${tag}>`,
|
||||
));
|
||||
expect(disabledel._inputNode.hasAttribute('disabled')).to.equal(true);
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it(`by setting corresponding aria-labelledby (for label) and aria-describedby (for helpText, feedback)
|
||||
~~~
|
||||
|
|
@ -437,37 +367,6 @@ describe('<lion-field>', () => {
|
|||
expect(disabledEl.validationStates.error).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('should remove validation when disabled state toggles', async () => {
|
||||
const HasX = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'HasX';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
execute(value) {
|
||||
const result = value.indexOf('x') === -1;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
const el = /** @type {LionField} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new HasX()]}
|
||||
.modelValue=${'a@b.nl'}
|
||||
>
|
||||
${inputSlot}
|
||||
</${tag}>
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
expect(el.validationStates.error.HasX).to.exist;
|
||||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||
expect(el.validationStates.error).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('can be required', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`
|
||||
<${tag}
|
||||
|
|
@ -555,27 +454,4 @@ describe('<lion-field>', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delegation', () => {
|
||||
it('delegates property value', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
el.value = 'one';
|
||||
expect(el.value).to.equal('one');
|
||||
expect(el._inputNode.value).to.equal('one');
|
||||
});
|
||||
|
||||
it('delegates property selectionStart and selectionEnd', async () => {
|
||||
const el = /** @type {LionField} */ (await fixture(html`
|
||||
<${tag}
|
||||
.modelValue=${'Some text to select'}
|
||||
>${unsafeHTML(inputSlotString)}</${tag}>
|
||||
`));
|
||||
|
||||
el.selectionStart = 5;
|
||||
el.selectionEnd = 12;
|
||||
expect(el._inputNode.selectionStart).to.equal(5);
|
||||
expect(el._inputNode.selectionEnd).to.equal(12);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
18
packages/form-core/types/ValueMixinTypes.d.ts
vendored
Normal file
18
packages/form-core/types/ValueMixinTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||
import { LionField } from '@lion/form-core/src/LionField';
|
||||
|
||||
export declare class LionFieldWithValue extends LionField {
|
||||
get _inputNode(): HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement;
|
||||
}
|
||||
|
||||
export declare class ValueHost {
|
||||
get value(): string;
|
||||
set value(value: string);
|
||||
_setValueAndPreserveCaret(newValue: string): void;
|
||||
}
|
||||
|
||||
export declare function ValueImplementation<T extends Constructor<LionFieldWithValue>>(
|
||||
superclass: T,
|
||||
): T & Constructor<ValueHost> & ValueHost;
|
||||
|
||||
export type ValueMixin = typeof ValueImplementation;
|
||||
|
|
@ -63,7 +63,7 @@ export declare class ChoiceInputHost {
|
|||
|
||||
type: string;
|
||||
|
||||
_inputNode: HTMLInputElement;
|
||||
_inputNode: HTMLElement;
|
||||
}
|
||||
|
||||
export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { LionField } from '@lion/form-core';
|
||||
import { ValueMixin } from '@lion/form-core/src/ValueMixin';
|
||||
|
||||
/**
|
||||
* LionInput: extension of lion-field with native input element in place and user friendly API.
|
||||
|
|
@ -7,7 +8,7 @@ import { LionField } from '@lion/form-core';
|
|||
* @extends {LionField}
|
||||
*/
|
||||
// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you.
|
||||
export class LionInput extends LionField {
|
||||
export class LionInput extends ValueMixin(LionField) {
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
|
|
@ -49,6 +50,42 @@ export class LionInput extends LionField {
|
|||
};
|
||||
}
|
||||
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get selectionStart() {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionStart) {
|
||||
return native.selectionStart;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
set selectionStart(value) {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionStart) {
|
||||
native.selectionStart = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
get selectionEnd() {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionEnd) {
|
||||
return native.selectionEnd;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
set selectionEnd(value) {
|
||||
const native = this._inputNode;
|
||||
if (native && native.selectionEnd) {
|
||||
native.selectionEnd = value;
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.readOnly = false;
|
||||
|
|
@ -79,9 +116,23 @@ export class LionInput extends LionField {
|
|||
if (changedProperties.has('type')) {
|
||||
this._inputNode.type = this.type;
|
||||
}
|
||||
|
||||
if (changedProperties.has('placeholder')) {
|
||||
this._inputNode.placeholder = this.placeholder;
|
||||
}
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this._inputNode.disabled = this.disabled;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
if (changedProperties.has('name')) {
|
||||
this._inputNode.name = this.name;
|
||||
}
|
||||
|
||||
if (changedProperties.has('autocomplete')) {
|
||||
this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete);
|
||||
}
|
||||
}
|
||||
|
||||
__delegateReadOnly() {
|
||||
|
|
|
|||
26
packages/input/test/input-integrations.test.js
Normal file
26
packages/input/test/input-integrations.test.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { defineCE } from '@open-wc/testing';
|
||||
import { runInteractionStateMixinSuite } from '@lion/form-core/test-suites/InteractionStateMixin.suite.js';
|
||||
import { runFormatMixinSuite } from '@lion/form-core/test-suites/FormatMixin.suite.js';
|
||||
import { LionInput } from '../src/LionInput.js';
|
||||
|
||||
const fieldTagString = defineCE(
|
||||
class extends LionInput {
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
// LionInput needs to have an _inputNode defined in order to work...
|
||||
input: () => document.createElement('input'),
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe('<lion-input> integrations', () => {
|
||||
runInteractionStateMixinSuite({
|
||||
tagString: fieldTagString,
|
||||
});
|
||||
|
||||
runFormatMixinSuite({
|
||||
tagString: fieldTagString,
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||
import { Validator } from '@lion/form-core';
|
||||
import { expect, fixture, html, unsafeStatic, triggerFocusFor, aTimeout } from '@open-wc/testing';
|
||||
|
||||
import '../lion-input.js';
|
||||
|
||||
|
|
@ -24,6 +25,81 @@ describe('<lion-input>', () => {
|
|||
expect(el._inputNode.getAttribute('value')).to.equal('prefilled');
|
||||
});
|
||||
|
||||
it('can be disabled via attribute', async () => {
|
||||
const elDisabled = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
||||
expect(elDisabled.disabled).to.equal(true);
|
||||
expect(elDisabled._inputNode.disabled).to.equal(true);
|
||||
});
|
||||
|
||||
it('can be disabled via property', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.disabled).to.equal(true);
|
||||
});
|
||||
|
||||
// TODO: Add test that css pointerEvents is none if disabled.
|
||||
it('is disabled when disabled property is passed', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(false);
|
||||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
await aTimeout(0);
|
||||
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
|
||||
const disabledel = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
||||
expect(disabledel._inputNode.hasAttribute('disabled')).to.equal(true);
|
||||
});
|
||||
|
||||
it('reads initial value from attribute value', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag} value="one"></${tag}>`));
|
||||
expect(
|
||||
/** @type {HTMLInputElement[]} */ (Array.from(el.children)).find(
|
||||
child => child.slot === 'input',
|
||||
)?.value,
|
||||
).to.equal('one');
|
||||
});
|
||||
|
||||
it('delegates value property', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
expect(
|
||||
/** @type {HTMLInputElement[]} */ (Array.from(el.children)).find(
|
||||
child => child.slot === 'input',
|
||||
)?.value,
|
||||
).to.equal('');
|
||||
el.value = 'one';
|
||||
expect(el.value).to.equal('one');
|
||||
expect(
|
||||
/** @type {HTMLInputElement[]} */ (Array.from(el.children)).find(
|
||||
child => child.slot === 'input',
|
||||
)?.value,
|
||||
).to.equal('one');
|
||||
});
|
||||
|
||||
// This is necessary for security, so that _inputNodes autocomplete can be set to 'off'
|
||||
it('delegates autocomplete property', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
expect(el._inputNode.autocomplete).to.equal('');
|
||||
expect(el._inputNode.hasAttribute('autocomplete')).to.be.false;
|
||||
el.autocomplete = 'off';
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.autocomplete).to.equal('off');
|
||||
expect(el._inputNode.getAttribute('autocomplete')).to.equal('off');
|
||||
});
|
||||
|
||||
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
await triggerFocusFor(el);
|
||||
await el.updateComplete;
|
||||
el._inputNode.value = 'hello world';
|
||||
el._inputNode.selectionStart = 2;
|
||||
el._inputNode.selectionEnd = 2;
|
||||
el.value = 'hey there universe';
|
||||
expect(el._inputNode.selectionStart).to.equal(2);
|
||||
expect(el._inputNode.selectionEnd).to.equal(2);
|
||||
});
|
||||
|
||||
it('automatically creates an <input> element if not provided by user', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`
|
||||
<${tag}></${tag}>
|
||||
|
|
@ -54,6 +130,58 @@ describe('<lion-input>', () => {
|
|||
expect(el._inputNode.getAttribute('placeholder')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('should remove validation when disabled state toggles', async () => {
|
||||
const HasX = class extends Validator {
|
||||
static get validatorName() {
|
||||
return 'HasX';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
execute(value) {
|
||||
const result = value.indexOf('x') === -1;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
const el = /** @type {LionInput} */ (await fixture(html`
|
||||
<${tag}
|
||||
.validators=${[new HasX()]}
|
||||
.modelValue=${'a@b.nl'}
|
||||
></${tag}>
|
||||
`));
|
||||
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
||||
expect(el.validationStates.error.HasX).to.exist;
|
||||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasFeedbackFor).to.deep.equal([]);
|
||||
expect(el.validationStates.error).to.deep.equal({});
|
||||
});
|
||||
|
||||
describe('Delegation', () => {
|
||||
it('delegates property value', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
el.value = 'one';
|
||||
expect(el.value).to.equal('one');
|
||||
expect(el._inputNode.value).to.equal('one');
|
||||
});
|
||||
|
||||
it('delegates property selectionStart and selectionEnd', async () => {
|
||||
const el = /** @type {LionInput} */ (await fixture(html`
|
||||
<${tag}
|
||||
.modelValue=${'Some text to select'}
|
||||
></${tag}>
|
||||
`));
|
||||
|
||||
el.selectionStart = 5;
|
||||
el.selectionEnd = 12;
|
||||
expect(el._inputNode.selectionStart).to.equal(5);
|
||||
expect(el._inputNode.selectionEnd).to.equal(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('is accessible', async () => {
|
||||
const el = await fixture(html`<${tag} label="Label"></${tag}>`);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
import { LionField } from '@lion/form-core';
|
||||
import { ValueMixin } from '@lion/form-core/src/ValueMixin';
|
||||
|
||||
class LionFieldWithSelect extends LionField {
|
||||
/**
|
||||
* @returns {HTMLSelectElement}
|
||||
*/
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLSelectElement} */ (Array.from(this.children).find(
|
||||
el => el.slot === 'input',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LionSelectNative: wraps the native HTML element select
|
||||
|
|
@ -27,7 +40,7 @@ import { LionField } from '@lion/form-core';
|
|||
* @customElement lion-select
|
||||
* @extends {LionField}
|
||||
*/
|
||||
export class LionSelect extends LionField {
|
||||
export class LionSelect extends ValueMixin(LionFieldWithSelect) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._inputNode.addEventListener('change', this._proxyChangeEvent);
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
/**
|
||||
* Input node here is the lion-switch-button, which is not compatible with LionField _inputNode --> HTMLInputElement
|
||||
* Therefore we do a full override and typecast to an intersection type that includes LionSwitchButton
|
||||
* @returns {HTMLInputElement & LionSwitchButton}
|
||||
* @returns {LionSwitchButton}
|
||||
*/
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLInputElement & LionSwitchButton} */ (Array.from(this.children).find(
|
||||
return /** @type {LionSwitchButton} */ (Array.from(this.children).find(
|
||||
el => el.slot === 'input',
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
// @ts-expect-error https://github.com/jackmoore/autosize/pull/384 wait for this, then we can switch to just 'autosize'; and then types will work!
|
||||
import autosize from 'autosize/src/autosize.js';
|
||||
import { LionField } from '@lion/form-core';
|
||||
import { css } from '@lion/core';
|
||||
import { ValueMixin } from '@lion/form-core/src/ValueMixin';
|
||||
|
||||
class LionFieldWithTextArea extends LionField {
|
||||
/**
|
||||
* @returns {HTMLTextAreaElement}
|
||||
*/
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLTextAreaElement} */ (Array.from(this.children).find(
|
||||
el => el.slot === 'input',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LionTextarea: extension of lion-field with native input element in place and user friendly API
|
||||
*
|
||||
* @customElement lion-textarea
|
||||
* @extends {LionField}
|
||||
*/
|
||||
// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you.
|
||||
export class LionTextarea extends LionField {
|
||||
// @ts-expect-error false positive, parent properties get merged by lit-element already
|
||||
export class LionTextarea extends ValueMixin(LionFieldWithTextArea) {
|
||||
static get properties() {
|
||||
return {
|
||||
maxRows: {
|
||||
|
|
@ -49,17 +61,6 @@ export class LionTextarea extends LionField {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Input node here is the textarea, which is not compatible with LionField _inputNode --> HTMLInputElement
|
||||
* Therefore we do a full override and typecast to an intersection type that includes HTMLTextAreaElement
|
||||
* @returns {HTMLTextAreaElement & HTMLInputElement}
|
||||
*/
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLTextAreaElement & HTMLInputElement} */ (Array.from(this.children).find(
|
||||
el => el.slot === 'input',
|
||||
));
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.rows = 2;
|
||||
|
|
@ -74,14 +75,14 @@ export class LionTextarea extends LionField {
|
|||
this.__initializeAutoresize();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
autosize.destroy(this._inputNode);
|
||||
}
|
||||
|
||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('disabled')) {
|
||||
this._inputNode.disabled = this.disabled;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
if (changedProperties.has('rows')) {
|
||||
const native = this._inputNode;
|
||||
if (native) {
|
||||
|
|
@ -112,6 +113,11 @@ export class LionTextarea extends LionField {
|
|||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
autosize.destroy(this._inputNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* To support maxRows we need to set max-height of the textarea
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue