Fixes dropdown closing on tab key (#2371)

* fix(sl-menu): tabbing closes the dropdown when it's open

* fix(arbitrary content): tabbing closes the dropdown when it's open

* feat: add pr suggestions
next
Gabriel Belgamo 2025-03-11 17:17:33 +00:00 zatwierdzone przez GitHub
rodzic 6f09a75567
commit 91235cbb32
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
2 zmienionych plików z 115 dodań i 2 usunięć

Wyświetl plik

@ -1,6 +1,7 @@
import { animateTo, stopAnimations } from '../../internal/animate.js';
import { classMap } from 'lit/directives/class-map.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
import { getDeepestActiveElement } from '../../internal/active-elements.js';
import { getTabbableBoundary } from '../../internal/tabbable.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
@ -175,6 +176,20 @@ export default class SlDropdown extends ShoelaceElement {
return;
}
const computeClosestContaining = (element: Element | null | undefined, tagName: string): Element | null => {
if (!element) return null;
const closest = element.closest(tagName);
if (closest) return closest;
const rootNode = element.getRootNode();
if (rootNode instanceof ShadowRoot) {
return computeClosestContaining(rootNode.host, tagName);
}
return null;
};
// Tabbing outside of the containing element closes the panel
//
// If the dropdown is used within a shadow DOM, we need to obtain the activeElement within that shadowRoot,
@ -182,12 +197,13 @@ export default class SlDropdown extends ShoelaceElement {
setTimeout(() => {
const activeElement =
this.containingElement?.getRootNode() instanceof ShadowRoot
? document.activeElement?.shadowRoot?.activeElement
? getDeepestActiveElement()
: document.activeElement;
if (
!this.containingElement ||
activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
computeClosestContaining(activeElement, this.containingElement.tagName.toLowerCase()) !==
this.containingElement
) {
this.hide();
}

Wyświetl plik

@ -1,6 +1,8 @@
import '../../../dist/shoelace.js';
import { clickOnElement } from '../../internal/test.js';
import { customElement } from 'lit/decorators.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import { LitElement } from 'lit';
import { sendKeys, sendMouse } from '@web/test-runner-commands';
import sinon from 'sinon';
import type SlDropdown from './dropdown.js';
@ -354,4 +356,99 @@ describe('<sl-dropdown>', () => {
expect(el.open).to.be.false;
});
describe('when a sl-menu is provided and the dropdown is opened', () => {
before(() => {
@customElement('custom-wrapper')
class Wrapper extends LitElement {
render() {
return html`<nested-dropdown></nested-dropdown>`;
}
}
// eslint-disable-next-line chai-friendly/no-unused-expressions
Wrapper;
@customElement('nested-dropdown')
class NestedDropdown extends LitElement {
render() {
return 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>
`;
}
}
// eslint-disable-next-line chai-friendly/no-unused-expressions
NestedDropdown;
});
it('should remain open on tab key', async () => {
const el = await fixture<SlDropdown>(html`<custom-wrapper></custom-wrapper>`);
const dropdown = el.shadowRoot!.querySelector('nested-dropdown')!.shadowRoot!.querySelector('sl-dropdown')!;
const trigger = dropdown.querySelector('sl-button')!;
trigger.focus();
await dropdown.updateComplete;
await sendKeys({ press: 'Enter' });
await dropdown.updateComplete;
await sendKeys({ press: 'Tab' });
await dropdown.updateComplete;
expect(dropdown.open).to.be.true;
});
});
describe('when arbitrary content is provided and the dropdown is opened', () => {
before(() => {
@customElement('custom-wrapper-arbitrary')
class WrapperArbitrary extends LitElement {
render() {
return html`<nested-dropdown-arbitrary></nested-dropdown-arbitrary>`;
}
}
// eslint-disable-next-line chai-friendly/no-unused-expressions
WrapperArbitrary;
@customElement('nested-dropdown-arbitrary')
class NestedDropdownArbitrary extends LitElement {
render() {
return html`
<sl-dropdown>
<sl-button slot="trigger" caret>Toggle</sl-button>
<ul>
<li><a href="/settings">Settings</a></li>
<li><a href="/profile">Profile</a></li>
</ul>
</sl-dropdown>
`;
}
}
// eslint-disable-next-line chai-friendly/no-unused-expressions
NestedDropdownArbitrary;
});
it('should remain open on tab key', async () => {
const el = await fixture<SlDropdown>(html`<custom-wrapper-arbitrary></custom-wrapper-arbitrary>`);
const dropdown = el
.shadowRoot!.querySelector('nested-dropdown-arbitrary')!
.shadowRoot!.querySelector('sl-dropdown')!;
const trigger = dropdown.querySelector('sl-button')!;
trigger.focus();
await dropdown.updateComplete;
await sendKeys({ press: 'Enter' });
await dropdown.updateComplete;
await sendKeys({ press: 'Tab' });
await dropdown.updateComplete;
expect(dropdown.open).to.be.true;
});
});
});