2023-06-13 19:43:21 +00:00
|
|
|
import '../../../dist/shoelace.js';
|
2023-06-22 14:56:24 +00:00
|
|
|
import { clickOnElement } from '../../internal/test.js';
|
2024-01-23 16:29:05 +00:00
|
|
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
2022-04-06 13:48:54 +00:00
|
|
|
import { sendKeys, sendMouse } from '@web/test-runner-commands';
|
2021-06-03 12:42:51 +00:00
|
|
|
import sinon from 'sinon';
|
2023-06-22 14:56:24 +00:00
|
|
|
import type SlDropdown from './dropdown.js';
|
2021-06-03 12:42:51 +00:00
|
|
|
|
|
|
|
describe('<sl-dropdown>', () => {
|
|
|
|
it('should be visible with the open attribute', async () => {
|
2021-08-10 21:11:07 +00:00
|
|
|
const el = await fixture<SlDropdown>(html`
|
2021-06-03 12:42:51 +00:00
|
|
|
<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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
|
|
|
`);
|
2022-11-17 14:35:44 +00:00
|
|
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
2021-06-03 12:42:51 +00:00
|
|
|
|
|
|
|
expect(panel.hidden).to.be.false;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not be visible without the open attribute', async () => {
|
2021-08-10 21:11:07 +00:00
|
|
|
const el = await fixture<SlDropdown>(html`
|
2021-06-03 12:42:51 +00:00
|
|
|
<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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
|
|
|
`);
|
2022-11-17 14:35:44 +00:00
|
|
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
2021-06-03 12:42:51 +00:00
|
|
|
|
|
|
|
expect(panel.hidden).to.be.true;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
2021-08-10 21:11:07 +00:00
|
|
|
const el = await fixture<SlDropdown>(html`
|
2021-06-03 12:42:51 +00:00
|
|
|
<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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
2021-08-10 21:11:07 +00:00
|
|
|
`);
|
2022-11-17 14:35:44 +00:00
|
|
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
2021-06-03 12:42:51 +00:00
|
|
|
const showHandler = sinon.spy();
|
|
|
|
const afterShowHandler = sinon.spy();
|
|
|
|
|
|
|
|
el.addEventListener('sl-show', showHandler);
|
|
|
|
el.addEventListener('sl-after-show', afterShowHandler);
|
2022-01-17 04:44:10 +00:00
|
|
|
el.show();
|
2021-06-03 12:42:51 +00:00
|
|
|
|
|
|
|
await waitUntil(() => showHandler.calledOnce);
|
|
|
|
await waitUntil(() => afterShowHandler.calledOnce);
|
|
|
|
|
|
|
|
expect(showHandler).to.have.been.calledOnce;
|
|
|
|
expect(afterShowHandler).to.have.been.calledOnce;
|
|
|
|
expect(panel.hidden).to.be.false;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
2021-08-10 21:11:07 +00:00
|
|
|
const el = await fixture<SlDropdown>(html`
|
2021-06-03 12:42:51 +00:00
|
|
|
<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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
2021-08-10 21:11:07 +00:00
|
|
|
`);
|
2022-11-17 14:35:44 +00:00
|
|
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
2021-06-03 12:42:51 +00:00
|
|
|
const hideHandler = sinon.spy();
|
|
|
|
const afterHideHandler = sinon.spy();
|
|
|
|
|
|
|
|
el.addEventListener('sl-hide', hideHandler);
|
|
|
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
2022-01-17 04:44:10 +00:00
|
|
|
el.hide();
|
2021-06-03 12:42:51 +00:00
|
|
|
|
|
|
|
await waitUntil(() => hideHandler.calledOnce);
|
|
|
|
await waitUntil(() => afterHideHandler.calledOnce);
|
|
|
|
|
|
|
|
expect(hideHandler).to.have.been.calledOnce;
|
|
|
|
expect(afterHideHandler).to.have.been.calledOnce;
|
|
|
|
expect(panel.hidden).to.be.true;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
2021-08-10 21:11:07 +00:00
|
|
|
const el = await fixture<SlDropdown>(html`
|
2021-06-03 12:42:51 +00:00
|
|
|
<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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
2021-08-10 21:11:07 +00:00
|
|
|
`);
|
2022-11-17 14:35:44 +00:00
|
|
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
2021-06-03 12:42:51 +00:00
|
|
|
const showHandler = sinon.spy();
|
|
|
|
const afterShowHandler = sinon.spy();
|
|
|
|
|
|
|
|
el.addEventListener('sl-show', showHandler);
|
|
|
|
el.addEventListener('sl-after-show', afterShowHandler);
|
|
|
|
el.open = true;
|
|
|
|
|
|
|
|
await waitUntil(() => showHandler.calledOnce);
|
|
|
|
await waitUntil(() => afterShowHandler.calledOnce);
|
|
|
|
|
|
|
|
expect(showHandler).to.have.been.calledOnce;
|
|
|
|
expect(afterShowHandler).to.have.been.calledOnce;
|
|
|
|
expect(panel.hidden).to.be.false;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
2021-08-10 21:11:07 +00:00
|
|
|
const el = await fixture<SlDropdown>(html`
|
2021-06-03 12:42:51 +00:00
|
|
|
<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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
2021-08-10 21:11:07 +00:00
|
|
|
`);
|
2022-11-17 14:35:44 +00:00
|
|
|
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part~="panel"]')!;
|
2021-06-03 12:42:51 +00:00
|
|
|
const hideHandler = sinon.spy();
|
|
|
|
const afterHideHandler = sinon.spy();
|
|
|
|
|
|
|
|
el.addEventListener('sl-hide', hideHandler);
|
|
|
|
el.addEventListener('sl-after-hide', afterHideHandler);
|
|
|
|
el.open = false;
|
|
|
|
|
|
|
|
await waitUntil(() => hideHandler.calledOnce);
|
|
|
|
await waitUntil(() => afterHideHandler.calledOnce);
|
|
|
|
|
|
|
|
expect(hideHandler).to.have.been.calledOnce;
|
|
|
|
expect(afterHideHandler).to.have.been.calledOnce;
|
|
|
|
expect(panel.hidden).to.be.true;
|
|
|
|
});
|
2022-04-05 19:08:27 +00:00
|
|
|
|
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2022-04-05 19:08:27 +00:00
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
trigger.focus();
|
|
|
|
await sendKeys({ press: 'ArrowDown' });
|
2022-04-05 19:08:27 +00:00
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(el.open).to.be.true;
|
|
|
|
});
|
|
|
|
|
2023-08-01 18:05:11 +00:00
|
|
|
it('should open on arrow down navigation', async () => {
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2023-08-01 18:05:11 +00:00
|
|
|
const firstMenuItem = el.querySelectorAll('sl-menu-item')[0];
|
2022-04-05 19:08:27 +00:00
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
trigger.focus();
|
|
|
|
await sendKeys({ press: 'ArrowDown' });
|
2022-04-05 19:08:27 +00:00
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(el.open).to.be.true;
|
2023-08-01 18:05:11 +00:00
|
|
|
expect(document.activeElement).to.equal(firstMenuItem);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should open on arrow up 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.querySelector('sl-button')!;
|
|
|
|
const secondMenuItem = el.querySelectorAll('sl-menu-item')[1];
|
|
|
|
|
|
|
|
trigger.focus();
|
|
|
|
await sendKeys({ press: 'ArrowUp' });
|
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(el.open).to.be.true;
|
|
|
|
expect(document.activeElement).to.equal(secondMenuItem);
|
2022-04-05 19:08:27 +00:00
|
|
|
});
|
|
|
|
|
2023-02-02 16:53:38 +00:00
|
|
|
it('should navigate to first focusable item on arrow navigation', async () => {
|
|
|
|
const el = await fixture<SlDropdown>(html`
|
|
|
|
<sl-dropdown>
|
|
|
|
<sl-button slot="trigger" caret>Toggle</sl-button>
|
|
|
|
<sl-menu>
|
|
|
|
<sl-menu-label>Top Label</sl-menu-label>
|
|
|
|
<sl-menu-item>Item 1</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
|
|
|
`);
|
|
|
|
const trigger = el.querySelector('sl-button')!;
|
|
|
|
const item = el.querySelector('sl-menu-item')!;
|
|
|
|
|
2023-02-06 17:18:33 +00:00
|
|
|
await clickOnElement(trigger);
|
2023-02-02 17:23:32 +00:00
|
|
|
await trigger.updateComplete;
|
2023-02-02 16:53:38 +00:00
|
|
|
await sendKeys({ press: 'ArrowDown' });
|
|
|
|
await el.updateComplete;
|
|
|
|
|
2023-02-06 17:18:33 +00:00
|
|
|
expect(document.activeElement).to.equal(item);
|
2023-02-02 16:53:38 +00:00
|
|
|
});
|
|
|
|
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2022-04-05 19:08:27 +00:00
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
trigger.focus();
|
|
|
|
await sendKeys({ press: 'Escape' });
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2022-04-05 19:08:27 +00:00
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
trigger.focus();
|
|
|
|
await sendKeys({ press: 'ArrowDown' });
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2022-04-05 19:08:27 +00:00
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
trigger.focus();
|
|
|
|
await el.updateComplete;
|
|
|
|
await sendKeys({ press: 'Enter' });
|
2022-04-05 19:08:27 +00:00
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(el.open).to.be.true;
|
|
|
|
});
|
|
|
|
|
2023-02-06 17:18:33 +00:00
|
|
|
it('should focus on menu items when clicking the trigger and arrowing through options', 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-item>Item 3</sl-menu-item>
|
|
|
|
</sl-menu>
|
|
|
|
</sl-dropdown>
|
|
|
|
`);
|
|
|
|
const trigger = el.querySelector('sl-button')!;
|
|
|
|
const secondMenuItem = el.querySelectorAll('sl-menu-item')[1];
|
|
|
|
|
|
|
|
await clickOnElement(trigger);
|
|
|
|
await trigger.updateComplete;
|
|
|
|
await sendKeys({ press: 'ArrowDown' });
|
|
|
|
await el.updateComplete;
|
|
|
|
await sendKeys({ press: 'ArrowDown' });
|
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(document.activeElement).to.equal(secondMenuItem);
|
|
|
|
});
|
|
|
|
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2022-04-05 19:08:27 +00:00
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
trigger.focus();
|
|
|
|
await el.updateComplete;
|
|
|
|
await sendKeys({ press: 'Enter' });
|
2022-04-05 19:08:27 +00:00
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(el.open).to.be.true;
|
|
|
|
});
|
|
|
|
|
2022-04-11 14:23:03 +00:00
|
|
|
it('should hide when clicked outside container and initially open', async () => {
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
|
|
|
|
2022-04-06 13:48:54 +00:00
|
|
|
await sendMouse({ type: 'click', position: [0, 0] });
|
2022-04-05 19:08:27 +00:00
|
|
|
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>
|
|
|
|
`);
|
2022-04-06 13:48:54 +00:00
|
|
|
const trigger = el.querySelector('sl-button')!;
|
2022-04-05 19:08:27 +00:00
|
|
|
|
|
|
|
trigger.click();
|
|
|
|
await el.updateComplete;
|
2022-04-06 13:48:54 +00:00
|
|
|
await sendMouse({ type: 'click', position: [0, 0] });
|
2022-04-05 19:08:27 +00:00
|
|
|
await el.updateComplete;
|
|
|
|
|
|
|
|
expect(el.open).to.be.false;
|
|
|
|
});
|
2021-06-03 12:42:51 +00:00
|
|
|
});
|