diff --git a/.changeset/five-onions-hope.md b/.changeset/five-onions-hope.md
new file mode 100644
index 000000000..89609021b
--- /dev/null
+++ b/.changeset/five-onions-hope.md
@@ -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.
diff --git a/packages/checkbox-group/README.md b/packages/checkbox-group/README.md
index 0cd5d842b..781223745 100644
--- a/packages/checkbox-group/README.md
+++ b/packages/checkbox-group/README.md
@@ -173,11 +173,75 @@ export const event = () => html`
```js preview-story
export const indeterminate = () => html`
-
+
+
+
+
+
+
+`;
+```
+
+```js preview-story
+export const indeterminateSiblings = () => html`
+
+
+
+
+
+
+
+
+
+
+
+`;
+```
+
+```js preview-story
+export const indeterminateChildren = () => html`
+
+
+
+
+
+
+
+
+
-
-
-
`;
```
diff --git a/packages/checkbox-group/src/LionCheckboxIndeterminate.js b/packages/checkbox-group/src/LionCheckboxIndeterminate.js
index a164ec8c4..e2ef1bf69 100644
--- a/packages/checkbox-group/src/LionCheckboxIndeterminate.js
+++ b/packages/checkbox-group/src/LionCheckboxIndeterminate.js
@@ -1,6 +1,28 @@
+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 {
/**
@@ -14,52 +36,99 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
}
get _checkboxGroupNode() {
- return /** @type {import('./LionCheckboxGroup').LionCheckboxGroup} */ (this.parentElement);
+ return /** @type LionCheckboxGroup */ (this._parentFormGroup);
}
get _subCheckboxes() {
- return this._checkboxGroupNode.formElements.filter(checkbox => checkbox !== this);
+ 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);
}
- _parentModelValueChanged() {
- const checkedElements = this._subCheckboxes.filter(checkbox => checkbox.checked);
- switch (this._subCheckboxes.length - checkedElements.length) {
+ _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 this._subCheckboxes.length:
+ case subCheckboxes.length:
this.indeterminate = false;
this.checked = false;
break;
default:
this.indeterminate = true;
+ this.checked = false;
}
+ this.updateComplete.then(() => {
+ this.__settingOwnChecked = false;
+ });
}
- _ownModelValueChanged(ev) {
- if (ev.target === this) {
+ /**
+ * @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.checked;
+ checkbox.checked = this._inputNode.checked;
});
}
+ this._setOwnCheckedState();
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ _afterTemplate() {
+ return html`
+
+
+
+ `;
+ }
+
+ _onRequestToAddFormElement() {
+ this._setOwnCheckedState();
}
constructor() {
super();
this.indeterminate = false;
+ this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);
+ this.__onModelValueChanged = this.__onModelValueChanged.bind(this);
}
connectedCallback() {
super.connectedCallback();
- this._checkboxGroupNode.addEventListener(
- 'model-value-changed',
- this._parentModelValueChanged.bind(this),
- );
- this.addEventListener('model-value-changed', this._ownModelValueChanged);
+ 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 */
@@ -69,23 +138,4 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
this._inputNode.indeterminate = this.indeterminate;
}
}
-
- /**
- * @override
- * clicking on indeterminate status will set the status as checked
- */
- __toggleChecked() {
- if (this.disabled) {
- return;
- }
-
- // always turn off indeterminate
- // and set checked to true
- if (this.indeterminate) {
- this.indeterminate = false;
- this.checked = true;
- } else {
- this.checked = !this.checked;
- }
- }
}
diff --git a/packages/checkbox-group/test/lion-checkbox-indeterminate-integrations.test.js b/packages/checkbox-group/test/lion-checkbox-indeterminate-integrations.test.js
new file mode 100644
index 000000000..8e3bb2f22
--- /dev/null
+++ b/packages/checkbox-group/test/lion-checkbox-indeterminate-integrations.test.js
@@ -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' });
diff --git a/packages/checkbox-group/test/lion-checkbox-indeterminate.test.js b/packages/checkbox-group/test/lion-checkbox-indeterminate.test.js
new file mode 100644
index 000000000..f96123dc7
--- /dev/null
+++ b/packages/checkbox-group/test/lion-checkbox-indeterminate.test.js
@@ -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('', () => {
+ it('should have type = checkbox', async () => {
+ // Arrange
+ const el = await fixture(html`
+
+ `);
+
+ // 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`
+
+
+
+
+
+
+
+ `);
+ 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`
+
+
+
+
+
+
+
+ `);
+ 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`
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+
+
+
+
+
+
+
+
+
+ `));
+ 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`
+
+
+ Let's have some fun
+
Hello I'm a div
+
+ useless div
+
+
+ absolutely useless
+
+
+
+ Too much fun, stop it !
+
+ `));
+ 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;
+ });
+});