kopia lustrzana https://github.com/shoelace-style/shoelace
fix tests
rodzic
2dc275defd
commit
f3010cecbe
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue