mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 12:09:26 +00:00
Merge branch 'next' into mpharoah/typescript-events
This commit is contained in:
@@ -295,13 +295,15 @@ This example demonstrates custom validation styles using `data-user-invalid` and
|
||||
required
|
||||
></sl-input>
|
||||
|
||||
<sl-select label="Favorite Animal" help-text="Select the best option." clearable required>
|
||||
<sl-select name="animal" label="Favorite Animal" help-text="Select the best option." clearable required>
|
||||
<sl-option value="birds">Birds</sl-option>
|
||||
<sl-option value="cats">Cats</sl-option>
|
||||
<sl-option value="dogs">Dogs</sl-option>
|
||||
<sl-option value="other">Other</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<sl-checkbox value="accept" required>Accept terms and conditions</sl-checkbox>
|
||||
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
</form>
|
||||
@@ -316,40 +318,57 @@ This example demonstrates custom validation styles using `data-user-invalid` and
|
||||
|
||||
<style>
|
||||
.validity-styles sl-input,
|
||||
.validity-styles sl-select {
|
||||
.validity-styles sl-select,
|
||||
.validity-styles sl-checkbox {
|
||||
display: block;
|
||||
margin-bottom: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
/* user invalid styles */
|
||||
.validity-styles sl-input[data-user-invalid]::part(base),
|
||||
.validity-styles sl-select[data-user-invalid]::part(combobox) {
|
||||
.validity-styles sl-select[data-user-invalid]::part(combobox),
|
||||
.validity-styles sl-checkbox[data-user-invalid]::part(control) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
.validity-styles [data-user-invalid]::part(form-control-label),
|
||||
.validity-styles [data-user-invalid]::part(form-control-help-text) {
|
||||
.validity-styles [data-user-invalid]::part(form-control-help-text),
|
||||
.validity-styles sl-checkbox[data-user-invalid]::part(label) {
|
||||
color: var(--sl-color-danger-700);
|
||||
}
|
||||
|
||||
.validity-styles sl-checkbox[data-user-invalid]::part(control) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.validity-styles sl-input:focus-within[data-user-invalid]::part(base),
|
||||
.validity-styles sl-select:focus-within[data-user-invalid]::part(combobox) {
|
||||
.validity-styles sl-select:focus-within[data-user-invalid]::part(combobox),
|
||||
.validity-styles sl-checkbox:focus-within[data-user-invalid]::part(control) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-300);
|
||||
}
|
||||
|
||||
/* User valid styles */
|
||||
.validity-styles sl-input[data-user-valid]::part(base),
|
||||
.validity-styles sl-select[data-user-valid]::part(combobox) {
|
||||
.validity-styles sl-select[data-user-valid]::part(combobox),
|
||||
.validity-styles sl-checkbox[data-user-valid]::part(control) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
.validity-styles [data-user-valid]::part(form-control-label),
|
||||
.validity-styles [data-user-valid]::part(form-control-help-text) {
|
||||
.validity-styles [data-user-valid]::part(form-control-help-text),
|
||||
.validity-styles sl-checkbox[data-user-valid]::part(label) {
|
||||
color: var(--sl-color-success-700);
|
||||
}
|
||||
|
||||
.validity-styles sl-checkbox[data-user-valid]::part(control) {
|
||||
background-color: var(--sl-color-success-600);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.validity-styles sl-input:focus-within[data-user-valid]::part(base),
|
||||
.validity-styles sl-select:focus-within[data-user-valid]::part(combobox) {
|
||||
.validity-styles sl-select:focus-within[data-user-valid]::part(combobox),
|
||||
.validity-styles sl-checkbox:focus-within[data-user-valid]::part(control) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
|
||||
- Added the `sl-focus` and `sl-blur` events to `<sl-color-picker>`
|
||||
- Added the `focus()` and `blur()` methods to `<sl-color-picker>`
|
||||
- Added the `sl-invalid` event to all form controls to enable custom validation logic [#1167](https://github.com/shoelace-style/shoelace/pull/1167)
|
||||
- Added `validity` and `validationMessage` properties to all form controls [#1167](https://github.com/shoelace-style/shoelace/pull/1167)
|
||||
- Fixed a bug in `<sl-animated-image>` where the play and pause buttons were transposed [#1147](https://github.com/shoelace-style/shoelace/issues/1147)
|
||||
- Fixed a bug that prevented `web-types.json` from being generated [#1154](https://github.com/shoelace-style/shoelace/discussions/1154)
|
||||
- Fixed a bug in `<sl-color-picker>` that prevented `sl-change` and `sl-input` from emitting when using the eye dropper [#1157](https://github.com/shoelace-style/shoelace/issues/1157)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import sinon from 'sinon';
|
||||
import type SlButton from './button';
|
||||
|
||||
@@ -234,4 +235,31 @@ describe('<sl-button>', () => {
|
||||
expect(clickHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests({
|
||||
tagName: 'sl-button',
|
||||
variantName: 'type="button"',
|
||||
|
||||
init: (control: SlButton) => {
|
||||
control.type = 'button';
|
||||
}
|
||||
});
|
||||
|
||||
runFormControlBaseTests({
|
||||
tagName: 'sl-button',
|
||||
variantName: 'type="submit"',
|
||||
|
||||
init: (control: SlButton) => {
|
||||
control.type = 'submit';
|
||||
}
|
||||
});
|
||||
|
||||
runFormControlBaseTests({
|
||||
tagName: 'sl-button',
|
||||
variantName: 'href="xyz"',
|
||||
|
||||
init: (control: SlButton) => {
|
||||
control.href = 'some-url';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import '../icon/icon';
|
||||
import '../spinner/spinner';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { FormControlController, validValidityState } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
@@ -24,6 +24,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
*
|
||||
* @event sl-blur - Emitted when the button loses focus.
|
||||
* @event sl-focus - Emitted when the button gains focus.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @slot - The button's label.
|
||||
* @slot prefix - A presentational prefix icon or similar element.
|
||||
@@ -140,6 +141,24 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** 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 '';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleHostClick = this.handleHostClick.bind(this);
|
||||
@@ -185,6 +204,11 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private isButton() {
|
||||
return this.href ? false : true;
|
||||
}
|
||||
@@ -216,7 +240,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** 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();
|
||||
@@ -290,6 +314,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
tabindex=${this.disabled ? '-1' : '0'}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
@invalid=${this.isButton() ? this.handleInvalid : null}
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<slot name="prefix" part="prefix" class="button__prefix"></slot>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlCheckbox from './checkbox';
|
||||
@@ -308,5 +309,7 @@ describe('<sl-checkbox>', () => {
|
||||
|
||||
expect(indeterminateIcon).to.be.null;
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
* @event sl-change - Emitted when the checked state changes.
|
||||
* @event sl-focus - Emitted when the checkbox gains focus.
|
||||
* @event sl-input - Emitted when the checkbox receives input.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart control - The square container that wraps the checkbox's checked state.
|
||||
@@ -85,6 +86,16 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
/** Makes the checkbox a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
@@ -104,6 +115,11 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
@@ -137,12 +153,12 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show a validation message. Returns true when valid and false when invalid. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows a validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
@@ -189,6 +205,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import sinon from 'sinon';
|
||||
@@ -545,4 +546,6 @@ describe('<sl-color-picker>', () => {
|
||||
// expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-color-picker');
|
||||
});
|
||||
|
||||
@@ -51,10 +51,11 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
*
|
||||
* @slot label - The color picker's form label. Alternatively, you can use the `label` attribute.
|
||||
*
|
||||
* @event sl-blur Emitted when the color picker loses focus.
|
||||
* @event sl-change Emitted when the color picker's value changes.
|
||||
* @event sl-focus Emitted when the color picker receives focus.
|
||||
* @event sl-input Emitted when the color picker receives input.
|
||||
* @event sl-blur - Emitted when the color picker loses focus.
|
||||
* @event sl-change - Emitted when the color picker's value changes.
|
||||
* @event sl-focus - Emitted when the color picker receives focus.
|
||||
* @event sl-input - Emitted when the color picker receives input.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart trigger - The color picker's dropdown trigger.
|
||||
@@ -176,6 +177,19 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
*/
|
||||
@property({ reflect: true }) form = '';
|
||||
|
||||
/** Makes the color picker a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleFocusIn = this.handleFocusIn.bind(this);
|
||||
@@ -190,6 +204,12 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
this.removeEventListener('focusout', this.handleFocusOut);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.input.updateComplete.then(() => {
|
||||
this.formControlController.updateValidity();
|
||||
});
|
||||
}
|
||||
|
||||
private handleCopy() {
|
||||
this.input.select();
|
||||
document.execCommand('copy');
|
||||
@@ -446,6 +466,11 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -734,18 +759,24 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
if (!this.inline && !this.checkValidity()) {
|
||||
if (!this.inline && !this.validity.valid) {
|
||||
// If the input is inline and invalid, show the dropdown so the browser can focus on it
|
||||
this.dropdown.show();
|
||||
this.addEventListener('sl-after-show', () => this.input.reportValidity(), { once: true });
|
||||
return this.checkValidity();
|
||||
|
||||
if (!this.disabled) {
|
||||
// By standards we have to emit a `sl-invalid` event here synchronously.
|
||||
this.formControlController.emitInvalidEvent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.input.reportValidity();
|
||||
@@ -895,11 +926,13 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
value=${this.isEmpty ? '' : this.inputValue}
|
||||
?required=${this.required}
|
||||
?disabled=${this.disabled}
|
||||
aria-label=${this.localize.term('currentValue')}
|
||||
@keydown=${this.handleInputKeyDown}
|
||||
@sl-change=${this.handleInputChange}
|
||||
@sl-input=${this.handleInputInput}
|
||||
@sl-invalid=${this.handleInputInvalid}
|
||||
@sl-blur=${this.stopNestedEventPropagation}
|
||||
@sl-focus=${this.stopNestedEventPropagation}
|
||||
></sl-input>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { getFormControls } from '../../../dist/utilities/form.js';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form'; // must come from the same module
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands'; // must come from the same module
|
||||
import { serialize } from '../../utilities/form';
|
||||
import sinon from 'sinon';
|
||||
import type SlInput from './input';
|
||||
|
||||
@@ -496,4 +497,6 @@ describe('<sl-input>', () => {
|
||||
expect(formControls.map((fc: HTMLInputElement) => fc.value).join('')).to.equal('12345678910'); // eslint-disable-line
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-input');
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ const isFirefox = isChromium ? false : navigator.userAgent.includes('Firefox');
|
||||
* @event sl-clear - Emitted when the clear button is activated.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
@@ -227,6 +228,16 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
this.value = input.value;
|
||||
}
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
@@ -262,8 +273,9 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleInvalid() {
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -372,7 +384,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlPopup extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private anchorEl: HTMLElement | null;
|
||||
private anchorEl: Element | null;
|
||||
private cleanup: ReturnType<typeof autoUpdate> | undefined;
|
||||
|
||||
/** A reference to the internal popup container. Useful for animating and styling the popup with JavaScript. */
|
||||
@@ -223,7 +223,7 @@ export default class SlPopup extends ShoelaceElement {
|
||||
// Locate the anchor by id
|
||||
const root = this.getRootNode() as Document | ShadowRoot;
|
||||
this.anchorEl = root.getElementById(this.anchor);
|
||||
} else if (this.anchor instanceof HTMLElement) {
|
||||
} else if (this.anchor instanceof Element) {
|
||||
// Use the anchor's reference
|
||||
this.anchorEl = this.anchor;
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlChangeEvent from '../../events/sl-change';
|
||||
@@ -316,4 +317,6 @@ describe('when the value changes', () => {
|
||||
radioGroup.value = '2';
|
||||
await radioGroup.updateComplete;
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-radio-group');
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import '../button-group/button-group';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import {
|
||||
customErrorValidityState,
|
||||
FormControlController,
|
||||
validValidityState,
|
||||
valueMissingValidityState
|
||||
} from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -26,6 +31,7 @@ import type SlRadioButton from '../radio-button/radio-button';
|
||||
*
|
||||
* @event sl-change - Emitted when the radio group's selected value changes.
|
||||
* @event sl-input - Emitted when the radio group receives user input.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
@@ -75,6 +81,34 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
/** Ensures a child radio is checked before allowing the containing form to submit. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
const isRequiredAndEmpty = this.required && !this.value;
|
||||
const hasCustomValidityMessage = this.customValidityMessage !== '';
|
||||
|
||||
if (hasCustomValidityMessage) {
|
||||
return customErrorValidityState;
|
||||
} else if (isRequiredAndEmpty) {
|
||||
return valueMissingValidityState;
|
||||
}
|
||||
|
||||
return validValidityState;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
const isRequiredAndEmpty = this.required && !this.value;
|
||||
const hasCustomValidityMessage = this.customValidityMessage !== '';
|
||||
|
||||
if (hasCustomValidityMessage) {
|
||||
return this.customValidityMessage;
|
||||
} else if (isRequiredAndEmpty) {
|
||||
return this.validationInput.validationMessage;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.defaultValue = this.value;
|
||||
@@ -187,10 +221,15 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private updateCheckedRadio() {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
this.formControlController.setValidity(this.checkValidity());
|
||||
this.formControlController.setValidity(this.validity.valid);
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
@@ -200,12 +239,13 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
const isRequiredAndEmpty = this.required && !this.value;
|
||||
const hasCustomValidityMessage = this.customValidityMessage !== '';
|
||||
|
||||
if (isRequiredAndEmpty || hasCustomValidityMessage) {
|
||||
this.formControlController.emitInvalidEvent();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -222,7 +262,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity(): boolean {
|
||||
const isValid = this.checkValidity();
|
||||
const isValid = this.validity.valid;
|
||||
|
||||
this.errorMessage = this.customValidityMessage || isValid ? '' : this.validationInput.validationMessage;
|
||||
this.formControlController.setValidity(isValid);
|
||||
@@ -289,6 +329,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
?required=${this.required}
|
||||
tabindex="-1"
|
||||
hidden
|
||||
@invalid=${this.handleInvalid}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import sinon from 'sinon';
|
||||
@@ -229,4 +230,6 @@ describe('<sl-range>', () => {
|
||||
expect(input.value).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-range');
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
@@ -101,6 +102,16 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue() defaultValue = 0;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.syncRange());
|
||||
@@ -207,6 +218,11 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
/** Sets focus on the range. */
|
||||
focus(options?: FocusOptions) {
|
||||
this.input.focus(options);
|
||||
@@ -233,7 +249,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
@@ -306,8 +322,9 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
.value=${live(this.value.toString())}
|
||||
aria-describedby="help-text"
|
||||
@change=${this.handleChange}
|
||||
@input=${this.handleInput}
|
||||
@focus=${this.handleFocus}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@blur=${this.handleBlur}
|
||||
/>
|
||||
${this.tooltip !== 'none' && !this.disabled
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import sinon from 'sinon';
|
||||
@@ -548,4 +549,6 @@ describe('<sl-select>', () => {
|
||||
|
||||
expect(tag.hasAttribute('pill')).to.be.true;
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-select');
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ import type SlRemoveEvent from '../../events/sl-remove';
|
||||
* @event sl-after-show - Emitted after the select's menu opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the select's menu closes.
|
||||
* @event sl-after-hide - Emitted after the select's menu closes and all animations are complete.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
@@ -163,6 +164,16 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** The select's required attribute. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.valueInput.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.valueInput.validationMessage;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleDocumentFocusIn = this.handleDocumentFocusIn.bind(this);
|
||||
@@ -521,6 +532,11 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
});
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Close the listbox when the control is disabled
|
||||
@@ -604,7 +620,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.valueInput.checkValidity();
|
||||
}
|
||||
@@ -753,6 +769,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
@focus=${() => this.focus()}
|
||||
@invalid=${this.handleInvalid}
|
||||
/>
|
||||
|
||||
${hasClearIcon
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlSwitch from './switch';
|
||||
@@ -260,4 +261,6 @@ describe('<sl-switch>', () => {
|
||||
expect(switchEl.checked).to.false;
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-switch');
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
* @event sl-change - Emitted when the control's checked state changes.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart control - The control that houses the switch's thumb.
|
||||
@@ -76,6 +77,16 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** Makes the switch a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
@@ -89,6 +100,11 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
this.checked = !this.checked;
|
||||
this.emit('sl-change');
|
||||
@@ -142,7 +158,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
@@ -185,6 +201,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
@keydown=${this.handleKeyDown}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import sinon from 'sinon';
|
||||
@@ -292,4 +293,6 @@ describe('<sl-textarea>', () => {
|
||||
expect(textarea.spellcheck).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-textarea');
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
* @event sl-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
@@ -135,6 +136,16 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/** Gets the validation message */
|
||||
get validationMessage() {
|
||||
return this.input.validationMessage;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.setTextareaHeight());
|
||||
@@ -175,6 +186,11 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
private setTextareaHeight() {
|
||||
if (this.resize === 'auto') {
|
||||
this.input.style.height = 'auto';
|
||||
@@ -260,7 +276,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
@@ -344,6 +360,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
|
||||
aria-describedby="help-text"
|
||||
@change=${this.handleChange}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@focus=${this.handleFocus}
|
||||
@blur=${this.handleBlur}
|
||||
></textarea>
|
||||
|
||||
@@ -127,7 +127,7 @@ export class FormControlController implements ReactiveController {
|
||||
}
|
||||
|
||||
if (this.host.hasUpdated) {
|
||||
this.setValidity(this.host.checkValidity());
|
||||
this.setValidity(this.host.validity.valid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,11 +341,68 @@ export class FormControlController implements ReactiveController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the form control's validity based on the current value of `host.checkValidity()`. Call this when anything
|
||||
* Updates the form control's validity based on the current value of `host.validity.valid`. Call this when anything
|
||||
* that affects constraint validation changes so the component receives the correct validity states.
|
||||
*/
|
||||
updateValidity() {
|
||||
const host = this.host;
|
||||
this.setValidity(host.checkValidity());
|
||||
this.setValidity(host.validity.valid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a non-bubbling, cancelable custom event of type `sl-invalid`.
|
||||
* If the `sl-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 `sl-invalid`
|
||||
* event will be cancelled before being dispatched.
|
||||
*/
|
||||
emitInvalidEvent(originalInvalidEvent?: Event) {
|
||||
const slInvalidEvent = new CustomEvent<void>('sl-invalid', {
|
||||
bubbles: false,
|
||||
composed: false,
|
||||
cancelable: true
|
||||
});
|
||||
|
||||
if (!originalInvalidEvent) {
|
||||
slInvalidEvent.preventDefault();
|
||||
}
|
||||
|
||||
if (!this.host.dispatchEvent(slInvalidEvent)) {
|
||||
originalInvalidEvent?.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Predefined common validity states.
|
||||
* All of them are read-only.
|
||||
*/
|
||||
|
||||
// A validity state object that represents `valid`
|
||||
export const validValidityState: ValidityState = Object.freeze({
|
||||
badInput: false,
|
||||
customError: false,
|
||||
patternMismatch: false,
|
||||
rangeOverflow: false,
|
||||
rangeUnderflow: false,
|
||||
stepMismatch: false,
|
||||
tooLong: false,
|
||||
tooShort: false,
|
||||
typeMismatch: false,
|
||||
valid: true,
|
||||
valueMissing: false
|
||||
});
|
||||
|
||||
// A validity state object that represents `value missing`
|
||||
export const valueMissingValidityState: ValidityState = Object.freeze({
|
||||
...validValidityState,
|
||||
valid: false,
|
||||
valueMissing: true
|
||||
});
|
||||
|
||||
// A validity state object that represents a custom error
|
||||
export const customErrorValidityState: ValidityState = Object.freeze({
|
||||
...validValidityState,
|
||||
valid: false,
|
||||
customError: true
|
||||
});
|
||||
|
||||
@@ -99,8 +99,13 @@ export interface ShoelaceFormControl extends ShoelaceElement {
|
||||
minlength?: number;
|
||||
maxlength?: number;
|
||||
|
||||
// Validation properties
|
||||
readonly validity: ValidityState;
|
||||
readonly validationMessage: string;
|
||||
|
||||
// Validation methods
|
||||
checkValidity: () => boolean;
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity: () => boolean;
|
||||
setCustomValidity: (message: string) => void;
|
||||
}
|
||||
|
||||
307
src/internal/test/form-control-base-tests.ts
Normal file
307
src/internal/test/form-control-base-tests.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { expect, fixture } from '@open-wc/testing';
|
||||
import type { ShoelaceFormControl } from '../shoelace-element';
|
||||
|
||||
type CreateControlFn = () => Promise<ShoelaceFormControl>;
|
||||
|
||||
/** Runs a set of generic tests for Shoelace form controls */
|
||||
export function runFormControlBaseTests<T extends ShoelaceFormControl = ShoelaceFormControl>(
|
||||
tagNameOrConfig:
|
||||
| string
|
||||
| {
|
||||
tagName: string;
|
||||
init?: (control: T) => void;
|
||||
variantName: string;
|
||||
}
|
||||
) {
|
||||
const isStringArg = typeof tagNameOrConfig === 'string';
|
||||
const tagName = isStringArg ? tagNameOrConfig : tagNameOrConfig.tagName;
|
||||
|
||||
// component initialization function or null
|
||||
const init =
|
||||
isStringArg || !tagNameOrConfig.init //
|
||||
? null
|
||||
: tagNameOrConfig.init || null;
|
||||
|
||||
// either `<tagName>` or `<tagName> (<variantName>)
|
||||
const displayName = isStringArg //
|
||||
? tagName
|
||||
: `${tagName} (${tagNameOrConfig.variantName})`;
|
||||
|
||||
// creates a testable form control instance
|
||||
const createControl = async () => {
|
||||
const control = await createFormControl<T>(tagName);
|
||||
init?.(control);
|
||||
return control;
|
||||
};
|
||||
|
||||
runAllValidityTests(tagName, displayName, createControl);
|
||||
}
|
||||
|
||||
//
|
||||
// Applicable for all Shoelace form controls. This function checks the behavior of:
|
||||
// - `.validity`
|
||||
// - `.validationMessage`,
|
||||
// - `.checkValidity()`
|
||||
// - `.reportValidity()`
|
||||
// - `.setCustomValidity(msg)`
|
||||
//
|
||||
function runAllValidityTests(
|
||||
tagName: string, //
|
||||
displayName: string,
|
||||
createControl: () => Promise<ShoelaceFormControl>
|
||||
) {
|
||||
// will be used later to retrieve meta information about the control
|
||||
describe(`Form validity base test for ${displayName}`, async () => {
|
||||
it('should have a property `validity` of type `object`', async () => {
|
||||
const control = await createControl();
|
||||
expect(control).satisfy(() => control.validity !== null && typeof control.validity === 'object');
|
||||
});
|
||||
|
||||
it('should have a property `validationMessage` of type `string`', async () => {
|
||||
const control = await createControl();
|
||||
expect(control).satisfy(() => typeof control.validationMessage === 'string');
|
||||
});
|
||||
|
||||
it('should implement method `checkValidity`', async () => {
|
||||
const control = await createControl();
|
||||
expect(control).satisfies(() => typeof control.checkValidity === 'function');
|
||||
});
|
||||
|
||||
it('should implement method `setCustomValidity`', async () => {
|
||||
const control = await createControl();
|
||||
expect(control).satisfies(() => typeof control.setCustomValidity === 'function');
|
||||
});
|
||||
|
||||
it('should implement method `reportValidity`', async () => {
|
||||
const control = await createControl();
|
||||
expect(control).satisfies(() => typeof control.reportValidity === 'function');
|
||||
});
|
||||
|
||||
it('should be valid initially', async () => {
|
||||
const control = await createControl();
|
||||
expect(control.validity.valid).to.equal(true);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.checkValidity()` will return `true` when valid', async () => {
|
||||
const control = await createControl();
|
||||
expect(control.checkValidity()).to.equal(true);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.reportValidity()` will return `true` when valid', async () => {
|
||||
const control = await createControl();
|
||||
expect(control.reportValidity()).to.equal(true);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.checkValidity()` is called while valid', async () => {
|
||||
const control = await createControl();
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.checkValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.reportValidity()` is called while valid', async () => {
|
||||
const control = await createControl();
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.reportValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
|
||||
// TODO: As soon as `SlRadioGroup` has a property `disabled` this
|
||||
// condition can be removed
|
||||
if (tagName !== 'sl-radio-group') {
|
||||
it('should not emit an `sl-invalid` event when `.checkValidity()` is called in custom error case while disabled', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = true;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.checkValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.reportValidity()` is called in custom error case while disabled', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = true;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.reportValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
}
|
||||
|
||||
// Run special tests depending on component type
|
||||
|
||||
const mode = getMode(await createControl());
|
||||
|
||||
if (mode === 'slButtonOfTypeButton') {
|
||||
runSpecialTests_slButtonOfTypeButton(createControl);
|
||||
} else if (mode === 'slButtonWithHRef') {
|
||||
runSpecialTests_slButtonWithHref(createControl);
|
||||
} else {
|
||||
runSpecialTests_standard(createControl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Special tests for <sl-button type="button">
|
||||
//
|
||||
function runSpecialTests_slButtonOfTypeButton(createControl: CreateControlFn) {
|
||||
it('should make sure that `.validity.valid` is `false` in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.validity.valid).to.equal(false);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.checkValidity()` will still return `true` when custom error has been set', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.checkValidity()).to.equal(true);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.reportValidity()` will still return `true` when custom error has been set', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.reportValidity()).to.equal(true);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.checkValidity()` is called in custom error case, and not disabled', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = false;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.checkValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.reportValidity()` is called in custom error case, and not disabled', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = false;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.reportValidity());
|
||||
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Special tests for <sl-button href="...">
|
||||
//
|
||||
function runSpecialTests_slButtonWithHref(createControl: CreateControlFn) {
|
||||
it('should make sure that calling `.checkValidity()` will return `true` in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.checkValidity()).to.equal(true);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.reportValidity()` will return `true` in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.reportValidity()).to.equal(true);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.checkValidity()` is called in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.checkValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should not emit an `sl-invalid` event when `.reportValidity()` is called in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.reportValidity());
|
||||
expect(emittedEvents.length).to.equal(0);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Special tests for all components with standard behavior
|
||||
//
|
||||
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');
|
||||
expect(control.validity.valid).to.equal(false);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.checkValidity()` will return `false` in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.checkValidity()).to.equal(false);
|
||||
});
|
||||
|
||||
it('should make sure that calling `.reportValidity()` will return `false` in custom error case', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
expect(control.reportValidity()).to.equal(false);
|
||||
});
|
||||
|
||||
it('should emit an `sl-invalid` event when `.checkValidity()` is called in custom error case and not disabled', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = false;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.checkValidity());
|
||||
expect(emittedEvents.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('should emit an `sl-invalid` event when `.reportValidity()` is called in custom error case and not disabled', async () => {
|
||||
const control = await createControl();
|
||||
control.setCustomValidity('error');
|
||||
control.disabled = false;
|
||||
await control.updateComplete;
|
||||
const emittedEvents = checkEventEmissions(control, 'sl-invalid', () => control.reportValidity());
|
||||
expect(emittedEvents.length).to.equal(1);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Local helper functions
|
||||
//
|
||||
|
||||
// Creates a testable Shoelace form control instance
|
||||
async function createFormControl<T extends ShoelaceFormControl = ShoelaceFormControl>(tagName: string): Promise<T> {
|
||||
return await fixture<T>(`<${tagName}></${tagName}>`);
|
||||
}
|
||||
|
||||
// Runs an action while listening for emitted events of a given type. Returns an array of all events of the given type
|
||||
// that have been been emitted while the action was running.
|
||||
function checkEventEmissions(control: ShoelaceFormControl, eventType: string, action: () => void): Event[] {
|
||||
const emittedEvents: Event[] = [];
|
||||
|
||||
const eventHandler = (event: Event) => {
|
||||
emittedEvents.push(event);
|
||||
};
|
||||
|
||||
try {
|
||||
control.addEventListener(eventType, eventHandler);
|
||||
action();
|
||||
} finally {
|
||||
control.removeEventListener(eventType, eventHandler);
|
||||
}
|
||||
|
||||
return emittedEvents;
|
||||
}
|
||||
|
||||
// Component `sl-button` behaves quite different to the other components. To keep things simple we use simple conditions
|
||||
// here. `sl-button` might stay the only component in Shoelace core behaves that way, so we just hard code it here.
|
||||
function getMode(control: ShoelaceFormControl) {
|
||||
if (
|
||||
control.localName === 'sl-button' && //
|
||||
'href' in control &&
|
||||
'type' in control &&
|
||||
control.type === 'button' &&
|
||||
!control.href
|
||||
) {
|
||||
return 'slButtonOfTypeButton';
|
||||
}
|
||||
|
||||
// <sl-button href="...">
|
||||
if (control.localName === 'sl-button' && 'href' in control && !!control.href) {
|
||||
return 'slButtonWithHRef';
|
||||
}
|
||||
|
||||
// all other components
|
||||
return 'standard';
|
||||
}
|
||||
Reference in New Issue
Block a user