pull/731/head
Cory LaViska 2022-04-14 18:01:25 -04:00
rodzic a635e4cd30
commit caaf2b0d1e
4 zmienionych plików z 80 dodań i 29 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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