Browse Source

[frontend] Various DropDown component improvements

- The actual dropdown panel is now fixed to prevent it from being cut in a flex layout
- Extracted a DropdownDrawer component to be used with upcoming ComboBox component
- Fixed issue where it doesn't close after click
Johan Ahlen 4 years ago
parent
commit
3654071545

+ 2 - 0
desktop/core/src/desktop/js/components/DateRangePicker.test.ts

@@ -15,6 +15,7 @@
 // limitations under the License.
 
 import { shallowMount } from '@vue/test-utils';
+import DropdownDrawer from './dropdown/DropdownDrawer.vue';
 import DropdownPanel from './dropdown/DropdownPanel.vue';
 import DateRangePicker from './DateRangePicker.vue';
 
@@ -23,6 +24,7 @@ describe('DateRangePicker.vue', () => {
     const wrapper = shallowMount(DateRangePicker, {
       global: {
         stubs: {
+          DropdownDrawer,
           DropdownPanel
         }
       }

+ 2 - 214
desktop/core/src/desktop/js/components/__snapshots__/DateRangePicker.test.ts.snap

@@ -9,221 +9,9 @@ exports[`DateRangePicker.vue should render 1`] = `
     disabled="false"
   />
   <div
-    class="hue-dropdown-container"
+    class="hue-dropdown-drawer"
   >
-    <div
-      class="hue-dropdown-inner"
-    >
-      
-      <div
-        class="date-range-picker-panel"
-      >
-        <div
-          class="date-range-picker-body"
-        >
-          <div
-            class="date-range-picker-preset"
-          >
-            <div
-              style="border-right: 1px solid gray;"
-            >
-              <header>
-                Quick Ranges
-              </header>
-              <div
-                class="preset-list"
-              >
-                
-                <div
-                  class="preset-column"
-                >
-                  
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  
-                </div>
-                <div
-                  class="preset-column"
-                >
-                  
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  
-                </div>
-                <div
-                  class="preset-column"
-                >
-                  
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  <div
-                    class="preset-value"
-                  >
-                    <hue-link-stub />
-                  </div>
-                  
-                </div>
-                
-              </div>
-            </div>
-          </div>
-          <div
-            class="date-range-picker-custom"
-          >
-            <header>
-              Time Range
-            </header>
-            <div>
-              <div
-                class="range-header"
-              >
-                FROM:
-              </div>
-              <datepicker-stub
-                calendar-class="range-calendar"
-                format="dd/MM/yyyy"
-                input-class="range-input"
-                placeholder="DD/MM/YYYY"
-                typeable="true"
-              />
-            </div>
-            <div>
-              <div
-                class="range-header"
-              >
-                TO:
-              </div>
-              <datepicker-stub
-                calendar-class="range-calendar"
-                format="dd/MM/yyyy"
-                input-class="range-input"
-                placeholder="DD/MM/YYYY"
-                typeable="true"
-              />
-            </div>
-          </div>
-        </div>
-        <div
-          class="date-range-picker-footer"
-        >
-          <hue-link-stub />
-          <hue-button-stub
-            primary="true"
-            small="true"
-          />
-        </div>
-      </div>
-      
-    </div>
+    <!--v-if-->
   </div>
 </div>
 `;

+ 34 - 0
desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.test.ts

@@ -0,0 +1,34 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// 'License'); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { shallowMount } from '@vue/test-utils';
+import DropdownDrawer from './DropdownDrawer.vue';
+
+describe('DropdownDrawer.vue', () => {
+  it('should render empty dropdown drawer', () => {
+    const wrapper = shallowMount(DropdownDrawer);
+    expect(wrapper.element).toMatchSnapshot();
+  });
+
+  it('should render dropdown drawer with slots', () => {
+    const wrapper = shallowMount(DropdownDrawer, {
+      slots: {
+        default: '<div>Some item</div>'
+      }
+    });
+    expect(wrapper.element).toMatchSnapshot();
+  });
+});

+ 143 - 0
desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.vue

@@ -0,0 +1,143 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<template>
+  <div class="hue-dropdown-drawer" :class="{ open: open }" @click="closeOnClick && closeDrawer()">
+    <div
+      v-if="open"
+      ref="innerPanelElement"
+      v-click-outside="closeDrawer"
+      class="hue-dropdown-drawer-inner"
+      :style="position"
+    >
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, nextTick, PropType } from 'vue';
+
+  import { clickOutsideDirective } from '../directives/clickOutsideDirective';
+
+  const isOutsideViewport = (element: Element): { bottom: boolean; right: boolean } => {
+    const clientRect = element.getBoundingClientRect();
+    return {
+      bottom: clientRect.bottom > window.innerHeight,
+      right: clientRect.right > window.innerWidth
+    };
+  };
+
+  type PositionStyle = Pick<CSSStyleDeclaration, 'left' | 'right' | 'top' | 'bottom'>;
+
+  const getDefaultPosition = (): PositionStyle => ({
+    top: '100%',
+    left: '0',
+    right: '',
+    bottom: ''
+  });
+
+  export default defineComponent({
+    directives: {
+      'click-outside': clickOutsideDirective
+    },
+    props: {
+      open: {
+        type: Boolean,
+        required: true
+      },
+      closeOnClick: {
+        type: Boolean,
+        default: true
+      },
+      triggerElement: {
+        type: Object as PropType<HTMLElement> | null,
+        default: null
+      }
+    },
+    emits: ['close'],
+    data() {
+      return {
+        positionTop: false,
+        positionLeft: false,
+        position: getDefaultPosition()
+      };
+    },
+    watch: {
+      async open(newValue) {
+        if (!newValue) {
+          return;
+        }
+
+        this.position = getDefaultPosition();
+        await nextTick();
+
+        if (!this.$refs.innerPanelElement) {
+          return;
+        }
+        const { bottom, right } = isOutsideViewport(<HTMLElement>this.$refs.innerPanelElement);
+        if (bottom) {
+          const bottomOffset = this.triggerElement ? this.triggerElement.offsetHeight : 0;
+          this.position.top = '';
+          this.position.bottom = `${bottomOffset}px`;
+        }
+        if (right) {
+          const rightOffset = this.triggerElement ? this.triggerElement.offsetWidth : 0;
+          this.position.left = '';
+          this.position.right = `${rightOffset}px`;
+        }
+      }
+    },
+    methods: {
+      closeDrawer(): void {
+        if (this.open) {
+          this.$emit('close');
+        }
+      }
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @import '../styles/colors';
+  @import '../styles/mixins';
+
+  .hue-dropdown-drawer {
+    position: fixed;
+    z-index: 1061;
+
+    &.open {
+      .hue-dropdown-drawer-inner {
+        display: block;
+      }
+    }
+
+    .hue-dropdown-drawer-inner {
+      display: none;
+      position: absolute;
+      z-index: 1000;
+      margin: 2px 0 0;
+      padding: 0;
+      background-color: $fluid-white;
+      border: 1px solid $hue-border-color;
+      border-radius: $hue-panel-border-radius;
+
+      @include box-shadow(0, 5px, 10px, rgba(0, 0, 0, 0.2));
+    }
+  }
+</style>

+ 5 - 2
desktop/core/src/desktop/js/components/dropdown/DropdownMenu.test.ts

@@ -15,6 +15,7 @@
 // limitations under the License.
 
 import { shallowMount } from '@vue/test-utils';
+import DropdownDrawer from './DropdownDrawer.vue';
 import DropdownPanel from './DropdownPanel.vue';
 import DropdownMenu from './DropdownMenu.vue';
 
@@ -23,7 +24,8 @@ describe('DropdownMenu.vue', () => {
     const wrapper = shallowMount(DropdownMenu, {
       global: {
         stubs: {
-          DropdownPanel
+          DropdownPanel,
+          DropdownDrawer
         }
       }
     });
@@ -34,7 +36,8 @@ describe('DropdownMenu.vue', () => {
     const wrapper = shallowMount(DropdownMenu, {
       global: {
         stubs: {
-          DropdownPanel
+          DropdownPanel,
+          DropdownDrawer
         }
       },
       slots: {

+ 6 - 8
desktop/core/src/desktop/js/components/dropdown/DropdownMenu.vue

@@ -18,13 +18,11 @@
 
 <template>
   <dropdown-panel class="hue-dropdown-menu" :text="text" :link="link">
-    <template #contents="{ closePanel }">
-      <div class="hue-dropdown-menu-inner" @click="closePanel">
-        <ul>
-          <slot />
-        </ul>
-      </div>
-    </template>
+    <div class="hue-dropdown-menu-inner">
+      <ul>
+        <slot />
+      </ul>
+    </div>
   </dropdown-panel>
 </template>
 
@@ -66,7 +64,7 @@
       overflow-x: hidden;
       overflow-y: auto;
 
-      /deep/ ul {
+      ::v-deep(ul) {
         overflow-x: hidden;
         margin: 0 !important;
         padding: 0;

+ 14 - 2
desktop/core/src/desktop/js/components/dropdown/DropdownPanel.test.ts

@@ -16,16 +16,28 @@
 
 import { shallowMount } from '@vue/test-utils';
 import DropdownPanel from './DropdownPanel.vue';
+import DropdownDrawer from './DropdownDrawer.vue';
 
 describe('DropdownPanel.vue', () => {
   it('should render empty dropdown panel', () => {
-    const wrapper = shallowMount(DropdownPanel);
+    const wrapper = shallowMount(DropdownPanel, {
+      global: {
+        stubs: {
+          DropdownDrawer
+        }
+      }
+    });
     expect(wrapper.element).toMatchSnapshot();
   });
 
   it('should render dropdown panel with slots', () => {
     const wrapper = shallowMount(DropdownPanel, {
-      scopedSlots: {
+      global: {
+        stubs: {
+          DropdownDrawer
+        }
+      },
+      slots: {
         default: '<div>Some item</div>'
       }
     });

+ 19 - 112
desktop/core/src/desktop/js/components/dropdown/DropdownPanel.vue

@@ -17,58 +17,32 @@
 -->
 
 <template>
-  <div v-click-outside="clickOutside" class="hue-dropdown-panel">
-    <hue-link v-if="link" :disabled="disabled" @click="togglePanel">
+  <div class="hue-dropdown-panel">
+    <hue-link v-if="link" ref="triggerLinkElement" :disabled="disabled" @click="toggleDrawer">
       {{ text }} <i class="fa fa-caret-down" />
     </hue-link>
-    <hue-button v-else :disabled="disabled" @click="togglePanel">
+    <hue-button v-else ref="triggerButtonElement" :disabled="disabled" @click="toggleDrawer">
       {{ text }} <i class="fa fa-caret-down" />
     </hue-button>
-    <div class="hue-dropdown-container" :class="{ open: panelOpen }">
-      <div
-        class="hue-dropdown-inner"
-        :class="{ 'position-top': positionTop, 'position-left': positionLeft }"
-      >
-        <slot name="contents" :closePanel="closePanel" />
-      </div>
-    </div>
+    <DropdownDrawer :open="open" :trigger-element="triggerElement" @close="closeDrawer">
+      <slot />
+    </DropdownDrawer>
   </div>
 </template>
 
 <script lang="ts">
   import { defineComponent } from 'vue';
 
-  import { clickOutsideDirective } from '../directives/clickOutsideDirective';
+  import DropdownDrawer from './DropdownDrawer.vue';
   import HueButton from '../HueButton.vue';
   import HueLink from '../HueLink.vue';
 
-  interface OutsideStatus {
-    left: boolean;
-    right: boolean;
-    top: boolean;
-    bottom: boolean;
-  }
-
-  const isOutsideViewport = (element: Element): OutsideStatus => {
-    const clientRect = element.getBoundingClientRect();
-    return {
-      top: clientRect.top < 0,
-      right: clientRect.right > window.innerWidth,
-      bottom: clientRect.bottom > window.innerHeight,
-      left: clientRect.left < 0
-    };
-  };
-
   export default defineComponent({
     components: {
+      DropdownDrawer,
       HueButton,
       HueLink
     },
-
-    directives: {
-      'click-outside': clickOutsideDirective
-    },
-
     props: {
       text: {
         type: String,
@@ -83,96 +57,29 @@
         default: false
       }
     },
-
-    data(): {
-      panelOpen: boolean;
-
-      positionTop: boolean;
-      positionLeft: boolean;
-    } {
+    data() {
       return {
-        panelOpen: false,
-
-        positionTop: false,
-        positionLeft: false
+        open: false,
+        triggerElement: null as HTMLElement | null
       };
     },
-
+    mounted() {
+      this.triggerElement =
+        <HTMLElement>(this.$refs.triggerLinkElement || this.$refs.triggerButtonElement) || null;
+    },
     methods: {
-      togglePanel(): void {
-        if (!this.panelOpen) {
-          const dropdownElement = this.$el.getElementsByClassName('hue-dropdown-container');
-          if (dropdownElement.length) {
-            const outsideStatus = isOutsideViewport(dropdownElement[0]);
-            this.positionTop = outsideStatus.bottom;
-            this.positionLeft = outsideStatus.right;
-          }
-        }
-        this.panelOpen = !this.panelOpen;
-      },
-
-      closePanel(): void {
-        if (this.panelOpen) {
-          this.togglePanel();
-        }
+      toggleDrawer(): void {
+        this.open = !this.open;
       },
-
-      clickOutside(): void {
-        if (this.panelOpen) {
-          this.panelOpen = false;
-        }
+      closeDrawer(): void {
+        this.open = false;
       }
     }
   });
 </script>
 
 <style lang="scss" scoped>
-  @import '../styles/colors';
-  @import '../styles/mixins';
-
   .hue-dropdown-panel {
     display: inline-block;
-
-    .hue-dropdown-container {
-      position: fixed;
-      z-index: 1061;
-
-      &.open {
-        position: absolute;
-        .hue-dropdown-inner {
-          display: block;
-        }
-      }
-
-      .hue-dropdown-inner {
-        display: none;
-        z-index: 1000;
-        float: left;
-        position: absolute;
-        margin: 2px 0 0;
-        padding: 0;
-        background-color: $fluid-white;
-        border: 1px solid $hue-border-color;
-        border-radius: $hue-panel-border-radius;
-
-        @include box-shadow(0, 5px, 10px, rgba(0, 0, 0, 0.2));
-
-        &:not(.position-top) {
-          top: 100%;
-        }
-
-        &.position-top {
-          bottom: 20px; // TODO: Calculate based on link/button dimensions
-        }
-
-        &:not(.position-left) {
-          left: 0;
-        }
-
-        &.position-left {
-          right: 0; // TODO: Calculate based on link/button dimensions
-        }
-      }
-    }
   }
 </style>

+ 17 - 0
desktop/core/src/desktop/js/components/dropdown/__snapshots__/DropdownDrawer.test.ts.snap

@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DropdownDrawer.vue should render dropdown drawer with slots 1`] = `
+<div
+  class="hue-dropdown-drawer"
+>
+  <!--v-if-->
+</div>
+`;
+
+exports[`DropdownDrawer.vue should render empty dropdown drawer 1`] = `
+<div
+  class="hue-dropdown-drawer"
+>
+  <!--v-if-->
+</div>
+`;

+ 4 - 31
desktop/core/src/desktop/js/components/dropdown/__snapshots__/DropdownMenu.test.ts.snap

@@ -8,23 +8,9 @@ exports[`DropdownMenu.vue should render dropdown with slots 1`] = `
     disabled="false"
   />
   <div
-    class="hue-dropdown-container"
+    class="hue-dropdown-drawer"
   >
-    <div
-      class="hue-dropdown-inner"
-    >
-      
-      <div
-        class="hue-dropdown-menu-inner"
-      >
-        <ul>
-          
-          <stub />
-          
-        </ul>
-      </div>
-      
-    </div>
+    <!--v-if-->
   </div>
 </div>
 `;
@@ -37,22 +23,9 @@ exports[`DropdownMenu.vue should render empty dropdown 1`] = `
     disabled="false"
   />
   <div
-    class="hue-dropdown-container"
+    class="hue-dropdown-drawer"
   >
-    <div
-      class="hue-dropdown-inner"
-    >
-      
-      <div
-        class="hue-dropdown-menu-inner"
-      >
-        <ul>
-          
-          
-        </ul>
-      </div>
-      
-    </div>
+    <!--v-if-->
   </div>
 </div>
 `;

+ 4 - 14
desktop/core/src/desktop/js/components/dropdown/__snapshots__/DropdownPanel.test.ts.snap

@@ -8,14 +8,9 @@ exports[`DropdownPanel.vue should render dropdown panel with slots 1`] = `
     disabled="false"
   />
   <div
-    class="hue-dropdown-container"
+    class="hue-dropdown-drawer"
   >
-    <div
-      class="hue-dropdown-inner"
-    >
-      
-      
-    </div>
+    <!--v-if-->
   </div>
 </div>
 `;
@@ -28,14 +23,9 @@ exports[`DropdownPanel.vue should render empty dropdown panel 1`] = `
     disabled="false"
   />
   <div
-    class="hue-dropdown-container"
+    class="hue-dropdown-drawer"
   >
-    <div
-      class="hue-dropdown-inner"
-    >
-      
-      
-    </div>
+    <!--v-if-->
   </div>
 </div>
 `;