From 7dbbf9baad46127318c33f39471b3c09144fee9c Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 8 Jan 2020 00:02:03 -0500 Subject: [PATCH] Add popper.js to dropdowns --- package-lock.json | 5 ++++ package.json | 4 ++- src/components.d.ts | 11 ++++++- src/components/dropdown/dropdown.scss | 18 +++++------ src/components/dropdown/dropdown.tsx | 43 +++++++++++++++++++++++---- src/index.html | 6 ++-- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index dae7bc4b0..83a8b4dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -452,6 +452,11 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "popper.js": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz", + "integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==" + }, "pretty-bytes": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", diff --git a/package.json b/package.json index bc1532e95..41abce3ad 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,7 @@ "workbox-build": "4.3.1" }, "license": "MIT", - "dependencies": {} + "dependencies": { + "popper.js": "^1.16.0" + } } diff --git a/src/components.d.ts b/src/components.d.ts index 13ba1900b..c6741818b 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -55,6 +55,10 @@ export namespace Components { interface SDropdown { 'close': () => Promise; 'open': () => Promise; + /** + * The preferred placement of the dropdown menu. Note that the actual placement may vary as needed to keep the menu inside of the viewport. + */ + 'placement': 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'; } interface SDropdownDivider {} interface SDropdownItem { @@ -354,7 +358,12 @@ declare namespace LocalJSX { */ 'type'?: string; } - interface SDropdown {} + interface SDropdown { + /** + * The preferred placement of the dropdown menu. Note that the actual placement may vary as needed to keep the menu inside of the viewport. + */ + 'placement'?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'; + } interface SDropdownDivider {} interface SDropdownItem { /** diff --git a/src/components/dropdown/dropdown.scss b/src/components/dropdown/dropdown.scss index 8c1a8c1c3..665e10e16 100644 --- a/src/components/dropdown/dropdown.scss +++ b/src/components/dropdown/dropdown.scss @@ -18,11 +18,13 @@ $dropdown-color: $menu-color !default; display: inline-block; } -// TODO: use $dropdown-* variables here +.s-dropdown__trigger { + display: inline-block; +} + .s-dropdown__menu { position: absolute; - top: calc(100% + 2px); - left: 0; + z-index: 9999; font-family: $dropdown-font-family; font-weight: $dropdown-font-weight; font-size: $dropdown-font-size; @@ -31,16 +33,12 @@ $dropdown-color: $menu-color !default; border: solid 1px $dropdown-border-color; border-radius: 4px; box-shadow: $dropdown-box-shadow; - padding-top: 10px; // TODO: these should match $dropdown-item-padding-y - padding-bottom: 10px; + padding-top: $menu-padding-y; + padding-bottom: $menu-padding-y; opacity: 0; - transform-origin: top left; - transform: scale(0.9); - transition: 100ms opacity, 100ms transform; - z-index: 10; + transition: 100ms opacity; } .s-dropdown--open .s-dropdown__menu { opacity: 1; - transform: scale(1) translateY(0); } diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index 0f3d844cf..5476bbc58 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -1,4 +1,5 @@ -import { Component, Method, State, h } from '@stencil/core'; +import { Component, Method, Prop, State, Watch, h } from '@stencil/core'; +import PopperJs from 'popper.js'; @Component({ tag: 's-dropdown', @@ -8,6 +9,8 @@ import { Component, Method, State, h } from '@stencil/core'; export class Dropdown { items: [HTMLSDropdownItemElement]; menu: HTMLElement; + popper: PopperJs; + trigger: HTMLElement; constructor() { this.handleDocumentClick = this.handleDocumentClick.bind(this); @@ -20,10 +23,41 @@ export class Dropdown { @State() isOpen = false; + /** + * The preferred placement of the dropdown menu. Note that the actual placement may vary as needed to keep the menu + * inside of the viewport. + */ + @Prop() placement: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' = 'bottom-start'; + + @Watch('placement') + handlePlacementChange() { + if (this.popper) { + this.popper.options.placement = this.placement; + } + } + + componentDidLoad() { + this.popper = new PopperJs(this.trigger, this.menu, { + placement: this.placement, + modifiers: { + offset: { + offset: '0, 2px' + } + } + }); + } + + componentDidUnload() { + if (this.popper) { + this.popper.destroy(); + } + } + @Method() async open() { this.items = this.getAllItems(); this.menu.hidden = false; + this.popper.scheduleUpdate(); requestAnimationFrame(() => (this.isOpen = true)); document.addEventListener('click', this.handleDocumentClick); @@ -70,8 +104,7 @@ export class Dropdown { } handleDocumentKeyDown(event: KeyboardEvent) { - if (event.key === 'Escape') { - event.preventDefault(); + if (event.key === 'Escape' || event.key === 'Tab') { this.close(); } @@ -98,7 +131,7 @@ export class Dropdown { } handleMenuMouseDown(event: MouseEvent) { - // Keep focus on the dropdown trigger when selecting a menu item + // Keep focus on the dropdown trigger when selecting menu items event.preventDefault(); } @@ -135,7 +168,7 @@ export class Dropdown { 's-dropdown--open': this.isOpen }} > - this.toggleMenu()}> + (this.trigger = el)} onClick={() => this.toggleMenu()}> diff --git a/src/index.html b/src/index.html index d9361efc3..073ca1b74 100644 --- a/src/index.html +++ b/src/index.html @@ -201,6 +201,9 @@ Dropdown Item 2 Dropdown Item 3 + Checked + Disabled + Prefix @@ -213,9 +216,6 @@ Suffix Icon - - Checked - Disabled