From a2e816253fcd0d55dad2b74fde764b951b2d065d Mon Sep 17 00:00:00 2001 From: 43081j <43081j@users.noreply.github.com> Date: Tue, 5 Apr 2022 20:08:27 +0100 Subject: [PATCH] fix (dropdown): tolerate dropdowns without menus --- src/components/dropdown/dropdown.test.ts | 176 +++++++++++++++++++++++ src/components/dropdown/dropdown.ts | 67 +++++---- 2 files changed, 212 insertions(+), 31 deletions(-) diff --git a/src/components/dropdown/dropdown.test.ts b/src/components/dropdown/dropdown.test.ts index 30779482..ea0d30c3 100644 --- a/src/components/dropdown/dropdown.test.ts +++ b/src/components/dropdown/dropdown.test.ts @@ -142,4 +142,180 @@ describe('', () => { expect(afterHideHandler).to.have.been.calledOnce; expect(panel.hidden).to.be.true; }); + + it('should still open on arrow navigation when no menu items', async () => { + const el = await fixture(html` + + Toggle + + + `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowDown' + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.true; + }); + + it('should open on arrow navigation', async () => { + const el = await fixture(html` + + Toggle + + Item 1 + Item 2 + + + `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowDown' + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.true; + }); + + it('should close on escape key', async () => { + const el = await fixture(html` + + Toggle + + Item 1 + Item 2 + + + `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape' + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.false; + }); + + it('should not open on arrow navigation when no menu exists', async () => { + const el = await fixture(html` + + Toggle +
Some custom content
+
+ `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowDown' + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.false; + }); + + it('should open on enter key', async () => { + const el = await fixture(html` + + Toggle + + Item 1 + + + `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter' + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.true; + }); + + it('should open on enter key when no menu exists', async () => { + const el = await fixture(html` + + Toggle +
Some custom content
+
+ `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter' + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.true; + }); + + // TODO (43081j): This is skipped until #720 is fixed + it.skip('should hide when clicked outside container and initially open', async () => { + const el = await fixture(html` + + Toggle + + Item 1 + + + `); + + document.body.dispatchEvent( + new MouseEvent('mousedown', { + bubbles: true + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.false; + }); + + it('should hide when clicked outside container', async () => { + const el = await fixture(html` + + Toggle + + Item 1 + + + `); + const trigger = el.shadowRoot!.querySelector('[part="trigger"]')!; + + trigger.click(); + + await el.updateComplete; + + document.body.dispatchEvent( + new MouseEvent('mousedown', { + bubbles: true + }) + ); + + await el.updateComplete; + + expect(el.open).to.be.false; + }); }); diff --git a/src/components/dropdown/dropdown.ts b/src/components/dropdown/dropdown.ts index 461062ea..4e89d16e 100644 --- a/src/components/dropdown/dropdown.ts +++ b/src/components/dropdown/dropdown.ts @@ -209,11 +209,6 @@ export default class SlDropdown extends LitElement { } handleTriggerKeyDown(event: KeyboardEvent) { - const menu = this.getMenu()!; - const menuItems = menu.defaultSlot.assignedElements({ flatten: true }) as SlMenuItem[]; - const firstMenuItem = menuItems[0]; - const lastMenuItem = menuItems[menuItems.length - 1]; - // Close when escape or tab is pressed if (event.key === 'Escape') { this.focusOnTrigger(); @@ -229,35 +224,45 @@ export default class SlDropdown extends LitElement { return; } - // When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a - // selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for - // faster navigation. - if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { - event.preventDefault(); + const menu = this.getMenu(); - // Show the menu if it's not already open - if (!this.open) { - this.show(); + if (menu) { + const menuItems = menu.defaultSlot.assignedElements({ flatten: true }) as SlMenuItem[]; + const firstMenuItem = menuItems[0]; + const lastMenuItem = menuItems[menuItems.length - 1]; + + // When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a + // selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for + // faster navigation. + if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { + event.preventDefault(); + + // Show the menu if it's not already open + if (!this.open) { + this.show(); + } + + if (menuItems.length > 0) { + // Focus on the first/last menu item after showing + requestAnimationFrame(() => { + if (event.key === 'ArrowDown' || event.key === 'Home') { + menu.setCurrentItem(firstMenuItem); + firstMenuItem.focus(); + } + + if (event.key === 'ArrowUp' || event.key === 'End') { + menu.setCurrentItem(lastMenuItem); + lastMenuItem.focus(); + } + }); + } } - // Focus on the first/last menu item after showing - requestAnimationFrame(() => { - if (event.key === 'ArrowDown' || event.key === 'Home') { - menu.setCurrentItem(firstMenuItem); - firstMenuItem.focus(); - } - - if (event.key === 'ArrowUp' || event.key === 'End') { - menu.setCurrentItem(lastMenuItem); - lastMenuItem.focus(); - } - }); - } - - // Other keys bring focus to the menu and initiate type-to-select behavior - const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt']; - if (this.open && !ignoredKeys.includes(event.key)) { - menu.typeToSelect(event); + // Other keys bring focus to the menu and initiate type-to-select behavior + const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt']; + if (this.open && !ignoredKeys.includes(event.key)) { + menu.typeToSelect(event); + } } }