mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
first pass at form association
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.1.2",
|
||||
"composed-offset-position": "^0.0.4",
|
||||
"form-associated-helpers": "^0.0.5",
|
||||
"lit": "^3.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
@@ -10835,6 +10836,11 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-associated-helpers": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-associated-helpers/-/form-associated-helpers-0.0.5.tgz",
|
||||
"integrity": "sha512-OTcCYvcDy9EJYh0p19z7ttwikBWrMURaEX5HFKvxIlmN/mIOj/YiqVTzifvgeaKPqlsoleyfzzc5qwQx1dm7fg=="
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.1.2",
|
||||
"composed-offset-position": "^0.0.4",
|
||||
"form-associated-helpers": "^0.0.5",
|
||||
"lit": "^3.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
@@ -100,8 +101,8 @@
|
||||
"cspell": "^6.18.1",
|
||||
"custom-element-jet-brains-integration": "^1.4.0",
|
||||
"custom-element-vs-code-integration": "^1.2.1",
|
||||
"dedent": "^1.5.1",
|
||||
"custom-element-vuejs-integration": "^1.0.0",
|
||||
"dedent": "^1.5.1",
|
||||
"del": "^7.1.0",
|
||||
"download": "^8.0.0",
|
||||
"esbuild": "^0.19.4",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { defaultValue } from '../../internal/default-value.js';
|
||||
import { FormControlController } from '../../internal/form.js';
|
||||
// import { FormControlController } from '../../internal/form.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import { html, LitElement } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
@@ -12,6 +12,10 @@ import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js';
|
||||
|
||||
import { LitTextareaMixin } from 'form-associated-helpers/exports/mixins/lit-textarea-mixin.js';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { MirrorValidator } from 'form-associated-helpers/exports/validators/mirror-validator.js';
|
||||
|
||||
/**
|
||||
* @summary Textareas collect data from the user and allow multiple lines of text.
|
||||
* @documentation https://shoelace.style/components/textarea
|
||||
@@ -41,17 +45,30 @@ import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js
|
||||
* @cssproperty --border-width - The width of the textarea's borders.
|
||||
* @cssproperty --box-shadow - The shadow effects around the edges of the textarea.
|
||||
*/
|
||||
export default class WaTextarea extends WebAwesomeElement implements WebAwesomeFormControl {
|
||||
export default class WaTextarea extends LitTextareaMixin(WebAwesomeElement) implements WebAwesomeFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
assumeInteractionOn: ['wa-blur', 'wa-input']
|
||||
});
|
||||
static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};
|
||||
// static get validators () { return [MirrorValidator] }
|
||||
|
||||
static get properties () {
|
||||
return {
|
||||
...super.properties,
|
||||
...LitTextareaMixin.formProperties,
|
||||
}
|
||||
}
|
||||
|
||||
// private readonly formControlController = new FormControlController(this, {
|
||||
// assumeInteractionOn: ['wa-blur', 'wa-input']
|
||||
// });
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@query('.textarea__control') input: HTMLTextAreaElement;
|
||||
|
||||
// Required to be public.
|
||||
@state() formControl: HTMLTextAreaElement | null;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
@@ -82,7 +99,7 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
/** Controls how the textarea can be resized. */
|
||||
@property() resize: 'none' | 'vertical' | 'auto' = 'vertical';
|
||||
|
||||
/** Disables the textarea. */
|
||||
/** Disables the textarea. We don't want to reflect here. It breaks callbacks. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Makes the textarea readonly. */
|
||||
@@ -93,7 +110,7 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
|
||||
* the same document or shadow root for this to work.
|
||||
*/
|
||||
@property({ reflect: true }) form = '';
|
||||
// @property({ reflect: true }) form = '';
|
||||
|
||||
/** Makes the textarea a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
@@ -114,7 +131,9 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
* Specifies what permission the browser has to provide assistance in filling out form field values. Refer to
|
||||
* [this page on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for available values.
|
||||
*/
|
||||
@property() autocomplete: string;
|
||||
// @ts-expect-error
|
||||
// The base LitTextareaMixin expects this as a "Autofill" type.
|
||||
@property() autocomplete: string = undefined;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean }) autofocus: boolean;
|
||||
@@ -142,14 +161,14 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
// @ts-expect-error
|
||||
this._boundEmitInvalidEvent = this.emitInvalidEvent.bind(this)
|
||||
|
||||
// @ts-expect-error
|
||||
this.addEventListener("invalid", this._boundEmitInvalidEvent)
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -162,9 +181,9 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
// firstUpdated() {
|
||||
// this.formControlController.updateValidity();
|
||||
// }
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
@@ -192,10 +211,36 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
this.emit('wa-input');
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
/**
|
||||
* Dispatches a non-bubbling, cancelable custom event of type `wa-invalid`.
|
||||
* If the `wa-invalid` event will be cancelled then the original `invalid`
|
||||
* event (which may have been passed as argument) will also be cancelled.
|
||||
* If no original `invalid` event has been passed then the `wa-invalid`
|
||||
* event will be cancelled before being dispatched.
|
||||
*/
|
||||
emitInvalidEvent(originalInvalidEvent?: Event) {
|
||||
const slInvalidEvent = new CustomEvent<Record<PropertyKey, never>>('wa-invalid', {
|
||||
bubbles: false,
|
||||
composed: false,
|
||||
cancelable: true,
|
||||
detail: {}
|
||||
});
|
||||
|
||||
if (!originalInvalidEvent) {
|
||||
slInvalidEvent.preventDefault();
|
||||
}
|
||||
|
||||
if (!this.dispatchEvent(slInvalidEvent)) {
|
||||
originalInvalidEvent?.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// `form-associated-helpers` also has a `handleInvalid` event.
|
||||
// handleInvalid = (event: Event) => {
|
||||
// super.handleInvalid(event)
|
||||
// this.emitInvalidEvent(event)
|
||||
// }
|
||||
|
||||
private setTextareaHeight() {
|
||||
if (this.resize === 'auto') {
|
||||
@@ -209,7 +254,9 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
// this.formControlController.setValidity(this.disabled);
|
||||
// this.formControl?.setCustomValidity('')
|
||||
// this.setValidity()
|
||||
}
|
||||
|
||||
@watch('rows', { waitUntilFirstUpdate: true })
|
||||
@@ -220,7 +267,7 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
async handleValueChange() {
|
||||
await this.updateComplete;
|
||||
this.formControlController.updateValidity();
|
||||
// this.formControlController.updateValidity();
|
||||
this.setTextareaHeight();
|
||||
}
|
||||
|
||||
@@ -280,25 +327,8 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Gets the associated form, if one exists. */
|
||||
getForm(): HTMLFormElement | null {
|
||||
return this.formControlController.getForm();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. Pass an empty string to restore validity. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.formControlController.updateValidity();
|
||||
private formControlChanged (el?: HTMLTextAreaElement) {
|
||||
this.formControl = el || null
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -372,6 +402,7 @@ export default class WaTextarea extends WebAwesomeElement implements WebAwesomeF
|
||||
@invalid=${this.handleInvalid}
|
||||
@focus=${this.handleFocus}
|
||||
@blur=${this.handleBlur}
|
||||
${ref(this.formControlChanged)}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import '../../../dist/webawesome.js';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form.js';
|
||||
@@ -122,16 +122,20 @@ describe('<wa-textarea>', () => {
|
||||
it('should be invalid when required and after removing disabled ', async () => {
|
||||
const el = await fixture<WaTextarea>(html` <wa-textarea disabled required></wa-textarea> `);
|
||||
|
||||
el.disabled = false;
|
||||
// el.disabled = false;
|
||||
el.removeAttribute("disabled")
|
||||
await el.updateComplete;
|
||||
// await aTimeout(0)
|
||||
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should be invalid when required and disabled is removed', async () => {
|
||||
const el = await fixture<WaTextarea>(html` <wa-textarea disabled required></wa-textarea> `);
|
||||
el.disabled = false;
|
||||
// el.disabled = false;
|
||||
el.removeAttribute("disabled")
|
||||
await el.updateComplete;
|
||||
// await aTimeout(0)
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
@@ -139,8 +143,8 @@ describe('<wa-textarea>', () => {
|
||||
const el = await fixture<WaTextarea>(html` <wa-textarea required value="a"></wa-textarea> `);
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
// expect(el.hasAttribute('data-required')).to.be.true;
|
||||
// expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
@@ -160,8 +164,8 @@ describe('<wa-textarea>', () => {
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
const el = await fixture<WaTextarea>(html` <wa-textarea required></wa-textarea> `);
|
||||
|
||||
expect(el.hasAttribute('data-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
// expect(el.hasAttribute('data-required')).to.be.true;
|
||||
// expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
@@ -182,8 +186,8 @@ describe('<wa-textarea>', () => {
|
||||
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-textarea required></wa-textarea></form> `);
|
||||
const textarea = el.querySelector<WaTextarea>('wa-textarea')!;
|
||||
|
||||
expect(textarea.hasAttribute('data-required')).to.be.true;
|
||||
expect(textarea.hasAttribute('data-optional')).to.be.false;
|
||||
// expect(textarea.hasAttribute('data-required')).to.be.true;
|
||||
// expect(textarea.hasAttribute('data-optional')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-invalid')).to.be.true;
|
||||
expect(textarea.hasAttribute('data-valid')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-user-invalid')).to.be.false;
|
||||
@@ -205,7 +209,8 @@ describe('<wa-textarea>', () => {
|
||||
});
|
||||
|
||||
it('should be invalid when setCustomValidity() is called with a non-empty value', async () => {
|
||||
const textarea = await fixture<HTMLFormElement>(html` <wa-textarea></wa-textarea> `);
|
||||
const form = await fixture<HTMLFormElement>(html` <form><wa-textarea></wa-textarea></form> `);
|
||||
const textarea = form.querySelector("wa-textarea") as WaTextarea
|
||||
|
||||
textarea.setCustomValidity('Invalid selection');
|
||||
await textarea.updateComplete;
|
||||
|
||||
@@ -44,7 +44,7 @@ export function runFormControlBaseTests<T extends WebAwesomeFormControl = WebAwe
|
||||
// - `.checkValidity()`
|
||||
// - `.reportValidity()`
|
||||
// - `.setCustomValidity(msg)`
|
||||
// - `.getForm()`
|
||||
// - `.form`
|
||||
//
|
||||
function runAllValidityTests(
|
||||
tagName: string, //
|
||||
@@ -130,21 +130,21 @@ function runAllValidityTests(
|
||||
const formId = 'test-form';
|
||||
const form = await fixture(`<form id='${formId}'></form>`);
|
||||
const control = await createControl();
|
||||
expect(control.getForm()).to.equal(null);
|
||||
control.form = 'test-form';
|
||||
expect(control.form).to.equal(null);
|
||||
control.setAttribute("form", 'test-form');
|
||||
await control.updateComplete;
|
||||
expect(control.getForm()).to.equal(form);
|
||||
expect(control.form).to.equal(form);
|
||||
});
|
||||
|
||||
it('Should find the correct form when given a form attribute', async () => {
|
||||
const formId = 'test-form';
|
||||
const form = await fixture(`<form id='${formId}'></form>`);
|
||||
const control = await createControl();
|
||||
expect(control.getForm()).to.equal(null);
|
||||
expect(control.form).to.equal(null);
|
||||
control.setAttribute('form', 'test-form');
|
||||
|
||||
await control.updateComplete;
|
||||
expect(control.getForm()).to.equal(form);
|
||||
expect(control.form).to.equal(form);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,6 +244,8 @@ function runSpecialTests_standard(createControl: CreateControlFn) {
|
||||
it('should make sure that `.validity.valid` is `false` in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
|
||||
await control.updateComplete
|
||||
expect(control.validity.valid).to.equal(false);
|
||||
});
|
||||
|
||||
@@ -273,8 +275,10 @@ function runSpecialTests_standard(createControl: CreateControlFn) {
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = false;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity());
|
||||
const emittedEvents = checkEventEmissions(control, 'invalid', () => control.reportValidity());
|
||||
const waEmittedEvents = checkEventEmissions(control, 'wa-invalid', () => control.reportValidity());
|
||||
expect(emittedEvents.length).to.equal(1);
|
||||
expect(waEmittedEvents.length).to.equal(1)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ export interface WebAwesomeFormControl extends WebAwesomeElement {
|
||||
|
||||
// Form validation methods
|
||||
checkValidity: () => boolean;
|
||||
getForm: () => HTMLFormElement | null;
|
||||
getForm?: () => HTMLFormElement | null;
|
||||
reportValidity: () => boolean;
|
||||
setCustomValidity: (message: string) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user