Skip to content

Commit

Permalink
feat(checkbox): Checkbox now supports form submission and label activ…
Browse files Browse the repository at this point in the history
…ation by using FormController and setting formAssociated.

PiperOrigin-RevId: 496420219
  • Loading branch information
material-web-copybara authored and copybara-github committed Dec 19, 2022
1 parent 6b74925 commit 7b84fca
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 21 deletions.
34 changes: 32 additions & 2 deletions checkbox/lib/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import '../../focus/focus-ring.js';
import '../../ripple/ripple.js';

import {html, LitElement, nothing, PropertyValues, TemplateResult} from 'lit';
import {property, queryAsync, state} from 'lit/decorators.js';
import {property, query, queryAsync, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {when} from 'lit/directives/when.js';

import {redispatchEvent} from '../../controller/events.js';
import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../controller/events.js';
import {FormController, getFormValue} from '../../controller/form-controller.js';
import {ariaProperty} from '../../decorators/aria-property.js';
import {pointerPress, shouldShowStrongFocus} from '../../focus/strong-focus.js';
import {ripple} from '../../ripple/directive.js';
Expand All @@ -22,10 +23,18 @@ import {MdRipple} from '../../ripple/ripple.js';
* A checkbox component.
*/
export class Checkbox extends LitElement {
static formAssociated = true;

@property({type: Boolean, reflect: true}) checked = false;
@property({type: Boolean, reflect: true}) disabled = false;
@property({type: Boolean, reflect: true}) error = false;
@property({type: Boolean, reflect: true}) indeterminate = false;
@property() value = 'on';
@property() name = '';

get form() {
return this.closest('form');
}

@ariaProperty // tslint:disable-line:no-new-decorators
@property({type: String, attribute: 'data-aria-label', noAccessor: true})
Expand All @@ -35,9 +44,30 @@ export class Checkbox extends LitElement {
@state() private prevDisabled = false;
@state() private prevIndeterminate = false;
@queryAsync('md-ripple') private readonly ripple!: Promise<MdRipple|null>;
@query('input') private readonly input!: HTMLInputElement|null;
@state() private showFocusRing = false;
@state() private showRipple = false;

constructor() {
super();
this.addController(new FormController(this));
this.addEventListener('click', (event: MouseEvent) => {
if (!isActivationClick(event)) {
return;
}
this.focus();
dispatchActivationClick(this.input!);
});
}

override focus() {
this.input?.focus();
}

[getFormValue]() {
return this.checked ? this.value : null;
}

protected override update(changed: PropertyValues<Checkbox>) {
if (changed.has('checked') || changed.has('disabled') ||
changed.has('indeterminate')) {
Expand Down
86 changes: 67 additions & 19 deletions checkbox/lib/checkbox_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ declare global {
describe('checkbox', () => {
const env = new Environment();

async function setupTest() {
const element = env.render(html`<md-test-checkbox></md-test-checkbox>`)
.querySelector('md-test-checkbox');
async function setupTest(
template = html`<md-test-checkbox></md-test-checkbox>`) {
const element = env.render(template).querySelector('md-test-checkbox');
if (!element) {
throw new Error('Could not query rendered <md-test-checkbox>.');
}
Expand Down Expand Up @@ -55,8 +55,7 @@ describe('checkbox', () => {
expect(harness.element.indeterminate).toEqual(false);
expect(harness.element.disabled).toEqual(false);
expect(harness.element.error).toEqual(false);
// TODO(b/261219117): re-add with FormController
// expect(harness.element.value).toEqual('on');
expect(harness.element.value).toEqual('on');
});

it('user input updates checked state', async () => {
Expand Down Expand Up @@ -147,20 +146,69 @@ describe('checkbox', () => {
});
});

// TODO(b/261219117): re-add with FormController
// describe('value', () => {
// it('get/set updates the value of the native checkbox element', async ()
// => {
// const {harness, input} = await setupTest();
// harness.element.value = 'new value';
// await env.waitForStability();

// expect(input.value).toEqual('new value');
// harness.element.value = 'new value 2';
// await env.waitForStability();
// expect(input.value).toEqual('new value 2');
// });
// });
describe('form submission', () => {
async function setupFormTest(propsInit: Partial<Checkbox> = {}) {
return await setupTest(html`
<form>
<md-test-checkbox
.checked=${propsInit.checked === true}
.disabled=${propsInit.disabled === true}
.name=${propsInit.name ?? ''}
.value=${propsInit.value ?? ''}
></md-test-checkbox>
</form>`);
}

it('does not submit if not checked', async () => {
const {harness} = await setupFormTest({name: 'foo'});
const formData = await harness.submitForm();
expect(formData.get('foo')).toBeNull();
});

it('does not submit if disabled', async () => {
const {harness} =
await setupFormTest({name: 'foo', checked: true, disabled: true});
const formData = await harness.submitForm();
expect(formData.get('foo')).toBeNull();
});

it('does not submit if name is not provided', async () => {
const {harness} = await setupFormTest({checked: true});
const formData = await harness.submitForm();
const keys = Array.from(formData.keys());
expect(keys.length).toEqual(0);
});

it('submits under correct conditions', async () => {
const {harness} =
await setupFormTest({name: 'foo', checked: true, value: 'bar'});
const formData = await harness.submitForm();
expect(formData.get('foo')).toEqual('bar');
});
});

describe('label activation', () => {
async function setupLabelTest() {
const test = await setupTest(html`
<label>
<md-test-checkbox></md-test-checkbox>
</label>
`);
const label = (test.harness.element.getRootNode() as HTMLElement)
.querySelector<HTMLLabelElement>('label')!;
return {...test, label};
}

it('toggles when label is clicked', async () => {
const {harness: {element}, label} = await setupLabelTest();
label.click();
await env.waitForStability();
expect(element.checked).toBeTrue();
label.click();
await env.waitForStability();
expect(element.checked).toBeFalse();
});
});

describe('focus ring', () => {
it('hidden on non-keyboard focus', async () => {
Expand Down

0 comments on commit 7b84fca

Please sign in to comment.