Эх сурвалжийг харах

[frontend] Extract a DropDownMenuOptions component

To be used with arbitrary trigger
Johan Ahlen 4 жил өмнө
parent
commit
da212bbb78

+ 17 - 0
desktop/core/src/desktop/js/components/dropdown/DropDownMenuOptions.d.ts

@@ -0,0 +1,17 @@
+// 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.
+
+export type Option = string | { value: string; label: string };

+ 53 - 12
desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.vue

@@ -18,13 +18,7 @@
 
 <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"
-    >
+    <div v-if="open" ref="innerPanelElement" class="hue-dropdown-drawer-inner" :style="position">
       <slot />
     </div>
   </div>
@@ -33,7 +27,11 @@
 <script lang="ts">
   import { defineComponent, nextTick, PropType } from 'vue';
 
-  import { clickOutsideDirective } from '../directives/clickOutsideDirective';
+  import {
+    addClickOutsideHandler,
+    removeClickOutsideHandler
+  } from 'components/directives/clickOutsideDirective';
+  import { defer } from 'utils/hueUtils';
 
   const isOutsideViewport = (element: Element): { bottom: boolean; right: boolean } => {
     const clientRect = element.getBoundingClientRect();
@@ -54,9 +52,6 @@
 
   export default defineComponent({
     name: 'DropdownDrawer',
-    directives: {
-      'click-outside': clickOutsideDirective
-    },
     props: {
       open: {
         type: Boolean,
@@ -67,7 +62,7 @@
         default: true
       },
       triggerElement: {
-        type: Object as PropType<HTMLElement> | null,
+        type: Object as PropType<HTMLElement | null>,
         default: null
       }
     },
@@ -82,6 +77,7 @@
     watch: {
       async open(newValue) {
         if (!newValue) {
+          removeClickOutsideHandler(this.$el);
           return;
         }
 
@@ -102,6 +98,13 @@
           this.position.left = '';
           this.position.right = `${rightOffset}px`;
         }
+        defer(() => {
+          addClickOutsideHandler(this.$el, event => {
+            if (this.triggerElement !== event.target) {
+              this.$emit('close');
+            }
+          });
+        });
       }
     },
     methods: {
@@ -139,6 +142,44 @@
       border-radius: $hue-panel-border-radius;
 
       @include box-shadow(0, 5px, 10px, rgba(0, 0, 0, 0.2));
+
+      > ::v-deep(ul) {
+        overflow-x: hidden;
+        margin: 0 !important;
+        padding: 0;
+        list-style: none;
+        font-size: 13px;
+
+        li {
+          color: $fluid-gray-800;
+
+          button,
+          a {
+            display: block;
+            width: 100%;
+            padding: 6px 16px;
+            clear: both;
+            font-weight: 400;
+            text-align: inherit;
+            white-space: nowrap;
+            background-color: transparent;
+            border: 0;
+            position: relative;
+            outline: 0;
+
+            &:hover,
+            &.active,
+            &.focus {
+              background-color: $hue-primary-color-light;
+            }
+          }
+
+          &.selected button,
+          &.selected a {
+            background-color: $hue-primary-color-light;
+          }
+        }
+      }
     }
   }
 </style>

+ 0 - 33
desktop/core/src/desktop/js/components/dropdown/DropdownMenu.vue

@@ -64,39 +64,6 @@
 
       overflow-x: hidden;
       overflow-y: auto;
-
-      ::v-deep(ul) {
-        overflow-x: hidden;
-        margin: 0 !important;
-        padding: 0;
-        list-style: none;
-        font-size: 13px;
-
-        li {
-          color: $fluid-gray-800;
-
-          button,
-          a {
-            display: block;
-            width: 100%;
-            padding: 6px 16px;
-            clear: both;
-            font-weight: 400;
-            text-align: inherit;
-            white-space: nowrap;
-            background-color: transparent;
-            border: 0;
-            position: relative;
-            outline: 0;
-
-            &:hover,
-            &.active,
-            &.focus {
-              background-color: $hue-primary-color-light;
-            }
-          }
-        }
-      }
     }
   }
 </style>

+ 103 - 0
desktop/core/src/desktop/js/components/dropdown/DropdownMenuOptions.vue

@@ -0,0 +1,103 @@
+<template>
+  <DropdownDrawer
+    :open="open && options.length"
+    :trigger-element="keydownElement"
+    @close="$emit('close')"
+  >
+    <ul>
+      <DropdownMenuButton
+        v-for="(option, idx) of options"
+        :key="option.value || option"
+        :class="{ selected: idx === selectedIndex }"
+        @click="onOptionClick(option, idx)"
+      >
+        {{ getLabel(option) }}
+      </DropdownMenuButton>
+    </ul>
+  </DropdownDrawer>
+</template>
+
+<script lang="ts">
+  import { defineComponent, PropType } from 'vue';
+
+  import DropdownDrawer from './DropdownDrawer.vue';
+  import DropdownMenuButton from './DropdownMenuButton.vue';
+  import { Option } from './DropDownMenuOptions';
+
+  export const getLabel = (option: Option): string =>
+    (option as { label: string }).label || (option as string);
+
+  export default defineComponent({
+    components: { DropdownMenuButton, DropdownDrawer },
+    props: {
+      options: {
+        type: Array as PropType<Option[]>,
+        default: []
+      },
+      keydownElement: {
+        type: Object as PropType<HTMLElement>,
+        default: null
+      },
+      open: {
+        type: Boolean,
+        default: false
+      }
+    },
+    emits: ['close', 'option-active', 'option-selected'],
+    data() {
+      return {
+        selectedIndex: 0,
+        keydownDispose: null as (() => void) | null
+      };
+    },
+    watch: {
+      keydownElement(element) {
+        if (this.keydownDispose) {
+          this.keydownDispose();
+        }
+        if (element) {
+          const handler = (event: KeyboardEvent) => {
+            switch (event.key) {
+              case 'ArrowDown':
+                this.selectedIndex = (this.selectedIndex + 1) % this.options.length;
+                break;
+              case 'ArrowUp':
+                this.selectedIndex =
+                  this.selectedIndex !== 0 ? this.selectedIndex - 1 : this.options.length - 1;
+                break;
+              case 'Enter':
+                this.$emit('option-selected', this.options[this.selectedIndex]);
+            }
+          };
+          element.addEventListener('keydown', handler);
+          this.keydownDispose = () => {
+            element.removeEventListener('keydown', handler);
+          };
+        }
+      },
+      selectedIndex(index) {
+        if (this.options.length) {
+          this.$emit('option-active', this.options[index]);
+        }
+      },
+      options() {
+        this.selectedIndex = 0;
+        if (this.options.length) {
+          this.$emit('option-active', this.options[this.selectedIndex]);
+        }
+      }
+    },
+    beforeUnmount() {
+      if (this.keydownDispose) {
+        this.keydownDispose();
+      }
+    },
+    methods: {
+      getLabel,
+      onOptionClick(option: Option, index: number) {
+        this.selectedIndex = index;
+        this.$emit('option-selected', option);
+      }
+    }
+  });
+</script>