Skip to content

Commit

Permalink
fix(radio): Radio supports form association and label activation by u…
Browse files Browse the repository at this point in the history
…sing FormController and setting `formAssociated`.

PiperOrigin-RevId: 496788453
  • Loading branch information
material-web-copybara authored and copybara-github committed Dec 21, 2022
1 parent 72edae4 commit 91c2425
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 10 deletions.
41 changes: 38 additions & 3 deletions radio/lib/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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 {dispatchActivationClick, isActivationClick} 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 @@ -27,6 +29,11 @@ import {SingleSelectionController} from './single-selection-controller.js';
* @soyCompatible
*/
export class Radio extends LitElement {
static override shadowRootOptions:
ShadowRootInit = {...LitElement.shadowRootOptions, delegatesFocus: true};

static formAssociated = true;

@property({type: Boolean, reflect: true})
get checked(): boolean {
return this._checked;
Expand Down Expand Up @@ -70,9 +77,15 @@ export class Radio extends LitElement {

@property({type: Boolean}) disabled = false;

/**
* The element value to use in form submission when checked.
*/
@property({type: String}) value = 'on';

@property({type: String}) name = '';
/**
* The HTML name to use in form submission.
*/
@property({type: String, reflect: true}) name = '';

/**
* Touch target extends beyond visual boundary of a component by default.
Expand All @@ -91,16 +104,38 @@ export class Radio extends LitElement {
@property({attribute: 'data-aria-label', noAccessor: true})
override ariaLabel!: string;

/**
* The associated form element with which this element's value will submit.
*/
get form() {
return this.closest('form');
}

@state() private focused = false;
@query('input') private readonly input!: HTMLInputElement|null;
@queryAsync('md-ripple') private readonly ripple!: Promise<MdRipple|null>;
private selectionController?: SingleSelectionController;
@state() private showFocusRing = false;
@state() private showRipple = false;

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

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

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

override connectedCallback() {
Expand Down
127 changes: 120 additions & 7 deletions radio/md-radio_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ const repeatedRadio = (values: string[]) => {
describe('md-radio', () => {
const env = new Environment();

// Note, this would be better in the harness, but waiting in the test setup
// can be flakey without access to the test `env`.
async function simulateKeyDown(element: HTMLElement, key: string) {
const event = new KeyboardEvent('keydown', {key, bubbles: true});
element.dispatchEvent(event);
// TODO(https://bugzilla.mozilla.org/show_bug.cgi?id=1804576)
// Remove delay when issue addressed.
await env.waitForStability();
}

let element: MdRadio;
let harness: RadioHarness;

Expand Down Expand Up @@ -119,9 +129,7 @@ describe('md-radio', () => {
const a2 = root.querySelectorAll('md-radio')[1];
expect(a2.checked).toBeTrue();

const eventRight =
new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true});
a2.dispatchEvent(eventRight);
await simulateKeyDown(a2, 'ArrowRight');

const a3 = root.querySelectorAll('md-radio')[2];
expect(a3.checked).toBeTrue();
Expand All @@ -136,11 +144,9 @@ describe('md-radio', () => {
const a2 = root.querySelectorAll('md-radio')[1];
expect(a2.checked).toBeTrue();

const eventRight =
new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true});
a2.dispatchEvent(eventRight);
await simulateKeyDown(a2, 'ArrowRight');
const a3 = root.querySelectorAll('md-radio')[2];
a3.dispatchEvent(eventRight);
await simulateKeyDown(a3, 'ArrowRight');

expect(a3.checked).toBeFalse();
const a1 = root.querySelectorAll('md-radio')[0];
Expand Down Expand Up @@ -343,4 +349,111 @@ describe('md-radio', () => {
expect(focusRing.visible).toBeFalse();
});
});

describe('form submission', () => {
async function setupFormTest() {
const root = env.render(html`
<form>
<md-radio id="first" name="a" value="first"></md-radio>
<md-radio id="disabled" name="a" value="disabled" disabled></md-radio>
<md-radio id="unNamed" value="unnamed"></md-radio>
<md-radio id="ownGroup" name="b" value="ownGroup"></md-radio>
<md-radio id="last" name="a" value="last"></md-radio>
</form>`);
await env.waitForStability();
const harnesses = new Map<string, RadioHarness>();
Array.from(root.querySelectorAll('md-radio')).forEach((el: MdRadio) => {
harnesses.set(el.id, new RadioHarness(el));
});
return harnesses;
}

it('does not submit if not checked', async () => {
const harness = (await setupFormTest()).get('first')!;
const formData = await harness.submitForm();
const keys = Array.from(formData.keys());
expect(keys.length).toEqual(0);
});

it('does not submit if disabled', async () => {
const harness = (await setupFormTest()).get('disabled')!;
expect(harness.element.disabled).toBeTrue();
harness.element.checked = true;
const formData = await harness.submitForm();
const keys = Array.from(formData.keys());
expect(keys.length).toEqual(0);
});

it('does not submit if name is not provided', async () => {
const harness = (await setupFormTest()).get('unNamed')!;
expect(harness.element.name).toBe('');
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()).get('first')!;
harness.element.checked = true;
const formData = await harness.submitForm();
const {name, value} = harness.element;
const keys = Array.from(formData.keys());
expect(keys.length).toEqual(1);
expect(formData.get(name)).toEqual(value);
});

it('submits changes to group value under correct conditions', async () => {
const harnesses = await setupFormTest();
const first = harnesses.get('first')!;
const last = harnesses.get('last')!;
const ownGroup = harnesses.get('ownGroup')!;

// check first and submit
first.element.checked = true;
let formData = await first.submitForm();
expect(Array.from(formData.keys()).length).toEqual(1);
expect(formData.get(first.element.name)).toEqual(first.element.value);

// check last and submit
last.element.checked = true;
formData = await last.submitForm();
expect(Array.from(formData.keys()).length).toEqual(1);
expect(formData.get(last.element.name)).toEqual(last.element.value);

// check ownGroup and submit
ownGroup.element.checked = true;
formData = await ownGroup.submitForm();
expect(Array.from(formData.keys()).length).toEqual(2);
expect(formData.get(last.element.name)).toEqual(last.element.value);
expect(formData.get(ownGroup.element.name))
.toEqual(ownGroup.element.value);
});
});

describe('label activation', () => {
async function setupLabelTest() {
const root = env.render(html`
<label> <md-radio name="a"></md-radio></label>
<label> <md-radio name="a"></md-radio></label>
`);
await env.waitForStability();
// [[label, radio]]
return Array.from(root.querySelectorAll('label'))
.map(el => ([el, el.firstElementChild as MdRadio] as const));
}

it('toggles when label is clicked', async () => {
const [[label1, radio1], [label2, radio2], ] = await setupLabelTest();

label1.click();
await env.waitForStability();
expect(radio1.checked).toBeTrue();
expect(radio2.checked).toBeFalse();

label2.click();
await env.waitForStability();
expect(radio1.checked).toBeFalse();
expect(radio2.checked).toBeTrue();
});
});
});

0 comments on commit 91c2425

Please sign in to comment.