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;
}
// /** @param {import('@lion/core').PropertyValues } changedProperties */
// updated(changedProperties) {
// super.updated(changedProperties);
// if (changedProperties.has('name') && !String(this.name).match(/\[\]$/)) {
// // throw new Error('Names should end in "[]".');
// }
// }
/** @param {import('@lion/core').PropertyValues } changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('name') && !String(this.name).match(/\[\]$/)) {
throw new Error('Names should end in "[]".');
}
}
}

View file

@ -105,7 +105,7 @@ describe('<lion-checkbox-group>', () => {
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>`);
el.name = 'woof';
let err;

View file

@ -1,4 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import '@lion/checkbox-group/define';
/**
@ -9,10 +10,14 @@ import '@lion/checkbox-group/define';
/**
* @param {LionCheckboxIndeterminate} el
*/
function getProtectedMembers(el) {
function getCheckboxIndeterminateMembers(el) {
const obj = getFormControlMembers(el);
return {
// @ts-ignore
subCheckboxes: el._subCheckboxes,
...obj,
...{
// @ts-ignore [allow-protected] in test
_subCheckboxes: el._subCheckboxes,
},
};
}
@ -103,10 +108,10 @@ describe('<lion-checkbox-indeterminate>', () => {
'lion-checkbox-indeterminate',
));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act
subCheckboxes[0].checked = true;
_subCheckboxes[0].checked = true;
await el.updateComplete;
// Assert
@ -127,12 +132,12 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act
subCheckboxes[0].checked = true;
subCheckboxes[1].checked = true;
subCheckboxes[2].checked = true;
_subCheckboxes[0].checked = true;
_subCheckboxes[1].checked = true;
_subCheckboxes[2].checked = true;
await el.updateComplete;
// Assert
@ -154,17 +159,17 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act
elIndeterminate._inputNode.click();
_inputNode.click();
await elIndeterminate.updateComplete;
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Assert
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(_subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(_subCheckboxes[1].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 () => {
@ -181,17 +186,17 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act
elIndeterminate._inputNode.click();
_inputNode.click();
await elIndeterminate.updateComplete;
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Assert
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(_subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(_subCheckboxes[1].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 () => {
@ -208,17 +213,17 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { _subCheckboxes, _inputNode } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act
elIndeterminate._inputNode.click();
_inputNode.click();
await elIndeterminate.updateComplete;
const elProts = getProtectedMembers(elIndeterminate);
// Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
expect(elProts.subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elProts.subCheckboxes[1].hasAttribute('checked')).to.be.false;
expect(elProts.subCheckboxes[2].hasAttribute('checked')).to.be.false;
expect(_subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(_subCheckboxes[1].hasAttribute('checked')).to.be.false;
expect(_subCheckboxes[2].hasAttribute('checked')).to.be.false;
});
it('should work as expected with siblings checkbox-indeterminate', async () => {
@ -258,27 +263,28 @@ describe('<lion-checkbox-indeterminate>', () => {
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#first-checkbox-indeterminate',
));
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#second-checkbox-indeterminate',
));
const elFirstSubCheckboxes = getCheckboxIndeterminateMembers(elFirstIndeterminate);
const elSecondSubCheckboxes = getCheckboxIndeterminateMembers(elSecondIndeterminate);
// Act - check the first sibling
elFirstIndeterminate._inputNode.click();
elFirstSubCheckboxes._inputNode.click();
await elFirstIndeterminate.updateComplete;
await elSecondIndeterminate.updateComplete;
const elFirstSubCheckboxes = getProtectedMembers(elFirstIndeterminate);
const elSecondSubCheckboxes = getProtectedMembers(elSecondIndeterminate);
// Assert - the second sibling should not be affected
expect(elFirstIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(elFirstSubCheckboxes.subCheckboxes[0].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[0].hasAttribute('checked')).to.be.true;
expect(elFirstSubCheckboxes._subCheckboxes[1].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[1].hasAttribute('checked')).to.be.false;
expect(elSecondSubCheckboxes._subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elSecondSubCheckboxes._subCheckboxes[1].hasAttribute('checked')).to.be.false;
});
it('should work as expected with nested indeterminate checkboxes', async () => {
@ -322,12 +328,13 @@ describe('<lion-checkbox-indeterminate>', () => {
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#parent-checkbox-indeterminate',
));
const elNestedSubCheckboxes = getProtectedMembers(elNestedIndeterminate);
const elParentSubCheckboxes = getProtectedMembers(elParentIndeterminate);
const elNestedSubCheckboxes = getCheckboxIndeterminateMembers(elNestedIndeterminate);
const elParentSubCheckboxes = getCheckboxIndeterminateMembers(elParentIndeterminate);
// Act - check a nested checkbox
if (elNestedIndeterminate) {
elNestedSubCheckboxes.subCheckboxes[0]._inputNode.click();
// @ts-ignore [allow-protected] in test
elNestedSubCheckboxes._subCheckboxes[0]._inputNode.click();
}
await el.updateComplete;
@ -336,8 +343,10 @@ describe('<lion-checkbox-indeterminate>', () => {
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
// Act - check all nested checkbox
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[1]._inputNode.click();
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[2]._inputNode.click();
// @ts-ignore [allow-protected] in test
if (elNestedIndeterminate) elNestedSubCheckboxes._subCheckboxes[1]._inputNode.click();
// @ts-ignore [allow-protected] in test
if (elNestedIndeterminate) elNestedSubCheckboxes._subCheckboxes[2]._inputNode.click();
await el.updateComplete;
// Assert
@ -348,10 +357,12 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act - finally check all remaining checkbox
if (elParentIndeterminate) {
elParentSubCheckboxes.subCheckboxes[0]._inputNode.click();
// @ts-ignore [allow-protected] in test
elParentSubCheckboxes._subCheckboxes[0]._inputNode.click();
}
if (elParentIndeterminate) {
elParentSubCheckboxes.subCheckboxes[1]._inputNode.click();
// @ts-ignore [allow-protected] in test
elParentSubCheckboxes._subCheckboxes[1]._inputNode.click();
}
await el.updateComplete;
@ -383,12 +394,12 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
const { _subCheckboxes } = getCheckboxIndeterminateMembers(elIndeterminate);
// Act
subCheckboxes[0].checked = true;
subCheckboxes[1].checked = true;
subCheckboxes[2].checked = true;
_subCheckboxes[0].checked = true;
_subCheckboxes[1].checked = true;
_subCheckboxes[2].checked = true;
await el.updateComplete;
// Assert

View file

@ -180,6 +180,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @configure FormControlMixin
* Will tell FormControlMixin that a11y wrt labels / descriptions / feedback
* should be applied here.
* @protected
*/
get _inputNode() {
if (this._ariaVersion === '1.1') {

View file

@ -4,34 +4,41 @@ import sinon from 'sinon';
import '@lion/combobox/define';
import { LionOptions } from '@lion/listbox';
import { browserDetection, LitElement } from '@lion/core';
import { getListboxMembers } from '@lion/listbox/test-helpers';
import { Required } from '@lion/form-core';
import { LionCombobox } from '../src/LionCombobox.js';
/**
* @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
*/
function getProtectedMembers(el) {
// @ts-ignore
const {
_comboboxNode: comboboxNode,
_inputNode: inputNode,
_listboxNode: listboxNode,
_selectionDisplayNode: selectionDisplayNode,
_activeDescendantOwnerNode: activeDescendantOwnerNode,
_ariaVersion: ariaVersion,
} = el;
function getComboboxMembers(el) {
const obj = getListboxMembers(el);
return {
comboboxNode,
inputNode,
listboxNode,
selectionDisplayNode,
activeDescendantOwnerNode,
ariaVersion,
...obj,
...{
// @ts-ignore [allow-protected] in test
_invokerNode: el._invokerNode,
// @ts-ignore [allow-protected] in test
_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
*/
function mimicUserTyping(el, value) {
const { inputNode } = getProtectedMembers(el);
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
const { _inputNode } = getComboboxMembers(el);
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
// eslint-disable-next-line no-param-reassign
inputNode.value = value;
inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
_inputNode.value = value;
_inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
_inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
}
/**
@ -63,8 +70,8 @@ function mimicKeyPress(el, key) {
* @param {string[]} values
*/
async function mimicUserTypingAdvanced(el, values) {
const { inputNode } = getProtectedMembers(el);
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (inputNode);
const { _inputNode } = getComboboxMembers(el);
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (_inputNode);
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
for (const key of values) {
@ -226,10 +233,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
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;
expect(el.opened).to.be.true;
});
@ -244,11 +251,11 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
const { listboxNode } = getProtectedMembers(el);
const { _listboxNode } = getComboboxMembers(el);
expect(listboxNode).to.exist;
expect(listboxNode).to.be.instanceOf(LionOptions);
expect(el.querySelector('[role=listbox]')).to.equal(listboxNode);
expect(_listboxNode).to.exist;
expect(_listboxNode).to.be.instanceOf(LionOptions);
expect(el.querySelector('[role=listbox]')).to.equal(_listboxNode);
});
it('has a textbox element', async () => {
@ -258,10 +265,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
expect(comboboxNode).to.exist;
expect(el.querySelector('[role=combobox]')).to.equal(comboboxNode);
expect(_comboboxNode).to.exist;
expect(el.querySelector('[role=combobox]')).to.equal(_comboboxNode);
});
});
@ -273,13 +280,13 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</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';
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 () => {
@ -328,11 +335,11 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
el.clear();
expect(el.modelValue).to.equal('');
expect(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
const el2 = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" multiple-choice .modelValue="${['Artichoke']}">
@ -345,7 +352,7 @@ describe('lion-combobox', () => {
el2.clear();
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-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
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;
expect(el.opened).to.equal(false);
});
@ -385,12 +392,12 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const options = el.formElements;
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
expect(el.opened).to.equal(false);
// step [1]
inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
_inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
await el.updateComplete;
expect(el.opened).to.equal(false);
@ -403,7 +410,7 @@ describe('lion-combobox', () => {
options[0].click();
await el.updateComplete;
expect(el.opened).to.equal(false);
expect(document.activeElement).to.equal(inputNode);
expect(document.activeElement).to.equal(_inputNode);
// step [4]
await el.updateComplete;
@ -422,19 +429,19 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode, inputNode } = getProtectedMembers(el);
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
// open
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
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(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
});
it('hides overlay on [Tab]', async () => {
@ -447,19 +454,19 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode, inputNode } = getProtectedMembers(el);
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
// open
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
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(inputNode.value).to.equal('Artichoke');
expect(_inputNode.value).to.equal('Artichoke');
});
it('clears checkedIndex on empty text', async () => {
@ -472,15 +479,15 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode, inputNode } = getProtectedMembers(el);
const { _comboboxNode, _inputNode } = getComboboxMembers(el);
// open
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
expect(el.opened).to.equal(true);
expect(inputNode.value).to.equal('Artichoke');
expect(_inputNode.value).to.equal('Artichoke');
expect(el.checkedIndex).to.equal(0);
mimicUserTyping(el, '');
@ -510,10 +517,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</${tag}>
`));
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
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;
expect(el.opened).to.equal(true);
});
@ -603,10 +610,10 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const options = el.formElements;
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
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');
await el.updateComplete;
@ -644,10 +651,10 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const options = el.formElements;
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
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');
expect(el.opened).to.equal(true);
@ -672,15 +679,15 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
expect(el.checkedIndex).to.equal(0);
// Simulate backspace deleting the char at the end of the string
mimicKeyPress(inputNode, 'Backspace');
inputNode.dispatchEvent(new Event('input'));
const arr = inputNode.value.split('');
arr.splice(inputNode.value.length - 1, 1);
inputNode.value = arr.join('');
mimicKeyPress(_inputNode, 'Backspace');
_inputNode.dispatchEvent(new Event('input'));
const arr = _inputNode.value.split('');
arr.splice(_inputNode.value.length - 1, 1);
_inputNode.value = arr.join('');
await el.updateComplete;
el.dispatchEvent(new Event('blur'));
@ -703,9 +710,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</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 () => {
@ -715,12 +722,12 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</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;
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`
<lion-combobox name="foo" ._ariaVersion="${'1.1'}">
@ -728,12 +735,12 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
const { comboboxNode: comboboxNode2 } = getProtectedMembers(el2);
const { _comboboxNode: comboboxNode2 } = getComboboxMembers(el2);
expect(comboboxNode2.getAttribute('aria-expanded')).to.equal('false');
el2.opened = true;
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 () => {
@ -743,9 +750,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</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-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 () => {
@ -904,16 +911,16 @@ describe('lion-combobox', () => {
`));
mimicUserTyping(el, 'ch');
await el.updateComplete;
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
expect(inputNode.value).to.equal('Chard');
expect(inputNode.selectionStart).to.equal(2);
expect(inputNode.selectionEnd).to.equal(inputNode.value.length);
expect(_inputNode.value).to.equal('Chard');
expect(_inputNode.selectionStart).to.equal(2);
expect(_inputNode.selectionEnd).to.equal(_inputNode.value.length);
// We don't autocomplete when characters are removed
mimicUserTyping(el, 'c'); // The user pressed backspace (number of chars decreased)
expect(inputNode.value).to.equal('c');
expect(inputNode.selectionStart).to.equal(inputNode.value.length);
expect(_inputNode.value).to.equal('c');
expect(_inputNode.selectionStart).to.equal(_inputNode.value.length);
});
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-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
mimicUserTyping(el, 'ch');
await el.updateComplete;
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 () => {
@ -1018,26 +1025,26 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(inputNode.value).to.equal('Chard');
expect(inputNode.selectionStart).to.equal('ch'.length);
expect(inputNode.selectionEnd).to.equal('Chard'.length);
expect(_inputNode.value).to.equal('Chard');
expect(_inputNode.selectionStart).to.equal('ch'.length);
expect(_inputNode.selectionEnd).to.equal('Chard'.length);
await mimicUserTypingAdvanced(el, ['i', 'c']);
await el.updateComplete;
expect(inputNode.value).to.equal('Chicory');
expect(inputNode.selectionStart).to.equal('chic'.length);
expect(inputNode.selectionEnd).to.equal('Chicory'.length);
expect(_inputNode.value).to.equal('Chicory');
expect(_inputNode.selectionStart).to.equal('chic'.length);
expect(_inputNode.selectionEnd).to.equal('Chicory'.length);
// Diminishing chars, but autocompleting
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(inputNode.value).to.equal('ch');
expect(inputNode.selectionStart).to.equal('ch'.length);
expect(inputNode.selectionEnd).to.equal('ch'.length);
expect(_inputNode.value).to.equal('ch');
expect(_inputNode.selectionStart).to.equal('ch'.length);
expect(_inputNode.selectionEnd).to.equal('ch'.length);
});
it('synchronizes textbox on overlay close', async () => {
@ -1049,8 +1056,8 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('');
const { _inputNode } = getComboboxMembers(el);
expect(_inputNode.value).to.equal('');
/**
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
@ -1066,7 +1073,7 @@ describe('lion-combobox', () => {
el.setCheckedIndex(index);
el.opened = false;
await el.updateComplete;
expect(inputNode.value).to.equal(valueOnClose);
expect(_inputNode.value).to.equal(valueOnClose);
}
await performChecks('none', 0, 'Artichoke');
@ -1091,8 +1098,8 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('');
const { _inputNode } = getComboboxMembers(el);
expect(_inputNode.value).to.equal('');
/**
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
@ -1108,7 +1115,7 @@ describe('lion-combobox', () => {
el.setCheckedIndex(index);
el.opened = false;
await el.updateComplete;
expect(inputNode.value).to.equal(valueOnClose);
expect(_inputNode.value).to.equal(valueOnClose);
}
await performChecks('none', 0, '');
@ -1185,21 +1192,21 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(inputNode.value).to.equal('Chard');
expect(inputNode.selectionStart).to.equal('Ch'.length);
expect(inputNode.selectionEnd).to.equal('Chard'.length);
expect(_inputNode.value).to.equal('Chard');
expect(_inputNode.selectionStart).to.equal('Ch'.length);
expect(_inputNode.selectionEnd).to.equal('Chard'.length);
// Autocompletion happened. When we go backwards ('Ch[ard]' => 'Ch'), we should not
// autocomplete to 'Chard' anymore.
await mimicUserTypingAdvanced(el, ['Backspace']);
await el.updateComplete;
expect(inputNode.value).to.equal('Ch'); // so not 'Chard'
expect(inputNode.selectionStart).to.equal('Ch'.length);
expect(inputNode.selectionEnd).to.equal('Ch'.length);
expect(_inputNode.value).to.equal('Ch'); // so not 'Chard'
expect(_inputNode.selectionStart).to.equal('Ch'.length);
expect(_inputNode.selectionEnd).to.equal('Ch'.length);
});
describe('Subclassers', () => {
@ -1265,29 +1272,29 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
expect(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'none';
el.setCheckedIndex(0);
expect(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'list';
el.setCheckedIndex(0);
expect(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'inline';
el.setCheckedIndex(0);
expect(inputNode.value).to.equal('Artichoke');
expect(_inputNode.value).to.equal('Artichoke');
el.setCheckedIndex(-1);
el.autocomplete = 'both';
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 () => {
@ -1299,35 +1306,35 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
expect(inputNode.value).to.eql('');
expect(_inputNode.value).to.eql('');
el.setCheckedIndex(-1);
el.autocomplete = 'none';
el.setCheckedIndex([0]);
el.setCheckedIndex([1]);
expect(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'list';
el.setCheckedIndex([0]);
el.setCheckedIndex([1]);
expect(inputNode.value).to.equal('');
expect(_inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'inline';
el.setCheckedIndex([0]);
expect(inputNode.value).to.equal('Artichoke');
expect(_inputNode.value).to.equal('Artichoke');
el.setCheckedIndex([1]);
expect(inputNode.value).to.equal('Chard');
expect(_inputNode.value).to.equal('Chard');
el.setCheckedIndex(-1);
el.autocomplete = 'both';
el.setCheckedIndex([0]);
expect(inputNode.value).to.equal('Artichoke');
expect(_inputNode.value).to.equal('Artichoke');
el.setCheckedIndex([1]);
expect(inputNode.value).to.equal('Chard');
expect(_inputNode.value).to.equal('Chard');
});
describe('Subclassers', () => {
@ -1360,27 +1367,27 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</${tag}>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
el.setCheckedIndex(-1);
el.autocomplete = 'none';
el.setCheckedIndex([0]);
expect(inputNode.value).to.equal('Artichoke--multi');
expect(_inputNode.value).to.equal('Artichoke--multi');
el.setCheckedIndex(-1);
el.autocomplete = 'list';
el.setCheckedIndex([0]);
expect(inputNode.value).to.equal('Artichoke--multi');
expect(_inputNode.value).to.equal('Artichoke--multi');
el.setCheckedIndex(-1);
el.autocomplete = 'inline';
el.setCheckedIndex([0]);
expect(inputNode.value).to.equal('Artichoke--multi');
expect(_inputNode.value).to.equal('Artichoke--multi');
el.setCheckedIndex(-1);
el.autocomplete = 'both';
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-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
/**
* @param {LionCombobox} elm
@ -1442,7 +1449,7 @@ describe('lion-combobox', () => {
expect(el.activeIndex).to.equal(-1);
expect(el.opened).to.be.true;
mimicKeyPress(inputNode, 'Enter');
mimicKeyPress(_inputNode, 'Enter');
expect(el.opened).to.be.false;
expect(el.activeIndex).to.equal(-1);
@ -1456,7 +1463,7 @@ describe('lion-combobox', () => {
expect(el.opened).to.be.true;
expect(el.activeIndex).to.equal(-1);
mimicKeyPress(inputNode, 'Enter');
mimicKeyPress(_inputNode, 'Enter');
expect(el.activeIndex).to.equal(-1);
expect(el.opened).to.be.false;
@ -1473,7 +1480,7 @@ describe('lion-combobox', () => {
expect(el.activeIndex).to.equal(1);
mimicKeyPress(inputNode, 'Enter');
mimicKeyPress(_inputNode, 'Enter');
await el.updateComplete;
await el.updateComplete;
@ -1487,7 +1494,7 @@ describe('lion-combobox', () => {
await el.updateComplete;
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
await el.updateComplete;
mimicKeyPress(inputNode, 'Enter');
mimicKeyPress(_inputNode, 'Enter');
expect(el.activeIndex).to.equal(1);
expect(el.opened).to.be.false;
});
@ -1501,7 +1508,7 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
await el.updateComplete;
@ -1518,7 +1525,7 @@ describe('lion-combobox', () => {
// select artichoke
mimicUserTyping(/** @type {LionCombobox} */ (el), 'artichoke');
await el.updateComplete;
mimicKeyPress(inputNode, 'Enter');
mimicKeyPress(_inputNode, 'Enter');
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
await el.updateComplete;
@ -1537,17 +1544,17 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
const { _inputNode } = getComboboxMembers(el);
// Select something
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
await el.updateComplete;
mimicKeyPress(inputNode, 'Enter');
mimicKeyPress(_inputNode, 'Enter');
expect(el.activeIndex).to.equal(1);
mimicKeyPress(inputNode, 'Escape');
mimicKeyPress(_inputNode, 'Escape');
await el.updateComplete;
expect(inputNode.textContent).to.equal('');
expect(_inputNode.textContent).to.equal('');
el.formElements.forEach(option => expect(option.active).to.be.false);
@ -1562,13 +1569,25 @@ describe('lion-combobox', () => {
it('synchronizes autocomplete option to textbox', async () => {
let el;
[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' });
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' });
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 () => {
@ -1581,21 +1600,21 @@ describe('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,
);
expect(el.formElements[1].active).to.equal(false);
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el.updateComplete;
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
expect(elProts._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
null,
);
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
mimicKeyPress(elProts.inputNode, 'ArrowDown');
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
mimicKeyPress(elProts._inputNode, 'ArrowDown');
expect(elProts._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
'artichoke-option',
);
expect(el.formElements[1].active).to.equal(false);
@ -1609,11 +1628,11 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const el2Prots = getProtectedMembers(el2);
const el2Prots = getComboboxMembers(el2);
mimicUserTyping(/** @type {LionCombobox} */ (el2), 'ch');
await el2.updateComplete;
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
expect(el2Prots._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
el2.formElements[1].id,
);
expect(el2.formElements[1].active).to.equal(true);
@ -1621,7 +1640,7 @@ describe('lion-combobox', () => {
el2.autocomplete = 'list';
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el2.updateComplete;
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
expect(el2Prots._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
el2.formElements[1].id,
);
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-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 () => {
@ -1657,9 +1676,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</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 () => {
@ -1671,9 +1690,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</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;
const el2 = /** @type {LionCombobox} */ (await fixture(html`
@ -1681,9 +1700,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</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...
browserDetection.isChromium = browserDetectionIsChromiumOriginal;
@ -1703,10 +1722,10 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
const { _comboboxNode } = getComboboxMembers(el);
// activate opened listbox
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
_comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'ch');
await el.updateComplete;

View file

@ -183,6 +183,7 @@ const FormControlMixinImplementation = superclass =>
};
}
/** @protected */
get _inputNode() {
return /** @type {HTMLElementWithValue} */ (this.__getDirectSlotChild('input'));
}
@ -195,6 +196,9 @@ const FormControlMixinImplementation = superclass =>
return /** @type {HTMLElement} */ (this.__getDirectSlotChild('help-text'));
}
/**
* @protected
*/
get _feedbackNode() {
return /** @type {LionValidationFeedback} */ (this.__getDirectSlotChild('feedback'));
}
@ -280,7 +284,7 @@ const FormControlMixinImplementation = superclass =>
/** @protected */
_triggerInitialModelValueChangedEvent() {
this.__dispatchInitialModelValueChangedEvent();
this._dispatchInitialModelValueChangedEvent();
}
/** @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
// to send a unified event
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
// the loop via sync observers is not needed anymore.
// - 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 make formattedValue as a concept obsolete, since for maximum flexibility, the
// 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?
// We only want to format values that are considered valid. For best UX,
// 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].
// If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
// the value, no matter what.
@ -290,7 +290,7 @@ const FormatMixinImplementation = superclass =>
// input into `._inputNode` with modelValue as input)
if (
this.__isHandlingUserInput &&
this._isHandlingUserInput &&
this.hasFeedbackFor &&
this.hasFeedbackFor.length &&
this.hasFeedbackFor.includes('error') &&
@ -333,7 +333,7 @@ const FormatMixinImplementation = superclass =>
bubbles: true,
detail: /** @type { ModelValueEventDetails } */ ({
formPath: [this],
isTriggeredByUser: Boolean(this.__isHandlingUserInput),
isTriggeredByUser: Boolean(this._isHandlingUserInput),
}),
}),
);
@ -376,7 +376,7 @@ const FormatMixinImplementation = superclass =>
* @protected
*/
_reflectBackOn() {
return !this.__isHandlingUserInput;
return !this._isHandlingUserInput;
}
// This can be called whenever the view value should be updated. Dependent on component type
@ -397,9 +397,9 @@ const FormatMixinImplementation = superclass =>
_onUserInputChanged() {
// Upwards syncing. Most properties are delegated right away, value is synced to
// `LionField`, to be able to act on (imperatively set) value changes
this.__isHandlingUserInput = true;
this._isHandlingUserInput = true;
this._syncValueUpwards();
this.__isHandlingUserInput = false;
this._isHandlingUserInput = false;
}
/**

View file

@ -1,12 +1,22 @@
import { dedupeMixin } from '@lion/core';
import { FormControlMixin } from './FormControlMixin.js';
import { FocusMixin } from './FocusMixin.js';
/**
* @typedef {import('../types/NativeTextFieldMixinTypes').NativeTextFieldMixin} 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 =>
class NativeTextFieldMixin extends superclass {
class NativeTextFieldMixin extends FocusMixin(FormControlMixin(superclass)) {
/**
* @protected
* @type {HTMLInputElement | HTMLTextAreaElement}
*/
get _inputNode() {
return /** @type {HTMLInputElement | HTMLTextAreaElement} */ (super._inputNode);
}
/** @type {number} */
get selectionStart() {
const native = this._inputNode;

View file

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

View file

@ -234,9 +234,9 @@ const ChoiceInputMixinImplementation = superclass =>
if (this.disabled) {
return;
}
this.__isHandlingUserInput = true;
this._isHandlingUserInput = true;
this.checked = !this.checked;
this.__isHandlingUserInput = false;
this._isHandlingUserInput = false;
}
// 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() {
return this;
}
@ -183,7 +184,7 @@ const FormGroupMixinImplementation = superclass =>
*/
_triggerInitialModelValueChangedEvent() {
this.registrationComplete.then(() => {
this.__dispatchInitialModelValueChangedEvent();
this._dispatchInitialModelValueChangedEvent();
});
}

View file

@ -80,6 +80,7 @@ const SyncUpdatableMixinImplementation = superclass =>
// Empty queue...
if (ns.queue) {
Array.from(ns.queue).forEach(name => {
// @ts-ignore [allow-private] in test
if (ctor.__syncUpdatableHasChanged(name, this[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
ns.queue.add(name);
} // After connectedCallback: guarded proxy to updateSync
// @ts-ignore [allow-private] in test
else if (ctor.__syncUpdatableHasChanged(name, this[name], oldValue)) {
this.updateSync(name, oldValue);
}

View file

@ -45,14 +45,15 @@ export class Required extends Validator {
/**
* @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
onFormControlConnect(formControl) {
if (formControl._inputNode) {
const role = formControl._inputNode.getAttribute('role') || '';
const elementTagName = formControl._inputNode.tagName.toLowerCase();
onFormControlConnect({ _inputNode: inputNode }) {
if (inputNode) {
const role = inputNode.getAttribute('role') || '';
const elementTagName = inputNode.tagName.toLowerCase();
const ctor = /** @type {typeof Required} */ (this.constructor);
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
*/
// @ts-ignore [allow-protected] we are allowed to know FormControl protcected props in form-core
// eslint-disable-next-line class-methods-use-this
onFormControlDisconnect(formControl) {
if (formControl._inputNode) {
formControl._inputNode.removeAttribute('aria-required');
onFormControlDisconnect({ _inputNode: inputNode }) {
if (inputNode) {
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 './getFormControlMembers.js';

View file

@ -4,10 +4,6 @@ import { FormRegisteringMixin } from '../src/registration/FormRegisteringMixin.j
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
import { FormRegistrarPortalMixin } from '../src/registration/FormRegistrarPortalMixin.js';
/**
* @typedef {import('../types/registration/FormRegistrarMixinTypes').FormRegistrarHost} FormRegistrarHost
*/
/**
* @typedef {Object} customConfig
* @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 { FormatMixin } from '../src/FormatMixin.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
*/
@ -284,10 +286,11 @@ export function runFormatMixinSuite(customConfig) {
describe('View value', () => {
it('has an input node (like <input>/<textarea>) which holds the formatted (view) value', async () => {
const { _inputNode } = getFormControlMembers(fooFormat);
fooFormat.modelValue = 'string';
expect(fooFormat.formattedValue).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 () => {
@ -305,16 +308,17 @@ export function runFormatMixinSuite(customConfig) {
<input slot="input" />
</${tag}>
`));
const { _inputNode } = getFormControlMembers(formatEl);
const generatedViewValue = generateValueBasedOnType({ viewValue: true });
const generatedModelValue = generateValueBasedOnType();
mimicUserInput(formatEl, generatedViewValue);
expect(formatEl._inputNode.value).to.not.equal(`foo: ${generatedModelValue}`);
expect(_inputNode.value).to.not.equal(`foo: ${generatedModelValue}`);
// user leaves field
formatEl._inputNode.dispatchEvent(new CustomEvent(formatEl.formatOn, { bubbles: true }));
_inputNode.dispatchEvent(new CustomEvent(formatEl.formatOn, { bubbles: true }));
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 () => {
@ -323,17 +327,20 @@ export function runFormatMixinSuite(customConfig) {
<input slot="input" />
</${tag}>
`));
const { _inputNode } = getFormControlMembers(el);
// The FormatMixin can be used in conjunction with the ValidateMixin, in which case
// it can hold errorState (affecting the formatting)
el.hasFeedbackFor = ['error'];
// users types value '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
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}>
`));
const { _inputNode } = getFormControlMembers(el);
expect(preprocessorSpy.callCount).to.equal(1);
const parserSpy = sinon.spy(el, 'parser');
@ -501,7 +510,7 @@ export function runFormatMixinSuite(customConfig) {
expect(preprocessorSpy.callCount).to.equal(2);
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 () => {
@ -510,13 +519,16 @@ export function runFormatMixinSuite(customConfig) {
<input slot="input">
</${tag}>
`));
const { _inputNode } = getFormControlMembers(el);
const preprocessorSpy = sinon.spy(el, 'preprocessor');
el._inputNode.dispatchEvent(new Event('compositionstart', { bubbles: true }));
_inputNode.dispatchEvent(new Event('compositionstart', { bubbles: true }));
mimicUserInput(el, '`');
expect(preprocessorSpy.callCount).to.equal(0);
// "à" would be sent by the browser after pressing "option + `", followed by "a"
mimicUserInput(el, 'à');
el._inputNode.dispatchEvent(new Event('compositionend', { bubbles: true }));
_inputNode.dispatchEvent(new Event('compositionend', { bubbles: true }));
expect(preprocessorSpy.callCount).to.equal(1);
});
});

View file

@ -9,6 +9,7 @@ import {
unsafeStatic,
} from '@open-wc/testing';
import sinon from 'sinon';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import { InteractionStateMixin } from '../src/InteractionStateMixin.js';
import { ValidateMixin } from '../src/validate/ValidateMixin.js';
import { MinLength } from '../src/validate/validators/StringValidators.js';
@ -135,6 +136,7 @@ export function runInteractionStateMixinSuite(customConfig) {
const targetEl = el._inputNode || el;
targetEl.dispatchEvent(new Event('focus', { bubbles: true }));
el.modelValue = modelValue;
// @ts-ignore [allow-protected] in test
targetEl.dispatchEvent(new Event(el._leaveEvent, { bubbles: true }));
};
@ -224,20 +226,22 @@ export function runInteractionStateMixinSuite(customConfig) {
const el = /** @type {IState} */ (await fixture(html`
<${tag} .validators=${[new MinLength(3)]}></${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete;
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
el.modelValue = '1';
await el.updateComplete;
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
expect(_feedbackNode.feedbackData).to.deep.equal([]);
el.submitted = true;
await el.updateComplete;
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 { aTimeout, defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon';
import {
MaxLength,
@ -15,7 +16,7 @@ import {
AlwaysValid,
AsyncAlwaysInvalid,
AsyncAlwaysValid,
} from '../test-helpers.js';
} from '../test-helpers/index.js';
/**
* @param {{tagString?: string | null, lightDom?: string}} [customConfig]
@ -153,6 +154,7 @@ export function runValidateMixinSuite(customConfig) {
>${lightDom}</${tag}>
`));
// @ts-ignore [allow-private] in test
const clearSpy = sinon.spy(el, '__clearValidationResults');
const validateSpy = sinon.spy(el, 'validate');
el.modelValue = 'x';
@ -174,6 +176,7 @@ export function runValidateMixinSuite(customConfig) {
const el = /** @type {ValidateElement} */ (await fixture(html`
<${tag} .validators=${[alwaysValid]}>${lightDom}</${tag}>
`));
// @ts-ignore [allow-private] in test
const isEmptySpy = sinon.spy(el, '__isEmpty');
const validateSpy = sinon.spy(el, 'validate');
el.modelValue = '';
@ -191,7 +194,9 @@ export function runValidateMixinSuite(customConfig) {
const el = /** @type {ValidateElement} */ (await fixture(html`
<${tag} .validators=${[new AlwaysValid()]}>${lightDom}</${tag}>
`));
// @ts-ignore [allow-private] in test
const isEmptySpy = sinon.spy(el, '__isEmpty');
// @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators');
el.modelValue = 'nonEmpty';
expect(isEmptySpy.calledBefore(syncSpy)).to.be.true;
@ -203,7 +208,9 @@ export function runValidateMixinSuite(customConfig) {
${lightDom}
</${tag}>
`));
// @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators');
// @ts-ignore [allow-private] in test
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
el.modelValue = 'nonEmpty';
expect(syncSpy.calledBefore(asyncSpy)).to.be.true;
@ -223,7 +230,9 @@ export function runValidateMixinSuite(customConfig) {
</${tag}>
`));
// @ts-ignore [allow-private] in test
const syncSpy = sinon.spy(el, '__executeSyncValidators');
// @ts-ignore [allow-private] in test
const resultSpy2 = sinon.spy(el, '__executeResultValidators');
el.modelValue = 'nonEmpty';
@ -236,7 +245,9 @@ export function runValidateMixinSuite(customConfig) {
</${tag}>
`);
// @ts-ignore [allow-private] in test
const asyncSpy = sinon.spy(el, '__executeAsyncValidators');
// @ts-ignore [allow-private] in test
const resultSpy = sinon.spy(el, '__executeResultValidators');
el.modelValue = 'nonEmpty';
@ -266,6 +277,7 @@ export function runValidateMixinSuite(customConfig) {
</${tag}>
`));
el.modelValue = 'nonEmpty';
// @ts-ignore [allow-private] in test
const validateResolveSpy = sinon.spy(el, '__validateCompleteResolve');
await el.validateComplete;
expect(validateResolveSpy.callCount).to.equal(1);
@ -610,10 +622,14 @@ export function runValidateMixinSuite(customConfig) {
.modelValue=${'myValue'}
>${lightDom}</${withSuccessTag}>
`));
// @ts-ignore [allow-private] in test
const prevValidationResult = el.__prevValidationResult;
// @ts-ignore [allow-private] in test
const prevShownValidationResult = el.__prevShownValidationResult;
const regularValidationResult = [
// @ts-ignore [allow-private] in test
...el.__syncValidationResult,
// @ts-ignore [allow-private] in test
...el.__asyncValidationResult,
];
@ -643,6 +659,7 @@ export function runValidateMixinSuite(customConfig) {
>${lightDom}</${tag}>
`));
// @ts-ignore [allow-private] in test
const totalValidationResult = el.__validationResult;
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 executeSpy = sinon.spy(validator, 'execute');
// @ts-ignore [allow-private] in test
const privateIsEmptySpy = sinon.spy(el, '__isEmpty');
el.modelValue = null;
expect(executeSpy.callCount).to.equal(0);
@ -722,9 +740,11 @@ export function runValidateMixinSuite(customConfig) {
.modelValue=${''}
>${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 = [];
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}
.validators=${[new MinLength(3)]}
></${preconfTag}>`));
const { _allValidators } = getFormControlMembers(el);
expect(el.validators.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(el._allValidators[1] instanceof AlwaysInvalid).to.be.true;
expect(_allValidators[0] instanceof MinLength).to.be.true;
expect(_allValidators[1] instanceof AlwaysInvalid).to.be.true;
el.validators = [new MaxLength(5)];
expect(el._allValidators[0] instanceof MaxLength).to.be.true;
expect(el._allValidators[1] instanceof AlwaysInvalid).to.be.true;
const { _allValidators: _allValidatorsMl } = getFormControlMembers(el);
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' })]}>
<input slot="input">
</${tag}>`));
const { _inputNode } = getFormControlMembers(el);
if (el._inputNode) {
if (_inputNode) {
// @ts-expect-error
const spy = sinon.spy(el._inputNode, 'setCustomValidity');
el.modelValue = '';
@ -1021,9 +1045,13 @@ export function runValidateMixinSuite(customConfig) {
.modelValue=${'1'}
>${lightDom}</${customTypeTag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
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);
expect(resultOrder).to.deep.equal(['error', 'x', 'y']);
@ -1164,6 +1192,7 @@ export function runValidateMixinSuite(customConfig) {
>${lightDom}</${elTag}>
`));
// @ts-ignore [allow-protected] in test
const spy = sinon.spy(el, '_updateShouldShowFeedbackFor');
let counter = 0;
// 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 { localizeTearDown } from '@lion/localize/test-helpers';
import { defineCE, expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon';
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() {
describe('Validity Feedback', () => {
@ -121,11 +122,13 @@ export function runValidateMixinFeedbackPart() {
.modelValue=${'cat'}
>${lightDom}</${tag}>
`));
expect(el._feedbackNode.feedbackData).to.deep.equal([]);
const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData).to.deep.equal([]);
el.validators = [new AlwaysInvalid()];
await el.updateComplete;
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 () => {
@ -136,14 +139,17 @@ export function runValidateMixinFeedbackPart() {
.validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete;
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
await el.validate();
await el.updateComplete;
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 () => {
@ -154,9 +160,11 @@ export function runValidateMixinFeedbackPart() {
.validators=${[new AlwaysInvalid(), new MinLength(4)]}
>${lightDom}</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
await el.updateComplete;
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 () => {
@ -178,13 +186,13 @@ export function runValidateMixinFeedbackPart() {
.validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}>
`));
expect(el._feedbackNode.feedbackData).to.be.undefined;
const { _feedbackNode } = getFormControlMembers(el);
expect(_feedbackNode.feedbackData).to.be.undefined;
unlockMessage();
await el.updateComplete;
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData?.[0].message).to.equal(
'this ends up in "._feedbackNode"',
);
expect(_feedbackNode.feedbackData?.[0].message).to.equal('this ends up in "._feedbackNode"');
});
// N.B. this replaces the 'config.hideFeedback' option we had before...
@ -207,14 +215,13 @@ export function runValidateMixinFeedbackPart() {
.validators=${[new AlwaysInvalid()]}
>${lightDom}</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
expect(el._feedbackNode.feedbackData).to.be.undefined;
expect(_feedbackNode.feedbackData).to.be.undefined;
unlockMessage();
await el.updateComplete;
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData?.[0].message).to.equal(
'this ends up in "._feedbackNode"',
);
expect(_feedbackNode.feedbackData?.[0].message).to.equal('this ends up in "._feedbackNode"');
});
it('supports custom element to render feedback', async () => {
@ -257,20 +264,21 @@ export function runValidateMixinFeedbackPart() {
<${customFeedbackTag} slot="feedback"><${customFeedbackTag}>
</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
expect(el._feedbackNode.localName).to.equal(customFeedbackTagString);
expect(_feedbackNode.localName).to.equal(customFeedbackTagString);
el.modelValue = 'dog';
await el.updateComplete;
await el.feedbackComplete;
await el._feedbackNode.updateComplete;
expect(el._feedbackNode).shadowDom.to.equal('Custom for ContainsLowercaseA');
await _feedbackNode.updateComplete;
expect(_feedbackNode).shadowDom.to.equal('Custom for ContainsLowercaseA');
el.modelValue = 'cat';
await el.updateComplete;
await el.feedbackComplete;
await el._feedbackNode.updateComplete;
expect(el._feedbackNode).shadowDom.to.equal('Custom for AlwaysInvalid');
await _feedbackNode.updateComplete;
expect(_feedbackNode).shadowDom.to.equal('Custom for AlwaysInvalid');
});
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' })]}
>${lightDom}</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
el.modelValue = 'a';
await el.updateComplete;
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 () => {
@ -295,13 +304,15 @@ export function runValidateMixinFeedbackPart() {
.modelValue=${'1'}
>${lightDom}</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData?.length).to.equal(1);
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
expect(_feedbackNode.feedbackData?.length).to.equal(1);
expect(_feedbackNode.feedbackData?.[0].message).to.equal('Message for MinLength');
localize.locale = 'de-DE';
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 () => {
@ -321,16 +332,17 @@ export function runValidateMixinFeedbackPart() {
]}
>${lightDom}</${elTag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
el.modelValue = 'a';
await el.updateComplete;
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';
await el.updateComplete;
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', () => {
@ -342,7 +354,9 @@ export function runValidateMixinFeedbackPart() {
.modelValue=${'a'}
>${lightDom}</${tag}>
`));
const inputNode = el._inputNode;
const { _inputNode } = getFormControlMembers(el);
const inputNode = _inputNode;
expect(inputNode.getAttribute('aria-invalid')).to.equal('false');
el.modelValue = '';
@ -493,12 +507,13 @@ export function runValidateMixinFeedbackPart() {
.modelValue=${'1'}
>${lightDom}</${tag}>
`));
const { _feedbackNode } = getFormControlMembers(el);
el.modelValue = '12345';
await el.updateComplete;
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 { LionInput } from '@lion/input';
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon';
import { ChoiceInputMixin } from '../../src/choice-group/ChoiceInputMixin.js';
@ -86,9 +87,11 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
<input slot="input" />
</${tag}>
`));
const { _inputNode } = getFormControlMembers(el);
expect(counter).to.equal(0);
// 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
expect(counter).to.equal(0);
nativeInput.dispatchEvent(new CustomEvent('change', { bubbles: true }));
@ -104,14 +107,16 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
<input slot="input" />
</${tag}>
`));
const { _inputNode, _labelNode } = getFormControlMembers(el);
el.click();
expect(spy.args[0][0].target).to.equal(el);
expect(spy.callCount).to.equal(1);
el._labelNode.click();
expect(spy.args[1][0].target).to.equal(el._labelNode);
_labelNode.click();
expect(spy.args[1][0].target).to.equal(_labelNode);
expect(spy.callCount).to.equal(2);
el._inputNode.click();
expect(spy.args[2][0].target).to.equal(el._inputNode);
_inputNode.click();
expect(spy.args[2][0].target).to.equal(_inputNode);
expect(spy.callCount).to.equal(3);
});
@ -126,7 +131,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
<input slot="input" />
</${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;
});
@ -134,6 +141,7 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
const el = /** @type {ChoiceInput} */ (await fixture(html`
<${tag} .choiceValue=${'foo'} .validators=${[new Required()]}></${tag}>
`));
expect(el.hasFeedbackFor).to.include('error');
expect(el.validationStates.error).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 () => {
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
const { _inputNode } = getFormControlMembers(el);
expect(el.checked).to.be.false;
el.checked = true;
expect(el.checked).to.be.true;
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 () => {
const el = /** @type {ChoiceInput} */ (await fixture(html`<${tag}></${tag}>`));
el._inputNode.click();
const { _inputNode } = getFormControlMembers(el);
_inputNode.click();
expect(el.checked).to.be.true;
el._inputNode.click();
_inputNode.click();
await el.updateComplete;
if (el.type === 'checkbox') {
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 () => {
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;
});
@ -206,7 +220,9 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
));
expect(el.checked).to.be.false;
// @ts-ignore [allow-private] in test
const spyModelCheckedToChecked = sinon.spy(el, '__syncModelCheckedToChecked');
// @ts-ignore [allow-private] in test
const spyCheckedToModel = sinon.spy(el, '__syncCheckedToModel');
el.checked = true;
expect(el.modelValue.checked).to.be.true;
@ -234,14 +250,16 @@ export function runChoiceInputMixinSuite({ tagString } = {}) {
<input slot="input" />
</${tag}>
`));
const { _inputNode } = getFormControlMembers(el);
const { _inputNode: _inputNodeChecked } = getFormControlMembers(elChecked);
// Initial values
expect(hasAttr(el)).to.equal(false, 'initial unchecked element');
expect(hasAttr(elChecked)).to.equal(true, 'initial checked element');
// Via user interaction
el._inputNode.click();
elChecked._inputNode.click();
_inputNode.click();
_inputNodeChecked.click();
await el.updateComplete;
expect(el.checked).to.be.true;
expect(hasAttr(el)).to.equal(true, 'user click checked');

View file

@ -1,6 +1,7 @@
import { LitElement } from '@lion/core';
import { localizeTearDown } from '@lion/localize/test-helpers';
import { defineCE, expect, html, unsafeStatic, fixture } from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import { LionInput } from '@lion/input';
import '@lion/form-core/define';
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
@ -67,20 +68,23 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
<${childTag} name="B" label="fieldB"></${childTag}>
</${tag}>
`));
const { _labelNode } = getFormControlMembers(el);
/**
* @param {LionInput} 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 field2 = el.formElements[1];
expect(getLabels(field1)).to.eql([field1._labelNode.id, el._labelNode.id]);
expect(getLabels(field2)).to.eql([field2._labelNode.id, el._labelNode.id]);
expect(getLabels(field1)).to.eql([field1._labelNode.id, _labelNode.id]);
expect(getLabels(field2)).to.eql([field2._labelNode.id, _labelNode.id]);
// Test the cleanup on disconnected
el.removeChild(field1);
@ -277,10 +281,12 @@ export function runFormGroupMixinInputSuite(cfg = {}) {
// Check cleanup of FormGroup on disconnect
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);
// @ts-expect-error removeChild should always be inherited via LitElement?
l2_g._parentFormGroup.removeChild(l2_g);
await l2_g.updateComplete;
// @ts-ignore [allow-private] in test
expect(l2_g.__descriptionElementsInParentChain.size).to.equal(0);
}
/* eslint-enable camelcase */

View file

@ -1,5 +1,4 @@
import { LitElement } from '@lion/core';
// @ts-ignore
import { localizeTearDown } from '@lion/localize/test-helpers';
import {
defineCE,
@ -11,10 +10,10 @@ import {
aTimeout,
} from '@open-wc/testing';
import sinon from 'sinon';
// @ts-ignore
import { IsNumber, Validator, LionField } from '@lion/form-core';
import '@lion/form-core/define';
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
import { getFormControlMembers } from '../../test-helpers/getFormControlMembers.js';
/**
* @param {{ tagString?: string, childTagString?:string }} [cfg]
@ -63,12 +62,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
const el1 = /** @type {FormGroup} */ (await fixture(
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(
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 () => {
@ -82,17 +83,18 @@ export function runFormGroupMixinSuite(cfg = {}) {
const el = /** @type {FormGroup} */ (await fixture(html`
<${tag} label="foo" .fieldName="${'bar'}">${inputSlots}</${tag}>
`));
// @ts-ignore [allow-proteced] in test
expect(el.__fieldName).to.equal(el.fieldName);
});
// 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 () => {
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['hobbies[]'].length).to.equal(2);
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['hobbies[]'].length).to.equal(1);
});
@ -207,16 +209,17 @@ export function runFormGroupMixinSuite(cfg = {}) {
const newField = /** @type {FormGroup} */ (await fixture(
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);
el.appendChild(newField);
// @ts-ignore
// @ts-ignore [allow-protected] in test
expect(el.formElements._keys().length).to.equal(4);
el._inputNode.removeChild(newField);
// @ts-ignore
_inputNode.removeChild(newField);
// @ts-ignore [allow-protected] in test
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 () => {
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
await triggerFocusFor(el.formElements['hobbies[]'][0]._inputNode);
await triggerFocusFor(
el.formElements['hobbies[]'][el.formElements['gender[]'].length - 1]._inputNode,
const { _inputNode: hobbyInputNode } = getFormControlMembers(
el.formElements['hobbies[]'][0],
);
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>`));
button.focus();
@ -925,12 +932,14 @@ export function runFormGroupMixinSuite(cfg = {}) {
html`<${tag} touched dirty>${inputSlots}</${tag}>`,
));
// Safety check initially
// @ts-ignore [allow-protected] in test
el._setValueForAllFormElements('prefilled', true);
expect(el.dirty).to.equal(true, '"dirty" initially');
expect(el.touched).to.equal(true, '"touched" initially');
expect(el.prefilled).to.equal(true, '"prefilled" initially');
// Reset all children states, with prefilled false
// @ts-ignore [allow-protected] in test
el._setValueForAllFormElements('modelValue', {});
el.resetInteractionState();
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');
// Reset all children states with prefilled true
// @ts-ignore [allow-protected] in test
el._setValueForAllFormElements('modelValue', { checked: true }); // not prefilled
el.resetInteractionState();
expect(el.dirty).to.equal(false, 'not "dirty" after 2nd reset');
@ -1024,6 +1034,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
`));
await el.updateComplete;
el.modelValue['child[]'] = ['foo2', 'bar2'];
// @ts-ignore [allow-protected] in test
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
});
@ -1040,6 +1051,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
</${childTag}>
`));
el.appendChild(childEl);
// @ts-ignore [allow-protected] in test
expect(el._initialModelValue['child[]']).to.eql(['foo1', 'bar1']);
});

View file

@ -1,5 +1,6 @@
import { LitElement } from '@lion/core';
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';
describe('FocusMixin', () => {
@ -16,16 +17,19 @@ describe('FocusMixin', () => {
const el = /** @type {Focusable} */ (await fixture(html`
<${tag}><input slot="input"></${tag}>
`));
const { _inputNode } = getFormControlMembers(el);
el.focus();
expect(document.activeElement === el._inputNode).to.be.true;
expect(document.activeElement === _inputNode).to.be.true;
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 () => {
const el = /** @type {Focusable} */ (await fixture(html`
<${tag}><input slot="input"></${tag}>
`));
el.focus();
await el.updateComplete;
expect(el.hasAttribute('focused')).to.be.true;
@ -39,10 +43,12 @@ describe('FocusMixin', () => {
const el = /** @type {Focusable} */ (await fixture(html`
<${tag}><input slot="input"></${tag}>
`));
const { _inputNode } = getFormControlMembers(el);
expect(el.focused).to.be.false;
el._inputNode?.focus();
_inputNode?.focus();
expect(el.focused).to.be.true;
el._inputNode?.blur();
_inputNode?.blur();
expect(el.focused).to.be.false;
});

View file

@ -1,5 +1,6 @@
import { expect, html, defineCE, unsafeStatic, fixture } from '@open-wc/testing';
import { LitElement } from '@lion/core';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon';
import { FormControlMixin } from '../src/FormControlMixin.js';
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';
@ -118,15 +119,17 @@ describe('FormControlMixin', () => {
</div>
`));
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const labelIdsBefore = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
const descriptionIdsBefore = /** @type {string} */ (el._inputNode.getAttribute(
const { _inputNode } = getFormControlMembers(el);
const labelIdsBefore = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
const descriptionIdsBefore = /** @type {string} */ (_inputNode.getAttribute(
'aria-describedby',
));
// Reconnect
wrapper.removeChild(el);
wrapper.appendChild(el);
const labelIdsAfter = /** @type {string} */ (el._inputNode.getAttribute('aria-labelledby'));
const descriptionIdsAfter = /** @type {string} */ (el._inputNode.getAttribute(
const labelIdsAfter = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
const descriptionIdsAfter = /** @type {string} */ (_inputNode.getAttribute(
'aria-describedby',
));
@ -141,8 +144,10 @@ describe('FormControlMixin', () => {
${inputSlot}
</${tag}>
`));
const { _labelNode } = getFormControlMembers(el);
expect(spy).to.not.have.been.called;
el._labelNode.click();
_labelNode.click();
expect(spy).to.have.been.calledOnce;
});
@ -231,6 +236,8 @@ describe('FormControlMixin', () => {
<div id="additionalDescription"> Same for this </div>
</div>`));
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el);
// wait until the field element is done rendering
await el.updateComplete;
await el.updateComplete;
@ -240,7 +247,7 @@ describe('FormControlMixin', () => {
// 1a. addToAriaLabelledBy()
// 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}`,
);
const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
@ -248,7 +255,7 @@ describe('FormControlMixin', () => {
));
el.addToAriaLabelledBy(additionalLabel);
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)
expect(labelledbyAttr).to.contain(`additionalLabel`);
// Should be placed in the end
@ -259,13 +266,13 @@ describe('FormControlMixin', () => {
// 1b. removeFromAriaLabelledBy()
el.removeFromAriaLabelledBy(additionalLabel);
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)
expect(labelledbyAttr).to.not.contain(`additionalLabel`);
// 2a. addToAriaDescribedBy()
// 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}`,
);
});
@ -284,6 +291,7 @@ describe('FormControlMixin', () => {
<div id="externalDescriptionB">should go after input internals</div>
</div>`);
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,
// but this example purely demonstrates dom order is respected.
@ -296,10 +304,10 @@ describe('FormControlMixin', () => {
await el.updateComplete;
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['myInput', 'internalLabel']);
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['myInput', 'internalDescription']);
// cleanup
@ -315,10 +323,10 @@ describe('FormControlMixin', () => {
await el.updateComplete;
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['internalLabel', 'myInput']);
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['internalDescription', 'myInput']);
});
@ -336,6 +344,7 @@ describe('FormControlMixin', () => {
<div id="externalDescriptionB">should go after input internals</div>
</div>`);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el);
// 1. addToAriaLabelledBy()
const labelA = /** @type {HTMLElement} */ (wrapper.querySelector('#externalLabelA'));
@ -346,7 +355,7 @@ describe('FormControlMixin', () => {
await el.updateComplete;
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-labelledby')).split(' '),
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['internalLabel', 'externalLabelA', 'externalLabelB']);
// 2. addToAriaDescribedBy()
@ -358,7 +367,7 @@ describe('FormControlMixin', () => {
await el.updateComplete;
expect(
/** @type {string} */ (el._inputNode.getAttribute('aria-describedby')).split(' '),
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['internalDescription', 'externalDescriptionA', 'externalDescriptionB']);
});
});

View file

@ -10,6 +10,7 @@ import {
triggerFocusFor,
unsafeStatic,
} from '@open-wc/testing';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import sinon from 'sinon';
import '@lion/form-core/define-field';
@ -31,8 +32,10 @@ const inputSlot = unsafeHTML(inputSlotString);
* @param {string} newViewValue
*/
function mimicUserInput(formControl, newViewValue) {
const { _inputNode } = getFormControlMembers(formControl);
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(() => {
@ -60,12 +63,15 @@ describe('<lion-field>', () => {
const el1 = /** @type {LionField} */ (await fixture(
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(
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 () => {
@ -79,23 +85,26 @@ describe('<lion-field>', () => {
const el = /** @type {LionField} */ (await fixture(
html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`,
));
// @ts-ignore [allow-proteced] in test
expect(el.__fieldName).to.equal(el.fieldName);
});
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 { _inputNode } = getFormControlMembers(el);
const cbFocusHost = sinon.spy();
el.addEventListener('focus', cbFocusHost);
const cbFocusNativeInput = sinon.spy();
el._inputNode.addEventListener('focus', cbFocusNativeInput);
_inputNode.addEventListener('focus', cbFocusNativeInput);
const cbBlurHost = sinon.spy();
el.addEventListener('blur', cbBlurHost);
const cbBlurNativeInput = sinon.spy();
el._inputNode.addEventListener('blur', cbBlurNativeInput);
_inputNode.addEventListener('blur', cbBlurNativeInput);
await triggerFocusFor(el);
expect(document.activeElement).to.equal(el._inputNode);
expect(document.activeElement).to.equal(_inputNode);
expect(cbFocusHost.callCount).to.equal(1);
expect(cbFocusNativeInput.callCount).to.equal(1);
expect(cbBlurHost.callCount).to.equal(0);
@ -106,7 +115,7 @@ describe('<lion-field>', () => {
expect(cbBlurNativeInput.callCount).to.equal(1);
await triggerFocusFor(el);
expect(document.activeElement).to.equal(el._inputNode);
expect(document.activeElement).to.equal(_inputNode);
expect(cbFocusHost.callCount).to.equal(2);
expect(cbFocusNativeInput.callCount).to.equal(2);

View file

@ -318,7 +318,9 @@ describe('SyncUpdatableMixin', () => {
const tagString = defineCE(UpdatableImplementation);
const tag = unsafeStatic(tagString);
const el = /** @type {UpdatableImplementation} */ (fixtureSync(html`<${tag}></${tag}>`));
// @ts-ignore [allow-private] in tests
const ns = el.__SyncUpdatableNamespace;
// @ts-ignore [allow-protected] in tests
const updateSyncSpy = sinon.spy(el, 'updateSync');
expect(ns.connected).to.be.true;

View file

@ -1,5 +1,6 @@
import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing';
import { LionField } from '@lion/form-core';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
import { Required } from '../../src/validate/validators/Required.js';
/**
@ -17,6 +18,7 @@ class RequiredElement extends LionField {
super.connectedCallback();
}
/** @protected */
get _inputNode() {
return inputNodeTag || super._inputNode;
}
@ -37,7 +39,8 @@ describe('Required validation', async () => {
inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName));
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
@ -46,7 +49,8 @@ describe('Required validation', async () => {
inputNodeTag = /** @type {HTMLDivElementWithValue} */ (document.createElement('div'));
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 () => {
const el = /** @type {FormControlHost & HTMLElement} */ (await fixture(
@ -59,7 +63,8 @@ describe('Required validation', async () => {
inputNodeTag.setAttribute('role', role);
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
@ -69,6 +74,7 @@ describe('Required validation', async () => {
inputNodeTag.setAttribute('role', 'group');
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 sinon from 'sinon';
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

View file

@ -4,16 +4,13 @@ import { FormControlHost } from './FormControlMixinTypes';
export declare class FocusHost {
focused: boolean;
connectedCallback(): void;
disconnectedCallback(): void;
focus(): void;
blur(): void;
__onFocus(): void;
__onBlur(): void;
__registerEventsForFocusMixin(): void;
__teardownEventsForFocusMixin(): void;
private __onFocus(): void;
private __onBlur(): void;
private __registerEventsForFocusMixin(): void;
private __teardownEventsForFocusMixin(): void;
}
export declare function FocusImplementation<T extends Constructor<LitElement>>(

View file

@ -1,5 +1,5 @@
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 { DisabledHost } from '@lion/core/types/DisabledMixinTypes';
import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes';
@ -66,12 +66,12 @@ export declare class FormControlHost {
* controls until they are enabled.
* (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
* of the parent.
*/
public name: string;
name: string;
/**
* 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.
@ -84,29 +84,56 @@ export declare class FormControlHost {
* - For a number input: a formatted String '1.234,56' will be converted to a Number:
* 1234.56
*/
public get modelValue(): any | Unparseable;
public set modelValue(value: any | Unparseable);
get modelValue(): any | Unparseable;
set modelValue(value: any | Unparseable);
/**
* The label text for the input node.
* When no light dom defined via [slot=label], this value will be used
*/
public get label(): string;
public set label(arg: string);
__label: string;
get label(): string;
set label(arg: string);
/**
* The helpt text for the input node.
* When no light dom defined via [slot=help-text], this value will be used
*/
public get helpText(): string;
public set helpText(arg: string);
__helpText: string | undefined;
public set fieldName(arg: string);
public get fieldName(): string;
__fieldName: string | undefined;
get _inputNode(): HTMLElementWithValue;
get _labelNode(): HTMLElement;
get _helpTextNode(): HTMLElement;
get _feedbackNode(): LionValidationFeedback;
get helpText(): string;
set helpText(arg: string);
set fieldName(arg: string);
get fieldName(): string;
addToAriaLabelledBy(
element: HTMLElement,
customConfig?: {
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 _ariaLabelledNodes: HTMLElement[];
protected _ariaDescribedNodes: HTMLElement[];
@ -125,11 +152,7 @@ export declare class FormControlHost {
* to true to hide private internals in the formPath.
*/
protected _isRepropagationEndpoint: boolean;
connectedCallback(): void;
updated(changedProperties: import('@lion/core').PropertyValues): void;
render(): TemplateResult;
protected _parentFormGroup: FormControlHost | undefined;
protected _groupOneTemplate(): TemplateResult;
protected _groupTwoTemplate(): TemplateResult;
protected _labelTemplate(): TemplateResult;
@ -141,49 +164,29 @@ export declare class FormControlHost {
protected _inputGroupSuffixTemplate(): TemplateResult | typeof nothing;
protected _inputGroupAfterTemplate(): TemplateResult;
protected _feedbackTemplate(): TemplateResult;
protected _triggerInitialModelValueChangedEvent(): void;
protected _enhanceLightDomClasses(): void;
protected _enhanceLightDomA11y(): void;
protected _enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void;
__reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void;
protected _isEmpty(modelValue?: any): boolean;
protected _getAriaDescriptionElements(): HTMLElement[];
public addToAriaLabelledBy(
element: HTMLElement,
customConfig?: {
idPrefix?: string | undefined;
reorder?: boolean | undefined;
},
): void;
__reorderAriaLabelledNodes: boolean | undefined;
public addToAriaDescribedBy(
element: HTMLElement,
customConfig?: {
idPrefix?: string | undefined;
reorder?: boolean | undefined;
},
): void;
public removeFromAriaLabelledBy(
element: HTMLElement,
customConfig?: {
reorder?: boolean | undefined;
},
): void;
public removeFromAriaDescribedBy(
element: HTMLElement,
customConfig?: {
reorder?: boolean | undefined;
},
): void;
__reorderAriaDescribedNodes: boolean | undefined;
__getDirectSlotChild(slotName: string): HTMLElement | undefined;
__dispatchInitialModelValueChangedEvent(): void;
__repropagateChildrenInitialized: boolean | undefined;
protected _dispatchInitialModelValueChangedEvent(): void;
protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
__repropagateChildrenValues(ev: CustomEvent): void;
protected _parentFormGroup: FormControlHost | undefined;
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>>(

View file

@ -5,36 +5,32 @@ import { ValidateHost } from './validate/ValidateMixinTypes';
import { FormControlHost } from './FormControlMixinTypes';
export declare class FormatHost {
formattedValue: string;
serializedValue: string;
formatOn: string;
formatOptions: FormatNumberOptions;
__preventRecursiveTrigger: boolean;
__isHandlingUserInput: boolean;
parser(v: string, opts: FormatNumberOptions): unknown;
formatter(v: unknown, opts?: FormatNumberOptions): string;
serializer(v: unknown): string;
deserializer(v: string): unknown;
preprocessor(v: string): string;
formattedValue: string;
serializedValue: string;
formatOn: string;
formatOptions: FormatNumberOptions;
get value(): string;
set value(value: string);
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
private __callParser(value: string | undefined): object;
__callFormatter(): string;
protected _isHandlingUserInput: boolean;
protected _calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
_dispatchModelValueChangedEvent(): void;
protected _dispatchModelValueChangedEvent(): void;
protected _syncValueUpwards(): void;
_reflectBackFormattedValueToUser(): void;
_reflectBackFormattedValueDebounced(): void;
_reflectBackOn(): boolean;
protected _reflectBackFormattedValueToUser(): void;
protected _reflectBackFormattedValueDebounced(): void;
protected _reflectBackOn(): boolean;
protected _proxyInputEvent(): void;
_onUserInputChanged(): void;
protected _onUserInputChanged(): void;
connectedCallback(): void;
disconnectedCallback(): void;
private __preventRecursiveTrigger: boolean;
private __callParser(value: string | undefined): object;
private __callFormatter(): string;
}
export declare function FormatImplementation<T extends Constructor<LitElement>>(

View file

@ -19,25 +19,18 @@ export declare class InteractionStateHost {
touched: boolean;
dirty: boolean;
submitted: boolean;
_leaveEvent: string;
_valueChangedEvent: string;
initInteractionState(): void;
resetInteractionState(): void;
connectedCallback(): void;
disconnectedCallback(): void;
initInteractionState(): void;
resetInteractionState(): void;
_iStateOnLeave(): void;
_iStateOnValueChange(): void;
_onTouchedChanged(): void;
_onDirtyChanged(): void;
showFeedbackConditionFor(
type: string,
meta: InteractionStates,
currentCondition: Function,
): boolean;
_feedbackConditionMeta: InteractionStates;
protected _leaveEvent: string;
protected _valueChangedEvent: string;
protected _iStateOnLeave(): void;
protected _iStateOnValueChange(): void;
protected _onTouchedChanged(): void;
protected _onDirtyChanged(): void;
}
export declare function InteractionStateImplementation<T extends Constructor<LitElement>>(

View file

@ -1,22 +1,32 @@
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 {
get _inputNode(): HTMLTextAreaElement | HTMLInputElement;
}
// export declare class NativeTextField extends LionField {
// protected get _inputNode(): HTMLTextAreaElement | HTMLInputElement;
// }
export declare class NativeTextFieldHost {
// protected get _inputNode(): HTMLTextAreaElement | HTMLInputElement;
get selectionStart(): number;
set selectionStart(value: number);
get selectionEnd(): number;
set selectionEnd(value: number);
}
export declare function NativeTextFieldImplementation<T extends Constructor<NativeTextField>>(
export declare function NativeTextFieldImplementation<T extends Constructor<LitElement>>(
superclass: T,
): T &
Constructor<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;

View file

@ -6,50 +6,27 @@ import { InteractionStateHost } from '../InteractionStateMixinTypes';
export declare class ChoiceGroupHost {
multipleChoice: boolean;
connectedCallback(): void;
disconnectedCallback(): void;
get modelValue(): any;
set modelValue(value: any);
get serializedValue(): string;
set serializedValue(value: string);
get formattedValue(): string;
set formattedValue(value: string);
connectedCallback(): void;
disconnectedCallback(): void;
addFormElement(child: FormControlHost, indexToInsertAt: number): 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 _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>>(

View file

@ -15,64 +15,37 @@ export interface ChoiceInputSerializedValue {
}
export declare class ChoiceInputHost {
type: string;
serializedValue: ChoiceInputSerializedValue;
checked: boolean;
get modelValue(): ChoiceInputModelValue;
set modelValue(value: ChoiceInputModelValue);
serializedValue: ChoiceInputSerializedValue;
checked: boolean;
get choiceValue(): 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;
parser(): any;
formatter(modelValue: ChoiceInputModelValue): string;
render(): TemplateResult;
_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 _isHandlingUserInput: boolean;
protected get _inputNode(): HTMLElement;
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(
newV: { modelValue: ChoiceInputModelValue },
oldV: { modelValue: ChoiceInputModelValue },
): void;
parser(): any;
formatter(modelValue: ChoiceInputModelValue): string;
protected _isEmpty(): void;
protected _syncValueUpwards(): void;
type: string;
get _inputNode(): HTMLElement;
private __syncModelCheckedToChecked(checked: boolean): void;
private __syncCheckedToModel(checked: boolean): void;
private __syncCheckedToInputElement(): void;
}
export declare function ChoiceInputImplementation<T extends Constructor<LitElement>>(

View file

@ -7,24 +7,26 @@ import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes';
import { ValidateHost } from '../validate/ValidateMixinTypes';
export declare class FormGroupHost {
protected static _addDescriptionElementIdsToField(): void;
get _inputNode(): HTMLElement;
submitGroup(): void;
resetGroup(): void;
prefilled: boolean;
touched: boolean;
dirty: boolean;
submitted: boolean;
serializedValue: { [key: string]: any };
get modelValue(): { [x: string]: any };
set modelValue(value: { [x: string]: any });
formattedValue: string;
children: Array<HTMLElement & FormControlHost>;
_initialModelValue: { [x: string]: any };
_setValueForAllFormElements(property: string, value: any): void;
get modelValue(): { [x: string]: any };
set modelValue(value: { [x: string]: any });
resetInteractionState(): 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>>(

View file

@ -4,10 +4,8 @@ import { FormRegistrarHost } from './FormRegistrarMixinTypes';
import { LitElement } from '@lion/core';
export declare class FormRegisteringHost {
connectedCallback(): void;
disconnectedCallback(): void;
name: string;
protected _parentFormGroup: FormRegistrarHost | undefined;
public name: string;
}
export declare function FormRegisteringImplementation<T extends Constructor<LitElement>>(

View file

@ -9,7 +9,6 @@ export declare class ElementWithParentFormGroup {
}
export declare class FormRegistrarHost {
protected _isFormOrFieldset: boolean;
formElements: FormControlsCollection & { [x: string]: any };
addFormElement(
child:
@ -19,10 +18,11 @@ export declare class FormRegistrarHost {
indexToInsertAt?: number,
): void;
removeFormElement(child: FormRegisteringHost): void;
_onRequestToAddFormElement(e: CustomEvent): void;
isRegisteredFormElement(el: FormControlHost): boolean;
registrationComplete: Promise<boolean>;
initComplete: Promise<boolean>;
protected _isFormOrFieldset: boolean;
protected _onRequestToAddFormElement(e: CustomEvent): void;
protected _completeRegistration(): void;
}

View file

@ -3,7 +3,7 @@ import { LitElement } from '@lion/core';
export declare class FormRegistrarPortalHost {
registrationTarget: HTMLElement;
__redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
private __redispatchEventForFormRegistrarPortalMixin(ev: CustomEvent): void;
}
export declare function FormRegistrarPortalImplementation<T extends Constructor<LitElement>>(

View file

@ -10,13 +10,10 @@ export declare interface SyncUpdatableNamespace {
}
export declare class SyncUpdatableHost {
static __syncUpdatableHasChanged(name: string, newValue: any, oldValue: any): boolean;
updateSync(name: string, oldValue: any): void;
__syncUpdatableInitialize(): void;
__SyncUpdatableNamespace: SyncUpdatableNamespace;
firstUpdated(changedProperties: PropertyValues): void;
disconnectedCallback(): void;
protected updateSync(name: string, oldValue: any): void;
private __syncUpdatableInitialize(): void;
private __SyncUpdatableNamespace: SyncUpdatableNamespace;
private static __syncUpdatableHasChanged(name: string, newValue: any, oldValue: any): boolean;
}
export type SyncUpdatableHostType = typeof SyncUpdatableHost;

View file

@ -1,6 +1,6 @@
import { LitElement } from '@lion/core';
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 { ScopedElementsHost } from '@open-wc/scoped-elements/src/types';
import { FormControlHost } from '../FormControlMixinTypes';
@ -26,51 +26,47 @@ export declare class ValidateHost {
validationStates: { [key: string]: { [key: string]: Object } };
isPending: boolean;
defaultValidators: Validator[];
_visibleMessagesAmount: number;
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>;
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[],
value: unknown,
opts: { hasAsync: boolean },
): void;
__executeAsyncValidators(asyncValidators: Validator[], value: unknown): void;
__executeResultValidators(regularValidationResult: Validator[]): Validator[];
__finishValidation(options: { source: 'sync' | 'async'; hasAsync?: boolean }): void;
__clearValidationResults(): void;
__onValidatorUpdated(e: Event | CustomEvent): void;
__setupValidators(): void;
__isEmpty(v: unknown): boolean;
__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;
private __executeAsyncValidators(asyncValidators: Validator[], value: unknown): void;
private __executeResultValidators(regularValidationResult: Validator[]): Validator[];
private __finishValidation(options: { source: 'sync' | 'async'; hasAsync?: boolean }): void;
private __clearValidationResults(): void;
private __onValidatorUpdated(e: Event | CustomEvent): void;
private __setupValidators(): void;
private __isEmpty(v: unknown): boolean;
private __getFeedbackMessages(validators: Validator[]): Promise<FeedbackMessage[]>;
}
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 { LionInput } from '@lion/input';
import sinon from 'sinon';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
describe('Form Validation Integrations', () => {
const lightDom = '';
@ -49,8 +50,9 @@ describe('Form Validation Integrations', () => {
]}
>${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.touched = true;
@ -61,7 +63,7 @@ describe('Form Validation Integrations', () => {
el.modelValue = 'warn';
await el.updateComplete;
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('warning');
expect(_feedbackNode.feedbackData?.[0].message).to.equal('warning');
el.modelValue = 'war';
await el.updateComplete;
@ -76,14 +78,14 @@ describe('Form Validation Integrations', () => {
'Changed!',
'Ok, correct.',
]).to.include(
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */ (el
._feedbackNode.feedbackData)[0].message,
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */
(_feedbackNode.feedbackData)[0].message,
);
el.modelValue = '';
await el.updateComplete;
await el.feedbackComplete;
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('error');
expect(_feedbackNode.feedbackData?.[0].message).to.equal('error');
el.modelValue = 'war';
await el.updateComplete;
@ -98,13 +100,15 @@ describe('Form Validation Integrations', () => {
'Changed!',
'Ok, correct.',
]).to.include(
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */ (el
._feedbackNode.feedbackData)[0].message,
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */
(_feedbackNode.feedbackData)[0].message,
);
// Check that change in focused or other interaction states does not refresh the success message
// without a change in validation results
// @ts-ignore [allow-protected] in test
const spy = sinon.spy(el, '_updateFeedbackComponent');
// @ts-ignore [allow-protected] in test
el._updateShouldShowFeedbackFor();
await el.updateComplete;
await el.feedbackComplete;

View file

@ -29,6 +29,8 @@ import '@lion/fieldset/define';
import '@lion/form/define';
import '@lion/form-core/define';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
/**
* @typedef {import('@lion/core').LitElement} LitElement
* @typedef {import('@lion/form-core').LionField} LionField
@ -129,7 +131,7 @@ const choiceGroupDispatchesCountOnFirstPaint = (groupTagname, itemTagname, count
it(getFirstPaintTitle(count), async () => {
const spy = sinon.spy();
await fixture(html`
<${groupTag} @model-value-changed="${spy}">
<${groupTag} @model-value-changed="${spy}" name="group[]">
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
@ -151,7 +153,7 @@ const choiceGroupDispatchesCountOnInteraction = (groupTagname, itemTagname, coun
it(getInteractionTitle(count), async () => {
const spy = sinon.spy();
const el = await fixture(html`
<${groupTag}>
<${groupTag} name="group[]">
<${itemTag} .choiceValue="${'option1'}"></${itemTag}>
<${itemTag} .choiceValue="${'option2'}"></${itemTag}>
<${itemTag} .choiceValue="${'option3'}"></${itemTag}>
@ -417,14 +419,15 @@ describe('detail.isTriggeredByUser', () => {
* @param {string | undefined} [triggerType]
*/
function mimicUserInput(el, newViewValue, triggerType) {
const { _inputNode } = getFormControlMembers(el);
const type = detectType(el);
let userInputEv;
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._inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true }));
_inputNode.dispatchEvent(new Event(userInputEv, { bubbles: true }));
} else if (type === 'ChoiceField') {
el._inputNode.dispatchEvent(new Event('change', { bubbles: true }));
_inputNode.dispatchEvent(new Event('change', { bubbles: true }));
} else if (type === 'OptionChoiceField') {
if (!triggerType) {
el.dispatchEvent(new Event('click', { bubbles: true }));
@ -456,8 +459,9 @@ describe('detail.isTriggeredByUser', () => {
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(
html`<${tag}>${childrenEl}</${tag}>`,
html`<${tag} name="${name}">${childrenEl}</${tag}>`,
));
await el.registrationComplete;
el.addEventListener('model-value-changed', spy);

View file

@ -2,11 +2,13 @@ import { html } from '@lion/core';
import { localize } from '@lion/localize';
import { localizeTearDown } from '@lion/localize/test-helpers';
import { aTimeout, expect, fixture } from '@open-wc/testing';
import { getInputMembers } from '@lion/input/test-helpers';
import '@lion/input-amount/define';
import { formatAmount } from '../src/formatters.js';
import { parseAmount } from '../src/parsers.js';
/**
* @typedef {import('@lion/input/src/LionInput').LionInput} LionInput
* @typedef {import('../src/LionInputAmount').LionInputAmount} LionInputAmount
*/
@ -73,14 +75,16 @@ describe('<lion-input-amount>', () => {
const el = /** @type {LionInputAmount} */ (await fixture(
`<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 () => {
const el = /** @type {LionInputAmount} */ (await fixture(
`<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 () => {
@ -143,7 +147,8 @@ describe('<lion-input-amount>', () => {
`<lion-input-amount currency="EUR"></lion-input-amount>`,
));
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 () => {

View file

@ -3,6 +3,7 @@ import { localize } from '@lion/localize';
import { localizeTearDown } from '@lion/localize/test-helpers';
import { MaxDate } from '@lion/form-core';
import { expect, fixture as _fixture } from '@open-wc/testing';
import { getInputMembers } from '@lion/input/test-helpers';
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 () => {
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 () => {

View file

@ -1,5 +1,5 @@
import { expect, fixture as _fixture } from '@open-wc/testing';
import { getInputMembers } from '@lion/input/test-helpers';
import '@lion/input-email/define';
/**
@ -11,7 +11,8 @@ const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionInputEmai
describe('<lion-input-email>', () => {
it('has a type = text', async () => {
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 () => {

View file

@ -1,10 +1,9 @@
import { expect, fixture as _fixture } from '@open-wc/testing';
import { html } from '@lion/core';
import { getInputMembers } from '@lion/input/test-helpers';
import { IsCountryIBAN } from '../src/validators.js';
import { formatIBAN } from '../src/formatters.js';
import { parseIBAN } from '../src/parsers.js';
import '@lion/input-iban/define';
/**
@ -26,7 +25,8 @@ describe('<lion-input-iban>', () => {
it('has a type = text', async () => {
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 () => {

View file

@ -50,6 +50,7 @@
"exports": {
".": "./index.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
*/
export class LionInput extends NativeTextFieldMixin(
/** @type {typeof import('@lion/form-core/types/NativeTextFieldMixinTypes').NativeTextField} */ (LionField),
) {
export class LionInput extends NativeTextFieldMixin(LionField) {
/** @type {any} */
static get properties() {
return {
@ -50,6 +48,10 @@ export class LionInput extends NativeTextFieldMixin(
};
}
/**
* @type {HTMLInputElement}
* @protected
*/
get _inputNode() {
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 { expect, fixture, html, unsafeStatic, triggerFocusFor, aTimeout } from '@open-wc/testing';
import { getInputMembers } from '../test-helpers/index.js';
import '@lion/input/define';
/**
@ -13,43 +13,51 @@ const tag = unsafeStatic(tagString);
describe('<lion-input>', () => {
it('delegates readOnly property and readonly attribute', async () => {
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;
await el.updateComplete;
expect(el.readOnly).to.equal(false);
expect(el._inputNode.readOnly).to.equal(false);
expect(_inputNode.readOnly).to.equal(false);
});
it('delegates value attribute', async () => {
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 () => {
const elDisabled = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
expect(elDisabled.disabled).to.equal(true);
expect(elDisabled._inputNode.disabled).to.equal(true);
const el = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
const { _inputNode } = getInputMembers(el);
expect(el.disabled).to.equal(true);
expect(_inputNode.disabled).to.equal(true);
});
it('can be disabled via property', async () => {
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
const { _inputNode } = getInputMembers(el);
el.disabled = true;
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.
it('is disabled when disabled property is passed', async () => {
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
expect(el._inputNode.hasAttribute('disabled')).to.equal(false);
const { _inputNode } = getInputMembers(el);
expect(_inputNode.hasAttribute('disabled')).to.equal(false);
el.disabled = true;
await el.updateComplete;
await aTimeout(0);
expect(el._inputNode.hasAttribute('disabled')).to.equal(true);
const disabledel = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
expect(disabledel._inputNode.hasAttribute('disabled')).to.equal(true);
expect(_inputNode.hasAttribute('disabled')).to.equal(true);
const disabledEl = /** @type {LionInput} */ (await fixture(html`<${tag} disabled></${tag}>`));
const { _inputNode: _inputNodeDisabled } = getInputMembers(disabledEl);
expect(_inputNodeDisabled.hasAttribute('disabled')).to.equal(true);
});
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'
it('delegates autocomplete property', async () => {
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
expect(el._inputNode.autocomplete).to.equal('');
expect(el._inputNode.hasAttribute('autocomplete')).to.be.false;
const { _inputNode } = getInputMembers(el);
expect(_inputNode.autocomplete).to.equal('');
expect(_inputNode.hasAttribute('autocomplete')).to.be.false;
el.autocomplete = 'off';
await el.updateComplete;
expect(el._inputNode.autocomplete).to.equal('off');
expect(el._inputNode.getAttribute('autocomplete')).to.equal('off');
expect(_inputNode.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 () => {
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
const { _inputNode } = getInputMembers(el);
await triggerFocusFor(el);
await el.updateComplete;
el._inputNode.value = 'hello world';
el._inputNode.selectionStart = 2;
el._inputNode.selectionEnd = 2;
_inputNode.value = 'hello world';
_inputNode.selectionStart = 2;
_inputNode.selectionEnd = 2;
el.value = 'hey there universe';
expect(el._inputNode.selectionStart).to.equal(2);
expect(el._inputNode.selectionEnd).to.equal(2);
expect(_inputNode.selectionStart).to.equal(2);
expect(_inputNode.selectionEnd).to.equal(2);
});
it('automatically creates an <input> element if not provided by user', async () => {
const el = /** @type {LionInput} */ (await fixture(html`
<${tag}></${tag}>
`));
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 () => {
const el = /** @type {LionInput} */ (await fixture(html`<${tag}></${tag}>`));
const { _inputNode } = getInputMembers(el);
expect(el.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';
await el.updateComplete;
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 () => {
const el = /** @type {LionInput} */ (await fixture(html`<${tag} placeholder="text"></${tag}>`));
const { _inputNode } = getInputMembers(el);
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';
await el.updateComplete;
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 () => {
@ -162,10 +180,12 @@ describe('<lion-input>', () => {
describe('Delegation', () => {
it('delegates property value', async () => {
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';
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 () => {
@ -174,11 +194,12 @@ describe('<lion-input>', () => {
.modelValue=${'Some text to select'}
></${tag}>
`));
const { _inputNode } = getInputMembers(el);
el.selectionStart = 5;
el.selectionEnd = 12;
expect(el._inputNode.selectionStart).to.equal(5);
expect(el._inputNode.selectionEnd).to.equal(12);
expect(_inputNode.selectionStart).to.equal(5);
expect(_inputNode.selectionEnd).to.equal(12);
});
});

View file

@ -54,6 +54,7 @@
"customElementsManifest": "custom-elements.json",
"exports": {
".": "./index.js",
"./test-helpers": "./test-helpers/index.js",
"./test-suites": "./test-suites/index.js",
"./define": "./define.js",
"./define-listbox": "./lion-listbox.js",

View file

@ -128,7 +128,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
return;
}
const parentForm = /** @type {unknown} */ (this._parentFormGroup);
this.__isHandlingUserInput = true;
this._isHandlingUserInput = true;
if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) {
this.checked = !this.checked;
this.active = !this.active;
@ -136,6 +136,6 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
this.checked = true;
this.active = true;
}
this.__isHandlingUserInput = false;
this._isHandlingUserInput = false;
}
}

View file

@ -528,11 +528,11 @@ const ListboxMixinImplementation = superclass =>
return;
}
this.__isHandlingUserInput = true;
this._isHandlingUserInput = true;
setTimeout(() => {
// Since we can't control when subclasses are done handling keyboard input, we
// schedule a timeout to reset __isHandlingUserInput
this.__isHandlingUserInput = false;
// schedule a timeout to reset _isHandlingUserInput
this._isHandlingUserInput = false;
});
const { key } = ev;
@ -638,11 +638,11 @@ const ListboxMixinImplementation = superclass =>
return;
}
this.__isHandlingUserInput = true;
this._isHandlingUserInput = true;
setTimeout(() => {
// Since we can't control when subclasses are done handling keyboard input, we
// schedule a timeout to reset __isHandlingUserInput
this.__isHandlingUserInput = false;
// schedule a timeout to reset _isHandlingUserInput
this._isHandlingUserInput = false;
});
const { key } = ev;
@ -769,7 +769,7 @@ const ListboxMixinImplementation = superclass =>
new CustomEvent('model-value-changed', {
detail: /** @type {ModelValueEventDetails} */ ({
formPath: ev.detail.formPath,
isTriggeredByUser: ev.detail.isTriggeredByUser || this.__isHandlingUserInput,
isTriggeredByUser: ev.detail.isTriggeredByUser || this._isHandlingUserInput,
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 { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing';
import sinon from 'sinon';
import { getListboxMembers } from '../test-helpers/index.js';
/**
* @typedef {import('../src/LionListbox').LionListbox} LionListbox
@ -23,22 +24,22 @@ function mimicKeyPress(el, key) {
el.dispatchEvent(new KeyboardEvent('keyup', { key }));
}
/**
* @param {LionListbox} lionListboxEl
*/
function getProtectedMembers(lionListboxEl) {
// @ts-ignore protected members allowed in test
const {
_inputNode: input,
_activeDescendantOwnerNode: activeDescendantOwner,
_listboxNode: listbox,
} = lionListboxEl;
return {
input,
activeDescendantOwner,
listbox,
};
}
// /**
// * @param {LionListbox} lionListboxEl
// */
// function getProtectedMembers(lionListboxEl) {
// // @ts-ignore protected members allowed in test
// const {
// _inputNode: input,
// _activeDescendantOwnerNode: activeDescendantOwner,
// _listboxNode: listbox,
// } = lionListboxEl;
// return {
// input,
// activeDescendantOwner,
// listbox,
// };
// }
/**
* @param { {tagString?:string, optionTagString?:string} } [customConfig]
@ -253,14 +254,16 @@ export function runListboxMixinSuite(customConfig = {}) {
const el1 = await fixture(html`
<${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`
<${tag}>
<label slot="label">bar</label>
</${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 () => {
@ -274,6 +277,7 @@ export function runListboxMixinSuite(customConfig = {}) {
const el = await fixture(html`
<${tag} label="foo" .fieldName="${'bar'}"></${tag}>
`);
// @ts-ignore [allow-proteced] in test
expect(el.__fieldName).to.equal(el.fieldName);
});
@ -370,9 +374,9 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue=${30} id="predefined">Item 3</${optionTag}>
</${tag}>
`);
expect(el.querySelectorAll('lion-option')[0].id).to.exist;
expect(el.querySelectorAll('lion-option')[1].id).to.exist;
expect(el.querySelectorAll('lion-option')[2].id).to.equal('predefined');
expect(el.querySelectorAll(cfg.optionTagString)[0].id).to.exist;
expect(el.querySelectorAll(cfg.optionTagString)[1].id).to.exist;
expect(el.querySelectorAll(cfg.optionTagString)[2].id).to.equal('predefined');
});
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}>
</${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;
// Normalize
el.activeIndex = 0;
await el.updateComplete;
expect(activeDescendantOwner.getAttribute('aria-activedescendant')).to.equal('first');
mimicKeyPress(activeDescendantOwner, 'ArrowDown');
// activeDescendantOwner.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
expect(_activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal('first');
mimicKeyPress(_activeDescendantOwnerNode, 'ArrowDown');
// _activeDescendantOwnerNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
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 () => {
@ -542,21 +546,21 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// Normalize
el.activeIndex = 0;
const options = el.formElements;
// mimicKeyPress(listbox, 'ArrowUp');
mimicKeyPress(listbox, 'ArrowUp');
mimicKeyPress(_listboxNode, 'ArrowUp');
expect(options[0].active).to.be.true;
expect(options[1].active).to.be.false;
expect(options[2].active).to.be.false;
el.activeIndex = 2;
// mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(options[0].active).to.be.false;
expect(options[1].active).to.be.false;
@ -571,26 +575,27 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${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;
// Normalize
el.activeIndex = 0;
expect(el.activeIndex).to.equal(0);
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
mimicKeyPress(el._inputNode, 'ArrowUp');
mimicKeyPress(_inputNode, 'ArrowUp');
await el.updateComplete;
expect(el.activeIndex).to.equal(2);
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
mimicKeyPress(el._inputNode, 'ArrowDown');
mimicKeyPress(_inputNode, 'ArrowDown');
expect(el.activeIndex).to.equal(0);
// Extra check: regular navigation
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
mimicKeyPress(el._inputNode, 'ArrowDown');
mimicKeyPress(_inputNode, 'ArrowDown');
expect(el.activeIndex).to.equal(1);
});
@ -605,14 +610,14 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// Normalize suite
el.activeIndex = 0;
const options = el.formElements;
el.checkedIndex = 0;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'ArrowDown');
mimicKeyPress(_listboxNode, 'Enter');
expect(options[1].checked).to.be.true;
});
});
@ -628,21 +633,21 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// Normalize suite
el.activeIndex = 0;
const options = el.formElements;
el.checkedIndex = 0;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(listbox, ' ');
mimicKeyPress(_listboxNode, 'ArrowDown');
mimicKeyPress(_listboxNode, ' ');
expect(options[1].checked).to.be.true;
el.checkedIndex = 0;
// @ts-ignore allow protected member access in test
el._listboxReceivesNoFocus = true;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(listbox, ' ');
mimicKeyPress(_listboxNode, 'ArrowDown');
mimicKeyPress(_listboxNode, ' ');
expect(options[1].checked).to.be.false;
});
@ -683,7 +688,7 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// @ts-ignore allow protected members in tests
if (el._listboxReceivesNoFocus) {
@ -691,9 +696,9 @@ export function runListboxMixinSuite(customConfig = {}) {
}
el.activeIndex = 2;
mimicKeyPress(listbox, 'Home');
mimicKeyPress(_listboxNode, 'Home');
expect(el.activeIndex).to.equal(0);
mimicKeyPress(listbox, 'End');
mimicKeyPress(_listboxNode, 'End');
expect(el.activeIndex).to.equal(3);
});
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}>
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// Normalize across listbox/select-rich/combobox
el.activeIndex = 0;
@ -713,10 +718,10 @@ export function runListboxMixinSuite(customConfig = {}) {
el.selectionFollowsFocus = false;
expect(el.activeIndex).to.equal(0);
expect(el.checkedIndex).to.equal(-1);
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(-1);
mimicKeyPress(listbox, 'ArrowUp');
mimicKeyPress(_listboxNode, 'ArrowUp');
expect(el.activeIndex).to.equal(0);
expect(el.checkedIndex).to.equal(-1);
@ -731,7 +736,7 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
expect(el.orientation).to.equal('vertical');
const options = el.formElements;
@ -742,23 +747,23 @@ export function runListboxMixinSuite(customConfig = {}) {
expect(options[0].active).to.be.true;
expect(options[1].active).to.be.false;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(options[0].active).to.be.false;
expect(options[1].active).to.be.true;
mimicKeyPress(listbox, 'ArrowUp');
mimicKeyPress(_listboxNode, 'ArrowUp');
expect(options[0].active).to.be.true;
expect(options[1].active).to.be.false;
// No response to horizontal arrows...
mimicKeyPress(listbox, 'ArrowRight');
mimicKeyPress(_listboxNode, 'ArrowRight');
expect(options[0].active).to.be.true;
expect(options[1].active).to.be.false;
el.activeIndex = 1;
mimicKeyPress(listbox, 'ArrowLeft');
mimicKeyPress(_listboxNode, 'ArrowLeft');
expect(options[0].active).to.be.false;
expect(options[1].active).to.be.true;
@ -771,7 +776,7 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
expect(el.orientation).to.equal('horizontal');
@ -780,20 +785,20 @@ export function runListboxMixinSuite(customConfig = {}) {
await el.updateComplete;
mimicKeyPress(listbox, 'ArrowRight');
mimicKeyPress(_listboxNode, 'ArrowRight');
expect(el.activeIndex).to.equal(1);
mimicKeyPress(listbox, 'ArrowLeft');
mimicKeyPress(_listboxNode, 'ArrowLeft');
expect(el.activeIndex).to.equal(0);
// No response to vertical arrows...
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.activeIndex).to.equal(0);
el.activeIndex = 1;
mimicKeyPress(listbox, 'ArrowUp');
mimicKeyPress(_listboxNode, 'ArrowUp');
expect(el.activeIndex).to.equal(1);
});
@ -806,8 +811,8 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
expect(listbox.getAttribute('aria-orientation')).to.equal('horizontal');
const { _listboxNode } = getListboxMembers(el);
expect(_listboxNode.getAttribute('aria-orientation')).to.equal('horizontal');
});
});
});
@ -838,7 +843,7 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Victoria Plum'}">Victoria Plum</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
const options = el.formElements;
// @ts-ignore feature detection select-rich
@ -864,13 +869,13 @@ export function runListboxMixinSuite(customConfig = {}) {
// Enter
el.activeIndex = 0;
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'Enter');
el.activeIndex = 1;
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'Enter');
expect(options[0].checked).to.equal(true);
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
// also deselect
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'Enter');
expect(options[0].checked).to.equal(true);
expect(el.modelValue).to.eql(['Artichoke']);
@ -885,15 +890,15 @@ export function runListboxMixinSuite(customConfig = {}) {
// Space
el.activeIndex = 0;
mimicKeyPress(listbox, ' ');
mimicKeyPress(_listboxNode, ' ');
el.activeIndex = 1;
mimicKeyPress(listbox, ' ');
mimicKeyPress(_listboxNode, ' ');
expect(options[0].checked).to.equal(true);
expect(el.modelValue).to.eql(['Artichoke', 'Chard']);
// also deselect
mimicKeyPress(listbox, ' ');
mimicKeyPress(_listboxNode, ' ');
expect(options[0].checked).to.equal(true);
expect(el.modelValue).to.eql(['Artichoke']);
@ -907,8 +912,8 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
expect(listbox.getAttribute('aria-multiselectable')).to.equal('true');
const { _listboxNode } = getListboxMembers(el);
expect(_listboxNode.getAttribute('aria-multiselectable')).to.equal('true');
});
it('does not allow "selectionFollowsFocus"', async () => {
@ -918,11 +923,11 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
</${tag}>
`);
const { listbox, input } = getProtectedMembers(el);
const { _listboxNode, _inputNode } = getListboxMembers(el);
input.focus();
listbox.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
expect(listbox.getAttribute('aria-multiselectable')).to.equal('true');
_inputNode.focus();
_listboxNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown' }));
expect(_listboxNode.getAttribute('aria-multiselectable')).to.equal('true');
});
});
});
@ -950,7 +955,7 @@ export function runListboxMixinSuite(customConfig = {}) {
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
const options = el.formElements;
// Normalize start values between listbox, slect and combobox and test interaction below
el.activeIndex = 0;
@ -958,11 +963,11 @@ export function runListboxMixinSuite(customConfig = {}) {
expect(el.activeIndex).to.equal(0);
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(1);
expectOnlyGivenOneOptionToBeChecked(options, 1);
mimicKeyPress(listbox, 'ArrowUp');
mimicKeyPress(_listboxNode, 'ArrowUp');
expect(el.activeIndex).to.equal(0);
expect(el.checkedIndex).to.equal(0);
@ -990,7 +995,7 @@ export function runListboxMixinSuite(customConfig = {}) {
</${tag}>
`));
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
const options = el.formElements;
// Normalize start values between listbox, slect and combobox and test interaction below
el.activeIndex = 0;
@ -998,12 +1003,12 @@ export function runListboxMixinSuite(customConfig = {}) {
expect(el.activeIndex).to.equal(0);
expect(el.checkedIndex).to.equal(0);
expectOnlyGivenOneOptionToBeChecked(options, 0);
mimicKeyPress(listbox, 'ArrowRight');
mimicKeyPress(_listboxNode, 'ArrowRight');
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(1);
expectOnlyGivenOneOptionToBeChecked(options, 1);
mimicKeyPress(listbox, 'ArrowLeft');
mimicKeyPress(_listboxNode, 'ArrowLeft');
expect(el.activeIndex).to.equal(0);
expect(el.checkedIndex).to.equal(0);
@ -1018,7 +1023,7 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue=${'40'}>Item 4</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// @ts-ignore allow protected
if (el._listboxReceivesNoFocus) {
@ -1026,9 +1031,9 @@ export function runListboxMixinSuite(customConfig = {}) {
}
expect(el.modelValue).to.equal('30');
mimicKeyPress(listbox, 'Home');
mimicKeyPress(_listboxNode, 'Home');
expect(el.modelValue).to.equal('10');
mimicKeyPress(listbox, 'End');
mimicKeyPress(_listboxNode, 'End');
expect(el.modelValue).to.equal('40');
});
});
@ -1041,11 +1046,11 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} checked .choiceValue=${'20'}>Item 2</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
await el.updateComplete;
const { checkedIndex } = el;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.checkedIndex).to.equal(checkedIndex);
});
@ -1096,16 +1101,16 @@ export function runListboxMixinSuite(customConfig = {}) {
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
el.activeIndex = 0;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(0);
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'Enter');
// Checked index stays where it was
expect(el.checkedIndex).to.equal(0);
});
@ -1118,16 +1123,16 @@ export function runListboxMixinSuite(customConfig = {}) {
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
// Normalize activeIndex across multiple implementers of ListboxMixinSuite
el.activeIndex = 0;
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.activeIndex).to.equal(1);
expect(el.checkedIndex).to.equal(-1);
mimicKeyPress(listbox, 'ArrowDown');
mimicKeyPress(_listboxNode, 'ArrowDown');
expect(el.activeIndex).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}>
</${tag}>
`);
const { activeDescendantOwner } = getProtectedMembers(el);
const { _activeDescendantOwnerNode } = getListboxMembers(el);
const opt = el.formElements[1];
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 () => {
@ -1326,15 +1331,15 @@ export function runListboxMixinSuite(customConfig = {}) {
<${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
expect(listbox).to.exist;
expect(listbox).to.be.instanceOf(LionOptions);
expect(el.querySelector('[role=listbox]')).to.equal(listbox);
expect(_listboxNode).to.exist;
expect(_listboxNode).to.be.instanceOf(LionOptions);
expect(el.querySelector('[role=listbox]')).to.equal(_listboxNode);
expect(el.formElements.length).to.equal(2);
expect(listbox.children.length).to.equal(2);
expect(listbox.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase());
expect(_listboxNode.children.length).to.equal(2);
expect(_listboxNode.children[0].tagName).to.equal(cfg.optionTagString.toUpperCase());
});
});
@ -1347,14 +1352,14 @@ export function runListboxMixinSuite(customConfig = {}) {
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
el.activeIndex = 1;
// Allow options that behave like anchors (think of Google Search) to trigger the anchor behavior
const activeOption = el.formElements[1];
const clickSpy = sinon.spy(activeOption, 'click');
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'Enter');
expect(clickSpy).to.have.been.calledOnce;
});
@ -1367,14 +1372,14 @@ export function runListboxMixinSuite(customConfig = {}) {
</${tag}>
`);
const { listbox } = getProtectedMembers(el);
const { _listboxNode } = getListboxMembers(el);
el.activeIndex = 0;
const activeOption = el.formElements[0];
const clickSpy = sinon.spy(activeOption, 'click');
mimicKeyPress(listbox, 'Enter');
mimicKeyPress(_listboxNode, 'Enter');
expect(clickSpy).to.not.have.been.called;
});

View file

@ -53,7 +53,7 @@
".": "./index.js",
"./test-suites": "./test-suites/index.js",
"./translations/*": "./translations/*",
"./test-helpers": "./test-helpers.js",
"./test-helpers": "./test-helpers/index.js",
"./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 { keyCodes } from '../src/utils/key-codes.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

View file

@ -13,10 +13,29 @@ import {
fixture as _fixture,
} from '@open-wc/testing';
import { LionSelectInvoker, LionSelectRich } from '@lion/select-rich';
import '@lion/core/differentKeyEventNamesShimIE';
import '@lion/listbox/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
@ -24,43 +43,14 @@ import '@lion/select-rich/define';
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', () => {
it('clicking the label should focus the invoker', async () => {
const el = await fixture(html` <lion-select-rich label="foo"> </lion-select-rich> `);
expect(document.activeElement === document.body).to.be.true;
const { label } = getProtectedMembers(el);
label.click();
const { _labelNode, _invokerNode } = getSelectRichMembers(el);
_labelNode.click();
// @ts-ignore allow protected access in tests
expect(document.activeElement === el._invokerNode).to.be.true;
expect(document.activeElement === _invokerNode).to.be.true;
});
it('checks the first enabled option', async () => {
@ -95,19 +85,18 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-select-rich>
`);
const { invoker } = getProtectedMembers(el);
expect(invoker.selectedElement).to.be.undefined;
const { _invokerNode } = getSelectRichMembers(el);
expect(_invokerNode.selectedElement).to.be.undefined;
expect(el.modelValue).to.equal('');
});
describe('Invoker', () => {
it('generates an lion-select-invoker if no invoker is provided', async () => {
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
const { _invokerNode } = getSelectRichMembers(el);
// @ts-ignore allow protected access in tests
expect(el._invokerNode).to.exist;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.tagName).to.include('LION-SELECT-INVOKER');
expect(_invokerNode).to.exist;
expect(_invokerNode).to.be.instanceOf(LionSelectInvoker);
});
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-select-rich>
`);
const { _invokerNode } = getSelectRichMembers(el);
const options = el.formElements;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
expect(_invokerNode.selectedElement).dom.to.equal(options[0]);
});
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-select-rich>
`);
const { _invokerNode } = getSelectRichMembers(el);
const options = el.querySelectorAll('lion-option');
// @ts-ignore allow protected access in tests
expect(el._invokerNode.selectedElement).dom.to.equal(options[1]);
expect(_invokerNode.selectedElement).dom.to.equal(options[1]);
el.checkedIndex = 0;
await el.updateComplete;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.selectedElement).dom.to.equal(options[0]);
expect(_invokerNode.selectedElement).dom.to.equal(options[0]);
});
it('delegates readonly to the invoker', async () => {
@ -146,10 +134,9 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-select-rich>
`);
const { _invokerNode } = getSelectRichMembers(el);
expect(el.hasAttribute('readonly')).to.be.true;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.hasAttribute('readonly')).to.be.true;
expect(_invokerNode.hasAttribute('readonly')).to.be.true;
});
it('delegates singleOption to the invoker', async () => {
@ -158,10 +145,9 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${10}>Item 1</lion-option>
</lion-select-rich>
`);
const { _invokerNode } = getSelectRichMembers(el);
expect(el.singleOption).to.be.true;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.hasAttribute('single-option')).to.be.true;
expect(_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 () => {
@ -189,14 +175,16 @@ describe('lion-select-rich', () => {
</lion-select-rich>
`);
// @ts-ignore allow protected access in tests
expect(el._invokerNode.shadowRoot.firstElementChild.textContent).to.equal('10');
const { _invokerNode } = getSelectRichMembers(el);
const firstChild = /** @type {HTMLElement} */ (
/** @type {ShadowRoot} */ (_invokerNode.shadowRoot).firstElementChild
);
expect(firstChild.textContent).to.equal('10');
firstOption.modelValue = { value: 30, checked: true };
await firstOption.updateComplete;
await el.updateComplete;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.shadowRoot.firstElementChild.textContent).to.equal('30');
expect(firstChild.textContent).to.equal('30');
});
// 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;
const options = el.formElements;
await el.updateComplete;
const { invoker } = getProtectedMembers(el);
expect(invoker.clientWidth).to.equal(options[1].clientWidth);
const { _invokerNode, _inputNode } = getSelectRichMembers(el);
expect(_invokerNode.clientWidth).to.equal(options[1].clientWidth);
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
newOption.choiceValue = 30;
newOption.textContent = '30 with longer label';
el._inputNode.appendChild(newOption);
_inputNode.appendChild(newOption);
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;
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 () => {
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
const { _overlayCtrl } = getSelectRichMembers(el);
el.opened = true;
await el.updateComplete;
// @ts-ignore allow protected access in tests
expect(el._overlayCtrl.isShown).to.be.true;
expect(_overlayCtrl.isShown).to.be.true;
el.opened = false;
await el.updateComplete;
await el.updateComplete; // safari takes a little longer
// @ts-ignore allow protected access in tests
expect(el._overlayCtrl.isShown).to.be.false;
expect(_overlayCtrl.isShown).to.be.false;
});
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 () => {
const el = await fixture(html` <lion-select-rich></lion-select-rich> `);
// @ts-ignore allow protected access in tests
await el._overlayCtrl.show();
const { _overlayCtrl, _listboxNode, _invokerNode } = getSelectRichMembers(el);
await _overlayCtrl.show();
await el.updateComplete;
// @ts-ignore allow protected access in tests
expect(document.activeElement === el._listboxNode).to.be.true;
// @ts-ignore allow protected access in tests
expect(document.activeElement === el._invokerNode).to.be.false;
expect(document.activeElement === _listboxNode).to.be.true;
expect(document.activeElement === _invokerNode).to.be.false;
el.opened = false;
await el.updateComplete;
await el.updateComplete; // safari takes a little longer
// @ts-ignore allow protected access in tests
expect(document.activeElement === el._listboxNode).to.be.false;
// @ts-ignore allow protected access in tests
expect(document.activeElement === el._invokerNode).to.be.true;
expect(document.activeElement === _listboxNode).to.be.false;
expect(document.activeElement === _invokerNode).to.be.true;
});
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-select-rich>
`);
// @ts-ignore allow protected access in tests
await el._overlayCtrl.show();
const { _overlayCtrl } = getSelectRichMembers(el);
await _overlayCtrl.show();
await el.updateComplete;
const options = el.formElements;
@ -302,6 +289,7 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
</lion-select-rich>
`);
const { _invokerNode: _invokerNodeReadOnly } = getSelectRichMembers(elReadOnly);
const elDisabled = await fixture(html`
<lion-select-rich disabled>
@ -309,25 +297,24 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${20} checked>Item 2</lion-option>
</lion-select-rich>
`);
const { _invokerNode: _invokerNodeDisabled } = getSelectRichMembers(elDisabled);
const elSingleoption = await fixture(html`
<lion-select-rich>
<lion-option .choiceValue=${10}>Item 1</lion-option>
</lion-select-rich>
`);
const { _invokerNode: _invokerNodeSingleOption } = getSelectRichMembers(elSingleoption);
// @ts-ignore allow protected access in tests
elReadOnly._invokerNode.click();
_invokerNodeReadOnly.click();
await elReadOnly.updateComplete;
expect(elReadOnly.opened).to.be.false;
// @ts-ignore allow protected access in tests
elDisabled._invokerNode.click();
_invokerNodeDisabled.click();
await elDisabled.updateComplete;
expect(elDisabled.opened).to.be.false;
// @ts-ignore allow protected access in tests
elSingleoption._invokerNode.click();
_invokerNodeSingleOption.click();
await elSingleoption.updateComplete;
expect(elSingleoption.opened).to.be.false;
});
@ -340,14 +327,13 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${'teal'}>Teal</lion-option>
</lion-select-rich>
`);
const { _overlayCtrl } = getSelectRichMembers(el);
// @ts-ignore allow protected access in tests
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('min');
el.opened = true;
await el.updateComplete;
// @ts-ignore allow protected access in tests
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('min');
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('min');
});
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>
`);
const { overlay } = getProtectedMembers(el);
const { _overlayCtrl } = getSelectRichMembers(el);
// The default is min, so we override that behavior here
// @ts-ignore allow protected access in tests
overlay.updateConfig({ inheritsReferenceWidth: 'full' });
_overlayCtrl.updateConfig({ inheritsReferenceWidth: 'full' });
el._initialInheritsReferenceWidth = 'full';
// @ts-ignore allow protected access in tests
expect(overlay.inheritsReferenceWidth).to.equal('full');
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('full');
el.opened = true;
await el.updateComplete;
// Opens while hasNoDefaultSelected = true, so we expect an override
// @ts-ignore allow protected access in tests
expect(overlay.inheritsReferenceWidth).to.equal('min');
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('min');
// Emulate selecting hotpink, it closing, and opening it again
el.modelValue = 'hotpink';
@ -382,11 +365,10 @@ describe('lion-select-rich', () => {
el.opened = true;
await el.updateComplete;
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
// @ts-ignore allow protected access in tests
expect(el._overlayCtrl.inheritsReferenceWidth).to.equal('full');
expect(_overlayCtrl.inheritsReferenceWidth).to.equal('full');
});
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-select-rich>
`);
const { _inputNode, _invokerNode } = getSelectRichMembers(el);
expect(el.singleOption).to.be.false;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.singleOption).to.be.false;
expect(_invokerNode.singleOption).to.be.false;
const optionELm = el.formElements[0];
// @ts-ignore allow protected access in tests
optionELm.parentNode.removeChild(optionELm);
el.requestUpdate();
await el.updateComplete;
expect(el.singleOption).to.be.true;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.singleOption).to.be.true;
expect(_invokerNode.singleOption).to.be.true;
const newOption = /** @type {LionOption} */ (document.createElement('lion-option'));
newOption.choiceValue = 30;
el._inputNode.appendChild(newOption);
_inputNode.appendChild(newOption);
el.requestUpdate();
await el.updateComplete;
expect(el.singleOption).to.be.false;
// @ts-ignore allow protected access in tests
expect(el._invokerNode.singleOption).to.be.false;
expect(_invokerNode.singleOption).to.be.false;
});
});
@ -432,32 +413,32 @@ describe('lion-select-rich', () => {
describe('Keyboard navigation', () => {
it('opens the listbox with [Enter] key via click handler', async () => {
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
// @ts-ignore allow protected access in tests
el._invokerNode.click();
const { _invokerNode } = getSelectRichMembers(el);
_invokerNode.click();
await aTimeout(0);
expect(el.opened).to.be.true;
});
it('opens the listbox with [ ](Space) key via click handler', async () => {
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
// @ts-ignore allow protected access in tests
el._invokerNode.click();
const { _invokerNode } = getSelectRichMembers(el);
_invokerNode.click();
await aTimeout(0);
expect(el.opened).to.be.true;
});
it('closes the listbox with [Escape] key once opened', async () => {
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
// @ts-ignore allow protected access in tests
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
const { _listboxNode } = getSelectRichMembers(el);
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(el.opened).to.be.false;
});
it('closes the listbox with [Tab] key once opened', async () => {
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
// tab can only be caught via keydown
// @ts-ignore allow protected access in tests
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
const { _listboxNode } = getSelectRichMembers(el);
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }));
expect(el.opened).to.be.false;
});
});
@ -466,8 +447,8 @@ describe('lion-select-rich', () => {
it('opens the listbox via click on invoker', async () => {
const el = await fixture(html` <lion-select-rich> </lion-select-rich> `);
expect(el.opened).to.be.false;
// @ts-ignore allow protected access in tests
el._invokerNode.click();
const { _invokerNode } = getSelectRichMembers(el);
_invokerNode.click();
await nextFrame(); // reflection of click takes some time
expect(el.opened).to.be.true;
});
@ -487,8 +468,8 @@ describe('lion-select-rich', () => {
describe('Keyboard navigation Windows', () => {
it('closes the listbox with [Enter] key once opened', async () => {
const el = await fixture(html` <lion-select-rich opened> </lion-select-rich> `);
// @ts-ignore allow protected access in tests
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
const { _listboxNode } = getSelectRichMembers(el);
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
expect(el.opened).to.be.false;
});
});
@ -501,12 +482,12 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${20}>Item 2</lion-option>
</lion-select-rich>
`);
const { _listboxNode } = getSelectRichMembers(el);
// changes active but not checked
el.activeIndex = 1;
expect(el.checkedIndex).to.equal(0);
// @ts-ignore allow protected access in tests
el._listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
_listboxNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
expect(el.opened).to.be.false;
expect(el.checkedIndex).to.equal(1);
});
@ -538,26 +519,26 @@ describe('lion-select-rich', () => {
<lion-option .choiceValue=${20}>Item 2</lion-option>
</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(invoker.getAttribute('aria-labelledby')).to.contain(invoker.id);
expect(invoker.getAttribute('aria-describedby')).to.contain(helpText.id);
expect(invoker.getAttribute('aria-describedby')).to.contain(feedback.id);
expect(invoker.getAttribute('aria-haspopup')).to.equal('listbox');
expect(_invokerNode.getAttribute('aria-labelledby')).to.contain(_labelNode.id);
expect(_invokerNode.getAttribute('aria-labelledby')).to.contain(_invokerNode.id);
expect(_invokerNode.getAttribute('aria-describedby')).to.contain(_helpTextNode.id);
expect(_invokerNode.getAttribute('aria-describedby')).to.contain(_feedbackNode.id);
expect(_invokerNode.getAttribute('aria-haspopup')).to.equal('listbox');
});
it('notifies when the listbox is expanded or not', async () => {
// smoke test for overlay functionality
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;
await el.updateComplete;
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')
);
const { invoker, listbox } = getProtectedMembers(selectRich);
const { _invokerNode, _listboxNode } = getSelectRichMembers(selectRich);
expect(selectRich.checkedIndex).to.equal(1);
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'));
newOption.modelValue = { checked: false, value: 'blue' };
newOption.textContent = 'Blue';
const hotpinkEl = listbox.children[1];
const hotpinkEl = _listboxNode.children[1];
hotpinkEl.insertAdjacentElement('beforebegin', newOption);
expect(selectRich.checkedIndex).to.equal(2);
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}>
`);
await el.updateComplete;
// @ts-ignore allow protected member access in tests
expect(el._overlayCtrl.placementMode).to.equal('global');
const { _overlayCtrl } = getSelectRichMembers(el);
expect(_overlayCtrl.placementMode).to.equal('global');
el.dispatchEvent(new Event('switch'));
// @ts-ignore allow protected member access in tests
expect(el._overlayCtrl.placementMode).to.equal('local');
expect(_overlayCtrl.placementMode).to.equal('local');
});
it('supports putting a placeholder template when there is no default selection initially', async () => {
@ -707,10 +688,10 @@ describe('lion-select-rich', () => {
</${selectTag}>
`);
const { invoker } = getProtectedMembers(el);
const { _invokerNode } = getSelectRichMembers(el);
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>`);
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
* 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
get _inputNode() {

View file

@ -3,9 +3,12 @@ import sinon from 'sinon';
import { Validator } from '@lion/form-core';
import { LionSwitch } from '@lion/switch';
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/form-core/types/FormControlMixinTypes').FormControlHost} FormControlHost
*/
const IsTrue = class extends Validator {
@ -19,13 +22,11 @@ const IsTrue = class extends Validator {
};
/**
* @param {LionSwitch} lionSwitchEl
* @param { LionSwitch } el
*/
function getProtectedMembers(lionSwitchEl) {
return {
// @ts-ignore
inputNode: lionSwitchEl._inputNode,
};
function getSwitchMembers(el) {
const obj = getFormControlMembers(/** @type { * & FormControlHost } */ (el));
return { ...obj, _inputNode: /** @type {LionSwitchButton} */ (obj._inputNode) };
}
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 () => {
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;
el._labelNode.click();
_labelNode.click();
expect(el.checked).to.be.false;
});
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>`);
el._labelNode.click();
const { _labelNode } = getFormControlMembers(el);
_labelNode.click();
expect(el.checked).to.be.false;
});
it('clicking the label should focus the toggle button', async () => {
const el = await fixture(html`<lion-switch label="Enable Setting"></lion-switch>`);
el._labelNode.click();
expect(document.activeElement).to.equal(el._inputNode);
const { _inputNode, _labelNode } = getSwitchMembers(el);
_labelNode.click();
expect(document.activeElement).to.equal(_inputNode);
});
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>`);
el._labelNode.click();
expect(document.activeElement).to.not.equal(el._inputNode);
const { _inputNode, _labelNode } = getSwitchMembers(el);
_labelNode.click();
expect(document.activeElement).to.not.equal(_inputNode);
});
it('should sync its "disabled" state to child button', async () => {
const el = await fixture(html`<lion-switch disabled></lion-switch>`);
const { inputNode } = getProtectedMembers(el);
expect(inputNode.disabled).to.be.true;
expect(inputNode.hasAttribute('disabled')).to.be.true;
const { _inputNode } = getSwitchMembers(el);
expect(_inputNode.disabled).to.be.true;
expect(_inputNode.hasAttribute('disabled')).to.be.true;
el.disabled = false;
await el.updateComplete;
await el.updateComplete; // safari takes longer
expect(inputNode.disabled).to.be.false;
expect(inputNode.hasAttribute('disabled')).to.be.false;
expect(_inputNode.disabled).to.be.false;
expect(_inputNode.hasAttribute('disabled')).to.be.false;
});
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 () => {
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 { inputNode: checkeInputNode } = getProtectedMembers(checkedEl);
const { _inputNode: checkeInputNode } = getSwitchMembers(checkedEl);
expect(uncheckeInputNode.checked).to.be.false;
expect(checkeInputNode.checked).to.be.true;
uncheckedEl.checked = true;
@ -96,8 +103,8 @@ describe('lion-switch', () => {
it('should sync "checked" state received from child button', async () => {
const el = await fixture(html`<lion-switch></lion-switch>`);
const { inputNode } = getProtectedMembers(el);
const button = inputNode;
const { _inputNode } = getSwitchMembers(el);
const button = _inputNode;
expect(el.checked).to.be.false;
button.click();
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 () => {
const handlerSpy = sinon.spy();
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);
inputNode.click();
el._labelNode.click();
_inputNode.click();
_labelNode.click();
await el.updateComplete;
expect(handlerSpy.callCount).to.equal(2);
const checkCall = /** @param {import('sinon').SinonSpyCall} call */ call => {

View file

@ -7,6 +7,7 @@ import { css } from '@lion/core';
class LionFieldWithTextArea extends LionField {
/**
* @returns {HTMLTextAreaElement}
* @protected
*/
get _inputNode() {
return /** @type {HTMLTextAreaElement} */ (Array.from(this.children).find(
@ -21,6 +22,7 @@ class LionFieldWithTextArea extends LionField {
* @customElement lion-textarea
*/
export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
/** @type {any} */
static get properties() {
return {
maxRows: {

View file

@ -1,6 +1,7 @@
import { aTimeout, expect, fixture as _fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import '@lion/textarea/define';
import { getFormControlMembers } from '@lion/form-core/test-helpers';
/**
* @typedef {import('../src/LionTextarea').LionTextarea} LionTextarea
@ -9,16 +10,6 @@ import '@lion/textarea/define';
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture);
/**
* @param {LionTextarea} lionTextareaEl
*/
function getProtectedMembers(lionTextareaEl) {
const { _inputNode: input } = lionTextareaEl;
return {
input,
};
}
function hasBrowserResizeSupport() {
const textarea = document.createElement('textarea');
return textarea.style.resize !== undefined;
@ -41,25 +32,25 @@ describe('<lion-textarea>', () => {
it('has .readOnly=false .rows=2 and rows="2" by default', async () => {
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.getAttribute('rows')).to.be.equal('2');
// @ts-ignore
expect(input.rows).to.equal(2);
expect(input.getAttribute('rows')).to.be.equal('2');
expect(_inputNode.rows).to.equal(2);
expect(_inputNode.getAttribute('rows')).to.be.equal('2');
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 () => {
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.getAttribute('rows')).to.be.equal('8');
// @ts-ignore
expect(input.rows).to.equal(8);
expect(input.getAttribute('rows')).to.be.equal('8');
expect(_inputNode.rows).to.equal(8);
expect(_inputNode.getAttribute('rows')).to.be.equal('8');
});
it('sync readOnly to the native textarea', async () => {
@ -74,8 +65,8 @@ describe('<lion-textarea>', () => {
}
const el = await fixture(`<lion-textarea></lion-textarea>`);
const { input } = getProtectedMembers(el);
const computedStyle = window.getComputedStyle(input);
const { _inputNode } = getFormControlMembers(el);
const computedStyle = window.getComputedStyle(_inputNode);
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 () => {
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(input.getAttribute('placeholder')).to.equal('text');
expect(_inputNode.getAttribute('placeholder')).to.equal('text');
el.placeholder = 'foo';
await el.updateComplete;
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 () => {