Compare commits

..

1 Commits

Author SHA1 Message Date
Cory LaViska
c1d8a986d0 update changelog 2025-07-03 12:04:53 -04:00
7 changed files with 72 additions and 159 deletions

View File

@@ -7,7 +7,7 @@ category: Form Controls
```html {.example}
<wa-select>
<wa-option value="">Option 1</wa-option>
<wa-option value="option-1">Option 1</wa-option>
<wa-option value="option-2">Option 2</wa-option>
<wa-option value="option-3">Option 3</wa-option>
<wa-option value="option-4">Option 4</wa-option>
@@ -366,7 +366,6 @@ Here's a comprehensive example showing different lazy loading scenarios:
const option = document.createElement('wa-option');
option.setAttribute('value', 'foo');
option.selected = true
option.innerText = 'Foo';
// For the multiple select with existing selected options, make the new option selected
@@ -403,4 +402,4 @@ Here's a comprehensive example showing different lazy loading scenarios:
:::info
The key principle is that the select component prioritizes user interactions and explicit selections over programmatic changes, ensuring a predictable user experience even with dynamically loaded content.
:::
:::

View File

@@ -10,13 +10,6 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
## 3.0.0-beta.2
### New Features {data-no-outline}
- Added `.wa-hover-rows` to native styles to opt-in to highlighting table rows on hover.
### Bug Fixes and Improvements {data-no-outline}
- Fixed a bug in `<wa-select>` with options that had blank string values. [pr:1136]
- Added `.wa-hover-rows` to native styles to opt-in to highlighting table rows on hover [pr:1111]
- Added missing changelog entries for beta.1 [pr:1117]
- Fixed a bug in `<wa-dropdown>` that prevented the menu from flipping/shifting to keep the menu in the viewport [pr:1122]
@@ -374,4 +367,4 @@ Many of these changes and improvements were the direct result of feedback from u
</details>
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)

View File

@@ -196,9 +196,8 @@ describe('<wa-details>', () => {
await first.show();
await second.show();
// height + 32 (padding probably?)
expect(firstBody.clientHeight).to.equal(232);
expect(secondBody.clientHeight).to.equal(432);
expect(firstBody.clientHeight).to.equal(200);
expect(secondBody.clientHeight).to.equal(400);
});
});
}

View File

@@ -1,5 +1,5 @@
import { aTimeout, expect, waitUntil } from '@open-wc/testing';
import { resetMouse, sendKeys } from '@web/test-runner-commands';
import { sendKeys } from '@web/test-runner-commands';
import { html } from 'lit';
import sinon from 'sinon';
import { fixtures } from '../../internal/test/fixture.js';
@@ -200,22 +200,21 @@ describe('<wa-select>', () => {
</wa-select>
`);
const option2 = el.querySelectorAll('wa-option')[1];
const handler = sinon.spy((_event: InputEvent | Event) => {});
const handler = sinon.spy((event: CustomEvent) => {
if (el.validationMessage) {
expect.fail(`Validation message should be empty when ${event.type} is emitted and a value is set`);
}
});
el.addEventListener('change', handler);
el.addEventListener('input', handler);
await clickOnElement(el);
await aTimeout(500);
await el.updateComplete;
await aTimeout(100);
await clickOnElement(option2);
await el.updateComplete;
await aTimeout(500);
// debugger
expect(handler).to.be.calledTwice;
expect(el.value).to.equal(option2.value);
});
});
@@ -649,8 +648,8 @@ describe('<wa-select>', () => {
const el = form.querySelector<WaSelect>('wa-select')!;
expect(el.defaultValue).to.equal('option-1');
expect(el.value).to.equal(null);
expect(new FormData(form).get('select')).equal(null);
expect(el.value).to.equal('');
expect(new FormData(form).get('select')).equal('');
const option = document.createElement('wa-option');
option.value = 'option-1';
@@ -698,8 +697,8 @@ describe('<wa-select>', () => {
);
const el = form.querySelector<WaSelect>('wa-select')!;
expect(el.value).to.equal(null);
expect(new FormData(form).get('select')).to.equal(null);
expect(el.value).to.equal('');
expect(new FormData(form).get('select')).to.equal('');
const option = document.createElement('wa-option');
option.value = 'foo';
@@ -772,12 +771,12 @@ describe('<wa-select>', () => {
);
const el = form.querySelector<WaSelect>('wa-select')!;
expect(el.value).to.equal(null);
expect(el.value).to.equal('');
el.defaultValue = 'foo';
el.value = 'foo';
await aTimeout(10);
await el.updateComplete;
expect(el.value).to.equal(null);
expect(el.value).to.equal('');
const option = document.createElement('wa-option');
option.value = 'foo';
@@ -889,43 +888,6 @@ describe('<wa-select>', () => {
// Get the popup element and check its active state
expect(popup?.active).to.be.true;
});
// https://github.com/shoelace-style/webawesome/issues/1131
// new test, failing only in CI
it.skip('Should work properly with empty values on select', async () => {
const el = await fixture<WaSelect>(html`
<wa-select label="Select one">
<wa-option value="">Blank Option</wa-option>
<wa-option value="option-2">Option 2</wa-option>
<wa-option value="option-3">Option 3</wa-option>
</wa-select>
`);
await resetMouse();
await el.show();
const options = el.querySelectorAll('wa-option');
await aTimeout(100);
// firefox doesnt like clicks -.-
await clickOnElement(options[0]);
await resetMouse();
await el.updateComplete;
expect(el.value).to.equal('');
await aTimeout(100);
await clickOnElement(options[1]);
await resetMouse();
await el.updateComplete;
await aTimeout(100);
expect(el.value).to.equal('option-2');
await clickOnElement(options[0]);
await resetMouse();
await el.updateComplete;
await aTimeout(100);
expect(el.value).to.equal('');
await resetMouse();
});
});
}
});

