Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 5 additions & 30 deletions button/internal/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ import {property, query, queryAssignedElements} from 'lit/decorators.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {mixinDelegatesAria} from '../../internal/aria/delegate.js';
import {
FormSubmitter,
setupFormSubmitter,
type FormSubmitterType,
} from '../../internal/controller/form-submitter.js';
import {
dispatchActivationClick,
isActivationClick,
Expand All @@ -25,18 +20,17 @@ import {
internals,
mixinElementInternals,
} from '../../labs/behaviors/element-internals.js';
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';

// Separate variable needed for closure.
const buttonBaseClass = mixinDelegatesAria(mixinElementInternals(LitElement));
const buttonBaseClass = mixinDelegatesAria(
mixinFormSubmitter(mixinElementInternals(LitElement)),
);

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

export abstract class Button extends buttonBaseClass {
/** @nocollapse */
static readonly formAssociated = true;

Expand Down Expand Up @@ -95,25 +89,6 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
@property({type: Boolean, attribute: 'has-icon', reflect: true}) hasIcon =
false;

/**
* The default behavior of the button. May be "button", "reset", or "submit"
* (default).
*/
@property() type: FormSubmitterType = 'submit';

/**
* The value added to a form with the button's name when the button submits a
* form.
*/
@property({reflect: true}) value = '';

get name() {
return this.getAttribute('name') ?? '';
}
set name(name: string) {
this.setAttribute('name', name);
}

/**
* The associated form element with which this element's value will submit.
*/
Expand Down
33 changes: 3 additions & 30 deletions iconbutton/internal/icon-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ import {literal, html as staticHtml} from 'lit/static-html.js';

import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {mixinDelegatesAria} from '../../internal/aria/delegate.js';
import {
FormSubmitter,
setupFormSubmitter,
type FormSubmitterType,
} from '../../internal/controller/form-submitter.js';
import {isRtl} from '../../internal/controller/is-rtl.js';
import {
afterDispatch,
Expand All @@ -28,12 +23,13 @@ import {
internals,
mixinElementInternals,
} from '../../labs/behaviors/element-internals.js';
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';

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

// Separate variable needed for closure.
const iconButtonBaseClass = mixinDelegatesAria(
mixinElementInternals(LitElement),
mixinFormSubmitter(mixinElementInternals(LitElement)),
);

/**
Expand All @@ -43,11 +39,7 @@ const iconButtonBaseClass = mixinDelegatesAria(
* --composed
* @fires change {Event} Dispatched when a toggle button toggles --bubbles
*/
export class IconButton extends iconButtonBaseClass implements FormSubmitter {
static {
setupFormSubmitter(IconButton);
}

export class IconButton extends iconButtonBaseClass {
/** @nocollapse */
static readonly formAssociated = true;

Expand Down Expand Up @@ -113,25 +105,6 @@ export class IconButton extends iconButtonBaseClass implements FormSubmitter {
*/
@property({type: Boolean, reflect: true}) selected = false;

/**
* The default behavior of the button. May be "button", "reset", or "submit"
* (default).
*/
@property() type: FormSubmitterType = 'submit';

/**
* The value added to a form with the button's name when the button submits a
* form.
*/
@property({reflect: true}) value = '';

get name() {
return this.getAttribute('name') ?? '';
}
set name(name: string) {
this.setAttribute('name', name);
}

/**
* The associated form element with which this element's value will submit.
*/
Expand Down
130 changes: 0 additions & 130 deletions internal/controller/form-submitter.ts

This file was deleted.

140 changes: 140 additions & 0 deletions labs/behaviors/form-submitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {isServer, LitElement} from 'lit';
import {property} from 'lit/decorators.js';

import {
afterDispatch,
setupDispatchHooks,
} from '../../internal/events/dispatch-hooks.js';
import {internals, WithElementInternals} from './element-internals.js';
import {MixinBase, MixinReturn} from './mixin.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 {
/**
* 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;

/**
* The HTML name to use in form submission. When combined with a `value`, the
* submitting button's name/value will be added to the form.
*
* Names must reflect to a `name` attribute for form integration.
*/
name: string;

/**
* The value of the button. When combined with a `name`, the submitting
* button's name/value will be added to the form.
*/
value: string;
}

/**
* Mixes in form submitter behavior for a class.
*
* 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
* const base = mixinFormSubmitter(mixinElementInternals(LitElement));
* class MyButton extends base {
* static formAssociated = true;
* }
* ```
*
* @param base The class to mix functionality into.
* @return The provided class with `FormSubmitter` mixed in.
*/
export function mixinFormSubmitter<
T extends MixinBase<LitElement & WithElementInternals>,
>(base: T): MixinReturn<T, FormSubmitter> {
abstract class FormSubmitterElement extends base implements FormSubmitter {
@property() type: FormSubmitterType = 'submit';
@property({reflect: true}) value = '';

// Name attribute must reflect synchronously for form integration.
get name() {
return this.getAttribute('name') ?? '';
}
set name(name: string) {
this.setAttribute('name', name);
}

// Mixins must have a constructor with `...args: any[]`
// tslint:disable-next-line:no-any
constructor(...args: any[]) {
super(...args);
if (isServer) return;
setupDispatchHooks(this, 'click');
this.addEventListener('click', async (event: Event) => {
const isReset = this.type === 'reset';
const isSubmit = this.type === 'submit';
const elementInternals = this[internals];
const {form} = elementInternals;
if (!form || !(isSubmit || isReset)) {
return;
}

afterDispatch(event, () => {
if (event.defaultPrevented) {
return;
}

if (isReset) {
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: Event) => {
Object.defineProperty(submitEvent, 'submitter', {
configurable: true,
enumerable: true,
get: () => this,
});
},
{capture: true, once: true},
);

elementInternals.setFormValue(this.value);
form.requestSubmit();
});
});
}
}

return FormSubmitterElement;
}
Loading
Loading