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._ 🐛
|
||||
|
||||
## 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
|
||||
|
||||
- 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 { classMap } from 'lit/directives/class-map.js';
|
||||
import '../../components/icon/icon';
|
||||
import { emit } from '../../internal/event';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './menu-item.styles';
|
||||
|
||||
|
@ -11,6 +13,9 @@ import styles from './menu-item.styles';
|
|||
*
|
||||
* @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 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.
|
||||
|
@ -24,6 +29,9 @@ import styles from './menu-item.styles';
|
|||
export default class SlMenuItem extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private cachedTextLabel: string;
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.menu-item') menuItem: HTMLElement;
|
||||
|
||||
/** Draws the item in a checked state. */
|
||||
|
@ -39,6 +47,11 @@ export default class SlMenuItem extends LitElement {
|
|||
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')
|
||||
handleCheckedChange() {
|
||||
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');
|
||||
}
|
||||
|
||||
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() {
|
||||
return html`
|
||||
<div
|
||||
|
@ -69,7 +97,7 @@ export default class SlMenuItem extends LitElement {
|
|||
</span>
|
||||
|
||||
<span part="label" class="menu-item__label">
|
||||
<slot></slot>
|
||||
<slot @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
</span>
|
||||
|
||||
<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 sinon from 'sinon';
|
||||
import type SlSelect from './select';
|
||||
|
@ -110,4 +110,24 @@ describe('<sl-select>', () => {
|
|||
|
||||
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 { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { getTextContent, HasSlotController } from '../../internal/slot';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './select.styles';
|
||||
|
@ -72,6 +72,7 @@ export default class SlSelect extends LitElement {
|
|||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private menuItems: SlMenuItem[] = [];
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
|
@ -166,15 +167,6 @@ export default class SlSelect extends LitElement {
|
|||
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() {
|
||||
// Single selects use '' as an empty selection value, so convert this to [] for an empty multi select
|
||||
if (this.multiple && this.value === '') {
|
||||
|
@ -229,9 +221,8 @@ export default class SlSelect extends LitElement {
|
|||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const items = this.getItems();
|
||||
const firstItem = items[0];
|
||||
const lastItem = items[items.length - 1];
|
||||
const firstItem = this.menuItems[0];
|
||||
const lastItem = this.menuItems[this.menuItems.length - 1];
|
||||
|
||||
// Ignore key presses on tags
|
||||
if (target.tagName.toLowerCase() === 'sl-tag') {
|
||||
|
@ -313,6 +304,14 @@ export default class SlSelect extends LitElement {
|
|||
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')
|
||||
handleMultipleChange() {
|
||||
// Cast to array | string based on `this.multiple`
|
||||
|
@ -323,11 +322,11 @@ export default class SlSelect extends LitElement {
|
|||
|
||||
async handleMenuSlotChange() {
|
||||
// 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
|
||||
const values: string[] = [];
|
||||
items.forEach(item => {
|
||||
this.menuItems.forEach(item => {
|
||||
if (values.includes(item.value)) {
|
||||
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);
|
||||
});
|
||||
|
||||
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) {
|
||||
|
@ -364,22 +364,20 @@ export default class SlSelect extends LitElement {
|
|||
|
||||
resizeMenu() {
|
||||
this.menu.style.width = `${this.control.clientWidth}px`;
|
||||
|
||||
this.dropdown.reposition();
|
||||
}
|
||||
|
||||
syncItemsFromValue() {
|
||||
const items = this.getItems();
|
||||
const value = this.getValueAsArray();
|
||||
|
||||
// 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
|
||||
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) => {
|
||||
return html`
|
||||
<sl-tag
|
||||
|
@ -403,7 +401,7 @@ export default class SlSelect extends LitElement {
|
|||
}
|
||||
}}
|
||||
>
|
||||
${this.getItemLabel(item)}
|
||||
${item.getTextLabel()}
|
||||
</sl-tag>
|
||||
`;
|
||||
});
|
||||
|
@ -428,16 +426,15 @@ export default class SlSelect extends LitElement {
|
|||
`);
|
||||
}
|
||||
} 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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
syncValueFromItems() {
|
||||
const items = this.getItems();
|
||||
const checkedItems = items.filter(item => item.checked);
|
||||
const checkedItems = this.menuItems.filter(item => item.checked);
|
||||
const checkedValues = checkedItems.map(item => item.value);
|
||||
|
||||
if (this.multiple) {
|
||||
|
@ -571,7 +568,7 @@ export default class SlSelect extends LitElement {
|
|||
</div>
|
||||
|
||||
<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-dropdown>
|
||||
</div>
|
||||
|
|
Ładowanie…
Reference in New Issue