edvoyui-component-library-test-flight 0.0.224 → 0.0.226

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,9 @@ declare function __VLS_template(): {
14
14
  menuitem: MenuItem;
15
15
  }): any;
16
16
  actionName?(_: {}): any;
17
+ emptySubmenu?(_: {
18
+ menuitem: MenuItem;
19
+ }): any;
17
20
  submenu?(_: {
18
21
  subItem: MenuItem;
19
22
  }): any;
@@ -34,7 +34,9 @@
34
34
  :key="item.text"
35
35
  class="relative flex items-center px-4 py-2 rounded-lg cursor-pointer hover:bg-gray-100"
36
36
  @mouseenter="setActiveMenuItem(item.text)"
37
- @mouseleave="clearActiveMenuItem"
37
+ @mouseleave="scheduleClearActiveMenuItem(item.text)"
38
+ @focusin="setActiveMenuItem(item.text)"
39
+ @focusout="scheduleClearActiveMenuItem(item.text)"
38
40
  @click.stop="selectMenuItem(item)"
39
41
  >
40
42
  <div
@@ -59,6 +61,7 @@
59
61
  <div
60
62
  v-if="item.subMenu && activeMenuItem === item.text"
61
63
  :class="['absolute top-0 z-10 transition-all duration-300 ease-in-out min-w-32 max-w-64 w-max', placement === 'right' ? 'right-full' : 'left-full']"
64
+ @mouseenter="setActiveMenuItem(item.text)"
62
65
  >
63
66
  <div
64
67
  class="bg-white border border-gray-200 border-solid rounded-lg shadow-2xl ms-2 shadow-gray-300"
@@ -73,6 +76,11 @@
73
76
  <div
74
77
  class="p-2 overflow-y-auto overscroll-auto max-h-[50svh] min-h-0 scrollbar--thin"
75
78
  >
79
+ <slot
80
+ v-if="item.subMenu.length === 0"
81
+ name="emptySubmenu"
82
+ :menuitem="item"
83
+ />
76
84
  <div
77
85
  v-for="subItem in item.subMenu"
78
86
  :key="subItem.text"
@@ -175,24 +183,37 @@ const dropdownMenu = ref<HTMLElement | null>(null);
175
183
  const resolvedPlacement = ref<"top" | "bottom" | "left" | "right">(
176
184
  props.placement
177
185
  );
186
+ let activeMenuCloseTimer: ReturnType<typeof setTimeout> | undefined;
178
187
 
179
188
  const emit = defineEmits(["subMenuItem", "menuItem", "actionItem"]);
180
189
 
181
190
  const toggleDropdown = () => {
182
- isOpen.value = !isOpen.value;
191
+ if (isOpen.value) {
192
+ closeDropdown();
193
+ return;
194
+ }
195
+
196
+ isOpen.value = true;
183
197
  };
184
198
 
185
199
  const closeDropdown = () => {
200
+ clearTimeout(activeMenuCloseTimer);
186
201
  isOpen.value = false;
187
202
  activeMenuItem.value = null;
188
203
  };
189
204
 
190
205
  const setActiveMenuItem = (text: string) => {
206
+ clearTimeout(activeMenuCloseTimer);
191
207
  activeMenuItem.value = text;
192
208
  };
193
209
 
194
- const clearActiveMenuItem = () => {
195
- activeMenuItem.value = null;
210
+ const scheduleClearActiveMenuItem = (text: string) => {
211
+ clearTimeout(activeMenuCloseTimer);
212
+ activeMenuCloseTimer = setTimeout(() => {
213
+ // Interactive submenu content (for example, search) must stay mounted while focused.
214
+ if (dropdownMenu.value?.contains(document.activeElement)) return;
215
+ if (activeMenuItem.value === text) activeMenuItem.value = null;
216
+ }, 150);
196
217
  };
197
218
 
198
219
  const selectMenuItem = (item: MenuItem) => {
@@ -272,6 +293,7 @@ onMounted(() => {
272
293
  });
273
294
 
274
295
  onBeforeUnmount(() => {
296
+ clearTimeout(activeMenuCloseTimer);
275
297
  window.removeEventListener("resize", updateDropdownPlacement);
276
298
  window.removeEventListener("scroll", updateDropdownPlacement, true);
277
299
  });
@@ -21,10 +21,13 @@
21
21
 
22
22
  <button
23
23
  v-if="isExpanded"
24
+ type="button"
25
+ aria-label="Clear search"
24
26
  :class="
25
27
  inputValue ? ' absolute inset-y-0 right-3 ' : ' hidden '
26
28
  "
27
- @click="(searchQuery=''), emit('update:modelValue', '')"
29
+ @mousedown.prevent
30
+ @click.stop="clearSearch"
28
31
  >
29
32
  <svg
30
33
  xmlns="http://www.w3.org/2000/svg"
@@ -63,7 +66,16 @@
63
66
  </template>
64
67
 
65
68
  <script lang="ts" setup>
66
- import { computed, onBeforeUnmount, onMounted, PropType, ref, toRefs, watch } from "vue";
69
+ import {
70
+ computed,
71
+ nextTick,
72
+ onBeforeUnmount,
73
+ onMounted,
74
+ PropType,
75
+ ref,
76
+ toRefs,
77
+ watch,
78
+ } from "vue";
67
79
  import SearchBigZoomIn from "../../assets/svg/SearchBigZoomIn.vue";
68
80
 
69
81
  const props = defineProps({
@@ -87,12 +99,20 @@ const emit = defineEmits(["update:modelValue"]);
87
99
 
88
100
  const input = ref<HTMLInputElement>();
89
101
  const isExpanded = ref(true);
90
- const searchQuery = ref();
102
+ const searchQuery = ref<string | number>(props.modelValue ?? "");
91
103
  const componentWrapper = ref<HTMLElement | null>(null);
92
104
 
93
- function expandInput() {
105
+ async function expandInput() {
94
106
  isExpanded.value = true;
95
- input?.value?.focus();
107
+ await nextTick();
108
+ input.value?.focus();
109
+ }
110
+
111
+ async function clearSearch() {
112
+ searchQuery.value = "";
113
+ emit("update:modelValue", "");
114
+ await nextTick();
115
+ input.value?.focus();
96
116
  }
97
117
 
98
118
  const inputValue = computed(() => {
@@ -124,10 +144,19 @@ watch(
124
144
  { immediate: true, deep: true }
125
145
  );
126
146
 
147
+ watch(
148
+ () => props.modelValue,
149
+ (newValue) => {
150
+ if (newValue !== searchQuery.value) {
151
+ searchQuery.value = newValue ?? "";
152
+ }
153
+ }
154
+ );
155
+
127
156
  onMounted(() => {
128
157
  document.addEventListener("click", handleOutsideClick);
129
- if (searchQuery.value) {
130
- searchQuery.value?.focus();
158
+ if (isExpanded.value) {
159
+ nextTick(() => input.value?.focus());
131
160
  }
132
161
  });
133
162