more finishing touches + tests

pull/1090/head
Cory LaViska 2022-12-28 16:07:37 -05:00
rodzic 563ed81984
commit 913243f8c1
5 zmienionych plików z 100 dodań i 19 usunięć

Wyświetl plik

@ -25,11 +25,10 @@ export default css`
color: var(--sl-color-neutral-700); color: var(--sl-color-neutral-700);
padding: var(--sl-spacing-x-small) var(--sl-spacing-medium) var(--sl-spacing-x-small) var(--sl-spacing-x-small); padding: var(--sl-spacing-x-small) var(--sl-spacing-medium) var(--sl-spacing-x-small) var(--sl-spacing-x-small);
transition: var(--sl-transition-fast) fill; transition: var(--sl-transition-fast) fill;
user-select: none;
cursor: pointer; cursor: pointer;
} }
:host(:hover) .option:not(.option--current):not(.option--disabled) { .option--hover:not(.option--current):not(.option--disabled) {
background-color: var(--sl-color-neutral-100); background-color: var(--sl-color-neutral-100);
color: var(--sl-color-neutral-1000); color: var(--sl-color-neutral-1000);
} }
@ -80,4 +79,11 @@ export default css`
.option__suffix::slotted(*) { .option__suffix::slotted(*) {
margin-inline-start: var(--sl-spacing-x-small); margin-inline-start: var(--sl-spacing-x-small);
} }
@media (forced-colors: active) {
:host(:hover:not([aria-disabled='true'])) .option {
outline: dashed 1px SelectedItem;
outline-offset: -1px;
}
}
`; `;

Wyświetl plik

@ -6,10 +6,10 @@ describe('<sl-option>', () => {
it('passes accessibility test', async () => { it('passes accessibility test', async () => {
const el = await fixture<SlOption>(html` const el = await fixture<SlOption>(html`
<sl-select label="Select one"> <sl-select label="Select one">
<sl-option>Option 1</sl-option> <sl-option value="1">Option 1</sl-option>
<sl-option>Option 2</sl-option> <sl-option value="2">Option 2</sl-option>
<sl-option>Option 3</sl-option> <sl-option value="3">Option 3</sl-option>
<sl-option disabled>Disabled</sl-option> <sl-option value="4" disabled>Disabled</sl-option>
</sl-select> </sl-select>
`); `);
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();

Wyświetl plik

@ -36,6 +36,7 @@ export default class SlOption extends ShoelaceElement {
@state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight) @state() current = false; // the user has keyed into the option, but hasn't selected it yet (shows a highlight)
@state() selected = false; // the option is selected and has aria-selected="true" @state() selected = false; // the option is selected and has aria-selected="true"
@state() hasHover = false; // we need this because Safari doesn't honor :hover styles while dragging
@query('.option__label') defaultSlot: HTMLSlotElement; @query('.option__label') defaultSlot: HTMLSlotElement;
@ -94,6 +95,14 @@ export default class SlOption extends ShoelaceElement {
} }
} }
handleMouseEnter() {
this.hasHover = true;
}
handleMouseLeave() {
this.hasHover = false;
}
render() { render() {
return html` return html`
<div <div
@ -102,8 +111,11 @@ export default class SlOption extends ShoelaceElement {
option: true, option: true,
'option--current': this.current, 'option--current': this.current,
'option--disabled': this.disabled, 'option--disabled': this.disabled,
'option--selected': this.selected 'option--selected': this.selected,
'option--hover': this.hasHover
})} })}
@mouseenter=${this.handleMouseEnter}
@mouseleave=${this.handleMouseLeave}
> >
<sl-icon part="checked-icon" class="option__check" name="check" library="system" aria-hidden="true"></sl-icon> <sl-icon part="checked-icon" class="option__check" name="check" library="system" aria-hidden="true"></sl-icon>
<slot part="prefix" name="prefix" class="option__prefix"></slot> <slot part="prefix" name="prefix" class="option__prefix"></slot>

Wyświetl plik

