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);
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;
user-select: none;
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);
color: var(--sl-color-neutral-1000);
}
@ -80,4 +79,11 @@ export default css`
.option__suffix::slotted(*) {
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 () => {
const el = await fixture<SlOption>(html`
<sl-select label="Select one">
<sl-option>Option 1</sl-option>
<sl-option>Option 2</sl-option>
<sl-option>Option 3</sl-option>
<sl-option disabled>Disabled</sl-option>
<sl-option value="1">Option 1</sl-option>
<sl-option value="2">Option 2</sl-option>
<sl-option value="3">Option 3</sl-option>
<sl-option value="4" disabled>Disabled</sl-option>
</sl-select>
`);
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() 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;
@ -94,6 +95,14 @@ export default class SlOption extends ShoelaceElement {
}
}
handleMouseEnter() {
this.hasHover = true;
}
handleMouseLeave() {
this.hasHover = false;
}
render() {
return html`
<div
@ -102,8 +111,11 @@ export default class SlOption extends ShoelaceElement {
option: true,
'option--current': this.current,
'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>
<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]')!;
await clickOnElement(el);
await waitForEvent(el, 'sl-after-show');
await el.show();
await clickOnElement(disabledOption);
await el.updateComplete;
@ -81,8 +80,7 @@ describe('<sl-select>', () => {
el.addEventListener('sl-change', changeHandler);
el.addEventListener('sl-input', inputHandler);
await clickOnElement(el);
await waitForEvent(el, 'sl-after-show');
await el.show();
await clickOnElement(secondOption);
await el.updateComplete;
@ -293,9 +291,7 @@ describe('<sl-select>', () => {
const select = form.querySelector('sl-select')!;
const option2 = form.querySelectorAll('sl-option')![1];
await clickOnElement(select);
await waitForEvent(select, 'sl-after-show');
await select.show();
await clickOnElement(option2);
await select.updateComplete;
expect(select.value).to.equal('option-2');
@ -326,4 +322,73 @@ describe('<sl-select>', () => {
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();
}
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior
private handleComboboxMouseDown(event: MouseEvent) {
const path = event.composedPath();
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();
}
// We use mousedown/mouseup instead of click to allow macOS-style menu behavior
private handleOptionMouseUp(event: MouseEvent) {
private handleOptionClick(event: MouseEvent) {
const target = event.target as HTMLElement;
const option = target.closest('sl-option');
const oldValue = this.value;
@ -433,7 +431,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
allOptions.forEach(option => {
if (values.includes(option.value)) {
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
);
}
@ -766,7 +764,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
part="listbox"
class="select__listbox"
tabindex="-1"
@mouseup=${this.handleOptionMouseUp}
@mouseup=${this.handleOptionClick}
@slotchange=${this.handleDefaultSlotChange}
></slot>
</sl-popup>