mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Merge pull request #314 from shoelace-style/custom-states
Custom states
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
{# Slots #}
|
||||
{% if component.slots.length %}
|
||||
<h2>Slots</h2>
|
||||
<p>Learn more about <a href="/docs/usage/#slots">using slots</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -67,6 +68,7 @@
|
||||
{# Properties #}
|
||||
{% if component.properties.length %}
|
||||
<h2>Attributes & Properties</h2>
|
||||
<p>Learn more about <a href="/docs/usage/#attributes-and-properties">attributes and properties</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -113,6 +115,8 @@
|
||||
{# Methods #}
|
||||
{% if component.methods.length %}
|
||||
<h2>Methods</h2>
|
||||
<p>Learn more about <a href="/docs/usage/#methods">methods</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
<thead>
|
||||
@@ -143,34 +147,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# States #}
|
||||
{% if component.states.length %}
|
||||
<h2>States</h2>
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
<th class="table-selector">CSS selector</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for state in component.states %}
|
||||
<tr>
|
||||
<td class="table-name"><code>{{ state.name }}</code></td>
|
||||
<td class="table-description">{{ state.description | inlineMarkdown | safe }}</td>
|
||||
<td class="table-selector"><code>[data-state-{{ state.name }}]</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Events #}
|
||||
{% if component.events.length %}
|
||||
<h2>Events</h2>
|
||||
<p>Learn more about <a href="/docs/usage/#events">events</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -197,6 +177,7 @@
|
||||
{# Custom Properties #}
|
||||
{% if component.cssProperties.length %}
|
||||
<h2>CSS custom properties</h2>
|
||||
<p>Learn more about <a href="/docs/customizing/#custom-properties">CSS custom properties</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -225,9 +206,37 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Custom States #}
|
||||
{% if component.cssStates.length %}
|
||||
<h2>Custom States</h2>
|
||||
<p>Learn more about <a href="/docs/customizing/#custom-states">custom states</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
<th class="table-selector">CSS selector</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for state in component.cssStates %}
|
||||
<tr>
|
||||
<td class="table-name"><code>{{ state.name }}</code></td>
|
||||
<td class="table-description">{{ state.description | inlineMarkdown | safe }}</td>
|
||||
<td class="table-selector"><code>:state({{ state.name }})</code></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# CSS Parts #}
|
||||
{% if component.cssParts.length %}
|
||||
<h2>CSS parts</h2>
|
||||
<p>Learn more about <a href="/docs/customizing/#css-parts">CSS parts</a>.</p>
|
||||
|
||||
<div class="table-scroll">
|
||||
<table class="component-table">
|
||||
@@ -253,7 +262,7 @@
|
||||
{% if component.dependencies.length %}
|
||||
<h2>Dependencies</h2>
|
||||
<p>
|
||||
This component automatically imports the following elements. Subdependencies, if any exist, will also be included in this list.
|
||||
This component automatically imports the following elements. Sub-dependencies, if any exist, will also be included in this list.
|
||||
</p>
|
||||
|
||||
<ul class="dependency-list">
|
||||
|
||||
@@ -11,7 +11,7 @@ let includes = `${stylesheets}
|
||||
<link rel="stylesheet" href="/assets/examples/page-demo/page.css">`;
|
||||
|
||||
function render() {
|
||||
let slots = Array.from(fieldset.querySelectorAll('wa-checkbox[name=slot]:is([data-wa-checked])'));
|
||||
let slots = Array.from(fieldset.querySelectorAll('wa-checkbox[name=slot]:state(checked)'));
|
||||
let slotsHTML = slots
|
||||
.map(slot => {
|
||||
let name = slot.getAttribute('value');
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Smooth links
|
||||
document.addEventListener('click', event => {
|
||||
const link = event.target.closest('a');
|
||||
const id = (link?.hash ?? '').substr(1);
|
||||
|
||||
if (!link || link.getAttribute('data-smooth-link') === 'off') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const id = (link.hash ?? '').substr(1);
|
||||
|
||||
// Only handle smooth scroll if there's a hash and the link points to the current page
|
||||
if (id && link.pathname === window.location.pathname) {
|
||||
const target = document.getElementById(id);
|
||||
const headerHeight = document.querySelector('wa-page > header').clientHeight;
|
||||
|
||||
|
||||
@@ -67,6 +67,19 @@ Alternatively, you can set them inline directly on the element.
|
||||
|
||||
The custom properties exposed by each component can be found in the component's API documentation.
|
||||
|
||||
### Custom States
|
||||
|
||||
Components can expose custom states that allow you to style them based on their current condition using the `:state()` selector. Custom states provide a way to target specific component states that aren't covered by standard pseudo-classes like `:hover` or `:focus`.
|
||||
Here's an example that styles a checkbox that's checked.
|
||||
|
||||
```css
|
||||
wa-checkbox:state(checked) {
|
||||
outline: dotted 2px tomato;
|
||||
}
|
||||
```
|
||||
|
||||
Custom states can be combined with CSS parts and custom properties to create sophisticated customizations. The custom states exposed by each component can be found in the component's API documentation under the "Custom States" section.
|
||||
|
||||
### CSS Parts
|
||||
|
||||
CSS parts offer further flexibility to customize individual components. The "parts" exposed by each component can be targeted with the [CSS part selector](https://developer.mozilla.org/en-US/docs/Web/CSS/::part), or `::part()`.
|
||||
|
||||
@@ -165,12 +165,12 @@ Custom validation can be applied to any form control that supports the `setCusto
|
||||
|
||||
Due to the many ways form controls are used, Web Awesome doesn't provide out of the box validation styles for form controls as part of its default theme. Instead, the following attributes will be applied to reflect a control's validity as users interact with it. You can use them to create custom styles for any of the validation states you're interested in.
|
||||
|
||||
- `data-wa-required` - the form control is required
|
||||
- `data-wa-optional` - the form control is optional
|
||||
- `data-wa-invalid` - the form control is invalid
|
||||
- `data-wa-valid` - the form control is valid
|
||||
- `data-wa-user-invalid` - the form control is invalid and the user has interacted with it
|
||||
- `data-wa-user-valid` - the form control is valid and the user has interacted with it
|
||||
- `required` - the form control is required
|
||||
- `optional` - the form control is optional
|
||||
- `invalid` - the form control is invalid
|
||||
- `valid` - the form control is valid
|
||||
- `user-invalid` - the form control is invalid and the user has interacted with it
|
||||
- `user-valid` - the form control is valid and the user has interacted with it
|
||||
|
||||
These attributes map to the browser's built-in pseudo classes for validation: [`:required`](https://developer.mozilla.org/en-US/docs/Web/CSS/:required), [`:optional`](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional), [`:invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid), [`:valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:valid), [`:user-invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid), and [`:user-valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-valid).
|
||||
|
||||
|
||||
@@ -12,6 +12,17 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
|
||||
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
|
||||
:::
|
||||
|
||||
## Next
|
||||
|
||||
- Added `checked` and `disabled` custom states to `<wa-checkbox>` and `<wa-radio>`
|
||||
- Added `disabled`, `expanded`, `indeterminate`, and `selected` custom states to `<wa-tree-item>`
|
||||
- Renamed the `navigation-button--previous` and `navigation-button--next` parts to `navigation-button-previous` and `navigation-button-next` in `<wa-carousel>`
|
||||
- Renamed the `scroll-button--start` and `scroll-button--end` parts to `scroll-button-start` and `scroll-button-end` in `<wa-tab-group>`
|
||||
- Removed stateful CSS parts in favor of custom states
|
||||
- `<wa-checkbox>`: `control--checked`, `control--indeterminate`
|
||||
- `<wa-radio>`: `control--checked`
|
||||
- `<wa-tree-item>`: `item--disabled`, `item--expanded`, `item--indeterminate`, `item--selected`
|
||||
|
||||
## 3.0.0-alpha.5
|
||||
|
||||
- Added the Finnish translation
|
||||
|
||||
@@ -110,21 +110,21 @@
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.carousel__navigation-button--disabled {
|
||||
.carousel__navigation-button-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.carousel__navigation-button--disabled::part(base) {
|
||||
.carousel__navigation-button-disabled::part(base) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.carousel__navigation-button--previous {
|
||||
.carousel__navigation-button-previous {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.carousel__navigation-button--next {
|
||||
.carousel__navigation-button-next {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ describe('<wa-carousel>', () => {
|
||||
</wa-carousel>
|
||||
`);
|
||||
const expectedSlides = el.querySelectorAll('.expected');
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button-next')!;
|
||||
|
||||
// Act
|
||||
await clickOnElement(nextButton);
|
||||
@@ -359,7 +359,7 @@ describe('<wa-carousel>', () => {
|
||||
</wa-carousel>
|
||||
`);
|
||||
const expectedSlides = el.querySelectorAll('.expected');
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button-next')!;
|
||||
|
||||
// Act
|
||||
await clickOnElement(nextButton);
|
||||
@@ -506,7 +506,7 @@ describe('<wa-carousel>', () => {
|
||||
<wa-carousel-item>Node 3</wa-carousel-item>
|
||||
</wa-carousel>
|
||||
`);
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button-next')!;
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
await el.updateComplete;
|
||||
@@ -529,7 +529,7 @@ describe('<wa-carousel>', () => {
|
||||
<wa-carousel-item>Node 3</wa-carousel-item>
|
||||
</wa-carousel>
|
||||
`);
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button-next')!;
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
el.goToSlide(2, 'auto');
|
||||
@@ -556,7 +556,7 @@ describe('<wa-carousel>', () => {
|
||||
<wa-carousel-item>Node 3</wa-carousel-item>
|
||||
</wa-carousel>
|
||||
`);
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button-next')!;
|
||||
|
||||
el.goToSlide(2, 'auto');
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
@@ -597,7 +597,7 @@ describe('<wa-carousel>', () => {
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await el.updateComplete;
|
||||
|
||||
const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!;
|
||||
const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button-previous')!;
|
||||
sandbox.stub(el, 'previous');
|
||||
|
||||
await el.updateComplete;
|
||||
@@ -622,7 +622,7 @@ describe('<wa-carousel>', () => {
|
||||
`);
|
||||
|
||||
const previousButton: HTMLElement = el.shadowRoot!.querySelector(
|
||||
'.carousel__navigation-button--previous',
|
||||
'.carousel__navigation-button-previous',
|
||||
)!;
|
||||
sandbox.stub(el, 'previous');
|
||||
await el.updateComplete;
|
||||
@@ -648,7 +648,7 @@ describe('<wa-carousel>', () => {
|
||||
`);
|
||||
|
||||
const previousButton: HTMLElement = el.shadowRoot!.querySelector(
|
||||
'.carousel__navigation-button--previous',
|
||||
'.carousel__navigation-button-previous',
|
||||
)!;
|
||||
await el.updateComplete;
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ import styles from './carousel.css';
|
||||
* @csspart pagination-item--active - Applied when the item is active.
|
||||
* @csspart navigation - The navigation wrapper.
|
||||
* @csspart navigation-button - The navigation button.
|
||||
* @csspart navigation-button--previous - Applied to the previous button.
|
||||
* @csspart navigation-button--next - Applied to the next button.
|
||||
* @csspart navigation-button-previous - Applied to the previous button.
|
||||
* @csspart navigation-button-next - Applied to the next button.
|
||||
*
|
||||
* @cssproperty [--aspect-ratio=16/9] - The aspect ratio of each slide.
|
||||
* @cssproperty --navigation-color - The color of the navigation arrows.
|
||||
@@ -596,11 +596,11 @@ export default class WaCarousel extends WebAwesomeElement {
|
||||
? html`
|
||||
<div part="navigation" class="carousel__navigation">
|
||||
<button
|
||||
part="navigation-button navigation-button--previous"
|
||||
part="navigation-button navigation-button-previous"
|
||||
class="${classMap({
|
||||
'carousel__navigation-button': true,
|
||||
'carousel__navigation-button--previous': true,
|
||||
'carousel__navigation-button--disabled': !prevEnabled,
|
||||
'carousel__navigation-button-previous': true,
|
||||
'carousel__navigation-button-disabled': !prevEnabled,
|
||||
})}"
|
||||
aria-label="${this.localize.term('previousSlide')}"
|
||||
aria-controls="scroll-container"
|
||||
@@ -613,11 +613,11 @@ export default class WaCarousel extends WebAwesomeElement {
|
||||
</button>
|
||||
|
||||
<button
|
||||
part="navigation-button navigation-button--next"
|
||||
part="navigation-button navigation-button-next"
|
||||
class=${classMap({
|
||||
'carousel__navigation-button': true,
|
||||
'carousel__navigation-button--next': true,
|
||||
'carousel__navigation-button--disabled': !nextEnabled,
|
||||
'carousel__navigation-button-next': true,
|
||||
'carousel__navigation-button-disabled': !nextEnabled,
|
||||
})}
|
||||
aria-label="${this.localize.term('nextSlide')}"
|
||||
aria-controls="scroll-container"
|
||||
|
||||
@@ -199,17 +199,17 @@ describe('<wa-checkbox>', () => {
|
||||
|
||||
expect(checkbox.checkValidity()).to.be.false;
|
||||
expect(checkbox.checkValidity()).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(checkbox.hasCustomState('invalid')).to.be.true;
|
||||
expect(checkbox.hasCustomState('valid')).to.be.false;
|
||||
expect(checkbox.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(checkbox.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await clickOnElement(checkbox);
|
||||
await checkbox.updateComplete;
|
||||
await aTimeout(0);
|
||||
|
||||
expect(checkbox.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(checkbox.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(checkbox.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be invalid when required and unchecked', async () => {
|
||||
@@ -244,12 +244,12 @@ describe('<wa-checkbox>', () => {
|
||||
`);
|
||||
const checkbox = el.querySelector<WaCheckbox>('wa-checkbox')!;
|
||||
|
||||
expect(checkbox.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(checkbox.hasCustomState('required')).to.be.true;
|
||||
expect(checkbox.hasCustomState('optional')).to.be.false;
|
||||
expect(checkbox.hasCustomState('invalid')).to.be.true;
|
||||
expect(checkbox.hasCustomState('valid')).to.be.false;
|
||||
expect(checkbox.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(checkbox.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ import styles from './checkbox.css';
|
||||
*
|
||||
* @csspart base - The component's label .
|
||||
* @csspart control - The square container that wraps the checkbox's checked state.
|
||||
* @csspart control--checked - Matches the control part when the checkbox is checked.
|
||||
* @csspart control--indeterminate - Matches the control part when the checkbox is indeterminate.
|
||||
* @csspart checked-icon - The checked icon, a `<wa-icon>` element.
|
||||
* @csspart indeterminate-icon - The indeterminate icon, a `<wa-icon>` element.
|
||||
* @csspart label - The container that wraps the checkbox's label.
|
||||
@@ -53,6 +51,11 @@ import styles from './checkbox.css';
|
||||
* @cssproperty --box-shadow - The shadow effects around the edges of the checkbox.
|
||||
* @cssproperty --checked-icon-color - The color of the checkbox's icon.
|
||||
* @cssproperty --toggle-size - The size of the checkbox.
|
||||
*
|
||||
* @cssstate checked - Applied when the checkbox is checked.
|
||||
* @cssstate disabled - Applied when the checkbox is disabled.
|
||||
* @cssstate indeterminate - Applied when the checkbox is in an indeterminate state.
|
||||
*
|
||||
*/
|
||||
@customElement('wa-checkbox')
|
||||
export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
|
||||
@@ -157,20 +160,28 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
handleValueOrCheckedChange() {
|
||||
this.toggleCustomState('checked', this.checked);
|
||||
|
||||
// These @watch() commands seem to override the base element checks for changes, so we need to setValue for the form and and updateValidity()
|
||||
this.setValue(this.checked ? this.value : null, this._value);
|
||||
this.updateValidity();
|
||||
}
|
||||
|
||||
@watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true })
|
||||
@watch(['checked', 'indeterminate'])
|
||||
handleStateChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.input.indeterminate = this.indeterminate; // force a sync update
|
||||
if (this.hasUpdated) {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.input.indeterminate = this.indeterminate; // force a sync update
|
||||
}
|
||||
|
||||
this.toggleCustomState('checked', this.checked);
|
||||
this.toggleCustomState('indeterminate', this.indeterminate);
|
||||
this.updateValidity();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
this.toggleCustomState('disabled', this.disabled);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
|
||||
@@ -501,12 +501,12 @@ describe('<wa-color-picker>', () => {
|
||||
const grid = el.shadowRoot!.querySelector('[part~="grid"]')!;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.false;
|
||||
expect(el.hasCustomState('valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await clickOnElement(trigger);
|
||||
await aTimeout(500);
|
||||
@@ -514,8 +514,8 @@ describe('<wa-color-picker>', () => {
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
@@ -523,12 +523,12 @@ describe('<wa-color-picker>', () => {
|
||||
const trigger = el.shadowRoot!.querySelector('[part~="trigger"]')!;
|
||||
const grid = el.shadowRoot!.querySelector('[part~="grid"]')!;
|
||||
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.true;
|
||||
expect(el.hasCustomState('valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await clickOnElement(trigger);
|
||||
await aTimeout(500);
|
||||
@@ -536,8 +536,8 @@ describe('<wa-color-picker>', () => {
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,12 +122,12 @@ describe('<wa-input>', () => {
|
||||
const el = await fixture<WaInput>(html` <wa-input required value="a"></wa-input> `);
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.false;
|
||||
expect(el.hasCustomState('valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
@@ -137,19 +137,19 @@ describe('<wa-input>', () => {
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
const el = await fixture<WaInput>(html` <wa-input required></wa-input> `);
|
||||
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.true;
|
||||
expect(el.hasCustomState('valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
@@ -159,20 +159,20 @@ describe('<wa-input>', () => {
|
||||
el.blur();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
|
||||
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-input required></wa-input></form> `);
|
||||
const input = el.querySelector<WaInput>('wa-input')!;
|
||||
|
||||
expect(input.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(input.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(input.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(input.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(input.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(input.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(input.hasCustomState('required')).to.be.true;
|
||||
expect(input.hasCustomState('optional')).to.be.false;
|
||||
expect(input.hasCustomState('invalid')).to.be.true;
|
||||
expect(input.hasCustomState('valid')).to.be.false;
|
||||
expect(input.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(input.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,10 +229,10 @@ describe('<wa-input>', () => {
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.checkValidity()).to.be.false;
|
||||
expect(input.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(input.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(input.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(input.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(input.hasCustomState('invalid')).to.be.true;
|
||||
expect(input.hasCustomState('valid')).to.be.false;
|
||||
expect(input.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(input.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
input.focus();
|
||||
await sendKeys({ type: 'test' });
|
||||
@@ -240,8 +240,8 @@ describe('<wa-input>', () => {
|
||||
input.blur();
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(input.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(input.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(input.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
|
||||
|
||||
@@ -100,19 +100,19 @@ describe('<wa-radio-group>', () => {
|
||||
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
|
||||
|
||||
expect(radioGroup.checkValidity()).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-invalid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-valid')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('required')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('optional')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('invalid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('valid')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await clickOnElement(secondRadio);
|
||||
await secondRadio.updateComplete;
|
||||
|
||||
expect(radioGroup.checkValidity()).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
@@ -124,19 +124,19 @@ describe('<wa-radio-group>', () => {
|
||||
`);
|
||||
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
|
||||
|
||||
expect(radioGroup.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('required')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('optional')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('invalid')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('valid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await clickOnElement(secondRadio);
|
||||
radioGroup.value = '';
|
||||
await radioGroup.updateComplete;
|
||||
|
||||
expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
|
||||
@@ -150,12 +150,12 @@ describe('<wa-radio-group>', () => {
|
||||
`);
|
||||
const radioGroup = el.querySelector<WaRadioGroup>('wa-radio-group')!;
|
||||
|
||||
expect(radioGroup.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(radioGroup.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('required')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('optional')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('invalid')).to.be.true;
|
||||
expect(radioGroup.hasCustomState('valid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should show a constraint validation error when setCustomValidity() is called', async () => {
|
||||
|
||||
@@ -24,7 +24,6 @@ import styles from './radio.css';
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart control - The circular container that wraps the radio's checked state.
|
||||
* @csspart control--checked - The radio control when the radio is checked.
|
||||
* @csspart checked-icon - The checked icon.
|
||||
* @csspart label - The container that wraps the radio's label.
|
||||
*
|
||||
@@ -38,6 +37,9 @@ import styles from './radio.css';
|
||||
* @cssproperty --checked-icon-color - The color of the radio's checked icon.
|
||||
* @cssproperty --checked-icon-scale - The size of the checked icon relative to the radio.
|
||||
* @cssproperty --toggle-size - The size of the radio.
|
||||
*
|
||||
* @cssstate checked - Applied when the control is checked.
|
||||
* @cssstate disabled - Applied when the control is disabled.
|
||||
*/
|
||||
@customElement('wa-radio')
|
||||
export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
@@ -92,6 +94,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.toggleCustomState('checked', this.checked);
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
this.tabIndex = this.checked ? 0 : -1;
|
||||
}
|
||||
@@ -105,6 +108,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.toggleCustomState('disabled', this.disabled);
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@@ -124,7 +128,7 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
'radio--disabled': this.disabled,
|
||||
})}
|
||||
>
|
||||
<span part="${`control${this.checked ? ' control--checked' : ''}`}" class="radio__control">
|
||||
<span part="control" class="radio__control">
|
||||
${this.checked
|
||||
? html`
|
||||
<svg
|
||||
|
||||
@@ -164,18 +164,18 @@ describe('<wa-range>', () => {
|
||||
await range.updateComplete;
|
||||
|
||||
expect(range.checkValidity()).to.be.false;
|
||||
expect(range.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(range.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(range.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(range.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(range.hasCustomState('invalid')).to.be.true;
|
||||
expect(range.hasCustomState('valid')).to.be.false;
|
||||
expect(range.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(range.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await clickOnElement(range);
|
||||
await range.updateComplete;
|
||||
range.blur();
|
||||
await range.updateComplete;
|
||||
|
||||
expect(range.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(range.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(range.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(range.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
|
||||
@@ -185,10 +185,10 @@ describe('<wa-range>', () => {
|
||||
range.setCustomValidity('Invalid value');
|
||||
await range.updateComplete;
|
||||
|
||||
expect(range.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(range.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(range.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(range.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(range.hasCustomState('invalid')).to.be.true;
|
||||
expect(range.hasCustomState('valid')).to.be.false;
|
||||
expect(range.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(range.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
|
||||
|
||||
@@ -330,12 +330,12 @@ describe('<wa-select>', () => {
|
||||
const secondOption = el.querySelectorAll('wa-option')[1];
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.false;
|
||||
expect(el.hasCustomState('valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await el.show();
|
||||
await clickOnElement(secondOption);
|
||||
@@ -344,8 +344,8 @@ describe('<wa-select>', () => {
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
@@ -358,12 +358,12 @@ describe('<wa-select>', () => {
|
||||
`);
|
||||
const secondOption = el.querySelectorAll('wa-option')[1];
|
||||
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.true;
|
||||
expect(el.hasCustomState('valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
await el.show();
|
||||
await clickOnElement(secondOption);
|
||||
@@ -372,8 +372,8 @@ describe('<wa-select>', () => {
|
||||
el.blur();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
|
||||
@@ -388,12 +388,12 @@ describe('<wa-select>', () => {
|
||||
`);
|
||||
const select = el.querySelector<WaSelect>('wa-select')!;
|
||||
|
||||
expect(select.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(select.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(select.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(select.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(select.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(select.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(select.hasCustomState('required')).to.be.true;
|
||||
expect(select.hasCustomState('optional')).to.be.false;
|
||||
expect(select.hasCustomState('invalid')).to.be.true;
|
||||
expect(select.hasCustomState('valid')).to.be.false;
|
||||
expect(select.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(select.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -230,12 +230,12 @@ describe('<wa-switch>', () => {
|
||||
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-switch required></wa-switch></form> `);
|
||||
const waSwitch = el.querySelector<WaSwitch>('wa-switch')!;
|
||||
|
||||
expect(waSwitch.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(waSwitch.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(waSwitch.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(waSwitch.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(waSwitch.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(waSwitch.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(waSwitch.hasCustomState('required')).to.be.true;
|
||||
expect(waSwitch.hasCustomState('optional')).to.be.false;
|
||||
expect(waSwitch.hasCustomState('invalid')).to.be.true;
|
||||
expect(waSwitch.hasCustomState('valid')).to.be.false;
|
||||
expect(waSwitch.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(waSwitch.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -40,20 +40,20 @@
|
||||
width: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.tab-group__scroll-button--start {
|
||||
.tab-group__scroll-button-start {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tab-group__scroll-button--end {
|
||||
.tab-group__scroll-button-end {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tab-group--rtl .tab-group__scroll-button--start {
|
||||
.tab-group--rtl .tab-group__scroll-button-start {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tab-group--rtl .tab-group__scroll-button--end {
|
||||
.tab-group--rtl .tab-group__scroll-button-end {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('<wa-tab-group>', () => {
|
||||
expect(isElementVisibleFromOverflow(tabGroup, firstTab!)).to.be.true;
|
||||
expect(isElementVisibleFromOverflow(tabGroup, lastTab!)).to.be.false;
|
||||
|
||||
const scrollToRightButton = tabGroup.shadowRoot?.querySelector('wa-icon-button[part*="scroll-button--end"]');
|
||||
const scrollToRightButton = tabGroup.shadowRoot?.querySelector('wa-icon-button[part*="scroll-button-end"]');
|
||||
expect(scrollToRightButton).not.to.be.null;
|
||||
await clickOnElement(scrollToRightButton!);
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ import styles from './tab-group.css';
|
||||
* @csspart tabs - The container that wraps the tabs.
|
||||
* @csspart body - The tab group's body where tab panels are slotted in.
|
||||
* @csspart scroll-button - The previous/next scroll buttons that show when tabs are scrollable, an `<wa-icon-button>`.
|
||||
* @csspart scroll-button--start - The starting scroll button.
|
||||
* @csspart scroll-button--end - The ending scroll button.
|
||||
* @csspart scroll-button-start - The starting scroll button.
|
||||
* @csspart scroll-button-end - The ending scroll button.
|
||||
* @csspart scroll-button__base - The scroll button's exported `base` part.
|
||||
*
|
||||
* @cssproperty --indicator-color - The color of the active tab indicator.
|
||||
@@ -392,9 +392,9 @@ export default class WaTabGroup extends WebAwesomeElement {
|
||||
${this.hasScrollControls
|
||||
? html`
|
||||
<wa-icon-button
|
||||
part="scroll-button scroll-button--start"
|
||||
part="scroll-button scroll-button-start"
|
||||
exportparts="base:scroll-button__base"
|
||||
class="tab-group__scroll-button tab-group__scroll-button--start"
|
||||
class="tab-group__scroll-button tab-group__scroll-button-start"
|
||||
name=${isRtl ? 'chevron-right' : 'chevron-left'}
|
||||
library="system"
|
||||
variant="solid"
|
||||
@@ -414,9 +414,9 @@ export default class WaTabGroup extends WebAwesomeElement {
|
||||
${this.hasScrollControls
|
||||
? html`
|
||||
<wa-icon-button
|
||||
part="scroll-button scroll-button--end"
|
||||
part="scroll-button scroll-button-end"
|
||||
exportparts="base:scroll-button__base"
|
||||
class="tab-group__scroll-button tab-group__scroll-button--end"
|
||||
class="tab-group__scroll-button tab-group__scroll-button-end"
|
||||
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
||||
library="system"
|
||||
variant="solid"
|
||||
|
||||
@@ -144,12 +144,12 @@ describe('<wa-textarea>', () => {
|
||||
const el = await fixture<WaTextarea>(html` <wa-textarea required value="a"></wa-textarea> `);
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.false;
|
||||
expect(el.hasCustomState('valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await sendKeys({ press: 'b' });
|
||||
@@ -158,19 +158,19 @@ describe('<wa-textarea>', () => {
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.true;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
const el = await fixture<WaTextarea>(html` <wa-textarea required></wa-textarea> `);
|
||||
|
||||
expect(el.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('required')).to.be.true;
|
||||
expect(el.hasCustomState('optional')).to.be.false;
|
||||
expect(el.hasCustomState('invalid')).to.be.true;
|
||||
expect(el.hasCustomState('valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await sendKeys({ press: 'a' });
|
||||
@@ -179,8 +179,8 @@ describe('<wa-textarea>', () => {
|
||||
el.blur();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(el.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(el.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
|
||||
@@ -189,12 +189,12 @@ describe('<wa-textarea>', () => {
|
||||
`);
|
||||
const textarea = el.querySelector<WaTextarea>('wa-textarea')!;
|
||||
|
||||
expect(textarea.hasAttribute('data-wa-required')).to.be.true;
|
||||
expect(textarea.hasAttribute('data-wa-optional')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(textarea.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(textarea.hasCustomState('required')).to.be.true;
|
||||
expect(textarea.hasCustomState('optional')).to.be.false;
|
||||
expect(textarea.hasCustomState('invalid')).to.be.true;
|
||||
expect(textarea.hasCustomState('valid')).to.be.false;
|
||||
expect(textarea.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(textarea.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,10 +222,10 @@ describe('<wa-textarea>', () => {
|
||||
await textarea.updateComplete;
|
||||
|
||||
expect(textarea.checkValidity()).to.be.false;
|
||||
expect(textarea.hasAttribute('data-wa-invalid')).to.be.true;
|
||||
expect(textarea.hasAttribute('data-wa-valid')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-wa-user-invalid')).to.be.false;
|
||||
expect(textarea.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(textarea.hasCustomState('invalid')).to.be.true;
|
||||
expect(textarea.hasCustomState('valid')).to.be.false;
|
||||
expect(textarea.hasCustomState('user-invalid')).to.be.false;
|
||||
expect(textarea.hasCustomState('user-valid')).to.be.false;
|
||||
|
||||
textarea.focus();
|
||||
await sendKeys({ type: 'test' });
|
||||
@@ -233,8 +233,8 @@ describe('<wa-textarea>', () => {
|
||||
textarea.blur();
|
||||
await textarea.updateComplete;
|
||||
|
||||
expect(textarea.hasAttribute('data-wa-user-invalid')).to.be.true;
|
||||
expect(textarea.hasAttribute('data-wa-user-valid')).to.be.false;
|
||||
expect(textarea.hasCustomState('user-invalid')).to.be.true;
|
||||
expect(textarea.hasCustomState('user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
|
||||
|
||||
@@ -44,10 +44,6 @@ import styles from './tree-item.css';
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart item - The tree item's container. This element wraps everything except slotted tree item children.
|
||||
* @csspart item--disabled - Applied when the tree item is disabled.
|
||||
* @csspart item--expanded - Applied when the tree item is expanded.
|
||||
* @csspart item--indeterminate - Applied when the selection is indeterminate.
|
||||
* @csspart item--selected - Applied when the tree item is selected.
|
||||
* @csspart indentation - The tree item's indentation container.
|
||||
* @csspart expand-button - The container that wraps the tree item's expand button and spinner.
|
||||
* @csspart spinner - The spinner that shows when a lazy tree item is in the loading state.
|
||||
@@ -57,8 +53,6 @@ import styles from './tree-item.css';
|
||||
* @csspart checkbox - The checkbox that shows when using multiselect.
|
||||
* @csspart checkbox__base - The checkbox's exported `base` part.
|
||||
* @csspart checkbox__control - The checkbox's exported `control` part.
|
||||
* @csspart checkbox__control--checked - The checkbox's exported `control--checked` part.
|
||||
* @csspart checkbox__control--indeterminate - The checkbox's exported `control--indeterminate` part.
|
||||
* @csspart checkbox__checked-icon - The checkbox's exported `checked-icon` part.
|
||||
* @csspart checkbox__indeterminate-icon - The checkbox's exported `indeterminate-icon` part.
|
||||
* @csspart checkbox__label - The checkbox's exported `label` part.
|
||||
@@ -68,6 +62,11 @@ import styles from './tree-item.css';
|
||||
* @cssproperty --expand-button-color - The color of the expand button.
|
||||
* @cssproperty [--show-duration=200ms] - The animation duration when expanding tree items.
|
||||
* @cssproperty [--hide-duration=200ms] - The animation duration when collapsing tree items.
|
||||
*
|
||||
* @cssstate disabled - Applied when the tree item is disabled.
|
||||
* @cssstate expanded - Applied when the tree item is expanded.
|
||||
* @cssstate indeterminate - Applied when the selection is indeterminate.
|
||||
* @cssstate selected - Applied when the tree item is selected.
|
||||
*/
|
||||
@customElement('wa-tree-item')
|
||||
export default class WaTreeItem extends WebAwesomeElement {
|
||||
@@ -189,11 +188,23 @@ export default class WaTreeItem extends WebAwesomeElement {
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
this.toggleCustomState('disabled', this.disabled);
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@watch('expanded')
|
||||
handleExpandedState() {
|
||||
this.toggleCustomState('expanded', this.expanded);
|
||||
}
|
||||
|
||||
@watch('indeterminate')
|
||||
handleIndeterminateStateChange() {
|
||||
this.toggleCustomState('indeterminate', this.indeterminate);
|
||||
}
|
||||
|
||||
@watch('selected')
|
||||
handleSelectedChange() {
|
||||
this.toggleCustomState('selected', this.selected);
|
||||
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@@ -251,16 +262,7 @@ export default class WaTreeItem extends WebAwesomeElement {
|
||||
'tree-item--rtl': this.localize.dir() === 'rtl',
|
||||
})}"
|
||||
>
|
||||
<div
|
||||
class="tree-item__item"
|
||||
part="
|
||||
item
|
||||
${this.disabled ? 'item--disabled' : ''}
|
||||
${this.expanded ? 'item--expanded' : ''}
|
||||
${this.indeterminate ? 'item--indeterminate' : ''}
|
||||
${this.selected ? 'item--selected' : ''}
|
||||
"
|
||||
>
|
||||
<div class="tree-item__item" part="item">
|
||||
<div class="tree-item__indentation" part="indentation"></div>
|
||||
|
||||
<div
|
||||
@@ -291,8 +293,6 @@ export default class WaTreeItem extends WebAwesomeElement {
|
||||
exportparts="
|
||||
base:checkbox__base,
|
||||
control:checkbox__control,
|
||||
control--checked:checkbox__control--checked,
|
||||
control--indeterminate:checkbox__control--indeterminate,
|
||||
checked-icon:checkbox__checked-icon,
|
||||
indeterminate-icon:checkbox__indeterminate-icon,
|
||||
label:checkbox__label
|
||||
|
||||
@@ -184,7 +184,7 @@ function runAllValidityTests(
|
||||
control.customError = 'MyError';
|
||||
await control.updateComplete;
|
||||
expect(control.validity.valid).to.equal(false);
|
||||
expect(control.hasAttribute('data-wa-invalid')).to.equal(true);
|
||||
expect(control.hasCustomState('invalid')).to.equal(true);
|
||||
expect(control.validationMessage).to.equal('MyError');
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ function runAllValidityTests(
|
||||
// expect(control.validity.valid).to.equal(true)
|
||||
control.setAttribute('custom-error', 'MyError');
|
||||
await control.updateComplete;
|
||||
expect(control.hasAttribute('data-wa-invalid')).to.equal(true);
|
||||
expect(control.hasCustomState('invalid')).to.equal(true);
|
||||
expect(control.validationMessage).to.equal('MyError');
|
||||
});
|
||||
|
||||
@@ -207,7 +207,7 @@ function runAllValidityTests(
|
||||
expect(control.disabled).to.equal(true);
|
||||
// expect(control.hasAttribute("disabled")).to.equal(false)
|
||||
expect(control.matches(':disabled')).to.equal(true);
|
||||
expect(control.hasAttribute('data-wa-disabled')).to.equal(true);
|
||||
expect(control.hasCustomState('disabled')).to.equal(true);
|
||||
|
||||
fieldset.disabled = false;
|
||||
|
||||
@@ -215,7 +215,7 @@ function runAllValidityTests(
|
||||
expect(control.disabled).to.equal(false);
|
||||
expect(control.hasAttribute('disabled')).to.equal(false);
|
||||
expect(control.matches(':disabled')).to.equal(false);
|
||||
expect(control.hasAttribute('data-wa-disabled')).to.equal(false);
|
||||
expect(control.hasCustomState('disabled')).to.equal(false);
|
||||
});
|
||||
|
||||
// it("This is the one edge case with ':disabled'. If you disable a fieldset, and then disable the element directly, it will not reflect the disabled attribute.", async () => {
|
||||
@@ -246,7 +246,7 @@ function runAllValidityTests(
|
||||
expect(control.disabled).to.equal(true);
|
||||
expect(control.hasAttribute('disabled')).to.equal(true);
|
||||
expect(control.matches(':disabled')).to.equal(true);
|
||||
expect(control.hasAttribute('data-wa-disabled')).to.equal(true);
|
||||
expect(control.hasCustomState('disabled')).to.equal(true);
|
||||
|
||||
control.disabled = false;
|
||||
await control.updateComplete;
|
||||
@@ -254,7 +254,7 @@ function runAllValidityTests(
|
||||
expect(control.disabled).to.equal(false);
|
||||
expect(control.hasAttribute('disabled')).to.equal(false);
|
||||
expect(control.matches(':disabled')).to.equal(false);
|
||||
expect(control.hasAttribute('data-wa-disabled')).to.equal(false);
|
||||
expect(control.hasCustomState('disabled')).to.equal(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,18 @@ import componentStyles from '../styles/shadow/component.css';
|
||||
import { CustomErrorValidator } from './validators/custom-error-validator.js';
|
||||
|
||||
export default class WebAwesomeElement extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
try {
|
||||
this.internals = this.attachInternals();
|
||||
} catch (_e) {
|
||||
/* Need to tell people if they need a polyfill. */
|
||||
/* eslint-disable-next-line */
|
||||
console.error('Element internals are not supported in your browser. Consider using a polyfill');
|
||||
}
|
||||
}
|
||||
|
||||
// Make localization attributes reactive
|
||||
@property() dir: string;
|
||||
@property() lang: string;
|
||||
@@ -40,6 +52,8 @@ export default class WebAwesomeElement extends LitElement {
|
||||
// Store the constructor value of all `static properties = {}`
|
||||
initialReflectedProperties: Map<string, unknown> = new Map();
|
||||
|
||||
internals: ElementInternals;
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
||||
if (!this.#hasRecordedInitialProperties) {
|
||||
(this.constructor as typeof WebAwesomeElement).elementProperties.forEach(
|
||||
@@ -98,6 +112,44 @@ export default class WebAwesomeElement extends LitElement {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if states are supported by the element */
|
||||
private hasStatesSupport(): boolean {
|
||||
return this.internals?.states instanceof Set;
|
||||
}
|
||||
|
||||
/** Adds a custom state to the element. */
|
||||
addCustomState(state: string) {
|
||||
if (this.hasStatesSupport()) {
|
||||
this.internals.states.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes a custom state from the element. */
|
||||
deleteCustomState(state: string) {
|
||||
if (this.hasStatesSupport()) {
|
||||
this.internals.states.delete(state);
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles a custom state on the element. */
|
||||
toggleCustomState(state: string, force?: boolean) {
|
||||
if (typeof force === 'boolean') {
|
||||
if (force) {
|
||||
this.addCustomState(state);
|
||||
} else {
|
||||
this.deleteCustomState(state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleCustomState(state, !this.hasCustomState(state));
|
||||
}
|
||||
|
||||
/** Determines if the element has the specified custom state. */
|
||||
hasCustomState(state: string): boolean {
|
||||
return this.hasStatesSupport() ? this.internals.states.has(state) : false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Validator<T extends WebAwesomeFormAssociatedElement = WebAwesomeFormAssociatedElement> {
|
||||
@@ -191,9 +243,6 @@ export class WebAwesomeFormAssociatedElement
|
||||
|
||||
required: boolean = false;
|
||||
|
||||
// Form validation methods
|
||||
internals: ElementInternals;
|
||||
|
||||
assumeInteractionOn: string[] = ['wa-input'];
|
||||
|
||||
// Additional
|
||||
@@ -213,14 +262,6 @@ export class WebAwesomeFormAssociatedElement
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
try {
|
||||
this.internals = this.attachInternals();
|
||||
} catch (_e) {
|
||||
/* Need to tell people if they need a polyfill. */
|
||||
/* eslint-disable-next-line */
|
||||
console.error('Element internals are not supported in your browser. Consider using a polyfill');
|
||||
}
|
||||
|
||||
if (!isServer) {
|
||||
// eslint-disable-next-line
|
||||
this.addEventListener('invalid', this.emitInvalid);
|
||||
@@ -490,55 +531,4 @@ export class WebAwesomeFormAssociatedElement
|
||||
|
||||
this.setValidity(flags, finalMessage, formControl);
|
||||
}
|
||||
|
||||
// Custom states
|
||||
addCustomState(state: string) {
|
||||
try {
|
||||
(this.internals.states as Set<string>).add(state);
|
||||
} catch (_) {
|
||||
// Without this, test suite errors.
|
||||
} finally {
|
||||
this.setAttribute(`data-wa-${state}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
deleteCustomState(state: string) {
|
||||
try {
|
||||
(this.internals.states as Set<string>).delete(state);
|
||||
} catch (_) {
|
||||
// Without this, test suite errors.
|
||||
} finally {
|
||||
this.removeAttribute(`data-wa-${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCustomState(state: string, force: boolean) {
|
||||
if (force) {
|
||||
this.addCustomState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
this.deleteCustomState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleCustomState(state, !this.hasCustomState(state));
|
||||
}
|
||||
|
||||
hasCustomState(state: string) {
|
||||
let bool = false;
|
||||
|
||||
try {
|
||||
bool = (this.internals.states as Set<string>).has(state);
|
||||
} catch (_) {
|
||||
// Without this, test suite errors.
|
||||
} finally {
|
||||
if (!bool) {
|
||||
bool = this.hasAttribute(`data-wa-${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
return bool;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user