kopia lustrzana https://github.com/shoelace-style/shoelace
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 suggestionsnext
rodzic
6f09a75567
commit
91235cbb32
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Ładowanie…
Reference in New Issue