From 7b84fca5b810a56d473e36dd962b75f3777c6529 Mon Sep 17 00:00:00 2001 From: Material Web Team Date: Mon, 19 Dec 2022 09:34:06 -0800 Subject: [PATCH] feat(checkbox): Checkbox now supports form submission and label activation by using FormController and setting formAssociated. PiperOrigin-RevId: 496420219 --- checkbox/lib/checkbox.ts | 34 +++++++++++++- checkbox/lib/checkbox_test.ts | 86 +++++++++++++++++++++++++++-------- 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/checkbox/lib/checkbox.ts b/checkbox/lib/checkbox.ts index a85b5611ec..6ab2422998 100644 --- a/checkbox/lib/checkbox.ts +++ b/checkbox/lib/checkbox.ts @@ -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'; @@ -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}) @@ -35,9 +44,30 @@ export class Checkbox extends LitElement { @state() private prevDisabled = false; @state() private prevIndeterminate = false; @queryAsync('md-ripple') private readonly ripple!: Promise; + @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) { if (changed.has('checked') || changed.has('disabled') || changed.has('indeterminate')) { diff --git a/checkbox/lib/checkbox_test.ts b/checkbox/lib/checkbox_test.ts index c987174457..516537e32d 100644 --- a/checkbox/lib/checkbox_test.ts +++ b/checkbox/lib/checkbox_test.ts @@ -22,9 +22,9 @@ declare global { describe('checkbox', () => { const env = new Environment(); - async function setupTest() { - const element = env.render(html``) - .querySelector('md-test-checkbox'); + async function setupTest( + template = html``) { + const element = env.render(template).querySelector('md-test-checkbox'); if (!element) { throw new Error('Could not query rendered .'); } @@ -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 () => { @@ -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 = {}) { + return await setupTest(html` +
+ +
`); + } + + 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` + + `); + const label = (test.harness.element.getRootNode() as HTMLElement) + .querySelector('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 () => {