use popup in dropdown

This commit is contained in:
Cory LaViska
2022-08-09 15:28:01 -04:00
parent bd5ca9034c
commit 2093568981
3 changed files with 46 additions and 102 deletions

View File

@@ -25,6 +25,7 @@ To upgrade to this version, you will need to rework your radio controls by movin
- Removed `setCustomValidity()` and `reportValidity()` from `<sl-radio>` and `<sl-radio-button>` (now available on the radio group)
- Added the experimental `<sl-popup>` component
- Fixed a bug where menu items weren't always aligned correctly
- Refactored `<sl-dropdown>` to use `<sl-popup>`
- Refactored `<sl-tooltip>` to use `<sl-popup>` and added the `body` part
- Revert disabled focus behavior in `<sl-tag-group>`, `<sl-menu>`, and `<sl-tree>` to be consistent with native form controls and menus [#845](https://github.com/shoelace-style/shoelace/issues/845)

View File

@@ -8,19 +8,30 @@ export default css`
display: inline-block;
}
.dropdown {
position: relative;
.dropdown::part(popup) {
z-index: var(--sl-z-index-dropdown);
}
.dropdown[data-current-placement^='top']::part(popup) {
transform-origin: bottom;
}
.dropdown[data-current-placement^='bottom']::part(popup) {
transform-origin: top;
}
.dropdown[data-current-placement^='left']::part(popup) {
transform-origin: right;
}
.dropdown[data-current-placement^='right']::part(popup) {
transform-origin: left;
}
.dropdown__trigger {
display: block;
}
.dropdown__positioner {
position: absolute;
z-index: var(--sl-z-index-dropdown);
}
.dropdown__panel {
font-family: var(--sl-font-sans);
font-size: var(--sl-font-size-medium);
@@ -35,20 +46,4 @@ export default css`
.dropdown--open .dropdown__panel {
pointer-events: all;
}
.dropdown__positioner[data-placement^='top'] .dropdown__panel {
transform-origin: bottom;
}
.dropdown__positioner[data-placement^='bottom'] .dropdown__panel {
transform-origin: top;
}
.dropdown__positioner[data-placement^='left'] .dropdown__panel {
transform-origin: right;
}
.dropdown__positioner[data-placement^='right'] .dropdown__panel {
transform-origin: left;
}
`;

View File

@@ -1,4 +1,3 @@
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom';
import { html, LitElement } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -9,17 +8,21 @@ import { getTabbableBoundary } from '../../internal/tabbable';
import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import '../popup/popup';
import styles from './dropdown.styles';
import type SlButton from '../button/button';
import type SlIconButton from '../icon-button/icon-button';
import type SlMenuItem from '../menu-item/menu-item';
import type SlMenu from '../menu/menu';
import type SlPopup from '../popup/popup';
import type { CSSResultGroup } from 'lit';
/**
* @since 2.0
* @status stable
*
* @dependency sl-popup
*
* @slot - The dropdown's content.
* @slot trigger - The dropdown's trigger, usually a `<sl-button>` element.
*
@@ -39,12 +42,11 @@ import type { CSSResultGroup } from 'lit';
export default class SlDropdown extends LitElement {
static styles: CSSResultGroup = styles;
@query('.dropdown') popup: SlPopup;
@query('.dropdown__trigger') trigger: HTMLElement;
@query('.dropdown__panel') panel: HTMLElement;
@query('.dropdown__positioner') positioner: HTMLElement;
private readonly localize = new LocalizeController(this);
private positionerCleanup: ReturnType<typeof autoUpdate> | undefined;
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false;
@@ -109,8 +111,7 @@ export default class SlDropdown extends LitElement {
// If the dropdown is visible on init, update its position
if (this.open) {
await this.updateComplete;
this.addOpenListeners();
this.startPositioner();
this.popup.reposition();
}
}
@@ -118,7 +119,6 @@ export default class SlDropdown extends LitElement {
super.disconnectedCallback();
this.removeOpenListeners();
this.hide();
this.stopPositioner();
}
focusOnTrigger() {
@@ -197,14 +197,6 @@ export default class SlDropdown extends LitElement {
}
}
@watch('distance')
@watch('hoist')
@watch('placement')
@watch('skidding')
handlePopoverOptionsChange() {
this.updatePositioner();
}
handleTriggerClick() {
if (this.open) {
this.hide();
@@ -340,7 +332,7 @@ export default class SlDropdown extends LitElement {
* is activated.
*/
reposition() {
this.updatePositioner();
this.popup.reposition();
}
addOpenListeners() {
@@ -372,10 +364,10 @@ export default class SlDropdown extends LitElement {
this.addOpenListeners();
await stopAnimations(this);
this.startPositioner();
this.panel.hidden = false;
this.popup.active = true;
const { keyframes, options } = getAnimation(this, 'dropdown.show', { dir: this.localize.dir() });
await animateTo(this.panel, keyframes, options);
await animateTo(this.popup.popup, keyframes, options);
emit(this, 'sl-after-show');
} else {
@@ -385,72 +377,32 @@ export default class SlDropdown extends LitElement {
await stopAnimations(this);
const { keyframes, options } = getAnimation(this, 'dropdown.hide', { dir: this.localize.dir() });
await animateTo(this.panel, keyframes, options);
await animateTo(this.popup.popup, keyframes, options);
this.panel.hidden = true;
this.stopPositioner();
this.popup.active = false;
emit(this, 'sl-after-hide');
}
}
private startPositioner() {
this.stopPositioner();
requestAnimationFrame(() => this.updatePositioner());
this.positionerCleanup = autoUpdate(this.trigger, this.positioner, this.updatePositioner.bind(this));
}
private updatePositioner() {
if (!this.open || !this.trigger || !this.positioner) {
return;
}
computePosition(this.trigger, this.positioner, {
placement: this.placement,
middleware: [
offset({ mainAxis: this.distance, crossAxis: this.skidding }),
flip(),
shift(),
size({
apply: ({ availableWidth, availableHeight }) => {
// Ensure the panel stays within the viewport when we have lots of menu items
Object.assign(this.panel.style, {
maxWidth: `${availableWidth}px`,
maxHeight: `${availableHeight}px`
});
}
})
],
strategy: this.hoist ? 'fixed' : 'absolute'
}).then(({ x, y, placement }) => {
this.positioner.setAttribute('data-placement', placement);
Object.assign(this.positioner.style, {
position: this.hoist ? 'fixed' : 'absolute',
left: `${x}px`,
top: `${y}px`
});
});
}
private stopPositioner() {
if (this.positionerCleanup) {
this.positionerCleanup();
this.positionerCleanup = undefined;
this.positioner.removeAttribute('data-placement');
}
}
render() {
return html`
<div
<sl-popup
part="base"
id="dropdown"
placement=${this.placement}
distance=${this.distance}
skidding=${this.skidding}
strategy=${this.hoist ? 'fixed' : 'absolute'}
flip
shift
class=${classMap({
dropdown: true,
'dropdown--open': this.open
})}
>
<span
slot="anchor"
part="trigger"
class="dropdown__trigger"
@click=${this.handleTriggerClick}
@@ -460,19 +412,15 @@ export default class SlDropdown extends LitElement {
<slot name="trigger" @slotchange=${this.handleTriggerSlotChange}></slot>
</span>
<!-- Position the panel with a wrapper since the popover makes use of translate. This let's us add animations
on the panel without interfering with the position. -->
<div class="dropdown__positioner">
<div
part="panel"
class="dropdown__panel"
aria-hidden=${this.open ? 'false' : 'true'}
aria-labelledby="dropdown"
>
<slot></slot>
</div>
<div
part="panel"
class="dropdown__panel"
aria-hidden=${this.open ? 'false' : 'true'}
aria-labelledby="dropdown"
>
<slot></slot>
</div>
</div>
</sl-popup>
`;
}
}