Skip to content
Open
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
20 changes: 4 additions & 16 deletions button/internal/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,19 @@ import {
dispatchActivationClick,
isActivationClick,
} from '../../internal/events/form-label-activation.js';
import {
internals,
mixinElementInternals,
} from '../../labs/behaviors/element-internals.js';
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
import {mixinFormAssociated} from '../../labs/behaviors/form-associated.js';
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';

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

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

/** @nocollapse */
static override shadowRootOptions: ShadowRootInit = {
mode: 'open',
Expand All @@ -43,7 +38,7 @@ export abstract class Button extends buttonBaseClass {
/**
* Whether or not the button is disabled.
*/
@property({type: Boolean, reflect: true}) disabled = false;
declare disabled: boolean; // for jsdoc until lit-analyzer is updated

/**
* Whether or not the button is "soft-disabled" (disabled but still
Expand Down Expand Up @@ -89,13 +84,6 @@ export abstract class Button extends buttonBaseClass {
@property({type: Boolean, attribute: 'has-icon', reflect: true}) hasIcon =
false;

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

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

@queryAssignedElements({slot: 'icon', flatten: true})
Expand Down
27 changes: 4 additions & 23 deletions iconbutton/internal/icon-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,15 @@ import {
afterDispatch,
setupDispatchHooks,
} from '../../internal/events/dispatch-hooks.js';
import {
internals,
mixinElementInternals,
} from '../../labs/behaviors/element-internals.js';
import {mixinElementInternals} from '../../labs/behaviors/element-internals.js';
import {mixinFormAssociated} from '../../labs/behaviors/form-associated.js';
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';

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

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

/**
Expand All @@ -40,9 +38,6 @@ const iconButtonBaseClass = mixinDelegatesAria(
* @fires change {Event} Dispatched when a toggle button toggles --bubbles
*/
export class IconButton extends iconButtonBaseClass {
/** @nocollapse */
static readonly formAssociated = true;

/** @nocollapse */
static override shadowRootOptions: ShadowRootInit = {
mode: 'open',
Expand All @@ -52,7 +47,7 @@ export class IconButton extends iconButtonBaseClass {
/**
* Disables the icon button and makes it non-interactive.
*/
@property({type: Boolean, reflect: true}) disabled = false;
declare disabled: boolean; // for jsdoc until lit-analyzer is updated

/**
* "Soft-disables" the icon button (disabled but still focusable).
Expand Down Expand Up @@ -105,20 +100,6 @@ export class IconButton extends iconButtonBaseClass {
*/
@property({type: Boolean, reflect: true}) selected = false;

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

/**
* The labels this element is associated with.
*/
get labels() {
return this[internals].labels;
}

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

constructor() {
Expand Down
33 changes: 14 additions & 19 deletions labs/behaviors/form-associated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ export interface FormAssociated {
disabled: boolean;

/**
* Gets the current form value of a component.
* Gets the current form value of a component. Defaults to the component's
* `value` attribute.
*
* @return The current form value.
*/
[getFormValue](): FormValue | null;

/**
* Gets the current form state of a component. Defaults to the component's
* `[formValue]`.
* `[getFormValue]()` result.
*
* Use this when the state of an element is different from its value, such as
* checkboxes (internal boolean state and a user string value).
Expand All @@ -72,25 +73,26 @@ export interface FormAssociated {
formDisabledCallback(disabled: boolean): void;

/**
* A callback for when the form requests to reset its value. Typically, the
* default value that is reset is represented in the attribute of an element.
* An optional callback for when the form requests to reset its value.
* Typically, the default value that is reset is represented in the attribute
* of an element.
*
* This means the attribute used for the value should not update as the value
* changes. For example, a checkbox should not change its default `checked`
* attribute when selected. Ensure form values do not reflect.
*/
formResetCallback(): void;
formResetCallback?(): void;

/**
* A callback for when the form restores the state of a component. For
* example, when a page is reloaded or forms are autofilled.
* An optional callback for when the form restores the state of a component.
* For example, when a page is reloaded or forms are autofilled.
*
* @param state The state to restore, or null to reset the form control's
* value.
* @param reason The reason state was restored, either `'restore'` or
* `'autocomplete'`.
*/
formStateRestoreCallback(
formStateRestoreCallback?(
state: FormRestoreState | null,
reason: FormRestoreReason,
): void;
Expand Down Expand Up @@ -238,7 +240,9 @@ export function mixinFormAssociated<
return this.hasAttribute('disabled');
}
set disabled(disabled: boolean) {
this.toggleAttribute('disabled', disabled);
// Coerce `disabled` in `Boolean()` to ensure that setting to `null` or
// `undefined` sets the attribute to `false`.
this.toggleAttribute('disabled', Boolean(disabled));
// We don't need to call `requestUpdate()` since it's called synchronously
// in `attributeChangedCallback()`.
}
Expand Down Expand Up @@ -282,9 +286,7 @@ export function mixinFormAssociated<
}

[getFormValue](): FormValue | null {
// Closure does not allow abstract symbol members, so a default
// implementation is needed.
throw new Error('Implement [getFormValue]');
return this.getAttribute('value');
}

[getFormState](): FormValue | null {
Expand All @@ -294,13 +296,6 @@ export function mixinFormAssociated<
formDisabledCallback(disabled: boolean) {
this.disabled = disabled;
}

abstract formResetCallback(): void;

abstract formStateRestoreCallback(
state: FormRestoreState | null,
reason: FormRestoreReason,
): void;
}

return FormAssociatedElement;
Expand Down
35 changes: 14 additions & 21 deletions labs/behaviors/form-associated_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,27 +217,20 @@ describe('mixinFormAssociated()', () => {
});

describe('[getFormValue]()', () => {
it('should throw an error if not implemented', () => {
expect(() => {
@customElement('test-bad-form-associated')
class TestBadFormAssociated extends mixinFormAssociated(
mixinElementInternals(LitElement),
) {
override requestUpdate(
...args: Parameters<LitElement['requestUpdate']>
) {
// Suppress errors that will occur async when the element is
// initialized. This is harder to test in jasmine, so we explicitly
// call the getFormValue function to test the error.
try {
super.requestUpdate(...args);
} catch {}
}
}

const element = new TestBadFormAssociated();
element[getFormValue]();
}).toThrowError(/getFormValue/);
it('should return the value attribute by default', () => {
@customElement('test-default-value-form-associated')
class TestDefaultValueFormAssociated extends mixinFormAssociated(
mixinElementInternals(LitElement),
) {}

const element = new TestDefaultValueFormAssociated();
expect(element[getFormValue]())
.withContext('[getFormValue]() return with no value attribute')
.toBeNull();
element.setAttribute('value', 'value');
expect(element[getFormValue]())
.withContext('[getFormValue]() return with value attribute')
.toBe('value');
});

it('should not add form data without a name', () => {
Expand Down
Loading