edvoyui-component-library-test-flight 0.0.223 → 0.0.225

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.
@@ -19,6 +19,7 @@ declare function __VLS_template(): {
19
19
  }): any;
20
20
  };
21
21
  refs: {
22
+ dropdownRoot: HTMLDivElement;
22
23
  dropdownButton: HTMLButtonElement;
23
24
  dropdownMenu: HTMLDivElement;
24
25
  };
@@ -67,6 +68,15 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
67
68
  type: PropType<"top" | "bottom" | "left" | "right">;
68
69
  default: string;
69
70
  };
71
+ /**
72
+ * Keep the dropdown open while the user interacts with content inside the menu
73
+ * (search fields, row actions). Selecting an item and clicking outside still close it.
74
+ * Off by default so existing menus keep dismissing on any click inside them.
75
+ */
76
+ keepOpenOnContentClick: {
77
+ type: BooleanConstructor;
78
+ default: boolean;
79
+ };
70
80
  }>, {}, {}, {}, {}, import('../../../node_modules/vue').ComponentOptionsMixin, import('../../../node_modules/vue').ComponentOptionsMixin, {
71
81
  subMenuItem: (...args: any[]) => void;
72
82
  menuItem: (...args: any[]) => void;
@@ -113,6 +123,15 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
113
123
  type: PropType<"top" | "bottom" | "left" | "right">;
114
124
  default: string;
115
125
  };
126
+ /**
127
+ * Keep the dropdown open while the user interacts with content inside the menu
128
+ * (search fields, row actions). Selecting an item and clicking outside still close it.
129
+ * Off by default so existing menus keep dismissing on any click inside them.
130
+ */
131
+ keepOpenOnContentClick: {
132
+ type: BooleanConstructor;
133
+ default: boolean;
134
+ };
116
135
  }>> & Readonly<{
117
136
  onSubMenuItem?: ((...args: any[]) => any) | undefined;
118
137
  onMenuItem?: ((...args: any[]) => any) | undefined;
@@ -123,7 +142,9 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
123
142
  placement: "top" | "bottom" | "right" | "left";
124
143
  isSolidArrow: boolean;
125
144
  menuItems: MenuItem[];
145
+ keepOpenOnContentClick: boolean;
126
146
  }, {}, {}, {}, string, import('../../../node_modules/vue').ComponentProvideOptions, true, {
147
+ dropdownRoot: HTMLDivElement;
127
148
  dropdownButton: HTMLButtonElement;
128
149
  dropdownMenu: HTMLDivElement;
129
150
  }, HTMLDivElement>;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="relative">
2
+ <div ref="dropdownRoot" class="relative">
3
3
  <!-- Main Dropdown Button -->
4
4
  <button
5
5
  type="button"
@@ -34,8 +34,10 @@
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"
38
- @click.stop="$emit('menuItem', item)"
37
+ @mouseleave="scheduleClearActiveMenuItem(item.text)"
38
+ @focusin="setActiveMenuItem(item.text)"
39
+ @focusout="scheduleClearActiveMenuItem(item.text)"
40
+ @click.stop="selectMenuItem(item)"
39
41
  >
40
42
  <div
41
43
  class="flex items-center justify-between w-full gap-2 text-sm font-medium text-gray-800 break-words hover:text-gray-900"
@@ -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"
@@ -66,7 +69,7 @@
66
69
  <div
67
70
  v-if="item.enableAction"
68
71
  class="flex items-center justify-center w-full gap-1 px-6 py-3 text-sm font-medium text-gray-900 bg-purple-100 rounded-t-md"
69
- @click.prevent="$emit('actionItem', 'action')"
72
+ @click.stop.prevent="triggerActionItem()"
70
73
  >
71
74
  <slot name="actionName">{{ "+ Action Name" }}</slot>
72
75
  </div>
@@ -77,7 +80,7 @@
77
80
  v-for="subItem in item.subMenu"
78
81
  :key="subItem.text"
79
82
  class="flex items-center justify-between gap-2 px-3 py-2 text-sm font-medium text-gray-700 rounded-lg cursor-pointer hover:bg-gray-100 hover:text-gray-900"
80
- @click.stop="$emit('subMenuItem', subItem)"
83
+ @click.stop="selectSubMenuItem(subItem)"
81
84
  >
82
85
  <slot name="submenu" :subItem="subItem">
83
86
  {{ subItem.text }}
@@ -156,32 +159,84 @@ const props = defineProps({
156
159
  type: String as PropType<"top" | "bottom" | "left" | "right">,
157
160
  default: "bottom",
158
161
  },
162
+ /**
163
+ * Keep the dropdown open while the user interacts with content inside the menu
164
+ * (search fields, row actions). Selecting an item and clicking outside still close it.
165
+ * Off by default so existing menus keep dismissing on any click inside them.
166
+ */
167
+ keepOpenOnContentClick: {
168
+ type: Boolean,
169
+ default: false,
170
+ },
159
171
  });
160
172
 
161
173
  const isOpen = ref(false);
162
174
  const activeMenuItem = ref<string | null>(null);
175
+ const dropdownRoot = ref<HTMLElement | null>(null);
163
176
  const dropdownButton = ref<HTMLElement | null>(null);
