diff --git a/packages/checkbox-group/src/vaadin-checkbox-group.d.ts b/packages/checkbox-group/src/vaadin-checkbox-group.d.ts index af44f5da24..5c24613b2a 100644 --- a/packages/checkbox-group/src/vaadin-checkbox-group.d.ts +++ b/packages/checkbox-group/src/vaadin-checkbox-group.d.ts @@ -61,6 +61,7 @@ export interface CheckboxGroupEventMap extends HTMLElementEventMap, CheckboxGrou * Attribute | Description | Part name * --------------------|-------------------------------------------|------------ * `disabled` | Set when the element is disabled | :host + * `readonly` | Set when the element is readonly | :host * `invalid` | Set when the element is invalid | :host * `focused` | Set when the element is focused | :host * `has-label` | Set when the element has a label | :host diff --git a/packages/checkbox-group/src/vaadin-checkbox-group.js b/packages/checkbox-group/src/vaadin-checkbox-group.js index 935cd8fc59..2c692e4144 100644 --- a/packages/checkbox-group/src/vaadin-checkbox-group.js +++ b/packages/checkbox-group/src/vaadin-checkbox-group.js @@ -42,6 +42,7 @@ registerStyles('vaadin-checkbox-group', checkboxGroupStyles, { moduleId: 'vaadin * Attribute | Description | Part name * --------------------|-------------------------------------------|------------ * `disabled` | Set when the element is disabled | :host + * `readonly` | Set when the element is readonly | :host * `invalid` | Set when the element is invalid | :host * `focused` | Set when the element is focused | :host * `has-label` | Set when the element has a label | :host diff --git a/packages/checkbox-group/test/dom/__snapshots__/checkbox-group.test.snap.js b/packages/checkbox-group/test/dom/__snapshots__/checkbox-group.test.snap.js index 9f18c60d6b..ca0574cd78 100644 --- a/packages/checkbox-group/test/dom/__snapshots__/checkbox-group.test.snap.js +++ b/packages/checkbox-group/test/dom/__snapshots__/checkbox-group.test.snap.js @@ -10,14 +10,21 @@ snapshots["vaadin-checkbox-group host default"] = value="1" > + + + + + + + + + + + + + + >( Constructor & Constructor & Constructor & + Constructor & Constructor & Constructor & Constructor & Constructor & + Constructor & T; export declare class CheckboxMixinClass { diff --git a/packages/checkbox/src/vaadin-checkbox-mixin.js b/packages/checkbox/src/vaadin-checkbox-mixin.js index 79aa5898db..2c920105cf 100644 --- a/packages/checkbox/src/vaadin-checkbox-mixin.js +++ b/packages/checkbox/src/vaadin-checkbox-mixin.js @@ -6,8 +6,8 @@ import { ActiveMixin } from '@vaadin/a11y-base/src/active-mixin.js'; import { DelegateFocusMixin } from '@vaadin/a11y-base/src/delegate-focus-mixin.js'; import { CheckedMixin } from '@vaadin/field-base/src/checked-mixin.js'; +import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js'; import { InputController } from '@vaadin/field-base/src/input-controller.js'; -import { LabelMixin } from '@vaadin/field-base/src/label-mixin.js'; import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js'; /** @@ -17,10 +17,10 @@ import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-c * @mixes ActiveMixin * @mixes CheckedMixin * @mixes DelegateFocusMixin - * @mixes LabelMixin + * @mixes FieldMixin */ export const CheckboxMixin = (superclass) => - class CheckboxMixinClass extends LabelMixin(CheckedMixin(DelegateFocusMixin(ActiveMixin(superclass)))) { + class CheckboxMixinClass extends FieldMixin(CheckedMixin(DelegateFocusMixin(ActiveMixin(superclass)))) { static get properties() { return { /** @@ -86,7 +86,7 @@ export const CheckboxMixin = (superclass) => /** @override */ static get delegateAttrs() { - return [...super.delegateAttrs, 'name']; + return [...super.delegateAttrs, 'name', 'invalid']; } constructor() { @@ -114,11 +114,14 @@ export const CheckboxMixin = (superclass) => }), ); this.addController(new LabelledInputController(this.inputElement, this._labelController)); + + this._createMethodObserver('_checkedChanged(checked)'); } /** * Override method inherited from `ActiveMixin` to prevent setting `active` - * attribute when readonly or when clicking a link placed inside the label. + * attribute when readonly, or when clicking a link placed inside the label, + * or when clicking slotted helper or error message element. * * @param {Event} event * @return {boolean} @@ -126,7 +129,12 @@ export const CheckboxMixin = (superclass) => * @override */ _shouldSetActive(event) { - if (this.readonly || event.target.localName === 'a') { + if ( + this.readonly || + event.target.localName === 'a' || + event.target === this._helperNode || + event.target === this._errorNode + ) { return false; } @@ -195,6 +203,58 @@ export const CheckboxMixin = (superclass) => super._toggleChecked(checked); } + /** + * @override + * @return {boolean} + */ + checkValidity() { + return !this.required || !!this.checked; + } + + /** + * Override method inherited from `FocusMixin` to validate on blur. + * @param {boolean} focused + * @protected + */ + _setFocused(focused) { + super._setFocused(focused); + + // Do not validate when focusout is caused by document + // losing focus, which happens on browser tab switch. + if (!focused && document.hasFocus()) { + this.validate(); + } + } + + /** @private */ + _checkedChanged(checked) { + if (checked || this.__oldChecked) { + this.validate(); + } + + this.__oldChecked = checked; + } + + /** + * Override an observer from `FieldMixin` + * to validate when required is removed. + * + * @protected + * @override + */ + _requiredChanged(required) { + super._requiredChanged(required); + + if (required === false) { + this.validate(); + } + } + + /** @private */ + _onRequiredIndicatorClick() { + this._labelNode.click(); + } + /** * Fired when the checkbox is checked or unchecked by the user. * diff --git a/packages/checkbox/src/vaadin-checkbox-styles.js b/packages/checkbox/src/vaadin-checkbox-styles.js index 55bb8a6763..ff660e6aa5 100644 --- a/packages/checkbox/src/vaadin-checkbox-styles.js +++ b/packages/checkbox/src/vaadin-checkbox-styles.js @@ -26,7 +26,7 @@ export const checkboxStyles = css` [part='checkbox'], ::slotted(input), - ::slotted(label) { + [part='label'] { grid-row: 1; } @@ -35,6 +35,16 @@ export const checkboxStyles = css` grid-column: 1; } + [part='helper-text'], + [part='error-message'] { + grid-column: 2; + } + + :host(:not([has-helper])) [part='helper-text'], + :host(:not([has-error-message])) [part='error-message'] { + display: none; + } + [part='checkbox'] { width: var(--vaadin-checkbox-size, 1em); height: var(--vaadin-checkbox-size, 1em); diff --git a/packages/checkbox/src/vaadin-checkbox.d.ts b/packages/checkbox/src/vaadin-checkbox.d.ts index 4d1e482f36..478391da02 100644 --- a/packages/checkbox/src/vaadin-checkbox.d.ts +++ b/packages/checkbox/src/vaadin-checkbox.d.ts @@ -45,21 +45,29 @@ export interface CheckboxEventMap extends HTMLElementEventMap, CheckboxCustomEve * * The following shadow DOM parts are available for styling: * - * Part name | Description - * ------------|------------- - * `checkbox` | The element representing a stylable custom checkbox. + * Part name | Description + * ---------------------|------------- + * `checkbox` | The element representing a stylable custom checkbox + * `label` | The slotted label element wrapper + * `helper-text` | The slotted helper text element wrapper + * `error-message` | The slotted error message element wrapper + * `required-indicator` | The `required` state indicator element * * The following state attributes are available for styling: * - * Attribute | Description - * ----------------|------------- - * `active` | Set when the checkbox is activated with mouse, touch or the keyboard. - * `checked` | Set when the checkbox is checked. - * `disabled` | Set when the checkbox is disabled. - * `focus-ring` | Set when the checkbox is focused using the keyboard. - * `focused` | Set when the checkbox is focused. - * `indeterminate` | Set when the checkbox is in the indeterminate state. - * `has-label` | Set when the checkbox has a label. + * Attribute | Description + * ---------------------|------------- + * `active` | Set when the checkbox is activated with mouse, touch or the keyboard. + * `checked` | Set when the checkbox is checked. + * `disabled` | Set when the checkbox is disabled. + * `readonly` | Set when the checkbox is readonly. + * `focus-ring` | Set when the checkbox is focused using the keyboard. + * `focused` | Set when the checkbox is focused. + * `indeterminate` | Set when the checkbox is in the indeterminate state. + * `invalid` | Set when the checkbox is invalid. + * `has-label` | Set when the checkbox has a label. + * `has-helper` | Set when the checkbox has helper text. + * `has-error-message` | Set when the checkbox has an error message. * * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation. * diff --git a/packages/checkbox/src/vaadin-checkbox.js b/packages/checkbox/src/vaadin-checkbox.js index 52800f408b..d5fbe99cff 100644 --- a/packages/checkbox/src/vaadin-checkbox.js +++ b/packages/checkbox/src/vaadin-checkbox.js @@ -24,21 +24,29 @@ registerStyles('vaadin-checkbox', checkboxStyles, { moduleId: 'vaadin-checkbox-s * * The following shadow DOM parts are available for styling: * - * Part name | Description - * ------------|------------- - * `checkbox` | The element representing a stylable custom checkbox. + * Part name | Description + * ---------------------|------------- + * `checkbox` | The element representing a stylable custom checkbox + * `label` | The slotted label element wrapper + * `helper-text` | The slotted helper text element wrapper + * `error-message` | The slotted error message element wrapper + * `required-indicator` | The `required` state indicator element * * The following state attributes are available for styling: * - * Attribute | Description - * ----------------|------------- - * `active` | Set when the checkbox is activated with mouse, touch or the keyboard. - * `checked` | Set when the checkbox is checked. - * `disabled` | Set when the checkbox is disabled. - * `focus-ring` | Set when the checkbox is focused using the keyboard. - * `focused` | Set when the checkbox is focused. - * `indeterminate` | Set when the checkbox is in the indeterminate state. - * `has-label` | Set when the checkbox has a label. + * Attribute | Description + * ---------------------|------------- + * `active` | Set when the checkbox is activated with mouse, touch or the keyboard. + * `checked` | Set when the checkbox is checked. + * `disabled` | Set when the checkbox is disabled. + * `readonly` | Set when the checkbox is readonly. + * `focus-ring` | Set when the checkbox is focused using the keyboard. + * `focused` | Set when the checkbox is focused. + * `indeterminate` | Set when the checkbox is in the indeterminate state. + * `invalid` | Set when the checkbox is invalid. + * `has-label` | Set when the checkbox has a label. + * `has-helper` | Set when the checkbox has helper text. + * `has-error-message` | Set when the checkbox has an error message. * * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation. * @@ -62,7 +70,16 @@ export class Checkbox extends CheckboxMixin(ElementMixin(ThemableMixin(PolymerEl
- +
+ +
+
+
+ +
+
+ +
`; diff --git a/packages/checkbox/src/vaadin-lit-checkbox.js b/packages/checkbox/src/vaadin-lit-checkbox.js index 1c06ca3a38..98e751e30d 100644 --- a/packages/checkbox/src/vaadin-lit-checkbox.js +++ b/packages/checkbox/src/vaadin-lit-checkbox.js @@ -36,7 +36,16 @@ export class Checkbox extends CheckboxMixin(ElementMixin(ThemableMixin(PolylitMi
- +
+ +
+
+
+ +
+
+ +
`; diff --git a/packages/checkbox/test/checkbox.common.js b/packages/checkbox/test/checkbox.common.js index 62cab3a762..404f504260 100644 --- a/packages/checkbox/test/checkbox.common.js +++ b/packages/checkbox/test/checkbox.common.js @@ -1,5 +1,5 @@ import { expect } from '@esm-bundle/chai'; -import { fixtureSync, mousedown, mouseup, nextRender, nextUpdate } from '@vaadin/testing-helpers'; +import { fixtureSync, mousedown, mouseup, nextFrame, nextRender, nextUpdate } from '@vaadin/testing-helpers'; import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands'; import sinon from 'sinon'; @@ -61,6 +61,17 @@ describe('checkbox', () => { expect(checkbox.checked).to.be.false; }); + it('should toggle checked property on required indicator click', async () => { + const indicator = checkbox.shadowRoot.querySelector('[part="required-indicator"]'); + indicator.click(); + await nextUpdate(checkbox); + expect(checkbox.checked).to.be.true; + + indicator.click(); + await nextUpdate(checkbox); + expect(checkbox.checked).to.be.false; + }); + it('should not toggle checked property on label link click', async () => { link.click(); await nextUpdate(checkbox); @@ -210,6 +221,21 @@ describe('checkbox', () => { await sendKeys({ up: 'Space' }); expect(checkbox.hasAttribute('active')).to.be.false; }); + + it('should not set active attribute on helper element click', async () => { + checkbox.helperText = 'Helper'; + await nextFrame(); + mousedown(checkbox.querySelector('[slot="helper"]')); + expect(checkbox.hasAttribute('active')).to.be.false; + }); + + it('should not set active attribute on error message element click', async () => { + checkbox.errorMessage = 'Error'; + checkbox.invalid = true; + await nextFrame(); + mousedown(checkbox.querySelector('[slot="error-message"]')); + expect(checkbox.hasAttribute('active')).to.be.false; + }); }); describe('change event', () => { diff --git a/packages/checkbox/test/dom/__snapshots__/checkbox.test.snap.js b/packages/checkbox/test/dom/__snapshots__/checkbox.test.snap.js index a0d23e270b..7f6f89cffe 100644 --- a/packages/checkbox/test/dom/__snapshots__/checkbox.test.snap.js +++ b/packages/checkbox/test/dom/__snapshots__/checkbox.test.snap.js @@ -4,13 +4,19 @@ export const snapshots = {}; snapshots["vaadin-checkbox host default"] = ` + + + + + + + + +
+ Helper +
+
+`; +/* end snapshot vaadin-checkbox host helper */ + +snapshots["vaadin-checkbox host required"] = +` + + + + +`; +/* end snapshot vaadin-checkbox host required */ + +snapshots["vaadin-checkbox host error"] = +` + + + + +`; +/* end snapshot vaadin-checkbox host error */ + snapshots["vaadin-checkbox shadow default"] = `
- - +
+ + +
+
+
+
+ + +
+
+ + +
diff --git a/packages/checkbox/test/dom/checkbox.test.js b/packages/checkbox/test/dom/checkbox.test.js index d03601da34..407fa38fc9 100644 --- a/packages/checkbox/test/dom/checkbox.test.js +++ b/packages/checkbox/test/dom/checkbox.test.js @@ -1,5 +1,5 @@ import { expect } from '@esm-bundle/chai'; -import { fixtureSync } from '@vaadin/testing-helpers'; +import { aTimeout, fixtureSync } from '@vaadin/testing-helpers'; import '../../vaadin-checkbox.js'; import { resetUniqueId } from '@vaadin/component-base/src/unique-id-utils.js'; @@ -35,6 +35,23 @@ describe('vaadin-checkbox', () => { checkbox.readonly = true; await expect(checkbox).dom.to.equalSnapshot(); }); + + it('helper', async () => { + checkbox.helperText = 'Helper'; + await expect(checkbox).dom.to.equalSnapshot(); + }); + + it('required', async () => { + checkbox.required = true; + await expect(checkbox).dom.to.equalSnapshot(); + }); + + it('error', async () => { + checkbox.errorMessage = 'Error'; + checkbox.invalid = true; + await aTimeout(0); + await expect(checkbox).dom.to.equalSnapshot(); + }); }); describe('shadow', () => { diff --git a/packages/checkbox/test/typings/checkbox.types.ts b/packages/checkbox/test/typings/checkbox.types.ts index b513bb5cd9..e9ea54ed6b 100644 --- a/packages/checkbox/test/typings/checkbox.types.ts +++ b/packages/checkbox/test/typings/checkbox.types.ts @@ -7,7 +7,9 @@ import type { KeyboardMixinClass } from '@vaadin/a11y-base/src/keyboard-mixin.js import type { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js'; import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js'; import type { CheckedMixinClass } from '@vaadin/field-base/src/checked-mixin.js'; +import type { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js'; import type { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js'; +import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js'; import type { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; import type { CheckboxMixinClass } from '../../src/vaadin-checkbox-mixin.js'; import type { @@ -37,12 +39,14 @@ assertType(checkbox); assertType(checkbox); assertType(checkbox); assertType(checkbox); +assertType(checkbox); assertType(checkbox); assertType(checkbox); assertType(checkbox); assertType(checkbox); assertType(checkbox); assertType(checkbox); +assertType(checkbox); // Events checkbox.addEventListener('checked-changed', (event) => { diff --git a/packages/checkbox/test/validation-lit.test.js b/packages/checkbox/test/validation-lit.test.js new file mode 100644 index 0000000000..eb186ae2bf --- /dev/null +++ b/packages/checkbox/test/validation-lit.test.js @@ -0,0 +1,2 @@ +import '../src/vaadin-lit-checkbox.js'; +import './validation.common.js'; diff --git a/packages/checkbox/test/validation-polymer.test.js b/packages/checkbox/test/validation-polymer.test.js new file mode 100644 index 0000000000..887e5b2c43 --- /dev/null +++ b/packages/checkbox/test/validation-polymer.test.js @@ -0,0 +1,2 @@ +import '../src/vaadin-checkbox.js'; +import './validation.common.js'; diff --git a/packages/checkbox/test/validation.common.js b/packages/checkbox/test/validation.common.js new file mode 100644 index 0000000000..91b98d0bf7 --- /dev/null +++ b/packages/checkbox/test/validation.common.js @@ -0,0 +1,147 @@ +import { expect } from '@esm-bundle/chai'; +import { fixtureSync, nextFrame, nextRender, nextUpdate } from '@vaadin/testing-helpers'; +import { sendKeys } from '@web/test-runner-commands'; +import sinon from 'sinon'; + +describe('validation', () => { + let checkbox, validateSpy; + + describe('initial', () => { + beforeEach(() => { + checkbox = document.createElement('vaadin-checkbox'); + validateSpy = sinon.spy(checkbox, 'validate'); + }); + + afterEach(() => { + checkbox.remove(); + }); + + it('should not validate by default', async () => { + document.body.appendChild(checkbox); + await nextRender(); + expect(validateSpy.called).to.be.false; + }); + + it('should not validate when the checkbox is initially checked', async () => { + checkbox.checked = true; + document.body.appendChild(checkbox); + await nextRender(); + expect(validateSpy.called).to.be.false; + }); + + it('should not validate when the field is initially checked and invalid', async () => { + checkbox.checked = true; + checkbox.invalid = true; + document.body.appendChild(checkbox); + await nextRender(); + expect(validateSpy.called).to.be.false; + }); + }); + + describe('basic', () => { + beforeEach(async () => { + checkbox = fixtureSync(''); + await nextRender(); + validateSpy = sinon.spy(checkbox, 'validate'); + }); + + it('should pass validation by default', () => { + expect(checkbox.checkValidity()).to.be.true; + }); + + it('should validate when toggling checked property', async () => { + checkbox.checked = true; + await nextUpdate(checkbox); + expect(validateSpy.calledOnce).to.be.true; + + checkbox.checked = false; + await nextUpdate(checkbox); + expect(validateSpy.calledTwice).to.be.true; + }); + + it('should validate on focusout', async () => { + // Focus the checkbox. + await sendKeys({ press: 'Tab' }); + expect(validateSpy.called).to.be.false; + + // Blur the checkbox. + await sendKeys({ down: 'Shift' }); + await sendKeys({ press: 'Tab' }); + await sendKeys({ up: 'Shift' }); + + expect(validateSpy.calledOnce).to.be.true; + }); + + it('should fire a validated event on validation success', () => { + const validatedSpy = sinon.spy(); + checkbox.addEventListener('validated', validatedSpy); + checkbox.validate(); + + expect(validatedSpy.calledOnce).to.be.true; + const event = validatedSpy.firstCall.args[0]; + expect(event.detail.valid).to.be.true; + }); + + it('should fire a validated event on validation failure', () => { + const validatedSpy = sinon.spy(); + checkbox.addEventListener('validated', validatedSpy); + checkbox.required = true; + checkbox.validate(); + + expect(validatedSpy.calledOnce).to.be.true; + const event = validatedSpy.firstCall.args[0]; + expect(event.detail.valid).to.be.false; + }); + + describe('document losing focus', () => { + beforeEach(() => { + sinon.stub(document, 'hasFocus').returns(false); + }); + + afterEach(() => { + document.hasFocus.restore(); + }); + + it('should not validate on blur when document does not have focus', async () => { + // Focus the checkbox. + await sendKeys({ press: 'Tab' }); + + // Blur the checkbox. + await sendKeys({ down: 'Shift' }); + await sendKeys({ press: 'Tab' }); + await sendKeys({ up: 'Shift' }); + + expect(validateSpy.called).to.be.false; + }); + }); + }); + + describe('required', () => { + let checkboxes; + + beforeEach(async () => { + checkbox = fixtureSync(''); + await nextFrame(); + validateSpy = sinon.spy(checkbox, 'validate'); + }); + + it('should fail validation with checked set to false', () => { + expect(checkbox.checkValidity()).to.be.false; + }); + + it('should pass validation with checked set to true', () => { + checkbox.checked = true; + expect(checkbox.checkValidity()).to.be.true; + }); + + it('should be valid after toggling a checkbox', () => { + checkbox.click(); + expect(checkbox.invalid).to.be.false; + }); + + it('should pass validation with required set to false', () => { + checkbox.required = false; + expect(checkbox.invalid).to.be.false; + }); + }); +}); diff --git a/packages/checkbox/test/visual/lumo/checkbox.test.js b/packages/checkbox/test/visual/lumo/checkbox.test.js index f05d9adf24..f3e5d3b607 100644 --- a/packages/checkbox/test/visual/lumo/checkbox.test.js +++ b/packages/checkbox/test/visual/lumo/checkbox.test.js @@ -44,6 +44,30 @@ describe('checkbox', () => { await visualDiff(div, 'checked-focus-ring'); }); + it('required', async () => { + element.required = true; + await visualDiff(div, 'required'); + }); + + it('invalid focus-ring', async () => { + element.required = true; + element.invalid = true; + await sendKeys({ press: 'Tab' }); + await visualDiff(div, 'invalid-focus-ring'); + }); + + it('error message', async () => { + element.errorMessage = 'This field is required'; + element.required = true; + element.validate(); + await visualDiff(div, 'error-message'); + }); + + it('helper text', async () => { + element.helperText = 'Helper text'; + await visualDiff(div, 'helper-text'); + }); + describe('disabled', () => { beforeEach(() => { element.disabled = true; @@ -106,6 +130,11 @@ describe('checkbox', () => { element.label = ''; await visualDiff(div, 'rtl-empty'); }); + + it('required', async () => { + element.required = true; + await visualDiff(div, 'rtl-required'); + }); }); describe('borders enabled', () => { @@ -141,6 +170,11 @@ describe('checkbox', () => { await visualDiff(div, 'bordered-readonly'); }); + it('bordered invalid', async () => { + element.invalid = true; + await visualDiff(div, 'bordered-invalid'); + }); + it('Bordered dark', async () => { document.documentElement.setAttribute('theme', 'dark'); await visualDiff(div, 'bordered-dark'); diff --git a/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/bordered-invalid.png b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/bordered-invalid.png new file mode 100644 index 0000000000..e3d7046662 Binary files /dev/null and b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/bordered-invalid.png differ diff --git a/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/error-message.png b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/error-message.png new file mode 100644 index 0000000000..703461ec09 Binary files /dev/null and b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/error-message.png differ diff --git a/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/helper-text.png b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/helper-text.png new file mode 100644 index 0000000000..bc0f92335f Binary files /dev/null and b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/helper-text.png differ diff --git a/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/invalid-focus-ring.png b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/invalid-focus-ring.png new file mode 100644 index 0000000000..21b8c70de3 Binary files /dev/null and b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/invalid-focus-ring.png differ diff --git a/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/required.png b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/required.png new file mode 100644 index 0000000000..64669cb057 Binary files /dev/null and b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/required.png differ diff --git a/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/rtl-required.png b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/rtl-required.png new file mode 100644 index 0000000000..796ee81a55 Binary files /dev/null and b/packages/checkbox/test/visual/lumo/screenshots/checkbox/baseline/rtl-required.png differ diff --git a/packages/checkbox/test/visual/material/checkbox.test.js b/packages/checkbox/test/visual/material/checkbox.test.js index 5b08468025..6145dbc006 100644 --- a/packages/checkbox/test/visual/material/checkbox.test.js +++ b/packages/checkbox/test/visual/material/checkbox.test.js @@ -43,6 +43,23 @@ describe('checkbox', () => { await visualDiff(div, 'checked-focus-ring'); }); + it('required', async () => { + element.required = true; + await visualDiff(div, 'required'); + }); + + it('error message', async () => { + element.errorMessage = 'This field is required'; + element.required = true; + element.validate(); + await visualDiff(div, 'error-message'); + }); + + it('helper text', async () => { + element.helperText = 'Helper text'; + await visualDiff(div, 'helper-text'); + }); + describe('readonly', () => { beforeEach(() => { element.readonly = true; @@ -106,5 +123,10 @@ describe('checkbox', () => { element.label = ''; await visualDiff(div, 'rtl-empty'); }); + + it('required', async () => { + element.required = true; + await visualDiff(div, 'rtl-required'); + }); }); }); diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/checked-focus-ring.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/checked-focus-ring.png index a4d5fa1bb5..015e54a385 100644 Binary files a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/checked-focus-ring.png and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/checked-focus-ring.png differ diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/error-message.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/error-message.png new file mode 100644 index 0000000000..d01f729141 Binary files /dev/null and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/error-message.png differ diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/focus-ring.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/focus-ring.png index 744278d649..7bab3d4815 100644 Binary files a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/focus-ring.png and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/focus-ring.png differ diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/helper-text.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/helper-text.png new file mode 100644 index 0000000000..62bcc54386 Binary files /dev/null and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/helper-text.png differ diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/readonly-checked-focus-ring.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/readonly-checked-focus-ring.png index aa8c7a3406..4370989cb4 100644 Binary files a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/readonly-checked-focus-ring.png and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/readonly-checked-focus-ring.png differ diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/required.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/required.png new file mode 100644 index 0000000000..3bbd7ed56f Binary files /dev/null and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/required.png differ diff --git a/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/rtl-required.png b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/rtl-required.png new file mode 100644 index 0000000000..bdf6672925 Binary files /dev/null and b/packages/checkbox/test/visual/material/screenshots/checkbox/baseline/rtl-required.png differ diff --git a/packages/checkbox/theme/lumo/vaadin-checkbox-styles.js b/packages/checkbox/theme/lumo/vaadin-checkbox-styles.js index de3a9ab275..8596d2dac6 100644 --- a/packages/checkbox/theme/lumo/vaadin-checkbox-styles.js +++ b/packages/checkbox/theme/lumo/vaadin-checkbox-styles.js @@ -26,6 +26,14 @@ registerStyles( --_focus-ring-color: var(--vaadin-focus-ring-color, var(--lumo-primary-color-50pct)); --_focus-ring-width: var(--vaadin-focus-ring-width, 2px); --_selection-color: var(--vaadin-selection-color, var(--lumo-primary-color)); + --_helper-spacing: var(--vaadin-input-field-helper-spacing, 0.2em); + --_invalid-background: var(--vaadin-input-field-invalid-background, var(--lumo-error-color-10pct)); + } + + [part='label'] { + display: flex; + position: relative; + max-width: max-content; } :host([has-label]) ::slotted(label) { @@ -35,6 +43,14 @@ registerStyles( ); } + :host([dir='rtl'][has-label]) ::slotted(label) { + padding: var(--lumo-space-xs) var(--lumo-space-xs) var(--lumo-space-xs) var(--lumo-space-s); + } + + :host([has-label][required]) ::slotted(label) { + padding-inline-end: var(--lumo-space-m); + } + [part='checkbox'] { width: var(--_checkbox-size); height: var(--_checkbox-size); @@ -149,11 +165,6 @@ registerStyles( background-color: var(--vaadin-checkbox-readonly-checked-background, var(--lumo-contrast-70pct)); } - /* RTL specific styles */ - :host([dir='rtl'][has-label]) ::slotted(label) { - padding: var(--lumo-space-xs) var(--lumo-space-xs) var(--lumo-space-xs) var(--lumo-space-s); - } - /* Used for activation "halo" */ [part='checkbox']::before { pointer-events: none; @@ -169,13 +180,14 @@ registerStyles( } /* Hover */ - :host(:not([checked]):not([indeterminate]):not([disabled]):not([readonly]):hover) [part='checkbox'] { + :host(:not([checked]):not([indeterminate]):not([disabled]):not([readonly]):not([invalid]):hover) [part='checkbox'] { background: var(--vaadin-checkbox-background-hover, var(--lumo-contrast-30pct)); } /* Disable hover for touch devices */ @media (pointer: coarse) { - :host(:not([checked]):not([indeterminate]):not([disabled]):not([readonly]):hover) [part='checkbox'] { + /* prettier-ignore */ + :host(:not([checked]):not([indeterminate]):not([disabled]):not([readonly]):not([invalid]):hover) [part='checkbox'] { background: var(--vaadin-checkbox-background, var(--lumo-contrast-20pct)); } } @@ -195,6 +207,100 @@ registerStyles( transform: scale(0); opacity: 0.4; } + + /* Required */ + :host([required]) [part='required-indicator'] { + position: absolute; + top: var(--lumo-space-xs); + right: var(--lumo-space-xs); + } + + :host([required][dir='rtl']) [part='required-indicator'] { + right: auto; + left: var(--lumo-space-xs); + } + + :host([required]) [part='required-indicator']::after { + content: var(--lumo-required-field-indicator, '\\2022'); + transition: opacity 0.2s; + color: var(--lumo-required-field-indicator-color, var(--lumo-primary-text-color)); + width: 1em; + text-align: center; + } + + /* Invalid */ + :host([invalid]) { + --vaadin-input-field-border-color: var(--lumo-error-color); + } + + :host([invalid]) [part='checkbox'] { + background: var(--_invalid-background); + background-image: linear-gradient(var(--_invalid-background) 0%, var(--_invalid-background) 100%); + } + + :host([invalid]:hover) [part='checkbox'] { + background-image: linear-gradient(var(--_invalid-background) 0%, var(--_invalid-background) 100%), + linear-gradient(var(--_invalid-background) 0%, var(--_invalid-background) 100%); + } + + :host([invalid][focus-ring]) { + --_focus-ring-color: var(--lumo-error-color-50pct); + } + + :host([invalid]) [part='required-indicator']::after { + color: var(--lumo-required-field-indicator-color, var(--lumo-error-text-color)); + } + + /* Error message */ + [part='error-message'] { + font-size: var(--vaadin-input-field-error-font-size, var(--lumo-font-size-xs)); + line-height: var(--lumo-line-height-xs); + font-weight: var(--vaadin-input-field-error-font-weight, 400); + color: var(--vaadin-input-field-error-color, var(--lumo-error-text-color)); + will-change: max-height; + transition: 0.4s max-height; + max-height: 5em; + padding-inline-start: var(--lumo-space-xs); + } + + :host([has-error-message]) [part='error-message']::before, + :host([has-error-message]) [part='error-message']::after { + content: ''; + display: block; + height: 0.2em; + } + + :host(:not([invalid])) [part='error-message'] { + max-height: 0; + overflow: hidden; + } + + /* Helper */ + :host([has-helper]) [part='helper-text']::before { + content: ''; + display: block; + height: var(--_helper-spacing); + } + + [part='helper-text'] { + display: block; + color: var(--vaadin-input-field-helper-color, var(--lumo-secondary-text-color)); + font-size: var(--vaadin-input-field-helper-font-size, var(--lumo-font-size-xs)); + line-height: var(--lumo-line-height-xs); + font-weight: var(--vaadin-input-field-helper-font-weight, 400); + margin-left: calc(var(--lumo-border-radius-m) / 4); + transition: color 0.2s; + padding-inline-start: var(--lumo-space-xs); + } + + :host(:hover:not([readonly])) [part='helper-text'] { + color: var(--lumo-body-text-color); + } + + :host([disabled]) [part='helper-text'] { + color: var(--lumo-disabled-text-color); + -webkit-text-fill-color: var(--lumo-disabled-text-color); + } `, { moduleId: 'lumo-checkbox' }, ); diff --git a/packages/checkbox/theme/material/vaadin-checkbox-styles.js b/packages/checkbox/theme/material/vaadin-checkbox-styles.js index 05b4e4eb18..0718ba8ed5 100644 --- a/packages/checkbox/theme/material/vaadin-checkbox-styles.js +++ b/packages/checkbox/theme/material/vaadin-checkbox-styles.js @@ -15,6 +15,12 @@ registerStyles( --_checkbox-size: var(--vaadin-checkbox-size, 16px); } + [part='label'] { + display: flex; + position: relative; + max-width: max-content; + } + :host([has-label]) ::slotted(label) { padding: 3px 12px 3px 6px; } @@ -128,6 +134,49 @@ registerStyles( :host([dir='rtl'][has-label]) ::slotted(label) { padding: 3px 6px 3px 12px; } + + /* Required */ + :host([required]) [part='required-indicator'] { + position: absolute; + top: 3px; + right: 2px; + } + + :host([dir='rtl'][required]) [part='required-indicator'] { + right: auto; + left: 2px; + } + + :host([required]) [part='required-indicator']::after { + content: '*'; + color: var(--material-secondary-text-color); + } + + :host([invalid]) [part='required-indicator']::after { + color: var(--material-error-text-color); + } + + [part='error-message'], + [part='helper-text'] { + font-size: 0.75em; + line-height: 1; + padding-left: 6px; + } + + [part='error-message'] { + color: var(--material-error-text-color); + } + + [part='helper-text'] { + color: var(--material-secondary-text-color); + } + + :host([has-error-message]) [part='error-message']::before, + :host([has-helper]) [part='helper-text']::before { + content: ''; + display: block; + height: 6px; + } `, { moduleId: 'material-checkbox' }, ); diff --git a/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-focused.png b/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-focused.png index 938a7d92f4..dcaa41a47d 100644 Binary files a/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-focused.png and b/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-focused.png differ diff --git a/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-group-focused.png b/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-group-focused.png index 6af915188d..4543e3c277 100644 Binary files a/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-group-focused.png and b/packages/field-highlighter/test/visual/material/screenshots/field-highlighter/baseline/checkbox-group-focused.png differ