Merge pull request #1328 from ing-bank/fix/manyIssues

Types and aria-live assertive fixes
This commit is contained in:
Thijs Louisse 2021-04-12 15:54:57 +02:00 committed by GitHub
commit a100fb43df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 1483 additions and 1163 deletions

View 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

View 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

View file

@ -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() : '';
/** /**

View file

@ -167,7 +167,6 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
)); ));
} }
// @ts-ignore
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,

View file

@ -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

View file

@ -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);
} }
} }

View file

@ -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;

View file

@ -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');

View file

@ -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

View file

@ -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
);
} }
/** /**

View file

@ -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;
} }
/** /**

View file

@ -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;

View file

@ -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
);
} }
}; };

View file

@ -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 });
} }

View file

@ -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;
} }

View file

@ -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 => {

View file

@ -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));
} }
} }
}; };

View file

@ -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);

View file

@ -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);
} }

View file

@ -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,

View file

@ -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');
} }
} }
} }

View file

@ -1,6 +0,0 @@
export {
AlwaysInvalid,
AlwaysValid,
AsyncAlwaysValid,
AsyncAlwaysInvalid,
} from './test-helpers/ExampleValidators.js';

View 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,
};
}

View file

@ -1 +1,2 @@
export * from './ExampleValidators.js'; export * from './ExampleValidators.js';
export * from './getFormControlMembers.js';

View file

@ -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();
} }

View file

@ -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);
}); });
}); });

View file

@ -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);
}); });
}); });

View file

@ -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...

View file

@ -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([]);
}); });
}); });
} }

View file

@ -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;
});
});
}); });
} }

View file

@ -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');

View file

@ -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 */

View file

@ -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']);
}); });

View file

@ -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;
}); });

View file

@ -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();

View file

@ -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}`,
); );
}); });
}); });

View file

@ -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;

View file

@ -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');
}); });
}); });

View file

@ -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

View file

@ -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>>(

View file

@ -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>>(

View file

@ -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>>(

View file

@ -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>>(

View file

@ -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;

View file

@ -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>>(

View file

@ -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>>(

View file

@ -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>>(

View file

@ -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>>(

View file

@ -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;
} }

View file

@ -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>>(

View file

@ -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;

View file

@ -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>>(

View file

@ -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;

View file

@ -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);

View file

@ -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);
} }

View file

@ -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 () => {

View file

@ -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);
} }

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -104,7 +104,6 @@ export class LionInputStepper extends LionInput {
} }
} }
// @ts-ignore
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,

View file

@ -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"
} }
} }

View file

@ -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
} }

View 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),
};
}

View file

@ -0,0 +1 @@
export { getInputMembers } from './getInputMembers.js';

View file

@ -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);
}); });
}); });

View file

@ -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",

View file

@ -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;
} }
} }

View file

@ -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;
} }
/** /**

View 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 },
};
}

View file

@ -0,0 +1 @@
export { getListboxMembers } from './getListboxMembers.js';

View file

@ -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;
}); });

View file

@ -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;

View file

@ -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';

View file

@ -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/"
} }
} }

View file

@ -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;

View file

@ -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,

View file

@ -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());
} }
} }

View file

@ -1 +0,0 @@
export { mimicClick } from './test-helpers/mimicClick.js';

View file

@ -0,0 +1 @@
export { mimicClick } from './mimicClick.js';

View file

@ -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

View file

@ -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() {

View file

@ -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('');
}); });

View file

@ -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,

View file

@ -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 => {

View file

@ -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,

View file

@ -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 () => {