View File

@@ -114,22 +114,22 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
@state() displayLabel = '';
@state() currentOption: WaOption;
@state() selectedOptions: WaOption[] = [];
@state() optionValues: Set<string | null> | undefined;
@state() optionValues: Set<string> | undefined;
/** The name of the select, submitted as a name/value pair with form data. */
@property() name = '';
private _defaultValue: null | string | string[] = null;
private _defaultValue: string | string[] = '';
@property({
attribute: false,
})
set defaultValue(val: null | string | string[]) {
set defaultValue(val: string | string[]) {
this._defaultValue = this.convertDefaultValue(val);
}
get defaultValue() {
return this.convertDefaultValue(this._defaultValue);
return this._defaultValue;
}
/**
@@ -147,40 +147,35 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
return val;
}
private _value: string[] | undefined | null;
private _value: string[] | undefined;
/** The select's value. This will be a string for single select or an array for multi-select. */
@property({ attribute: 'value', reflect: false })
set value(val: string | string[] | null) {
set value(val: string | string[]) {
let oldValue = this.value;
if ((val as any) instanceof FormData) {
val = (val as unknown as FormData).getAll(this.name) as string[];
}
if (val != null && !Array.isArray(val)) {
if (!Array.isArray(val)) {
val = [val];
}
this._value = val ?? null;
this._value = val;
let newValue = this.value;
if (newValue !== oldValue) {
this.valueHasChanged = true;
this.requestUpdate('value', oldValue);
}
}
get value() {
let value = this._value ?? this.defaultValue ?? null;
let value = this._value ?? this.defaultValue;
value = Array.isArray(value) ? value : [value];
let optionsChanged = !this.optionValues;
if (value != null) {
value = Array.isArray(value) ? value : [value];
}
if (value == null) {
this.optionValues = new Set(null);
} else {
if (optionsChanged) {
this.optionValues = new Set(
this.getAllOptions()
.filter(option => !option.disabled)
@@ -189,11 +184,11 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
}
// Drop values not in the DOM
let ret: null | string | string[] = value;
if (value != null) {
ret = value.filter(v => this.optionValues!.has(v));
ret = this.multiple ? ret : ret[0];
ret = ret ?? null;
let ret: string | string[] = value.filter(v => this.optionValues!.has(v));
ret = this.multiple ? ret : (ret[0] ?? '');
if (optionsChanged) {
this.requestUpdate('value');
}
return ret;
@@ -296,17 +291,16 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Because this is a form control, it shouldn't be opened initially
this.open = false;
}
private updateDefaultValue() {
const allOptions = this.getAllOptions();
const defaultSelectedOptions = allOptions.filter(el => el.hasAttribute('selected') || el.defaultSelected);
if (defaultSelectedOptions.length > 0) {
const selectedValues = defaultSelectedOptions.map(el => el.value);
this._defaultValue = this.multiple ? selectedValues : selectedValues[0];
}
if (this.hasAttribute('value')) {
this._defaultValue = this.getAttribute('value') || null;
if (!this._defaultValue) {
const allOptions = this.getAllOptions();
const selectedOptions = allOptions.filter(el => el.selected || el.defaultSelected);
if (selectedOptions.length > 0) {
const selectedValues = selectedOptions.map(el => el.value);
this._defaultValue = this.multiple ? selectedValues : selectedValues[0];
} else if (this.hasAttribute('value')) {
this._defaultValue = this.getAttribute('value') || '';
}
}
}
@@ -381,7 +375,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// If it is open, update the value based on the current selection and close it
if (this.currentOption && !this.currentOption.disabled) {
this.valueHasChanged = true;
this.hasInteracted = true;
if (this.multiple) {
this.toggleOptionSelection(this.currentOption);
} else {
@@ -513,7 +506,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
private handleClearClick(event: MouseEvent) {
event.stopPropagation();
if (this.value !== null) {
if (this.value !== '') {
this.setSelectedOptions([]);
this.displayInput.focus({ preventScroll: true });
@@ -535,11 +528,10 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
private handleOptionClick(event: MouseEvent) {
const target = event.target as HTMLElement;
const option = target.closest('wa-option');
const oldValue = this.value;
if (option && !option.disabled) {
this.hasInteracted = true;
this.valueHasChanged = true;
if (this.multiple) {
this.toggleOptionSelection(option);
} else {
@@ -549,13 +541,13 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Set focus after updating so the value is announced by screen readers
this.updateComplete.then(() => this.displayInput.focus({ preventScroll: true }));
this.requestUpdate('value');
// Emit after updating
this.updateComplete.then(() => {
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
});
if (this.value !== oldValue) {
// Emit after updating
this.updateComplete.then(() => {
this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
});
}
if (!this.multiple) {
this.hide();
@@ -574,22 +566,18 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.optionValues = undefined; // dirty the value so it gets recalculated
// Update defaultValue if it hasn't been explicitly set and we have selected options
this.updateDefaultValue();
let value = this.value;
if (value == null || (!this.valueHasChanged && !this.hasInteracted)) {
this.selectionChanged();
return;
if (!this._defaultValue && !this.hasUpdated) {
const selectedOptions = allOptions.filter(el => el.selected || el.defaultSelected);
if (selectedOptions.length > 0) {
const selectedValues = selectedOptions.map(el => el.value);
this._defaultValue = this.multiple ? selectedValues : selectedValues[0];
}
}
if (!Array.isArray(value)) {
value = [value];
}
const value = this.value;
// Select only the options that match the new value
const selectedOptions = allOptions.filter(el => value.includes(el.value));
this.setSelectedOptions(selectedOptions);
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value) || el.selected));
}
private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
@@ -702,36 +690,29 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Update selected options cache
this.selectedOptions = options.filter(el => {
if (!this.hasInteracted && !this.valueHasChanged) {
const defaultValue = this.defaultValue;
const defaultValues = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
return el.hasAttribute('selected') || el.defaultSelected || el.selected || defaultValues?.includes(el.value);
}
return el.selected;
});
let selectedValues = new Set(this.selectedOptions.map(el => el.value));
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
// Note that options NOT present in the DOM will be moved to the end after this
if (selectedValues.size > 0 || this._value) {
const oldValue = this._value;
if (this._value == null) {
if (!this._value) {
// First time it's set
let value = this.defaultValue ?? [];
this._value = Array.isArray(value) ? value : [value];
}
// Filter out values that are in the DOM
this._value = this._value?.filter(value => !this.optionValues?.has(value)) ?? null;
this._value?.unshift(...selectedValues);
this._value = this._value.filter(value => !this.optionValues?.has(value));
this._value.unshift(...selectedValues);
this.requestUpdate('value', oldValue);
}
// Update the value and display label
if (this.multiple) {
if (this.placeholder && !this.value?.length) {
if (this.placeholder && this.value.length === 0) {
// When no items are selected, keep the value empty so the placeholder shows
this.displayLabel = '';
} else {
@@ -795,8 +776,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
const value = Array.isArray(this.value) ? this.value : [this.value];
// Select only the options that match the new value
const selectedOptions = allOptions.filter(el => value.includes(el.value));
this.setSelectedOptions(selectedOptions);
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
this.updateValidity();
}

View File

@@ -248,15 +248,12 @@
text-underline-offset: 0.125em;
}
*:is([appearance~='accent'], .wa-accent) {
a,
a:hover {
color: currentColor;
}
*:is([appearance~='accent'], .wa-accent) a {
color: var(--wa-color-brand-on-loud);
}
a:hover {
color: color-mix(in oklab, var(--wa-color-text-link), var(--wa-color-mix-hover));
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
text-decoration: var(--wa-link-decoration-hover);
-webkit-text-decoration: var(--wa-link-decoration-hover);
}

View File

@@ -40,26 +40,9 @@ export default {
middleware: [
// When using relative CSS imports, we need to rewrite the paths so the test runner can find them.
function rewriteCssUrls(context, next) {
if (context.url.endsWith('.css')) {
// Okay, this is all fucked up. WTR doesn't seem to like how we use `@import`.
if (context.url.startsWith('/base.css')) {
context.url = '/dist/styles/color/palettes/base.css';
}
if (context.url.startsWith('/variants')) {
context.url = '/dist/styles/color' + context.url;
}
if (context.url.startsWith('/color/variants.css')) {
context.url = '/dist/styles' + context.url;
}
if (context.url.startsWith('/color/palettes')) {
context.url = '/dist/styles' + context.url;
}
// console.log(context)
// console.log({ context, before, after: context.url })
if (context.url.endsWith('.css') && context.url.match(/^\/[^/]+\//)) {
const theme = context.url.split('/')[1];
context.url = `/dist/styles/themes/${theme}${context.url.slice(theme.length + 1)}`;
}
return next();
},