Merge pull request #1154 from MatthieuLebigre/feat/checkbox-inderminate
feat: add checkbox indeterminate
This commit is contained in:
commit
a54d8c3d56
23 changed files with 648 additions and 30 deletions
5
.changeset/five-onions-hope.md
Normal file
5
.changeset/five-onions-hope.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/checkbox-group': minor
|
||||
---
|
||||
|
||||
Add checkbox-indeterminate component, which is a mixed state checkbox that depends on its child checkbox states.
|
||||
6
.changeset/lazy-chicken-speak.md
Normal file
6
.changeset/lazy-chicken-speak.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@lion/form-core': patch
|
||||
'@lion/listbox': patch
|
||||
---
|
||||
|
||||
Make \_\_parentFormGroup --> \_parentFormGroup so it is protected and not private
|
||||
6
.changeset/witty-mugs-vanish.md
Normal file
6
.changeset/witty-mugs-vanish.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@lion/form-core': patch
|
||||
'@lion/switch': patch
|
||||
---
|
||||
|
||||
Make \_\_toggleChecked protected property (\_toggleChecked)
|
||||
|
|
@ -11,6 +11,7 @@ Its purpose is to provide a way for users to check **multiple** options amongst
|
|||
import { html } from '@lion/core';
|
||||
import { Required, Validator } from '@lion/form-core';
|
||||
import './lion-checkbox-group.js';
|
||||
import './lion-checkbox-indeterminate.js';
|
||||
import './lion-checkbox.js';
|
||||
|
||||
export default {
|
||||
|
|
@ -166,3 +167,81 @@ export const event = () => html`
|
|||
<span>Selected scientists: <strong id="selectedDinosaur">N/A</strong></span>
|
||||
`;
|
||||
```
|
||||
|
||||
### Indeterminate
|
||||
|
||||
```js preview-story
|
||||
export const indeterminate = () => html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const indeterminateSiblings = () => html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox-indeterminate label="Old Greek scientists">
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Pythagoras"
|
||||
.choiceValue=${'Pythagoras'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
<lion-checkbox-indeterminate label="17th Century scientists">
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Isaac Newton"
|
||||
.choiceValue=${'Isaac Newton'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Galileo Galilei"
|
||||
.choiceValue=${'Galileo Galilei'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const indeterminateChildren = () => html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox-indeterminate label="Scientists">
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Isaac Newton"
|
||||
.choiceValue=${'Isaac Newton'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Galileo Galilei"
|
||||
.choiceValue=${'Galileo Galilei'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox-indeterminate slot="checkbox" label="Old Greek scientists">
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Pythagoras"
|
||||
.choiceValue=${'Pythagoras'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { LionCheckboxGroup } from './src/LionCheckboxGroup.js';
|
||||
export { LionCheckboxIndeterminate } from './src/LionCheckboxIndeterminate.js';
|
||||
export { LionCheckbox } from './src/LionCheckbox.js';
|
||||
|
|
|
|||
3
packages/checkbox-group/lion-checkbox-indeterminate.js
Normal file
3
packages/checkbox-group/lion-checkbox-indeterminate.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionCheckboxIndeterminate } from './src/LionCheckboxIndeterminate.js';
|
||||
|
||||
customElements.define('lion-checkbox-indeterminate', LionCheckboxIndeterminate);
|
||||
|
|
@ -31,7 +31,8 @@
|
|||
},
|
||||
"sideEffects": [
|
||||
"lion-checkbox.js",
|
||||
"lion-checkbox-group.js"
|
||||
"lion-checkbox-group.js",
|
||||
"lion-checkbox-indeterminate.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@lion/core": "0.13.8",
|
||||
|
|
|
|||
141
packages/checkbox-group/src/LionCheckboxIndeterminate.js
Normal file
141
packages/checkbox-group/src/LionCheckboxIndeterminate.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { html, css } from '@lion/core';
|
||||
import { LionCheckbox } from './LionCheckbox.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('./LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
|
||||
*/
|
||||
|
||||
// @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you.
|
||||
export class LionCheckboxIndeterminate extends LionCheckbox {
|
||||
static get styles() {
|
||||
const superCtor = /** @type {typeof LionCheckbox} */ (super.prototype.constructor);
|
||||
return [
|
||||
superCtor.styles ? superCtor.styles : [],
|
||||
css`
|
||||
:host .choice-field__nested-checkboxes {
|
||||
display: block;
|
||||
}
|
||||
|
||||
::slotted([slot='checkbox']) {
|
||||
padding-left: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
/**
|
||||
* Indeterminate state of the checkbox
|
||||
*/
|
||||
indeterminate: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get _checkboxGroupNode() {
|
||||
return /** @type LionCheckboxGroup */ (this._parentFormGroup);
|
||||
}
|
||||
|
||||
get _subCheckboxes() {
|
||||
let checkboxes = [];
|
||||
if (
|
||||
this._checkboxGroupNode &&
|
||||
this._checkboxGroupNode.formElements &&
|
||||
this._checkboxGroupNode.formElements.length > 0
|
||||
) {
|
||||
checkboxes = this._checkboxGroupNode.formElements.filter(
|
||||
checkbox => checkbox !== this && this.contains(checkbox),
|
||||
);
|
||||
}
|
||||
return /** @type LionCheckbox[] */ (checkboxes);
|
||||
}
|
||||
|
||||
_setOwnCheckedState() {
|
||||
const subCheckboxes = this._subCheckboxes;
|
||||
if (!subCheckboxes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.__settingOwnChecked = true;
|
||||
const checkedElements = subCheckboxes.filter(checkbox => checkbox.checked);
|
||||
switch (subCheckboxes.length - checkedElements.length) {
|
||||
// all checked
|
||||
case 0:
|
||||
this.indeterminate = false;
|
||||
this.checked = true;
|
||||
break;
|
||||
// none checked
|
||||
case subCheckboxes.length:
|
||||
this.indeterminate = false;
|
||||
this.checked = false;
|
||||
break;
|
||||
default:
|
||||
this.indeterminate = true;
|
||||
this.checked = false;
|
||||
}
|
||||
this.updateComplete.then(() => {
|
||||
this.__settingOwnChecked = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
*/
|
||||
__onModelValueChanged(ev) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _ev = /** @type {CustomEvent} */ (ev);
|
||||
if (_ev.detail.formPath[0] === this && !this.__settingOwnChecked) {
|
||||
this._subCheckboxes.forEach(checkbox => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
checkbox.checked = this._inputNode.checked;
|
||||
});
|
||||
}
|
||||
this._setOwnCheckedState();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_afterTemplate() {
|
||||
return html`
|
||||
<div class="choice-field__nested-checkboxes">
|
||||
<slot name="checkbox"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_onRequestToAddFormElement() {
|
||||
this._setOwnCheckedState();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.indeterminate = false;
|
||||
this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
|
||||
this.__onModelValueChanged = this.__onModelValueChanged.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('model-value-changed', this.__onModelValueChanged);
|
||||
this.addEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('model-value-changed', this.__onModelValueChanged);
|
||||
this.removeEventListener('form-element-register', this._onRequestToAddFormElement);
|
||||
}
|
||||
|
||||
/** @param {import('lit-element').PropertyValues } changedProperties */
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('indeterminate')) {
|
||||
this._inputNode.indeterminate = this.indeterminate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import '../lion-checkbox-indeterminate.js';
|
||||
import { runChoiceInputMixinSuite } from '@lion/form-core/test-suites/choice-group/ChoiceInputMixin.suite.js';
|
||||
|
||||
runChoiceInputMixinSuite({ tagString: 'lion-checkbox-indeterminate' });
|
||||
370
packages/checkbox-group/test/lion-checkbox-indeterminate.test.js
Normal file
370
packages/checkbox-group/test/lion-checkbox-indeterminate.test.js
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import '../lion-checkbox-indeterminate.js';
|
||||
import '../lion-checkbox-group.js';
|
||||
import '../lion-checkbox.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../src/LionCheckboxIndeterminate').LionCheckboxIndeterminate} LionCheckboxIndeterminate
|
||||
* @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
|
||||
*/
|
||||
|
||||
describe('<lion-checkbox-indeterminate>', () => {
|
||||
it('should have type = checkbox', async () => {
|
||||
// Arrange
|
||||
const el = await fixture(html`
|
||||
<lion-checkbox-indeterminate
|
||||
name="checkbox"
|
||||
.choiceValue="${'male'}"
|
||||
></lion-checkbox-indeterminate>
|
||||
`);
|
||||
|
||||
// Assert
|
||||
expect(el.getAttribute('type')).to.equal('checkbox');
|
||||
});
|
||||
|
||||
it('should not be indeterminate by default if all children are unchecked', async () => {
|
||||
// Arrange
|
||||
const el = await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`);
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be indeterminate if one child is checked', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`);
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
});
|
||||
|
||||
it('should be checked if all children are checked', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should become indeterminate if one child is checked', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act
|
||||
elIndeterminate._subCheckboxes[0].checked = true;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
});
|
||||
|
||||
it('should become checked if all children are checked', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act
|
||||
elIndeterminate._subCheckboxes[0].checked = true;
|
||||
elIndeterminate._subCheckboxes[1].checked = true;
|
||||
elIndeterminate._subCheckboxes[2].checked = true;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should sync all children when parent is checked (from indeterminate to checked)', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act
|
||||
elIndeterminate._inputNode.click();
|
||||
await elIndeterminate.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
});
|
||||
|
||||
it('should sync all children when parent is checked (from unchecked to checked)', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act
|
||||
elIndeterminate._inputNode.click();
|
||||
await elIndeterminate.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
});
|
||||
|
||||
it('should sync all children when parent is checked (from checked to unchecked)', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<lion-checkbox slot="checkbox" label="Archimedes" checked></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon" checked></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie" checked></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act
|
||||
elIndeterminate._inputNode.click();
|
||||
await elIndeterminate.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.false;
|
||||
});
|
||||
|
||||
it('should work as expected with siblings checkbox-indeterminate', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox-indeterminate label="Old Greek scientists" id="first-checkbox-indeterminate">
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Pythagoras"
|
||||
.choiceValue=${'Pythagoras'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
<lion-checkbox-indeterminate
|
||||
label="17th Century scientists"
|
||||
id="second-checkbox-indeterminate"
|
||||
>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Isaac Newton"
|
||||
.choiceValue=${'Isaac Newton'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Galileo Galilei"
|
||||
.choiceValue=${'Galileo Galilei'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elFirstIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'#first-checkbox-indeterminate',
|
||||
));
|
||||
const elSecondIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'#second-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act - check the first sibling
|
||||
elFirstIndeterminate._inputNode.click();
|
||||
await elFirstIndeterminate.updateComplete;
|
||||
await elSecondIndeterminate.updateComplete;
|
||||
|
||||
// Assert - the second sibling should not be affected
|
||||
expect(elFirstIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elFirstIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elFirstIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elFirstIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
expect(elSecondIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||
expect(elSecondIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||
});
|
||||
|
||||
it('should work as expected with nested indeterminate checkboxes', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox-indeterminate label="Scientists" id="parent-checkbox-indeterminate">
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Isaac Newton"
|
||||
.choiceValue=${'Isaac Newton'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Galileo Galilei"
|
||||
.choiceValue=${'Galileo Galilei'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox-indeterminate
|
||||
slot="checkbox"
|
||||
label="Old Greek scientists"
|
||||
id="nested-checkbox-indeterminate"
|
||||
>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Archimedes"
|
||||
.choiceValue=${'Archimedes'}
|
||||
></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Plato" .choiceValue=${'Plato'}></lion-checkbox>
|
||||
<lion-checkbox
|
||||
slot="checkbox"
|
||||
label="Pythagoras"
|
||||
.choiceValue=${'Pythagoras'}
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-indeterminate>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elNestedIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'#nested-checkbox-indeterminate',
|
||||
));
|
||||
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'#parent-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act - check a nested checkbox
|
||||
elNestedIndeterminate?._subCheckboxes[0]._inputNode.click();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elNestedIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
|
||||
// Act - check all nested checkbox
|
||||
elNestedIndeterminate?._subCheckboxes[1]._inputNode.click();
|
||||
elNestedIndeterminate?._subCheckboxes[2]._inputNode.click();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elNestedIndeterminate?.hasAttribute('checked')).to.be.true;
|
||||
expect(elNestedIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elParentIndeterminate?.hasAttribute('checked')).to.be.false;
|
||||
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
|
||||
// Act - finally check all remaining checkbox
|
||||
elParentIndeterminate?._subCheckboxes[0]._inputNode.click();
|
||||
elParentIndeterminate?._subCheckboxes[1]._inputNode.click();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elNestedIndeterminate?.hasAttribute('checked')).to.be.true;
|
||||
expect(elNestedIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elParentIndeterminate?.hasAttribute('checked')).to.be.true;
|
||||
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
});
|
||||
|
||||
it('should work as expected if extra html', async () => {
|
||||
// Arrange
|
||||
const el = /** @type {LionCheckboxGroup} */ (await fixture(html`
|
||||
<lion-checkbox-group name="scientists[]">
|
||||
<div>
|
||||
Let's have some fun
|
||||
<div>Hello I'm a div</div>
|
||||
<lion-checkbox-indeterminate label="Favorite scientists">
|
||||
<div>useless div</div>
|
||||
<lion-checkbox slot="checkbox" label="Archimedes"></lion-checkbox>
|
||||
<lion-checkbox slot="checkbox" label="Francis Bacon"></lion-checkbox>
|
||||
<div>absolutely useless</div>
|
||||
<lion-checkbox slot="checkbox" label="Marie Curie"></lion-checkbox>
|
||||
</lion-checkbox-indeterminate>
|
||||
</div>
|
||||
<div>Too much fun, stop it !</div>
|
||||
</lion-checkbox-group>
|
||||
`));
|
||||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
// Act
|
||||
elIndeterminate._subCheckboxes[0].checked = true;
|
||||
elIndeterminate._subCheckboxes[1].checked = true;
|
||||
elIndeterminate._subCheckboxes[2].checked = true;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?.checked).to.be.true;
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
|||
* @typedef {import('../../types/choice-group/ChoiceGroupMixinTypes').ChoiceGroupMixin} ChoiceGroupMixin
|
||||
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} ChoiceInputHost
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { FormatMixin } from '../FormatMixin.js';
|
|||
|
||||
/**
|
||||
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputMixin} ChoiceInputMixin
|
||||
* @typedef {import('../../types/choice-group/ChoiceInputMixinTypes').ChoiceInputModelValue} ChoiceInputModelValue
|
||||
*/
|
||||
|
|
@ -115,9 +115,9 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
if (
|
||||
changedProperties.has('name') &&
|
||||
// @ts-expect-error not all choice inputs have a parent form group, since this mixin does not have a strict contract with the registration system
|
||||
this.__parentFormGroup &&
|
||||
this._parentFormGroup &&
|
||||
// @ts-expect-error
|
||||
this.__parentFormGroup.name !== this.name
|
||||
this._parentFormGroup.name !== this.name
|
||||
) {
|
||||
// @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin
|
||||
this.name = changedProperties.get('name');
|
||||
|
|
@ -129,7 +129,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
this.modelValue = { value: '', checked: false };
|
||||
this.disabled = false;
|
||||
this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this);
|
||||
this.__toggleChecked = this.__toggleChecked.bind(this);
|
||||
this._toggleChecked = this._toggleChecked.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -193,7 +193,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
if (this._labelNode) {
|
||||
this._labelNode.addEventListener('click', this._preventDuplicateLabelClick);
|
||||
}
|
||||
this.addEventListener('user-input-changed', this.__toggleChecked);
|
||||
this.addEventListener('user-input-changed', this._toggleChecked);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
|
@ -201,7 +201,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
if (this._labelNode) {
|
||||
this._labelNode.removeEventListener('click', this._preventDuplicateLabelClick);
|
||||
}
|
||||
this.removeEventListener('user-input-changed', this.__toggleChecked);
|
||||
this.removeEventListener('user-input-changed', this._toggleChecked);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,7 +221,9 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
this._inputNode.addEventListener('click', __inputClickHandler);
|
||||
}
|
||||
|
||||
__toggleChecked() {
|
||||
/** @param {Event} ev */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_toggleChecked(ev) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { FormElementsHaveNoError } from './FormElementsHaveNoError.js';
|
|||
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {import('../../types/registration/FormRegisteringMixinTypes').FormRegisteringHost} FormRegisteringHost
|
||||
* @typedef {import('../../types/registration/FormRegistrarMixinTypes').ElementWithParentFormGroup} ElementWithParentFormGroup
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?: HTMLElement, checked?: boolean, disabled: boolean, hasFeedbackFor: string[], makeRequestToBeDisabled: Function }} FormControl
|
||||
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?: HTMLElement, checked?: boolean, disabled: boolean, hasFeedbackFor: string[], makeRequestToBeDisabled: Function }} FormControl
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -449,12 +449,12 @@ const FormGroupMixinImplementation = superclass =>
|
|||
__linkChildrenMessagesToParent(child) {
|
||||
// aria-describedby of (nested) children
|
||||
const unTypedThis = /** @type {unknown} */ (this);
|
||||
let parent = /** @type {FormControlHost & { __parentFormGroup:any }} */ (unTypedThis);
|
||||
let parent = /** @type {FormControlHost & { _parentFormGroup:any }} */ (unTypedThis);
|
||||
const ctor = /** @type {typeof FormGroupMixin} */ (this.constructor);
|
||||
while (parent) {
|
||||
ctor._addDescriptionElementIdsToField(child, parent._getAriaDescriptionElements());
|
||||
// Also check if the newly added child needs to refer grandparents
|
||||
parent = parent.__parentFormGroup;
|
||||
parent = parent._parentFormGroup;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const FormRegisteringMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
super();
|
||||
/** @type {FormRegistrarHost | undefined} */
|
||||
this.__parentFormGroup = undefined;
|
||||
this._parentFormGroup = undefined;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -42,8 +42,8 @@ const FormRegisteringMixinImplementation = superclass =>
|
|||
// @ts-expect-error check it anyway, because could be lit-element extension
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
if (this.__parentFormGroup) {
|
||||
this.__parentFormGroup.removeFormElement(this);
|
||||
if (this._parentFormGroup) {
|
||||
this._parentFormGroup.removeFormElement(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { FormRegisteringMixin } from './FormRegisteringMixin.js';
|
|||
|
||||
/**
|
||||
* @typedef {import('../../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -77,7 +77,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
addFormElement(child, indexToInsertAt) {
|
||||
// This is a way to let the child element (a lion-fieldset or lion-field) know, about its parent
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
child.__parentFormGroup = this;
|
||||
child._parentFormGroup = this;
|
||||
|
||||
// 1. Add children as array element
|
||||
if (indexToInsertAt >= 0) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import '../lion-field.js';
|
|||
/**
|
||||
* @typedef {import('../src/LionField.js').LionField} LionField
|
||||
* @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
|
||||
* @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
* @typedef {FormControlHost & HTMLElement & {_parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
|
||||
*/
|
||||
|
||||
/** @typedef {HTMLElement & {shadowRoot: HTMLElement, assignedNodes: Function}} ShadowHTMLElement */
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export declare class ChoiceInputHost {
|
|||
|
||||
_preventDuplicateLabelClick(ev: Event): void;
|
||||
|
||||
__toggleChecked(): void;
|
||||
_toggleChecked(ev: Event): void;
|
||||
|
||||
__syncModelCheckedToChecked(checked: boolean): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export declare class FormRegisteringHost {
|
|||
constructor(...args: any[]);
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
__parentFormGroup?: FormRegistrarHost;
|
||||
_parentFormGroup?: FormRegistrarHost;
|
||||
}
|
||||
|
||||
export declare function FormRegisteringImplementation<T extends Constructor<LitElement>>(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { FormControlHost } from '../../types/FormControlMixinTypes';
|
|||
import { LitElement } from '@lion/core';
|
||||
|
||||
export declare class ElementWithParentFormGroup {
|
||||
__parentFormGroup: FormRegistrarHost;
|
||||
_parentFormGroup: FormRegistrarHost;
|
||||
}
|
||||
|
||||
export declare class FormRegistrarHost {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
|||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const parentForm = /** @type {unknown} */ (this.__parentFormGroup);
|
||||
const parentForm = /** @type {unknown} */ (this._parentFormGroup);
|
||||
this.__isHandlingUserInput = true;
|
||||
if (parentForm && /** @type {ChoiceGroupHost} */ (parentForm).multipleChoice) {
|
||||
this.checked = !this.checked;
|
||||
|
|
|
|||
2
packages/listbox/types/LionOption.d.ts
vendored
2
packages/listbox/types/LionOption.d.ts
vendored
|
|
@ -2,5 +2,5 @@ import { ChoiceGroupHost } from '@lion/form-core/types/choice-group/ChoiceGroupM
|
|||
|
||||
export declare class LionOptionHost {
|
||||
constructor(...args: any[]);
|
||||
private __parentFormGroup: ChoiceGroupHost;
|
||||
protected _parentFormGroup: ChoiceGroupHost;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
this._inputNode.addEventListener('checked-changed', this.__handleButtonSwitchCheckedChanged);
|
||||
}
|
||||
if (this._labelNode) {
|
||||
this._labelNode.addEventListener('click', this.__toggleChecked);
|
||||
this._labelNode.addEventListener('click', this._toggleChecked);
|
||||
}
|
||||
this._syncButtonSwitch();
|
||||
this.submitted = true;
|
||||
|
|
@ -94,7 +94,7 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
);
|
||||
}
|
||||
if (this._labelNode) {
|
||||
this._labelNode.removeEventListener('click', this.__toggleChecked);
|
||||
this._labelNode.removeEventListener('click', this._toggleChecked);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
|
||||
this.role = 'switch';
|
||||
this.checked = false;
|
||||
this.__toggleChecked = this.__toggleChecked.bind(this);
|
||||
this._toggleChecked = this._toggleChecked.bind(this);
|
||||
this.__handleKeydown = this.__handleKeydown.bind(this);
|
||||
this.__handleKeyup = this.__handleKeyup.bind(this);
|
||||
}
|
||||
|
|
@ -85,19 +85,19 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('aria-checked', `${this.checked}`);
|
||||
this.addEventListener('click', this.__toggleChecked);
|
||||
this.addEventListener('click', this._toggleChecked);
|
||||
this.addEventListener('keydown', this.__handleKeydown);
|
||||
this.addEventListener('keyup', this.__handleKeyup);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('click', this.__toggleChecked);
|
||||
this.removeEventListener('click', this._toggleChecked);
|
||||
this.removeEventListener('keydown', this.__handleKeydown);
|
||||
this.removeEventListener('keyup', this.__handleKeyup);
|
||||
}
|
||||
|
||||
__toggleChecked() {
|
||||
_toggleChecked() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -132,7 +132,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
*/
|
||||
__handleKeyup(e) {
|
||||
if ([32 /* space */, 13 /* enter */].indexOf(e.keyCode) !== -1) {
|
||||
this.__toggleChecked();
|
||||
this._toggleChecked();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue