kopia lustrzana https://github.com/shoelace-style/shoelace
* #1163 - added read-only properties 'validity' and 'validationMessage' to all nine form controls * #1163 - added base support for showing form validation messages below the form controls * #1163 - animated validation errors in demo * #1181 - Removed all previous changes that have been validation error specific * Started with 'Inline validation' demo / fixed merge issues / etc. * #1181 - continued work on missing form validation features * #1181 - enhanced validation support for SlColorPicker / some cleanup * #1181 - fixed CSS issues * #1181 - fixed again CSS issues * '1181 - added form validation features finally working * #1181 - bug fixes * #1181 - fixed open issues / added API doc comments * #1181 - updated inline validation demos / removed some legacy code * #1181 - finished invalid form validation example * #1181 - added tests / several bugfixes * #1181 - fixed typos etc. * #1181 - tests * #1181 - tests * #1181 - testspull/1192/head
rodzic
19cf823da5
commit
4a28825ea7
|
|
@ -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,46 +318,452 @@ 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);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Inline Form Validation
|
||||
|
||||
You can switch from normal validation mode, where validation messages are presented by browser specific tooltips, to an inline validation mode where the validation messages are displayed below the form fields, normally in red color.
|
||||
This can be achieved completely in userland with customizations using CSS and JavaScript.
|
||||
Here's the same example as the previous one, but this time we use inline form validation.
|
||||
|
||||
```html preview
|
||||
<sl-animation class="animation-inline-validation" name="shakeX" duration="1000" iterations="1" easing="easeInOut">
|
||||
<form class="inline-validation">
|
||||
<sl-input
|
||||
name="name"
|
||||
label="Name"
|
||||
help-text="What would you like people to call you?"
|
||||
autocomplete="off"
|
||||
required
|
||||
></sl-input>
|
||||
|
||||
<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>
|
||||
</sl-animation>
|
||||
|
||||
<style>
|
||||
.inline-validation sl-input,
|
||||
.inline-validation sl-select,
|
||||
.inline-validation sl-checkbox {
|
||||
display: block;
|
||||
margin-bottom: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
/* user invalid styles */
|
||||
.inline-validation sl-input[data-user-invalid]::part(base),
|
||||
.inline-validation sl-select[data-user-invalid]::part(combobox),
|
||||
.inline-validation sl-checkbox[data-user-invalid]::part(control) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
.inline-validation [data-user-invalid]::part(form-control-label),
|
||||
.inline-validation [data-user-invalid]::part(form-control-help-text),
|
||||
.inline-validation sl-checkbox[data-user-invalid]::part(label) {
|
||||
color: var(--sl-color-danger-700);
|
||||
}
|
||||
|
||||
.inline-validation sl-checkbox[data-user-invalid]::part(control) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-validation sl-input:focus-within[data-user-invalid]::part(base),
|
||||
.inline-validation sl-select:focus-within[data-user-invalid]::part(combobox),
|
||||
.inline-validation 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 */
|
||||
.inline-validation sl-input[data-user-valid]::part(base),
|
||||
.inline-validation sl-select[data-user-valid]::part(combobox),
|
||||
.inline-validation sl-checkbox[data-user-valid]::part(control) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
.inline-validation [data-user-valid]::part(form-control-label),
|
||||
.inline-validation [data-user-valid]::part(form-control-help-text),
|
||||
.inline-validation sl-checkbox[data-user-valid]::part(label) {
|
||||
color: var(--sl-color-success-700);
|
||||
}
|
||||
|
||||
.inline-validation sl-checkbox[data-user-valid]::part(control) {
|
||||
background-color: var(--sl-color-success-600);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-validation sl-input:focus-within[data-user-valid]::part(base),
|
||||
.inline-validation sl-select:focus-within[data-user-valid]::part(combobox),
|
||||
.inline-validation 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);
|
||||
}
|
||||
|
||||
/* styles for the inline validation messages */
|
||||
|
||||
.inline-validation :is([data-valid], [data-invalid]):not(sl-button)::after {
|
||||
display: block;
|
||||
font-size: var(--sl-font-size-small);
|
||||
color: var(--sl-color-danger-700);
|
||||
content: '\00a0';
|
||||
}
|
||||
|
||||
.inline-validation [data-user-invalid]:not(sl-button)::after {
|
||||
content: attr(data-error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
// With the following few lines of JavaScript code plus the app independent
|
||||
// utility function `activateInlineFormValidation` you can switch to
|
||||
// inline form validation mode.
|
||||
const form = document.querySelector('form.inline-validation');
|
||||
const animation = document.querySelector('sl-animation.animation-inline-validation');
|
||||
|
||||
activateInlineFormValidation(form);
|
||||
|
||||
form.addEventListener('submit', ev => {
|
||||
ev.preventDefault();
|
||||
alert('All fields are valid');
|
||||
});
|
||||
|
||||
// I the user tries to submit invalid form data then shake the form
|
||||
// for a moment to indicate an submit error
|
||||
form.addEventListener(
|
||||
'sl-invalid',
|
||||
() => {
|
||||
animation.play = true;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
/**
|
||||
* `activateInlineFormValidation` is a utility function for Shoelace based HTML
|
||||
* forms. It allows to switch from the usual tooltip based way of showing validation
|
||||
* errors to inline form validation where validation errors will be displayed below
|
||||
* the corresponding form controls.
|
||||
* This will be achieved by dynamically adding data attributes for error messages
|
||||
* to the form controls, if required. And to use the CSS function `attr(...)`
|
||||
* to retrieve the error messages in CSS (by using the `::after` pseudo-element).
|
||||
*
|
||||
* @param container A DOM container element, for example the form element
|
||||
* @param errorAttribute Name of the data attribute of the form controls to
|
||||
* store the current validation message. Default value is
|
||||
* 'data-error'.
|
||||
*
|
||||
* @return Returns a cancellation function to undo the changes that
|
||||
* have been necessary to activate inline validation
|
||||
*/
|
||||
function activateInlineFormValidation(container, errorAttribute = 'data-error') {
|
||||
let formControls = null; // type: Set<HTMLElement> | null
|
||||
|
||||
// Checks whether an element is a Shoelace form control
|
||||
const isFormControl = elem => {
|
||||
return (
|
||||
elem instanceof HTMLElement &&
|
||||
typeof elem.checkValidity === 'function' &&
|
||||
typeof elem.reportValidity === 'function' &&
|
||||
typeof elem.validationMessage === 'string'
|
||||
);
|
||||
};
|
||||
|
||||
// Updates the error data attribute of a given Shoelace form control,
|
||||
// depending on the form control's `validationMessage` property
|
||||
const updateValidationMessage = formControl => {
|
||||
const message = formControl.validationMessage;
|
||||
|
||||
if (typeof message === 'string' && message !== '') {
|
||||
formControl.setAttribute(errorAttribute, message);
|
||||
} else {
|
||||
formControl.removeAttribute(errorAttribute);
|
||||
}
|
||||
};
|
||||
|
||||
// Updates the error attributes for all Shoelace form controls
|
||||
// in the container and returns a set of all currently existing
|
||||
// Shoelace form controls in the container.
|
||||
const updateAllValidationMessages = () => {
|
||||
const ret = new Set();
|
||||
|
||||
for (const elem of container.querySelectorAll(':is([data-valid], [data-invalid])')) {
|
||||
if (isFormControl(elem)) {
|
||||
ret.add(elem);
|
||||
updateValidationMessage(elem);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// --- event handlers --------------
|
||||
|
||||
const onInvalid = event => {
|
||||
// Prevent the browser from showing the usual validation error tooltips
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onInput = event => {
|
||||
const target = event.target;
|
||||
|
||||
if (formControls.has(target)) {
|
||||
// Update error attribute depending on validation message
|
||||
updateValidationMessage(target);
|
||||
}
|
||||
};
|
||||
|
||||
// --- main ------------------------
|
||||
|
||||
// Register event handlers
|
||||
container.addEventListener('sl-input', onInput);
|
||||
container.addEventListener('sl-invalid', onInvalid, true);
|
||||
|
||||
// Register mutation observer to detect dynamically added
|
||||
// or removed form controls
|
||||
const observer = new MutationObserver(() => {
|
||||
// Update and remember current form controls
|
||||
const newFormControls = updateAllValidationMessages();
|
||||
|
||||
// Cleanup previously removed form controls
|
||||
for (const formControl of formControls) {
|
||||
if (!newFormControls.has(formControl)) {
|
||||
formControl.removeAttribute(errorAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
formControls = newFormControls;
|
||||
});
|
||||
|
||||
// Observe the whole DOM subtree of the container
|
||||
observer.observe(container, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
formControls = updateAllValidationMessages();
|
||||
|
||||
// provide cancellation functionality
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const cancel = () => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.removeEventListener('sl-input', onInput);
|
||||
container.removeEventListener('sl-invalid', onInvalid, true);
|
||||
observer.disconnect();
|
||||
|
||||
for (const formControl of formControls) {
|
||||
formControl.removeAttribute(errorAttribute);
|
||||
}
|
||||
|
||||
formControls = null;
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
return cancel;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Inline Form Validation (old version - to be deleted after testing) // TODO!!!!
|
||||
|
||||
```html preview
|
||||
<sl-animation class="animation-inline-validation2" name="shakeX" duration="1000" iterations="1" easing="easeInOut">
|
||||
<form class="inline-validation2">
|
||||
<sl-radio-group name="salutation" label="Salutation" required>
|
||||
<sl-radio value="mrs">Mrs.</sl-radio>
|
||||
<sl-radio value="mr">Mr.</sl-radio>
|
||||
<sl-radio value="other">Other</sl-radio>
|
||||
</sl-radio-group>
|
||||
|
||||
<sl-input name="name" label="Name" required></sl-input>
|
||||
<sl-input name="email" type="email" label="Email" required></sl-input>
|
||||
|
||||
<sl-select name="country" label="Country" help-text="Only USA and Canada" clearable required>
|
||||
<sl-option value="US">USA</sl-option>
|
||||
<sl-option value="CA">Canada</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<label>
|
||||
Your favorite color *
|
||||
<sl-color-picker required>Your favorite color</sl-color-picker>
|
||||
</label>
|
||||
|
||||
<sl-switch name="customer" required>Please approve that this is really your favorite color</sl-switch>
|
||||
|
||||
<sl-textarea name="question" label="Your question" required></sl-textarea>
|
||||
|
||||
<sl-checkbox name="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>
|
||||
</sl-animation>
|
||||
|
||||
<script type="module">
|
||||
const form = document.querySelector('form.inline-validation2');
|
||||
const animation = document.querySelector('sl-animation.animation-inline-validation2');
|
||||
|
||||
updateAllValidationMessages(form);
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
|
||||
form.addEventListener(
|
||||
'sl-invalid',
|
||||
event => {
|
||||
updateValidationMessage(event.target);
|
||||
event.preventDefault();
|
||||
animation.play = true;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
form.addEventListener('sl-input', event => {
|
||||
updateValidationMessage(event.target);
|
||||
});
|
||||
|
||||
function isFormControl(elem) {
|
||||
return (
|
||||
elem.hasAttribute('data-valid') ||
|
||||
(elem.hasAttribute('data-invalid') && typeof elem.validationMessage === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
function updateValidationMessage(formControl) {
|
||||
if (isFormControl(formControl)) {
|
||||
formControl.setAttribute('data-error', formControl.validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllValidationMessages(container) {
|
||||
for (const elem of container.querySelectorAll('*')) {
|
||||
if (isFormControl(elem)) {
|
||||
updateValidationMessage(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.inline-validation2 :is([data-valid], [data-invalid]):not(sl-button) {
|
||||
display: block;
|
||||
margin-bottom: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.inline-validation2 sl-radio-group sl-radio {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
/* user invalid styles */
|
||||
.inline-validation2 sl-input[data-user-invalid]::part(base),
|
||||
.inline-validation2 sl-select[data-user-invalid]::part(combobox) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
.inline-validation2 sl-input:focus-within[data-user-invalid]::part(base),
|
||||
.inline-validation2 sl-textarea:focus-within[data-user-invalid]::part(base),
|
||||
.inline-validation2 sl-select:focus-within[data-user-invalid]::part(combobox) {
|
||||
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 */
|
||||
.inline-validation2 sl-input[data-user-valid]::part(base),
|
||||
.inline-validation2 sl-textarea[data-user-valid]::part(base),
|
||||
.inline-validation2 sl-select[data-user-valid]::part(combobox) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
.inline-validation2 sl-input:focus-within[data-user-valid]::part(base),
|
||||
.inline-validation2 sl-textarea:focus-within[data-user-valid]::part(base),
|
||||
.inline-validation2 sl-select:focus-within[data-user-valid]::part(combobox) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
||||
}
|
||||
|
||||
.inline-validation2 :is([data-valid], [data-invalid]):not(sl-button)::after {
|
||||
display: block;
|
||||
font-size: var(--sl-font-size-small);
|
||||
color: var(--sl-color-danger-700);
|
||||
content: '\00a0';
|
||||
}
|
||||
|
||||
.inline-validation2 [data-user-invalid]:not(sl-button)::after {
|
||||
content: attr(data-error);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Getting Associated Form Controls
|
||||
|
||||
At this time, using [`HTMLFormElement.elements`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements) will not return Shoelace form controls because the browser is unaware of their status as custom element form controls. Fortunately, Shoelace provides an `elements()` function that does something very similar. However, instead of returning an [`HTMLFormControlsCollection`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection), it returns an array of HTML and Shoelace form controls in the order they appear in the DOM.
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -140,6 +140,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 +203,11 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
|||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
private isButton() {
|
||||
return this.href ? false : true;
|
||||
}
|
||||
|
|
@ -290,6 +313,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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @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.emitSlInvalidEvent(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. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows a validation message if the control is invalid. */
|
||||
/** Checks for validity and shows a validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,10 +49,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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart trigger - The color picker's dropdown trigger.
|
||||
|
|
@ -174,6 +175,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);
|
||||
|
|
@ -188,6 +202,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');
|
||||
|
|
@ -444,6 +464,11 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
}
|
||||
}
|
||||
|
||||
private handleInputInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
|
@ -732,18 +757,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 the browser's validation message. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
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.emitSlInvalidEvent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.input.reportValidity();
|
||||
|
|
@ -893,11 +924,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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @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.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
|
|
@ -372,12 +384,12 @@ 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 the browser's validation message. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 SlRadio from '../radio/radio';
|
||||
|
|
@ -315,4 +316,6 @@ describe('when the value changes', () => {
|
|||
radioGroup.value = '2';
|
||||
await radioGroup.updateComplete;
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-radio-group');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
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';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
|
||||
import styles from './radio-group.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
|
|
@ -26,6 +34,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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
|
|
@ -75,6 +84,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 +224,15 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitSlInvalidEvent(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 +242,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 the browser's validation message. Will emit an `sl-invalid` event in case of negative result. */
|
||||
checkValidity() {
|
||||
const isRequiredAndEmpty = this.required && !this.value;
|
||||
const hasCustomValidityMessage = this.customValidityMessage !== '';
|
||||
|
||||
if (isRequiredAndEmpty || hasCustomValidityMessage) {
|
||||
this.formControlController.emitSlInvalidEvent();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -220,9 +263,9 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result. */
|
||||
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 +332,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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,6 +101,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 +217,11 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
|||
}
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
/** Sets focus on the range. */
|
||||
focus(options?: FocusOptions) {
|
||||
this.input.focus(options);
|
||||
|
|
@ -306,8 +321,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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import type SlPopup from '../popup/popup';
|
|||
* @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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
|
|
@ -162,6 +163,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);
|
||||
|
|
@ -520,6 +531,11 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||
});
|
||||
}
|
||||
|
||||
private handleInvalid(event: Event) {
|
||||
this.formControlController.setValidity(false);
|
||||
this.formControlController.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Close the listbox when the control is disabled
|
||||
|
|
@ -603,12 +619,12 @@ 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 the browser's validation message. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
checkValidity() {
|
||||
return this.valueInput.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
reportValidity() {
|
||||
return this.valueInput.reportValidity();
|
||||
}
|
||||
|
|
@ -752,6 +768,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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @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.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
this.checked = !this.checked;
|
||||
this.emit('sl-change');
|
||||
|
|
@ -142,12 +158,12 @@ 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 the browser's validation message. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
|
@ -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 `.checkValidity()` or `.reportValidity()` has been called and the returned value is `false`.
|
||||
*
|
||||
* @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.emitSlInvalidEvent(event);
|
||||
}
|
||||
|
||||
private setTextareaHeight() {
|
||||
if (this.resize === 'auto') {
|
||||
this.input.style.height = 'auto';
|
||||
|
|
@ -260,12 +276,12 @@ 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 the browser's validation message. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. Will emit an `sl-invalid` event in case of negative result (if not disabled). */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
emitSlInvalidEvent(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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ export interface ShoelaceFormControl extends ShoelaceElement {
|
|||
minlength?: number;
|
||||
maxlength?: number;
|
||||
|
||||
// Validation properties
|
||||
readonly validity: ValidityState;
|
||||
readonly validationMessage: string;
|
||||
|
||||
// Validation methods
|
||||
checkValidity: () => boolean;
|
||||
reportValidity: () => boolean;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,323 @@
|
|||
import { expect, fixture } from '@open-wc/testing';
|
||||
import type { ShoelaceFormControl } from '../shoelace-element';
|
||||
|
||||
// === exports =======================================================
|
||||
|
||||
export { runFormControlBaseTests };
|
||||
|
||||
// === types =========================================================
|
||||
|
||||
type CreateControlFn = () => Promise<ShoelaceFormControl>;
|
||||
|
||||
// === all form control tests ========================================
|
||||
|
||||
// Runs a set of generic tests for Shoelace form controls
|
||||
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);
|
||||
}
|
||||
|
||||
// === all validity tests ============================================
|
||||
|
||||
// Checks the correct behavior of:
|
||||
// - `.validity`
|
||||
// - `.validationMessage`,
|
||||
// - `.checkValidity()`
|
||||
// - `.reportValidity()`
|
||||
// - `.setCustomValidity(msg)`
|
||||
//
|
||||
// Applicable for all Shoelace form controls
|
||||
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="xyz"...> ===================
|
||||
|
||||
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 usual 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) {
|
||||
// <sl-button type="button" ...> shall behave the same way as
|
||||
// <button type="button" ...>
|
||||
// Please find here a little demo:
|
||||
// https://jsbin.com/qiwaxivuta/edit?html,console,output
|
||||
if (
|
||||
control.localName === 'sl-button' && //
|
||||
'href' in control &&
|
||||
'type' in control &&
|
||||
control.type === 'button' &&
|
||||
!control.href
|
||||
) {
|
||||
return 'slButtonOfTypeButton';
|
||||
}
|
||||
|
||||
// <sl-button href="[xyz]" ...>
|
||||
if (
|
||||
control.localName === 'sl-button' && //
|
||||
'href' in control &&
|
||||
!!control.href
|
||||
) {
|
||||
return 'slButtonWithHRef';
|
||||
}
|
||||
|
||||
// all other components
|
||||
return 'standard';
|
||||
}
|
||||
Ładowanie…
Reference in New Issue