fix (dropdown): tolerate dropdowns without menus

pull/721/head
43081j 2022-04-05 20:08:27 +01:00
rodzic 68603f9aed
commit a2e816253f
2 zmienionych plików z 212 dodań i 31 usunięć

Wyświetl plik

@ -142,4 +142,180 @@ describe('<sl-dropdown>', () => {
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<SlDropdown>(html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu> </sl-menu>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[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<SlDropdown>(html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
<sl-menu-item>Item 2</sl-menu-item>
</sl-menu>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[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<SlDropdown>(html`
<sl-dropdown open>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
<sl-menu-item>Item 2</sl-menu-item>
</sl-menu>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[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<SlDropdown>(html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<div>Some custom content</div>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[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<SlDropdown>(html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
</sl-menu>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[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<SlDropdown>(html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<div>Some custom content</div>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[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<SlDropdown>(html`
<sl-dropdown open>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
</sl-menu>
</sl-dropdown>
`);
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<SlDropdown>(html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu>
<sl-menu-item>Item 1</sl-menu-item>
</sl-menu>
</sl-dropdown>
`);
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part="trigger"]')!;
trigger.click();
await el.updateComplete;
document.body.dispatchEvent(
new MouseEvent('mousedown', {
bubbles: true
})
);
await el.updateComplete;
expect(el.open).to.be.false;
});
});

Wyświetl plik

@ -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);
}
}
}