Skip to content

Commit

Permalink
feat(iconbutton): add form submission
Browse files Browse the repository at this point in the history
This is needed for `<md-dialog>` for close icon buttons.

I split out button's logic into a static helper method. I also added a symbol to share ElementInternals.

PiperOrigin-RevId: 554943716
  • Loading branch information
asyncLiz authored and Copybara-Service committed Aug 8, 2023
1 parent 0510496 commit c0da72b
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 152 deletions.
62 changes: 8 additions & 54 deletions button/internal/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import {html as staticHtml, literal} from 'lit/static-html.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../internal/controller/events.js';
import {internals} from '../../internal/controller/element-internals.js';
import {dispatchActivationClick, isActivationClick} from '../../internal/controller/events.js';
import {FormSubmitter, FormSubmitterType, setupFormSubmitter} from '../../internal/controller/form-submitter.js';

/**
* A button component.
*/
export abstract class Button extends LitElement {
export abstract class Button extends LitElement implements FormSubmitter {
static {
requestUpdateOnAriaChange(Button);
setupFormSubmitter(Button);
}

/** @nocollapse */
Expand Down Expand Up @@ -62,28 +65,17 @@ export abstract class Button extends LitElement {
*/
@property({type: Boolean, attribute: 'has-icon'}) hasIcon = false;

/**
* A string indicating the behavior of the button.
*
* - submit: The button submits the form. This is the default value if the
* attribute is not specified, or if it is dynamically changed to an empty or
* invalid value.
* - reset: The button resets the form.
* - button: The button does nothing.
*/
@property() type: 'button'|'submit'|'reset' = 'submit';
@property() type: FormSubmitterType = 'submit';

@query('.button') private readonly buttonElement!: HTMLElement|null;

@queryAssignedElements({slot: 'icon', flatten: true})
private readonly assignedIcons!: HTMLElement[];

private readonly internals =
/** @private */
[internals] =
(this as HTMLElement /* needed for closure */).attachInternals();

// flag to avoid processing redispatched event.
private isRedispatchingEvent = false;

constructor() {
super();
if (!isServer) {
Expand Down Expand Up @@ -115,7 +107,6 @@ export abstract class Button extends LitElement {
aria-expanded="${ariaExpanded || nothing}"
href=${this.href || nothing}
target=${this.target || nothing}
@click="${this.handleClick}"
>
${this.renderFocusRing()}
${this.renderElevation()}
Expand Down Expand Up @@ -183,43 +174,6 @@ export abstract class Button extends LitElement {
this.handleSlotChange}"></slot>`;
}

private handleClick(event: MouseEvent) {
if (this.isRedispatchingEvent) {
return;
}
// based on type, trigger form action.
const {type, internals: {form}} = this;
if (!form || type === 'button') {
return;
}

this.isRedispatchingEvent = true;
const prevented = !redispatchEvent(this, event);
this.isRedispatchingEvent = false;
if (prevented) {
return;
}

if (type === 'reset') {
form.reset();
return;
}

// form.requestSubmit(submitter) does not work with form associated custom
// elements. This patches the dispatched submit event to add the correct
// `submitter`.
// See https://github.com/WICG/webcomponents/issues/814
form.addEventListener('submit', submitEvent => {
Object.defineProperty(submitEvent, 'submitter', {
configurable: true,
enumerable: true,
get: () => this,
});
}, {capture: true, once: true});

form.requestSubmit();
}

private handleSlotChange() {
this.hasIcon = this.assignedIcons.length > 0;
}
Expand Down
97 changes: 0 additions & 97 deletions button/internal/button_test.ts

This file was deleted.

16 changes: 15 additions & 1 deletion iconbutton/internal/icon-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@ import {html as staticHtml, literal} from 'lit/static-html.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
import {internals} from '../../internal/controller/element-internals.js';
import {FormSubmitter, FormSubmitterType, setupFormSubmitter} from '../../internal/controller/form-submitter.js';
import {isRtl} from '../../internal/controller/is-rtl.js';

type LinkTarget = '_blank'|'_parent'|'_self'|'_top';

// tslint:disable-next-line:enforce-comments-on-exported-symbols
export class IconButton extends LitElement {
export class IconButton extends LitElement implements FormSubmitter {
static {
requestUpdateOnAriaChange(IconButton);
setupFormSubmitter(IconButton);
}

/** @nocollapse */
static get formAssociated() {
return true;
}

/** @nocollapse */
Expand Down Expand Up @@ -68,8 +76,14 @@ export class IconButton extends LitElement {
*/
@property({type: Boolean, reflect: true}) selected = false;

@property() type: FormSubmitterType = 'submit';

@state() private flipIcon = isRtl(this, this.flipIconInRtl);

/** @private */
[internals] =
(this as HTMLElement /* needed for closure */).attachInternals();

/**
* Link buttons cannot be disabled.
*/
Expand Down
37 changes: 37 additions & 0 deletions internal/controller/element-internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* A unique symbol used for protected access to an instance's
* `ElementInternals`.
*
* @example
* ```ts
* class MyElement extends LitElement {
* static formAssociated = true;
*
* [internals] = this.attachInternals();
* }
*
* function getForm(element: MyElement) {
* return element[internals].form;
* }
* ```
*/
export const internals = Symbol('internals');

/**
* An instance with `ElementInternals`.
*
* Use this when protected access is needed for an instance's `ElementInternals`
* from other files. A unique symbol is used to access the internals.
*/
export interface WithInternals {
/**
* An instance's `ElementInternals`.
*/
[internals]: ElementInternals;
}
108 changes: 108 additions & 0 deletions internal/controller/form-submitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {isServer, ReactiveElement} from 'lit';

import {internals, WithInternals} from './element-internals.js';

/**
* A string indicating the form submission behavior of the element.
*
* - submit: The element submits the form. This is the default value if the
* attribute is not specified, or if it is dynamically changed to an empty or
* invalid value.
* - reset: The element resets the form.
* - button: The element does nothing.
*/
export type FormSubmitterType = 'button'|'submit'|'reset';

/**
* An element that can submit or reset a `<form>`, similar to
* `<button type="submit">`.
*/
export interface FormSubmitter extends ReactiveElement, WithInternals {
/**
* A string indicating the form submission behavior of the element.
*
* - submit: The element submits the form. This is the default value if the
* attribute is not specified, or if it is dynamically changed to an empty or
* invalid value.
* - reset: The element resets the form.
* - button: The element does nothing.
*/
type: FormSubmitterType;
}

type FormSubmitterConstructor =
(new () => FormSubmitter)|(abstract new () => FormSubmitter);

/**
* Sets up an element's constructor to enable form submission. The element
* instance should be form associated and have a `type` property.
*
* A click listener is added to each element instance. If the click is not
* default prevented, it will submit the element's form, if any.
*
* @example
* ```ts
* class MyElement extends LitElement {
* static {
* setupFormSubmitter(MyElement);
* }
*
* static formAssociated = true;
*
* type: FormSubmitterType = 'submit';
*
* [internals] = this.attachInternals();
* }
* ```
*
* @param ctor The form submitter element's constructor.
*/
export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
if (isServer) {
return;
}

(ctor as unknown as typeof ReactiveElement).addInitializer(instance => {
const submitter = instance as FormSubmitter;
submitter.addEventListener('click', async event => {
const {type, [internals]: {form}} = submitter;
if (!form || type === 'button') {
return;
}

// Wait a microtask for event bubbling to complete.
await new Promise<void>(resolve => {
resolve();
});

if (event.defaultPrevented) {
return;
}

if (type === 'reset') {
form.reset();
return;
}

// form.requestSubmit(submitter) does not work with form associated custom
// elements. This patches the dispatched submit event to add the correct
// `submitter`.
// See https://github.com/WICG/webcomponents/issues/814
form.addEventListener('submit', submitEvent => {
Object.defineProperty(submitEvent, 'submitter', {
configurable: true,
enumerable: true,
get: () => submitter,
});
}, {capture: true, once: true});

form.requestSubmit();
});
});
}

0 comments on commit c0da72b

Please sign in to comment.