From 2093568981229fb73c5583e89c77ca813e15309b Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 9 Aug 2022 15:28:01 -0400 Subject: [PATCH] use popup in dropdown --- docs/resources/changelog.md | 1 + src/components/dropdown/dropdown.styles.ts | 41 ++++---- src/components/dropdown/dropdown.ts | 106 ++++++--------------- 3 files changed, 46 insertions(+), 102 deletions(-) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 7d9d7e545..de43c466b 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -25,6 +25,7 @@ To upgrade to this version, you will need to rework your radio controls by movin - Removed `setCustomValidity()` and `reportValidity()` from `` and `` (now available on the radio group) - Added the experimental `` component - Fixed a bug where menu items weren't always aligned correctly +- Refactored `` to use `` - Refactored `` to use `` and added the `body` part - Revert disabled focus behavior in ``, ``, and `` to be consistent with native form controls and menus [#845](https://github.com/shoelace-style/shoelace/issues/845) diff --git a/src/components/dropdown/dropdown.styles.ts b/src/components/dropdown/dropdown.styles.ts index be0d8b902..9bf267352 100644 --- a/src/components/dropdown/dropdown.styles.ts +++ b/src/components/dropdown/dropdown.styles.ts @@ -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; - } `; diff --git a/src/components/dropdown/dropdown.ts b/src/components/dropdown/dropdown.ts index 4d543dcae..823482669 100644 --- a/src/components/dropdown/dropdown.ts +++ b/src/components/dropdown/dropdown.ts @@ -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 `` 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 | 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` -