From 913243f8c14b1e3fba35443e36328574e6833c96 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 28 Dec 2022 16:07:37 -0500 Subject: [PATCH] more finishing touches + tests --- src/components/option/option.styles.ts | 10 +++- src/components/option/option.test.ts | 8 +-- src/components/option/option.ts | 14 ++++- src/components/select/select.test.ts | 79 +++++++++++++++++++++++--- src/components/select/select.ts | 8 +-- 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/components/option/option.styles.ts b/src/components/option/option.styles.ts index 53ceb8ec..6f5bb367 100644 --- a/src/components/option/option.styles.ts +++ b/src/components/option/option.styles.ts @@ -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; + } + } `; diff --git a/src/components/option/option.test.ts b/src/components/option/option.test.ts index 32029676..e3f7883d 100644 --- a/src/components/option/option.test.ts +++ b/src/components/option/option.test.ts @@ -6,10 +6,10 @@ describe('', () => { it('passes accessibility test', async () => { const el = await fixture(html` - Option 1 - Option 2 - Option 3 - Disabled + Option 1 + Option 2 + Option 3 + Disabled `); await expect(el).to.be.accessible(); diff --git a/src/components/option/option.ts b/src/components/option/option.ts index a1599480..7668a532 100644 --- a/src/components/option/option.ts +++ b/src/components/option/option.ts @@ -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`
diff --git a/src/components/select/select.test.ts b/src/components/select/select.test.ts index 18cef8e1..ede58a2b 100644 --- a/src/components/select/select.test.ts +++ b/src/components/select/select.test.ts @@ -39,8 +39,7 @@ describe('', () => { `); 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('', () => { 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('', () => { 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('', () => { expect(displayInput.value).to.equal('updated'); }); + + it('should emit sl-focus and sl-blur when receiving and losing focus', async () => { + const el = await fixture(html` + + Option 1 + Option 2 + Option 3 + + `); + 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(html` + + Option 1 + Option 2 + Option 3 + + `); + 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(html` + + Option 1 + Option 2 + Option 3 + + `); + 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; + }); }); diff --git a/src/components/select/select.ts b/src/components/select/select.ts index cad0ce06..f8055a94 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -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 . All options must be unique.`, + `An option with duplicate values has been found in . 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} >