164
177
  const dropdownMenu = ref<HTMLElement | null>(null);
165
178
  const resolvedPlacement = ref<"top" | "bottom" | "left" | "right">(
166
179
  props.placement
167
180
  );
181
+ let activeMenuCloseTimer: ReturnType<typeof setTimeout> | undefined;
168
182
 
169
- defineEmits(["subMenuItem", "menuItem", "actionItem"]);
183
+ const emit = defineEmits(["subMenuItem", "menuItem", "actionItem"]);
170
184
 
171
185
  const toggleDropdown = () => {
172
- isOpen.value = !isOpen.value;
186
+ if (isOpen.value) {
187
+ closeDropdown();
188
+ return;
189
+ }
190
+
191
+ isOpen.value = true;
192
+ };
193
+
194
+ const closeDropdown = () => {
195
+ clearTimeout(activeMenuCloseTimer);
196
+ isOpen.value = false;
197
+ activeMenuItem.value = null;
173
198
  };
174
199
 
175
200
  const setActiveMenuItem = (text: string) => {
201
+ clearTimeout(activeMenuCloseTimer);
176
202
  activeMenuItem.value = text;
177
203
  };
178
204
 
179
- const clearActiveMenuItem = () => {
180
- activeMenuItem.value = null;
205
+ const scheduleClearActiveMenuItem = (text: string) => {
206
+ clearTimeout(activeMenuCloseTimer);
207
+ activeMenuCloseTimer = setTimeout(() => {
208
+ // Interactive submenu content (for example, search) must stay mounted while focused.
209
+ if (dropdownMenu.value?.contains(document.activeElement)) return;
210
+ if (activeMenuItem.value === text) activeMenuItem.value = null;
211
+ }, 150);
212
+ };
213
+
214
+ const selectMenuItem = (item: MenuItem) => {
215
+ emit("menuItem", item);
216
+ closeDropdown();
217
+ };
218
+
219
+ const selectSubMenuItem = (subItem: MenuItem) => {
220
+ emit("subMenuItem", subItem);
221
+ closeDropdown();
181
222
  };
182
223
 
224
+ const triggerActionItem = () => {
225
+ emit("actionItem", "action");
226
+ closeDropdown();
227
+ };
228
+
229
+ // A click anywhere outside the dropdown always closes it.
230
+ onClickOutside(dropdownRoot, () => {
231
+ closeDropdown();
232
+ });
233
+
234
+ // Legacy behaviour: the watcher used to sit on the trigger button alone, so clicks on the
235
+ // menu itself counted as "outside" and dismissed it. Menus that host interactive content
236
+ // opt out with `keepOpenOnContentClick` - they close on selection instead.
183
237
  onClickOutside(dropdownButton, () => {
184
- isOpen.value = false;
238
+ if (props.keepOpenOnContentClick) return;
239
+ closeDropdown();
185
240
  });
186
241
 
187
242
  const updateDropdownPlacement = () => {
@@ -233,6 +288,7 @@ onMounted(() => {
233
288
  });
234
289
 
235
290
  onBeforeUnmount(() => {
291
+ clearTimeout(activeMenuCloseTimer);
236
292
  window.removeEventListener("resize", updateDropdownPlacement);
237
293
  window.removeEventListener("scroll", updateDropdownPlacement, true);
238
294
  });
@@ -63,7 +63,16 @@
63
63
  </template>
64
64
 
65
65
  <script lang="ts" setup>
66
- import { computed, onBeforeUnmount, onMounted, PropType, ref, toRefs, watch } from "vue";
66
+ import {
67
+ computed,
68
+ nextTick,
69
+ onBeforeUnmount,
70
+ onMounted,
71
+ PropType,
72
+ ref,
73
+ toRefs,
74
+ watch,
75
+ } from "vue";
67
76
  import SearchBigZoomIn from "../../assets/svg/SearchBigZoomIn.vue";
68
77
 
69
78
  const props = defineProps({
@@ -87,12 +96,13 @@ const emit = defineEmits(["update:modelValue"]);
87
96
 
88
97
  const input = ref<HTMLInputElement>();
89
98
  const isExpanded = ref(true);
90
- const searchQuery = ref();
99
+ const searchQuery = ref<string | number>(props.modelValue ?? "");
91
100
  const componentWrapper = ref<HTMLElement | null>(null);
92
101
 
93
- function expandInput() {
102
+ async function expandInput() {
94
103
  isExpanded.value = true;
95
- input?.value?.focus();
104
+ await nextTick();
105
+ input.value?.focus();
96
106
  }
97
107
 
98
108
  const inputValue = computed(() => {
@@ -124,10 +134,19 @@ watch(
124
134
  { immediate: true, deep: true }
125
135
  );
126
136
 
137
+ watch(
138
+ () => props.modelValue,
139
+ (newValue) => {
140
+ if (newValue !== searchQuery.value) {
141
+ searchQuery.value = newValue ?? "";
142
+ }
143
+ }
144
+ );
145
+
127
146
  onMounted(() => {
128
147
  document.addEventListener("click", handleOutsideClick);
129
- if (searchQuery.value) {
130
- searchQuery.value?.focus();
148
+ if (isExpanded.value) {
149
+ nextTick(() => input.value?.focus());
131
150
  }
132
151
  });
133
152