feat: private and protected types form-core

This commit is contained in:
Thijs Louisse 2021-04-12 08:56:56 +02:00 committed by Thijs Louisse
parent cc02ae2450
commit 43e4bb810c
70 changed files with 1197 additions and 957 deletions

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

@ -10,11 +10,11 @@ export class LionCheckboxGroup extends ChoiceGroupMixin(FormGroupMixin(LitElemen
this.multipleChoice = true; this.multipleChoice = true;
} }
// /** @param {import('@lion/core').PropertyValues } changedProperties */ /** @param {import('@lion/core').PropertyValues } changedProperties */
// updated(changedProperties) { updated(changedProperties) {
// super.updated(changedProperties); super.updated(changedProperties);
// if (changedProperties.has('name') && !String(this.name).match(/\[\]$/)) { if (changedProperties.has('name') && !String(this.name).match(/\[\]$/)) {
// // throw new Error('Names should end in "[]".'); throw new Error('Names should end in "[]".');
// } }
// } }
} }

View file

@ -105,7 +105,7 @@ describe('<lion-checkbox-group>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it.skip("should throw exception if name doesn't end in []", async () => { it("should throw exception if name doesn't end in []", async () => {
const el = await fixture(html`<lion-checkbox-group name="woof[]"></lion-checkbox-group>`); const el = await fixture(html`<lion-checkbox-group name="woof[]"></lion-checkbox-group>`);
el.name = 'woof'; el.name = 'woof';
let err; let err;

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

@ -180,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') {

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

@ -183,6 +183,7 @@ const FormControlMixinImplementation = superclass =>
}; };
} }
/** @protected */
get _inputNode() { get _inputNode() {
return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input')); return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input'));
} }
@ -195,6 +196,9 @@ const FormControlMixinImplementation = superclass =>
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text')); return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text'));
} }
/**
* @protected
*/
get _feedbackNode() { get _feedbackNode() {
return /** @type {LionValidationFeedback} */ (this.__getDirectSlotChild('feedback')); return /** @type {LionValidationFeedback} */ (this.__getDirectSlotChild('feedback'));
} }
@ -280,7 +284,7 @@ const FormControlMixinImplementation = superclass =>
/** @protected */ /** @protected */
_triggerInitialModelValueChangedEvent() { _triggerInitialModelValueChangedEvent() {
this.__dispatchInitialModelValueChangedEvent(); this._dispatchInitialModelValueChangedEvent();
} }
/** @protected */ /** @protected */
@ -779,7 +783,7 @@ const FormControlMixinImplementation = superclass =>
); );
} }
__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') {

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

@ -200,7 +200,7 @@ const ChoiceGroupMixinImplementation = superclass =>
*/ */
_triggerInitialModelValueChangedEvent() { _triggerInitialModelValueChangedEvent() {
this.registrationComplete.then(() => { this.registrationComplete.then(() => {
this.__dispatchInitialModelValueChangedEvent(); this._dispatchInitialModelValueChangedEvent();
}); });
} }

View file

@ -234,9 +234,9 @@ 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 // TODO: make this less fuzzy by applying these methods in LionRadio and LionCheckbox

View file

@ -77,6 +77,7 @@ const FormGroupMixinImplementation = superclass =>
}; };
} }
/** @protected */
get _inputNode() { get _inputNode() {
return this; return this;
} }
@ -183,7 +184,7 @@ const FormGroupMixinImplementation = superclass =>
*/ */
_triggerInitialModelValueChangedEvent() { _triggerInitialModelValueChangedEvent() {
this.registrationComplete.then(() => { this.registrationComplete.then(() => {
this.__dispatchInitialModelValueChangedEvent(); this._dispatchInitialModelValueChangedEvent();
}); });
} }

View file

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

@ -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,10 +4,6 @@ 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|undefined} [baseElement] * @property {typeof LitElement|undefined} [baseElement]

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);
@ -722,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;
}); });
}); });
@ -776,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;
}); });
}); });
@ -915,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 = '';
@ -1021,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']);
@ -1164,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

