pull/1090/head
Cory LaViska 2022-12-20 12:13:39 -05:00
rodzic 2dc275defd
commit f3010cecbe
3 zmienionych plików z 94 dodań i 70 usunięć

Wyświetl plik

@ -1,7 +1,8 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import ShoelaceElement from '../../internal/shoelace-element';
import { getTextContent } from '../../internal/slot';
import { watch } from '../../internal/watch';
import { LocalizeController } from '../../utilities/localize';
import '../icon/icon';
@ -16,7 +17,8 @@ import type { CSSResultGroup } from 'lit';
*
* @dependency sl-icon
*
* @event sl-event-name - Emitted as an example.
* @event sl-label-change - Emitted when the option's label changes. For performance reasons, this event is only emitted
* when the default slot's `slotchange` event is triggered. It will not fire when the label is first set.
*
* @slot - The default slot.
* @slot example - An example slot.
@ -29,8 +31,11 @@ import type { CSSResultGroup } from 'lit';
export default class SlOption extends ShoelaceElement {
static styles: CSSResultGroup = styles;
private cachedTextLabel: string;
private readonly localize = new LocalizeController(this);
@query('.option__label') defaultSlot: HTMLSlotElement;
/** The option's value. When selected, the containing form control will receive this value. */
@property() value = '';
@ -59,6 +64,21 @@ export default class SlOption extends ShoelaceElement {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}
handleDefaultSlotChange() {
const textLabel = getTextContent(this.defaultSlot);
// Ignore the first time the label is set
if (typeof this.cachedTextLabel === 'undefined') {
this.cachedTextLabel = textLabel;
return;
}
if (textLabel !== this.cachedTextLabel) {
this.cachedTextLabel = textLabel;
this.emit('sl-label-change');
}
}
render() {
return html`
<div
@ -72,7 +92,7 @@ export default class SlOption extends ShoelaceElement {
<sl-icon name="check" library="system" aria-hidden="true"></sl-icon>
</span>
<slot name="prefix" class="option__prefix"></slot>
<slot class="option__label"></slot>
<slot class="option__label" @slotchange=${this.handleDefaultSlotChange}></slot>
<slot name="suffix" class="option__suffix"></slot>
</div>
`;

Wyświetl plik

@ -1,17 +1,18 @@
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import { waitForEvent } from '../../internal/event';
import { clickOnElement } from '../../internal/test';
import type SlMenuItem from '../menu-item/menu-item';
import type SlOption from '../option/option';
import type SlSelect from './select';
describe('<sl-select>', () => {
it('should pass accessibility tests', async () => {
const el = await fixture<SlSelect>(html`
<sl-select>
<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 label="Select one">
<sl-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
await expect(el).to.be.accessible();
@ -20,21 +21,20 @@ describe('<sl-select>', () => {
it('should be disabled with the disabled attribute', async () => {
const el = await fixture<SlSelect>(html`
<sl-select disabled>
<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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
expect(el.dropdown.disabled).to.be.true;
expect(el.control.tabIndex).to.equal(-1);
expect(el.displayInput.disabled).to.be.true;
});
it('should focus the select when clicking on the label', async () => {
const el = await fixture<SlSelect>(html`
<sl-select label="Select One">
<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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
@ -51,21 +51,20 @@ describe('<sl-select>', () => {
it('should emit sl-change when the value is changed with the mouse', 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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part~="control"]')!;
const secondOption = el.querySelectorAll<SlMenuItem>('sl-menu-item')[1];
const secondOption = el.querySelectorAll<SlOption>('sl-option')[1];
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('sl-change', changeHandler);
el.addEventListener('sl-input', inputHandler);
await clickOnElement(trigger);
await el.updateComplete;
await clickOnElement(el);
await waitForEvent(el, 'sl-after-show');
await clickOnElement(secondOption);
await el.updateComplete;
@ -77,9 +76,9 @@ describe('<sl-select>', () => {
it('should emit sl-change and sl-input when the value is changed with the keyboard', 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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
const changeHandler = sinon.spy();
@ -92,24 +91,24 @@ describe('<sl-select>', () => {
await el.updateComplete;
await sendKeys({ press: ' ' }); // open the dropdown
await aTimeout(500); // wait for the dropdown to open
await sendKeys({ press: 'ArrowDown' }); // select the first option
await sendKeys({ press: 'ArrowDown' }); // move selection to the second option
await el.updateComplete;
await sendKeys({ press: 'ArrowDown' }); // select the second option
await sendKeys({ press: 'ArrowDown' }); // move selection to the third option
await el.updateComplete;
await sendKeys({ press: 'Enter' }); // commit the selection
await el.updateComplete;
expect(changeHandler).to.have.been.calledOnce;
expect(inputHandler).to.have.been.calledOnce;
expect(el.value).to.equal('option-2');
expect(el.value).to.equal('option-3');
});
it('should not emit sl-change or sl-input when the value is changed programmatically', 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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
@ -121,73 +120,75 @@ describe('<sl-select>', () => {
});
});
it('should open the menu when any letter key is pressed with sl-select is on focus', async () => {
it('should open the listbox when any letter key is pressed with sl-select is on focus', async () => {
const el = await fixture<SlSelect>(html`
<sl-select>
<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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
const control = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__control')!;
control.focus();
const displayInput = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__display-input')!;
el.focus();
await sendKeys({ press: 'r' });
await el.updateComplete;
expect(control.getAttribute('aria-expanded')).to.equal('true');
expect(displayInput.getAttribute('aria-expanded')).to.equal('true');
});
it('should not open the menu when ctrl + R is pressed with sl-select is on focus', async () => {
it('should not open the listbox when ctrl + R is pressed with sl-select is on focus', async () => {
const el = await fixture<SlSelect>(html`
<sl-select>
<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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
const control = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__control')!;
control.focus();
const displayInput = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__display-input')!;
el.focus();
await sendKeys({ down: 'Control' });
await sendKeys({ press: 'r' });
await sendKeys({ up: 'Control' });
await el.updateComplete;
expect(control.getAttribute('aria-expanded')).to.equal('false');
expect(displayInput.getAttribute('aria-expanded')).to.equal('false');
});
it('should focus on the custom control when constraint validation occurs', async () => {
it('should focus on the displayInput when constraint validation occurs', async () => {
const el = await fixture<HTMLFormElement>(html`
<form>
<sl-select required>
<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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
</form>
`);
const select = el.querySelector('sl-select')!;
el.requestSubmit();
expect(select.shadowRoot!.activeElement).to.equal(select.control);
expect(select.shadowRoot!.activeElement).to.equal(select.displayInput);
});
it('should update the display label when a menu item changes', async () => {
it('should update the display label when an option 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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
`);
const displayLabel = el.shadowRoot!.querySelector('[part~="display-label"]')!;
const menuItem = el.querySelector('sl-menu-item')!;
const displayInput = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__display-input')!;
const option = el.querySelector('sl-option')!;
expect(displayLabel.textContent?.trim()).to.equal('Option 1');
menuItem.textContent = 'updated';
expect(displayInput.value).to.equal('Option 1');
await oneEvent(el, 'sl-label-change');
option.textContent = 'updated';
await oneEvent(option, 'sl-label-change');
await el.updateComplete;
expect(displayLabel.textContent?.trim()).to.equal('updated');
expect(displayInput.value).to.equal('updated');
});
describe('when resetting a form', () => {
@ -195,26 +196,27 @@ describe('<sl-select>', () => {
const form = await fixture<HTMLFormElement>(html`
<form>
<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-option value="option-1">Option 1</sl-option>
<sl-option value="option-2">Option 2</sl-option>
<sl-option value="option-3">Option 3</sl-option>
</sl-select>
<sl-button type="reset">Reset</sl-button>
</form>
`);
const button = form.querySelector('sl-button')!;
const resetButton = form.querySelector('sl-button')!;
const select = form.querySelector('sl-select')!;
const option2 = form.querySelectorAll('sl-menu-item')![1];
const option2 = form.querySelectorAll('sl-option')![1];
option2.click();
await option2.updateComplete;
await clickOnElement(select);
await waitForEvent(select, 'sl-after-show');
await clickOnElement(option2);
await select.updateComplete;
expect(select.value).to.equal('option-2');
setTimeout(() => button.click());
setTimeout(() => clickOnElement(resetButton));
await oneEvent(form, 'reset');
await select.updateComplete;
expect(select.value).to.equal('option-1');
});
});

Wyświetl plik

@ -609,6 +609,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
class="select__display-input"
type="text"
placeholder=${this.placeholder}
.disabled=${this.disabled}
.value=${this.displayLabel}
autocomplete="off"
spellcheck="false"
@ -674,6 +675,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
tabindex="-1"
@mouseup=${this.handleOptionMouseUp}
@slotchange=${this.handleDefaultSlotChange}
@sl-label-change=${this.handleDefaultSlotChange}
></slot>
</sl-popup>