Merge pull request #1328 from ing-bank/fix/manyIssues
Types and aria-live assertive fixes
This commit is contained in:
commit
a100fb43df
87 changed files with 1483 additions and 1163 deletions
5
.changeset/smart-olives-tap.md
Normal file
5
.changeset/smart-olives-tap.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
aria-live is set to assertive on blur, so next focused input message will be read first by screen reader
|
||||||
40
.changeset/stupid-cycles-sniff.md
Normal file
40
.changeset/stupid-cycles-sniff.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
'@lion/ajax': minor
|
||||||
|
'@lion/button': minor
|
||||||
|
'@lion/checkbox-group': minor
|
||||||
|
'@lion/combobox': minor
|
||||||
|
'@lion/core': minor
|
||||||
|
'@lion/form-core': minor
|
||||||
|
'@lion/form-integrations': minor
|
||||||
|
'@lion/input': minor
|
||||||
|
'@lion/input-amount': minor
|
||||||
|
'@lion/input-date': minor
|
||||||
|
'@lion/input-email': minor
|
||||||
|
'@lion/input-iban': minor
|
||||||
|
'@lion/input-stepper': minor
|
||||||
|
'@lion/listbox': minor
|
||||||
|
'@lion/localize': minor
|
||||||
|
'@lion/overlays': minor
|
||||||
|
'@lion/select-rich': minor
|
||||||
|
'@lion/switch': minor
|
||||||
|
'@lion/textarea': minor
|
||||||
|
'@lion/fieldset': minor
|
||||||
|
'@lion/form': minor
|
||||||
|
'@lion/input-datepicker': minor
|
||||||
|
'@lion/input-range': minor
|
||||||
|
'@lion/radio-group': minor
|
||||||
|
'@lion/select': minor
|
||||||
|
'@lion/tooltip': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Type fixes and enhancements:
|
||||||
|
|
||||||
|
- all protected/private entries added to form-core type definitions, and their dependents were fixed
|
||||||
|
- a lot @ts-expect-error and @ts-ignore (all `get slots()` and `get modelValue()` issues are fixed)
|
||||||
|
- categorized @ts-expect-error / @ts-ignore into:
|
||||||
|
- [external]: when a 3rd party didn't ship types (could also be browser specs)
|
||||||
|
- [allow-protected]: when we are allowed to know about protected methods. For instance when code
|
||||||
|
resides in the same package
|
||||||
|
- [allow-private]: when we need to check a private value inside a test
|
||||||
|
- [allow]: miscellaneous allows
|
||||||
|
- [editor]: when the editor complains, but the cli/ci doesn't
|
||||||
|
|
@ -123,7 +123,6 @@ class Cache {
|
||||||
*/
|
*/
|
||||||
_validateCache() {
|
_validateCache() {
|
||||||
if (new Date().getTime() > this.expiration) {
|
if (new Date().getTime() > this.expiration) {
|
||||||
// @ts-ignore
|
|
||||||
this._cacheObject = {};
|
this._cacheObject = {};
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +139,6 @@ let caches = {};
|
||||||
* @returns {string} of querystring parameters WITHOUT `?` or empty string ''
|
* @returns {string} of querystring parameters WITHOUT `?` or empty string ''
|
||||||
*/
|
*/
|
||||||
export const searchParamSerializer = (params = {}) =>
|
export const searchParamSerializer = (params = {}) =>
|
||||||
// @ts-ignore
|
|
||||||
typeof params === 'object' && params !== null ? new URLSearchParams(params).toString() : '';
|
typeof params === 'object' && params !== null ? new URLSearchParams(params).toString() : '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,6 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import '@lion/checkbox-group/define';
|
import '@lion/checkbox-group/define';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -9,10 +10,14 @@ import '@lion/checkbox-group/define';
|
||||||
/**
|
/**
|
||||||
* @param {LionCheckboxIndeterminate} el
|
* @param {LionCheckboxIndeterminate} el
|
||||||
*/
|
*/
|
||||||
function getProtectedMembers(el) {
|
function getCheckboxIndeterminateMembers(el) {
|
||||||
|
const obj = getFormControlMembers(el);
|
||||||
return {
|
return {
|
||||||
// @ts-ignore
|
...obj,
|
||||||
subCheckboxes: el._subCheckboxes,
|
...{
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_subCheckboxes: el._subCheckboxes,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,10 +108,10 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
'lion-checkbox-indeterminate',
|
'lion-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
|
|
||||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
subCheckboxes[0].checked = true;
|
_subCheckboxes[0].checked = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -127,12 +132,12 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'lion-checkbox-indeterminate',
|
'lion-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
subCheckboxes[0].checked = true;
|
_subCheckboxes[0].checked = true;
|
||||||
subCheckboxes[1].checked = true;
|
_subCheckboxes[1].checked = true;
|
||||||
subCheckboxes[2].checked = true;
|
_subCheckboxes[2].checked = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -154,17 +159,17 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'lion-checkbox-indeterminate',
|
'lion-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
|
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
elIndeterminate._inputNode.click();
|
_inputNode.click();
|
||||||
await elIndeterminate.updateComplete;
|
await elIndeterminate.updateComplete;
|
||||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
||||||
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
expect(_subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||||
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
expect(_subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||||
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
expect(_subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sync all children when parent is checked (from unchecked to checked)', async () => {
|
it('should sync all children when parent is checked (from unchecked to checked)', async () => {
|
||||||
|
|
@ -181,17 +186,17 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'lion-checkbox-indeterminate',
|
'lion-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
|
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
elIndeterminate._inputNode.click();
|
_inputNode.click();
|
||||||
await elIndeterminate.updateComplete;
|
await elIndeterminate.updateComplete;
|
||||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
||||||
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
expect(_subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||||
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
expect(_subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||||
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
expect(_subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sync all children when parent is checked (from checked to unchecked)', async () => {
|
it('should sync all children when parent is checked (from checked to unchecked)', async () => {
|
||||||
|
|
@ -208,17 +213,17 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'lion-checkbox-indeterminate',
|
'lion-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
|
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
elIndeterminate._inputNode.click();
|
_inputNode.click();
|
||||||
await elIndeterminate.updateComplete;
|
await elIndeterminate.updateComplete;
|
||||||
const elProts = getProtectedMembers(elIndeterminate);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||||
expect(elProts.subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
expect(_subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||||
expect(elProts.subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
expect(_subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||||
expect(elProts.subCheckboxes[2].hasAttribute('checked')).to.be.false;
|
expect(_subCheckboxes[2].hasAttribute('checked')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work as expected with siblings checkbox-indeterminate', async () => {
|
it('should work as expected with siblings checkbox-indeterminate', async () => {
|
||||||
|
|
@ -258,27 +263,28 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'#first-checkbox-indeterminate',
|
'#first-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
|
|
||||||
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'#second-checkbox-indeterminate',
|
'#second-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const elFirstSubCheckboxes = getCheckboxIndeterminateMembers(elFirstIndeterminate);
|
||||||
|
const elSecondSubCheckboxes = getCheckboxIndeterminateMembers(elSecondIndeterminate);
|
||||||
|
|
||||||
// Act - check the first sibling
|
// Act - check the first sibling
|
||||||
elFirstIndeterminate._inputNode.click();
|
elFirstSubCheckboxes._inputNode.click();
|
||||||
await elFirstIndeterminate.updateComplete;
|
await elFirstIndeterminate.updateComplete;
|
||||||
await elSecondIndeterminate.updateComplete;
|
await elSecondIndeterminate.updateComplete;
|
||||||
|
|
||||||
const elFirstSubCheckboxes = getProtectedMembers(elFirstIndeterminate);
|
|
||||||
const elSecondSubCheckboxes = getProtectedMembers(elSecondIndeterminate);
|
|
||||||
|
|
||||||
// Assert - the second sibling should not be affected
|
// Assert - the second sibling should not be affected
|
||||||
|
|
||||||
expect(elFirstIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
expect(elFirstIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
||||||
expect(elFirstSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
expect(elFirstSubCheckboxes._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||||
expect(elFirstSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
expect(elFirstSubCheckboxes._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||||
expect(elFirstSubCheckboxes.subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
expect(elFirstSubCheckboxes._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||||
|
|
||||||
expect(elSecondSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
expect(elSecondSubCheckboxes._subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||||
expect(elSecondSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
expect(elSecondSubCheckboxes._subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work as expected with nested indeterminate checkboxes', async () => {
|
it('should work as expected with nested indeterminate checkboxes', async () => {
|
||||||
|
|
@ -322,12 +328,13 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'#parent-checkbox-indeterminate',
|
'#parent-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
const elNestedSubCheckboxes = getProtectedMembers(elNestedIndeterminate);
|
const elNestedSubCheckboxes = getCheckboxIndeterminateMembers(elNestedIndeterminate);
|
||||||
const elParentSubCheckboxes = getProtectedMembers(elParentIndeterminate);
|
const elParentSubCheckboxes = getCheckboxIndeterminateMembers(elParentIndeterminate);
|
||||||
|
|
||||||
// Act - check a nested checkbox
|
// Act - check a nested checkbox
|
||||||
if (elNestedIndeterminate) {
|
if (elNestedIndeterminate) {
|
||||||
elNestedSubCheckboxes.subCheckboxes[0]._inputNode.click();
|
// @ts-ignore [allow-protected] in test
|
||||||
|
elNestedSubCheckboxes._subCheckboxes[0]._inputNode.click();
|
||||||
}
|
}
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
|
|
@ -336,8 +343,10 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||||
|
|
||||||
// Act - check all nested checkbox
|
// Act - check all nested checkbox
|
||||||
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[1]._inputNode.click();
|
// @ts-ignore [allow-protected] in test
|
||||||
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[2]._inputNode.click();
|
if (elNestedIndeterminate) elNestedSubCheckboxes._subCheckboxes[1]._inputNode.click();
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
if (elNestedIndeterminate) elNestedSubCheckboxes._subCheckboxes[2]._inputNode.click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -348,10 +357,12 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
|
|
||||||
// Act - finally check all remaining checkbox
|
// Act - finally check all remaining checkbox
|
||||||
if (elParentIndeterminate) {
|
if (elParentIndeterminate) {
|
||||||
elParentSubCheckboxes.subCheckboxes[0]._inputNode.click();
|
// @ts-ignore [allow-protected] in test
|
||||||
|
elParentSubCheckboxes._subCheckboxes[0]._inputNode.click();
|
||||||
}
|
}
|
||||||
if (elParentIndeterminate) {
|
if (elParentIndeterminate) {
|
||||||
elParentSubCheckboxes.subCheckboxes[1]._inputNode.click();
|
// @ts-ignore [allow-protected] in test
|
||||||
|
elParentSubCheckboxes._subCheckboxes[1]._inputNode.click();
|
||||||
}
|
}
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
|
|
@ -383,12 +394,12 @@ describe('<lion-checkbox-indeterminate>', () => {
|
||||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||||
'lion-checkbox-indeterminate',
|
'lion-checkbox-indeterminate',
|
||||||
));
|
));
|
||||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
subCheckboxes[0].checked = true;
|
_subCheckboxes[0].checked = true;
|
||||||
subCheckboxes[1].checked = true;
|
_subCheckboxes[1].checked = true;
|
||||||
subCheckboxes[2].checked = true;
|
_subCheckboxes[2].checked = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ import { LionListbox } from '@lion/listbox';
|
||||||
* LionCombobox: implements the wai-aria combobox design pattern and integrates it as a Lion
|
* LionCombobox: implements the wai-aria combobox design pattern and integrates it as a Lion
|
||||||
* FormControl
|
* FormControl
|
||||||
*/
|
*/
|
||||||
// @ts-expect-error static properties are not compatible
|
|
||||||
export class LionCombobox extends OverlayMixin(LionListbox) {
|
export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
autocomplete: { type: String, reflect: true },
|
autocomplete: { type: String, reflect: true },
|
||||||
|
|
@ -77,7 +77,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_inputGroupInputTemplate() {
|
_inputGroupInputTemplate() {
|
||||||
// @ts-ignore
|
|
||||||
return html`
|
return html`
|
||||||
<div class="input-group__input">
|
<div class="input-group__input">
|
||||||
<slot name="selection-display"></slot>
|
<slot name="selection-display"></slot>
|
||||||
|
|
@ -111,7 +110,6 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
/**
|
/**
|
||||||
* @type {SlotsMap}
|
* @type {SlotsMap}
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -182,6 +180,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
* @configure FormControlMixin
|
* @configure FormControlMixin
|
||||||
* Will tell FormControlMixin that a11y wrt labels / descriptions / feedback
|
* Will tell FormControlMixin that a11y wrt labels / descriptions / feedback
|
||||||
* should be applied here.
|
* should be applied here.
|
||||||
|
* @protected
|
||||||
*/
|
*/
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
if (this._ariaVersion === '1.1') {
|
if (this._ariaVersion === '1.1') {
|
||||||
|
|
@ -317,11 +316,11 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
this.__setComboboxDisabledAndReadOnly();
|
this.__setComboboxDisabledAndReadOnly();
|
||||||
}
|
}
|
||||||
if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) {
|
if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) {
|
||||||
if (this._syncToTextboxCondition(this.modelValue, this.__oldModelValue)) {
|
if (this._syncToTextboxCondition(this.modelValue, this._oldModelValue)) {
|
||||||
if (!this.multipleChoice) {
|
if (!this.multipleChoice) {
|
||||||
this._setTextboxValue(this.modelValue);
|
this._setTextboxValue(this.modelValue);
|
||||||
} else {
|
} else {
|
||||||
this._syncToTextboxMultiple(this.modelValue, this.__oldModelValue);
|
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -482,7 +481,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
if (!this.multipleChoice) {
|
if (!this.multipleChoice) {
|
||||||
if (
|
if (
|
||||||
this.checkedIndex !== -1 &&
|
this.checkedIndex !== -1 &&
|
||||||
this._syncToTextboxCondition(this.modelValue, this.__oldModelValue, {
|
this._syncToTextboxCondition(this.modelValue, this._oldModelValue, {
|
||||||
phase: 'overlay-close',
|
phase: 'overlay-close',
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
|
|
@ -491,7 +490,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
||||||
].choiceValue;
|
].choiceValue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._syncToTextboxMultiple(this.modelValue, this.__oldModelValue);
|
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,41 @@ import sinon from 'sinon';
|
||||||
import '@lion/combobox/define';
|
import '@lion/combobox/define';
|
||||||
import { LionOptions } from '@lion/listbox';
|
import { LionOptions } from '@lion/listbox';
|
||||||
import { browserDetection, LitElement } from '@lion/core';
|
import { browserDetection, LitElement } from '@lion/core';
|
||||||
|
import { getListboxMembers } from '@lion/listbox/test-helpers';
|
||||||
import { Required } from '@lion/form-core';
|
import { Required } from '@lion/form-core';
|
||||||
import { LionCombobox } from '../src/LionCombobox.js';
|
import { LionCombobox } from '../src/LionCombobox.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/SelectionDisplay').SelectionDisplay} SelectionDisplay
|
* @typedef {import('../types/SelectionDisplay').SelectionDisplay} SelectionDisplay
|
||||||
|
* @typedef {import('@lion/listbox/types/ListboxMixinTypes').ListboxHost} ListboxHost
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {LionCombobox} el
|
* @param { LionCombobox } el
|
||||||
*/
|
*/
|
||||||
|
function getComboboxMembers(el) {
|
||||||
function getProtectedMembers(el) {
|
const obj = getListboxMembers(el);
|
||||||
// @ts-ignore
|
|
||||||
const {
|
|
||||||
_comboboxNode: comboboxNode,
|
|
||||||
_inputNode: inputNode,
|
|
||||||
_listboxNode: listboxNode,
|
|
||||||
_selectionDisplayNode: selectionDisplayNode,
|
|
||||||
_activeDescendantOwnerNode: activeDescendantOwnerNode,
|
|
||||||
_ariaVersion: ariaVersion,
|
|
||||||
} = el;
|
|
||||||
return {
|
return {
|
||||||
comboboxNode,
|
...obj,
|
||||||
inputNode,
|
...{
|
||||||
listboxNode,
|
// @ts-ignore [allow-protected] in test
|
||||||
selectionDisplayNode,
|
_invokerNode: el._invokerNode,
|
||||||
activeDescendantOwnerNode,
|
// @ts-ignore [allow-protected] in test
|
||||||
ariaVersion,
|
_overlayCtrl: el._overlayCtrl,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_comboboxNode: el._comboboxNode,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_inputNode: el._inputNode,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_listboxNode: el._listboxNode,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_selectionDisplayNode: el._selectionDisplayNode,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_activeDescendantOwnerNode: el._activeDescendantOwnerNode,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
_ariaVersion: el._ariaVersion,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,13 +47,13 @@ function getProtectedMembers(el) {
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
*/
|
*/
|
||||||
function mimicUserTyping(el, value) {
|
function mimicUserTyping(el, value) {
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
inputNode.value = value;
|
_inputNode.value = value;
|
||||||
inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
_inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||||
inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
|
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
|
||||||
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
|
_inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,8 +70,8 @@ function mimicKeyPress(el, key) {
|
||||||
* @param {string[]} values
|
* @param {string[]} values
|
||||||
*/
|
*/
|
||||||
async function mimicUserTypingAdvanced(el, values) {
|
async function mimicUserTypingAdvanced(el, values) {
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (inputNode);
|
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (_inputNode);
|
||||||
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
|
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||||
|
|
||||||
for (const key of values) {
|
for (const key of values) {
|
||||||
|
|
@ -226,10 +233,10 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
@ -244,11 +251,11 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { listboxNode } = getProtectedMembers(el);
|
const { _listboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(listboxNode).to.exist;
|
expect(_listboxNode).to.exist;
|
||||||
expect(listboxNode).to.be.instanceOf(LionOptions);
|
expect(_listboxNode).to.be.instanceOf(LionOptions);
|
||||||
expect(el.querySelector('[role=listbox]')).to.equal(listboxNode);
|
expect(el.querySelector('[role=listbox]')).to.equal(_listboxNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a textbox element', async () => {
|
it('has a textbox element', async () => {
|
||||||
|
|
@ -258,10 +265,10 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(comboboxNode).to.exist;
|
expect(_comboboxNode).to.exist;
|
||||||
expect(el.querySelector('[role=combobox]')).to.equal(comboboxNode);
|
expect(el.querySelector('[role=combobox]')).to.equal(_comboboxNode);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -273,13 +280,13 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(inputNode.value).to.equal('10');
|
expect(_inputNode.value).to.equal('10');
|
||||||
|
|
||||||
el.modelValue = '20';
|
el.modelValue = '20';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal('20');
|
expect(_inputNode.value).to.equal('20');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets modelValue to empty string if no option is selected', async () => {
|
it('sets modelValue to empty string if no option is selected', async () => {
|
||||||
|
|
@ -328,11 +335,11 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
el.clear();
|
el.clear();
|
||||||
expect(el.modelValue).to.equal('');
|
expect(el.modelValue).to.equal('');
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo" multiple-choice .modelValue="${['Artichoke']}">
|
<lion-combobox name="foo" multiple-choice .modelValue="${['Artichoke']}">
|
||||||
|
|
@ -345,7 +352,7 @@ describe('lion-combobox', () => {
|
||||||
|
|
||||||
el2.clear();
|
el2.clear();
|
||||||
expect(el2.modelValue).to.eql([]);
|
expect(el2.modelValue).to.eql([]);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -359,10 +366,10 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
@ -385,12 +392,12 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
|
|
||||||
// step [1]
|
// step [1]
|
||||||
inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
|
|
||||||
|
|
@ -403,7 +410,7 @@ describe('lion-combobox', () => {
|
||||||
options[0].click();
|
options[0].click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
expect(document.activeElement).to.equal(inputNode);
|
expect(document.activeElement).to.equal(_inputNode);
|
||||||
|
|
||||||
// step [4]
|
// step [4]
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -422,19 +429,19 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
// open
|
// open
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
mimicUserTyping(el, 'art');
|
mimicUserTyping(el, 'art');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
|
|
||||||
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
_inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides overlay on [Tab]', async () => {
|
it('hides overlay on [Tab]', async () => {
|
||||||
|
|
@ -447,19 +454,19 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
// open
|
// open
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
mimicUserTyping(el, 'art');
|
mimicUserTyping(el, 'art');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
|
|
||||||
mimicKeyPress(inputNode, 'Tab');
|
mimicKeyPress(_inputNode, 'Tab');
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears checkedIndex on empty text', async () => {
|
it('clears checkedIndex on empty text', async () => {
|
||||||
|
|
@ -472,15 +479,15 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
// open
|
// open
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
mimicUserTyping(el, 'art');
|
mimicUserTyping(el, 'art');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
||||||
mimicUserTyping(el, '');
|
mimicUserTyping(el, '');
|
||||||
|
|
@ -510,10 +517,10 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
@ -603,10 +610,10 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
|
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
mimicUserTyping(el, 'art');
|
mimicUserTyping(el, 'art');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -644,10 +651,10 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
expect(el.opened).to.equal(false);
|
expect(el.opened).to.equal(false);
|
||||||
|
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
|
|
||||||
mimicUserTyping(el, 'art');
|
mimicUserTyping(el, 'art');
|
||||||
expect(el.opened).to.equal(true);
|
expect(el.opened).to.equal(true);
|
||||||
|
|
@ -672,15 +679,15 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
||||||
// Simulate backspace deleting the char at the end of the string
|
// Simulate backspace deleting the char at the end of the string
|
||||||
mimicKeyPress(inputNode, 'Backspace');
|
mimicKeyPress(_inputNode, 'Backspace');
|
||||||
inputNode.dispatchEvent(new Event('input'));
|
_inputNode.dispatchEvent(new Event('input'));
|
||||||
const arr = inputNode.value.split('');
|
const arr = _inputNode.value.split('');
|
||||||
arr.splice(inputNode.value.length - 1, 1);
|
arr.splice(_inputNode.value.length - 1, 1);
|
||||||
inputNode.value = arr.join('');
|
_inputNode.value = arr.join('');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
el.dispatchEvent(new Event('blur'));
|
el.dispatchEvent(new Event('blur'));
|
||||||
|
|
||||||
|
|
@ -703,9 +710,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(comboboxNode.getAttribute('role')).to.equal('combobox');
|
expect(_comboboxNode.getAttribute('role')).to.equal('combobox');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets aria-expanded to element with role="combobox" in wai-aria 1.0 and 1.1', async () => {
|
it('sets aria-expanded to element with role="combobox" in wai-aria 1.0 and 1.1', async () => {
|
||||||
|
|
@ -715,12 +722,12 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(comboboxNode.getAttribute('aria-expanded')).to.equal('false');
|
expect(_comboboxNode.getAttribute('aria-expanded')).to.equal('false');
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(comboboxNode.getAttribute('aria-expanded')).to.equal('true');
|
expect(_comboboxNode.getAttribute('aria-expanded')).to.equal('true');
|
||||||
|
|
||||||
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
<lion-combobox name="foo" ._ariaVersion="${'1.1'}">
|
<lion-combobox name="foo" ._ariaVersion="${'1.1'}">
|
||||||
|
|
@ -728,12 +735,12 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode: comboboxNode2 } = getProtectedMembers(el2);
|
const { _comboboxNode: comboboxNode2 } = getComboboxMembers(el2);
|
||||||
|
|
||||||
expect(comboboxNode2.getAttribute('aria-expanded')).to.equal('false');
|
expect(comboboxNode2.getAttribute('aria-expanded')).to.equal('false');
|
||||||
el2.opened = true;
|
el2.opened = true;
|
||||||
await el2.updateComplete;
|
await el2.updateComplete;
|
||||||
expect(comboboxNode.getAttribute('aria-expanded')).to.equal('true');
|
expect(_comboboxNode.getAttribute('aria-expanded')).to.equal('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makes sure listbox node is not focusable', async () => {
|
it('makes sure listbox node is not focusable', async () => {
|
||||||
|
|
@ -743,9 +750,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { listboxNode } = getProtectedMembers(el);
|
const { _listboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(listboxNode.hasAttribute('tabindex')).to.be.false;
|
expect(_listboxNode.hasAttribute('tabindex')).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -774,9 +781,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { selectionDisplayNode } = getProtectedMembers(el);
|
const { _selectionDisplayNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(selectionDisplayNode).to.equal(el.querySelector('[slot=selection-display]'));
|
expect(_selectionDisplayNode).to.equal(el.querySelector('[slot=selection-display]'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets a reference to combobox element in _selectionDisplayNode', async () => {
|
it('sets a reference to combobox element in _selectionDisplayNode', async () => {
|
||||||
|
|
@ -904,16 +911,16 @@ describe('lion-combobox', () => {
|
||||||
`));
|
`));
|
||||||
mimicUserTyping(el, 'ch');
|
mimicUserTyping(el, 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(inputNode.value).to.equal('Chard');
|
expect(_inputNode.value).to.equal('Chard');
|
||||||
expect(inputNode.selectionStart).to.equal(2);
|
expect(_inputNode.selectionStart).to.equal(2);
|
||||||
expect(inputNode.selectionEnd).to.equal(inputNode.value.length);
|
expect(_inputNode.selectionEnd).to.equal(_inputNode.value.length);
|
||||||
|
|
||||||
// We don't autocomplete when characters are removed
|
// We don't autocomplete when characters are removed
|
||||||
mimicUserTyping(el, 'c'); // The user pressed backspace (number of chars decreased)
|
mimicUserTyping(el, 'c'); // The user pressed backspace (number of chars decreased)
|
||||||
expect(inputNode.value).to.equal('c');
|
expect(_inputNode.value).to.equal('c');
|
||||||
expect(inputNode.selectionStart).to.equal(inputNode.value.length);
|
expect(_inputNode.selectionStart).to.equal(_inputNode.value.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters options when autocomplete is "list"', async () => {
|
it('filters options when autocomplete is "list"', async () => {
|
||||||
|
|
@ -925,12 +932,12 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
mimicUserTyping(el, 'ch');
|
mimicUserTyping(el, 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(getFilteredOptionValues(el)).to.eql(['Artichoke', 'Chard', 'Chicory']);
|
expect(getFilteredOptionValues(el)).to.eql(['Artichoke', 'Chard', 'Chicory']);
|
||||||
expect(inputNode.value).to.equal('ch');
|
expect(_inputNode.value).to.equal('ch');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not filter options when autocomplete is "none"', async () => {
|
it('does not filter options when autocomplete is "none"', async () => {
|
||||||
|
|
@ -1018,26 +1025,26 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
mimicUserTyping(el, 'ch');
|
mimicUserTyping(el, 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal('Chard');
|
expect(_inputNode.value).to.equal('Chard');
|
||||||
expect(inputNode.selectionStart).to.equal('ch'.length);
|
expect(_inputNode.selectionStart).to.equal('ch'.length);
|
||||||
expect(inputNode.selectionEnd).to.equal('Chard'.length);
|
expect(_inputNode.selectionEnd).to.equal('Chard'.length);
|
||||||
|
|
||||||
await mimicUserTypingAdvanced(el, ['i', 'c']);
|
await mimicUserTypingAdvanced(el, ['i', 'c']);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal('Chicory');
|
expect(_inputNode.value).to.equal('Chicory');
|
||||||
expect(inputNode.selectionStart).to.equal('chic'.length);
|
expect(_inputNode.selectionStart).to.equal('chic'.length);
|
||||||
expect(inputNode.selectionEnd).to.equal('Chicory'.length);
|
expect(_inputNode.selectionEnd).to.equal('Chicory'.length);
|
||||||
|
|
||||||
// Diminishing chars, but autocompleting
|
// Diminishing chars, but autocompleting
|
||||||
mimicUserTyping(el, 'ch');
|
mimicUserTyping(el, 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal('ch');
|
expect(_inputNode.value).to.equal('ch');
|
||||||
expect(inputNode.selectionStart).to.equal('ch'.length);
|
expect(_inputNode.selectionStart).to.equal('ch'.length);
|
||||||
expect(inputNode.selectionEnd).to.equal('ch'.length);
|
expect(_inputNode.selectionEnd).to.equal('ch'.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('synchronizes textbox on overlay close', async () => {
|
it('synchronizes textbox on overlay close', async () => {
|
||||||
|
|
@ -1049,8 +1056,8 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
|
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
|
||||||
|
|
@ -1066,7 +1073,7 @@ describe('lion-combobox', () => {
|
||||||
el.setCheckedIndex(index);
|
el.setCheckedIndex(index);
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal(valueOnClose);
|
expect(_inputNode.value).to.equal(valueOnClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
await performChecks('none', 0, 'Artichoke');
|
await performChecks('none', 0, 'Artichoke');
|
||||||
|
|
@ -1091,8 +1098,8 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
|
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
|
||||||
|
|
@ -1108,7 +1115,7 @@ describe('lion-combobox', () => {
|
||||||
el.setCheckedIndex(index);
|
el.setCheckedIndex(index);
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal(valueOnClose);
|
expect(_inputNode.value).to.equal(valueOnClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
await performChecks('none', 0, '');
|
await performChecks('none', 0, '');
|
||||||
|
|
@ -1185,21 +1192,21 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
mimicUserTyping(el, 'ch');
|
mimicUserTyping(el, 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal('Chard');
|
expect(_inputNode.value).to.equal('Chard');
|
||||||
expect(inputNode.selectionStart).to.equal('Ch'.length);
|
expect(_inputNode.selectionStart).to.equal('Ch'.length);
|
||||||
expect(inputNode.selectionEnd).to.equal('Chard'.length);
|
expect(_inputNode.selectionEnd).to.equal('Chard'.length);
|
||||||
|
|
||||||
// Autocompletion happened. When we go backwards ('Ch[ard]' => 'Ch'), we should not
|
// Autocompletion happened. When we go backwards ('Ch[ard]' => 'Ch'), we should not
|
||||||
// autocomplete to 'Chard' anymore.
|
// autocomplete to 'Chard' anymore.
|
||||||
await mimicUserTypingAdvanced(el, ['Backspace']);
|
await mimicUserTypingAdvanced(el, ['Backspace']);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.value).to.equal('Ch'); // so not 'Chard'
|
expect(_inputNode.value).to.equal('Ch'); // so not 'Chard'
|
||||||
expect(inputNode.selectionStart).to.equal('Ch'.length);
|
expect(_inputNode.selectionStart).to.equal('Ch'.length);
|
||||||
expect(inputNode.selectionEnd).to.equal('Ch'.length);
|
expect(_inputNode.selectionEnd).to.equal('Ch'.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
|
|
@ -1265,29 +1272,29 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'none';
|
el.autocomplete = 'none';
|
||||||
el.setCheckedIndex(0);
|
el.setCheckedIndex(0);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'list';
|
el.autocomplete = 'list';
|
||||||
el.setCheckedIndex(0);
|
el.setCheckedIndex(0);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'inline';
|
el.autocomplete = 'inline';
|
||||||
el.setCheckedIndex(0);
|
el.setCheckedIndex(0);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'both';
|
el.autocomplete = 'both';
|
||||||
el.setCheckedIndex(0);
|
el.setCheckedIndex(0);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('synchronizes last index to textbox when autocomplete is "inline" or "both" when multipleChoice', async () => {
|
it('synchronizes last index to textbox when autocomplete is "inline" or "both" when multipleChoice', async () => {
|
||||||
|
|
@ -1299,35 +1306,35 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(inputNode.value).to.eql('');
|
expect(_inputNode.value).to.eql('');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'none';
|
el.autocomplete = 'none';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
el.setCheckedIndex([1]);
|
el.setCheckedIndex([1]);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'list';
|
el.autocomplete = 'list';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
el.setCheckedIndex([1]);
|
el.setCheckedIndex([1]);
|
||||||
expect(inputNode.value).to.equal('');
|
expect(_inputNode.value).to.equal('');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'inline';
|
el.autocomplete = 'inline';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
el.setCheckedIndex([1]);
|
el.setCheckedIndex([1]);
|
||||||
expect(inputNode.value).to.equal('Chard');
|
expect(_inputNode.value).to.equal('Chard');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'both';
|
el.autocomplete = 'both';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
expect(inputNode.value).to.equal('Artichoke');
|
expect(_inputNode.value).to.equal('Artichoke');
|
||||||
el.setCheckedIndex([1]);
|
el.setCheckedIndex([1]);
|
||||||
expect(inputNode.value).to.equal('Chard');
|
expect(_inputNode.value).to.equal('Chard');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Subclassers', () => {
|
describe('Subclassers', () => {
|
||||||
|
|
@ -1360,27 +1367,27 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'none';
|
el.autocomplete = 'none';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
expect(_inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'list';
|
el.autocomplete = 'list';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
expect(_inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'inline';
|
el.autocomplete = 'inline';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
expect(_inputNode.value).to.equal('Artichoke--multi');
|
||||||
|
|
||||||
el.setCheckedIndex(-1);
|
el.setCheckedIndex(-1);
|
||||||
el.autocomplete = 'both';
|
el.autocomplete = 'both';
|
||||||
el.setCheckedIndex([0]);
|
el.setCheckedIndex([0]);
|
||||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
expect(_inputNode.value).to.equal('Artichoke--multi');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1414,7 +1421,7 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {LionCombobox} elm
|
* @param {LionCombobox} elm
|
||||||
|
|
@ -1442,7 +1449,7 @@ describe('lion-combobox', () => {
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
|
|
||||||
mimicKeyPress(inputNode, 'Enter');
|
mimicKeyPress(_inputNode, 'Enter');
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
|
|
||||||
|
|
@ -1456,7 +1463,7 @@ describe('lion-combobox', () => {
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
|
|
||||||
mimicKeyPress(inputNode, 'Enter');
|
mimicKeyPress(_inputNode, 'Enter');
|
||||||
expect(el.activeIndex).to.equal(-1);
|
expect(el.activeIndex).to.equal(-1);
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
|
|
||||||
|
|
@ -1473,7 +1480,7 @@ describe('lion-combobox', () => {
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
mimicKeyPress(inputNode, 'Enter');
|
mimicKeyPress(_inputNode, 'Enter');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
|
|
@ -1487,7 +1494,7 @@ describe('lion-combobox', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
mimicKeyPress(inputNode, 'Enter');
|
mimicKeyPress(_inputNode, 'Enter');
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -1501,7 +1508,7 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -1518,7 +1525,7 @@ describe('lion-combobox', () => {
|
||||||
// select artichoke
|
// select artichoke
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'artichoke');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'artichoke');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
mimicKeyPress(inputNode, 'Enter');
|
mimicKeyPress(_inputNode, 'Enter');
|
||||||
|
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -1537,17 +1544,17 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
// Select something
|
// Select something
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
mimicKeyPress(inputNode, 'Enter');
|
mimicKeyPress(_inputNode, 'Enter');
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
mimicKeyPress(inputNode, 'Escape');
|
mimicKeyPress(_inputNode, 'Escape');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(inputNode.textContent).to.equal('');
|
expect(_inputNode.textContent).to.equal('');
|
||||||
|
|
||||||
el.formElements.forEach(option => expect(option.active).to.be.false);
|
el.formElements.forEach(option => expect(option.active).to.be.false);
|
||||||
|
|
||||||
|
|
@ -1562,13 +1569,25 @@ describe('lion-combobox', () => {
|
||||||
it('synchronizes autocomplete option to textbox', async () => {
|
it('synchronizes autocomplete option to textbox', async () => {
|
||||||
let el;
|
let el;
|
||||||
[el] = await fruitFixture({ autocomplete: 'both' });
|
[el] = await fruitFixture({ autocomplete: 'both' });
|
||||||
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('both');
|
expect(
|
||||||
|
getComboboxMembers(/** @type {LionCombobox} */ (el))._inputNode.getAttribute(
|
||||||
|
'aria-autocomplete',
|
||||||
|
),
|
||||||
|
).to.equal('both');
|
||||||
|
|
||||||
[el] = await fruitFixture({ autocomplete: 'list' });
|
[el] = await fruitFixture({ autocomplete: 'list' });
|
||||||
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('list');
|
expect(
|
||||||
|
getComboboxMembers(/** @type {LionCombobox} */ (el))._inputNode.getAttribute(
|
||||||
|
'aria-autocomplete',
|
||||||
|
),
|
||||||
|
).to.equal('list');
|
||||||
|
|
||||||
[el] = await fruitFixture({ autocomplete: 'none' });
|
[el] = await fruitFixture({ autocomplete: 'none' });
|
||||||
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('none');
|
expect(
|
||||||
|
getComboboxMembers(/** @type {LionCombobox} */ (el))._inputNode.getAttribute(
|
||||||
|
'aria-autocomplete',
|
||||||
|
),
|
||||||
|
).to.equal('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates aria-activedescendant on textbox node', async () => {
|
it('updates aria-activedescendant on textbox node', async () => {
|
||||||
|
|
@ -1581,21 +1600,21 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const elProts = getProtectedMembers(el);
|
const elProts = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(elProts._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
expect(el.formElements[1].active).to.equal(false);
|
expect(el.formElements[1].active).to.equal(false);
|
||||||
|
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(elProts._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
mimicKeyPress(elProts.inputNode, 'ArrowDown');
|
mimicKeyPress(elProts._inputNode, 'ArrowDown');
|
||||||
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(elProts._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||||
'artichoke-option',
|
'artichoke-option',
|
||||||
);
|
);
|
||||||
expect(el.formElements[1].active).to.equal(false);
|
expect(el.formElements[1].active).to.equal(false);
|
||||||
|
|
@ -1609,11 +1628,11 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const el2Prots = getProtectedMembers(el2);
|
const el2Prots = getComboboxMembers(el2);
|
||||||
|
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el2), 'ch');
|
mimicUserTyping(/** @type {LionCombobox} */ (el2), 'ch');
|
||||||
await el2.updateComplete;
|
await el2.updateComplete;
|
||||||
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(el2Prots._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||||
el2.formElements[1].id,
|
el2.formElements[1].id,
|
||||||
);
|
);
|
||||||
expect(el2.formElements[1].active).to.equal(true);
|
expect(el2.formElements[1].active).to.equal(true);
|
||||||
|
|
@ -1621,7 +1640,7 @@ describe('lion-combobox', () => {
|
||||||
el2.autocomplete = 'list';
|
el2.autocomplete = 'list';
|
||||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||||
await el2.updateComplete;
|
await el2.updateComplete;
|
||||||
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
expect(el2Prots._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||||
el2.formElements[1].id,
|
el2.formElements[1].id,
|
||||||
);
|
);
|
||||||
expect(el2.formElements[1].active).to.equal(true);
|
expect(el2.formElements[1].active).to.equal(true);
|
||||||
|
|
@ -1646,9 +1665,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(comboboxNode.contains(inputNode)).to.be.true;
|
expect(_comboboxNode.contains(_inputNode)).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has one input node with [role=combobox] in v1.0', async () => {
|
it('has one input node with [role=combobox] in v1.0', async () => {
|
||||||
|
|
@ -1657,9 +1676,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(comboboxNode).to.equal(inputNode);
|
expect(_comboboxNode).to.equal(_inputNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('autodetects aria version and sets it to 1.1 on Chromium browsers', async () => {
|
it('autodetects aria version and sets it to 1.1 on Chromium browsers', async () => {
|
||||||
|
|
@ -1671,9 +1690,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const elProts = getProtectedMembers(el);
|
const elProts = getComboboxMembers(el);
|
||||||
|
|
||||||
expect(elProts.ariaVersion).to.equal('1.1');
|
expect(elProts._ariaVersion).to.equal('1.1');
|
||||||
|
|
||||||
browserDetection.isChromium = false;
|
browserDetection.isChromium = false;
|
||||||
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
||||||
|
|
@ -1681,9 +1700,9 @@ describe('lion-combobox', () => {
|
||||||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
const el2Prots = getProtectedMembers(el2);
|
const el2Prots = getComboboxMembers(el2);
|
||||||
|
|
||||||
expect(el2Prots.ariaVersion).to.equal('1.0');
|
expect(el2Prots._ariaVersion).to.equal('1.0');
|
||||||
|
|
||||||
// restore...
|
// restore...
|
||||||
browserDetection.isChromium = browserDetectionIsChromiumOriginal;
|
browserDetection.isChromium = browserDetectionIsChromiumOriginal;
|
||||||
|
|
@ -1703,10 +1722,10 @@ describe('lion-combobox', () => {
|
||||||
</lion-combobox>
|
</lion-combobox>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { comboboxNode } = getProtectedMembers(el);
|
const { _comboboxNode } = getComboboxMembers(el);
|
||||||
|
|
||||||
// activate opened listbox
|
// activate opened listbox
|
||||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
mimicUserTyping(el, 'ch');
|
mimicUserTyping(el, 'ch');
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,13 @@
|
||||||
* @param {string} [flavor]
|
* @param {string} [flavor]
|
||||||
*/
|
*/
|
||||||
function checkChrome(flavor = 'google-chrome') {
|
function checkChrome(flavor = 'google-chrome') {
|
||||||
// @ts-ignore
|
const isChromium = /** @type {window & { chrome?: boolean}} */ (window).chrome;
|
||||||
const isChromium = window.chrome;
|
|
||||||
if (flavor === 'chromium') {
|
if (flavor === 'chromium') {
|
||||||
return isChromium;
|
return isChromium;
|
||||||
}
|
}
|
||||||
const winNav = window.navigator;
|
const winNav = window.navigator;
|
||||||
const vendorName = winNav.vendor;
|
const vendorName = winNav.vendor;
|
||||||
// @ts-ignore
|
const isOpera = typeof (/** @type {window & { opr?: boolean}} */ (window).opr) !== 'undefined';
|
||||||
const isOpera = typeof window.opr !== 'undefined';
|
|
||||||
const isIEedge = winNav.userAgent.indexOf('Edge') > -1;
|
const isIEedge = winNav.userAgent.indexOf('Edge') > -1;
|
||||||
const isIOSChrome = winNav.userAgent.match('CriOS');
|
const isIOSChrome = winNav.userAgent.match('CriOS');
|
||||||
|
|
||||||
|
|
|
||||||
2
packages/core/types/SlotMixinTypes.d.ts
vendored
2
packages/core/types/SlotMixinTypes.d.ts
vendored
|
|
@ -10,7 +10,7 @@ export declare class SlotHost {
|
||||||
/**
|
/**
|
||||||
* Obtains all the slots to create
|
* Obtains all the slots to create
|
||||||
*/
|
*/
|
||||||
get slots(): SlotsMap;
|
public get slots(): SlotsMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the creation of slots
|
* Starts the creation of slots
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { css, dedupeMixin, html, nothing, SlotMixin, DisabledMixin } from '@lion/core';
|
import { css, dedupeMixin, html, nothing, SlotMixin, DisabledMixin } from '@lion/core';
|
||||||
import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
|
|
||||||
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js';
|
||||||
import { Unparseable } from './validate/Unparseable.js';
|
import { Unparseable } from './validate/Unparseable.js';
|
||||||
|
import { FormRegisteringMixin } from './registration/FormRegisteringMixin.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
|
|
@ -9,7 +9,10 @@ import { Unparseable } from './validate/Unparseable.js';
|
||||||
* @typedef {import('@lion/core').CSSResultArray} CSSResultArray
|
* @typedef {import('@lion/core').CSSResultArray} CSSResultArray
|
||||||
* @typedef {import('@lion/core').nothing} nothing
|
* @typedef {import('@lion/core').nothing} nothing
|
||||||
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
* @typedef {import('@lion/core/types/SlotMixinTypes').SlotsMap} SlotsMap
|
||||||
|
* @typedef {import('./validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback
|
||||||
* @typedef {import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost
|
* @typedef {import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes.js').HTMLElementWithValue} HTMLElementWithValue
|
||||||
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
* @typedef {import('../types/FormControlMixinTypes.js').FormControlMixin} FormControlMixin
|
||||||
* @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
|
* @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
|
||||||
*/
|
*/
|
||||||
|
|
@ -162,7 +165,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {SlotsMap}
|
* @type {SlotsMap}
|
||||||
*/
|
*/
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -180,28 +183,30 @@ const FormControlMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return this.__getDirectSlotChild('input');
|
return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input'));
|
||||||
}
|
}
|
||||||
|
|
||||||
get _labelNode() {
|
get _labelNode() {
|
||||||
return this.__getDirectSlotChild('label');
|
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('label'));
|
||||||
}
|
}
|
||||||
|
|
||||||
get _helpTextNode() {
|
get _helpTextNode() {
|
||||||
return this.__getDirectSlotChild('help-text');
|
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _feedbackNode() {
|
get _feedbackNode() {
|
||||||
return /** @type {import('./validate/LionValidationFeedback').LionValidationFeedback | undefined} */ (this.__getDirectSlotChild(
|
return /** @type {LionValidationFeedback} */ (this.__getDirectSlotChild('feedback'));
|
||||||
'feedback',
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/** @type {string | undefined} */
|
/** @type {string} */
|
||||||
this.name = undefined;
|
this.name = '';
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this._inputId = uuid(this.localName);
|
this._inputId = uuid(this.localName);
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
|
|
@ -211,6 +216,8 @@ const FormControlMixinImplementation = superclass =>
|
||||||
/** @type {'child'|'choice-group'|'fieldset'} */
|
/** @type {'child'|'choice-group'|'fieldset'} */
|
||||||
this._repropagationRole = 'child';
|
this._repropagationRole = 'child';
|
||||||
this._isRepropagationEndpoint = false;
|
this._isRepropagationEndpoint = false;
|
||||||
|
/** @private */
|
||||||
|
this.__label = '';
|
||||||
this.addEventListener(
|
this.addEventListener(
|
||||||
'model-value-changed',
|
'model-value-changed',
|
||||||
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
/** @type {EventListenerOrEventListenerObject} */ (this.__repropagateChildrenValues),
|
||||||
|
|
@ -277,7 +284,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
_triggerInitialModelValueChangedEvent() {
|
_triggerInitialModelValueChangedEvent() {
|
||||||
this.__dispatchInitialModelValueChangedEvent();
|
this._dispatchInitialModelValueChangedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
|
|
@ -302,7 +309,14 @@ const FormControlMixinImplementation = superclass =>
|
||||||
this.addToAriaDescribedBy(_helpTextNode, { idPrefix: 'help-text' });
|
this.addToAriaDescribedBy(_helpTextNode, { idPrefix: 'help-text' });
|
||||||
}
|
}
|
||||||
if (_feedbackNode) {
|
if (_feedbackNode) {
|
||||||
_feedbackNode.setAttribute('aria-live', 'polite');
|
// Generic focus/blur handling that works for both Fields/FormGroups
|
||||||
|
this.addEventListener('focusin', () => {
|
||||||
|
_feedbackNode.setAttribute('aria-live', 'polite');
|
||||||
|
});
|
||||||
|
this.addEventListener('focusout', () => {
|
||||||
|
_feedbackNode.setAttribute('aria-live', 'assertive');
|
||||||
|
});
|
||||||
|
|
||||||
this.addToAriaDescribedBy(_feedbackNode, { idPrefix: 'feedback' });
|
this.addToAriaDescribedBy(_feedbackNode, { idPrefix: 'feedback' });
|
||||||
}
|
}
|
||||||
this._enhanceLightDomA11yForAdditionalSlots();
|
this._enhanceLightDomA11yForAdditionalSlots();
|
||||||
|
|
@ -340,7 +354,6 @@ const FormControlMixinImplementation = superclass =>
|
||||||
* @param {string} attrName
|
* @param {string} attrName
|
||||||
* @param {HTMLElement[]} nodes
|
* @param {HTMLElement[]} nodes
|
||||||
* @param {boolean|undefined} reorder
|
* @param {boolean|undefined} reorder
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
__reflectAriaAttr(attrName, nodes, reorder) {
|
__reflectAriaAttr(attrName, nodes, reorder) {
|
||||||
if (this._inputNode) {
|
if (this._inputNode) {
|
||||||
|
|
@ -538,17 +551,14 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?} modelValue
|
* @param {any} modelValue
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
// @ts-ignore FIXME: Move to FormatMixin? Since there we have access to modelValue prop
|
_isEmpty(modelValue = /** @type {any} */ (this).modelValue) {
|
||||||
_isEmpty(modelValue = this.modelValue) {
|
|
||||||
let value = modelValue;
|
let value = modelValue;
|
||||||
// @ts-ignore
|
if (/** @type {any} */ (this).modelValue instanceof Unparseable) {
|
||||||
if (this.modelValue instanceof Unparseable) {
|
value = /** @type {any} */ (this).modelValue.viewValue;
|
||||||
// @ts-ignore
|
|
||||||
value = this.modelValue.viewValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks for empty platform types: Objects, Arrays, Dates
|
// Checks for empty platform types: Objects, Arrays, Dates
|
||||||
|
|
@ -638,7 +648,6 @@ const FormControlMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
.../** @type {CSSResultArray} */ (super.styles || []),
|
|
||||||
css`
|
css`
|
||||||
/**********************
|
/**********************
|
||||||
{block} .form-field
|
{block} .form-field
|
||||||
|
|
@ -695,7 +704,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
/**
|
/**
|
||||||
* This function exposes descripion elements that a FormGroup should expose to its
|
* This function exposes descripion elements that a FormGroup should expose to its
|
||||||
* children. See FormGroupMixin.__getAllDescriptionElementsInParentChain()
|
* children. See FormGroupMixin.__getAllDescriptionElementsInParentChain()
|
||||||
* @return {Array.<HTMLElement|undefined>}
|
* @return {Array.<HTMLElement>}
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
// Returns dom references to all elements that should be referred to by field(s)
|
// Returns dom references to all elements that should be referred to by field(s)
|
||||||
|
|
@ -767,7 +776,6 @@ const FormControlMixinImplementation = superclass =>
|
||||||
/**
|
/**
|
||||||
* @param {string} slotName
|
* @param {string} slotName
|
||||||
* @return {HTMLElement | undefined}
|
* @return {HTMLElement | undefined}
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
__getDirectSlotChild(slotName) {
|
__getDirectSlotChild(slotName) {
|
||||||
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
||||||
|
|
@ -775,8 +783,7 @@ const FormControlMixinImplementation = superclass =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
_dispatchInitialModelValueChangedEvent() {
|
||||||
__dispatchInitialModelValueChangedEvent() {
|
|
||||||
// When we are not a fieldset / choice-group, we don't need to wait for our children
|
// When we are not a fieldset / choice-group, we don't need to wait for our children
|
||||||
// to send a unified event
|
// to send a unified event
|
||||||
if (this._repropagationRole === 'child') {
|
if (this._repropagationRole === 'child') {
|
||||||
|
|
@ -811,7 +818,6 @@ const FormControlMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {CustomEvent} ev
|
* @param {CustomEvent} ev
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
__repropagateChildrenValues(ev) {
|
__repropagateChildrenValues(ev) {
|
||||||
// Allows sub classes to internally listen to the children change events
|
// Allows sub classes to internally listen to the children change events
|
||||||
|
|
@ -882,19 +888,15 @@ const FormControlMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Extend this in choice group so that target is always a choice input and multipleChoice exists.
|
* Based on provided target, this condition determines whether received model-value-changed
|
||||||
* This will fix the types and reduce the need for ignores/expect-errors
|
* event should be repropagated
|
||||||
* @param {EventTarget & ChoiceInputHost} target
|
* @param {FormControlHost} target
|
||||||
* @protected
|
* @protected
|
||||||
* @overridable
|
* @overridable
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_repropagationCondition(target) {
|
_repropagationCondition(target) {
|
||||||
return !(
|
return Boolean(target);
|
||||||
this._repropagationRole === 'choice-group' &&
|
|
||||||
// @ts-expect-error multipleChoice is not directly available but only as side effect
|
|
||||||
!this.multipleChoice &&
|
|
||||||
!target.checked
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { ValidateMixin } from './validate/ValidateMixin.js';
|
||||||
// - simplify _calculateValues: recursive trigger lock can be omitted, since need for connecting
|
// - simplify _calculateValues: recursive trigger lock can be omitted, since need for connecting
|
||||||
// the loop via sync observers is not needed anymore.
|
// the loop via sync observers is not needed anymore.
|
||||||
// - consider `formatOn` as an overridable function, by default something like:
|
// - consider `formatOn` as an overridable function, by default something like:
|
||||||
// `(!__isHandlingUserInput || !hasError) && !focused`
|
// `(!_isHandlingUserInput || !hasError) && !focused`
|
||||||
// This would allow for more advanced scenarios, like formatting an input whenever it becomes valid.
|
// This would allow for more advanced scenarios, like formatting an input whenever it becomes valid.
|
||||||
// This would make formattedValue as a concept obsolete, since for maximum flexibility, the
|
// This would make formattedValue as a concept obsolete, since for maximum flexibility, the
|
||||||
// formattedValue condition needs to be evaluated right before syncing back to the view
|
// formattedValue condition needs to be evaluated right before syncing back to the view
|
||||||
|
|
@ -281,7 +281,7 @@ const FormatMixinImplementation = superclass =>
|
||||||
// - Why check for this.hasError?
|
// - Why check for this.hasError?
|
||||||
// We only want to format values that are considered valid. For best UX,
|
// We only want to format values that are considered valid. For best UX,
|
||||||
// we only 'reward' valid inputs.
|
// we only 'reward' valid inputs.
|
||||||
// - Why check for __isHandlingUserInput?
|
// - Why check for _isHandlingUserInput?
|
||||||
// Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2].
|
// Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2].
|
||||||
// If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
|
// If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
|
||||||
// the value, no matter what.
|
// the value, no matter what.
|
||||||
|
|
@ -290,7 +290,7 @@ const FormatMixinImplementation = superclass =>
|
||||||
// input into `._inputNode` with modelValue as input)
|
// input into `._inputNode` with modelValue as input)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.__isHandlingUserInput &&
|
this._isHandlingUserInput &&
|
||||||
this.hasFeedbackFor &&
|
this.hasFeedbackFor &&
|
||||||
this.hasFeedbackFor.length &&
|
this.hasFeedbackFor.length &&
|
||||||
this.hasFeedbackFor.includes('error') &&
|
this.hasFeedbackFor.includes('error') &&
|
||||||
|
|
@ -333,7 +333,7 @@ const FormatMixinImplementation = superclass =>
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: /** @type { ModelValueEventDetails } */ ({
|
detail: /** @type { ModelValueEventDetails } */ ({
|
||||||
formPath: [this],
|
formPath: [this],
|
||||||
isTriggeredByUser: Boolean(this.__isHandlingUserInput),
|
isTriggeredByUser: Boolean(this._isHandlingUserInput),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -376,7 +376,7 @@ const FormatMixinImplementation = superclass =>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_reflectBackOn() {
|
_reflectBackOn() {
|
||||||
return !this.__isHandlingUserInput;
|
return !this._isHandlingUserInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This can be called whenever the view value should be updated. Dependent on component type
|
// This can be called whenever the view value should be updated. Dependent on component type
|
||||||
|
|
@ -397,9 +397,9 @@ const FormatMixinImplementation = superclass =>
|
||||||
_onUserInputChanged() {
|
_onUserInputChanged() {
|
||||||
// Upwards syncing. Most properties are delegated right away, value is synced to
|
// Upwards syncing. Most properties are delegated right away, value is synced to
|
||||||
// `LionField`, to be able to act on (imperatively set) value changes
|
// `LionField`, to be able to act on (imperatively set) value changes
|
||||||
this.__isHandlingUserInput = true;
|
this._isHandlingUserInput = true;
|
||||||
this._syncValueUpwards();
|
this._syncValueUpwards();
|
||||||
this.__isHandlingUserInput = false;
|
this._isHandlingUserInput = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import { dedupeMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
import { FormControlMixin } from './FormControlMixin.js';
|
||||||
|
import { FocusMixin } from './FocusMixin.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/NativeTextFieldMixinTypes').NativeTextFieldMixin} NativeTextFieldMixin
|
* @typedef {import('../types/NativeTextFieldMixinTypes').NativeTextFieldMixin} NativeTextFieldMixin
|
||||||
* @type {NativeTextFieldMixin}
|
* @type {NativeTextFieldMixin}
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<import('../types/NativeTextFieldMixinTypes').NativeTextField>} superclass} superclass
|
* @param {import('@open-wc/dedupe-mixin').Constructor<import('@lion/core').LitElement>} superclass} superclass
|
||||||
*/
|
*/
|
||||||
const NativeTextFieldMixinImplementation = superclass =>
|
const NativeTextFieldMixinImplementation = superclass =>
|
||||||
class NativeTextFieldMixin extends superclass {
|
class NativeTextFieldMixin extends FocusMixin(FormControlMixin(superclass)) {
|
||||||
|
/**
|
||||||
|
* @protected
|
||||||
|
* @type {HTMLInputElement | HTMLTextAreaElement}
|
||||||
|
*/
|
||||||
|
get _inputNode() {
|
||||||
|
return /** @type {HTMLInputElement | HTMLTextAreaElement} */ (super._inputNode);
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
get selectionStart() {
|
get selectionStart() {
|
||||||
const native = this._inputNode;
|
const native = this._inputNode;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get modelValue() {
|
get modelValue() {
|
||||||
const elems = this._getCheckedElements();
|
const elems = this._getCheckedElements();
|
||||||
if (this.multipleChoice) {
|
if (this.multipleChoice) {
|
||||||
|
|
@ -62,13 +61,13 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
this.registrationComplete.then(() => {
|
this.registrationComplete.then(() => {
|
||||||
this.__isInitialModelValue = false;
|
this.__isInitialModelValue = false;
|
||||||
this._setCheckedElements(value, checkCondition);
|
this._setCheckedElements(value, checkCondition);
|
||||||
this.requestUpdate('modelValue', this.__oldModelValue);
|
this.requestUpdate('modelValue', this._oldModelValue);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._setCheckedElements(value, checkCondition);
|
this._setCheckedElements(value, checkCondition);
|
||||||
this.requestUpdate('modelValue', this.__oldModelValue);
|
this.requestUpdate('modelValue', this._oldModelValue);
|
||||||
}
|
}
|
||||||
this.__oldModelValue = this.modelValue;
|
this._oldModelValue = this.modelValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
get serializedValue() {
|
get serializedValue() {
|
||||||
|
|
@ -201,7 +200,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
_triggerInitialModelValueChangedEvent() {
|
_triggerInitialModelValueChangedEvent() {
|
||||||
this.registrationComplete.then(() => {
|
this.registrationComplete.then(() => {
|
||||||
this.__dispatchInitialModelValueChangedEvent();
|
this._dispatchInitialModelValueChangedEvent();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +228,6 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
_throwWhenInvalidChildModelValue(child) {
|
_throwWhenInvalidChildModelValue(child) {
|
||||||
if (
|
if (
|
||||||
// @ts-expect-error
|
|
||||||
typeof child.modelValue.checked !== 'boolean' ||
|
typeof child.modelValue.checked !== 'boolean' ||
|
||||||
!Object.prototype.hasOwnProperty.call(child.modelValue, 'value')
|
!Object.prototype.hasOwnProperty.call(child.modelValue, 'value')
|
||||||
) {
|
) {
|
||||||
|
|
@ -350,8 +348,22 @@ const ChoiceGroupMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.__setChoiceGroupTouched();
|
this.__setChoiceGroupTouched();
|
||||||
this.requestUpdate('modelValue', this.__oldModelValue);
|
this.requestUpdate('modelValue', this._oldModelValue);
|
||||||
this.__oldModelValue = this.modelValue;
|
this._oldModelValue = this.modelValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't repropagate unchecked single choice choiceInputs
|
||||||
|
* @param {FormControlHost & ChoiceInputHost} target
|
||||||
|
* @protected
|
||||||
|
* @overridable
|
||||||
|
*/
|
||||||
|
_repropagationCondition(target) {
|
||||||
|
return !(
|
||||||
|
this._repropagationRole === 'choice-group' &&
|
||||||
|
!this.multipleChoice &&
|
||||||
|
!target.checked
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,11 +234,13 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.__isHandlingUserInput = true;
|
this._isHandlingUserInput = true;
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
this.__isHandlingUserInput = false;
|
this._isHandlingUserInput = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: make this less fuzzy by applying these methods in LionRadio and LionCheckbox
|
||||||
|
// via instanceof (or feat. detection for tree-shaking in case parentGroup not needed)
|
||||||
/**
|
/**
|
||||||
* Override this in case of extending ChoiceInputMixin and requiring
|
* Override this in case of extending ChoiceInputMixin and requiring
|
||||||
* to sync differently with parent form group name
|
* to sync differently with parent form group name
|
||||||
|
|
@ -247,9 +249,9 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_syncNameToParentFormGroup() {
|
_syncNameToParentFormGroup() {
|
||||||
// @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin
|
// @ts-expect-error [external]: tagName should be a prop of HTMLElement
|
||||||
if (this._parentFormGroup.tagName.includes(this.tagName)) {
|
if (this._parentFormGroup.tagName.includes(this.tagName)) {
|
||||||
this.name = this._parentFormGroup.name;
|
this.name = this._parentFormGroup?.name || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,7 +307,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
||||||
if (old && old.modelValue) {
|
if (old && old.modelValue) {
|
||||||
_old = old.modelValue;
|
_old = old.modelValue;
|
||||||
}
|
}
|
||||||
// @ts-expect-error lit private property
|
// @ts-expect-error [external]: lit private property
|
||||||
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, _old)) {
|
if (this.constructor._classProperties.get('modelValue').hasChanged(modelValue, _old)) {
|
||||||
super._onModelValueChanged({ modelValue });
|
super._onModelValueChanged({ modelValue });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ export class FormElementsHaveNoError extends Validator {
|
||||||
/**
|
/**
|
||||||
* @param {unknown} [value]
|
* @param {unknown} [value]
|
||||||
* @param {string | undefined} [options]
|
* @param {string | undefined} [options]
|
||||||
* @param {{ node: any }} config
|
* @param {{ node: any }} [config]
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
execute(value, options, config) {
|
execute(value, options, config) {
|
||||||
const hasError = config.node._anyFormElementHasFeedbackFor('error');
|
const hasError = config?.node._anyFormElementHasFeedbackFor('error');
|
||||||
return hasError;
|
return hasError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,11 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get modelValue() {
|
get modelValue() {
|
||||||
return this._getFromAllFormElements('modelValue');
|
return this._getFromAllFormElements('modelValue');
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +184,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
_triggerInitialModelValueChangedEvent() {
|
_triggerInitialModelValueChangedEvent() {
|
||||||
this.registrationComplete.then(() => {
|
this.registrationComplete.then(() => {
|
||||||
this.__dispatchInitialModelValueChangedEvent();
|
this._dispatchInitialModelValueChangedEvent();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -306,7 +306,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
||||||
const result = {};
|
const result = {};
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-protected]: allow Form internals to access this protected method
|
||||||
this.formElements._keys().forEach(name => {
|
this.formElements._keys().forEach(name => {
|
||||||
const elem = this.formElements[name];
|
const elem = this.formElements[name];
|
||||||
if (elem instanceof FormControlsCollection) {
|
if (elem instanceof FormControlsCollection) {
|
||||||
|
|
@ -448,6 +448,7 @@ const FormGroupMixinImplementation = superclass =>
|
||||||
const unTypedThis = /** @type {unknown} */ (this);
|
const unTypedThis = /** @type {unknown} */ (this);
|
||||||
let parent = /** @type {FormControlHost & { _parentFormGroup:any }} */ (unTypedThis);
|
let parent = /** @type {FormControlHost & { _parentFormGroup:any }} */ (unTypedThis);
|
||||||
while (parent) {
|
while (parent) {
|
||||||
|
// @ts-ignore [allow-protected]: in parent/child relations we are allowed to call protected methods
|
||||||
const descriptionElements = parent._getAriaDescriptionElements();
|
const descriptionElements = parent._getAriaDescriptionElements();
|
||||||
const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true });
|
const orderedEls = getAriaElementsInRightDomOrder(descriptionElements, { reverse: true });
|
||||||
orderedEls.forEach(el => {
|
orderedEls.forEach(el => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { dedupeMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import('@lion/core').LitElement} LitElement
|
||||||
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringMixin} FormRegisteringMixin
|
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringMixin} FormRegisteringMixin
|
||||||
|
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost
|
||||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
||||||
*/
|
*/
|
||||||
|
|
@ -12,7 +15,7 @@ import { dedupeMixin } from '@lion/core';
|
||||||
* This Mixin registers a form element to a Registrar
|
* This Mixin registers a form element to a Registrar
|
||||||
*
|
*
|
||||||
* @type {FormRegisteringMixin}
|
* @type {FormRegisteringMixin}
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
* @param {import('@open-wc/dedupe-mixin').Constructor<LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const FormRegisteringMixinImplementation = superclass =>
|
const FormRegisteringMixinImplementation = superclass =>
|
||||||
class extends superclass {
|
class extends superclass {
|
||||||
|
|
@ -23,11 +26,7 @@ const FormRegisteringMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
super.connectedCallback();
|
||||||
if (super.connectedCallback) {
|
|
||||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
|
||||||
super.connectedCallback();
|
|
||||||
}
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('form-element-register', {
|
new CustomEvent('form-element-register', {
|
||||||
detail: { element: this },
|
detail: { element: this },
|
||||||
|
|
@ -37,13 +36,9 @@ const FormRegisteringMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
super.disconnectedCallback();
|
||||||
if (super.disconnectedCallback) {
|
|
||||||
// @ts-expect-error check it anyway, because could be lit-element extension
|
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
if (this._parentFormGroup) {
|
if (this._parentFormGroup) {
|
||||||
this._parentFormGroup.removeFormElement(this);
|
this._parentFormGroup.removeFormElement(/** @type {* & FormRegisteringHost} */ (this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@ import { FormControlsCollection } from './FormControlsCollection.js';
|
||||||
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarMixin} FormRegistrarMixin
|
||||||
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
||||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||||
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost
|
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
|
||||||
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -28,6 +26,7 @@ import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
||||||
const FormRegistrarMixinImplementation = superclass =>
|
const FormRegistrarMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-shadow, no-unused-vars
|
// eslint-disable-next-line no-shadow, no-unused-vars
|
||||||
class extends FormRegisteringMixin(superclass) {
|
class extends FormRegisteringMixin(superclass) {
|
||||||
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
|
|
@ -131,9 +130,8 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
*/
|
*/
|
||||||
addFormElement(child, indexToInsertAt) {
|
addFormElement(child, indexToInsertAt) {
|
||||||
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
||||||
// @ts-expect-error FormControl needs to be at the bottom of the hierarchy
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
child._parentFormGroup = this;
|
child._parentFormGroup = /** @type {* & FormRegistrarHost} */ (this);
|
||||||
|
|
||||||
// 1. Add children as array element
|
// 1. Add children as array element
|
||||||
if (indexToInsertAt >= 0) {
|
if (indexToInsertAt >= 0) {
|
||||||
|
|
@ -149,7 +147,6 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||||
throw new TypeError('You need to define a name');
|
throw new TypeError('You need to define a name');
|
||||||
}
|
}
|
||||||
// @ts-expect-error this._isFormOrFieldset true means we can assume `this.name` exists
|
|
||||||
if (name === this.name) {
|
if (name === this.name) {
|
||||||
console.info('Error Node:', child); // eslint-disable-line no-console
|
console.info('Error Node:', child); // eslint-disable-line no-console
|
||||||
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
throw new TypeError(`You can not have the same name "${name}" as your parent`);
|
||||||
|
|
@ -176,7 +173,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FormRegisteringHost} child the child element (field)
|
* @param {FormControlHost} child the child element (field)
|
||||||
*/
|
*/
|
||||||
removeFormElement(child) {
|
removeFormElement(child) {
|
||||||
// 1. Handle array based children
|
// 1. Handle array based children
|
||||||
|
|
@ -187,7 +184,6 @@ const FormRegistrarMixinImplementation = superclass =>
|
||||||
|
|
||||||
// 2. Handle name based object keys
|
// 2. Handle name based object keys
|
||||||
if (this._isFormOrFieldset) {
|
if (this._isFormOrFieldset) {
|
||||||
// @ts-expect-error
|
|
||||||
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
const { name } = child; // FIXME: <-- ElementWithParentFormGroup should become LionFieldWithParentFormGroup so that "name" exists
|
||||||
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
if (name.substr(-2) === '[]' && this.formElements[name]) {
|
||||||
const idx = this.formElements[name].indexOf(child);
|
const idx = this.formElements[name].indexOf(child);
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
||||||
// @ts-expect-error accessing private lit property
|
// @ts-expect-error [external]: accessing private lit property
|
||||||
const properties = this._classProperties;
|
const properties = this._classProperties;
|
||||||
if (properties.get(name) && properties.get(name).hasChanged) {
|
if (properties.get(name) && properties.get(name).hasChanged) {
|
||||||
return properties.get(name).hasChanged(newValue, oldValue);
|
return properties.get(name).hasChanged(newValue, oldValue);
|
||||||
|
|
@ -80,6 +80,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
// Empty queue...
|
// Empty queue...
|
||||||
if (ns.queue) {
|
if (ns.queue) {
|
||||||
Array.from(ns.queue).forEach(name => {
|
Array.from(ns.queue).forEach(name => {
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
if (ctor.__syncUpdatableHasChanged(name, this[name], undefined)) {
|
if (ctor.__syncUpdatableHasChanged(name, this[name], undefined)) {
|
||||||
this.updateSync(name, undefined);
|
this.updateSync(name, undefined);
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +106,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
||||||
// Makes sure that we only initialize one time, with most up to date value
|
// Makes sure that we only initialize one time, with most up to date value
|
||||||
ns.queue.add(name);
|
ns.queue.add(name);
|
||||||
} // After connectedCallback: guarded proxy to updateSync
|
} // After connectedCallback: guarded proxy to updateSync
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
else if (ctor.__syncUpdatableHasChanged(name, this[name], oldValue)) {
|
else if (ctor.__syncUpdatableHasChanged(name, this[name], oldValue)) {
|
||||||
this.updateSync(name, oldValue);
|
this.updateSync(name, oldValue);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,6 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
* @overridable
|
* @overridable
|
||||||
* Adds "._feedbackNode" as described below
|
* Adds "._feedbackNode" as described below
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
/**
|
/**
|
||||||
* FIXME: Ugly workaround https://github.com/microsoft/TypeScript/issues/40110
|
* FIXME: Ugly workaround https://github.com/microsoft/TypeScript/issues/40110
|
||||||
|
|
@ -460,7 +459,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
||||||
if (source === 'async' || !hasAsync) {
|
if (source === 'async' || !hasAsync) {
|
||||||
if (this.__validateCompleteResolve) {
|
if (this.__validateCompleteResolve) {
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-private]
|
||||||
this.__validateCompleteResolve();
|
this.__validateCompleteResolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -569,7 +568,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
if (validator.config.fieldName) {
|
if (validator.config.fieldName) {
|
||||||
fieldName = await validator.config.fieldName;
|
fieldName = await validator.config.fieldName;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-protected]
|
||||||
const message = await validator._getMessage({
|
const message = await validator._getMessage({
|
||||||
modelValue: this.modelValue,
|
modelValue: this.modelValue,
|
||||||
formControl: this,
|
formControl: this,
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,15 @@ export class Required extends Validator {
|
||||||
/**
|
/**
|
||||||
* @param {FormControlHost & HTMLElement} formControl
|
* @param {FormControlHost & HTMLElement} formControl
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore [allow-protected] we are allowed to know FormControl protcected props in form-core
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onFormControlConnect(formControl) {
|
onFormControlConnect({ _inputNode: inputNode }) {
|
||||||
if (formControl._inputNode) {
|
if (inputNode) {
|
||||||
const role = formControl._inputNode.getAttribute('role') || '';
|
const role = inputNode.getAttribute('role') || '';
|
||||||
const elementTagName = formControl._inputNode.tagName.toLowerCase();
|
const elementTagName = inputNode.tagName.toLowerCase();
|
||||||
const ctor = /** @type {typeof Required} */ (this.constructor);
|
const ctor = /** @type {typeof Required} */ (this.constructor);
|
||||||
if (ctor._compatibleRoles.includes(role) || ctor._compatibleTags.includes(elementTagName)) {
|
if (ctor._compatibleRoles.includes(role) || ctor._compatibleTags.includes(elementTagName)) {
|
||||||
formControl._inputNode.setAttribute('aria-required', 'true');
|
inputNode.setAttribute('aria-required', 'true');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,10 +61,11 @@ export class Required extends Validator {
|
||||||
/**
|
/**
|
||||||
* @param {FormControlHost & HTMLElement} formControl
|
* @param {FormControlHost & HTMLElement} formControl
|
||||||
*/
|
*/
|
||||||
|
// @ts-ignore [allow-protected] we are allowed to know FormControl protcected props in form-core
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onFormControlDisconnect(formControl) {
|
onFormControlDisconnect({ _inputNode: inputNode }) {
|
||||||
if (formControl._inputNode) {
|
if (inputNode) {
|
||||||
formControl._inputNode.removeAttribute('aria-required');
|
inputNode.removeAttribute('aria-required');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export {
|
|
||||||
AlwaysInvalid,
|
|
||||||
AlwaysValid,
|
|
||||||
AsyncAlwaysValid,
|
|
||||||
AsyncAlwaysInvalid,
|
|
||||||
} from './test-helpers/ExampleValidators.js';
|
|
||||||
21
packages/form-core/test-helpers/getFormControlMembers.js
Normal file
21
packages/form-core/test-helpers/getFormControlMembers.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
* @typedef {import('../types/validate/ValidateMixinTypes').ValidateHost} ValidateHost
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes private and protected FormControl members
|
||||||
|
* @param {FormControlHost} el
|
||||||
|
*/
|
||||||
|
export function getFormControlMembers(el) {
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const { _inputNode, _helpTextNode, _labelNode, _feedbackNode, _allValidators } = el;
|
||||||
|
return {
|
||||||
|
_inputNode,
|
||||||
|
_helpTextNode,
|
||||||
|
_labelNode,
|
||||||
|
_feedbackNode,
|
||||||
|
_allValidators: /** @type {* & ValidateHost} */ (el)._allValidators,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './ExampleValidators.js';
|
export * from './ExampleValidators.js';
|
||||||
|
export * from './getFormControlMembers.js';
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,9 @@ import { FormRegisteringMixin } from '../src/registration/FormRegisteringMixin.j
|
||||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||||
import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPortalMixin.js';
|
import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPortalMixin.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} customConfig
|
* @typedef {Object} customConfig
|
||||||
* @property {typeof LitElement} [baseElement]
|
* @property {typeof LitElement|undefined} [baseElement]
|
||||||
* @property {string} [customConfig.suffix]
|
* @property {string} [customConfig.suffix]
|
||||||
* @property {string} [customConfig.parentTagString]
|
* @property {string} [customConfig.parentTagString]
|
||||||
* @property {string} [customConfig.childTagString]
|
* @property {string} [customConfig.childTagString]
|
||||||
|
|
@ -22,7 +18,6 @@ import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPorta
|
||||||
*/
|
*/
|
||||||
export const runRegistrationSuite = customConfig => {
|
export const runRegistrationSuite = customConfig => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/38535 fixed in later typescript version
|
|
||||||
baseElement: LitElement,
|
baseElement: LitElement,
|
||||||
...customConfig,
|
...customConfig,
|
||||||
};
|
};
|
||||||
|
|
@ -90,7 +85,7 @@ export const runRegistrationSuite = customConfig => {
|
||||||
it('works for components that have a delayed render', async () => {
|
it('works for components that have a delayed render', async () => {
|
||||||
class PerformUpdate extends FormRegistrarMixin(LitElement) {
|
class PerformUpdate extends FormRegistrarMixin(LitElement) {
|
||||||
async performUpdate() {
|
async performUpdate() {
|
||||||
await new Promise(resolve => setTimeout(() => resolve(), 10));
|
await new Promise(resolve => setTimeout(() => resolve(undefined), 10));
|
||||||
await super.performUpdate();
|
await super.performUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +259,7 @@ export const runRegistrationSuite = customConfig => {
|
||||||
const delayedPortalString = defineCE(
|
const delayedPortalString = defineCE(
|
||||||
class extends FormRegistrarPortalMixin(LitElement) {
|
class extends FormRegistrarPortalMixin(LitElement) {
|
||||||
async performUpdate() {
|
async performUpdate() {
|
||||||
await new Promise(resolve => setTimeout(() => resolve(), 10));
|
await new Promise(resolve => setTimeout(() => resolve(undefined), 10));
|
||||||
await super.performUpdate();
|
await super.performUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-w
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { FormatMixin } from '../src/FormatMixin.js';
|
import { FormatMixin } from '../src/FormatMixin.js';
|
||||||
import { Unparseable, Validator } from '../index.js';
|
import { Unparseable, Validator } from '../index.js';
|
||||||
|
import { getFormControlMembers } from '../test-helpers/getFormControlMembers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
* @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType
|
* @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -284,10 +286,11 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
|
|
||||||
describe('View value', () => {
|
describe('View value', () => {
|
||||||
it('has an input node (like <input>/<textarea>) which holds the formatted (view) value', async () => {
|
it('has an input node (like <input>/<textarea>) which holds the formatted (view) value', async () => {
|
||||||
|
const { _inputNode } = getFormControlMembers(fooFormat);
|
||||||
fooFormat.modelValue = 'string';
|
fooFormat.modelValue = 'string';
|
||||||
expect(fooFormat.formattedValue).to.equal('foo: string');
|
expect(fooFormat.formattedValue).to.equal('foo: string');
|
||||||
expect(fooFormat.value).to.equal('foo: string');
|
expect(fooFormat.value).to.equal('foo: string');
|
||||||
expect(fooFormat._inputNode.value).to.equal('foo: string');
|
expect(_inputNode.value).to.equal('foo: string');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works if there is no underlying _inputNode', async () => {
|
it('works if there is no underlying _inputNode', async () => {
|
||||||
|
|
@ -305,16 +308,17 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getFormControlMembers(formatEl);
|
||||||
|
|
||||||
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
|
||||||
const generatedModelValue = generateValueBasedOnType();
|
const generatedModelValue = generateValueBasedOnType();
|
||||||
mimicUserInput(formatEl, generatedViewValue);
|
mimicUserInput(formatEl, generatedViewValue);
|
||||||
expect(formatEl._inputNode.value).to.not.equal(`foo: ${generatedModelValue}`);
|
expect(_inputNode.value).to.not.equal(`foo: ${generatedModelValue}`);
|
||||||
|
|
||||||
// user leaves field
|
// user leaves field
|
||||||
formatEl._inputNode.dispatchEvent(new CustomEvent(formatEl.formatOn, { bubbles: true }));
|
_inputNode.dispatchEvent(new CustomEvent(formatEl.formatOn, { bubbles: true }));
|
||||||
await aTimeout(0);
|
await aTimeout(0);
|
||||||
expect(formatEl._inputNode.value).to.equal(`foo: ${generatedModelValue}`);
|
expect(_inputNode.value).to.equal(`foo: ${generatedModelValue}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
|
it('reflects back .formattedValue immediately when .modelValue changed imperatively', async () => {
|
||||||
|
|
@ -323,17 +327,20 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
|
||||||
// it can hold errorState (affecting the formatting)
|
// it can hold errorState (affecting the formatting)
|
||||||
el.hasFeedbackFor = ['error'];
|
el.hasFeedbackFor = ['error'];
|
||||||
|
|
||||||
// users types value 'test'
|
// users types value 'test'
|
||||||
mimicUserInput(el, 'test');
|
mimicUserInput(el, 'test');
|
||||||
expect(el._inputNode.value).to.not.equal('foo: test');
|
expect(_inputNode.value).to.not.equal('foo: test');
|
||||||
|
|
||||||
// Now see the difference for an imperative change
|
// Now see the difference for an imperative change
|
||||||
el.modelValue = 'test2';
|
el.modelValue = 'test2';
|
||||||
expect(el._inputNode.value).to.equal('foo: test2');
|
expect(_inputNode.value).to.equal('foo: test2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -494,6 +501,8 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(preprocessorSpy.callCount).to.equal(1);
|
expect(preprocessorSpy.callCount).to.equal(1);
|
||||||
|
|
||||||
const parserSpy = sinon.spy(el, 'parser');
|
const parserSpy = sinon.spy(el, 'parser');
|
||||||
|
|
@ -501,7 +510,7 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
|
|
||||||
expect(preprocessorSpy.callCount).to.equal(2);
|
expect(preprocessorSpy.callCount).to.equal(2);
|
||||||
expect(parserSpy.lastCall.args[0]).to.equal(val);
|
expect(parserSpy.lastCall.args[0]).to.equal(val);
|
||||||
expect(el._inputNode.value).to.equal(val);
|
expect(_inputNode.value).to.equal(val);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not preprocess during composition', async () => {
|
it('does not preprocess during composition', async () => {
|
||||||
|
|
@ -510,13 +519,16 @@ export function runFormatMixinSuite(customConfig) {
|
||||||
<input slot="input">
|
<input slot="input">
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
const preprocessorSpy = sinon.spy(el, 'preprocessor');
|
const preprocessorSpy = sinon.spy(el, 'preprocessor');
|
||||||
el._inputNode.dispatchEvent(new Event('compositionstart', { bubbles: true }));
|
_inputNode.dispatchEvent(new Event('compositionstart', { bubbles: true }));
|
||||||
mimicUserInput(el, '`');
|
mimicUserInput(el, '`');
|
||||||
expect(preprocessorSpy.callCount).to.equal(0);
|
expect(preprocessorSpy.callCount).to.equal(0);
|
||||||
// "à" would be sent by the browser after pressing "option + `", followed by "a"
|
// "à" would be sent by the browser after pressing "option + `", followed by "a"
|
||||||
mimicUserInput(el, 'à');
|
mimicUserInput(el, 'à');
|
||||||
el._inputNode.dispatchEvent(new Event('compositionend', { bubbles: true }));
|
_inputNode.dispatchEvent(new Event('compositionend', { bubbles: true }));
|
||||||
expect(preprocessorSpy.callCount).to.equal(1);
|
expect(preprocessorSpy.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
unsafeStatic,
|
unsafeStatic,
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
|
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
|
||||||
import { ValidateMixin } from '../src/validate/ValidateMixin.js';
|
import { ValidateMixin } from '../src/validate/ValidateMixin.js';
|
||||||
import { MinLength } from '../src/validate/validators/StringValidators.js';
|
import { MinLength } from '../src/validate/validators/StringValidators.js';
|
||||||
|
|
@ -135,6 +136,7 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
const targetEl = el._inputNode || el;
|
const targetEl = el._inputNode || el;
|
||||||
targetEl.dispatchEvent(new Event('focus', { bubbles: true }));
|
targetEl.dispatchEvent(new Event('focus', { bubbles: true }));
|
||||||
el.modelValue = modelValue;
|
el.modelValue = modelValue;
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
targetEl.dispatchEvent(new Event(el._leaveEvent, { bubbles: true }));
|
targetEl.dispatchEvent(new Event(el._leaveEvent, { bubbles: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -224,20 +226,22 @@ export function runInteractionStateMixinSuite(customConfig) {
|
||||||
const el = /** @type {IState} */ (await fixture(html`
|
const el = /** @type {IState} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new MinLength(3)]}></${tag}>
|
<${tag} .validators=${[new MinLength(3)]}></${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
expect(_feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
|
|
||||||
// has error but does not show/forward to component as showCondition is not met
|
// has error but does not show/forward to component as showCondition is not met
|
||||||
el.modelValue = '1';
|
el.modelValue = '1';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
expect(_feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
|
|
||||||
el.submitted = true;
|
el.submitted = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.length).to.equal(1);
|
expect(_feedbackNode.feedbackData?.length).to.equal(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import {
|
import {
|
||||||
MaxLength,
|
MaxLength,
|
||||||
|
|
@ -15,7 +16,7 @@ import {
|
||||||
AlwaysValid,
|
AlwaysValid,
|
||||||
AsyncAlwaysInvalid,
|
AsyncAlwaysInvalid,
|
||||||
AsyncAlwaysValid,
|
AsyncAlwaysValid,
|
||||||
} from '../test-helpers.js';
|
} from '../test-helpers/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{tagString?: string | null, lightDom?: string}} [customConfig]
|
* @param {{tagString?: string | null, lightDom?: string}} [customConfig]
|
||||||
|
|
@ -153,6 +154,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const clearSpy = sinon.spy(el, '__clearValidationResults');
|
const clearSpy = sinon.spy(el, '__clearValidationResults');
|
||||||
const validateSpy = sinon.spy(el, 'validate');
|
const validateSpy = sinon.spy(el, 'validate');
|
||||||
el.modelValue = 'x';
|
el.modelValue = 'x';
|
||||||
|
|
@ -174,6 +176,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
|
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
||||||
const validateSpy = sinon.spy(el, 'validate');
|
const validateSpy = sinon.spy(el, 'validate');
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
|
|
@ -191,7 +194,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
const el = /** @type {ValidateElement} */ (await fixture(html`
|
const el = /** @type {ValidateElement} */ (await fixture(html`
|
||||||
<${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}>
|
<${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
const isEmptySpy = sinon.spy(el, '__isEmpty');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
expect(isEmptySpy.calledBefore(syncSpy)).to.be.true;
|
expect(isEmptySpy.calledBefore(syncSpy)).to.be.true;
|
||||||
|
|
@ -203,7 +208,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
${lightDom}
|
${lightDom}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
expect(syncSpy.calledBefore(asyncSpy)).to.be.true;
|
expect(syncSpy.calledBefore(asyncSpy)).to.be.true;
|
||||||
|
|
@ -223,7 +230,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
const syncSpy = sinon.spy(el, '__executeSyncValidators');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
|
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
|
||||||
|
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
|
|
@ -236,7 +245,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const resultSpy = sinon.spy(el, '__executeResultValidators');
|
const resultSpy = sinon.spy(el, '__executeResultValidators');
|
||||||
|
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
|
|
@ -266,6 +277,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
el.modelValue = 'nonEmpty';
|
el.modelValue = 'nonEmpty';
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
|
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
|
||||||
await el.validateComplete;
|
await el.validateComplete;
|
||||||
expect(validateResolveSpy.callCount).to.equal(1);
|
expect(validateResolveSpy.callCount).to.equal(1);
|
||||||
|
|
@ -610,10 +622,14 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.modelValue=${'myValue'}
|
.modelValue=${'myValue'}
|
||||||
>${lightDom}</${withSuccessTag}>
|
>${lightDom}</${withSuccessTag}>
|
||||||
`));
|
`));
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const prevValidationResult = el.__prevValidationResult;
|
const prevValidationResult = el.__prevValidationResult;
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const prevShownValidationResult = el.__prevShownValidationResult;
|
const prevShownValidationResult = el.__prevShownValidationResult;
|
||||||
const regularValidationResult = [
|
const regularValidationResult = [
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
...el.__syncValidationResult,
|
...el.__syncValidationResult,
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
...el.__asyncValidationResult,
|
...el.__asyncValidationResult,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -643,6 +659,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const totalValidationResult = el.__validationResult;
|
const totalValidationResult = el.__validationResult;
|
||||||
expect(totalValidationResult).to.eql([resultV, validator]);
|
expect(totalValidationResult).to.eql([resultV, validator]);
|
||||||
});
|
});
|
||||||
|
|
@ -673,6 +690,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
`));
|
`));
|
||||||
const validator = /** @type {Validator} */ (el.validators.find(v => v instanceof Required));
|
const validator = /** @type {Validator} */ (el.validators.find(v => v instanceof Required));
|
||||||
const executeSpy = sinon.spy(validator, 'execute');
|
const executeSpy = sinon.spy(validator, 'execute');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const privateIsEmptySpy = sinon.spy(el, '__isEmpty');
|
const privateIsEmptySpy = sinon.spy(el, '__isEmpty');
|
||||||
el.modelValue = null;
|
el.modelValue = null;
|
||||||
expect(executeSpy.callCount).to.equal(0);
|
expect(executeSpy.callCount).to.equal(0);
|
||||||
|
|
@ -682,7 +700,6 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
it('calls "._isEmpty" when provided (useful for different modelValues)', async () => {
|
it('calls "._isEmpty" when provided (useful for different modelValues)', async () => {
|
||||||
class _isEmptyValidate extends ValidateMixin(LitElement) {
|
class _isEmptyValidate extends ValidateMixin(LitElement) {
|
||||||
_isEmpty() {
|
_isEmpty() {
|
||||||
// @ts-expect-error
|
|
||||||
return this.modelValue.model === '';
|
return this.modelValue.model === '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -723,9 +740,11 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.modelValue=${''}
|
.modelValue=${''}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
expect(el._inputNode?.getAttribute('aria-required')).to.equal('true');
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
expect(_inputNode?.getAttribute('aria-required')).to.equal('true');
|
||||||
el.validators = [];
|
el.validators = [];
|
||||||
expect(el._inputNode?.getAttribute('aria-required')).to.be.null;
|
expect(_inputNode?.getAttribute('aria-required')).to.be.null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -777,17 +796,20 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
<${preconfTag}
|
<${preconfTag}
|
||||||
.validators=${[new MinLength(3)]}
|
.validators=${[new MinLength(3)]}
|
||||||
></${preconfTag}>`));
|
></${preconfTag}>`));
|
||||||
|
const { _allValidators } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el.validators.length).to.equal(1);
|
expect(el.validators.length).to.equal(1);
|
||||||
expect(el.defaultValidators.length).to.equal(1);
|
expect(el.defaultValidators.length).to.equal(1);
|
||||||
expect(el._allValidators.length).to.equal(2);
|
expect(_allValidators.length).to.equal(2);
|
||||||
|
|
||||||
expect(el._allValidators[0] instanceof MinLength).to.be.true;
|
expect(_allValidators[0] instanceof MinLength).to.be.true;
|
||||||
expect(el._allValidators[1] instanceof AlwaysInvalid).to.be.true;
|
expect(_allValidators[1] instanceof AlwaysInvalid).to.be.true;
|
||||||
|
|
||||||
el.validators = [new MaxLength(5)];
|
el.validators = [new MaxLength(5)];
|
||||||
expect(el._allValidators[0] instanceof MaxLength).to.be.true;
|
const { _allValidators: _allValidatorsMl } = getFormControlMembers(el);
|
||||||
expect(el._allValidators[1] instanceof AlwaysInvalid).to.be.true;
|
|
||||||
|
expect(_allValidatorsMl[0] instanceof MaxLength).to.be.true;
|
||||||
|
expect(_allValidatorsMl[1] instanceof AlwaysInvalid).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -916,8 +938,9 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
.validators=${[new MinLength(3, { message: 'foo' })]}>
|
||||||
<input slot="input">
|
<input slot="input">
|
||||||
</${tag}>`));
|
</${tag}>`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
if (el._inputNode) {
|
if (_inputNode) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const spy = sinon.spy(el._inputNode, 'setCustomValidity');
|
const spy = sinon.spy(el._inputNode, 'setCustomValidity');
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
|
|
@ -1022,9 +1045,13 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${customTypeTag}>
|
>${lightDom}</${customTypeTag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
||||||
const feedbackNode = /** @type {import('../src/validate/LionValidationFeedback').LionValidationFeedback} */ (el._feedbackNode);
|
const feedbackNode =
|
||||||
|
/** @type {import('../src/validate/LionValidationFeedback').LionValidationFeedback} */
|
||||||
|
(_feedbackNode);
|
||||||
const resultOrder = feedbackNode.feedbackData?.map(v => v.type);
|
const resultOrder = feedbackNode.feedbackData?.map(v => v.type);
|
||||||
expect(resultOrder).to.deep.equal(['error', 'x', 'y']);
|
expect(resultOrder).to.deep.equal(['error', 'x', 'y']);
|
||||||
|
|
||||||
|
|
@ -1165,6 +1192,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
const spy = sinon.spy(el, '_updateShouldShowFeedbackFor');
|
const spy = sinon.spy(el, '_updateShouldShowFeedbackFor');
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
// for ... of is already allowed we should update eslint...
|
// for ... of is already allowed we should update eslint...
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { LitElement } from '@lion/core';
|
||||||
import { localize } from '@lion/localize';
|
import { localize } from '@lion/localize';
|
||||||
import { localizeTearDown } from '@lion/localize/test-helpers';
|
import { localizeTearDown } from '@lion/localize/test-helpers';
|
||||||
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
|
import { DefaultSuccess, MinLength, Required, ValidateMixin, Validator } from '../index.js';
|
||||||
import { AlwaysInvalid } from '../test-helpers.js';
|
import { AlwaysInvalid } from '../test-helpers/index.js';
|
||||||
|
|
||||||
export function runValidateMixinFeedbackPart() {
|
export function runValidateMixinFeedbackPart() {
|
||||||
describe('Validity Feedback', () => {
|
describe('Validity Feedback', () => {
|
||||||
|
|
@ -121,11 +122,13 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.modelValue=${'cat'}
|
.modelValue=${'cat'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
expect(_feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
el.validators = [new AlwaysInvalid()];
|
el.validators = [new AlwaysInvalid()];
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has configurable feedback visibility hook', async () => {
|
it('has configurable feedback visibility hook', async () => {
|
||||||
|
|
@ -136,14 +139,17 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
el._prioritizeAndFilterFeedback = () => []; // filter out all errors
|
el._prioritizeAndFilterFeedback = () => []; // filter out all errors
|
||||||
await el.validate();
|
await el.validate();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
expect(_feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
|
it('writes prioritized result to "._feedbackNode" based on Validator order', async () => {
|
||||||
|
|
@ -154,9 +160,11 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
|
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for AlwaysInvalid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders validation result to "._feedbackNode" when async messages are resolved', async () => {
|
it('renders validation result to "._feedbackNode" when async messages are resolved', async () => {
|
||||||
|
|
@ -178,13 +186,13 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
expect(_feedbackNode.feedbackData).to.be.undefined;
|
||||||
unlockMessage();
|
unlockMessage();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal(
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('this ends up in "._feedbackNode"');
|
||||||
'this ends up in "._feedbackNode"',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// N.B. this replaces the 'config.hideFeedback' option we had before...
|
// N.B. this replaces the 'config.hideFeedback' option we had before...
|
||||||
|
|
@ -207,14 +215,13 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.validators=${[new AlwaysInvalid()]}
|
.validators=${[new AlwaysInvalid()]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el._feedbackNode.feedbackData).to.be.undefined;
|
expect(_feedbackNode.feedbackData).to.be.undefined;
|
||||||
unlockMessage();
|
unlockMessage();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal(
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('this ends up in "._feedbackNode"');
|
||||||
'this ends up in "._feedbackNode"',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports custom element to render feedback', async () => {
|
it('supports custom element to render feedback', async () => {
|
||||||
|
|
@ -257,20 +264,21 @@ export function runValidateMixinFeedbackPart() {
|
||||||
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
|
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
|
expect(_feedbackNode.localName).to.equal(customFeedbackTagString);
|
||||||
|
|
||||||
el.modelValue = 'dog';
|
el.modelValue = 'dog';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
await el._feedbackNode.updateComplete;
|
await _feedbackNode.updateComplete;
|
||||||
expect(el._feedbackNode).shadowDom.to.equal('Custom for ContainsLowercaseA');
|
expect(_feedbackNode).shadowDom.to.equal('Custom for ContainsLowercaseA');
|
||||||
|
|
||||||
el.modelValue = 'cat';
|
el.modelValue = 'cat';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
await el._feedbackNode.updateComplete;
|
await _feedbackNode.updateComplete;
|
||||||
expect(el._feedbackNode).shadowDom.to.equal('Custom for AlwaysInvalid');
|
expect(_feedbackNode).shadowDom.to.equal('Custom for AlwaysInvalid');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports custom messages in Validator instance configuration object', async () => {
|
it('supports custom messages in Validator instance configuration object', async () => {
|
||||||
|
|
@ -280,11 +288,12 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
.validators=${[new MinLength(3, { getMessage: () => 'custom via config' })]}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('custom via config');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('custom via config');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the feedback component when locale changes', async () => {
|
it('updates the feedback component when locale changes', async () => {
|
||||||
|
|
@ -295,13 +304,15 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.length).to.equal(1);
|
expect(_feedbackNode.feedbackData?.length).to.equal(1);
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
localize.locale = 'de-DE';
|
localize.locale = 'de-DE';
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Nachricht für MinLength');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows success message after fixing an error', async () => {
|
it('shows success message after fixing an error', async () => {
|
||||||
|
|
@ -321,16 +332,17 @@ export function runValidateMixinFeedbackPart() {
|
||||||
]}
|
]}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
el.modelValue = 'a';
|
el.modelValue = 'a';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
|
||||||
|
|
||||||
el.modelValue = 'abcd';
|
el.modelValue = 'abcd';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('This is a success message');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
|
|
@ -342,7 +354,9 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.modelValue=${'a'}
|
.modelValue=${'a'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
const inputNode = el._inputNode;
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
const inputNode = _inputNode;
|
||||||
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
|
||||||
|
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
|
|
@ -493,12 +507,13 @@ export function runValidateMixinFeedbackPart() {
|
||||||
.modelValue=${'1'}
|
.modelValue=${'1'}
|
||||||
>${lightDom}</${tag}>
|
>${lightDom}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
el.modelValue = '12345';
|
el.modelValue = '12345';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
||||||
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
|
expect(_feedbackNode.feedbackData).to.deep.equal([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { LionInput } from '@lion/input';
|
||||||
import '@lion/fieldset/define';
|
import '@lion/fieldset/define';
|
||||||
import { FormGroupMixin, Required } from '@lion/form-core';
|
import { FormGroupMixin, Required } from '@lion/form-core';
|
||||||
import { expect, html, fixture, fixtureSync, unsafeStatic } from '@open-wc/testing';
|
import { expect, html, fixture, fixtureSync, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import sinon from 'sinon';
|
||||||
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
|
import { ChoiceGroupMixin } from '../../src/choice-group/ChoiceGroupMixin.js';
|
||||||
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
||||||
|
|
||||||
|
|
@ -11,7 +12,7 @@ customElements.define('choice-input-foo', ChoiceInputFoo);
|
||||||
class ChoiceInputBar extends ChoiceInputMixin(LionInput) {
|
class ChoiceInputBar extends ChoiceInputMixin(LionInput) {
|
||||||
_syncNameToParentFormGroup() {
|
_syncNameToParentFormGroup() {
|
||||||
// Always sync, without conditions
|
// Always sync, without conditions
|
||||||
this.name = this._parentFormGroup.name;
|
this.name = this._parentFormGroup?.name || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define('choice-input-bar', ChoiceInputBar);
|
customElements.define('choice-input-bar', ChoiceInputBar);
|
||||||
|
|
@ -626,5 +627,54 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Modelvalue event propagation', () => {
|
||||||
|
it('sends one event for single select choice-groups', async () => {
|
||||||
|
const formSpy = sinon.spy();
|
||||||
|
const choiceGroupSpy = sinon.spy();
|
||||||
|
const formEl = await fixture(html`
|
||||||
|
<lion-fieldset name="form">
|
||||||
|
<${parentTag} name="choice-group">
|
||||||
|
<${childTag} id="option1" .choiceValue="${'1'}" checked></${childTag}>
|
||||||
|
<${childTag} id="option2" .choiceValue="${'2'}"></${childTag}>
|
||||||
|
</${parentTag}>
|
||||||
|
</lion-fieldset>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const choiceGroupEl = /** @type {ChoiceInputGroup} */ (formEl.querySelector(
|
||||||
|
'[name=choice-group]',
|
||||||
|
));
|
||||||
|
if (choiceGroupEl.multipleChoice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/** @typedef {{ checked: boolean }} checkedInterface */
|
||||||
|
const option1El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector(
|
||||||
|
'#option1',
|
||||||
|
));
|
||||||
|
const option2El = /** @type {HTMLElement & checkedInterface} */ (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(choiceGroupEv.detail.isTriggeredByUser).to.be.false;
|
||||||
|
|
||||||
|
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]);
|
||||||
|
expect(formEv.detail.isTriggeredByUser).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Required } from '@lion/form-core';
|
import { Required } from '@lion/form-core';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
|
||||||
|
|
||||||
|
|
@ -86,9 +87,11 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(counter).to.equal(0);
|
expect(counter).to.equal(0);
|
||||||
// Here we try to mimic user interaction by firing browser events
|
// Here we try to mimic user interaction by firing browser events
|
||||||
const nativeInput = el._inputNode;
|
const nativeInput = _inputNode;
|
||||||
nativeInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); // fired by (at least) Chrome
|
nativeInput.dispatchEvent(new CustomEvent('input', { bubbles: true })); // fired by (at least) Chrome
|
||||||
expect(counter).to.equal(0);
|
expect(counter).to.equal(0);
|
||||||
nativeInput.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
nativeInput.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
||||||
|
|
@ -104,14 +107,16 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode, _labelNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
el.click();
|
el.click();
|
||||||
expect(spy.args[0][0].target).to.equal(el);
|
expect(spy.args[0][0].target).to.equal(el);
|
||||||
expect(spy.callCount).to.equal(1);
|
expect(spy.callCount).to.equal(1);
|
||||||
el._labelNode.click();
|
_labelNode.click();
|
||||||
expect(spy.args[1][0].target).to.equal(el._labelNode);
|
expect(spy.args[1][0].target).to.equal(_labelNode);
|
||||||
expect(spy.callCount).to.equal(2);
|
expect(spy.callCount).to.equal(2);
|
||||||
el._inputNode.click();
|
_inputNode.click();
|
||||||
expect(spy.args[2][0].target).to.equal(el._inputNode);
|
expect(spy.args[2][0].target).to.equal(_inputNode);
|
||||||
expect(spy.callCount).to.equal(3);
|
expect(spy.callCount).to.equal(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -126,7 +131,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
el._inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
_inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
||||||
expect(isTriggeredByUser).to.be.true;
|
expect(isTriggeredByUser).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -134,6 +141,7 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
const el = /** @type {ChoiceInput} */ (await fixture(html`
|
||||||
<${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}>
|
<${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
expect(el.hasFeedbackFor).to.include('error');
|
expect(el.hasFeedbackFor).to.include('error');
|
||||||
expect(el.validationStates.error).to.exist;
|
expect(el.validationStates.error).to.exist;
|
||||||
expect(el.validationStates.error.Required).to.exist;
|
expect(el.validationStates.error.Required).to.exist;
|
||||||
|
|
@ -156,19 +164,23 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
|
|
||||||
it('can be checked and unchecked programmatically', async () => {
|
it('can be checked and unchecked programmatically', async () => {
|
||||||
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
el.checked = true;
|
el.checked = true;
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._inputNode.checked).to.be.true;
|
expect(/** @type {HTMLInputElement} */ (_inputNode).checked).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be checked and unchecked via user interaction', async () => {
|
it('can be checked and unchecked via user interaction', async () => {
|
||||||
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
el._inputNode.click();
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
_inputNode.click();
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
el._inputNode.click();
|
_inputNode.click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
if (el.type === 'checkbox') {
|
if (el.type === 'checkbox') {
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
|
|
@ -177,7 +189,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
|
|
||||||
it('can not toggle the checked state when disabled via user interaction', async () => {
|
it('can not toggle the checked state when disabled via user interaction', async () => {
|
||||||
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
||||||
el._inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
|
_inputNode.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -206,7 +220,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
));
|
));
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
|
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked');
|
const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked');
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
const spyCheckedToModel = sinon.spy(el, '__syncCheckedToModel');
|
const spyCheckedToModel = sinon.spy(el, '__syncCheckedToModel');
|
||||||
el.checked = true;
|
el.checked = true;
|
||||||
expect(el.modelValue.checked).to.be.true;
|
expect(el.modelValue.checked).to.be.true;
|
||||||
|
|
@ -234,14 +250,16 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
|
||||||
<input slot="input" />
|
<input slot="input" />
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
const { _inputNode: _inputNodeChecked } = getFormControlMembers(elChecked);
|
||||||
|
|
||||||
// Initial values
|
// Initial values
|
||||||
expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
|
expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
|
||||||
expect(hasAttr(elChecked)).to.equal(true, 'initial checked element');
|
expect(hasAttr(elChecked)).to.equal(true, 'initial checked element');
|
||||||
|
|
||||||
// Via user interaction
|
// Via user interaction
|
||||||
el._inputNode.click();
|
_inputNode.click();
|
||||||
elChecked._inputNode.click();
|
_inputNodeChecked.click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
expect(hasAttr(el)).to.equal(true, 'user click checked');
|
expect(hasAttr(el)).to.equal(true, 'user click checked');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
import { localizeTearDown } from '@lion/localize/test-helpers';
|
import { localizeTearDown } from '@lion/localize/test-helpers';
|
||||||
import { defineCE, expect, html, unsafeStatic, fixture } from '@open-wc/testing';
|
import { defineCE, expect, html, unsafeStatic, fixture } from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import '@lion/form-core/define';
|
import '@lion/form-core/define';
|
||||||
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
||||||
|
|
@ -67,20 +68,23 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
|
||||||
<${childTag} name="B" label="fieldB"></${childTag}>
|
<${childTag} name="B" label="fieldB"></${childTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _labelNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {LionInput} formControl
|
* @param {LionInput} formControl
|
||||||
*/
|
*/
|
||||||
function getLabels(formControl) {
|
function getLabels(formControl) {
|
||||||
return /** @type {string} */ (formControl._inputNode.getAttribute('aria-labelledby')).split(
|
const control = getFormControlMembers(formControl);
|
||||||
|
|
||||||
|
return /** @type {string} */ (control._inputNode.getAttribute('aria-labelledby')).split(
|
||||||
' ',
|
' ',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const field1 = el.formElements[0];
|
const field1 = el.formElements[0];
|
||||||
const field2 = el.formElements[1];
|
const field2 = el.formElements[1];
|
||||||
|
|
||||||
expect(getLabels(field1)).to.eql([field1._labelNode.id, el._labelNode.id]);
|
expect(getLabels(field1)).to.eql([field1._labelNode.id, _labelNode.id]);
|
||||||
expect(getLabels(field2)).to.eql([field2._labelNode.id, el._labelNode.id]);
|
expect(getLabels(field2)).to.eql([field2._labelNode.id, _labelNode.id]);
|
||||||
|
|
||||||
// Test the cleanup on disconnected
|
// Test the cleanup on disconnected
|
||||||
el.removeChild(field1);
|
el.removeChild(field1);
|
||||||
|
|
@ -277,10 +281,12 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
|
||||||
|
|
||||||
// Check cleanup of FormGroup on disconnect
|
// Check cleanup of FormGroup on disconnect
|
||||||
const l2_g = /** @type {FormGroup} */ (childAriaFixture.querySelector('[name=l2_g]'));
|
const l2_g = /** @type {FormGroup} */ (childAriaFixture.querySelector('[name=l2_g]'));
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
expect(l2_g.__descriptionElementsInParentChain.size).to.not.equal(0);
|
expect(l2_g.__descriptionElementsInParentChain.size).to.not.equal(0);
|
||||||
// @ts-expect-error removeChild should always be inherited via LitElement?
|
// @ts-expect-error removeChild should always be inherited via LitElement?
|
||||||
l2_g._parentFormGroup.removeChild(l2_g);
|
l2_g._parentFormGroup.removeChild(l2_g);
|
||||||
await l2_g.updateComplete;
|
await l2_g.updateComplete;
|
||||||
|
// @ts-ignore [allow-private] in test
|
||||||
expect(l2_g.__descriptionElementsInParentChain.size).to.equal(0);
|
expect(l2_g.__descriptionElementsInParentChain.size).to.equal(0);
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
// @ts-ignore
|
|
||||||
import { localizeTearDown } from '@lion/localize/test-helpers';
|
import { localizeTearDown } from '@lion/localize/test-helpers';
|
||||||
import {
|
import {
|
||||||
defineCE,
|
defineCE,
|
||||||
|
|
@ -11,10 +10,10 @@ import {
|
||||||
aTimeout,
|
aTimeout,
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
// @ts-ignore
|
|
||||||
import { IsNumber, Validator, LionField } from '@lion/form-core';
|
import { IsNumber, Validator, LionField } from '@lion/form-core';
|
||||||
import '@lion/form-core/define';
|
import '@lion/form-core/define';
|
||||||
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
||||||
|
import { getFormControlMembers } from '../../test-helpers/getFormControlMembers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ tagString?: string, childTagString?:string }} [cfg]
|
* @param {{ tagString?: string, childTagString?:string }} [cfg]
|
||||||
|
|
@ -63,12 +62,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
const el1 = /** @type {FormGroup} */ (await fixture(
|
const el1 = /** @type {FormGroup} */ (await fixture(
|
||||||
html`<${tag} label="foo">${inputSlots}</${tag}>`,
|
html`<${tag} label="foo">${inputSlots}</${tag}>`,
|
||||||
));
|
));
|
||||||
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
|
const { _labelNode: _labelNode1 } = getFormControlMembers(el1);
|
||||||
|
expect(el1.fieldName).to.equal(_labelNode1.textContent);
|
||||||
|
|
||||||
const el2 = /** @type {FormGroup} */ (await fixture(
|
const el2 = /** @type {FormGroup} */ (await fixture(
|
||||||
html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`,
|
html`<${tag}><label slot="label">bar</label>${inputSlots}</${tag}>`,
|
||||||
));
|
));
|
||||||
expect(el2.fieldName).to.equal(el2._labelNode.textContent);
|
const { _labelNode: _labelNode2 } = getFormControlMembers(el2);
|
||||||
|
expect(el2.fieldName).to.equal(_labelNode2.textContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`has a fieldName based on the name if no label exists`, async () => {
|
it(`has a fieldName based on the name if no label exists`, async () => {
|
||||||
|
|
@ -82,17 +83,18 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
const el = /** @type {FormGroup} */ (await fixture(html`
|
const el = /** @type {FormGroup} */ (await fixture(html`
|
||||||
<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>
|
<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>
|
||||||
`));
|
`));
|
||||||
|
// @ts-ignore [allow-proteced] in test
|
||||||
expect(el.__fieldName).to.equal(el.fieldName);
|
expect(el.__fieldName).to.equal(el.fieldName);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test
|
// TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test
|
||||||
it(`${tagString} has an up to date list of every form element in .formElements`, async () => {
|
it(`${tagString} has an up to date list of every form element in .formElements`, async () => {
|
||||||
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
|
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-proteced] in test
|
||||||
expect(el.formElements._keys().length).to.equal(3);
|
expect(el.formElements._keys().length).to.equal(3);
|
||||||
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
||||||
el.removeChild(el.formElements['hobbies[]'][0]);
|
el.removeChild(el.formElements['hobbies[]'][0]);
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-proteced] in test
|
||||||
expect(el.formElements._keys().length).to.equal(3);
|
expect(el.formElements._keys().length).to.equal(3);
|
||||||
expect(el.formElements['hobbies[]'].length).to.equal(1);
|
expect(el.formElements['hobbies[]'].length).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
@ -207,16 +209,17 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
const newField = /** @type {FormGroup} */ (await fixture(
|
const newField = /** @type {FormGroup} */ (await fixture(
|
||||||
html`<${childTag} name="lastName"></${childTag}>`,
|
html`<${childTag} name="lastName"></${childTag}>`,
|
||||||
));
|
));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-protected] in test
|
||||||
expect(el.formElements._keys().length).to.equal(3);
|
expect(el.formElements._keys().length).to.equal(3);
|
||||||
|
|
||||||
el.appendChild(newField);
|
el.appendChild(newField);
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-protected] in test
|
||||||
expect(el.formElements._keys().length).to.equal(4);
|
expect(el.formElements._keys().length).to.equal(4);
|
||||||
|
|
||||||
el._inputNode.removeChild(newField);
|
_inputNode.removeChild(newField);
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-protected] in test
|
||||||
expect(el.formElements._keys().length).to.equal(3);
|
expect(el.formElements._keys().length).to.equal(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -332,11 +335,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
};
|
};
|
||||||
expect(el.modelValue).to.deep.equal(initState);
|
expect(el.modelValue).to.deep.equal(initState);
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
el.modelValue = undefined;
|
el.modelValue = undefined;
|
||||||
expect(el.modelValue).to.deep.equal(initState);
|
expect(el.modelValue).to.deep.equal(initState);
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
el.modelValue = null;
|
el.modelValue = null;
|
||||||
expect(el.modelValue).to.deep.equal(initState);
|
expect(el.modelValue).to.deep.equal(initState);
|
||||||
});
|
});
|
||||||
|
|
@ -512,11 +513,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
|
|
||||||
it('sets touched when last field in fieldset left after focus', async () => {
|
it('sets touched when last field in fieldset left after focus', async () => {
|
||||||
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
|
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
|
||||||
|
const { _inputNode: hobbyInputNode } = getFormControlMembers(
|
||||||
await triggerFocusFor(el.formElements['hobbies[]'][0]._inputNode);
|
el.formElements['hobbies[]'][0],
|
||||||
await triggerFocusFor(
|
|
||||||
el.formElements['hobbies[]'][el.formElements['gender[]'].length - 1]._inputNode,
|
|
||||||
);
|
);
|
||||||
|
const { _inputNode: genderInputNode } = getFormControlMembers(
|
||||||
|
el.formElements['hobbies[]'][el.formElements['gender[]'].length - 1],
|
||||||
|
);
|
||||||
|
|
||||||
|
await triggerFocusFor(hobbyInputNode);
|
||||||
|
await triggerFocusFor(genderInputNode);
|
||||||
const button = /** @type {FormGroup} */ (await fixture(html`<button></button>`));
|
const button = /** @type {FormGroup} */ (await fixture(html`<button></button>`));
|
||||||
button.focus();
|
button.focus();
|
||||||
|
|
||||||
|
|
@ -927,12 +932,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
html`<${tag} touched dirty>${inputSlots}</${tag}>`,
|
html`<${tag} touched dirty>${inputSlots}</${tag}>`,
|
||||||
));
|
));
|
||||||
// Safety check initially
|
// Safety check initially
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
el._setValueForAllFormElements('prefilled', true);
|
el._setValueForAllFormElements('prefilled', true);
|
||||||
expect(el.dirty).to.equal(true, '"dirty" initially');
|
expect(el.dirty).to.equal(true, '"dirty" initially');
|
||||||
expect(el.touched).to.equal(true, '"touched" initially');
|
expect(el.touched).to.equal(true, '"touched" initially');
|
||||||
expect(el.prefilled).to.equal(true, '"prefilled" initially');
|
expect(el.prefilled).to.equal(true, '"prefilled" initially');
|
||||||
|
|
||||||
// Reset all children states, with prefilled false
|
// Reset all children states, with prefilled false
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
el._setValueForAllFormElements('modelValue', {});
|
el._setValueForAllFormElements('modelValue', {});
|
||||||
el.resetInteractionState();
|
el.resetInteractionState();
|
||||||
expect(el.dirty).to.equal(false, 'not "dirty" after reset');
|
expect(el.dirty).to.equal(false, 'not "dirty" after reset');
|
||||||
|
|
@ -940,6 +947,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
expect(el.prefilled).to.equal(false, 'not "prefilled" after reset');
|
expect(el.prefilled).to.equal(false, 'not "prefilled" after reset');
|
||||||
|
|
||||||
// Reset all children states with prefilled true
|
// Reset all children states with prefilled true
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
el._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled
|
el._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled
|
||||||
el.resetInteractionState();
|
el.resetInteractionState();
|
||||||
expect(el.dirty).to.equal(false, 'not "dirty" after 2nd reset');
|
expect(el.dirty).to.equal(false, 'not "dirty" after 2nd reset');
|
||||||
|
|
@ -1026,6 +1034,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
`));
|
`));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
el.modelValue['child[]'] = ['foo2', 'bar2'];
|
el.modelValue['child[]'] = ['foo2', 'bar2'];
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
|
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1042,6 +1051,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
||||||
</${childTag}>
|
</${childTag}>
|
||||||
`));
|
`));
|
||||||
el.appendChild(childEl);
|
el.appendChild(childEl);
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
|
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
import { defineCE, expect, fixture, html, oneEvent, unsafeStatic } from '@open-wc/testing';
|
import { defineCE, expect, fixture, html, oneEvent, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import { FocusMixin } from '../src/FocusMixin.js';
|
import { FocusMixin } from '../src/FocusMixin.js';
|
||||||
|
|
||||||
describe('FocusMixin', () => {
|
describe('FocusMixin', () => {
|
||||||
|
|
@ -16,16 +17,19 @@ describe('FocusMixin', () => {
|
||||||
const el = /** @type {Focusable} */ (await fixture(html`
|
const el = /** @type {Focusable} */ (await fixture(html`
|
||||||
<${tag}><input slot="input"></${tag}>
|
<${tag}><input slot="input"></${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
el.focus();
|
el.focus();
|
||||||
expect(document.activeElement === el._inputNode).to.be.true;
|
expect(document.activeElement === _inputNode).to.be.true;
|
||||||
el.blur();
|
el.blur();
|
||||||
expect(document.activeElement === el._inputNode).to.be.false;
|
expect(document.activeElement === _inputNode).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has an attribute focused when focused', async () => {
|
it('has an attribute focused when focused', async () => {
|
||||||
const el = /** @type {Focusable} */ (await fixture(html`
|
const el = /** @type {Focusable} */ (await fixture(html`
|
||||||
<${tag}><input slot="input"></${tag}>
|
<${tag}><input slot="input"></${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
el.focus();
|
el.focus();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.hasAttribute('focused')).to.be.true;
|
expect(el.hasAttribute('focused')).to.be.true;
|
||||||
|
|
@ -39,10 +43,12 @@ describe('FocusMixin', () => {
|
||||||
const el = /** @type {Focusable} */ (await fixture(html`
|
const el = /** @type {Focusable} */ (await fixture(html`
|
||||||
<${tag}><input slot="input"></${tag}>
|
<${tag}><input slot="input"></${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el.focused).to.be.false;
|
expect(el.focused).to.be.false;
|
||||||
el._inputNode?.focus();
|
_inputNode?.focus();
|
||||||
expect(el.focused).to.be.true;
|
expect(el.focused).to.be.true;
|
||||||
el._inputNode?.blur();
|
_inputNode?.blur();
|
||||||
expect(el.focused).to.be.false;
|
expect(el.focused).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
|
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { FormControlMixin } from '../src/FormControlMixin.js';
|
import { FormControlMixin } from '../src/FormControlMixin.js';
|
||||||
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
|
||||||
|
import { FocusMixin } from '../src/FocusMixin.js';
|
||||||
|
import { FormGroupMixin } from '../src/form-group/FormGroupMixin.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
*/
|
||||||
|
|
||||||
describe('FormControlMixin', () => {
|
describe('FormControlMixin', () => {
|
||||||
const inputSlot = html`<input slot="input" />`;
|
const inputSlot = html`<input slot="input" />`;
|
||||||
|
|
@ -112,15 +119,17 @@ describe('FormControlMixin', () => {
|
||||||
</div>
|
</div>
|
||||||
`));
|
`));
|
||||||
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
||||||
const labelIdsBefore = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
const descriptionIdsBefore = /** @type {string} */ (el._inputNode.getAttribute(
|
|
||||||
|
const labelIdsBefore = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
|
||||||
|
const descriptionIdsBefore = /** @type {string} */ (_inputNode.getAttribute(
|
||||||
'aria-describedby',
|
'aria-describedby',
|
||||||
));
|
));
|
||||||
// Reconnect
|
// Reconnect
|
||||||
wrapper.removeChild(el);
|
wrapper.removeChild(el);
|
||||||
wrapper.appendChild(el);
|
wrapper.appendChild(el);
|
||||||
const labelIdsAfter = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
|
const labelIdsAfter = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
|
||||||
const descriptionIdsAfter = /** @type {string} */ (el._inputNode.getAttribute(
|
const descriptionIdsAfter = /** @type {string} */ (_inputNode.getAttribute(
|
||||||
'aria-describedby',
|
'aria-describedby',
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
@ -128,21 +137,6 @@ describe('FormControlMixin', () => {
|
||||||
expect(descriptionIdsBefore).to.equal(descriptionIdsAfter);
|
expect(descriptionIdsBefore).to.equal(descriptionIdsAfter);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds aria-live="polite" to the feedback slot', async () => {
|
|
||||||
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
|
|
||||||
<${tag}>
|
|
||||||
${inputSlot}
|
|
||||||
<div slot="feedback">Added to see attributes</div>
|
|
||||||
</${tag}>
|
|
||||||
`));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
Array.from(el.children)
|
|
||||||
.find(child => child.slot === 'feedback')
|
|
||||||
?.getAttribute('aria-live'),
|
|
||||||
).to.equal('polite');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clicking the label should call `_onLabelClick`', async () => {
|
it('clicking the label should call `_onLabelClick`', async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
|
const el = /** @type {FormControlMixinClass} */ (await fixture(html`
|
||||||
|
|
@ -150,11 +144,84 @@ describe('FormControlMixin', () => {
|
||||||
${inputSlot}
|
${inputSlot}
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _labelNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(spy).to.not.have.been.called;
|
expect(spy).to.not.have.been.called;
|
||||||
el._labelNode.click();
|
_labelNode.click();
|
||||||
expect(spy).to.have.been.calledOnce;
|
expect(spy).to.have.been.calledOnce;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Feedback slot aria-live', () => {
|
||||||
|
// See: https://www.w3.org/WAI/tutorials/forms/notifications/#on-focus-change
|
||||||
|
it(`adds aria-live="polite" to the feedback slot on focus, aria-live="assertive" to the feedback slot on blur,
|
||||||
|
so error messages appearing on blur will be read before those of the next input`, async () => {
|
||||||
|
const FormControlWithRegistrarMixinClass = class extends FormGroupMixin(LitElement) {};
|
||||||
|
|
||||||
|
const groupTagString = defineCE(FormControlWithRegistrarMixinClass);
|
||||||
|
const groupTag = unsafeStatic(groupTagString);
|
||||||
|
|
||||||
|
const focusableTagString = defineCE(
|
||||||
|
class extends FocusMixin(FormControlMixin(LitElement)) {},
|
||||||
|
);
|
||||||
|
const focusableTag = unsafeStatic(focusableTagString);
|
||||||
|
|
||||||
|
const formEl = await fixture(html`
|
||||||
|
<${groupTag} name="form">
|
||||||
|
<${groupTag} name="fieldset">
|
||||||
|
<${focusableTag} name="field1">
|
||||||
|
${inputSlot}
|
||||||
|
<div slot="feedback">
|
||||||
|
Error message with:
|
||||||
|
- aria-live="polite" on focused (during typing an end user should not be bothered for best UX)
|
||||||
|
- aria-live="assertive" on blur (so that the message that eventually appears
|
||||||
|
on blur will be read before message of the next focused input)
|
||||||
|
</div>
|
||||||
|
</${focusableTag}>
|
||||||
|
<${focusableTag} name="field2">
|
||||||
|
${inputSlot}
|
||||||
|
<div slot="feedback">
|
||||||
|
Should be read after the error message of field 1
|
||||||
|
</div>
|
||||||
|
</${focusableTag}>
|
||||||
|
<div slot="feedback">
|
||||||
|
Group message... Should be read after the error message of field 2
|
||||||
|
</div>
|
||||||
|
</${groupTag}>
|
||||||
|
<${focusableTag} name="field3">
|
||||||
|
${inputSlot}
|
||||||
|
<div slot="feedback">
|
||||||
|
Should be read after the error message of field 2
|
||||||
|
</div>
|
||||||
|
</${focusableTag}>
|
||||||
|
</${groupTag}>
|
||||||
|
`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {* & import('../types/FormControlMixinTypes').FormControlHost} FormControl
|
||||||
|
*/
|
||||||
|
const field1El = /** @type {FormControl} */ (formEl.querySelector('[name=field1]'));
|
||||||
|
const field2El = /** @type {FormControl} */ (formEl.querySelector('[name=field2]'));
|
||||||
|
const field3El = /** @type {FormControl} */ (formEl.querySelector('[name=field3]'));
|
||||||
|
const fieldsetEl = /** @type {FormControl} */ (formEl.querySelector('[name=fieldset]'));
|
||||||
|
|
||||||
|
field1El.focus();
|
||||||
|
expect(field1El._feedbackNode.getAttribute('aria-live')).to.equal('polite');
|
||||||
|
|
||||||
|
field2El.focus();
|
||||||
|
// field1El just blurred
|
||||||
|
expect(field1El._feedbackNode.getAttribute('aria-live')).to.equal('assertive');
|
||||||
|
expect(field2El._feedbackNode.getAttribute('aria-live')).to.equal('polite');
|
||||||
|
|
||||||
|
field3El.focus();
|
||||||
|
// field2El just blurred
|
||||||
|
expect(field2El._feedbackNode.getAttribute('aria-live')).to.equal('assertive');
|
||||||
|
// fieldsetEl just blurred
|
||||||
|
|
||||||
|
expect(fieldsetEl._feedbackNode.getAttribute('aria-live')).to.equal('assertive');
|
||||||
|
expect(field3El._feedbackNode.getAttribute('aria-live')).to.equal('polite');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Adding extra labels and descriptions', () => {
|
describe('Adding extra labels and descriptions', () => {
|
||||||
it(`supports centrally orchestrated labels/descriptions via addToAriaLabelledBy() /
|
it(`supports centrally orchestrated labels/descriptions via addToAriaLabelledBy() /
|
||||||
removeFromAriaLabelledBy() / addToAriaDescribedBy() / removeFromAriaDescribedBy()`, async () => {
|
removeFromAriaLabelledBy() / addToAriaDescribedBy() / removeFromAriaDescribedBy()`, async () => {
|
||||||
|
|
@ -169,40 +236,44 @@ describe('FormControlMixin', () => {
|
||||||
<div id="additionalDescription"> Same for this </div>
|
<div id="additionalDescription"> Same for this </div>
|
||||||
</div>`));
|
</div>`));
|
||||||
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
// wait until the field element is done rendering
|
// wait until the field element is done rendering
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
|
// @ts-ignore allow protected accessors in tests
|
||||||
|
const inputId = el._inputId;
|
||||||
|
|
||||||
// 1a. addToAriaLabelledBy()
|
// 1a. addToAriaLabelledBy()
|
||||||
// Check if the aria attr is filled initially
|
// Check if the aria attr is filled initially
|
||||||
expect(/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'))).to.contain(
|
expect(/** @type {string} */ (_inputNode.getAttribute('aria-labelledby'))).to.contain(
|
||||||
`label-${el._inputId}`,
|
`label-${inputId}`,
|
||||||
);
|
);
|
||||||
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
|
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
|
||||||
'#additionalLabel',
|
'#additionalLabel',
|
||||||
));
|
));
|
||||||
el.addToAriaLabelledBy(additionalLabel);
|
el.addToAriaLabelledBy(additionalLabel);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
let labelledbyAttr = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
|
let labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
|
||||||
// Now check if ids are added to the end (not overridden)
|
// Now check if ids are added to the end (not overridden)
|
||||||
expect(labelledbyAttr).to.contain(`additionalLabel`);
|
expect(labelledbyAttr).to.contain(`additionalLabel`);
|
||||||
// Should be placed in the end
|
// Should be placed in the end
|
||||||
expect(
|
expect(
|
||||||
labelledbyAttr.indexOf(`label-${el._inputId}`) <
|
labelledbyAttr.indexOf(`label-${inputId}`) < labelledbyAttr.indexOf('additionalLabel'),
|
||||||
labelledbyAttr.indexOf('additionalLabel'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1b. removeFromAriaLabelledBy()
|
// 1b. removeFromAriaLabelledBy()
|
||||||
el.removeFromAriaLabelledBy(additionalLabel);
|
el.removeFromAriaLabelledBy(additionalLabel);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
labelledbyAttr = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
|
labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
|
||||||
// Now check if ids are added to the end (not overridden)
|
// Now check if ids are added to the end (not overridden)
|
||||||
expect(labelledbyAttr).to.not.contain(`additionalLabel`);
|
expect(labelledbyAttr).to.not.contain(`additionalLabel`);
|
||||||
|
|
||||||
// 2a. addToAriaDescribedBy()
|
// 2a. addToAriaDescribedBy()
|
||||||
// Check if the aria attr is filled initially
|
// Check if the aria attr is filled initially
|
||||||
expect(/** @type {string} */ (el._inputNode.getAttribute('aria-describedby'))).to.contain(
|
expect(/** @type {string} */ (_inputNode.getAttribute('aria-describedby'))).to.contain(
|
||||||
`feedback-${el._inputId}`,
|
`feedback-${inputId}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -220,6 +291,7 @@ describe('FormControlMixin', () => {
|
||||||
<div id="externalDescriptionB">should go after input internals</div>
|
<div id="externalDescriptionB">should go after input internals</div>
|
||||||
</div>`);
|
</div>`);
|
||||||
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
// N.B. in real life we would never add the input to aria-describedby or -labelledby,
|
// N.B. in real life we would never add the input to aria-describedby or -labelledby,
|
||||||
// but this example purely demonstrates dom order is respected.
|
// but this example purely demonstrates dom order is respected.
|
||||||
|
|
@ -232,10 +304,10 @@ describe('FormControlMixin', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
|
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
|
||||||
).to.eql(['myInput', 'internalLabel']);
|
).to.eql(['myInput', 'internalLabel']);
|
||||||
expect(
|
expect(
|
||||||
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
|
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
|
||||||
).to.eql(['myInput', 'internalDescription']);
|
).to.eql(['myInput', 'internalDescription']);
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
|
|
@ -251,10 +323,10 @@ describe('FormControlMixin', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
|
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
|
||||||
).to.eql(['internalLabel', 'myInput']);
|
).to.eql(['internalLabel', 'myInput']);
|
||||||
expect(
|
expect(
|
||||||
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
|
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
|
||||||
).to.eql(['internalDescription', 'myInput']);
|
).to.eql(['internalDescription', 'myInput']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -272,6 +344,7 @@ describe('FormControlMixin', () => {
|
||||||
<div id="externalDescriptionB">should go after input internals</div>
|
<div id="externalDescriptionB">should go after input internals</div>
|
||||||
</div>`);
|
</div>`);
|
||||||
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
// 1. addToAriaLabelledBy()
|
// 1. addToAriaLabelledBy()
|
||||||
const labelA = /** @type {HTMLElement} */ (wrapper.querySelector('#externalLabelA'));
|
const labelA = /** @type {HTMLElement} */ (wrapper.querySelector('#externalLabelA'));
|
||||||
|
|
@ -282,7 +355,7 @@ describe('FormControlMixin', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
|
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
|
||||||
).to.eql(['internalLabel', 'externalLabelA', 'externalLabelB']);
|
).to.eql(['internalLabel', 'externalLabelA', 'externalLabelB']);
|
||||||
|
|
||||||
// 2. addToAriaDescribedBy()
|
// 2. addToAriaDescribedBy()
|
||||||
|
|
@ -294,7 +367,7 @@ describe('FormControlMixin', () => {
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
|
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
|
||||||
).to.eql(['internalDescription', 'externalDescriptionA', 'externalDescriptionB']);
|
).to.eql(['internalDescription', 'externalDescriptionA', 'externalDescriptionB']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -370,47 +443,6 @@ describe('FormControlMixin', () => {
|
||||||
expect(formEv.detail.formPath).to.eql([fieldEl, fieldsetEl, 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]');
|
|
||||||
/** @typedef {{ checked: boolean }} checkedInterface */
|
|
||||||
const option1El = /** @type {HTMLElement & checkedInterface} */ (formEl.querySelector(
|
|
||||||
'#option1',
|
|
||||||
));
|
|
||||||
const option2El = /** @type {HTMLElement & checkedInterface} */ (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(choiceGroupEv.detail.isTriggeredByUser).to.be.false;
|
|
||||||
|
|
||||||
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]);
|
|
||||||
expect(formEv.detail.isTriggeredByUser).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets "isTriggeredByUser" event detail when event triggered by user', async () => {
|
it('sets "isTriggeredByUser" event detail when event triggered by user', async () => {
|
||||||
const formSpy = sinon.spy();
|
const formSpy = sinon.spy();
|
||||||
const fieldsetSpy = sinon.spy();
|
const fieldsetSpy = sinon.spy();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
triggerFocusFor,
|
triggerFocusFor,
|
||||||
unsafeStatic,
|
unsafeStatic,
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import '@lion/form-core/define-field';
|
import '@lion/form-core/define-field';
|
||||||
|
|
||||||
|
|
@ -31,8 +32,10 @@ const inputSlot = unsafeHTML(inputSlotString);
|
||||||
* @param {string} newViewValue
|
* @param {string} newViewValue
|
||||||
*/
|
*/
|
||||||
function mimicUserInput(formControl, newViewValue) {
|
function mimicUserInput(formControl, newViewValue) {
|
||||||
|
const { _inputNode } = getFormControlMembers(formControl);
|
||||||
|
|
||||||
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
formControl.value = newViewValue; // eslint-disable-line no-param-reassign
|
||||||
formControl._inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
_inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -51,19 +54,24 @@ function getSlot(el, slot) {
|
||||||
describe('<lion-field>', () => {
|
describe('<lion-field>', () => {
|
||||||
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
|
||||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||||
expect(getSlot(el, 'input').id).to.equal(el._inputId);
|
// @ts-ignore allow protected accessors in tests
|
||||||
|
const inputId = el._inputId;
|
||||||
|
expect(getSlot(el, 'input').id).to.equal(inputId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`has a fieldName based on the label`, async () => {
|
it(`has a fieldName based on the label`, async () => {
|
||||||
const el1 = /** @type {LionField} */ (await fixture(
|
const el1 = /** @type {LionField} */ (await fixture(
|
||||||
html`<${tag} label="foo">${inputSlot}</${tag}>`,
|
html`<${tag} label="foo">${inputSlot}</${tag}>`,
|
||||||
));
|
));
|
||||||
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
|
const { _labelNode: _labelNode1 } = getFormControlMembers(el1);
|
||||||
|
|
||||||
|
expect(el1.fieldName).to.equal(_labelNode1.textContent);
|
||||||
|
|
||||||
const el2 = /** @type {LionField} */ (await fixture(
|
const el2 = /** @type {LionField} */ (await fixture(
|
||||||
html`<${tag}><label slot="label">bar</label>${inputSlot}</${tag}>`,
|
html`<${tag}><label slot="label">bar</label>${inputSlot}</${tag}>`,
|
||||||
));
|
));
|
||||||
expect(el2.fieldName).to.equal(el2._labelNode.textContent);
|
const { _labelNode: _labelNode2 } = getFormControlMembers(el2);
|
||||||
|
expect(el2.fieldName).to.equal(_labelNode2.textContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`has a fieldName based on the name if no label exists`, async () => {
|
it(`has a fieldName based on the name if no label exists`, async () => {
|
||||||
|
|
@ -77,23 +85,26 @@ describe('<lion-field>', () => {
|
||||||
const el = /** @type {LionField} */ (await fixture(
|
const el = /** @type {LionField} */ (await fixture(
|
||||||
html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`,
|
html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`,
|
||||||
));
|
));
|
||||||
|
// @ts-ignore [allow-proteced] in test
|
||||||
expect(el.__fieldName).to.equal(el.fieldName);
|
expect(el.__fieldName).to.equal(el.fieldName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires focus/blur event on host and native input if focused/blurred', async () => {
|
it('fires focus/blur event on host and native input if focused/blurred', async () => {
|
||||||
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
const cbFocusHost = sinon.spy();
|
const cbFocusHost = sinon.spy();
|
||||||
el.addEventListener('focus', cbFocusHost);
|
el.addEventListener('focus', cbFocusHost);
|
||||||
const cbFocusNativeInput = sinon.spy();
|
const cbFocusNativeInput = sinon.spy();
|
||||||
el._inputNode.addEventListener('focus', cbFocusNativeInput);
|
_inputNode.addEventListener('focus', cbFocusNativeInput);
|
||||||
const cbBlurHost = sinon.spy();
|
const cbBlurHost = sinon.spy();
|
||||||
el.addEventListener('blur', cbBlurHost);
|
el.addEventListener('blur', cbBlurHost);
|
||||||
const cbBlurNativeInput = sinon.spy();
|
const cbBlurNativeInput = sinon.spy();
|
||||||
el._inputNode.addEventListener('blur', cbBlurNativeInput);
|
_inputNode.addEventListener('blur', cbBlurNativeInput);
|
||||||
|
|
||||||
await triggerFocusFor(el);
|
await triggerFocusFor(el);
|
||||||
|
|
||||||
expect(document.activeElement).to.equal(el._inputNode);
|
expect(document.activeElement).to.equal(_inputNode);
|
||||||
expect(cbFocusHost.callCount).to.equal(1);
|
expect(cbFocusHost.callCount).to.equal(1);
|
||||||
expect(cbFocusNativeInput.callCount).to.equal(1);
|
expect(cbFocusNativeInput.callCount).to.equal(1);
|
||||||
expect(cbBlurHost.callCount).to.equal(0);
|
expect(cbBlurHost.callCount).to.equal(0);
|
||||||
|
|
@ -104,7 +115,7 @@ describe('<lion-field>', () => {
|
||||||
expect(cbBlurNativeInput.callCount).to.equal(1);
|
expect(cbBlurNativeInput.callCount).to.equal(1);
|
||||||
|
|
||||||
await triggerFocusFor(el);
|
await triggerFocusFor(el);
|
||||||
expect(document.activeElement).to.equal(el._inputNode);
|
expect(document.activeElement).to.equal(_inputNode);
|
||||||
expect(cbFocusHost.callCount).to.equal(2);
|
expect(cbFocusHost.callCount).to.equal(2);
|
||||||
expect(cbFocusNativeInput.callCount).to.equal(2);
|
expect(cbFocusNativeInput.callCount).to.equal(2);
|
||||||
|
|
||||||
|
|
@ -168,10 +179,11 @@ describe('<lion-field>', () => {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const nativeInput = getSlot(el, 'input');
|
const nativeInput = getSlot(el, 'input');
|
||||||
|
// @ts-ignore allow protected accessors in tests
|
||||||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${el._inputId}`);
|
const inputId = el._inputId;
|
||||||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(`help-text-${el._inputId}`);
|
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${inputId}`);
|
||||||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
|
expect(nativeInput.getAttribute('aria-describedby')).to.contain(`help-text-${inputId}`);
|
||||||
|
expect(nativeInput.getAttribute('aria-describedby')).to.contain(`feedback-${inputId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
|
it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
|
||||||
|
|
@ -186,11 +198,13 @@ describe('<lion-field>', () => {
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const nativeInput = getSlot(el, 'input');
|
const nativeInput = getSlot(el, 'input');
|
||||||
|
// @ts-ignore allow protected accessors in tests
|
||||||
|
const inputId = el._inputId;
|
||||||
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
|
||||||
`before-${el._inputId} after-${el._inputId}`,
|
`before-${inputId} after-${inputId}`,
|
||||||
);
|
);
|
||||||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
|
expect(nativeInput.getAttribute('aria-describedby')).to.contain(
|
||||||
`prefix-${el._inputId} suffix-${el._inputId}`,
|
`prefix-${inputId} suffix-${inputId}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,9 @@ describe('SyncUpdatableMixin', () => {
|
||||||
const tagString = defineCE(UpdatableImplementation);
|
const tagString = defineCE(UpdatableImplementation);
|
||||||
const tag = unsafeStatic(tagString);
|
const tag = unsafeStatic(tagString);
|
||||||
const el = /** @type {UpdatableImplementation} */ (fixtureSync(html`<${tag}></${tag}>`));
|
const el = /** @type {UpdatableImplementation} */ (fixtureSync(html`<${tag}></${tag}>`));
|
||||||
|
// @ts-ignore [allow-private] in tests
|
||||||
const ns = el.__SyncUpdatableNamespace;
|
const ns = el.__SyncUpdatableNamespace;
|
||||||
|
// @ts-ignore [allow-protected] in tests
|
||||||
const updateSyncSpy = sinon.spy(el, 'updateSync');
|
const updateSyncSpy = sinon.spy(el, 'updateSync');
|
||||||
|
|
||||||
expect(ns.connected).to.be.true;
|
expect(ns.connected).to.be.true;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
|
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
|
||||||
import { LionField } from '@lion/form-core';
|
import { LionField } from '@lion/form-core';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
import { Required } from '../../src/validate/validators/Required.js';
|
import { Required } from '../../src/validate/validators/Required.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,6 +18,7 @@ class RequiredElement extends LionField {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return inputNodeTag || super._inputNode;
|
return inputNodeTag || super._inputNode;
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +39,8 @@ describe('Required validation', async () => {
|
||||||
inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName));
|
inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName));
|
||||||
|
|
||||||
validator.onFormControlConnect(el);
|
validator.onFormControlConnect(el);
|
||||||
expect(el._inputNode).to.have.attribute('aria-required', 'true');
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
expect(_inputNode).to.have.attribute('aria-required', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
// When incompatible tags are used, aria-required will not be added
|
// When incompatible tags are used, aria-required will not be added
|
||||||
|
|
@ -46,7 +49,8 @@ describe('Required validation', async () => {
|
||||||
inputNodeTag = /** @type {HTMLDivElementWithValue} */ (document.createElement('div'));
|
inputNodeTag = /** @type {HTMLDivElementWithValue} */ (document.createElement('div'));
|
||||||
|
|
||||||
validator.onFormControlConnect(el);
|
validator.onFormControlConnect(el);
|
||||||
expect(el._inputNode).to.not.have.attribute('aria-required');
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
expect(_inputNode).to.not.have.attribute('aria-required');
|
||||||
});
|
});
|
||||||
it('get aria-required attribute if element is part of the right roles', async () => {
|
it('get aria-required attribute if element is part of the right roles', async () => {
|
||||||
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture(
|
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture(
|
||||||
|
|
@ -59,7 +63,8 @@ describe('Required validation', async () => {
|
||||||
inputNodeTag.setAttribute('role', role);
|
inputNodeTag.setAttribute('role', role);
|
||||||
|
|
||||||
validator.onFormControlConnect(el);
|
validator.onFormControlConnect(el);
|
||||||
expect(el._inputNode).to.have.attribute('aria-required', 'true');
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
expect(_inputNode).to.have.attribute('aria-required', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
// When incompatible roles are used, aria-required will not be added
|
// When incompatible roles are used, aria-required will not be added
|
||||||
|
|
@ -69,6 +74,7 @@ describe('Required validation', async () => {
|
||||||
inputNodeTag.setAttribute('role', 'group');
|
inputNodeTag.setAttribute('role', 'group');
|
||||||
|
|
||||||
validator.onFormControlConnect(el);
|
validator.onFormControlConnect(el);
|
||||||
expect(el._inputNode).to.not.have.attribute('aria-required');
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
expect(_inputNode).to.not.have.attribute('aria-required');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import '@lion/form-core/define-validation-feedback';
|
import '@lion/form-core/define-validation-feedback';
|
||||||
import { AlwaysInvalid, AlwaysValid } from '../../test-helpers.js';
|
import { AlwaysInvalid, AlwaysValid } from '../../test-helpers/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../src/validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback
|
* @typedef {import('../../src/validate/LionValidationFeedback').LionValidationFeedback} LionValidationFeedback
|
||||||
|
|
|
||||||
13
packages/form-core/types/FocusMixinTypes.d.ts
vendored
13
packages/form-core/types/FocusMixinTypes.d.ts
vendored
|
|
@ -4,16 +4,13 @@ import { FormControlHost } from './FormControlMixinTypes';
|
||||||
|
|
||||||
export declare class FocusHost {
|
export declare class FocusHost {
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
|
|
||||||
connectedCallback(): void;
|
|
||||||
disconnectedCallback(): void;
|
|
||||||
|
|
||||||
focus(): void;
|
focus(): void;
|
||||||
blur(): void;
|
blur(): void;
|
||||||
__onFocus(): void;
|
|
||||||
__onBlur(): void;
|
private __onFocus(): void;
|
||||||
__registerEventsForFocusMixin(): void;
|
private __onBlur(): void;
|
||||||
__teardownEventsForFocusMixin(): void;
|
private __registerEventsForFocusMixin(): void;
|
||||||
|
private __teardownEventsForFocusMixin(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FocusImplementation<T extends Constructor<LitElement>>(
|
export declare function FocusImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
154
packages/form-core/types/FormControlMixinTypes.d.ts
vendored
154
packages/form-core/types/FormControlMixinTypes.d.ts
vendored
|
|
@ -1,10 +1,11 @@
|
||||||
import { LitElement, nothing, TemplateResult, CSSResultArray } from '@lion/core';
|
import { LitElement, nothing, TemplateResult, CSSResultArray } from '@lion/core';
|
||||||
import { SlotsMap, SlotHost } from '@lion/core/types/SlotMixinTypes';
|
import { SlotHost } from '@lion/core/types/SlotMixinTypes';
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||||
|
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
|
||||||
|
|
||||||
import { LionValidationFeedback } from '../src/validate/LionValidationFeedback';
|
import { LionValidationFeedback } from '../src/validate/LionValidationFeedback';
|
||||||
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
|
import { Unparseable } from '../src/validate/Unparseable.js';
|
||||||
|
|
||||||
export type ModelValueEventDetails = {
|
export type ModelValueEventDetails = {
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,12 +66,12 @@ export declare class FormControlHost {
|
||||||
* controls until they are enabled.
|
* controls until they are enabled.
|
||||||
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
* (From: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly)
|
||||||
*/
|
*/
|
||||||
public readOnly: boolean;
|
readOnly: boolean;
|
||||||
/**
|
/**
|
||||||
* The name the element will be registered with to the .formElements collection
|
* The name the element will be registered with to the .formElements collection
|
||||||
* of the parent.
|
* of the parent.
|
||||||
*/
|
*/
|
||||||
public name: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
* The model value is the result of the parser function(when available).
|
* The model value is the result of the parser function(when available).
|
||||||
* It should be considered as the internal value used for validation and reasoning/logic.
|
* It should be considered as the internal value used for validation and reasoning/logic.
|
||||||
|
|
@ -83,32 +84,59 @@ export declare class FormControlHost {
|
||||||
* - For a number input: a formatted String '1.234,56' will be converted to a Number:
|
* - For a number input: a formatted String '1.234,56' will be converted to a Number:
|
||||||
* 1234.56
|
* 1234.56
|
||||||
*/
|
*/
|
||||||
public modelValue: unknown;
|
get modelValue(): any | Unparseable;
|
||||||
|
set modelValue(value: any | Unparseable);
|
||||||
/**
|
/**
|
||||||
* The label text for the input node.
|
* The label text for the input node.
|
||||||
* When no light dom defined via [slot=label], this value will be used
|
* When no light dom defined via [slot=label], this value will be used
|
||||||
*/
|
*/
|
||||||
public get label(): string;
|
get label(): string;
|
||||||
public set label(arg: string);
|
set label(arg: string);
|
||||||
__label: string | undefined;
|
|
||||||
/**
|
/**
|
||||||
* The helpt text for the input node.
|
* The helpt text for the input node.
|
||||||
* When no light dom defined via [slot=help-text], this value will be used
|
* When no light dom defined via [slot=help-text], this value will be used
|
||||||
*/
|
*/
|
||||||
public get helpText(): string;
|
get helpText(): string;
|
||||||
public set helpText(arg: string);
|
set helpText(arg: string);
|
||||||
__helpText: string | undefined;
|
|
||||||
public set fieldName(arg: string);
|
set fieldName(arg: string);
|
||||||
public get fieldName(): string;
|
get fieldName(): string;
|
||||||
__fieldName: string | undefined;
|
|
||||||
public get slots(): SlotsMap;
|
addToAriaLabelledBy(
|
||||||
get _inputNode(): HTMLElementWithValue;
|
element: HTMLElement,
|
||||||
get _labelNode(): HTMLElement;
|
customConfig?: {
|
||||||
get _helpTextNode(): HTMLElement;
|
idPrefix?: string | undefined;
|
||||||
get _feedbackNode(): LionValidationFeedback | undefined;
|
reorder?: boolean | undefined;
|
||||||
_inputId: string;
|
},
|
||||||
_ariaLabelledNodes: HTMLElement[];
|
): void;
|
||||||
_ariaDescribedNodes: HTMLElement[];
|
addToAriaDescribedBy(
|
||||||
|
element: HTMLElement,
|
||||||
|
customConfig?: {
|
||||||
|
idPrefix?: string | undefined;
|
||||||
|
reorder?: boolean | undefined;
|
||||||
|
},
|
||||||
|
): void;
|
||||||
|
removeFromAriaLabelledBy(
|
||||||
|
element: HTMLElement,
|
||||||
|
customConfig?: {
|
||||||
|
reorder?: boolean | undefined;
|
||||||
|
},
|
||||||
|
): void;
|
||||||
|
removeFromAriaDescribedBy(
|
||||||
|
element: HTMLElement,
|
||||||
|
customConfig?: {
|
||||||
|
reorder?: boolean | undefined;
|
||||||
|
},
|
||||||
|
): void;
|
||||||
|
updated(changedProperties: import('@lion/core').PropertyValues): void;
|
||||||
|
|
||||||
|
protected get _inputNode(): HTMLElementWithValue | HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
protected get _labelNode(): HTMLElement;
|
||||||
|
protected get _helpTextNode(): HTMLElement;
|
||||||
|
protected get _feedbackNode(): LionValidationFeedback;
|
||||||
|
protected _inputId: string;
|
||||||
|
protected _ariaLabelledNodes: HTMLElement[];
|
||||||
|
protected _ariaDescribedNodes: HTMLElement[];
|
||||||
/**
|
/**
|
||||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||||
*/
|
*/
|
||||||
|
|
@ -124,65 +152,41 @@ export declare class FormControlHost {
|
||||||
* to true to hide private internals in the formPath.
|
* to true to hide private internals in the formPath.
|
||||||
*/
|
*/
|
||||||
protected _isRepropagationEndpoint: boolean;
|
protected _isRepropagationEndpoint: boolean;
|
||||||
|
protected _parentFormGroup: FormControlHost | undefined;
|
||||||
connectedCallback(): void;
|
|
||||||
updated(changedProperties: import('@lion/core').PropertyValues): void;
|
|
||||||
|
|
||||||
render(): TemplateResult;
|
|
||||||
protected _groupOneTemplate(): TemplateResult;
|
protected _groupOneTemplate(): TemplateResult;
|
||||||
protected _groupTwoTemplate(): TemplateResult;
|
protected _groupTwoTemplate(): TemplateResult;
|
||||||
_labelTemplate(): TemplateResult;
|
protected _labelTemplate(): TemplateResult;
|
||||||
_helpTextTemplate(): TemplateResult;
|
protected _helpTextTemplate(): TemplateResult;
|
||||||
protected _inputGroupTemplate(): TemplateResult;
|
protected _inputGroupTemplate(): TemplateResult;
|
||||||
_inputGroupBeforeTemplate(): TemplateResult;
|
protected _inputGroupBeforeTemplate(): TemplateResult;
|
||||||
_inputGroupPrefixTemplate(): TemplateResult | typeof nothing;
|
protected _inputGroupPrefixTemplate(): TemplateResult | typeof nothing;
|
||||||
protected _inputGroupInputTemplate(): TemplateResult;
|
protected _inputGroupInputTemplate(): TemplateResult;
|
||||||
_inputGroupSuffixTemplate(): TemplateResult | typeof nothing;
|
protected _inputGroupSuffixTemplate(): TemplateResult | typeof nothing;
|
||||||
_inputGroupAfterTemplate(): TemplateResult;
|
protected _inputGroupAfterTemplate(): TemplateResult;
|
||||||
_feedbackTemplate(): TemplateResult;
|
protected _feedbackTemplate(): TemplateResult;
|
||||||
|
|
||||||
protected _triggerInitialModelValueChangedEvent(): void;
|
protected _triggerInitialModelValueChangedEvent(): void;
|
||||||
_enhanceLightDomClasses(): void;
|
protected _enhanceLightDomClasses(): void;
|
||||||
_enhanceLightDomA11y(): void;
|
protected _enhanceLightDomA11y(): void;
|
||||||
_enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void;
|
protected _enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void;
|
||||||
__reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void;
|
protected _isEmpty(modelValue?: any): boolean;
|
||||||
protected _isEmpty(modelValue?: unknown): boolean;
|
protected _getAriaDescriptionElements(): HTMLElement[];
|
||||||
_getAriaDescriptionElements(): HTMLElement[];
|
protected _dispatchInitialModelValueChangedEvent(): void;
|
||||||
public addToAriaLabelledBy(
|
|
||||||
element: HTMLElement,
|
|
||||||
customConfig?: {
|
|
||||||
idPrefix?: string | undefined;
|
|
||||||
reorder?: boolean | undefined;
|
|
||||||
},
|
|
||||||
): void;
|
|
||||||
__reorderAriaLabelledNodes: boolean | undefined;
|
|
||||||
public addToAriaDescribedBy(
|
|
||||||
element: HTMLElement,
|
|
||||||
customConfig?: {
|
|
||||||
idPrefix?: string | undefined;
|
|
||||||
reorder?: boolean | undefined;
|
|
||||||
},
|
|
||||||
): void;
|
|
||||||
public removeFromAriaLabelledBy(
|
|
||||||
element: HTMLElement,
|
|
||||||
customConfig?: {
|
|
||||||
reorder?: boolean | undefined;
|
|
||||||
},
|
|
||||||
): void;
|
|
||||||
public removeFromAriaDescribedBy(
|
|
||||||
element: HTMLElement,
|
|
||||||
customConfig?: {
|
|
||||||
reorder?: boolean | undefined;
|
|
||||||
},
|
|
||||||
): void;
|
|
||||||
__reorderAriaDescribedNodes: boolean | undefined;
|
|
||||||
__getDirectSlotChild(slotName: string): HTMLElement;
|
|
||||||
__dispatchInitialModelValueChangedEvent(): void;
|
|
||||||
__repropagateChildrenInitialized: boolean | undefined;
|
|
||||||
protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
|
protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
|
||||||
__repropagateChildrenValues(ev: CustomEvent): void;
|
protected _repropagationCondition(target: FormControlHost): boolean;
|
||||||
_parentFormGroup: FormControlHost;
|
|
||||||
_repropagationCondition(target: FormControlHost): boolean;
|
private __helpText: string | undefined;
|
||||||
|
private __label: string;
|
||||||
|
private __fieldName: string | undefined;
|
||||||
|
private __reorderAriaLabelledNodes: boolean | undefined;
|
||||||
|
private __reflectAriaAttr(
|
||||||
|
attrName: string,
|
||||||
|
nodes: HTMLElement[],
|
||||||
|
reorder: boolean | undefined,
|
||||||
|
): void;
|
||||||
|
private __reorderAriaDescribedNodes: boolean | undefined;
|
||||||
|
private __getDirectSlotChild(slotName: string): HTMLElement | undefined;
|
||||||
|
private __repropagateChildrenInitialized: boolean | undefined;
|
||||||
|
private __repropagateChildrenValues(ev: CustomEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormControlImplementation<T extends Constructor<LitElement>>(
|
export declare function FormControlImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
34
packages/form-core/types/FormatMixinTypes.d.ts
vendored
34
packages/form-core/types/FormatMixinTypes.d.ts
vendored
|
|
@ -5,36 +5,32 @@ import { ValidateHost } from './validate/ValidateMixinTypes';
|
||||||
import { FormControlHost } from './FormControlMixinTypes';
|
import { FormControlHost } from './FormControlMixinTypes';
|
||||||
|
|
||||||
export declare class FormatHost {
|
export declare class FormatHost {
|
||||||
|
parser(v: string, opts: FormatNumberOptions): unknown;
|
||||||
|
formatter(v: unknown, opts?: FormatNumberOptions): string;
|
||||||
|
serializer(v: unknown): string;
|
||||||
|
deserializer(v: string): unknown;
|
||||||
|
preprocessor(v: string): string;
|
||||||
formattedValue: string;
|
formattedValue: string;
|
||||||
serializedValue: string;
|
serializedValue: string;
|
||||||
formatOn: string;
|
formatOn: string;
|
||||||
formatOptions: FormatNumberOptions;
|
formatOptions: FormatNumberOptions;
|
||||||
__preventRecursiveTrigger: boolean;
|
|
||||||
__isHandlingUserInput: boolean;
|
|
||||||
|
|
||||||
parser(v: string, opts: FormatNumberOptions): unknown;
|
|
||||||
formatter(v: unknown, opts: FormatNumberOptions): string;
|
|
||||||
serializer(v: unknown): string;
|
|
||||||
deserializer(v: string): unknown;
|
|
||||||
preprocessor(v: string): string;
|
|
||||||
|
|
||||||
get value(): string;
|
get value(): string;
|
||||||
set value(value: string);
|
set value(value: string);
|
||||||
|
|
||||||
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
protected _isHandlingUserInput: boolean;
|
||||||
private __callParser(value: string | undefined): object;
|
protected _calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
||||||
__callFormatter(): string;
|
|
||||||
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
|
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
|
||||||
_dispatchModelValueChangedEvent(): void;
|
protected _dispatchModelValueChangedEvent(): void;
|
||||||
protected _syncValueUpwards(): void;
|
protected _syncValueUpwards(): void;
|
||||||
_reflectBackFormattedValueToUser(): void;
|
protected _reflectBackFormattedValueToUser(): void;
|
||||||
_reflectBackFormattedValueDebounced(): void;
|
protected _reflectBackFormattedValueDebounced(): void;
|
||||||
_reflectBackOn(): boolean;
|
protected _reflectBackOn(): boolean;
|
||||||
protected _proxyInputEvent(): void;
|
protected _proxyInputEvent(): void;
|
||||||
_onUserInputChanged(): void;
|
protected _onUserInputChanged(): void;
|
||||||
|
|
||||||
connectedCallback(): void;
|
private __preventRecursiveTrigger: boolean;
|
||||||
disconnectedCallback(): void;
|
private __callParser(value: string | undefined): object;
|
||||||
|
private __callFormatter(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormatImplementation<T extends Constructor<LitElement>>(
|
export declare function FormatImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -19,25 +19,18 @@ export declare class InteractionStateHost {
|
||||||
touched: boolean;
|
touched: boolean;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
_leaveEvent: string;
|
initInteractionState(): void;
|
||||||
_valueChangedEvent: string;
|
resetInteractionState(): void;
|
||||||
|
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
disconnectedCallback(): void;
|
disconnectedCallback(): void;
|
||||||
|
|
||||||
initInteractionState(): void;
|
protected _leaveEvent: string;
|
||||||
resetInteractionState(): void;
|
protected _valueChangedEvent: string;
|
||||||
_iStateOnLeave(): void;
|
protected _iStateOnLeave(): void;
|
||||||
_iStateOnValueChange(): void;
|
protected _iStateOnValueChange(): void;
|
||||||
_onTouchedChanged(): void;
|
protected _onTouchedChanged(): void;
|
||||||
_onDirtyChanged(): void;
|
protected _onDirtyChanged(): void;
|
||||||
|
|
||||||
showFeedbackConditionFor(
|
|
||||||
type: string,
|
|
||||||
meta: InteractionStates,
|
|
||||||
currentCondition: Function,
|
|
||||||
): boolean;
|
|
||||||
_feedbackConditionMeta: InteractionStates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(
|
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,32 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { LionField } from '@lion/form-core/src/LionField';
|
// import { LionField } from '@lion/form-core/src/LionField';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { FocusHost } from '@lion/form-core/types/FocusMixinTypes';
|
||||||
|
import { FormControlHost } from '@lion/form-core/types/FormControlMixinTypes';
|
||||||
|
|
||||||
export declare class NativeTextField extends LionField {
|
// export declare class NativeTextField extends LionField {
|
||||||
get _inputNode(): HTMLTextAreaElement | HTMLInputElement;
|
// protected get _inputNode(): HTMLTextAreaElement | HTMLInputElement;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export declare class NativeTextFieldHost {
|
export declare class NativeTextFieldHost {
|
||||||
|
// protected get _inputNode(): HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
|
||||||
get selectionStart(): number;
|
get selectionStart(): number;
|
||||||
set selectionStart(value: number);
|
set selectionStart(value: number);
|
||||||
get selectionEnd(): number;
|
get selectionEnd(): number;
|
||||||
set selectionEnd(value: number);
|
set selectionEnd(value: number);
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function NativeTextFieldImplementation<T extends Constructor<NativeTextField>>(
|
export declare function NativeTextFieldImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T &
|
): T &
|
||||||
Constructor<NativeTextFieldHost> &
|
Constructor<NativeTextFieldHost> &
|
||||||
Pick<typeof NativeTextFieldHost, keyof typeof NativeTextFieldHost> &
|
Pick<typeof NativeTextFieldHost, keyof typeof NativeTextFieldHost> &
|
||||||
Pick<typeof NativeTextField, keyof typeof NativeTextField>;
|
Constructor<FocusHost> &
|
||||||
|
Pick<typeof FocusHost, keyof typeof FocusHost> &
|
||||||
|
Constructor<FormControlHost> &
|
||||||
|
Pick<typeof FormControlHost, keyof typeof FormControlHost>;
|
||||||
|
// &
|
||||||
|
// Pick<typeof NativeTextField, keyof typeof NativeTextField>;
|
||||||
|
|
||||||
export type NativeTextFieldMixin = typeof NativeTextFieldImplementation;
|
export type NativeTextFieldMixin = typeof NativeTextFieldImplementation;
|
||||||
|
|
|
||||||
|
|
@ -6,50 +6,27 @@ import { InteractionStateHost } from '../InteractionStateMixinTypes';
|
||||||
|
|
||||||
export declare class ChoiceGroupHost {
|
export declare class ChoiceGroupHost {
|
||||||
multipleChoice: boolean;
|
multipleChoice: boolean;
|
||||||
|
|
||||||
connectedCallback(): void;
|
|
||||||
disconnectedCallback(): void;
|
|
||||||
|
|
||||||
get modelValue(): any;
|
get modelValue(): any;
|
||||||
|
|
||||||
set modelValue(value: any);
|
set modelValue(value: any);
|
||||||
|
|
||||||
get serializedValue(): string;
|
get serializedValue(): string;
|
||||||
|
|
||||||
set serializedValue(value: string);
|
set serializedValue(value: string);
|
||||||
|
|
||||||
get formattedValue(): string;
|
get formattedValue(): string;
|
||||||
|
|
||||||
set formattedValue(value: string);
|
set formattedValue(value: string);
|
||||||
|
|
||||||
connectedCallback(): void;
|
|
||||||
|
|
||||||
disconnectedCallback(): void;
|
|
||||||
|
|
||||||
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
|
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
|
||||||
|
|
||||||
clear(): void;
|
clear(): void;
|
||||||
|
|
||||||
|
protected _oldModelValue: any;
|
||||||
protected _triggerInitialModelValueChangedEvent(): void;
|
protected _triggerInitialModelValueChangedEvent(): void;
|
||||||
|
protected _getFromAllFormElements(property: string, filterCondition: Function): void;
|
||||||
_getFromAllFormElements(property: string, filterCondition: Function): void;
|
protected _throwWhenInvalidChildModelValue(child: FormControlHost): void;
|
||||||
|
|
||||||
_throwWhenInvalidChildModelValue(child: FormControlHost): void;
|
|
||||||
|
|
||||||
protected _isEmpty(): void;
|
protected _isEmpty(): void;
|
||||||
|
protected _checkSingleChoiceElements(ev: Event): void;
|
||||||
_checkSingleChoiceElements(ev: Event): void;
|
|
||||||
|
|
||||||
protected _getCheckedElements(): void;
|
protected _getCheckedElements(): void;
|
||||||
|
protected _setCheckedElements(value: any, check: boolean): void;
|
||||||
_setCheckedElements(value: any, check: boolean): void;
|
|
||||||
|
|
||||||
__setChoiceGroupTouched(): void;
|
|
||||||
|
|
||||||
__delegateNameAttribute(child: FormControlHost): void;
|
|
||||||
|
|
||||||
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
|
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
|
||||||
__oldModelValue: any;
|
|
||||||
|
private __setChoiceGroupTouched(): void;
|
||||||
|
private __delegateNameAttribute(child: FormControlHost): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function ChoiceGroupImplementation<T extends Constructor<LitElement>>(
|
export declare function ChoiceGroupImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -15,63 +15,37 @@ export interface ChoiceInputSerializedValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class ChoiceInputHost {
|
export declare class ChoiceInputHost {
|
||||||
modelValue: ChoiceInputModelValue;
|
type: string;
|
||||||
serializedValue: ChoiceInputSerializedValue;
|
serializedValue: ChoiceInputSerializedValue;
|
||||||
|
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
get modelValue(): ChoiceInputModelValue;
|
||||||
|
set modelValue(value: ChoiceInputModelValue);
|
||||||
get choiceValue(): any;
|
get choiceValue(): any;
|
||||||
|
|
||||||
set choiceValue(value: any);
|
set choiceValue(value: any);
|
||||||
|
|
||||||
protected requestUpdateInternal(name: string, oldValue: any): void;
|
|
||||||
|
|
||||||
firstUpdated(changedProperties: Map<string, any>): void;
|
|
||||||
|
|
||||||
updated(changedProperties: Map<string, any>): void;
|
|
||||||
|
|
||||||
static get styles(): CSSResultArray;
|
static get styles(): CSSResultArray;
|
||||||
|
parser(): any;
|
||||||
|
formatter(modelValue: ChoiceInputModelValue): string;
|
||||||
|
|
||||||
render(): TemplateResult;
|
protected _isHandlingUserInput: boolean;
|
||||||
|
protected get _inputNode(): HTMLElement;
|
||||||
_choiceGraphicTemplate(): TemplateResult;
|
|
||||||
protected _afterTemplate(): TemplateResult;
|
|
||||||
|
|
||||||
connectedCallback(): void;
|
|
||||||
disconnectedCallback(): void;
|
|
||||||
|
|
||||||
_preventDuplicateLabelClick(ev: Event): void;
|
|
||||||
|
|
||||||
_syncNameToParentFormGroup(): void;
|
|
||||||
|
|
||||||
_toggleChecked(ev: Event): void;
|
|
||||||
|
|
||||||
__syncModelCheckedToChecked(checked: boolean): void;
|
|
||||||
|
|
||||||
__syncCheckedToModel(checked: boolean): void;
|
|
||||||
|
|
||||||
__syncCheckedToInputElement(): void;
|
|
||||||
|
|
||||||
__isHandlingUserInput: boolean;
|
|
||||||
|
|
||||||
protected _proxyInputEvent(): void;
|
protected _proxyInputEvent(): void;
|
||||||
|
protected requestUpdateInternal(name: string, oldValue: any): void;
|
||||||
|
protected _choiceGraphicTemplate(): TemplateResult;
|
||||||
|
protected _afterTemplate(): TemplateResult;
|
||||||
|
protected _preventDuplicateLabelClick(ev: Event): void;
|
||||||
|
protected _syncNameToParentFormGroup(): void;
|
||||||
|
protected _toggleChecked(ev: Event): void;
|
||||||
protected _onModelValueChanged(
|
protected _onModelValueChanged(
|
||||||
newV: { modelValue: ChoiceInputModelValue },
|
newV: { modelValue: ChoiceInputModelValue },
|
||||||
oldV: { modelValue: ChoiceInputModelValue },
|
oldV: { modelValue: ChoiceInputModelValue },
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
parser(): any;
|
|
||||||
|
|
||||||
formatter(modelValue: ChoiceInputModelValue): string;
|
|
||||||
|
|
||||||
protected _isEmpty(): void;
|
protected _isEmpty(): void;
|
||||||
|
|
||||||
protected _syncValueUpwards(): void;
|
protected _syncValueUpwards(): void;
|
||||||
|
|
||||||
type: string;
|
private __syncModelCheckedToChecked(checked: boolean): void;
|
||||||
|
private __syncCheckedToModel(checked: boolean): void;
|
||||||
_inputNode: HTMLElement;
|
private __syncCheckedToInputElement(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>(
|
export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,26 @@ import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes';
|
||||||
import { ValidateHost } from '../validate/ValidateMixinTypes';
|
import { ValidateHost } from '../validate/ValidateMixinTypes';
|
||||||
|
|
||||||
export declare class FormGroupHost {
|
export declare class FormGroupHost {
|
||||||
protected static _addDescriptionElementIdsToField(): void;
|
|
||||||
_inputNode: HTMLElement;
|
|
||||||
submitGroup(): void;
|
|
||||||
resetGroup(): void;
|
|
||||||
prefilled: boolean;
|
prefilled: boolean;
|
||||||
touched: boolean;
|
touched: boolean;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
submitted: boolean;
|
submitted: boolean;
|
||||||
serializedValue: { [key: string]: any };
|
serializedValue: { [key: string]: any };
|
||||||
modelValue: { [x: string]: any };
|
|
||||||
formattedValue: string;
|
formattedValue: string;
|
||||||
children: Array<HTMLElement & FormControlHost>;
|
children: Array<HTMLElement & FormControlHost>;
|
||||||
_initialModelValue: { [x: string]: any };
|
get modelValue(): { [x: string]: any };
|
||||||
_setValueForAllFormElements(property: string, value: any): void;
|
set modelValue(value: { [x: string]: any });
|
||||||
resetInteractionState(): void;
|
resetInteractionState(): void;
|
||||||
clearGroup(): void;
|
clearGroup(): void;
|
||||||
__descriptionElementsInParentChain: Set<HTMLElement>;
|
submitGroup(): void;
|
||||||
|
resetGroup(): void;
|
||||||
|
|
||||||
|
protected _initialModelValue: { [x: string]: any };
|
||||||
|
protected get _inputNode(): HTMLElement;
|
||||||
|
protected static _addDescriptionElementIdsToField(): void;
|
||||||
|
|
||||||
|
protected _setValueForAllFormElements(property: string, value: any): void;
|
||||||
|
private __descriptionElementsInParentChain: Set<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormGroupImplementation<T extends Constructor<LitElement>>(
|
export declare function FormGroupImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { FormRegistrarHost } from './FormRegistrarMixinTypes';
|
import { FormRegistrarHost } from './FormRegistrarMixinTypes';
|
||||||
|
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class FormRegisteringHost {
|
export declare class FormRegisteringHost {
|
||||||
connectedCallback(): void;
|
name: string;
|
||||||
disconnectedCallback(): void;
|
protected _parentFormGroup: FormRegistrarHost | undefined;
|
||||||
_parentFormGroup?: FormRegistrarHost;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormRegisteringImplementation<T extends Constructor<LitElement>>(
|
export declare function FormRegisteringImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ export declare class ElementWithParentFormGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class FormRegistrarHost {
|
export declare class FormRegistrarHost {
|
||||||
protected _isFormOrFieldset: boolean;
|
|
||||||
formElements: FormControlsCollection & { [x: string]: any };
|
formElements: FormControlsCollection & { [x: string]: any };
|
||||||
addFormElement(
|
addFormElement(
|
||||||
child:
|
child:
|
||||||
|
|
@ -19,10 +18,11 @@ export declare class FormRegistrarHost {
|
||||||
indexToInsertAt?: number,
|
indexToInsertAt?: number,
|
||||||
): void;
|
): void;
|
||||||
removeFormElement(child: FormRegisteringHost): void;
|
removeFormElement(child: FormRegisteringHost): void;
|
||||||
_onRequestToAddFormElement(e: CustomEvent): void;
|
|
||||||
isRegisteredFormElement(el: FormControlHost): boolean;
|
isRegisteredFormElement(el: FormControlHost): boolean;
|
||||||
registrationComplete: Promise<boolean>;
|
registrationComplete: Promise<boolean>;
|
||||||
initComplete: Promise<boolean>;
|
initComplete: Promise<boolean>;
|
||||||
|
protected _isFormOrFieldset: boolean;
|
||||||
|
protected _onRequestToAddFormElement(e: CustomEvent): void;
|
||||||
protected _completeRegistration(): void;
|
protected _completeRegistration(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { LitElement } from '@lion/core';
|
||||||
|
|
||||||
export declare class FormRegistrarPortalHost {
|
export declare class FormRegistrarPortalHost {
|
||||||
registrationTarget: HTMLElement;
|
registrationTarget: HTMLElement;
|
||||||
__redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
private __redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function FormRegistrarPortalImplementation<T extends Constructor<LitElement>>(
|
export declare function FormRegistrarPortalImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,10 @@ export declare interface SyncUpdatableNamespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class SyncUpdatableHost {
|
export declare class SyncUpdatableHost {
|
||||||
static __syncUpdatableHasChanged(name: string, newValue: any, oldValue: any): boolean;
|
protected updateSync(name: string, oldValue: any): void;
|
||||||
updateSync(name: string, oldValue: any): void;
|
private __syncUpdatableInitialize(): void;
|
||||||
__syncUpdatableInitialize(): void;
|
private __SyncUpdatableNamespace: SyncUpdatableNamespace;
|
||||||
__SyncUpdatableNamespace: SyncUpdatableNamespace;
|
private static __syncUpdatableHasChanged(name: string, newValue: any, oldValue: any): boolean;
|
||||||
|
|
||||||
firstUpdated(changedProperties: PropertyValues): void;
|
|
||||||
disconnectedCallback(): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SyncUpdatableHostType = typeof SyncUpdatableHost;
|
export type SyncUpdatableHostType = typeof SyncUpdatableHost;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { LitElement } from '@lion/core';
|
import { LitElement } from '@lion/core';
|
||||||
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
import { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
|
||||||
import { SlotsMap, SlotHost } from '@lion/core/types/SlotMixinTypes';
|
import { SlotHost } from '@lion/core/types/SlotMixinTypes';
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
import { ScopedElementsHost } from '@open-wc/scoped-elements/src/types';
|
import { ScopedElementsHost } from '@open-wc/scoped-elements/src/types';
|
||||||
import { FormControlHost } from '../FormControlMixinTypes';
|
import { FormControlHost } from '../FormControlMixinTypes';
|
||||||
|
|
@ -26,52 +26,47 @@ export declare class ValidateHost {
|
||||||
validationStates: { [key: string]: { [key: string]: Object } };
|
validationStates: { [key: string]: { [key: string]: Object } };
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
defaultValidators: Validator[];
|
defaultValidators: Validator[];
|
||||||
_visibleMessagesAmount: number;
|
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
|
|
||||||
static validationTypes: string[];
|
|
||||||
slots: SlotsMap;
|
|
||||||
_feedbackNode: LionValidationFeedback;
|
|
||||||
_allValidators: Validator[];
|
|
||||||
|
|
||||||
__syncValidationResult: Validator[];
|
|
||||||
__asyncValidationResult: Validator[];
|
|
||||||
__validationResult: Validator[];
|
|
||||||
__prevValidationResult: Validator[];
|
|
||||||
__prevShownValidationResult: Validator[];
|
|
||||||
|
|
||||||
connectedCallback(): void;
|
|
||||||
disconnectedCallback(): void;
|
|
||||||
firstUpdated(changedProperties: import('@lion/core').PropertyValues): void;
|
|
||||||
updateSync(name: string, oldValue: unknown): void;
|
|
||||||
updated(changedProperties: import('@lion/core').PropertyValues): void;
|
|
||||||
|
|
||||||
validate(opts?: { clearCurrentResult?: boolean }): void;
|
|
||||||
__storePrevResult(): void;
|
|
||||||
__executeValidators(): void;
|
|
||||||
validateComplete: Promise<void>;
|
validateComplete: Promise<void>;
|
||||||
feedbackComplete: Promise<void>;
|
feedbackComplete: Promise<void>;
|
||||||
__validateCompleteResolve(): void;
|
|
||||||
__executeSyncValidators(
|
static validationTypes: string[];
|
||||||
|
|
||||||
|
validate(opts?: { clearCurrentResult?: boolean }): void;
|
||||||
|
|
||||||
|
protected _visibleMessagesAmount: number;
|
||||||
|
protected _allValidators: Validator[];
|
||||||
|
protected get _feedbackNode(): LionValidationFeedback;
|
||||||
|
|
||||||
|
protected _updateFeedbackComponent(): void;
|
||||||
|
protected _showFeedbackConditionFor(type: string, meta: object): boolean;
|
||||||
|
protected _hasFeedbackVisibleFor(type: string): boolean;
|
||||||
|
protected _updateShouldShowFeedbackFor(): void;
|
||||||
|
protected _prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[];
|
||||||
|
protected updateSync(name: string, oldValue: unknown): void;
|
||||||
|
|
||||||
|
private __syncValidationResult: Validator[];
|
||||||
|
private __asyncValidationResult: Validator[];
|
||||||
|
private __validationResult: Validator[];
|
||||||
|
private __prevValidationResult: Validator[];
|
||||||
|
private __prevShownValidationResult: Validator[];
|
||||||
|
|
||||||
|
private __storePrevResult(): void;
|
||||||
|
private __executeValidators(): void;
|
||||||
|
private __validateCompleteResolve(): void;
|
||||||
|
private __executeSyncValidators(
|
||||||
syncValidators: Validator[],
|
syncValidators: Validator[],
|
||||||
value: unknown,
|
value: unknown,
|
||||||
opts: { hasAsync: boolean },
|
opts: { hasAsync: boolean },
|
||||||
): void;
|
): void;
|
||||||
__executeAsyncValidators(asyncValidators: Validator[], value: unknown): void;
|
private __executeAsyncValidators(asyncValidators: Validator[], value: unknown): void;
|
||||||
__executeResultValidators(regularValidationResult: Validator[]): Validator[];
|
private __executeResultValidators(regularValidationResult: Validator[]): Validator[];
|
||||||
__finishValidation(options: { source: 'sync' | 'async'; hasAsync?: boolean }): void;
|
private __finishValidation(options: { source: 'sync' | 'async'; hasAsync?: boolean }): void;
|
||||||
__clearValidationResults(): void;
|
private __clearValidationResults(): void;
|
||||||
__onValidatorUpdated(e: Event | CustomEvent): void;
|
private __onValidatorUpdated(e: Event | CustomEvent): void;
|
||||||
__setupValidators(): void;
|
private __setupValidators(): void;
|
||||||
__isEmpty(v: unknown): boolean;
|
private __isEmpty(v: unknown): boolean;
|
||||||
__getFeedbackMessages(validators: Validator[]): Promise<FeedbackMessage[]>;
|
private __getFeedbackMessages(validators: Validator[]): Promise<FeedbackMessage[]>;
|
||||||
_updateFeedbackComponent(): void;
|
|
||||||
_showFeedbackConditionFor(type: string, meta: object): boolean;
|
|
||||||
showFeedbackConditionFor(type: string, meta: object, currentCondition: Function): boolean;
|
|
||||||
_hasFeedbackVisibleFor(type: string): boolean;
|
|
||||||
_updateShouldShowFeedbackFor(): void;
|
|
||||||
_prioritizeAndFilterFeedback(opts: { validationResult: Validator[] }): Validator[];
|
|
||||||
_feedbackConditionMeta: object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function ValidateImplementation<T extends Constructor<LitElement>>(
|
export declare function ValidateImplementation<T extends Constructor<LitElement>>(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Required, DefaultSuccess, Validator } from '@lion/form-core';
|
||||||
import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
|
import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
|
||||||
import { LionInput } from '@lion/input';
|
import { LionInput } from '@lion/input';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
|
|
||||||
describe('Form Validation Integrations', () => {
|
describe('Form Validation Integrations', () => {
|
||||||
const lightDom = '';
|
const lightDom = '';
|
||||||
|
|
@ -49,8 +50,9 @@ describe('Form Validation Integrations', () => {
|
||||||
]}
|
]}
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`));
|
`));
|
||||||
|
const { _feedbackNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el._feedbackNode.feedbackData?.length).to.equal(0);
|
expect(_feedbackNode.feedbackData?.length).to.equal(0);
|
||||||
|
|
||||||
el.modelValue = 'w';
|
el.modelValue = 'w';
|
||||||
el.touched = true;
|
el.touched = true;
|
||||||
|
|
@ -61,7 +63,7 @@ describe('Form Validation Integrations', () => {
|
||||||
el.modelValue = 'warn';
|
el.modelValue = 'warn';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('warning');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('warning');
|
||||||
|
|
||||||
el.modelValue = 'war';
|
el.modelValue = 'war';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -76,14 +78,14 @@ describe('Form Validation Integrations', () => {
|
||||||
'Changed!',
|
'Changed!',
|
||||||
'Ok, correct.',
|
'Ok, correct.',
|
||||||
]).to.include(
|
]).to.include(
|
||||||
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */ (el
|
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */
|
||||||
._feedbackNode.feedbackData)[0].message,
|
(_feedbackNode.feedbackData)[0].message,
|
||||||
);
|
);
|
||||||
|
|
||||||
el.modelValue = '';
|
el.modelValue = '';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('error');
|
expect(_feedbackNode.feedbackData?.[0].message).to.equal('error');
|
||||||
|
|
||||||
el.modelValue = 'war';
|
el.modelValue = 'war';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
@ -98,13 +100,15 @@ describe('Form Validation Integrations', () => {
|
||||||
'Changed!',
|
'Changed!',
|
||||||
'Ok, correct.',
|
'Ok, correct.',
|
||||||
]).to.include(
|
]).to.include(
|
||||||
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */ (el
|
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */
|
||||||
._feedbackNode.feedbackData)[0].message,
|
(_feedbackNode.feedbackData)[0].message,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that change in focused or other interaction states does not refresh the success message
|
// Check that change in focused or other interaction states does not refresh the success message
|
||||||
// without a change in validation results
|
// without a change in validation results
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
el._updateShouldShowFeedbackFor();
|
el._updateShouldShowFeedbackFor();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.feedbackComplete;
|
await el.feedbackComplete;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import '@lion/fieldset/define';
|
||||||
import '@lion/form/define';
|
import '@lion/form/define';
|
||||||
import '@lion/form-core/define';
|
import '@lion/form-core/define';
|
||||||
|
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/core').LitElement} LitElement
|
* @typedef {import('@lion/core').LitElement} LitElement
|
||||||
* @typedef {import('@lion/form-core').LionField} LionField
|
* @typedef {import('@lion/form-core').LionField} LionField
|
||||||
|
|
@ -129,7 +131,7 @@ const choiceGroupDispatchesCountOnFirstPaint = (groupTagname, itemTagname, count
|
||||||
it(getFirstPaintTitle(count), async () => {
|
it(getFirstPaintTitle(count), async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<${groupTag} @model-value-changed="${spy}">
|
<${groupTag} @model-value-changed="${spy}" name="group[]">
|
||||||
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
||||||
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
||||||
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
||||||
|
|
@ -151,7 +153,7 @@ const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, coun
|
||||||
it(getInteractionTitle(count), async () => {
|
it(getInteractionTitle(count), async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${groupTag}>
|
<${groupTag} name="group[]">
|
||||||
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
|
||||||
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
|
||||||
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
|
||||||
|
|
@ -218,7 +220,7 @@ describe('lion-select', () => {
|
||||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<lion-select @model-value-changed="${spy}">
|
<lion-select @model-value-changed="${/** @type {function} */ (spy)}">
|
||||||
<select slot="input">
|
<select slot="input">
|
||||||
<option value="option1"></option>
|
<option value="option1"></option>
|
||||||
<option value="option2"></option>
|
<option value="option2"></option>
|
||||||
|
|
@ -310,7 +312,7 @@ describe('lion-fieldset', () => {
|
||||||
it(getFirstPaintTitle(firstStampCount), async () => {
|
it(getFirstPaintTitle(firstStampCount), async () => {
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
await fixture(html`
|
await fixture(html`
|
||||||
<lion-fieldset name="parent" @model-value-changed="${spy}">
|
<lion-fieldset name="parent" @model-value-changed="${/** @type {function} */ (spy)}">
|
||||||
<lion-input name="input"></lion-input>
|
<lion-input name="input"></lion-input>
|
||||||
</lion-fieldset>
|
</lion-fieldset>
|
||||||
`);
|
`);
|
||||||
|
|
@ -417,14 +419,15 @@ describe('detail.isTriggeredByUser', () => {
|
||||||
* @param {string | undefined} [triggerType]
|
* @param {string | undefined} [triggerType]
|
||||||
*/
|
*/
|
||||||
function mimicUserInput(el, newViewValue, triggerType) {
|
function mimicUserInput(el, newViewValue, triggerType) {
|
||||||
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
const type = detectType(el);
|
const type = detectType(el);
|
||||||
let userInputEv;
|
let userInputEv;
|
||||||
if (type === 'RegularField') {
|
if (type === 'RegularField') {
|
||||||
userInputEv = el._inputNode.tagName === 'SELECT' ? 'change' : 'input';
|
userInputEv = _inputNode.tagName === 'SELECT' ? 'change' : 'input';
|
||||||
el.value = newViewValue; // eslint-disable-line no-param-reassign
|
el.value = newViewValue; // eslint-disable-line no-param-reassign
|
||||||
el._inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true }));
|
_inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true }));
|
||||||
} else if (type === 'ChoiceField') {
|
} else if (type === 'ChoiceField') {
|
||||||
el._inputNode.dispatchEvent(new Event('change', { bubbles: true }));
|
_inputNode.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
} else if (type === 'OptionChoiceField') {
|
} else if (type === 'OptionChoiceField') {
|
||||||
if (!triggerType) {
|
if (!triggerType) {
|
||||||
el.dispatchEvent(new Event('click', { bubbles: true }));
|
el.dispatchEvent(new Event('click', { bubbles: true }));
|
||||||
|
|
@ -456,8 +459,9 @@ describe('detail.isTriggeredByUser', () => {
|
||||||
childrenEl = await fixture(html`<input slot="input" />`);
|
childrenEl = await fixture(html`<input slot="input" />`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const name = controlName === 'checkbox-group' ? 'test[]' : 'test';
|
||||||
const el = /** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (await fixture(
|
const el = /** @type {LitElement & FormControl & {value: string} & {registrationComplete: Promise<boolean>} & {formElements: Array.<FormControl & {value: string}>}} */ (await fixture(
|
||||||
html`<${tag}>${childrenEl}</${tag}>`,
|
html`<${tag} name="${name}">${childrenEl}</${tag}>`,
|
||||||
));
|
));
|
||||||
await el.registrationComplete;
|
await el.registrationComplete;
|
||||||
el.addEventListener('model-value-changed', spy);
|
el.addEventListener('model-value-changed', spy);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ import { parseAmount } from './parsers.js';
|
||||||
*
|
*
|
||||||
* @customElement lion-input-amount
|
* @customElement lion-input-amount
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// TODO: make __callParser protected => _callParser
|
||||||
|
// @ts-ignore [allow-private]: __callParser
|
||||||
export class LionInputAmount extends LocalizeMixin(LionInput) {
|
export class LionInputAmount extends LocalizeMixin(LionInput) {
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
|
|
@ -110,7 +111,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
||||||
this.__parserCallcountSincePaste += 1;
|
this.__parserCallcountSincePaste += 1;
|
||||||
this.__isPasting = this.__parserCallcountSincePaste === 2;
|
this.__isPasting = this.__parserCallcountSincePaste === 2;
|
||||||
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
|
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
|
||||||
// @ts-ignore
|
// @ts-ignore [allow-private]
|
||||||
return super.__callParser(value);
|
return super.__callParser(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ import { html } from '@lion/core';
|
||||||
import { localize } from '@lion/localize';
|
import { localize } from '@lion/localize';
|
||||||
import { localizeTearDown } from '@lion/localize/test-helpers';
|
import { localizeTearDown } from '@lion/localize/test-helpers';
|
||||||
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
||||||
|
import { getInputMembers } from '@lion/input/test-helpers';
|
||||||
import '@lion/input-amount/define';
|
import '@lion/input-amount/define';
|
||||||
import { formatAmount } from '../src/formatters.js';
|
import { formatAmount } from '../src/formatters.js';
|
||||||
import { parseAmount } from '../src/parsers.js';
|
import { parseAmount } from '../src/parsers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import('@lion/input/src/LionInput').LionInput} LionInput
|
||||||
* @typedef {import('../src/LionInputAmount').LionInputAmount} LionInputAmount
|
* @typedef {import('../src/LionInputAmount').LionInputAmount} LionInputAmount
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -73,14 +75,16 @@ describe('<lion-input-amount>', () => {
|
||||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||||
`<lion-input-amount></lion-input-amount>`,
|
`<lion-input-amount></lion-input-amount>`,
|
||||||
));
|
));
|
||||||
expect(el._inputNode.getAttribute('inputmode')).to.equal('decimal');
|
const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el));
|
||||||
|
expect(_inputNode.getAttribute('inputmode')).to.equal('decimal');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
|
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
|
||||||
const el = /** @type {LionInputAmount} */ (await fixture(
|
const el = /** @type {LionInputAmount} */ (await fixture(
|
||||||
`<lion-input-amount></lion-input-amount>`,
|
`<lion-input-amount></lion-input-amount>`,
|
||||||
));
|
));
|
||||||
expect(el._inputNode.type).to.equal('text');
|
const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el));
|
||||||
|
expect(_inputNode.type).to.equal('text');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows no currency by default', async () => {
|
it('shows no currency by default', async () => {
|
||||||
|
|
@ -143,7 +147,8 @@ describe('<lion-input-amount>', () => {
|
||||||
`<lion-input-amount currency="EUR"></lion-input-amount>`,
|
`<lion-input-amount currency="EUR"></lion-input-amount>`,
|
||||||
));
|
));
|
||||||
expect(el._currencyDisplayNode?.getAttribute('data-label')).to.be.not.null;
|
expect(el._currencyDisplayNode?.getAttribute('data-label')).to.be.not.null;
|
||||||
expect(el._inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode?.id);
|
const { _inputNode } = getInputMembers(/** @type {* & LionInput} */ (el));
|
||||||
|
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(el._currencyDisplayNode?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds an aria-label to currency slot', async () => {
|
it('adds an aria-label to currency slot', async () => {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { formatDate, LocalizeMixin, parseDate } from '@lion/localize';
|
||||||
*/
|
*/
|
||||||
function isValidDate(date) {
|
function isValidDate(date) {
|
||||||
// to make sure it is a valid date we use isNaN and not Number.isNaN
|
// to make sure it is a valid date we use isNaN and not Number.isNaN
|
||||||
// @ts-ignore dirty hack, you're not supposed to pass Date instances to isNaN
|
// @ts-ignore [allow]: dirty hack, you're not supposed to pass Date instances to isNaN
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
return date instanceof Date && !isNaN(date);
|
return date instanceof Date && !isNaN(date);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { localize } from '@lion/localize';
|
||||||
import { localizeTearDown } from '@lion/localize/test-helpers';
|
import { localizeTearDown } from '@lion/localize/test-helpers';
|
||||||
import { MaxDate } from '@lion/form-core';
|
import { MaxDate } from '@lion/form-core';
|
||||||
import { expect, fixture as _fixture } from '@open-wc/testing';
|
import { expect, fixture as _fixture } from '@open-wc/testing';
|
||||||
|
import { getInputMembers } from '@lion/input/test-helpers';
|
||||||
import '@lion/input-date/define';
|
import '@lion/input-date/define';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,7 +24,8 @@ describe('<lion-input-date>', () => {
|
||||||
|
|
||||||
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
|
it('has type="text" to activate default keyboard on mobile with all necessary symbols', async () => {
|
||||||
const el = await fixture(html`<lion-input-date></lion-input-date>`);
|
const el = await fixture(html`<lion-input-date></lion-input-date>`);
|
||||||
expect(el._inputNode.type).to.equal('text');
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
expect(_inputNode.type).to.equal('text');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has validator "isDate" applied by default', async () => {
|
it('has validator "isDate" applied by default', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { expect, fixture as _fixture } from '@open-wc/testing';
|
import { expect, fixture as _fixture } from '@open-wc/testing';
|
||||||
|
import { getInputMembers } from '@lion/input/test-helpers';
|
||||||
import '@lion/input-email/define';
|
import '@lion/input-email/define';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,7 +11,8 @@ const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionInputEmai
|
||||||
describe('<lion-input-email>', () => {
|
describe('<lion-input-email>', () => {
|
||||||
it('has a type = text', async () => {
|
it('has a type = text', async () => {
|
||||||
const el = await fixture(`<lion-input-email></lion-input-email>`);
|
const el = await fixture(`<lion-input-email></lion-input-email>`);
|
||||||
expect(el._inputNode.type).to.equal('text');
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
expect(_inputNode.type).to.equal('text');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has validator "IsEmail" applied by default', async () => {
|
it('has validator "IsEmail" applied by default', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { expect, fixture as _fixture } from '@open-wc/testing';
|
import { expect, fixture as _fixture } from '@open-wc/testing';
|
||||||
import { html } from '@lion/core';
|
import { html } from '@lion/core';
|
||||||
|
import { getInputMembers } from '@lion/input/test-helpers';
|
||||||
import { IsCountryIBAN } from '../src/validators.js';
|
import { IsCountryIBAN } from '../src/validators.js';
|
||||||
import { formatIBAN } from '../src/formatters.js';
|
import { formatIBAN } from '../src/formatters.js';
|
||||||
import { parseIBAN } from '../src/parsers.js';
|
import { parseIBAN } from '../src/parsers.js';
|
||||||
|
|
||||||
import '@lion/input-iban/define';
|
import '@lion/input-iban/define';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,7 +25,8 @@ describe('<lion-input-iban>', () => {
|
||||||
|
|
||||||
it('has a type = text', async () => {
|
it('has a type = text', async () => {
|
||||||
const el = await fixture(`<lion-input-iban></lion-input-iban>`);
|
const el = await fixture(`<lion-input-iban></lion-input-iban>`);
|
||||||
expect(el._inputNode.type).to.equal('text');
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
expect(_inputNode.type).to.equal('text');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has validator "IsIBAN" applied by default', async () => {
|
it('has validator "IsIBAN" applied by default', async () => {
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,6 @@ export class LionInputStepper extends LionInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.js",
|
".": "./index.js",
|
||||||
"./define": "./lion-input.js",
|
"./define": "./lion-input.js",
|
||||||
"./docs/": "./docs/"
|
"./docs/": "./docs/",
|
||||||
|
"./test-helpers": "./test-helpers/index.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ import { LionField, NativeTextFieldMixin } from '@lion/form-core';
|
||||||
*
|
*
|
||||||
* @customElement lion-input
|
* @customElement lion-input
|
||||||
*/
|
*/
|
||||||
export class LionInput extends NativeTextFieldMixin(
|
export class LionInput extends NativeTextFieldMixin(LionField) {
|
||||||
/** @type {typeof import('@lion/form-core/types/NativeTextFieldMixinTypes').NativeTextField} */ (LionField),
|
|
||||||
) {
|
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -50,6 +48,10 @@ export class LionInput extends NativeTextFieldMixin(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {HTMLInputElement}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
return /** @type {HTMLInputElement} */ (super._inputNode); // casts type
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
packages/input/test-helpers/getInputMembers.js
Normal file
17
packages/input/test-helpers/getInputMembers.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../src/LionInput').LionInput} LionInput
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { LionInput } el
|
||||||
|
*/
|
||||||
|
export function getInputMembers(el) {
|
||||||
|
const obj = getFormControlMembers(/** @type { * & FormControlHost } */ (el));
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
_inputNode: /** @type {HTMLInputElement} */ (obj._inputNode),
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/input/test-helpers/index.js
Normal file
1
packages/input/test-helpers/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { getInputMembers } from './getInputMembers.js';
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Validator } from '@lion/form-core';
|
import { Validator } from '@lion/form-core';
|
||||||
import { expect, fixture, html, unsafeStatic, triggerFocusFor, aTimeout } from '@open-wc/testing';
|
import { expect, fixture, html, unsafeStatic, triggerFocusFor, aTimeout } from '@open-wc/testing';
|
||||||
|
import { getInputMembers } from '../test-helpers/index.js';
|
||||||
import '@lion/input/define';
|
import '@lion/input/define';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,43 +13,51 @@ const tag = unsafeStatic(tagString);
|
||||||
describe('<lion-input>', () => {
|
describe('<lion-input>', () => {
|
||||||
it('delegates readOnly property and readonly attribute', async () => {
|
it('delegates readOnly property and readonly attribute', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag} readonly></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag} readonly></${tag}>`));
|
||||||
expect(el._inputNode.readOnly).to.equal(true);
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
expect(_inputNode.readOnly).to.equal(true);
|
||||||
el.readOnly = false;
|
el.readOnly = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.readOnly).to.equal(false);
|
expect(el.readOnly).to.equal(false);
|
||||||
expect(el._inputNode.readOnly).to.equal(false);
|
expect(_inputNode.readOnly).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates value attribute', async () => {
|
it('delegates value attribute', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag} value="prefilled"></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag} value="prefilled"></${tag}>`));
|
||||||
expect(el._inputNode.getAttribute('value')).to.equal('prefilled');
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
expect(_inputNode.getAttribute('value')).to.equal('prefilled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be disabled via attribute', async () => {
|
it('can be disabled via attribute', async () => {
|
||||||
const elDisabled = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
||||||
expect(elDisabled.disabled).to.equal(true);
|
const { _inputNode } = getInputMembers(el);
|
||||||
expect(elDisabled._inputNode.disabled).to.equal(true);
|
|
||||||
|
expect(el.disabled).to.equal(true);
|
||||||
|
expect(_inputNode.disabled).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be disabled via property', async () => {
|
it('can be disabled via property', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._inputNode.disabled).to.equal(true);
|
expect(_inputNode.disabled).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Add test that css pointerEvents is none if disabled.
|
// TODO: Add test that css pointerEvents is none if disabled.
|
||||||
it('is disabled when disabled property is passed', async () => {
|
it('is disabled when disabled property is passed', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(false);
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
expect(_inputNode.hasAttribute('disabled')).to.equal(false);
|
||||||
|
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await aTimeout(0);
|
await aTimeout(0);
|
||||||
|
|
||||||
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
|
expect(_inputNode.hasAttribute('disabled')).to.equal(true);
|
||||||
const disabledel = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
const disabledEl = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
|
||||||
expect(disabledel._inputNode.hasAttribute('disabled')).to.equal(true);
|
const { _inputNode: _inputNodeDisabled } = getInputMembers(disabledEl);
|
||||||
|
expect(_inputNodeDisabled.hasAttribute('disabled')).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reads initial value from attribute value', async () => {
|
it('reads initial value from attribute value', async () => {
|
||||||
|
|
@ -80,54 +88,64 @@ describe('<lion-input>', () => {
|
||||||
// This is necessary for security, so that _inputNodes autocomplete can be set to 'off'
|
// This is necessary for security, so that _inputNodes autocomplete can be set to 'off'
|
||||||
it('delegates autocomplete property', async () => {
|
it('delegates autocomplete property', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
expect(el._inputNode.autocomplete).to.equal('');
|
const { _inputNode } = getInputMembers(el);
|
||||||
expect(el._inputNode.hasAttribute('autocomplete')).to.be.false;
|
|
||||||
|
expect(_inputNode.autocomplete).to.equal('');
|
||||||
|
expect(_inputNode.hasAttribute('autocomplete')).to.be.false;
|
||||||
el.autocomplete = 'off';
|
el.autocomplete = 'off';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el._inputNode.autocomplete).to.equal('off');
|
expect(_inputNode.autocomplete).to.equal('off');
|
||||||
expect(el._inputNode.getAttribute('autocomplete')).to.equal('off');
|
expect(_inputNode.getAttribute('autocomplete')).to.equal('off');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
|
it('preserves the caret position on value change for native text fields (input|textarea)', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
await triggerFocusFor(el);
|
await triggerFocusFor(el);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
el._inputNode.value = 'hello world';
|
_inputNode.value = 'hello world';
|
||||||
el._inputNode.selectionStart = 2;
|
_inputNode.selectionStart = 2;
|
||||||
el._inputNode.selectionEnd = 2;
|
_inputNode.selectionEnd = 2;
|
||||||
el.value = 'hey there universe';
|
el.value = 'hey there universe';
|
||||||
expect(el._inputNode.selectionStart).to.equal(2);
|
expect(_inputNode.selectionStart).to.equal(2);
|
||||||
expect(el._inputNode.selectionEnd).to.equal(2);
|
expect(_inputNode.selectionEnd).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('automatically creates an <input> element if not provided by user', async () => {
|
it('automatically creates an <input> element if not provided by user', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`
|
const el = /** @type {LionInput} */ (await fixture(html`
|
||||||
<${tag}></${tag}>
|
<${tag}></${tag}>
|
||||||
`));
|
`));
|
||||||
expect(el.querySelector('input')).to.equal(el._inputNode);
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
|
expect(el.querySelector('input')).to.equal(_inputNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a type which is reflected to an attribute and is synced down to the native input', async () => {
|
it('has a type which is reflected to an attribute and is synced down to the native input', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
expect(el.type).to.equal('text');
|
expect(el.type).to.equal('text');
|
||||||
expect(el.getAttribute('type')).to.equal('text');
|
expect(el.getAttribute('type')).to.equal('text');
|
||||||
expect(el._inputNode.getAttribute('type')).to.equal('text');
|
expect(_inputNode.getAttribute('type')).to.equal('text');
|
||||||
|
|
||||||
el.type = 'foo';
|
el.type = 'foo';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.getAttribute('type')).to.equal('foo');
|
expect(el.getAttribute('type')).to.equal('foo');
|
||||||
expect(el._inputNode.getAttribute('type')).to.equal('foo');
|
expect(_inputNode.getAttribute('type')).to.equal('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has an attribute that can be used to set the placeholder text of the input', async () => {
|
it('has an attribute that can be used to set the placeholder text of the input', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag} placeholder="text"></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag} placeholder="text"></${tag}>`));
|
||||||
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
expect(el.getAttribute('placeholder')).to.equal('text');
|
expect(el.getAttribute('placeholder')).to.equal('text');
|
||||||
expect(el._inputNode.getAttribute('placeholder')).to.equal('text');
|
expect(_inputNode.getAttribute('placeholder')).to.equal('text');
|
||||||
|
|
||||||
el.placeholder = 'foo';
|
el.placeholder = 'foo';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.getAttribute('placeholder')).to.equal('foo');
|
expect(el.getAttribute('placeholder')).to.equal('foo');
|
||||||
expect(el._inputNode.getAttribute('placeholder')).to.equal('foo');
|
expect(_inputNode.getAttribute('placeholder')).to.equal('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove validation when disabled state toggles', async () => {
|
it('should remove validation when disabled state toggles', async () => {
|
||||||
|
|
@ -162,10 +180,12 @@ describe('<lion-input>', () => {
|
||||||
describe('Delegation', () => {
|
describe('Delegation', () => {
|
||||||
it('delegates property value', async () => {
|
it('delegates property value', async () => {
|
||||||
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
|
||||||
expect(el._inputNode.value).to.equal('');
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
|
expect(_inputNode.value).to.equal('');
|
||||||
el.value = 'one';
|
el.value = 'one';
|
||||||
expect(el.value).to.equal('one');
|
expect(el.value).to.equal('one');
|
||||||
expect(el._inputNode.value).to.equal('one');
|
expect(_inputNode.value).to.equal('one');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates property selectionStart and selectionEnd', async () => {
|
it('delegates property selectionStart and selectionEnd', async () => {
|
||||||
|
|
@ -174,11 +194,12 @@ describe('<lion-input>', () => {
|
||||||
.modelValue=${'Some text to select'}
|
.modelValue=${'Some text to select'}
|
||||||
></${tag}>
|
></${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getInputMembers(el);
|
||||||
|
|
||||||
el.selectionStart = 5;
|
el.selectionStart = 5;
|
||||||
el.selectionEnd = 12;
|
el.selectionEnd = 12;
|
||||||
expect(el._inputNode.selectionStart).to.equal(5);
|
expect(_inputNode.selectionStart).to.equal(5);
|
||||||
expect(el._inputNode.selectionEnd).to.equal(12);
|
expect(_inputNode.selectionEnd).to.equal(12);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
"customElementsManifest": "custom-elements.json",
|
"customElementsManifest": "custom-elements.json",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.js",
|
".": "./index.js",
|
||||||
|
"./test-helpers": "./test-helpers/index.js",
|
||||||
"./test-suites": "./test-suites/index.js",
|
"./test-suites": "./test-suites/index.js",
|
||||||
"./define": "./define.js",
|
"./define": "./define.js",
|
||||||
"./define-listbox": "./lion-listbox.js",
|
"./define-listbox": "./lion-listbox.js",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { css, DisabledMixin, html, LitElement } from '@lion/core';
|
||||||
* enabling SubClassers to style based on those states
|
* enabling SubClassers to style based on those states
|
||||||
*/
|
*/
|
||||||
export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMixin(LitElement))) {
|
export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMixin(LitElement))) {
|
||||||
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
active: {
|
active: {
|
||||||
|
|
@ -127,7 +128,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentForm = /** @type {unknown} */ (this._parentFormGroup);
|
const parentForm = /** @type {unknown} */ (this._parentFormGroup);
|
||||||
this.__isHandlingUserInput = true;
|
this._isHandlingUserInput = true;
|
||||||
if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) {
|
if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) {
|
||||||
this.checked = !this.checked;
|
this.checked = !this.checked;
|
||||||
this.active = !this.active;
|
this.active = !this.active;
|
||||||
|
|
@ -135,6 +136,6 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
||||||
this.checked = true;
|
this.checked = true;
|
||||||
this.active = true;
|
this.active = true;
|
||||||
}
|
}
|
||||||
this.__isHandlingUserInput = false;
|
this._isHandlingUserInput = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { LionOptions } from './LionOptions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').HTMLElementWithValue} HTMLElementWithValue
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').HTMLElementWithValue} HTMLElementWithValue
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
* @typedef {import('./LionOption').LionOption} LionOption
|
* @typedef {import('./LionOption').LionOption} LionOption
|
||||||
* @typedef {import('../types/ListboxMixinTypes').ListboxMixin} ListboxMixin
|
* @typedef {import('../types/ListboxMixinTypes').ListboxMixin} ListboxMixin
|
||||||
* @typedef {import('../types/ListboxMixinTypes').ListboxHost} ListboxHost
|
* @typedef {import('../types/ListboxMixinTypes').ListboxHost} ListboxHost
|
||||||
|
|
@ -54,6 +55,7 @@ const ListboxMixinImplementation = superclass =>
|
||||||
class ListboxMixin extends FormControlMixin(
|
class ListboxMixin extends FormControlMixin(
|
||||||
ScopedElementsMixin(ChoiceGroupMixin(SlotMixin(FormRegistrarMixin(superclass)))),
|
ScopedElementsMixin(ChoiceGroupMixin(SlotMixin(FormRegistrarMixin(superclass)))),
|
||||||
) {
|
) {
|
||||||
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
orientation: String,
|
orientation: String,
|
||||||
|
|
@ -117,7 +119,6 @@ const ListboxMixinImplementation = superclass =>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -267,7 +268,10 @@ const ListboxMixinImplementation = superclass =>
|
||||||
this._listboxActiveDescendant = null;
|
this._listboxActiveDescendant = null;
|
||||||
/** @private */
|
/** @private */
|
||||||
this.__hasInitialSelectedFormElement = false;
|
this.__hasInitialSelectedFormElement = false;
|
||||||
/** @protected */
|
/**
|
||||||
|
* @type {'fieldset' | 'child' | 'choice-group'}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
this._repropagationRole = 'choice-group'; // configures FormControlMixin
|
this._repropagationRole = 'choice-group'; // configures FormControlMixin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -279,9 +283,9 @@ const ListboxMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string | string[] | undefined}
|
* @type {string | string[] | undefined}
|
||||||
* @private
|
* @protected
|
||||||
*/
|
*/
|
||||||
this.__oldModelValue = undefined;
|
this._oldModelValue = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {EventListener}
|
* @type {EventListener}
|
||||||
|
|
@ -403,12 +407,10 @@ const ListboxMixinImplementation = superclass =>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enhance FormRegistrarMixin: make sure children have specific default states when added
|
* @enhance FormRegistrarMixin: make sure children have specific default states when added
|
||||||
* @param {LionOption} child
|
* @param {FormControlHost & LionOption} child
|
||||||
* @param {Number} indexToInsertAt
|
* @param {Number} indexToInsertAt
|
||||||
*/
|
*/
|
||||||
// @ts-expect-error
|
|
||||||
addFormElement(child, indexToInsertAt) {
|
addFormElement(child, indexToInsertAt) {
|
||||||
// @ts-expect-error
|
|
||||||
super.addFormElement(/** @type {FormControl} */ child, indexToInsertAt);
|
super.addFormElement(/** @type {FormControl} */ child, indexToInsertAt);
|
||||||
// we need to adjust the elements being registered
|
// we need to adjust the elements being registered
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
|
|
@ -426,7 +428,7 @@ const ListboxMixinImplementation = superclass =>
|
||||||
});
|
});
|
||||||
|
|
||||||
this.__proxyChildModelValueChanged(
|
this.__proxyChildModelValueChanged(
|
||||||
/** @type {CustomEvent & { target: LionOption; }} */ ({ target: child }),
|
/** @type {CustomEvent & { target: FormControlHost & LionOption; }} */ ({ target: child }),
|
||||||
);
|
);
|
||||||
this.resetInteractionState();
|
this.resetInteractionState();
|
||||||
/* eslint-enable no-param-reassign */
|
/* eslint-enable no-param-reassign */
|
||||||
|
|
@ -526,11 +528,11 @@ const ListboxMixinImplementation = superclass =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__isHandlingUserInput = true;
|
this._isHandlingUserInput = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Since we can't control when subclasses are done handling keyboard input, we
|
// Since we can't control when subclasses are done handling keyboard input, we
|
||||||
// schedule a timeout to reset __isHandlingUserInput
|
// schedule a timeout to reset _isHandlingUserInput
|
||||||
this.__isHandlingUserInput = false;
|
this._isHandlingUserInput = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { key } = ev;
|
const { key } = ev;
|
||||||
|
|
@ -636,11 +638,11 @@ const ListboxMixinImplementation = superclass =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__isHandlingUserInput = true;
|
this._isHandlingUserInput = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Since we can't control when subclasses are done handling keyboard input, we
|
// Since we can't control when subclasses are done handling keyboard input, we
|
||||||
// schedule a timeout to reset __isHandlingUserInput
|
// schedule a timeout to reset _isHandlingUserInput
|
||||||
this.__isHandlingUserInput = false;
|
this._isHandlingUserInput = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { key } = ev;
|
const { key } = ev;
|
||||||
|
|
@ -760,20 +762,20 @@ const ListboxMixinImplementation = superclass =>
|
||||||
this.__onChildCheckedChanged(ev);
|
this.__onChildCheckedChanged(ev);
|
||||||
|
|
||||||
// don't send this.modelValue as oldValue, since it will take modelValue getter which takes it from child elements, which is already the updated value
|
// don't send this.modelValue as oldValue, since it will take modelValue getter which takes it from child elements, which is already the updated value
|
||||||
this.requestUpdate('modelValue', this.__oldModelValue);
|
this.requestUpdate('modelValue', this._oldModelValue);
|
||||||
// only send model-value-changed if the event is caused by one of its children
|
// only send model-value-changed if the event is caused by one of its children
|
||||||
if (ev.detail && ev.detail.formPath) {
|
if (ev.detail && ev.detail.formPath) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('model-value-changed', {
|
new CustomEvent('model-value-changed', {
|
||||||
detail: /** @type {ModelValueEventDetails} */ ({
|
detail: /** @type {ModelValueEventDetails} */ ({
|
||||||
formPath: ev.detail.formPath,
|
formPath: ev.detail.formPath,
|
||||||
isTriggeredByUser: ev.detail.isTriggeredByUser || this.__isHandlingUserInput,
|
isTriggeredByUser: ev.detail.isTriggeredByUser || this._isHandlingUserInput,
|
||||||
element: ev.target,
|
element: ev.target,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.__oldModelValue = this.modelValue;
|
this._oldModelValue = this.modelValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
21
packages/listbox/test-helpers/getListboxMembers.js
Normal file
21
packages/listbox/test-helpers/getListboxMembers.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@lion/listbox/src/LionOptions').LionOptions} LionOptions
|
||||||
|
* @typedef {import('@lion/listbox/types/ListboxMixinTypes').ListboxHost} ListboxHost
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { ListboxHost } el
|
||||||
|
*/
|
||||||
|
export function getListboxMembers(el) {
|
||||||
|
const obj = getFormControlMembers(/** @type { * & FormControlHost } */ (el));
|
||||||
|
// eslint-disable-next-line no-return-assign
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
_inputNode: /** @type {* & LionOptions} */ (obj._inputNode),
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
...{ _listboxNode: el._listboxNode, _activeDescendantOwnerNode: el._activeDescendantOwnerNode },
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/listbox/test-helpers/index.js
Normal file
1
packages/listbox/test-helpers/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { getListboxMembers } from './getListboxMembers.js';
|
||||||
|
|
@ -4,6 +4,7 @@ import { LionOptions } from '@lion/listbox';
|
||||||
import '@lion/listbox/define';
|
import '@lion/listbox/define';
|
||||||
import { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing';
|
import { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { getListboxMembers } from '../test-helpers/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../src/LionListbox').LionListbox} LionListbox
|
* @typedef {import('../src/LionListbox').LionListbox} LionListbox
|
||||||
|
|
@ -23,22 +24,22 @@ function mimicKeyPress(el, key) {
|
||||||
el.dispatchEvent(new KeyboardEvent('keyup', { key }));
|
el.dispatchEvent(new KeyboardEvent('keyup', { key }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* @param {LionListbox} lionListboxEl
|
// * @param {LionListbox} lionListboxEl
|
||||||
*/
|
// */
|
||||||
function getProtectedMembers(lionListboxEl) {
|
// function getProtectedMembers(lionListboxEl) {
|
||||||
// @ts-ignore protected members allowed in test
|
// // @ts-ignore protected members allowed in test
|
||||||
const {
|
// const {
|
||||||
_inputNode: input,
|
// _inputNode: input,
|
||||||
_activeDescendantOwnerNode: activeDescendantOwner,
|
// _activeDescendantOwnerNode: activeDescendantOwner,
|
||||||
_listboxNode: listbox,
|
// _listboxNode: listbox,
|
||||||
} = lionListboxEl;
|
// } = lionListboxEl;
|
||||||
return {
|
// return {
|
||||||
input,
|
// input,
|
||||||
activeDescendantOwner,
|
// activeDescendantOwner,
|
||||||
listbox,
|
// listbox,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param { {tagString?:string, optionTagString?:string} } [customConfig]
|
* @param { {tagString?:string, optionTagString?:string} } [customConfig]
|
||||||
|
|
@ -253,14 +254,16 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
const el1 = await fixture(html`
|
const el1 = await fixture(html`
|
||||||
<${tag} label="foo"></${tag}>
|
<${tag} label="foo"></${tag}>
|
||||||
`);
|
`);
|
||||||
expect(el1.fieldName).to.equal(el1._labelNode.textContent);
|
const { _labelNode: _labelNode1 } = getListboxMembers(el1);
|
||||||
|
expect(el1.fieldName).to.equal(_labelNode1.textContent);
|
||||||
|
|
||||||
const el2 = await fixture(html`
|
const el2 = await fixture(html`
|
||||||
<${tag}>
|
<${tag}>
|
||||||
<label slot="label">bar</label>
|
<label slot="label">bar</label>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
expect(el2.fieldName).to.equal(el2._labelNode.textContent);
|
const { _labelNode: _labelNode2 } = getListboxMembers(el2);
|
||||||
|
expect(el2.fieldName).to.equal(_labelNode2.textContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`has a fieldName based on the name if no label exists`, async () => {
|
it(`has a fieldName based on the name if no label exists`, async () => {
|
||||||
|
|
@ -274,6 +277,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<${tag} label="foo" .fieldName="${'bar'}"></${tag}>
|
<${tag} label="foo" .fieldName="${'bar'}"></${tag}>
|
||||||
`);
|
`);
|
||||||
|
// @ts-ignore [allow-proteced] in test
|
||||||
expect(el.__fieldName).to.equal(el.fieldName);
|
expect(el.__fieldName).to.equal(el.fieldName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -370,9 +374,9 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${30} id="predefined">Item 3</${optionTag}>
|
<${optionTag} .choiceValue=${30} id="predefined">Item 3</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
expect(el.querySelectorAll('lion-option')[0].id).to.exist;
|
expect(el.querySelectorAll(cfg.optionTagString)[0].id).to.exist;
|
||||||
expect(el.querySelectorAll('lion-option')[1].id).to.exist;
|
expect(el.querySelectorAll(cfg.optionTagString)[1].id).to.exist;
|
||||||
expect(el.querySelectorAll('lion-option')[2].id).to.equal('predefined');
|
expect(el.querySelectorAll(cfg.optionTagString)[2].id).to.equal('predefined');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a reference to the active option', async () => {
|
it('has a reference to the active option', async () => {
|
||||||
|
|
@ -382,19 +386,19 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${'20'} checked id="second">Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { activeDescendantOwner } = getProtectedMembers(el);
|
const { _activeDescendantOwnerNode } = getListboxMembers(el);
|
||||||
|
|
||||||
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.be.null;
|
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.be.null;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Normalize
|
// Normalize
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('first');
|
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('first');
|
||||||
mimicKeyPress(activeDescendantOwner, 'ArrowDown');
|
mimicKeyPress(_activeDescendantOwnerNode, 'ArrowDown');
|
||||||
// activeDescendantOwner.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
// _activeDescendantOwnerNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('second');
|
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('second');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('puts "aria-setsize" on all options to indicate the total amount of options', async () => {
|
it('puts "aria-setsize" on all options to indicate the total amount of options', async () => {
|
||||||
|
|
@ -542,21 +546,21 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// Normalize
|
// Normalize
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// mimicKeyPress(listbox, 'ArrowUp');
|
// mimicKeyPress(listbox, 'ArrowUp');
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowUp');
|
mimicKeyPress(_listboxNode, 'ArrowUp');
|
||||||
|
|
||||||
expect(options[0].active).to.be.true;
|
expect(options[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
expect(options[2].active).to.be.false;
|
expect(options[2].active).to.be.false;
|
||||||
el.activeIndex = 2;
|
el.activeIndex = 2;
|
||||||
// mimicKeyPress(listbox, 'ArrowDown');
|
// mimicKeyPress(listbox, 'ArrowDown');
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
|
|
||||||
expect(options[0].active).to.be.false;
|
expect(options[0].active).to.be.false;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
|
|
@ -571,26 +575,27 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
const { _inputNode } = getListboxMembers(el);
|
||||||
|
|
||||||
el._inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// Normalize
|
// Normalize
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
||||||
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
|
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
|
||||||
mimicKeyPress(el._inputNode, 'ArrowUp');
|
mimicKeyPress(_inputNode, 'ArrowUp');
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.activeIndex).to.equal(2);
|
expect(el.activeIndex).to.equal(2);
|
||||||
|
|
||||||
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
mimicKeyPress(el._inputNode, 'ArrowDown');
|
mimicKeyPress(_inputNode, 'ArrowDown');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
// Extra check: regular navigation
|
// Extra check: regular navigation
|
||||||
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||||
mimicKeyPress(el._inputNode, 'ArrowDown');
|
mimicKeyPress(_inputNode, 'ArrowDown');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
@ -605,14 +610,14 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// Normalize suite
|
// Normalize suite
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
expect(options[1].checked).to.be.true;
|
expect(options[1].checked).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -628,21 +633,21 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// Normalize suite
|
// Normalize suite
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
mimicKeyPress(listbox, ' ');
|
mimicKeyPress(_listboxNode, ' ');
|
||||||
|
|
||||||
expect(options[1].checked).to.be.true;
|
expect(options[1].checked).to.be.true;
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
// @ts-ignore allow protected member access in test
|
// @ts-ignore allow protected member access in test
|
||||||
el._listboxReceivesNoFocus = true;
|
el._listboxReceivesNoFocus = true;
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
mimicKeyPress(listbox, ' ');
|
mimicKeyPress(_listboxNode, ' ');
|
||||||
|
|
||||||
expect(options[1].checked).to.be.false;
|
expect(options[1].checked).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -683,7 +688,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// @ts-ignore allow protected members in tests
|
// @ts-ignore allow protected members in tests
|
||||||
if (el._listboxReceivesNoFocus) {
|
if (el._listboxReceivesNoFocus) {
|
||||||
|
|
@ -691,9 +696,9 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
el.activeIndex = 2;
|
el.activeIndex = 2;
|
||||||
mimicKeyPress(listbox, 'Home');
|
mimicKeyPress(_listboxNode, 'Home');
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
mimicKeyPress(listbox, 'End');
|
mimicKeyPress(_listboxNode, 'End');
|
||||||
expect(el.activeIndex).to.equal(3);
|
expect(el.activeIndex).to.equal(3);
|
||||||
});
|
});
|
||||||
it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => {
|
it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => {
|
||||||
|
|
@ -704,7 +709,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'Item 3'}>Item 3</${optionTag}>
|
<${optionTag} .choiceValue=${'Item 3'}>Item 3</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// Normalize across listbox/select-rich/combobox
|
// Normalize across listbox/select-rich/combobox
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
|
|
@ -713,10 +718,10 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
el.selectionFollowsFocus = false;
|
el.selectionFollowsFocus = false;
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
expect(el.checkedIndex).to.equal(-1);
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
expect(el.checkedIndex).to.equal(-1);
|
||||||
mimicKeyPress(listbox, 'ArrowUp');
|
mimicKeyPress(_listboxNode, 'ArrowUp');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
expect(el.checkedIndex).to.equal(-1);
|
||||||
|
|
@ -731,7 +736,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
expect(el.orientation).to.equal('vertical');
|
expect(el.orientation).to.equal('vertical');
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
|
|
@ -742,23 +747,23 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
expect(options[0].active).to.be.true;
|
expect(options[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(options[0].active).to.be.false;
|
expect(options[0].active).to.be.false;
|
||||||
expect(options[1].active).to.be.true;
|
expect(options[1].active).to.be.true;
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowUp');
|
mimicKeyPress(_listboxNode, 'ArrowUp');
|
||||||
|
|
||||||
expect(options[0].active).to.be.true;
|
expect(options[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
|
|
||||||
// No response to horizontal arrows...
|
// No response to horizontal arrows...
|
||||||
mimicKeyPress(listbox, 'ArrowRight');
|
mimicKeyPress(_listboxNode, 'ArrowRight');
|
||||||
|
|
||||||
expect(options[0].active).to.be.true;
|
expect(options[0].active).to.be.true;
|
||||||
expect(options[1].active).to.be.false;
|
expect(options[1].active).to.be.false;
|
||||||
|
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
mimicKeyPress(listbox, 'ArrowLeft');
|
mimicKeyPress(_listboxNode, 'ArrowLeft');
|
||||||
|
|
||||||
expect(options[0].active).to.be.false;
|
expect(options[0].active).to.be.false;
|
||||||
expect(options[1].active).to.be.true;
|
expect(options[1].active).to.be.true;
|
||||||
|
|
@ -771,7 +776,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
expect(el.orientation).to.equal('horizontal');
|
expect(el.orientation).to.equal('horizontal');
|
||||||
|
|
||||||
|
|
@ -780,20 +785,20 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowRight');
|
mimicKeyPress(_listboxNode, 'ArrowRight');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowLeft');
|
mimicKeyPress(_listboxNode, 'ArrowLeft');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
||||||
// No response to vertical arrows...
|
// No response to vertical arrows...
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
|
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
mimicKeyPress(listbox, 'ArrowUp');
|
mimicKeyPress(_listboxNode, 'ArrowUp');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
@ -806,8 +811,8 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
expect(listbox.getAttribute('aria-orientation')).to.equal('horizontal');
|
expect(_listboxNode.getAttribute('aria-orientation')).to.equal('horizontal');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -838,7 +843,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
|
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
|
|
||||||
// @ts-ignore feature detection select-rich
|
// @ts-ignore feature detection select-rich
|
||||||
|
|
@ -864,13 +869,13 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
// Enter
|
// Enter
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
expect(options[0].checked).to.equal(true);
|
expect(options[0].checked).to.equal(true);
|
||||||
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
||||||
// also deselect
|
// also deselect
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
expect(options[0].checked).to.equal(true);
|
expect(options[0].checked).to.equal(true);
|
||||||
expect(el.modelValue).to.eql(['Artichoke']);
|
expect(el.modelValue).to.eql(['Artichoke']);
|
||||||
|
|
||||||
|
|
@ -885,15 +890,15 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
|
|
||||||
// Space
|
// Space
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
mimicKeyPress(listbox, ' ');
|
mimicKeyPress(_listboxNode, ' ');
|
||||||
|
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
mimicKeyPress(listbox, ' ');
|
mimicKeyPress(_listboxNode, ' ');
|
||||||
|
|
||||||
expect(options[0].checked).to.equal(true);
|
expect(options[0].checked).to.equal(true);
|
||||||
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
|
||||||
// also deselect
|
// also deselect
|
||||||
mimicKeyPress(listbox, ' ');
|
mimicKeyPress(_listboxNode, ' ');
|
||||||
|
|
||||||
expect(options[0].checked).to.equal(true);
|
expect(options[0].checked).to.equal(true);
|
||||||
expect(el.modelValue).to.eql(['Artichoke']);
|
expect(el.modelValue).to.eql(['Artichoke']);
|
||||||
|
|
@ -907,8 +912,8 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
expect(listbox.getAttribute('aria-multiselectable')).to.equal('true');
|
expect(_listboxNode.getAttribute('aria-multiselectable')).to.equal('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not allow "selectionFollowsFocus"', async () => {
|
it('does not allow "selectionFollowsFocus"', async () => {
|
||||||
|
|
@ -918,11 +923,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox, input } = getProtectedMembers(el);
|
const { _listboxNode, _inputNode } = getListboxMembers(el);
|
||||||
|
|
||||||
input.focus();
|
_inputNode.focus();
|
||||||
listbox.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
_listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
|
||||||
expect(listbox.getAttribute('aria-multiselectable')).to.equal('true');
|
expect(_listboxNode.getAttribute('aria-multiselectable')).to.equal('true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -950,7 +955,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// Normalize start values between listbox, slect and combobox and test interaction below
|
// Normalize start values between listbox, slect and combobox and test interaction below
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
|
|
@ -958,11 +963,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 1);
|
expectOnlyGivenOneOptionToBeChecked(options, 1);
|
||||||
mimicKeyPress(listbox, 'ArrowUp');
|
mimicKeyPress(_listboxNode, 'ArrowUp');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
@ -990,7 +995,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// Normalize start values between listbox, slect and combobox and test interaction below
|
// Normalize start values between listbox, slect and combobox and test interaction below
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
|
|
@ -998,12 +1003,12 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
expectOnlyGivenOneOptionToBeChecked(options, 0);
|
||||||
mimicKeyPress(listbox, 'ArrowRight');
|
mimicKeyPress(_listboxNode, 'ArrowRight');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
expectOnlyGivenOneOptionToBeChecked(options, 1);
|
expectOnlyGivenOneOptionToBeChecked(options, 1);
|
||||||
mimicKeyPress(listbox, 'ArrowLeft');
|
mimicKeyPress(_listboxNode, 'ArrowLeft');
|
||||||
|
|
||||||
expect(el.activeIndex).to.equal(0);
|
expect(el.activeIndex).to.equal(0);
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
|
|
@ -1018,7 +1023,7 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// @ts-ignore allow protected
|
// @ts-ignore allow protected
|
||||||
if (el._listboxReceivesNoFocus) {
|
if (el._listboxReceivesNoFocus) {
|
||||||
|
|
@ -1026,9 +1031,9 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(el.modelValue).to.equal('30');
|
expect(el.modelValue).to.equal('30');
|
||||||
mimicKeyPress(listbox, 'Home');
|
mimicKeyPress(_listboxNode, 'Home');
|
||||||
expect(el.modelValue).to.equal('10');
|
expect(el.modelValue).to.equal('10');
|
||||||
mimicKeyPress(listbox, 'End');
|
mimicKeyPress(_listboxNode, 'End');
|
||||||
expect(el.modelValue).to.equal('40');
|
expect(el.modelValue).to.equal('40');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1041,11 +1046,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} checked .choiceValue=${'20'}>Item 2</${optionTag}>
|
<${optionTag} checked .choiceValue=${'20'}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const { checkedIndex } = el;
|
const { checkedIndex } = el;
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.checkedIndex).to.equal(checkedIndex);
|
expect(el.checkedIndex).to.equal(checkedIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1096,16 +1101,16 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
|
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
|
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
// Checked index stays where it was
|
// Checked index stays where it was
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
@ -1118,16 +1123,16 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
|
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.activeIndex).to.equal(1);
|
expect(el.activeIndex).to.equal(1);
|
||||||
expect(el.checkedIndex).to.equal(-1);
|
expect(el.checkedIndex).to.equal(-1);
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'ArrowDown');
|
mimicKeyPress(_listboxNode, 'ArrowDown');
|
||||||
expect(el.activeIndex).to.equal(2);
|
expect(el.activeIndex).to.equal(2);
|
||||||
expect(el.checkedIndex).to.equal(2);
|
expect(el.checkedIndex).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
@ -1141,11 +1146,11 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20} id="myId">Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20} id="myId">Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { activeDescendantOwner } = getProtectedMembers(el);
|
const { _activeDescendantOwnerNode } = getListboxMembers(el);
|
||||||
|
|
||||||
const opt = el.formElements[1];
|
const opt = el.formElements[1];
|
||||||
opt.active = true;
|
opt.active = true;
|
||||||
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('myId');
|
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('myId');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can set checked state', async () => {
|
it('can set checked state', async () => {
|
||||||
|
|
@ -1326,15 +1331,15 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
expect(listbox).to.exist;
|
expect(_listboxNode).to.exist;
|
||||||
expect(listbox).to.be.instanceOf(LionOptions);
|
expect(_listboxNode).to.be.instanceOf(LionOptions);
|
||||||
expect(el.querySelector('[role=listbox]')).to.equal(listbox);
|
expect(el.querySelector('[role=listbox]')).to.equal(_listboxNode);
|
||||||
|
|
||||||
expect(el.formElements.length).to.equal(2);
|
expect(el.formElements.length).to.equal(2);
|
||||||
expect(listbox.children.length).to.equal(2);
|
expect(_listboxNode.children.length).to.equal(2);
|
||||||
expect(listbox.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase());
|
expect(_listboxNode.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1347,14 +1352,14 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
|
|
||||||
// Allow options that behave like anchors (think of Google Search) to trigger the anchor behavior
|
// Allow options that behave like anchors (think of Google Search) to trigger the anchor behavior
|
||||||
const activeOption = el.formElements[1];
|
const activeOption = el.formElements[1];
|
||||||
const clickSpy = sinon.spy(activeOption, 'click');
|
const clickSpy = sinon.spy(activeOption, 'click');
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
|
|
||||||
expect(clickSpy).to.have.been.calledOnce;
|
expect(clickSpy).to.have.been.calledOnce;
|
||||||
});
|
});
|
||||||
|
|
@ -1367,14 +1372,14 @@ export function runListboxMixinSuite(customConfig = {}) {
|
||||||
</${tag}>
|
</${tag}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { listbox } = getProtectedMembers(el);
|
const { _listboxNode } = getListboxMembers(el);
|
||||||
|
|
||||||
el.activeIndex = 0;
|
el.activeIndex = 0;
|
||||||
|
|
||||||
const activeOption = el.formElements[0];
|
const activeOption = el.formElements[0];
|
||||||
const clickSpy = sinon.spy(activeOption, 'click');
|
const clickSpy = sinon.spy(activeOption, 'click');
|
||||||
|
|
||||||
mimicKeyPress(listbox, 'Enter');
|
mimicKeyPress(_listboxNode, 'Enter');
|
||||||
|
|
||||||
expect(clickSpy).to.not.have.been.called;
|
expect(clickSpy).to.not.have.been.called;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export declare class ListboxHost {
|
||||||
/** Reset interaction states and modelValue */
|
/** Reset interaction states and modelValue */
|
||||||
public reset(): void;
|
public reset(): void;
|
||||||
|
|
||||||
protected get _scrollTargetNode(): LionOptions;
|
protected get _scrollTargetNode(): HTMLElement;
|
||||||
|
|
||||||
protected get _listboxNode(): LionOptions;
|
protected get _listboxNode(): LionOptions;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// @ts-expect-error no types for this package
|
// @ts-expect-error [external]: no types for this package
|
||||||
import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
|
import MessageFormat from '@bundled-es-modules/message-format/MessageFormat.js';
|
||||||
import isLocalizeESModule from './isLocalizeESModule.js';
|
import isLocalizeESModule from './isLocalizeESModule.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
".": "./index.js",
|
".": "./index.js",
|
||||||
"./test-suites": "./test-suites/index.js",
|
"./test-suites": "./test-suites/index.js",
|
||||||
"./translations/*": "./translations/*",
|
"./translations/*": "./translations/*",
|
||||||
"./test-helpers": "./test-helpers.js",
|
"./test-helpers": "./test-helpers/index.js",
|
||||||
"./docs/": "./docs/"
|
"./docs/": "./docs/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ import { containFocus } from './utils/contain-focus.js';
|
||||||
* @returns {Promise<PopperModule>}
|
* @returns {Promise<PopperModule>}
|
||||||
*/
|
*/
|
||||||
async function preloadPopper() {
|
async function preloadPopper() {
|
||||||
// @ts-ignore import complains about untyped module, but we typecast it ourselves
|
// @ts-ignore [external]: import complains about untyped module, but we typecast it ourselves
|
||||||
return /** @type {Promise<PopperModule>} */ (import('@popperjs/core/dist/esm/popper.js'));
|
return /** @type {* & Promise<PopperModule>} */ (import('@popperjs/core/dist/esm/popper.js'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLOBAL_OVERLAYS_CONTAINER_CLASS = 'global-overlays__overlay-container';
|
const GLOBAL_OVERLAYS_CONTAINER_CLASS = 'global-overlays__overlay-container';
|
||||||
const GLOBAL_OVERLAYS_CLASS = 'global-overlays__overlay';
|
const GLOBAL_OVERLAYS_CLASS = 'global-overlays__overlay';
|
||||||
// @ts-expect-error CSS not yet typed
|
// @ts-expect-error [external]: CSS not yet typed
|
||||||
const supportsCSSTypedObject = window.CSS && CSS.number;
|
const supportsCSSTypedObject = window.CSS && CSS.number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -398,7 +398,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
}
|
}
|
||||||
/** config [l2] or [l4] */
|
/** config [l2] or [l4] */
|
||||||
if (this.__isContentNodeProjected) {
|
if (this.__isContentNodeProjected) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error [external]: fix Node types
|
||||||
return this.__originalContentParent?.getRootNode().host;
|
return this.__originalContentParent?.getRootNode().host;
|
||||||
}
|
}
|
||||||
/** config [l1] or [l3] */
|
/** config [l1] or [l3] */
|
||||||
|
|
@ -529,7 +529,7 @@ export class OverlayController extends EventTargetShim {
|
||||||
if (this.placementMode === 'local') {
|
if (this.placementMode === 'local') {
|
||||||
// Lazily load Popper if not done yet
|
// Lazily load Popper if not done yet
|
||||||
if (!OverlayController.popperModule) {
|
if (!OverlayController.popperModule) {
|
||||||
// @ts-expect-error FIXME: for some reason createPopper is missing here
|
// a@ts-expect-error FIXME: for some reason createPopper is missing here
|
||||||
OverlayController.popperModule = preloadPopper();
|
OverlayController.popperModule = preloadPopper();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -784,9 +784,9 @@ export class OverlayController extends EventTargetShim {
|
||||||
const newMarginRight = this.__bodyMarginRight + scrollbarWidth;
|
const newMarginRight = this.__bodyMarginRight + scrollbarWidth;
|
||||||
const newMarginBottom = this.__bodyMarginBottom + scrollbarHeight;
|
const newMarginBottom = this.__bodyMarginBottom + scrollbarHeight;
|
||||||
if (supportsCSSTypedObject) {
|
if (supportsCSSTypedObject) {
|
||||||
// @ts-expect-error types attributeStyleMap + CSS.px not available yet
|
// @ts-expect-error [external]: types attributeStyleMap + CSS.px not available yet
|
||||||
document.body.attributeStyleMap.set('margin-right', CSS.px(newMarginRight));
|
document.body.attributeStyleMap.set('margin-right', CSS.px(newMarginRight));
|
||||||
// @ts-expect-error types attributeStyleMap + CSS.px not available yet
|
// @ts-expect-error [external]: types attributeStyleMap + CSS.px not available yet
|
||||||
document.body.attributeStyleMap.set('margin-bottom', CSS.px(newMarginBottom));
|
document.body.attributeStyleMap.set('margin-bottom', CSS.px(newMarginBottom));
|
||||||
} else {
|
} else {
|
||||||
document.body.style.marginRight = `${newMarginRight}px`;
|
document.body.style.marginRight = `${newMarginRight}px`;
|
||||||
|
|
@ -1300,5 +1300,5 @@ export class OverlayController extends EventTargetShim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** @type {PopperModule | undefined} */
|
/** @type {Promise<PopperModule> | undefined} */
|
||||||
OverlayController.popperModule = undefined;
|
OverlayController.popperModule = undefined;
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,9 @@ export class OverlaysManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
/**
|
||||||
|
* @param {{ disabledCtrl?:OverlayController, findNewTrap?:boolean }} [options]
|
||||||
|
*/
|
||||||
informTrapsKeyboardFocusGotDisabled({ disabledCtrl, findNewTrap = true } = {}) {
|
informTrapsKeyboardFocusGotDisabled({ disabledCtrl, findNewTrap = true } = {}) {
|
||||||
const next = this.shownList.find(
|
const next = this.shownList.find(
|
||||||
ctrl => ctrl !== disabledCtrl && ctrl.trapsKeyboardFocus === true,
|
ctrl => ctrl !== disabledCtrl && ctrl.trapsKeyboardFocus === true,
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,9 @@ function mergeSortByTabIndex(left, right) {
|
||||||
const result = [];
|
const result = [];
|
||||||
while (left.length > 0 && right.length > 0) {
|
while (left.length > 0 && right.length > 0) {
|
||||||
if (hasLowerTabOrder(left[0], right[0])) {
|
if (hasLowerTabOrder(left[0], right[0])) {
|
||||||
// @ts-ignore
|
result.push(/** @type {HTMLElement} */ (right.shift()));
|
||||||
result.push(right.shift());
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
result.push(/** @type {HTMLElement} */ (left.shift()));
|
||||||
result.push(left.shift());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { mimicClick } from './test-helpers/mimicClick.js';
|
|
||||||
1
packages/overlays/test-helpers/index.js
Normal file
1
packages/overlays/test-helpers/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { mimicClick } from './mimicClick.js';
|
||||||
|
|
@ -8,7 +8,7 @@ import { OverlayController } from '../src/OverlayController.js';
|
||||||
import { overlays } from '../src/overlays.js';
|
import { overlays } from '../src/overlays.js';
|
||||||
import { keyCodes } from '../src/utils/key-codes.js';
|
import { keyCodes } from '../src/utils/key-codes.js';
|
||||||
import { simulateTab } from '../src/utils/simulate-tab.js';
|
import { simulateTab } from '../src/utils/simulate-tab.js';
|
||||||
import { mimicClick } from '../test-helpers.js';
|
import { mimicClick } from '../test-helpers/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,6 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -98,8 +97,10 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
||||||
*/
|
*/
|
||||||
get _scrollTargetNode() {
|
get _scrollTargetNode() {
|
||||||
// TODO: should this be defined here or in extension layer?
|
// TODO: should this be defined here or in extension layer?
|
||||||
// @ts-expect-error we allow the _overlayContentNode to define its own _scrollTargetNode
|
return /** @type {HTMLElement} */ (
|
||||||
return this._overlayContentNode._scrollTargetNode || this._overlayContentNode;
|
/** @type {HTMLElement & {_scrollTargetNode?: HTMLElement}} */ (this._overlayContentNode)
|
||||||
|
._scrollTargetNode || this._overlayContentNode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,29 @@ import {
|
||||||
fixture as _fixture,
|
fixture as _fixture,
|
||||||
} from '@open-wc/testing';
|
} from '@open-wc/testing';
|
||||||
import { LionSelectInvoker, LionSelectRich } from '@lion/select-rich';
|
import { LionSelectInvoker, LionSelectRich } from '@lion/select-rich';
|
||||||
|
|
||||||
import '@lion/core/differentKeyEventNamesShimIE';
|
import '@lion/core/differentKeyEventNamesShimIE';
|
||||||
import '@lion/listbox/define';
|
import '@lion/listbox/define';
|
||||||
import '@lion/select-rich/define';
|
import '@lion/select-rich/define';
|
||||||
|
import { getListboxMembers } from '@lion/listbox/test-helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('@lion/listbox/src/LionOptions').LionOptions} LionOptions
|
||||||
|
* @typedef {import('@lion/listbox/types/ListboxMixinTypes').ListboxHost} ListboxHost
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { LionSelectRich } el
|
||||||
|
*/
|
||||||
|
function getSelectRichMembers(el) {
|
||||||
|
const obj = getListboxMembers(el);
|
||||||
|
// eslint-disable-next-line no-return-assign
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
// @ts-ignore [allow-protected] in test
|
||||||
|
...{ _invokerNode: el._invokerNode, _overlayCtrl: el._overlayCtrl },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
|
|
@ -24,43 +43,14 @@ import '@lion/select-rich/define';
|
||||||
|
|
||||||
const fixture = /** @type {(arg: TemplateResult) => Promise<LionSelectRich>} */ (_fixture);
|
const fixture = /** @type {(arg: TemplateResult) => Promise<LionSelectRich>} */ (_fixture);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {LionSelectRich} lionSelectEl
|
|
||||||
*/
|
|
||||||
function getProtectedMembers(lionSelectEl) {
|
|
||||||
// @ts-ignore protected members allowed in test
|
|
||||||
const {
|
|
||||||
_invokerNode: invoker,
|
|
||||||
// @ts-ignore
|
|
||||||
_feedbackNode: feedback,
|
|
||||||
// @ts-ignore
|
|
||||||
_labelNode: label,
|
|
||||||
// @ts-ignore
|
|
||||||
_helpTextNode: helpText,
|
|
||||||
// @ts-ignore
|
|
||||||
_listboxNode: listbox,
|
|
||||||
// @ts-ignore
|
|
||||||
_overlayCtrl: overlay,
|
|
||||||
} = lionSelectEl;
|
|
||||||
return {
|
|
||||||
invoker,
|
|
||||||
feedback,
|
|
||||||
label,
|
|
||||||
helpText,
|
|
||||||
listbox,
|
|
||||||
overlay,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('lion-select-rich', () => {
|
describe('lion-select-rich', () => {
|
||||||
it('clicking the label should focus the invoker', async () => {
|
it('clicking the label should focus the invoker', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich label="foo"> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich label="foo"> </lion-select-rich> `);
|
||||||
expect(document.activeElement === document.body).to.be.true;
|
expect(document.activeElement === document.body).to.be.true;
|
||||||
const { label } = getProtectedMembers(el);
|
const { _labelNode, _invokerNode } = getSelectRichMembers(el);
|
||||||
label.click();
|
_labelNode.click();
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
expect(document.activeElement === _invokerNode).to.be.true;
|
||||||
expect(document.activeElement === el._invokerNode).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks the first enabled option', async () => {
|
it('checks the first enabled option', async () => {
|
||||||
|
|
@ -95,19 +85,18 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
const { invoker } = getProtectedMembers(el);
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
expect(invoker.selectedElement).to.be.undefined;
|
expect(_invokerNode.selectedElement).to.be.undefined;
|
||||||
expect(el.modelValue).to.equal('');
|
expect(el.modelValue).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Invoker', () => {
|
describe('Invoker', () => {
|
||||||
it('generates an lion-select-invoker if no invoker is provided', async () => {
|
it('generates an lion-select-invoker if no invoker is provided', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
||||||
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode).to.exist;
|
||||||
expect(el._invokerNode).to.exist;
|
expect(_invokerNode).to.be.instanceOf(LionSelectInvoker);
|
||||||
// @ts-ignore allow protected access in tests
|
|
||||||
expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the first option as the selectedElement if no option is checked', async () => {
|
it('sets the first option as the selectedElement if no option is checked', async () => {
|
||||||
|
|
@ -117,9 +106,9 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.selectedElement).dom.to.equal(options[0]);
|
||||||
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs the selected element to the invoker', async () => {
|
it('syncs the selected element to the invoker', async () => {
|
||||||
|
|
@ -129,14 +118,13 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
const options = el.querySelectorAll('lion-option');
|
const options = el.querySelectorAll('lion-option');
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.selectedElement).dom.to.equal(options[1]);
|
||||||
expect(el._invokerNode.selectedElement).dom.to.equal(options[1]);
|
|
||||||
|
|
||||||
el.checkedIndex = 0;
|
el.checkedIndex = 0;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.selectedElement).dom.to.equal(options[0]);
|
||||||
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates readonly to the invoker', async () => {
|
it('delegates readonly to the invoker', async () => {
|
||||||
|
|
@ -146,10 +134,9 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
expect(el.hasAttribute('readonly')).to.be.true;
|
expect(el.hasAttribute('readonly')).to.be.true;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.hasAttribute('readonly')).to.be.true;
|
||||||
expect(el._invokerNode.hasAttribute('readonly')).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates singleOption to the invoker', async () => {
|
it('delegates singleOption to the invoker', async () => {
|
||||||
|
|
@ -158,10 +145,9 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
expect(el.singleOption).to.be.true;
|
expect(el.singleOption).to.be.true;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.hasAttribute('single-option')).to.be.true;
|
||||||
expect(el._invokerNode.hasAttribute('single-option')).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the invoker when the selected element is the same but the modelValue was updated asynchronously', async () => {
|
it('updates the invoker when the selected element is the same but the modelValue was updated asynchronously', async () => {
|
||||||
|
|
@ -189,14 +175,16 @@ describe('lion-select-rich', () => {
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
expect(el._invokerNode.shadowRoot.firstElementChild.textContent).to.equal('10');
|
const firstChild = /** @type {HTMLElement} */ (
|
||||||
|
/** @type {ShadowRoot} */ (_invokerNode.shadowRoot).firstElementChild
|
||||||
|
);
|
||||||
|
expect(firstChild.textContent).to.equal('10');
|
||||||
|
|
||||||
firstOption.modelValue = { value: 30, checked: true };
|
firstOption.modelValue = { value: 30, checked: true };
|
||||||
await firstOption.updateComplete;
|
await firstOption.updateComplete;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(firstChild.textContent).to.equal('30');
|
||||||
expect(el._invokerNode.shadowRoot.firstElementChild.textContent).to.equal('30');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: wrong values in safari/webkit even though this passes in the "real" debug browsers
|
// FIXME: wrong values in safari/webkit even though this passes in the "real" debug browsers
|
||||||
|
|
@ -210,20 +198,21 @@ describe('lion-select-rich', () => {
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const { invoker } = getProtectedMembers(el);
|
const { _invokerNode, _inputNode } = getSelectRichMembers(el);
|
||||||
expect(invoker.clientWidth).to.equal(options[1].clientWidth);
|
|
||||||
|
expect(_invokerNode.clientWidth).to.equal(options[1].clientWidth);
|
||||||
|
|
||||||
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
||||||
newOption.choiceValue = 30;
|
newOption.choiceValue = 30;
|
||||||
newOption.textContent = '30 with longer label';
|
newOption.textContent = '30 with longer label';
|
||||||
|
|
||||||
el._inputNode.appendChild(newOption);
|
_inputNode.appendChild(newOption);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(invoker.clientWidth).to.equal(options[2].clientWidth);
|
expect(_invokerNode.clientWidth).to.equal(options[2].clientWidth);
|
||||||
|
|
||||||
el._inputNode.removeChild(newOption);
|
_inputNode.removeChild(newOption);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(invoker.clientWidth).to.equal(options[1].clientWidth);
|
expect(_invokerNode.clientWidth).to.equal(options[1].clientWidth);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -235,16 +224,16 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
it('shows/hides the listbox via opened attribute', async () => {
|
it('shows/hides the listbox via opened attribute', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
||||||
|
const { _overlayCtrl } = getSelectRichMembers(el);
|
||||||
|
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.isShown).to.be.true;
|
||||||
expect(el._overlayCtrl.isShown).to.be.true;
|
|
||||||
|
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // safari takes a little longer
|
await el.updateComplete; // safari takes a little longer
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.isShown).to.be.false;
|
||||||
expect(el._overlayCtrl.isShown).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs opened state with overlay shown', async () => {
|
it('syncs opened state with overlay shown', async () => {
|
||||||
|
|
@ -262,21 +251,18 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
it('will focus the listbox on open and invoker on close', async () => {
|
it('will focus the listbox on open and invoker on close', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
|
||||||
// @ts-ignore allow protected access in tests
|
const { _overlayCtrl, _listboxNode, _invokerNode } = getSelectRichMembers(el);
|
||||||
await el._overlayCtrl.show();
|
|
||||||
|
await _overlayCtrl.show();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(document.activeElement === _listboxNode).to.be.true;
|
||||||
expect(document.activeElement === el._listboxNode).to.be.true;
|
expect(document.activeElement === _invokerNode).to.be.false;
|
||||||
// @ts-ignore allow protected access in tests
|
|
||||||
expect(document.activeElement === el._invokerNode).to.be.false;
|
|
||||||
|
|
||||||
el.opened = false;
|
el.opened = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // safari takes a little longer
|
await el.updateComplete; // safari takes a little longer
|
||||||
// @ts-ignore allow protected access in tests
|
expect(document.activeElement === _listboxNode).to.be.false;
|
||||||
expect(document.activeElement === el._listboxNode).to.be.false;
|
expect(document.activeElement === _invokerNode).to.be.true;
|
||||||
// @ts-ignore allow protected access in tests
|
|
||||||
expect(document.activeElement === el._invokerNode).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens the listbox with checked option as active', async () => {
|
it('opens the listbox with checked option as active', async () => {
|
||||||
|
|
@ -286,8 +272,9 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
// @ts-ignore allow protected access in tests
|
const { _overlayCtrl } = getSelectRichMembers(el);
|
||||||
await el._overlayCtrl.show();
|
|
||||||
|
await _overlayCtrl.show();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
const options = el.formElements;
|
const options = el.formElements;
|
||||||
|
|
||||||
|
|
@ -302,6 +289,7 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode: _invokerNodeReadOnly } = getSelectRichMembers(elReadOnly);
|
||||||
|
|
||||||
const elDisabled = await fixture(html`
|
const elDisabled = await fixture(html`
|
||||||
<lion-select-rich disabled>
|
<lion-select-rich disabled>
|
||||||
|
|
@ -309,25 +297,24 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode: _invokerNodeDisabled } = getSelectRichMembers(elDisabled);
|
||||||
|
|
||||||
const elSingleoption = await fixture(html`
|
const elSingleoption = await fixture(html`
|
||||||
<lion-select-rich>
|
<lion-select-rich>
|
||||||
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _invokerNode: _invokerNodeSingleOption } = getSelectRichMembers(elSingleoption);
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
_invokerNodeReadOnly.click();
|
||||||
elReadOnly._invokerNode.click();
|
|
||||||
await elReadOnly.updateComplete;
|
await elReadOnly.updateComplete;
|
||||||
expect(elReadOnly.opened).to.be.false;
|
expect(elReadOnly.opened).to.be.false;
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
_invokerNodeDisabled.click();
|
||||||
elDisabled._invokerNode.click();
|
|
||||||
await elDisabled.updateComplete;
|
await elDisabled.updateComplete;
|
||||||
expect(elDisabled.opened).to.be.false;
|
expect(elDisabled.opened).to.be.false;
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
_invokerNodeSingleOption.click();
|
||||||
elSingleoption._invokerNode.click();
|
|
||||||
await elSingleoption.updateComplete;
|
await elSingleoption.updateComplete;
|
||||||
expect(elSingleoption.opened).to.be.false;
|
expect(elSingleoption.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
@ -340,14 +327,13 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _overlayCtrl } = getSelectRichMembers(el);
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should override the inheritsWidth prop when no default selected feature is used', async () => {
|
it('should override the inheritsWidth prop when no default selected feature is used', async () => {
|
||||||
|
|
@ -359,20 +345,17 @@ describe('lion-select-rich', () => {
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { overlay } = getProtectedMembers(el);
|
const { _overlayCtrl } = getSelectRichMembers(el);
|
||||||
|
|
||||||
// The default is min, so we override that behavior here
|
// The default is min, so we override that behavior here
|
||||||
// @ts-ignore allow protected access in tests
|
_overlayCtrl.updateConfig({ inheritsReferenceWidth: 'full' });
|
||||||
overlay.updateConfig({ inheritsReferenceWidth: 'full' });
|
|
||||||
el._initialInheritsReferenceWidth = 'full';
|
el._initialInheritsReferenceWidth = 'full';
|
||||||
|
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('full');
|
||||||
expect(overlay.inheritsReferenceWidth).to.equal('full');
|
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// Opens while hasNoDefaultSelected = true, so we expect an override
|
// Opens while hasNoDefaultSelected = true, so we expect an override
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('min');
|
||||||
expect(overlay.inheritsReferenceWidth).to.equal('min');
|
|
||||||
|
|
||||||
// Emulate selecting hotpink, it closing, and opening it again
|
// Emulate selecting hotpink, it closing, and opening it again
|
||||||
el.modelValue = 'hotpink';
|
el.modelValue = 'hotpink';
|
||||||
|
|
@ -382,11 +365,10 @@ describe('lion-select-rich', () => {
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // safari takes a little longer
|
await el.updateComplete; // safari takes a little longer
|
||||||
await overlay._showComplete;
|
await _overlayCtrl._showComplete;
|
||||||
|
|
||||||
// noDefaultSelected will now flip the override back to what was the initial reference width
|
// noDefaultSelected will now flip the override back to what was the initial reference width
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('full');
|
||||||
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have singleOption only if there is exactly one option', async () => {
|
it('should have singleOption only if there is exactly one option', async () => {
|
||||||
|
|
@ -396,27 +378,26 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const { _inputNode, _invokerNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
expect(el.singleOption).to.be.false;
|
expect(el.singleOption).to.be.false;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.singleOption).to.be.false;
|
||||||
expect(el._invokerNode.singleOption).to.be.false;
|
|
||||||
|
|
||||||
const optionELm = el.formElements[0];
|
const optionELm = el.formElements[0];
|
||||||
// @ts-ignore allow protected access in tests
|
|
||||||
optionELm.parentNode.removeChild(optionELm);
|
optionELm.parentNode.removeChild(optionELm);
|
||||||
el.requestUpdate();
|
el.requestUpdate();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.singleOption).to.be.true;
|
expect(el.singleOption).to.be.true;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.singleOption).to.be.true;
|
||||||
expect(el._invokerNode.singleOption).to.be.true;
|
|
||||||
|
|
||||||
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
||||||
newOption.choiceValue = 30;
|
newOption.choiceValue = 30;
|
||||||
el._inputNode.appendChild(newOption);
|
_inputNode.appendChild(newOption);
|
||||||
el.requestUpdate();
|
el.requestUpdate();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.singleOption).to.be.false;
|
expect(el.singleOption).to.be.false;
|
||||||
// @ts-ignore allow protected access in tests
|
expect(_invokerNode.singleOption).to.be.false;
|
||||||
expect(el._invokerNode.singleOption).to.be.false;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -432,32 +413,32 @@ describe('lion-select-rich', () => {
|
||||||
describe('Keyboard navigation', () => {
|
describe('Keyboard navigation', () => {
|
||||||
it('opens the listbox with [Enter] key via click handler', async () => {
|
it('opens the listbox with [Enter] key via click handler', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
||||||
// @ts-ignore allow protected access in tests
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
el._invokerNode.click();
|
_invokerNode.click();
|
||||||
await aTimeout(0);
|
await aTimeout(0);
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens the listbox with [ ](Space) key via click handler', async () => {
|
it('opens the listbox with [ ](Space) key via click handler', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
||||||
// @ts-ignore allow protected access in tests
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
el._invokerNode.click();
|
_invokerNode.click();
|
||||||
await aTimeout(0);
|
await aTimeout(0);
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes the listbox with [Escape] key once opened', async () => {
|
it('closes the listbox with [Escape] key once opened', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
||||||
// @ts-ignore allow protected access in tests
|
const { _listboxNode } = getSelectRichMembers(el);
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes the listbox with [Tab] key once opened', async () => {
|
it('closes the listbox with [Tab] key once opened', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
||||||
// tab can only be caught via keydown
|
// tab can only be caught via keydown
|
||||||
// @ts-ignore allow protected access in tests
|
const { _listboxNode } = getSelectRichMembers(el);
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -466,8 +447,8 @@ describe('lion-select-rich', () => {
|
||||||
it('opens the listbox via click on invoker', async () => {
|
it('opens the listbox via click on invoker', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
// @ts-ignore allow protected access in tests
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
el._invokerNode.click();
|
_invokerNode.click();
|
||||||
await nextFrame(); // reflection of click takes some time
|
await nextFrame(); // reflection of click takes some time
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
@ -487,8 +468,8 @@ describe('lion-select-rich', () => {
|
||||||
describe('Keyboard navigation Windows', () => {
|
describe('Keyboard navigation Windows', () => {
|
||||||
it('closes the listbox with [Enter] key once opened', async () => {
|
it('closes the listbox with [Enter] key once opened', async () => {
|
||||||
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
|
||||||
// @ts-ignore allow protected access in tests
|
const { _listboxNode } = getSelectRichMembers(el);
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -501,12 +482,12 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
|
const { _listboxNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
// changes active but not checked
|
// changes active but not checked
|
||||||
el.activeIndex = 1;
|
el.activeIndex = 1;
|
||||||
expect(el.checkedIndex).to.equal(0);
|
expect(el.checkedIndex).to.equal(0);
|
||||||
// @ts-ignore allow protected access in tests
|
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||||
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
|
||||||
expect(el.opened).to.be.false;
|
expect(el.opened).to.be.false;
|
||||||
expect(el.checkedIndex).to.equal(1);
|
expect(el.checkedIndex).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
@ -538,26 +519,26 @@ describe('lion-select-rich', () => {
|
||||||
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
const { invoker, feedback, label, helpText } = getProtectedMembers(el);
|
const { _invokerNode, _feedbackNode, _labelNode, _helpTextNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
expect(invoker.getAttribute('aria-labelledby')).to.contain(label.id);
|
expect(_invokerNode.getAttribute('aria-labelledby')).to.contain(_labelNode.id);
|
||||||
expect(invoker.getAttribute('aria-labelledby')).to.contain(invoker.id);
|
expect(_invokerNode.getAttribute('aria-labelledby')).to.contain(_invokerNode.id);
|
||||||
expect(invoker.getAttribute('aria-describedby')).to.contain(helpText.id);
|
expect(_invokerNode.getAttribute('aria-describedby')).to.contain(_helpTextNode.id);
|
||||||
expect(invoker.getAttribute('aria-describedby')).to.contain(feedback.id);
|
expect(_invokerNode.getAttribute('aria-describedby')).to.contain(_feedbackNode.id);
|
||||||
expect(invoker.getAttribute('aria-haspopup')).to.equal('listbox');
|
expect(_invokerNode.getAttribute('aria-haspopup')).to.equal('listbox');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('notifies when the listbox is expanded or not', async () => {
|
it('notifies when the listbox is expanded or not', async () => {
|
||||||
// smoke test for overlay functionality
|
// smoke test for overlay functionality
|
||||||
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
|
||||||
const { invoker } = getProtectedMembers(el);
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
expect(invoker.getAttribute('aria-expanded')).to.equal('false');
|
expect(_invokerNode.getAttribute('aria-expanded')).to.equal('false');
|
||||||
el.opened = true;
|
el.opened = true;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
await el.updateComplete; // need 2 awaits as overlay.show is an async function
|
||||||
|
|
||||||
expect(invoker.getAttribute('aria-expanded')).to.equal('true');
|
expect(_invokerNode.getAttribute('aria-expanded')).to.equal('true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -617,21 +598,21 @@ describe('lion-select-rich', () => {
|
||||||
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector('lion-select-rich')
|
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector('lion-select-rich')
|
||||||
);
|
);
|
||||||
|
|
||||||
const { invoker, listbox } = getProtectedMembers(selectRich);
|
const { _invokerNode, _listboxNode } = getSelectRichMembers(selectRich);
|
||||||
|
|
||||||
expect(selectRich.checkedIndex).to.equal(1);
|
expect(selectRich.checkedIndex).to.equal(1);
|
||||||
expect(selectRich.modelValue).to.equal('hotpink');
|
expect(selectRich.modelValue).to.equal('hotpink');
|
||||||
expect(/** @type {LionOption} */ (invoker.selectedElement).value).to.equal('hotpink');
|
expect(/** @type {LionOption} */ (_invokerNode.selectedElement).value).to.equal('hotpink');
|
||||||
|
|
||||||
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
|
||||||
newOption.modelValue = { checked: false, value: 'blue' };
|
newOption.modelValue = { checked: false, value: 'blue' };
|
||||||
newOption.textContent = 'Blue';
|
newOption.textContent = 'Blue';
|
||||||
const hotpinkEl = listbox.children[1];
|
const hotpinkEl = _listboxNode.children[1];
|
||||||
hotpinkEl.insertAdjacentElement('beforebegin', newOption);
|
hotpinkEl.insertAdjacentElement('beforebegin', newOption);
|
||||||
|
|
||||||
expect(selectRich.checkedIndex).to.equal(2);
|
expect(selectRich.checkedIndex).to.equal(2);
|
||||||
expect(selectRich.modelValue).to.equal('hotpink');
|
expect(selectRich.modelValue).to.equal('hotpink');
|
||||||
expect(/** @type {LionOption} */ (invoker.selectedElement).value).to.equal('hotpink');
|
expect(/** @type {LionOption} */ (_invokerNode.selectedElement).value).to.equal('hotpink');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -669,11 +650,11 @@ describe('lion-select-rich', () => {
|
||||||
</${mySelectTag}>
|
</${mySelectTag}>
|
||||||
`);
|
`);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
// @ts-ignore allow protected member access in tests
|
const { _overlayCtrl } = getSelectRichMembers(el);
|
||||||
expect(el._overlayCtrl.placementMode).to.equal('global');
|
|
||||||
|
expect(_overlayCtrl.placementMode).to.equal('global');
|
||||||
el.dispatchEvent(new Event('switch'));
|
el.dispatchEvent(new Event('switch'));
|
||||||
// @ts-ignore allow protected member access in tests
|
expect(_overlayCtrl.placementMode).to.equal('local');
|
||||||
expect(el._overlayCtrl.placementMode).to.equal('local');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports putting a placeholder template when there is no default selection initially', async () => {
|
it('supports putting a placeholder template when there is no default selection initially', async () => {
|
||||||
|
|
@ -707,10 +688,10 @@ describe('lion-select-rich', () => {
|
||||||
|
|
||||||
</${selectTag}>
|
</${selectTag}>
|
||||||
`);
|
`);
|
||||||
const { invoker } = getProtectedMembers(el);
|
const { _invokerNode } = getSelectRichMembers(el);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
/** @type {ShadowRoot} */ (invoker.shadowRoot).getElementById('content-wrapper'),
|
/** @type {ShadowRoot} */ (_invokerNode.shadowRoot).getElementById('content-wrapper'),
|
||||||
).dom.to.equal(`<div id="content-wrapper">Please select an option..</div>`);
|
).dom.to.equal(`<div id="content-wrapper">Please select an option..</div>`);
|
||||||
expect(el.modelValue).to.equal('');
|
expect(el.modelValue).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,16 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
||||||
/**
|
/**
|
||||||
* Input node here is the lion-switch-button, which is not compatible with LionField _inputNode --> HTMLInputElement
|
* 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
|
* Therefore we do a full override and typecast to an intersection type that includes LionSwitchButton
|
||||||
* @returns {LionSwitchButton}
|
* @type {LionSwitchButton}
|
||||||
|
* @protected
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore [editor]: prevents vscode from complaining
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return /** @type {LionSwitchButton} */ (Array.from(this.children).find(
|
return /** @type {LionSwitchButton} */ (Array.from(this.children).find(
|
||||||
el => el.slot === 'input',
|
el => el.slot === 'input',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ import sinon from 'sinon';
|
||||||
import { Validator } from '@lion/form-core';
|
import { Validator } from '@lion/form-core';
|
||||||
import { LionSwitch } from '@lion/switch';
|
import { LionSwitch } from '@lion/switch';
|
||||||
import '@lion/switch/define';
|
import '@lion/switch/define';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @typedef {import('../src/LionSwitchButton').LionSwitchButton} LionSwitchButton
|
||||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||||
|
* @typedef {import('@lion/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const IsTrue = class extends Validator {
|
const IsTrue = class extends Validator {
|
||||||
|
|
@ -19,13 +22,11 @@ const IsTrue = class extends Validator {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {LionSwitch} lionSwitchEl
|
* @param { LionSwitch } el
|
||||||
*/
|
*/
|
||||||
function getProtectedMembers(lionSwitchEl) {
|
function getSwitchMembers(el) {
|
||||||
return {
|
const obj = getFormControlMembers(/** @type { * & FormControlHost } */ (el));
|
||||||
// @ts-ignore
|
return { ...obj, _inputNode: /** @type {LionSwitchButton} */ (obj._inputNode) };
|
||||||
inputNode: lionSwitchEl._inputNode,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixture = /** @type {(arg: TemplateResult) => Promise<LionSwitch>} */ (_fixture);
|
const fixture = /** @type {(arg: TemplateResult) => Promise<LionSwitch>} */ (_fixture);
|
||||||
|
|
@ -38,40 +39,46 @@ describe('lion-switch', () => {
|
||||||
|
|
||||||
it('clicking the label should toggle the checked state', async () => {
|
it('clicking the label should toggle the checked state', async () => {
|
||||||
const el = await fixture(html`<lion-switch label="Enable Setting"></lion-switch>`);
|
const el = await fixture(html`<lion-switch label="Enable Setting"></lion-switch>`);
|
||||||
el._labelNode.click();
|
const { _labelNode } = getFormControlMembers(el);
|
||||||
|
_labelNode.click();
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
el._labelNode.click();
|
_labelNode.click();
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking the label should not toggle the checked state when disabled', async () => {
|
it('clicking the label should not toggle the checked state when disabled', async () => {
|
||||||
const el = await fixture(html`<lion-switch disabled label="Enable Setting"></lion-switch>`);
|
const el = await fixture(html`<lion-switch disabled label="Enable Setting"></lion-switch>`);
|
||||||
el._labelNode.click();
|
const { _labelNode } = getFormControlMembers(el);
|
||||||
|
_labelNode.click();
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking the label should focus the toggle button', async () => {
|
it('clicking the label should focus the toggle button', async () => {
|
||||||
const el = await fixture(html`<lion-switch label="Enable Setting"></lion-switch>`);
|
const el = await fixture(html`<lion-switch label="Enable Setting"></lion-switch>`);
|
||||||
el._labelNode.click();
|
const { _inputNode, _labelNode } = getSwitchMembers(el);
|
||||||
expect(document.activeElement).to.equal(el._inputNode);
|
|
||||||
|
_labelNode.click();
|
||||||
|
expect(document.activeElement).to.equal(_inputNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking the label should not focus the toggle button when disabled', async () => {
|
it('clicking the label should not focus the toggle button when disabled', async () => {
|
||||||
const el = await fixture(html`<lion-switch disabled label="Enable Setting"></lion-switch>`);
|
const el = await fixture(html`<lion-switch disabled label="Enable Setting"></lion-switch>`);
|
||||||
el._labelNode.click();
|
const { _inputNode, _labelNode } = getSwitchMembers(el);
|
||||||
expect(document.activeElement).to.not.equal(el._inputNode);
|
|
||||||
|
_labelNode.click();
|
||||||
|
expect(document.activeElement).to.not.equal(_inputNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sync its "disabled" state to child button', async () => {
|
it('should sync its "disabled" state to child button', async () => {
|
||||||
const el = await fixture(html`<lion-switch disabled></lion-switch>`);
|
const el = await fixture(html`<lion-switch disabled></lion-switch>`);
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getSwitchMembers(el);
|
||||||
expect(inputNode.disabled).to.be.true;
|
expect(_inputNode.disabled).to.be.true;
|
||||||
expect(inputNode.hasAttribute('disabled')).to.be.true;
|
expect(_inputNode.hasAttribute('disabled')).to.be.true;
|
||||||
el.disabled = false;
|
el.disabled = false;
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
await el.updateComplete; // safari takes longer
|
await el.updateComplete; // safari takes longer
|
||||||
expect(inputNode.disabled).to.be.false;
|
expect(_inputNode.disabled).to.be.false;
|
||||||
expect(inputNode.hasAttribute('disabled')).to.be.false;
|
expect(_inputNode.hasAttribute('disabled')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is hidden when attribute hidden is true', async () => {
|
it('is hidden when attribute hidden is true', async () => {
|
||||||
|
|
@ -81,9 +88,9 @@ describe('lion-switch', () => {
|
||||||
|
|
||||||
it('should sync its "checked" state to child button', async () => {
|
it('should sync its "checked" state to child button', async () => {
|
||||||
const uncheckedEl = await fixture(html`<lion-switch></lion-switch>`);
|
const uncheckedEl = await fixture(html`<lion-switch></lion-switch>`);
|
||||||
const { inputNode: uncheckeInputNode } = getProtectedMembers(uncheckedEl);
|
const { _inputNode: uncheckeInputNode } = getSwitchMembers(uncheckedEl);
|
||||||
const checkedEl = await fixture(html`<lion-switch checked></lion-switch>`);
|
const checkedEl = await fixture(html`<lion-switch checked></lion-switch>`);
|
||||||
const { inputNode: checkeInputNode } = getProtectedMembers(checkedEl);
|
const { _inputNode: checkeInputNode } = getSwitchMembers(checkedEl);
|
||||||
expect(uncheckeInputNode.checked).to.be.false;
|
expect(uncheckeInputNode.checked).to.be.false;
|
||||||
expect(checkeInputNode.checked).to.be.true;
|
expect(checkeInputNode.checked).to.be.true;
|
||||||
uncheckedEl.checked = true;
|
uncheckedEl.checked = true;
|
||||||
|
|
@ -96,8 +103,8 @@ describe('lion-switch', () => {
|
||||||
|
|
||||||
it('should sync "checked" state received from child button', async () => {
|
it('should sync "checked" state received from child button', async () => {
|
||||||
const el = await fixture(html`<lion-switch></lion-switch>`);
|
const el = await fixture(html`<lion-switch></lion-switch>`);
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode } = getSwitchMembers(el);
|
||||||
const button = inputNode;
|
const button = _inputNode;
|
||||||
expect(el.checked).to.be.false;
|
expect(el.checked).to.be.false;
|
||||||
button.click();
|
button.click();
|
||||||
expect(el.checked).to.be.true;
|
expect(el.checked).to.be.true;
|
||||||
|
|
@ -123,10 +130,10 @@ describe('lion-switch', () => {
|
||||||
it('should dispatch "checked-changed" event when toggled via button or label', async () => {
|
it('should dispatch "checked-changed" event when toggled via button or label', async () => {
|
||||||
const handlerSpy = sinon.spy();
|
const handlerSpy = sinon.spy();
|
||||||
const el = await fixture(html`<lion-switch .choiceValue=${'foo'}></lion-switch>`);
|
const el = await fixture(html`<lion-switch .choiceValue=${'foo'}></lion-switch>`);
|
||||||
const { inputNode } = getProtectedMembers(el);
|
const { _inputNode, _labelNode } = getSwitchMembers(el);
|
||||||
el.addEventListener('checked-changed', handlerSpy);
|
el.addEventListener('checked-changed', handlerSpy);
|
||||||
inputNode.click();
|
_inputNode.click();
|
||||||
el._labelNode.click();
|
_labelNode.click();
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(handlerSpy.callCount).to.equal(2);
|
expect(handlerSpy.callCount).to.equal(2);
|
||||||
const checkCall = /** @param {import('sinon').SinonSpyCall} call */ call => {
|
const checkCall = /** @param {import('sinon').SinonSpyCall} call */ call => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable max-classes-per-file */
|
/* 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!
|
// @ts-expect-error [external]: 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 autosize from 'autosize/src/autosize.js';
|
||||||
import { LionField, NativeTextFieldMixin } from '@lion/form-core';
|
import { LionField, NativeTextFieldMixin } from '@lion/form-core';
|
||||||
import { css } from '@lion/core';
|
import { css } from '@lion/core';
|
||||||
|
|
@ -7,6 +7,7 @@ import { css } from '@lion/core';
|
||||||
class LionFieldWithTextArea extends LionField {
|
class LionFieldWithTextArea extends LionField {
|
||||||
/**
|
/**
|
||||||
* @returns {HTMLTextAreaElement}
|
* @returns {HTMLTextAreaElement}
|
||||||
|
* @protected
|
||||||
*/
|
*/
|
||||||
get _inputNode() {
|
get _inputNode() {
|
||||||
return /** @type {HTMLTextAreaElement} */ (Array.from(this.children).find(
|
return /** @type {HTMLTextAreaElement} */ (Array.from(this.children).find(
|
||||||
|
|
@ -21,6 +22,7 @@ class LionFieldWithTextArea extends LionField {
|
||||||
* @customElement lion-textarea
|
* @customElement lion-textarea
|
||||||
*/
|
*/
|
||||||
export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
||||||
|
/** @type {any} */
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
maxRows: {
|
maxRows: {
|
||||||
|
|
@ -43,7 +45,6 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { aTimeout, expect, fixture as _fixture, html } from '@open-wc/testing';
|
import { aTimeout, expect, fixture as _fixture, html } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import '@lion/textarea/define';
|
import '@lion/textarea/define';
|
||||||
|
import { getFormControlMembers } from '@lion/form-core/test-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../src/LionTextarea').LionTextarea} LionTextarea
|
* @typedef {import('../src/LionTextarea').LionTextarea} LionTextarea
|
||||||
|
|
@ -9,16 +10,6 @@ import '@lion/textarea/define';
|
||||||
|
|
||||||
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture);
|
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture);
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {LionTextarea} lionTextareaEl
|
|
||||||
*/
|
|
||||||
function getProtectedMembers(lionTextareaEl) {
|
|
||||||
const { _inputNode: input } = lionTextareaEl;
|
|
||||||
return {
|
|
||||||
input,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasBrowserResizeSupport() {
|
function hasBrowserResizeSupport() {
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
return textarea.style.resize !== undefined;
|
return textarea.style.resize !== undefined;
|
||||||
|
|
@ -41,25 +32,25 @@ describe('<lion-textarea>', () => {
|
||||||
|
|
||||||
it('has .readOnly=false .rows=2 and rows="2" by default', async () => {
|
it('has .readOnly=false .rows=2 and rows="2" by default', async () => {
|
||||||
const el = await fixture(`<lion-textarea>foo</lion-textarea>`);
|
const el = await fixture(`<lion-textarea>foo</lion-textarea>`);
|
||||||
const { input } = getProtectedMembers(el);
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
|
|
||||||
expect(el.rows).to.equal(2);
|
expect(el.rows).to.equal(2);
|
||||||
expect(el.getAttribute('rows')).to.be.equal('2');
|
expect(el.getAttribute('rows')).to.be.equal('2');
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(input.rows).to.equal(2);
|
expect(_inputNode.rows).to.equal(2);
|
||||||
expect(input.getAttribute('rows')).to.be.equal('2');
|
expect(_inputNode.getAttribute('rows')).to.be.equal('2');
|
||||||
expect(el.readOnly).to.be.false;
|
expect(el.readOnly).to.be.false;
|
||||||
expect(input.hasAttribute('readonly')).to.be.false;
|
expect(_inputNode.hasAttribute('readonly')).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sync rows down to the native textarea', async () => {
|
it('sync rows down to the native textarea', async () => {
|
||||||
const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`);
|
const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`);
|
||||||
const { input } = getProtectedMembers(el);
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
expect(el.rows).to.equal(8);
|
expect(el.rows).to.equal(8);
|
||||||
expect(el.getAttribute('rows')).to.be.equal('8');
|
expect(el.getAttribute('rows')).to.be.equal('8');
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(input.rows).to.equal(8);
|
expect(_inputNode.rows).to.equal(8);
|
||||||
expect(input.getAttribute('rows')).to.be.equal('8');
|
expect(_inputNode.getAttribute('rows')).to.be.equal('8');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sync readOnly to the native textarea', async () => {
|
it('sync readOnly to the native textarea', async () => {
|
||||||
|
|
@ -74,8 +65,8 @@ describe('<lion-textarea>', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = await fixture(`<lion-textarea></lion-textarea>`);
|
const el = await fixture(`<lion-textarea></lion-textarea>`);
|
||||||
const { input } = getProtectedMembers(el);
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
const computedStyle = window.getComputedStyle(input);
|
const computedStyle = window.getComputedStyle(_inputNode);
|
||||||
expect(computedStyle.resize).to.equal('none');
|
expect(computedStyle.resize).to.equal('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -155,14 +146,14 @@ describe('<lion-textarea>', () => {
|
||||||
|
|
||||||
it('has an attribute that can be used to set the placeholder text of the textarea', async () => {
|
it('has an attribute that can be used to set the placeholder text of the textarea', async () => {
|
||||||
const el = await fixture(`<lion-textarea placeholder="text"></lion-textarea>`);
|
const el = await fixture(`<lion-textarea placeholder="text"></lion-textarea>`);
|
||||||
const { input } = getProtectedMembers(el);
|
const { _inputNode } = getFormControlMembers(el);
|
||||||
expect(el.getAttribute('placeholder')).to.equal('text');
|
expect(el.getAttribute('placeholder')).to.equal('text');
|
||||||
expect(input.getAttribute('placeholder')).to.equal('text');
|
expect(_inputNode.getAttribute('placeholder')).to.equal('text');
|
||||||
|
|
||||||
el.placeholder = 'foo';
|
el.placeholder = 'foo';
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
expect(el.getAttribute('placeholder')).to.equal('foo');
|
expect(el.getAttribute('placeholder')).to.equal('foo');
|
||||||
expect(input.getAttribute('placeholder')).to.equal('foo');
|
expect(_inputNode.getAttribute('placeholder')).to.equal('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fires resize textarea when a visibility change has been detected', async () => {
|
it('fires resize textarea when a visibility change has been detected', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue