From 189ad7889d725fff06af92bf8e474ae7870f7c61 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 26 May 2021 08:42:33 -0400 Subject: [PATCH] update dropdown --- docs/resources/changelog.md | 2 +- src/components/dropdown/dropdown.scss | 10 --- src/components/dropdown/dropdown.ts | 121 ++++++++++++++++++-------- 3 files changed, 86 insertions(+), 47 deletions(-) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index fdccdc5a4..012648018 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -18,7 +18,7 @@ The most elegant solution I found was to use the [Web Animations API](https://de - 🚨 BREAKING: changed `left` and `right` placements to `start` and `end` in `sl-tab-group` - Added the Animation Registry - Fixed a bug where removing `sl-dropdown` from the DOM and adding it back destroyed the popover reference [#443](https://github.com/shoelace-style/shoelace/issues/443) -- Updated animations for `sl-alert`, `sl-dialog`, `sl-drawer` to use the Animation Registry instead of CSS transitions +- Updated animations for `sl-alert`, `sl-dialog`, `sl-drawer`, and `sl-dropdown` to use the Animation Registry instead of CSS transitions - Improved a11y by respecting `prefers-reduced-motion` for all show/hide animations ## 2.0.0-beta.40 diff --git a/src/components/dropdown/dropdown.scss b/src/components/dropdown/dropdown.scss index e919221a6..389d5e163 100644 --- a/src/components/dropdown/dropdown.scss +++ b/src/components/dropdown/dropdown.scss @@ -27,12 +27,8 @@ border: solid 1px var(--sl-panel-border-color); border-radius: var(--sl-border-radius-medium); box-shadow: var(--sl-shadow-large); - opacity: 0; overflow: auto; overscroll-behavior: none; - pointer-events: none; - transform: scale(0.9); - transition: var(--sl-transition-fast) opacity, var(--sl-transition-fast) transform; } .dropdown__positioner { @@ -51,10 +47,4 @@ &[data-popper-placement^='right'] .dropdown__panel { transform-origin: left; } - - &.popover-visible .dropdown__panel { - opacity: 1; - transform: none; - pointer-events: all; - } } diff --git a/src/components/dropdown/dropdown.ts b/src/components/dropdown/dropdown.ts index 5abd623ef..287ebf4f5 100644 --- a/src/components/dropdown/dropdown.ts +++ b/src/components/dropdown/dropdown.ts @@ -1,10 +1,12 @@ import { LitElement, html, unsafeCSS } from 'lit'; import { customElement, property, query } from 'lit/decorators'; import { classMap } from 'lit-html/directives/class-map'; +import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm'; +import { animateTo, stopAnimations } from '../../internal/animate'; import { event, EventEmitter, watch } from '../../internal/decorators'; import { scrollIntoView } from '../../internal/scroll'; import { getNearestTabbableElement } from '../../internal/tabbable'; -import Popover from '../../internal/popover'; +import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry'; import SlMenu from '../menu/menu'; import SlMenuItem from '../menu-item/menu-item'; import styles from 'sass:./dropdown.scss'; @@ -21,6 +23,9 @@ let id = 0; * @part base - The component's base wrapper. * @part trigger - The container that wraps the trigger. * @part panel - The panel that gets shown when the dropdown is open. + * + * @animation dropdown.show - The animation to use when showing the dropdown. + * @animation dropdown.hide - The animation to use when hiding the dropdown. */ @customElement('sl-dropdown') export default class SlDropdown extends LitElement { @@ -31,8 +36,8 @@ export default class SlDropdown extends LitElement { @query('.dropdown__positioner') positioner: HTMLElement; private componentId = `dropdown-${++id}`; - private isVisible = false; - private popover: Popover; + private hasInitialized = false; + private popover: PopperInstance; /** 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; @@ -76,13 +81,13 @@ export default class SlDropdown extends LitElement { /** Emitted when the dropdown opens. Calling `event.preventDefault()` will prevent it from being opened. */ @event('sl-show') slShow: EventEmitter; - /** Emitted after the dropdown opens and all transitions are complete. */ + /** Emitted after the dropdown opens and all animations are complete. */ @event('sl-after-show') slAfterShow: EventEmitter; /** Emitted when the dropdown closes. Calling `event.preventDefault()` will prevent it from being closed. */ @event('sl-hide') slHide: EventEmitter; - /** Emitted after the dropdown closes and all transitions are complete. */ + /** Emitted after the dropdown closes and all animations are complete. */ @event('sl-after-hide') slAfterHide: EventEmitter; connectedCallback() { @@ -98,28 +103,34 @@ export default class SlDropdown extends LitElement { // Create the popover after render this.updateComplete.then(() => { - this.popover = new Popover(this.trigger, this.positioner, { - strategy: this.hoist ? 'fixed' : 'absolute', + this.popover = createPopper(this.trigger, this.positioner, { placement: this.placement, - distance: this.distance, - skidding: this.skidding, - transitionElement: this.panel, - onAfterHide: () => this.slAfterHide.emit(), - onAfterShow: () => this.slAfterShow.emit(), - onTransitionEnd: () => { - if (!this.open) { - this.panel.scrollTop = 0; + strategy: this.hoist ? 'fixed' : 'absolute', + modifiers: [ + { + name: 'flip', + options: { + boundary: 'viewport' + } + }, + { + name: 'offset', + options: { + offset: [this.skidding, this.distance] + } } - } + ] }); }); } - firstUpdated() { - // Show on init if open - if (this.open) { - this.show(); - } + async firstUpdated() { + // Set initial visibility + this.panel.hidden = !this.open; + + // Set the initialized flag after the first update is complete + await this.updateComplete; + this.hasInitialized = true; } disconnectedCallback() { @@ -208,10 +219,22 @@ export default class SlDropdown extends LitElement { handlePopoverOptionsChange() { if (this.popover) { this.popover.setOptions({ - strategy: this.hoist ? 'fixed' : 'absolute', placement: this.placement, - distance: this.distance, - skidding: this.skidding + strategy: this.hoist ? 'fixed' : 'absolute', + modifiers: [ + { + name: 'flip', + options: { + boundary: 'viewport' + } + }, + { + name: 'offset', + options: { + offset: [this.skidding, this.distance] + } + } + ] }); } } @@ -307,9 +330,9 @@ export default class SlDropdown extends LitElement { } /** Shows the dropdown panel */ - show() { + async show() { // Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher - if (this.isVisible) { + if (!this.hasInitialized) { return; } @@ -319,20 +342,25 @@ export default class SlDropdown extends LitElement { return; } + this.open = true; + this.panel.addEventListener('sl-activate', this.handleMenuItemActivate); this.panel.addEventListener('sl-select', this.handlePanelSelect); document.addEventListener('keydown', this.handleDocumentKeyDown); document.addEventListener('mousedown', this.handleDocumentMouseDown); - this.isVisible = true; - this.open = true; - this.popover.show(); + await stopAnimations(this); + this.panel.hidden = false; + const { keyframes, options } = getAnimation(this, 'dropdown.show'); + await animateTo(this.panel, keyframes, options); + + this.slAfterShow.emit(); } /** Hides the dropdown panel */ - hide() { + async hide() { // Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher - if (!this.isVisible) { + if (!this.hasInitialized) { return; } @@ -342,14 +370,19 @@ export default class SlDropdown extends LitElement { return; } + this.open = false; + this.panel.removeEventListener('sl-activate', this.handleMenuItemActivate); this.panel.removeEventListener('sl-select', this.handlePanelSelect); document.addEventListener('keydown', this.handleDocumentKeyDown); document.removeEventListener('mousedown', this.handleDocumentMouseDown); - this.isVisible = false; - this.open = false; - this.popover.hide(); + await stopAnimations(this); + const { keyframes, options } = getAnimation(this, 'dropdown.hide'); + await animateTo(this.panel, keyframes, options); + this.panel.hidden = true; + + this.slAfterHide.emit(); } /** @@ -361,7 +394,7 @@ export default class SlDropdown extends LitElement { return; } - this.popover.reposition(); + this.popover.update(); } @watch('open') @@ -391,7 +424,7 @@ export default class SlDropdown extends LitElement { -