kopia lustrzana https://github.com/shoelace-style/shoelace
fixes #729
rodzic
a635e4cd30
commit
caaf2b0d1e
|
@ -6,6 +6,12 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
|
||||||
|
|
||||||
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
|
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
- Added `sl-label-change` event to `<sl-menu-item>`
|
||||||
|
- Fixed a bug where updating a menu item's label wouldn't update the display label in `<sl-select>` [#729](https://github.com/shoelace-style/shoelace/issues/729)
|
||||||
|
- Improved performance of `<sl-select>` by caching menu items instead of traversing for them each time
|
||||||
|
|
||||||
## 2.0.0-beta.73
|
## 2.0.0-beta.73
|
||||||
|
|
||||||
- Added `button` part to `<sl-radio-button>`
|
- Added `button` part to `<sl-radio-button>`
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { html, LitElement } from 'lit';
|
||||||
import { customElement, property, query } from 'lit/decorators.js';
|
import { customElement, property, query } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import '../../components/icon/icon';
|
import '../../components/icon/icon';
|
||||||
|
import { emit } from '../../internal/event';
|
||||||
|
import { getTextContent } from '../../internal/slot';
|
||||||
import { watch } from '../../internal/watch';
|
import { watch } from '../../internal/watch';
|
||||||
import styles from './menu-item.styles';
|
import styles from './menu-item.styles';
|
||||||
|
|
||||||
|
@ -11,6 +13,9 @@ import styles from './menu-item.styles';
|
||||||
*
|
*
|
||||||
* @dependency sl-icon
|
* @dependency sl-icon
|
||||||
*
|
*
|
||||||
|
* @event sl-label-change - Emitted when the menu item's text label changes. For performance reasons, this event is only
|
||||||
|
* emitted if the default slot's `slotchange` event is triggered. It will not fire when the label is first set.
|
||||||
|
*
|
||||||
* @slot - The menu item's label.
|
* @slot - The menu item's label.
|
||||||
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
||||||
* @slot suffix - Used to append an icon or similar element to the menu item.
|
* @slot suffix - Used to append an icon or similar element to the menu item.
|
||||||
|
@ -24,6 +29,9 @@ import styles from './menu-item.styles';
|
||||||
export default class SlMenuItem extends LitElement {
|
export default class SlMenuItem extends LitElement {
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
|
private cachedTextLabel: string;
|
||||||
|
|
||||||
|
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||||
@query('.menu-item') menuItem: HTMLElement;
|
@query('.menu-item') menuItem: HTMLElement;
|
||||||
|
|
||||||
/** Draws the item in a checked state. */
|
/** Draws the item in a checked state. */
|
||||||
|
@ -39,6 +47,11 @@ export default class SlMenuItem extends LitElement {
|
||||||
this.setAttribute('role', 'menuitem');
|
this.setAttribute('role', 'menuitem');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a text label based on the contents of the menu item's default slot. */
|
||||||
|
getTextLabel() {
|
||||||
|
return getTextContent(this.defaultSlot);
|
||||||
|
}
|
||||||
|
|
||||||
@watch('checked')
|
@watch('checked')
|
||||||
handleCheckedChange() {
|
handleCheckedChange() {
|
||||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||||
|
@ -49,6 +62,21 @@ export default class SlMenuItem extends LitElement {
|
||||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDefaultSlotChange() {
|
||||||
|
const textLabel = this.getTextLabel();
|
||||||
|
|
||||||
|
// Ignore the first time the label is set
|
||||||
|
if (typeof this.cachedTextLabel === 'undefined') {
|
||||||
|
this.cachedTextLabel = textLabel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textLabel !== this.cachedTextLabel) {
|
||||||
|
this.cachedTextLabel = textLabel;
|
||||||
|
emit(this, 'sl-label-change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
|
@ -69,7 +97,7 @@ export default class SlMenuItem extends LitElement {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span part="label" class="menu-item__label">
|
<span part="label" class="menu-item__label">
|
||||||
<slot></slot>
|
<slot @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span part="suffix" class="menu-item__suffix">
|
<span part="suffix" class="menu-item__suffix">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||||
import { sendKeys } from '@web/test-runner-commands';
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import type SlSelect from './select';
|
import type SlSelect from './select';
|
||||||
|
@ -110,4 +110,24 @@ describe('<sl-select>', () => {
|
||||||
|
|
||||||
expect(select.shadowRoot!.activeElement).to.equal(select.control);
|
expect(select.shadowRoot!.activeElement).to.equal(select.control);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the display label when a menu item changes', async () => {
|
||||||
|
const el = await fixture<SlSelect>(html`
|
||||||
|
<sl-select value="option-1">
|
||||||
|
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||||
|
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||||
|
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||||
|
</sl-select>
|
||||||
|
`);
|
||||||
|
const displayLabel = el.shadowRoot!.querySelector('[part="display-label"]')!;
|
||||||
|
const menuItem = el.querySelector('sl-menu-item')!;
|
||||||
|
|
||||||
|
expect(displayLabel.textContent?.trim()).to.equal('Option 1');
|
||||||
|
menuItem.textContent = 'updated';
|
||||||
|
|
||||||
|
await oneEvent(el, 'sl-label-change');
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(displayLabel.textContent?.trim()).to.equal('updated');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ import '../../components/menu/menu';
|
||||||
import '../../components/tag/tag';
|
import '../../components/tag/tag';
|
||||||
import { emit } from '../../internal/event';
|
import { emit } from '../../internal/event';
|
||||||
import { FormSubmitController } from '../../internal/form';
|
import { FormSubmitController } from '../../internal/form';
|
||||||
import { getTextContent, HasSlotController } from '../../internal/slot';
|
import { HasSlotController } from '../../internal/slot';
|
||||||
import { watch } from '../../internal/watch';
|
import { watch } from '../../internal/watch';
|
||||||
import { LocalizeController } from '../../utilities/localize';
|
import { LocalizeController } from '../../utilities/localize';
|
||||||
import styles from './select.styles';
|
import styles from './select.styles';
|
||||||
|
@ -72,6 +72,7 @@ export default class SlSelect extends LitElement {
|
||||||
private readonly formSubmitController = new FormSubmitController(this);
|
private readonly formSubmitController = new FormSubmitController(this);
|
||||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||||
private readonly localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
private menuItems: SlMenuItem[] = [];
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
@state() private hasFocus = false;
|
@state() private hasFocus = false;
|
||||||
|
@ -166,15 +167,6 @@ export default class SlSelect extends LitElement {
|
||||||
this.invalid = !this.input.checkValidity();
|
this.invalid = !this.input.checkValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemLabel(item: SlMenuItem) {
|
|
||||||
const slot = item.shadowRoot!.querySelector<HTMLSlotElement>('slot:not([name])');
|
|
||||||
return getTextContent(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
getItems() {
|
|
||||||
return [...this.querySelectorAll<SlMenuItem>('sl-menu-item')];
|
|
||||||
}
|
|
||||||
|
|
||||||
getValueAsArray() {
|
getValueAsArray() {
|
||||||
// Single selects use '' as an empty selection value, so convert this to [] for an empty multi select
|
// Single selects use '' as an empty selection value, so convert this to [] for an empty multi select
|
||||||
if (this.multiple && this.value === '') {
|
if (this.multiple && this.value === '') {
|
||||||
|
@ -229,9 +221,8 @@ export default class SlSelect extends LitElement {
|
||||||
|
|
||||||
handleKeyDown(event: KeyboardEvent) {
|
handleKeyDown(event: KeyboardEvent) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const items = this.getItems();
|
const firstItem = this.menuItems[0];
|
||||||
const firstItem = items[0];
|
const lastItem = this.menuItems[this.menuItems.length - 1];
|
||||||
const lastItem = items[items.length - 1];
|
|
||||||
|
|
||||||
// Ignore key presses on tags
|
// Ignore key presses on tags
|
||||||
if (target.tagName.toLowerCase() === 'sl-tag') {
|
if (target.tagName.toLowerCase() === 'sl-tag') {
|
||||||
|
@ -313,6 +304,14 @@ export default class SlSelect extends LitElement {
|
||||||
this.control.focus();
|
this.control.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMenuItemLabelChange() {
|
||||||
|
// Update the display label when checked menu item's label changes
|
||||||
|
if (!this.multiple) {
|
||||||
|
const checkedItem = this.menuItems.find(item => item.value === this.value);
|
||||||
|
this.displayLabel = checkedItem ? checkedItem.getTextLabel() : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@watch('multiple')
|
@watch('multiple')
|
||||||
handleMultipleChange() {
|
handleMultipleChange() {
|
||||||
// Cast to array | string based on `this.multiple`
|
// Cast to array | string based on `this.multiple`
|
||||||
|
@ -323,11 +322,11 @@ export default class SlSelect extends LitElement {
|
||||||
|
|
||||||
async handleMenuSlotChange() {
|
async handleMenuSlotChange() {
|
||||||
// Wait for items to render before gathering labels otherwise the slot won't exist
|
// Wait for items to render before gathering labels otherwise the slot won't exist
|
||||||
const items = this.getItems();
|
this.menuItems = [...this.querySelectorAll<SlMenuItem>('sl-menu-item')];
|
||||||
|
|
||||||
// Check for duplicate values in menu items
|
// Check for duplicate values in menu items
|
||||||
const values: string[] = [];
|
const values: string[] = [];
|
||||||
items.forEach(item => {
|
this.menuItems.forEach(item => {
|
||||||
if (values.includes(item.value)) {
|
if (values.includes(item.value)) {
|
||||||
console.error(`Duplicate value found in <sl-select> menu item: '${item.value}'`, item);
|
console.error(`Duplicate value found in <sl-select> menu item: '${item.value}'`, item);
|
||||||
}
|
}
|
||||||
|
@ -335,7 +334,8 @@ export default class SlSelect extends LitElement {
|
||||||
values.push(item.value);
|
values.push(item.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(items.map(item => item.render)).then(() => this.syncItemsFromValue());
|
await Promise.all(this.menuItems.map(item => item.render));
|
||||||
|
this.syncItemsFromValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagInteraction(event: KeyboardEvent | MouseEvent) {
|
handleTagInteraction(event: KeyboardEvent | MouseEvent) {
|
||||||
|
@ -364,22 +364,20 @@ export default class SlSelect extends LitElement {
|
||||||
|
|
||||||
resizeMenu() {
|
resizeMenu() {
|
||||||
this.menu.style.width = `${this.control.clientWidth}px`;
|
this.menu.style.width = `${this.control.clientWidth}px`;
|
||||||
|
|
||||||
this.dropdown.reposition();
|
this.dropdown.reposition();
|
||||||
}
|
}
|
||||||
|
|
||||||
syncItemsFromValue() {
|
syncItemsFromValue() {
|
||||||
const items = this.getItems();
|
|
||||||
const value = this.getValueAsArray();
|
const value = this.getValueAsArray();
|
||||||
|
|
||||||
// Sync checked states
|
// Sync checked states
|
||||||
items.map(item => (item.checked = value.includes(item.value)));
|
this.menuItems.map(item => (item.checked = value.includes(item.value)));
|
||||||
|
|
||||||
// Sync display label and tags
|
// Sync display label and tags
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
const checkedItems = items.filter(item => value.includes(item.value));
|
const checkedItems = this.menuItems.filter(item => value.includes(item.value));
|
||||||
|
|
||||||
this.displayLabel = checkedItems.length > 0 ? this.getItemLabel(checkedItems[0]) : '';
|
this.displayLabel = checkedItems.length > 0 ? checkedItems[0].getTextLabel() : '';
|
||||||
this.displayTags = checkedItems.map((item: SlMenuItem) => {
|
this.displayTags = checkedItems.map((item: SlMenuItem) => {
|
||||||
return html`
|
return html`
|
||||||
<sl-tag
|
<sl-tag
|
||||||
|
@ -403,7 +401,7 @@ export default class SlSelect extends LitElement {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${this.getItemLabel(item)}
|
${item.getTextLabel()}
|
||||||
</sl-tag>
|
</sl-tag>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
@ -428,16 +426,15 @@ export default class SlSelect extends LitElement {
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const checkedItem = items.find(item => item.value === value[0]);
|
const checkedItem = this.menuItems.find(item => item.value === value[0]);
|
||||||
|
|
||||||
this.displayLabel = checkedItem ? this.getItemLabel(checkedItem) : '';
|
this.displayLabel = checkedItem ? checkedItem.getTextLabel() : '';
|
||||||
this.displayTags = [];
|
this.displayTags = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncValueFromItems() {
|
syncValueFromItems() {
|
||||||
const items = this.getItems();
|
const checkedItems = this.menuItems.filter(item => item.checked);
|
||||||
const checkedItems = items.filter(item => item.checked);
|
|
||||||
const checkedValues = checkedItems.map(item => item.value);
|
const checkedValues = checkedItems.map(item => item.value);
|
||||||
|
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
|
@ -571,7 +568,7 @@ export default class SlSelect extends LitElement {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<sl-menu part="menu" id="menu" class="select__menu" @sl-select=${this.handleMenuSelect}>
|
<sl-menu part="menu" id="menu" class="select__menu" @sl-select=${this.handleMenuSelect}>
|
||||||
<slot @slotchange=${this.handleMenuSlotChange}></slot>
|
<slot @slotchange=${this.handleMenuSlotChange} @sl-label-change=${this.handleMenuItemLabelChange}></slot>
|
||||||
</sl-menu>
|
</sl-menu>
|
||||||
</sl-dropdown>
|
</sl-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
Ładowanie…
Reference in New Issue