This commit is contained in:
Lea Verou
2024-12-17 13:00:57 -05:00
28 changed files with 353 additions and 1217 deletions

View File

@@ -16,7 +16,11 @@
</div>
</fieldset>
<wa-viewport-demo viewport="1000">
<iframe srcdoc="" id="page_slots_iframe" data-turbo="false" data-turbo-temporary></iframe>
<iframe srcdoc="" id="page_slots_iframe"></iframe>
</wa-viewport-demo>
</div>
<script type=module src="/assets/examples/page-demo/demo.js"></script>
<script type="module">
const cacheBust = new Date().toString()
import(`/assets/examples/page-demo/demo.js?${cacheBust}`)
</script>

View File

@@ -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">

View File

@@ -1,5 +1,6 @@
await customElements.whenDefined('wa-checkbox');
let container = document.getElementById('page_slots_demo');
let fieldset = container.querySelector('fieldset');
let iframe = container.querySelector('iframe');
let stylesheets = Array.from(document.querySelectorAll("link[rel=stylesheet][href^='/dist/']"))
@@ -10,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');
@@ -40,11 +41,3 @@ function render() {
}
fieldset?.addEventListener('input', render);
render();
//
// TODO - fix Turbo caching. When this is removed, visiting the <wa-page> docs via Turbo will cause the <iframe srcdoc>
// to not render. Even with this, there are console errors when leaving the page.
//
// NOTE - the iframe already has `data-turbo="false"` and `data-turbo-temporary` on it.
//
document.body.setAttribute('data-turbo', 'false');

View File

@@ -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;

View File

@@ -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()`.

View File

@@ -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).

View File

@@ -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

899
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
import { litCssPlugin } from '@konnorr/esbuild-plugin-lit-css';
import browserSync from 'browser-sync';
import chalk from 'chalk';
import { execSync } from 'child_process';
@@ -182,7 +181,7 @@ async function generateBundle() {
bundle: true,
splitting: true,
minify: false,
plugins: [replace({ __WEBAWESOME_VERSION__: version }), litCssPlugin()],
plugins: [replace({ __WEBAWESOME_VERSION__: version })],
loader: {
'.css': 'text',
},

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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;
});
});

View File

@@ -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);

View File

@@ -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;
});
});
});

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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 () => {

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
}

View File

@@ -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!);

View File

@@ -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"

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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);
});
}
});

View File

@@ -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;
}
}