@ -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);
}); });
@ -510,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();
@ -925,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');
@ -938,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');
@ -1024,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']);
}); });
@ -1040,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,5 +1,6 @@
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';
@ -118,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',
)); ));
@ -141,8 +144,10 @@ 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;
}); });
@ -231,6 +236,8 @@ 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;
@ -240,7 +247,7 @@ describe('FormControlMixin', () => {
// 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-${inputId}`, `label-${inputId}`,
); );
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector( const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
@ -248,7 +255,7 @@ describe('FormControlMixin', () => {
)); ));
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
@ -259,13 +266,13 @@ describe('FormControlMixin', () => {
// 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-${inputId}`, `feedback-${inputId}`,
); );
}); });
@ -284,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.
@ -296,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
@ -315,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']);
}); });
@ -336,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'));
@ -346,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()
@ -358,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']);
}); });
}); });

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(() => {
@ -60,12 +63,15 @@ describe('<lion-field>', () => {
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 () => {
@ -79,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);
@ -106,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);

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,5 +1,5 @@
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 { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
@ -66,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.
@ -84,29 +84,56 @@ 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 get modelValue(): any | Unparseable; get modelValue(): any | Unparseable;
public set modelValue(value: 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;
/** /**
* 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;
get _inputNode(): HTMLElementWithValue; addToAriaLabelledBy(
get _labelNode(): HTMLElement; element: HTMLElement,
get _helpTextNode(): HTMLElement; customConfig?: {
get _feedbackNode(): LionValidationFeedback; idPrefix?: string | undefined;
reorder?: boolean | undefined;
},
): void;
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 _inputId: string;
protected _ariaLabelledNodes: HTMLElement[]; protected _ariaLabelledNodes: HTMLElement[];
protected _ariaDescribedNodes: HTMLElement[]; protected _ariaDescribedNodes: HTMLElement[];
@ -125,11 +152,7 @@ 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;
protected _labelTemplate(): TemplateResult; protected _labelTemplate(): TemplateResult;
@ -141,49 +164,29 @@ export declare class FormControlHost {
protected _inputGroupSuffixTemplate(): TemplateResult | typeof nothing; protected _inputGroupSuffixTemplate(): TemplateResult | typeof nothing;
protected _inputGroupAfterTemplate(): TemplateResult; protected _inputGroupAfterTemplate(): TemplateResult;
protected _feedbackTemplate(): TemplateResult; protected _feedbackTemplate(): TemplateResult;
protected _triggerInitialModelValueChangedEvent(): void; protected _triggerInitialModelValueChangedEvent(): void;
protected _enhanceLightDomClasses(): void; protected _enhanceLightDomClasses(): void;
protected _enhanceLightDomA11y(): void; protected _enhanceLightDomA11y(): void;
protected _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?: any): boolean;
protected _getAriaDescriptionElements(): HTMLElement[]; protected _getAriaDescriptionElements(): HTMLElement[];
public addToAriaLabelledBy( protected _dispatchInitialModelValueChangedEvent(): void;
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 | undefined;
__dispatchInitialModelValueChangedEvent(): void;
__repropagateChildrenInitialized: boolean | undefined;
protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void; protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
__repropagateChildrenValues(ev: CustomEvent): void;
protected _parentFormGroup: FormControlHost | undefined;
protected _repropagationCondition(target: FormControlHost): boolean; protected _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 {
formattedValue: string;
serializedValue: string;
formatOn: string;
formatOptions: FormatNumberOptions;
__preventRecursiveTrigger: boolean;
__isHandlingUserInput: boolean;
parser(v: string, opts: FormatNumberOptions): unknown; parser(v: string, opts: FormatNumberOptions): unknown;
formatter(v: unknown, opts?: FormatNumberOptions): string; formatter(v: unknown, opts?: FormatNumberOptions): string;
serializer(v: unknown): string; serializer(v: unknown): string;
deserializer(v: string): unknown; deserializer(v: string): unknown;
preprocessor(v: string): string; preprocessor(v: string): string;
formattedValue: string;
serializedValue: string;
formatOn: string;
formatOptions: FormatNumberOptions;
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 _triggerInitialModelValueChangedEvent(): void;
_getFromAllFormElements(property: string, filterCondition: Function): void;
_throwWhenInvalidChildModelValue(child: FormControlHost): void;
protected _isEmpty(): void;
_checkSingleChoiceElements(ev: Event): void;
protected _getCheckedElements(): void;
_setCheckedElements(value: any, check: boolean): void;
__setChoiceGroupTouched(): void;
__delegateNameAttribute(child: FormControlHost): void;
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
protected _oldModelValue: any; protected _oldModelValue: any;
protected _triggerInitialModelValueChangedEvent(): void;
protected _getFromAllFormElements(property: string, filterCondition: Function): void;
protected _throwWhenInvalidChildModelValue(child: FormControlHost): void;
protected _isEmpty(): void;
protected _checkSingleChoiceElements(ev: Event): void;
protected _getCheckedElements(): void;
protected _setCheckedElements(value: any, check: boolean): void;
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
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,64 +15,37 @@ export interface ChoiceInputSerializedValue {
} }
export declare class ChoiceInputHost { export declare class ChoiceInputHost {
type: string;
serializedValue: ChoiceInputSerializedValue;
checked: boolean;
get modelValue(): ChoiceInputModelValue; get modelValue(): ChoiceInputModelValue;
set modelValue(value: ChoiceInputModelValue); set modelValue(value: ChoiceInputModelValue);
serializedValue: ChoiceInputSerializedValue;
checked: boolean;
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;
get _inputNode(): HTMLElement; private __syncCheckedToInputElement(): void;
} }
export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>( export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>(

View file

@ -7,24 +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;
get _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 };
get modelValue(): { [x: string]: any };
set modelValue(value: { [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

@ -4,10 +4,8 @@ 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; protected _parentFormGroup: FormRegistrarHost | undefined;
public name: string;
} }
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,51 +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[];
_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}>
@ -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

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

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

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

@ -128,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;
@ -136,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

@ -528,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;
@ -638,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;
@ -769,7 +769,7 @@ const ListboxMixinImplementation = superclass =>
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,
}), }),
}), }),

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

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

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

@ -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,7 +28,8 @@ 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 [editor]: prevents vscode from complaining // @ts-ignore [editor]: prevents vscode from complaining
get _inputNode() { get _inputNode() {

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

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

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