mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
fixing validation
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
{# Component API #}
|
||||
{% block afterContent %}
|
||||
{# Slots #}
|
||||
{% if component.slots.length %}
|
||||
{% if component.slots?.length %}
|
||||
<h2>Slots</h2>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -61,7 +61,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Properties #}
|
||||
{% if component.properties.length %}
|
||||
{% if component.properties?.length %}
|
||||
<h2>Properties</h2>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -113,7 +113,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Methods #}
|
||||
{% if component.methods.length %}
|
||||
{% if component.methods?.length %}
|
||||
<h2>Methods</h2>
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -130,7 +130,7 @@
|
||||
<td class="table-name"><code>{{ method.name }}()</code></td>
|
||||
<td class="table-description">{{ method.description | inlineMarkdown | safe }}</td>
|
||||
<td class="table-arguments">
|
||||
{% if method.parameters.length %}
|
||||
{% if method.parameters?.length %}
|
||||
<code>
|
||||
{% for param in method.parameters %}
|
||||
{{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %}
|
||||
@@ -146,7 +146,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# States #}
|
||||
{% if component.states.length %}
|
||||
{% if component.states?.length %}
|
||||
<h2>States</h2>
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -171,7 +171,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Events #}
|
||||
{% if component.events.length %}
|
||||
{% if component.events?.length %}
|
||||
<h2>Events</h2>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -195,7 +195,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Custom Properties #}
|
||||
{% if component.cssProperties.length %}
|
||||
{% if component.cssProperties?.length %}
|
||||
<h2>CSS custom properties</h2>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -225,7 +225,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# CSS Parts #}
|
||||
{% if component.cssParts.length %}
|
||||
{% if component.cssParts?.length %}
|
||||
<h2>CSS parts</h2>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -251,7 +251,7 @@
|
||||
{% endif %}
|
||||
|
||||
{# Dependencies #}
|
||||
{% if component.dependencies.length %}
|
||||
{% if component.dependencies?.length %}
|
||||
<h2>Dependencies</h2>
|
||||
<p>
|
||||
This component automatically imports the following elements. Subdependencies, if any exist, will also be included in this list.
|
||||
|
||||
@@ -244,7 +244,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
|
||||
const errorMessage = 'You must choose the last option';
|
||||
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('wa-radio').then(() => {
|
||||
customElements.whenDefined('wa-radio-group').then(() => {
|
||||
radioGroup.setCustomValidity(errorMessage);
|
||||
});
|
||||
|
||||
@@ -301,4 +301,4 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
```
|
||||
{% endraw %}
|
||||
{% endraw %}
|
||||
@@ -1,18 +1,17 @@
|
||||
import '../icon/icon.js';
|
||||
import '../spinner/spinner.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { FormControlController, validValidityState } from '../../internal/form.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import styles from './button.styles.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js';
|
||||
|
||||
/**
|
||||
* @summary Buttons represent actions that are available to the user.
|
||||
@@ -53,13 +52,17 @@ import type { WebAwesomeFormControl } from '../../internal/webawesome-element.js
|
||||
* @cssproperty --label-color-active - The color of the button's label when active.
|
||||
* @cssproperty --label-color-hover - The color of the button's label on hover.
|
||||
*/
|
||||
@customElement('wa-button')
|
||||
export default class WaButton extends WebAwesomeElement implements WebAwesomeFormControl {
|
||||
@customElement("wa-button")
|
||||
export default class WaButton extends WebAwesomeFormAssociated {
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
assumeInteractionOn: ['click']
|
||||
});
|
||||
static get validators () {
|
||||
return [
|
||||
MirrorValidator()
|
||||
]
|
||||
}
|
||||
|
||||
assumeInteractionOn = ["click"]
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@@ -79,7 +82,7 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
@property({ type: Boolean, reflect: true }) caret = false;
|
||||
|
||||
/** Disables the button. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/** Draws the button in a loading state. */
|
||||
@property({ type: Boolean, reflect: true }) loading = false;
|
||||
@@ -106,7 +109,7 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
* The value of the button, submitted as a pair with the button's name as part of the form data, but only when this
|
||||
* button is the submitter. This attribute is ignored when `href` is present.
|
||||
*/
|
||||
@property() value = '';
|
||||
@property({ reflect: true }) value = '';
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property() href = '';
|
||||
@@ -129,7 +132,7 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
* The "form owner" to associate the button with. If omitted, the closest containing form will be used instead. The
|
||||
* value of this attribute must be an id of a form in the same document or shadow root as the button.
|
||||
*/
|
||||
@property() form: string;
|
||||
@property({ reflect: true }) form: string | null = null
|
||||
|
||||
/** Used to override the form owner's `action` attribute. */
|
||||
@property({ attribute: 'formaction' }) formAction: string;
|
||||
@@ -147,30 +150,6 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
/** Used to override the form owner's `target` attribute. */
|
||||
@property({ attribute: 'formtarget' }) formTarget: '_self' | '_blank' | '_parent' | '_top' | string;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
if (this.isButton()) {
|
||||
return (this.button as HTMLButtonElement).validity;
|
||||
}
|
||||
|
||||
return validValidityState;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
if (this.isButton()) {
|
||||
return (this.button as HTMLButtonElement).validationMessage;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.isButton()) {
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('wa-blur');
|
||||
@@ -182,18 +161,41 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
if (this.type === 'submit') {
|
||||
this.formControlController.submit(this);
|
||||
}
|
||||
const form = this.getForm()
|
||||
|
||||
if (this.type === 'reset') {
|
||||
this.formControlController.reset(this);
|
||||
}
|
||||
if (!form) return
|
||||
|
||||
const lightDOMButton = this.constructLightDOMButton()
|
||||
|
||||
// form.append(lightDOMButton);
|
||||
this.parentElement?.append(lightDOMButton)
|
||||
lightDOMButton.click();
|
||||
lightDOMButton.remove();
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
private constructLightDOMButton () {
|
||||
const button = document.createElement('button');
|
||||
button.type = this.type;
|
||||
button.style.position = 'absolute';
|
||||
button.style.width = '0';
|
||||
button.style.height = '0';
|
||||
button.style.clipPath = 'inset(50%)';
|
||||
button.style.overflow = 'hidden';
|
||||
button.style.whiteSpace = 'nowrap';
|
||||
button.name = this.name;
|
||||
button.value = this.value;
|
||||
|
||||
['form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
|
||||
if (this.hasAttribute(attr)) {
|
||||
button.setAttribute(attr, this.getAttribute(attr)!);
|
||||
}
|
||||
});
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private handleInvalid() {
|
||||
this.emit("wa-invalid");
|
||||
}
|
||||
|
||||
private isButton() {
|
||||
@@ -206,10 +208,13 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
if (this.isButton()) {
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
this.updateValidity()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
setValue (..._args: Parameters<WebAwesomeFormAssociated["setValue"]>) {
|
||||
// This is just a stub. We dont ever actually want to set a value on the form. That happens when the button is clicked and added
|
||||
// via the light dom button.
|
||||
}
|
||||
|
||||
/** Simulates a click on the button. */
|
||||
@@ -227,37 +232,6 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
if (this.isButton()) {
|
||||
return (this.button as HTMLButtonElement).checkValidity();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 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() {
|
||||
if (this.isButton()) {
|
||||
return (this.button as HTMLButtonElement).reportValidity();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. Pass an empty string to restore validity. */
|
||||
setCustomValidity(message: string) {
|
||||
if (this.isButton()) {
|
||||
(this.button as HTMLButtonElement).setCustomValidity(message);
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.isLink();
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
@@ -330,7 +304,6 @@ export default class WaButton extends WebAwesomeElement implements WebAwesomeFor
|
||||
/* eslint-enable lit/binding-positions */
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'wa-button': WaButton;
|
||||
|
||||
@@ -783,7 +783,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociated {
|
||||
|
||||
if (!this.disabled) {
|
||||
// By standards we have to emit a `wa-invalid` event here synchronously.
|
||||
// this.formControlController.emitInvalidEvent();
|
||||
this.emit('wa-invalid');
|
||||
}
|
||||
|
||||
@@ -793,18 +792,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociated {
|
||||
return super.reportValidity();
|
||||
}
|
||||
|
||||
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
|
||||
const [value, reason] = args;
|
||||
const oldValue = this.value;
|
||||
super.formStateRestoreCallback(value, reason);
|
||||
|
||||
this.handleValueChange(oldValue, this.value);
|
||||
}
|
||||
|
||||
formResetCallback() {
|
||||
const oldValue = this.value;
|
||||
this.value = this.defaultValue;
|
||||
this.handleValueChange(oldValue, this.value);
|
||||
|
||||
super.formResetCallback();
|
||||
}
|
||||
|
||||
@@ -374,9 +374,6 @@ describe('<wa-input>', () => {
|
||||
</form>
|
||||
`);
|
||||
|
||||
// Yes, this is a "breakage" from previous overloads, but this is how the browser works :shrug:
|
||||
// https://codepen.io/paramagicdev/pen/rNbpqje
|
||||
|
||||
expect(form.reportValidity()).to.be.false;
|
||||
});
|
||||
|
||||
@@ -582,7 +579,7 @@ describe('<wa-input>', () => {
|
||||
it('Should be invalid if the pattern is invalid', async () => {
|
||||
const el = await fixture<WaInput>(html` <wa-input required pattern="1234"></wa-input> `);
|
||||
|
||||
el.formControl.focus();
|
||||
el.input.focus();
|
||||
await el.updateComplete;
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
|
||||
|
||||
@@ -72,8 +72,6 @@ export default class WaInput extends WebAwesomeFormAssociated {
|
||||
@state() private hasFocus = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
private __numberInput = Object.assign(document.createElement('input'), { type: 'number' });
|
||||
private __dateInput = Object.assign(document.createElement('input'), { type: 'date' });
|
||||
|
||||
/**
|
||||
* The type of input. Works the same as a native `<input>` element, but only a subset of types are supported. Defaults
|
||||
@@ -196,6 +194,9 @@ export default class WaInput extends WebAwesomeFormAssociated {
|
||||
})
|
||||
spellcheck = true;
|
||||
|
||||
private __numberInput = Object.assign(document.createElement('input'), { type: 'number' });
|
||||
private __dateInput = Object.assign(document.createElement('input'), { type: 'date' });
|
||||
|
||||
/**
|
||||
* Tells the browser what type of data will be entered by the user, allowing it to display the appropriate virtual
|
||||
* keyboard on supportive devices.
|
||||
@@ -359,16 +360,7 @@ export default class WaInput extends WebAwesomeFormAssociated {
|
||||
}
|
||||
}
|
||||
|
||||
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
|
||||
const [value, reason] = args;
|
||||
super.formStateRestoreCallback(value, reason);
|
||||
|
||||
/** @ts-expect-error Type widening issue due to what a formStateRestoreCallback can accept. */
|
||||
this.input.value = value;
|
||||
}
|
||||
|
||||
formResetCallback() {
|
||||
this.input.value = this.defaultValue;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
super.formResetCallback();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { watch } from '../../internal/watch.js';
|
||||
import { WebAwesomeFormAssociated } from '../../internal/webawesome-element.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import styles from './radio-button.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { CSSResultGroup, PropertyValues } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Radios buttons allow the user to select a single option from a group using a button-like control.
|
||||
@@ -46,10 +46,9 @@ export default class WaRadioButton extends WebAwesomeFormAssociated {
|
||||
* it easier to style in button groups.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
@property({ type: Boolean, attribute: 'default-checked' }) defaultChecked = false;
|
||||
|
||||
/** The radio's value. When selected, the radio group will receive this value. */
|
||||
@property({ attribute: false }) value: string;
|
||||
@property({ reflect: true }) value: string;
|
||||
|
||||
/** Disables the radio button. */
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@@ -96,6 +95,12 @@ export default class WaRadioButton extends WebAwesomeFormAssociated {
|
||||
this.emit('wa-focus');
|
||||
}
|
||||
|
||||
// protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
// if (this.disabled && changedProperties.has("checked")) {
|
||||
// this.checked = Boolean(changedProperties.get("checked"))
|
||||
// }
|
||||
// }
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
|
||||
@@ -224,14 +224,14 @@ export default class WaRadioGroup extends WebAwesomeFormAssociated {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const radios = this.getAllRadios().filter(radio => !radio.disabled);
|
||||
|
||||
if (radios.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const oldValue = this.value;
|
||||
|
||||
const checkedRadio = radios.find(radio => radio.checked) ?? radios[0];
|
||||
|
||||
@@ -231,17 +231,7 @@ export default class WaRange extends WebAwesomeFormAssociated {
|
||||
}
|
||||
}
|
||||
|
||||
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
|
||||
const [value, reason] = args;
|
||||
super.formStateRestoreCallback(value, reason);
|
||||
|
||||
/** @ts-expect-error Type widening issue due to what a formStateRestoreCallback can accept. */
|
||||
this.input.value = value;
|
||||
}
|
||||
|
||||
formResetCallback() {
|
||||
// @ts-expect-error We pass a Number, but it wants a string :shrug:
|
||||
this.input.value = this.defaultValue;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
super.formResetCallback();
|
||||
|
||||
@@ -264,16 +264,7 @@ export default class WaTextarea extends WebAwesomeFormAssociated {
|
||||
}
|
||||
}
|
||||
|
||||
formStateRestoreCallback(...args: Parameters<WebAwesomeFormAssociated['formStateRestoreCallback']>) {
|
||||
const [value, reason] = args;
|
||||
super.formStateRestoreCallback(value, reason);
|
||||
|
||||
/** @ts-expect-error Type widening issue due to what a formStateRestoreCallback can accept. */
|
||||
this.input.value = value;
|
||||
}
|
||||
|
||||
formResetCallback() {
|
||||
this.input.value = this.defaultValue;
|
||||
this.value = this.defaultValue;
|
||||
|
||||
super.formResetCallback();
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Validator } from '../webawesome-element.js';
|
||||
export const MirrorValidator = (): Validator => {
|
||||
return {
|
||||
checkValidity(element) {
|
||||
const formControl = element.formControl;
|
||||
const formControl = element.input;
|
||||
|
||||
const validity: ReturnType<Validator['checkValidity']> = {
|
||||
message: '',
|
||||
|
||||
@@ -182,7 +182,7 @@ export class WebAwesomeFormAssociated
|
||||
assumeInteractionOn: string[] = ['wa-input'];
|
||||
|
||||
// Additional
|
||||
formControl?: (HTMLElement & { value: unknown }) | HTMLInputElement | HTMLTextAreaElement;
|
||||
input?: (HTMLElement & { value: unknown }) | HTMLInputElement | HTMLTextAreaElement;
|
||||
|
||||
validators: Validator[] = [];
|
||||
|
||||
@@ -244,11 +244,23 @@ export class WebAwesomeFormAssociated
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('value')) {
|
||||
if (
|
||||
changedProperties.has('value') ||
|
||||
changedProperties.has("disabled")
|
||||
) {
|
||||
|
||||
// this is a hack because of how "disabled" attribute can be set by static HTML, but then changed via property, but we don't
|
||||
// want to use reflection because of a bug in "formDisabledCallback"
|
||||
if (!this.disabled) { this.removeAttribute("disabled") }
|
||||
|
||||
if (this.hasInteracted && this.value !== this.defaultValue) {
|
||||
this.valueHasChanged = true;
|
||||
}
|
||||
|
||||
if (this.input) {
|
||||
this.input.value = this.value
|
||||
}
|
||||
|
||||
const value = this.value;
|
||||
|
||||
// Accounts for the snowflake case on `<wa-select>`
|
||||
@@ -316,7 +328,7 @@ export class WebAwesomeFormAssociated
|
||||
* Override this to change where constraint validation popups are anchored.
|
||||
*/
|
||||
get validationTarget(): undefined | HTMLElement {
|
||||
return (this.formControl || undefined) as undefined | HTMLElement;
|
||||
return (this.input || undefined) as undefined | HTMLElement;
|
||||
}
|
||||
|
||||
setValidity(...args: Parameters<typeof this.internals.setValidity>) {
|
||||
@@ -434,7 +446,7 @@ export class WebAwesomeFormAssociated
|
||||
customError: Boolean(this.__manualCustomError)
|
||||
};
|
||||
|
||||
const formControl = this.validationTarget || this.formControl || undefined;
|
||||
const formControl = this.validationTarget || this.input || undefined;
|
||||
|
||||
let finalMessage = '';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user