kopia lustrzana https://github.com/shoelace-style/shoelace
fix (dropdown): tolerate dropdowns without menus
rodzic
68603f9aed
commit
a2e816253f
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue