mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
3 Commits
changelog
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84e842a4e | ||
|
|
fb8c06235f | ||
|
|
f6a10e9dda |
@@ -7,7 +7,7 @@ category: Form Controls
|
||||
|
||||
```html {.example}
|
||||
<wa-select>
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="">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,6 +366,7 @@ 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
|
||||
@@ -402,4 +403,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.
|
||||
:::
|
||||
:::
|
||||
@@ -8,7 +8,7 @@ Web Awesome follows [Semantic Versioning](https://semver.org/). Breaking changes
|
||||
|
||||
Components with the <wa-badge variant="warning">Experimental</wa-badge> badge should not be used in production. They are made available as release candidates for development and testing purposes. As such, changes to experimental components will not be subject to semantic versioning.
|
||||
|
||||
## Next
|
||||
## 3.0.0-beta.2
|
||||
|
||||
### New Features {data-no-outline}
|
||||
|
||||
@@ -16,8 +16,15 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
|
||||
|
||||
### Bug Fixes and Improvements {data-no-outline}
|
||||
|
||||
- Fixed a bug in `<wa-dropdown>` that prevented the menu from flipping/shifting to keep the menu in the viewport [discuss:1106]
|
||||
- Fixed the themes page so it shows the correct palette and imports
|
||||
- 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]
|
||||
- Fixed the themes page so it shows the correct palette and imports [pr:1125]
|
||||
- Fixed `filled` and `outlined` appearance styles in various components [issue:1102]
|
||||
- Fixed active state styles in the Awesome theme [pr:1129]
|
||||
- Fixed native text styles when applied to certain backgrounds [pr:https://github.com/shoelace-style/webawesome/pull/1130]
|
||||
- Improved the organization of essential and optional styles [pr:1113]
|
||||
|
||||
## 3.0.0-beta.1
|
||||
|
||||
@@ -39,10 +46,8 @@ Many of these changes and improvements were the direct result of feedback from u
|
||||
- Renamed the `classic` theme to `shoelace`
|
||||
- Removed `:root` selector from all theme, color palette, and semantic color stylesheets except for the default theme and colors. All of these styles are now solely scoped to classes, such as `.wa-theme-awesome`, `.wa-palette-bright`, and `.wa-brand-orange`.
|
||||
- Removed most custom properties from components that can otherwise be styled with `::part()` selectors and standard CSS properties.
|
||||
<<<<<<< HEAD
|
||||
- `<wa-dropdown>` was reworked and simplified to not use menu, menu item, menu label; use `<wa-dropdown-item>` instead
|
||||
- Renamed `pulse` attribute in `<wa-badge>` to `attention="pulse"` and added `attention="bounce"` [issue:940]
|
||||
> > > > > > > next
|
||||
- Renamed the `vertical` attribute to `orientation="vertical"` in `<wa-split-panel>` and `<wa-divider>` to align with other components and the platform [issue:674]
|
||||
- Renamed certain boolean attributes to be consistent using the `with-*` and `without-*` pattern:
|
||||
- `<wa-button caret>` => `<wa-button with-caret>`
|
||||
@@ -369,4 +374,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)
|
||||
@@ -196,8 +196,9 @@ describe('<wa-details>', () => {
|
||||
await first.show();
|
||||
await second.show();
|
||||
|
||||
expect(firstBody.clientHeight).to.equal(200);
|
||||
expect(secondBody.clientHeight).to.equal(400);
|
||||
// height + 32 (padding probably?)
|
||||
expect(firstBody.clientHeight).to.equal(232);
|
||||
expect(secondBody.clientHeight).to.equal(432);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { aTimeout, expect, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { resetMouse, sendKeys } from '@web/test-runner-commands';
|
||||
import { html } from 'lit';
|
||||
import sinon from 'sinon';
|
||||
import { fixtures } from '../../internal/test/fixture.js';
|
||||
@@ -200,21 +200,22 @@ describe('<wa-select>', () => {
|
||||
</wa-select>
|
||||
`);
|
||||
const option2 = el.querySelectorAll('wa-option')[1];
|
||||
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`);
|
||||
}
|
||||
});
|
||||
const handler = sinon.spy((_event: InputEvent | Event) => {});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -648,8 +649,8 @@ describe('<wa-select>', () => {
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
|
||||
expect(el.defaultValue).to.equal('option-1');
|
||||
expect(el.value).to.equal('');
|
||||
expect(new FormData(form).get('select')).equal('');
|
||||
expect(el.value).to.equal(null);
|
||||
expect(new FormData(form).get('select')).equal(null);
|
||||
|
||||
const option = document.createElement('wa-option');
|
||||
option.value = 'option-1';
|
||||
@@ -697,8 +698,8 @@ describe('<wa-select>', () => {
|
||||
);
|
||||
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
expect(el.value).to.equal('');
|
||||
expect(new FormData(form).get('select')).to.equal('');
|
||||
expect(el.value).to.equal(null);
|
||||
expect(new FormData(form).get('select')).to.equal(null);
|
||||
|
||||
const option = document.createElement('wa-option');
|
||||
option.value = 'foo';
|
||||
@@ -771,12 +772,12 @@ describe('<wa-select>', () => {
|
||||
);
|
||||
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.value).to.equal(null);
|
||||
|
||||
el.value = 'foo';
|
||||
el.defaultValue = 'foo';
|
||||
await aTimeout(10);
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.value).to.equal(null);
|
||||
|
||||
const option = document.createElement('wa-option');
|
||||
option.value = 'foo';
|
||||
@@ -888,6 +889,43 @@ 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,22 +114,22 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
@state() displayLabel = '';
|
||||
@state() currentOption: WaOption;
|
||||
@state() selectedOptions: WaOption[] = [];
|
||||
@state() optionValues: Set<string> | undefined;
|
||||
@state() optionValues: Set<string | null> | undefined;
|
||||
|
||||
/** The name of the select, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
private _defaultValue: string | string[] = '';
|
||||
private _defaultValue: null | string | string[] = null;
|
||||
|
||||
@property({
|
||||
attribute: false,
|
||||
})
|
||||
set defaultValue(val: string | string[]) {
|
||||
set defaultValue(val: null | string | string[]) {
|
||||
this._defaultValue = this.convertDefaultValue(val);
|
||||
}
|
||||
|
||||
get defaultValue() {
|
||||
return this._defaultValue;
|
||||
return this.convertDefaultValue(this._defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,35 +147,40 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
return val;
|
||||
}
|
||||
|
||||
private _value: string[] | undefined;
|
||||
private _value: string[] | undefined | null;
|
||||
|
||||
/** 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[]) {
|
||||
set value(val: string | string[] | null) {
|
||||
let oldValue = this.value;
|
||||
|
||||
if ((val as any) instanceof FormData) {
|
||||
val = (val as unknown as FormData).getAll(this.name) as string[];
|
||||
}
|
||||
|
||||
if (!Array.isArray(val)) {
|
||||
if (val != null && !Array.isArray(val)) {
|
||||
val = [val];
|
||||
}
|
||||
|
||||
this._value = val;
|
||||
this._value = val ?? null;
|
||||
let newValue = this.value;
|
||||
|
||||
if (newValue !== oldValue) {
|
||||
this.valueHasChanged = true;
|
||||
this.requestUpdate('value', oldValue);
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
let value = this._value ?? this.defaultValue;
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
let optionsChanged = !this.optionValues;
|
||||
let value = this._value ?? this.defaultValue ?? null;
|
||||
|
||||
if (optionsChanged) {
|
||||
if (value != null) {
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
this.optionValues = new Set(null);
|
||||
} else {
|
||||
this.optionValues = new Set(
|
||||
this.getAllOptions()
|
||||
.filter(option => !option.disabled)
|
||||
@@ -184,11 +189,11 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
// Drop values not in the DOM
|
||||
let ret: string | string[] = value.filter(v => this.optionValues!.has(v));
|
||||
ret = this.multiple ? ret : (ret[0] ?? '');
|
||||
|
||||
if (optionsChanged) {
|
||||
this.requestUpdate('value');
|
||||
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;
|
||||
}
|
||||
|
||||
return ret;
|
||||
@@ -291,16 +296,17 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
// Because this is a form control, it shouldn't be opened initially
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
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') || '';
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +381,7 @@ 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 {
|
||||
@@ -506,7 +513,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
private handleClearClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.value !== '') {
|
||||
if (this.value !== null) {
|
||||
this.setSelectedOptions([]);
|
||||
this.displayInput.focus({ preventScroll: true });
|
||||
|
||||
@@ -528,10 +535,11 @@ 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 {
|
||||
@@ -541,13 +549,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 }));
|
||||
|
||||
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 }));
|
||||
});
|
||||
}
|
||||
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.multiple) {
|
||||
this.hide();
|
||||
@@ -566,18 +574,22 @@ 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
|
||||
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];
|
||||
}
|
||||
this.updateDefaultValue();
|
||||
|
||||
let value = this.value;
|
||||
|
||||
if (value == null || (!this.valueHasChanged && !this.hasInteracted)) {
|
||||
this.selectionChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.value;
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
|
||||
// Select only the options that match the new value
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value) || el.selected));
|
||||
const selectedOptions = allOptions.filter(el => value.includes(el.value));
|
||||
this.setSelectedOptions(selectedOptions);
|
||||
}
|
||||
|
||||
private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
|
||||
@@ -690,29 +702,36 @@ 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) {
|
||||
if (this._value == null) {
|
||||
// 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));
|
||||
this._value.unshift(...selectedValues);
|
||||
this._value = this._value?.filter(value => !this.optionValues?.has(value)) ?? null;
|
||||
this._value?.unshift(...selectedValues);
|
||||
this.requestUpdate('value', oldValue);
|
||||
}
|
||||
|
||||
// Update the value and display label
|
||||
if (this.multiple) {
|
||||
if (this.placeholder && this.value.length === 0) {
|
||||
if (this.placeholder && !this.value?.length) {
|
||||
// When no items are selected, keep the value empty so the placeholder shows
|
||||
this.displayLabel = '';
|
||||
} else {
|
||||
@@ -776,7 +795,8 @@ 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
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||
const selectedOptions = allOptions.filter(el => value.includes(el.value));
|
||||
this.setSelectedOptions(selectedOptions);
|
||||
this.updateValidity();
|
||||
}
|
||||
|
||||
|
||||
@@ -248,12 +248,15 @@
|
||||
text-underline-offset: 0.125em;
|
||||
}
|
||||
|
||||
*:is([appearance~='accent'], .wa-accent) a {
|
||||
color: var(--wa-color-brand-on-loud);
|
||||
*:is([appearance~='accent'], .wa-accent) {
|
||||
a,
|
||||
a:hover {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
|
||||
color: color-mix(in oklab, var(--wa-color-text-link), var(--wa-color-mix-hover));
|
||||
text-decoration: var(--wa-link-decoration-hover);
|
||||
-webkit-text-decoration: var(--wa-link-decoration-hover);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,26 @@ 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') && context.url.match(/^\/[^/]+\//)) {
|
||||
const theme = context.url.split('/')[1];
|
||||
context.url = `/dist/styles/themes/${theme}${context.url.slice(theme.length + 1)}`;
|
||||
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 })
|
||||
}
|
||||
return next();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user