Skip to content

Commit

Permalink
feat(checkbox): add required and form validity
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 551311394
  • Loading branch information
asyncLiz authored and copybara-github committed Jul 26, 2023
1 parent 08d50e2 commit 5606eef
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 1 deletion.
143 changes: 142 additions & 1 deletion checkbox/internal/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export class Checkbox extends LitElement {
*/
@property({type: Boolean}) indeterminate = false;

/**
* When true, require the checkbox to be selected when participating in
* form submission.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
*/
@property({type: Boolean}) required = false;

/**
* The value of the checkbox that is submitted with a form when selected.
*
Expand Down Expand Up @@ -85,10 +93,46 @@ export class Checkbox extends LitElement {
return this.internals.labels;
}

/**
* Returns a ValidityState object that represents the validity states of the
* checkbox.
*
* Note that checkboxes will only set `valueMissing` if `required` and not
* checked.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#validation
*/
get validity() {
this.syncValidity();
return this.internals.validity;
}

/**
* Returns the native validation error message.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
*/
get validationMessage() {
this.syncValidity();
return this.internals.validationMessage;
}

/**
* Returns whether an element will successfully validate based on forms
* validation rules and constraints.
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation#constraint_validation_process
*/
get willValidate() {
this.syncValidity();
return this.internals.willValidate;
}

@state() private prevChecked = false;
@state() private prevDisabled = false;
@state() private prevIndeterminate = false;
@query('input') private readonly input!: HTMLInputElement|null;
@query('.outline') private readonly outline!: HTMLElement|null;
private readonly internals =
(this as HTMLElement /* needed for closure */).attachInternals();

Expand All @@ -105,7 +149,72 @@ export class Checkbox extends LitElement {
}
}

protected override update(changed: PropertyValues<Checkbox>) {
/**
* Checks the checkbox's native validation and returns whether or not the
* element is valid.
*
* If invalid, this method will dispatch the `invalid` event.
*
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity
*
* @return true if the checkbox is valid, or false if not.
*/
checkValidity() {
this.syncValidity();
return this.internals.checkValidity();
}

/**
* Checks the checkbox's native validation and returns whether or not the
* element is valid.
*
* If invalid, this method will dispatch the `invalid` event.
*
* The `validationMessage` is reported to the user by the browser. Use
* `setCustomValidity()` to customize the `validationMessage`.
*
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/reportValidity
*
* @return true if the checkbox is valid, or false if not.
*/
reportValidity() {
this.syncValidity();
return this.internals.reportValidity();
}

/**
* Checks the checkbox's native validation and returns whether or not the
* element is valid.
*
* If invalid, this method will dispatch the `invalid` event.
*
* The checkbox's `error` state will be set to true if invalid, or false if
* valid.
*
* @return true if the checkbox is valid, or false if not.
*/
showValidity() {
const isValid = this.checkValidity();
this.error = !isValid;
return isValid;
}

/**
* Sets the checkbox's native validation error message. This is used to
* customize `validationMessage`.
*
* When the error is not an empty string, the checkbox is considered invalid
* and `validity.customError` will be true.
*
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity
*
* @param error The error message to display.
*/
setCustomValidity(error: string) {
this.internals.setValidity({customError: !!error}, error);
}

protected override update(changed: PropertyValues<this>) {
if (changed.has('checked') || changed.has('disabled') ||
changed.has('indeterminate')) {
this.prevChecked = changed.get('checked') ?? this.checked;
Expand Down Expand Up @@ -159,6 +268,7 @@ export class Checkbox extends LitElement {
aria-label=${ariaLabel || nothing}
aria-invalid=${this.error || nothing}
?disabled=${this.disabled}
?required=${this.required}
.indeterminate=${this.indeterminate}
.checked=${this.checked}
@change=${this.handleChange}
Expand All @@ -175,6 +285,37 @@ export class Checkbox extends LitElement {
redispatchEvent(this, event);
}

private syncValidity() {
// Sync the internal <input>'s validity and the host's ElementInternals
// validity. We do this to re-use native `<input>` validation messages.
const input = this.getInput();
if (this.internals.validity.customError) {
input.setCustomValidity(this.internals.validationMessage);
} else {
input.setCustomValidity('');
}

this.internals.setValidity(
input.validity, input.validationMessage, this.outline!);
}

private getInput() {
if (!this.input) {
// If the input is not yet defined, synchronously render.
this.connectedCallback();
this.performUpdate();
}

if (this.isUpdatePending) {
// If there are pending updates, synchronously perform them. This ensures
// that constraint validation properties (like `required`) are synced
// before interacting with input APIs that depend on them.
this.scheduleUpdate();
}

return this.input!;
}

/** @private */
formResetCallback() {
// The checked property does not reflect, so the original attribute set by
Expand Down
53 changes: 53 additions & 0 deletions checkbox/internal/checkbox_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,57 @@ describe('checkbox', () => {
expect(element.checked).toBeFalse();
});
});

describe('validation', () => {
it('should set valueMissing when required and not selected', async () => {
const {harness} = await setupTest();
harness.element.required = true;

expect(harness.element.validity.valueMissing)
.withContext('checkbox.validity.valueMissing')
.toBeTrue();
});

it('should not set valueMissing when required and checked', async () => {
const {harness} = await setupTest();
harness.element.required = true;
harness.element.checked = true;

expect(harness.element.validity.valueMissing)
.withContext('checkbox.validity.valueMissing')
.toBeFalse();
});

it('should set valueMissing when required and indeterminate', async () => {
const {harness} = await setupTest();
harness.element.required = true;
harness.element.indeterminate = true;

expect(harness.element.validity.valueMissing)
.withContext('checkbox.validity.valueMissing')
.toBeTrue();
});

it('should set error to true when showValidity() is called and checkbox is invalid',
async () => {
const {harness} = await setupTest();
harness.element.required = true;

harness.element.showValidity();
expect(harness.element.error).withContext('checkbox.error').toBeTrue();
});

it('should set error to false when showValidity() is called and checkbox is valid',
async () => {
const {harness} = await setupTest();
harness.element.required = true;
harness.element.error = true;
harness.element.checked = true;

harness.element.showValidity();
expect(harness.element.error)
.withContext('checkbox.error')
.toBeFalse();
});
});
});

0 comments on commit 5606eef

Please sign in to comment.