@ -39,8 +39,7 @@ describe('<sl-select>', () => {
`); `);
const disabledOption = el.querySelector('sl-option[disabled]')!; const disabledOption = el.querySelector('sl-option[disabled]')!;
await clickOnElement(el); await el.show();
await waitForEvent(el, 'sl-after-show');
await clickOnElement(disabledOption); await clickOnElement(disabledOption);
await el.updateComplete; await el.updateComplete;
@ -81,8 +80,7 @@ describe('<sl-select>', () => {
el.addEventListener('sl-change', changeHandler); el.addEventListener('sl-change', changeHandler);
el.addEventListener('sl-input', inputHandler); el.addEventListener('sl-input', inputHandler);
await clickOnElement(el); await el.show();
await waitForEvent(el, 'sl-after-show');
await clickOnElement(secondOption); await clickOnElement(secondOption);
await el.updateComplete; await el.updateComplete;
@ -293,9 +291,7 @@ describe('<sl-select>', () => {
const select = form.querySelector('sl-select')!; const select = form.querySelector('sl-select')!;
const option2 = form.querySelectorAll('sl-option')![1]; const option2 = form.querySelectorAll('sl-option')![1];
await clickOnElement(select); await select.show();
await waitForEvent(select, 'sl-after-show');
await clickOnElement(option2); await clickOnElement(option2);
await select.updateComplete; await select.updateComplete;
expect(select.value).to.equal('option-2'); expect(select.value).to.equal('option-2');
@ -326,4 +322,73 @@ describe('<sl-select>', () => {
expect(displayInput.value).to.equal('updated'); expect(displayInput.value).to.equal('updated');
}); });
it('should emit sl-focus and sl-blur when receiving and losing focus', async () => {
const el = await fixture<SlSelect>(html`
<sl-select value="option-1">
<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 focusHandler = sinon.spy();
const blurHandler = sinon.spy();
el.addEventListener('sl-focus', focusHandler);
el.addEventListener('sl-blur', blurHandler);
el.focus();
await el.updateComplete;
el.blur();
await el.updateComplete;
expect(focusHandler).to.have.been.calledOnce;
expect(blurHandler).to.have.been.calledOnce;
});
it('should emit sl-clear when the clear button is clicked', async () => {
const el = await fixture<SlSelect>(html`
<sl-select value="option-1" clearable>
<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 clearHandler = sinon.spy();
const clearButton = el.shadowRoot!.querySelector('[part~="clear-button"]')!;
el.addEventListener('sl-clear', clearHandler);
await el.show();
await clickOnElement(clearButton);
await el.updateComplete;
expect(clearHandler).to.have.been.calledOnce;
});
it('should emit sl-show, sl-after-show, sl-hide, and sl-after-hide events when the listbox opens and closes', async () => {
const el = await fixture<SlSelect>(html`
<sl-select value="option-1">
<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 showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();
el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler);
el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler);
await el.show();
expect(showHandler).to.have.been.calledOnce;
expect(afterShowHandler).to.have.been.calledOnce;
await el.hide();
expect(hideHandler).to.have.been.calledOnce;
expect(afterHideHandler).to.have.been.calledOnce;
});
}); });

Wyświetl plik

@ -358,7 +358,6 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
this.displayInput.focus(); this.displayInput.focus();
} }
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior
private handleComboboxMouseDown(event: MouseEvent) { private handleComboboxMouseDown(event: MouseEvent) {
const path = event.composedPath(); const path = event.composedPath();
const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'sl-icon-button'); const isIconButton = path.some(el => el instanceof Element && el.tagName.toLowerCase() === 'sl-icon-button');
@ -396,8 +395,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
event.preventDefault(); event.preventDefault();
} }
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior private handleOptionClick(event: MouseEvent) {
private handleOptionMouseUp(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const option = target.closest('sl-option'); const option = target.closest('sl-option');
const oldValue = this.value; const oldValue = this.value;
@ -433,7 +431,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
allOptions.forEach(option => { allOptions.forEach(option => {
if (values.includes(option.value)) { if (values.includes(option.value)) {
console.error( console.error(
`An option with duplicate values has been found in <sl-select>. All options must be unique.`, `An option with duplicate values has been found in <sl-select>. All options must have unique values.`,
option option
); );
} }
@ -766,7 +764,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
part="listbox" part="listbox"
class="select__listbox" class="select__listbox"
tabindex="-1" tabindex="-1"
@mouseup=${this.handleOptionMouseUp} @mouseup=${this.handleOptionClick}
@slotchange=${this.handleDefaultSlotChange} @slotchange=${this.handleDefaultSlotChange}
></slot> ></slot>
</sl-popup> </sl-popup>