diff --git a/packages/dropdown/src/dropdown.ts b/packages/dropdown/src/dropdown.ts index a1af1e0263..ab0a46b7a2 100644 --- a/packages/dropdown/src/dropdown.ts +++ b/packages/dropdown/src/dropdown.ts @@ -40,8 +40,7 @@ import { MenuItem, MenuItemQueryRoleEventDetail, } from '@spectrum-web-components/menu-item'; -import '@spectrum-web-components/popover'; -import { Overlay, Placement } from '@spectrum-web-components/overlay'; +import { Placement } from '@spectrum-web-components/overlay'; /** * @slot label - The placeholder content for the dropdown @@ -227,15 +226,17 @@ export class DropdownBase extends Focusable { delete this.placeholder; } - private openMenu(): void { + private async openMenu(): Promise { /* istanbul ignore if */ if ( !this.popover || !this.button || !this.optionsMenu || this.optionsMenu.children.length === 0 - ) + ) { + this.menuStateResolver(); return; + } this.placeholder = document.createComment( 'placeholder for optionsMenu' @@ -256,9 +257,30 @@ export class DropdownBase extends Focusable { if (menuWidth) { this.popover.style.setProperty('width', menuWidth); } + const Overlay = await Promise.all([ + import('@spectrum-web-components/overlay'), + import('@spectrum-web-components/popover'), + ]).then( + ([module]) => + (module as typeof import('@spectrum-web-components/overlay')) + .Overlay + ); this.closeOverlay = Overlay.open(this.button, 'click', this.popover, { placement: this.placement, }); + requestAnimationFrame(() => { + /* istanbul ignore else */ + if (this.optionsMenu) { + /* Trick :focus-visible polyfill into thinking keyboard based focus */ + this.dispatchEvent( + new KeyboardEvent('keydown', { + code: 'Tab', + }) + ); + this.optionsMenu.focus(); + } + this.menuStateResolver(); + }); } private closeMenu(): void { @@ -266,6 +288,8 @@ export class DropdownBase extends Focusable { this.closeOverlay(); delete this.closeOverlay; } + + this.menuStateResolver(); } protected get buttonContent(): TemplateResult[] { @@ -352,27 +376,25 @@ export class DropdownBase extends Focusable { this.open = false; } if (changedProperties.has('open')) { + this.menuStatePromise = new Promise( + (res) => (this.menuStateResolver = res) + ); if (this.open) { this.openMenu(); - requestAnimationFrame(() => { - /* istanbul ignore if */ - if (!this.optionsMenu) { - return; - } - /* Trick :focus-visible polyfill into thinking keyboard based focus */ - this.dispatchEvent( - new KeyboardEvent('keydown', { - code: 'Tab', - }) - ); - this.optionsMenu.focus(); - }); } else { this.closeMenu(); } } } + private menuStatePromise = Promise.resolve(); + private menuStateResolver!: () => void; + + protected async _getUpdateComplete(): Promise { + await super._getUpdateComplete(); + await this.menuStatePromise; + } + public disconnectedCallback(): void { this.open = false; diff --git a/packages/dropdown/test/dropdown.test.ts b/packages/dropdown/test/dropdown.test.ts index 78a5ce07e3..f613f9a816 100644 --- a/packages/dropdown/test/dropdown.test.ts +++ b/packages/dropdown/test/dropdown.test.ts @@ -15,13 +15,11 @@ import { Dropdown } from '../'; import '../../menu'; import '../../menu-item'; import { MenuItem } from '../../menu-item'; -import { Popover } from '../../popover'; import { fixture, elementUpdated, html, expect, - nextFrame, waitUntil, } from '@open-wc/testing'; import { waitForPredicate } from '../../../test/testing-helpers'; @@ -38,12 +36,8 @@ const keyboardEvent = (code: string): KeyboardEvent => const arrowDownEvent = keyboardEvent('ArrowDown'); const arrowUpEvent = keyboardEvent('ArrowUp'); -type testableDropdown = { - popover: Popover; -}; - -const dropdownFixture = async (): Promise => - await fixture( +const dropdownFixture = async (): Promise => { + const el = await fixture( html` => ` ); + await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); + return el; +}; describe('Dropdown', () => { it('loads', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); expect(el).to.not.be.undefined; @@ -85,7 +81,6 @@ describe('Dropdown', () => { }); it('renders invalid', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); @@ -98,7 +93,6 @@ describe('Dropdown', () => { }); it('closes when becoming disabled', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); @@ -114,7 +108,6 @@ describe('Dropdown', () => { }); it('selects', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); @@ -139,7 +132,6 @@ describe('Dropdown', () => { }); it('re-selects', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); @@ -181,7 +173,6 @@ describe('Dropdown', () => { }); it('can have selection prevented', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); @@ -210,7 +201,6 @@ describe('Dropdown', () => { }); it('opens on ArrowDown', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); @@ -245,7 +235,6 @@ describe('Dropdown', () => { }); it('loads', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); expect(el).to.not.be.undefined; @@ -254,7 +243,6 @@ describe('Dropdown', () => { }); it('refocuses on list when open', async () => { const el = await dropdownFixture(); - await waitForPredicate(() => !!window.applyFocusVisiblePolyfill); await elementUpdated(el); const firstItem = el.querySelector('sp-menu-item') as MenuItem; @@ -328,7 +316,6 @@ describe('Dropdown', () => { button.click(); await elementUpdated(el); - await nextFrame(); expect(focusFirstSpy.called, 'do not focus first element').to.be.false; expect(focusSelectedSpy.calledOnce, 'focus selected element').to.be @@ -397,13 +384,12 @@ describe('Dropdown', () => { await elementUpdated(el); await waitUntil(() => el.value === ''); + const hoverEl = el.querySelector('sp-menu-item') as MenuItem; + el.open = true; await elementUpdated(el); expect(el.open).to.be.true; - const hoverEl = ((el as unknown) as testableDropdown).popover.querySelector( - 'sp-menu-item' - ) as MenuItem; hoverEl.dispatchEvent(new MouseEvent('mouseenter')); await elementUpdated(el);