edvoyui-component-library-test-flight 0.0.244 → 0.0.246

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.
@@ -28,6 +28,10 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
28
28
  type: BooleanConstructor;
29
29
  default: boolean;
30
30
  };
31
+ indeterminate: {
32
+ type: BooleanConstructor;
33
+ default: boolean;
34
+ };
31
35
  label: {
32
36
  type: PropType<string>;
33
37
  required: false;
@@ -56,6 +60,10 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
56
60
  type: BooleanConstructor;
57
61
  default: boolean;
58
62
  };
63
+ indeterminate: {
64
+ type: BooleanConstructor;
65
+ default: boolean;
66
+ };
59
67
  label: {
60
68
  type: PropType<string>;
61
69
  required: false;
@@ -70,6 +78,7 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
70
78
  name: string;
71
79
  errors: Record<string, any>;
72
80
  modelValue: boolean;
81
+ indeterminate: boolean;
73
82
  }, {}, {}, {}, string, import('../../../node_modules/vue').ComponentProvideOptions, true, {}, HTMLDivElement>;
74
83
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
75
84
  export default _default;
@@ -1,8 +1,16 @@
1
1
  <template>
2
2
  <div>
3
- <label class="flex flex-row items-center scale-100 translate-x-0 translate-y-0 eui-checkbox">
4
- <input v-model="isChecked" type="checkbox" id="checkbox" class="appearance-none sr-only input-checkbox" :disabled="disabled"
5
- @change="toggle($event)" />
3
+ <label class="flex flex-row items-center scale-100 translate-x-0 translate-y-0 eui-checkbox" :class="{'select-none pointer-events-none opacity-50':disabled}">
4
+ <input
5
+ v-model="isChecked"
6
+ type="checkbox"
7
+ id="checkbox"
8
+ class="appearance-none sr-only input-checkbox"
9
+ :disabled="disabled"
10
+ :indeterminate.prop="indeterminate"
11
+ :aria-checked="indeterminate ? 'mixed' : isChecked"
12
+ @change="toggle($event)"
13
+ />
6
14
  <div class="cbx-icon" for="checkbox">
7
15
  <span>
8
16
  <svg width="14px" height="14px">
@@ -45,6 +53,10 @@ const props = defineProps({
45
53
  type: Boolean,
46
54
  default: false,
47
55
  },
56
+ indeterminate: {
57
+ type: Boolean,
58
+ default: false,
59
+ },
48
60
  label: { type: String as PropType<string>, required: false, default: "" },
49
61
  });
50
62
 
@@ -56,9 +68,7 @@ watch(isChecked, (newValue) => {
56
68
  });
57
69
 
58
70
  const toggle = ($event: Event) => {
59
- if($event) {
60
- emit("toggle", isChecked.value);
61
- }
71
+ emit("toggle", ($event.target as HTMLInputElement).checked);
62
72
  };
63
73
 
64
74
  watch(props, (newValue) => {
@@ -69,37 +79,55 @@ watch(props, (newValue) => {
69
79
  <style lang="scss">
70
80
  .eui-checkbox .cbx-icon {
71
81
  transition: all 0.2s ease;
72
- @apply select-none cursor-pointer p-px rounded-md overflow-hidden inline-block scale-110;
82
+ @apply select-none cursor-pointer p-px rounded-md overflow-hidden inline-block scale-100;
73
83
  }
74
84
 
75
85
  .eui-checkbox .cbx-icon span {
76
86
  transform: translate3d(0, 0, 0);
77
87
  transition: all 0.2s ease;
78
- @apply float-left align-middle relative size-5 border-[1.5px] border-gray-400 rounded scale-100;
88
+ @apply float-left align-middle relative size-5 -outline-offset-1 outline outline-1 outline-gray-400 rounded scale-100;
79
89
  }
80
90
 
81
91
  .eui-checkbox .cbx-icon span > svg {
82
- top: 3.5px;
83
- left: 2.5px;
92
+ top: 5px;
93
+ left: 4px;
84
94
  stroke-width: 1.75;
85
95
  stroke-linecap: round;
86
96
  stroke-linejoin: round;
87
97
  stroke-dasharray: 18px;
88
98
  stroke-dashoffset: 18px;
89
- transition: all 0.3s ease;
90
- transition-delay: 0.1s;
99
+ transition: stroke-dashoffset 0.16s ease-out;
91
100
  transform: translate3d(0, 0, 0);
92
101
  @apply absolute fill-none stroke-white;
93
102
  }
94
103
 
95
- .eui-checkbox .input-checkbox:checked + .cbx-icon > span {
96
- animation: anim 0.4s ease;
97
- @apply bg-purple-700 border-purple-700;
104
+ .eui-checkbox .input-checkbox:checked:not(:indeterminate) + .cbx-icon > span {
105
+ animation: anim 0.18s ease-out;
106
+ @apply bg-violet-700 outline-violet-700;
98
107
  }
99
108
 
100
- .eui-checkbox .input-checkbox:checked + .cbx-icon span > svg {
109
+ .eui-checkbox .input-checkbox:indeterminate + .cbx-icon > span {
110
+ @apply bg-violet-700 outline-violet-700;
111
+ }
112
+
113
+ .eui-checkbox .input-checkbox:indeterminate + .cbx-icon span > svg {
114
+ stroke-dashoffset: 18px;
115
+ transition: none;
116
+ }
117
+
118
+ .eui-checkbox .input-checkbox:indeterminate + .cbx-icon > span::after {
119
+ content: '';
120
+ animation: indeterminate-in 0.14s ease-out both;
121
+ @apply absolute left-1/2 top-1/2 h-0.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded bg-white;
122
+ }
123
+
124
+ .eui-checkbox .input-checkbox:checked:not(:indeterminate) + .cbx-icon span > svg {
101
125
  stroke-dashoffset: 0;
102
- transition: rotate(8deg);
126
+ }
127
+
128
+ @keyframes indeterminate-in {
129
+ from { opacity: 0; scale: 0.5 1; }
130
+ to { opacity: 1; scale: 1 1; }
103
131
  }
104
132
 
105
133
  @keyframes anim {
@@ -17,7 +17,7 @@
17
17
  'is-open': isDatepickerOpen,
18
18
  },
19
19
  inputFilled
20
- ? 'overflow-hidden border border-gray-100 cursor-pointer group h-14 rounded-xl eui-datepicker'
20
+ ? 'overflow-hidden border border-gray-200 cursor-pointer group h-14 rounded-xl eui-datepicker'
21
21
  : 'h-10 eui-datepicker normal',
22
22
  'relative w-full mb-2',
23
23
  ]"
@@ -283,7 +283,7 @@ const clearIconRight = computed(() =>
283
283
  @apply min-w-44;
284
284
  .dp__main {
285
285
  .dp__input {
286
- @apply pb-2 pt-6 border-0 transition-colors duration-100 ease-in-out text-sm font-medium leading-6 placeholder:text-gray-400 placeholder:font-normal;
286
+ @apply pb-2 pt-6 border-0 transition-colors duration-100 ease-in-out text-sm font-medium leading-6 placeholder:text-gray-600 placeholder:font-normal;
287
287
  padding-left: v-bind(iconshowLeft);
288
288
  padding-right: v-bind(iconshowRight);
289
289
  }
@@ -300,7 +300,7 @@ const clearIconRight = computed(() =>
300
300
  &.normal {
301
301
  .dp__main {
302
302
  .dp__input {
303
- @apply py-2 ring-1 ring-gray-100;
303
+ @apply py-2 ring-1 ring-gray-200;
304
304
  }
305
305
  .dp__input.dp__input_focus {
306
306
  @apply ring-2 h-10 rounded-md ring-purple-600;
@@ -15,6 +15,8 @@ interface Props {
15
15
  searchable?: boolean;
16
16
  backendPagination?: boolean;
17
17
  hideNoData?: boolean;
18
+ /** Show a loading state while options are being fetched. */
19
+ loading?: boolean;
18
20
  disabled?: boolean;
19
21
  tag?: string;
20
22
  tagColor?: "None" | "Success" | "Error";
@@ -26,6 +28,13 @@ interface Props {
26
28
  ignoreDeselect?: boolean;
27
29
  popperClasses?: string;
28
30
  showCount?: boolean;
31
+ /** Show an All row when search has results; it selects every item in `items`. Enabled by default in multiple mode. */
32
+ showSelectAll?: boolean;
33
+ selectAllLabel?: string;
34
+ /** Maximum selected tags shown before the More/Less control; 0 shows all. */
35
+ maxVisibleTags?: number;
36
+ tagSize?: "sm" | "md" | "lg";
37
+ dropdownWidth?: string;
29
38
  }
30
39
  export type SelectItemKey = string | (string | number)[] | ((item: object, fallback?: any) => any);
31
40
  declare function __VLS_template(): {
@@ -176,6 +185,7 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
176
185
  onCreate?: ((value: string) => any) | undefined;
177
186
  "onOn-deselect"?: ((value: any) => any) | undefined;
178
187
  }>, {
188
+ loading: boolean;
179
189
  disabled: boolean;
180
190
  name: string;
181
191
  items: any[];
@@ -199,6 +209,11 @@ declare const __VLS_component: import('../../../node_modules/vue').DefineCompone
199
209
  ignoreDeselect: boolean;
200
210
  popperClasses: string;
201
211
  showCount: boolean;
212
+ showSelectAll: boolean;
213
+ selectAllLabel: string;
214
+ maxVisibleTags: number;
215
+ tagSize: "sm" | "md" | "lg";
216
+ dropdownWidth: string;
202
217
  }, {}, {}, {}, string, import('../../../node_modules/vue').ComponentProvideOptions, false, {
203
218
  componentRef: HTMLDivElement;
204
219
  searchInputRef: import('../../../node_modules/vue').CreateComponentPublicInstanceWithMixins<Readonly<import('../../../node_modules/vue').ExtractPropTypes<{
@@ -38,6 +38,7 @@
38
38
  <button
39
39
  v-else-if="!showDropDown && selectedValues.length > 0"
40
40
  type="button"
41
+ :disabled="disabled"
41
42
  class="flex items-center justify-between w-full h-10 gap-2 px-4 py-2 text-left bg-white border border-gray-200 border-solid cursor-pointer search-textstyle rounded-3xl"
42
43
  @click="handleOpen"
43
44
  >
@@ -85,10 +86,10 @@
85
86
  <template #content>
86
87
  <div
87
88
  v-if="showDropDown"
88
- class="w-full p-4 overflow-y-auto text-sm bg-white border-2 border-gray-100 border-solid shadow-lg rounded-3xl max-h-72 scrollbar--hide"
89
+ :class="['w-full p-4 overflow-y-auto text-sm bg-white border-2 border-gray-100 border-solid shadow-lg rounded-3xl max-h-72 scrollbar--hide min-w-28', dropdownWidth]"
89
90
  >
90
91
  <h3
91
- class="sticky z-10 p-2 mb-2 -mx-px -mt-3 font-semibold text-gray-500 bg-white text-sm/4 -top-4"
92
+ class="sticky z-10 p-2 pb-0 mb-2 -mx-px -mt-3 text-xs font-bold tracking-wide text-gray-500 uppercase bg-white -top-4"
92
93
  >
93
94
  Search Results
94
95
  </h3>
@@ -100,14 +101,48 @@
100
101
  aria-activedescendant="listbox-option-3"
101
102
  class="space-y-0.5"
102
103
  >
103
- <li v-if="filteredItems.length === 0 && !hideNoData">
104
+ <li
105
+ v-if="selectType === 'multiple' && showSelectAll && !loading && filteredItems.length"
106
+ role="option"
107
+ :aria-selected="allItemsSelected"
108
+ class="pb-0.5 border-b border-gray-100"
109
+ @click.stop
110
+ >
111
+ <EUICheckbox
112
+ :model-value="allItemsSelected"
113
+ :indeterminate="someItemsSelected && !allItemsSelected"
114
+ :disabled="disabled"
115
+ :class="[
116
+ 'relative flex items-center text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:h-9 [&_label]:px-2.5',
117
+ allItemsSelected || someItemsSelected
118
+ ? 'text-gray-800 hover:bg-violet-50 hover:text-violet-600'
119
+ : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
120
+ ]"
121
+ @toggle="handleSelectAll"
122
+ >
123
+ <template #label>
124
+ <span class="ms-2">{{ selectAllLabel }}</span>
125
+ </template>
126
+ </EUICheckbox>
127
+ </li>
128
+ <li v-if="loading" role="status" aria-live="polite" class="px-2 py-4 text-sm text-gray-500">
129
+ <span class="inline-flex items-center gap-2">
130
+ <span class="inline-block border-2 border-gray-200 rounded-full size-4 border-t-violet-600 animate-spin" aria-hidden="true"></span>
131
+ Loading options...
132
+ </span>
133
+ </li>
134
+ <li v-else-if="filteredItems.length === 0 && !hideNoData">
104
135
  <slot name="no-items">
105
136
  <div
106
- v-if="internalSearch.length > 0"
107
- class="px-2 py-4 mb-6 text-sm font-medium text-center text-gray-500 min-h-32"
137
+ class="px-2 py-4 text-sm font-medium text-center text-gray-500"
108
138
  >
109
- No results found for "<strong>{{ internalSearch }}</strong
110
- >"
139
+ <template v-if="internalSearch.trim()">
140
+ No results found for "<strong>{{ internalSearch }}</strong>"
141
+ </template>
142
+ <template v-else-if="backendPagination">
143
+ Type in the search box to see results
144
+ </template>
145
+ <template v-else>No options available</template>
111
146
  </div>
112
147
  </slot>
113
148
  </li>
@@ -135,7 +170,7 @@
135
170
  <div
136
171
  :class="[
137
172
  selected(item)
138
- ? 'bg-purple-50 text-purple-600'
173
+ ? 'bg-violet-50 text-violet-600'
139
174
  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
140
175
  'relative py-2 px-4 flex items-center capitalize text-sm transition-colors duration-75 rounded',
141
176
  ]"
@@ -169,11 +204,11 @@
169
204
  name="checkbox"
170
205
  :class="[
171
206
  selected(item)
172
- ? 'text-gray-800 hover:bg-purple-50 hover:text-purple-600'
207
+ ? 'text-gray-800 hover:bg-violet-50 hover:text-violet-600'
173
208
  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800',
174
- 'relative flex items-center capitalize text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:p-2.5',
209
+ 'relative flex items-center capitalize text-sm transition-colors duration-75 w-full multiple-select rounded-md font-medium [&_label]:w-full [&_label]:h-9 [&_label]:px-2.5',
175
210
  ]"
176
- @update:model-value="
211
+ @toggle="
177
212
  (checked) => handleCheckboxChange(item, checked)
178
213
  "
179
214
  >
@@ -197,26 +232,50 @@
197
232
  <slot v-if="$slots.labelhint" name="labelhint"></slot>
198
233
  <div
199
234
  v-if="selectType === 'multiple' && selectedValues.length > 0 && showTags"
200
- class="px-0.5 my-2 inline-flex gap-x-3 gap-y-2 items-center flex-wrap"
235
+ class="my-2 space-y-2"
201
236
  >
202
- <slot
203
- v-for="(item, index) in selectedValues"
204
- name="selection"
205
- :item="item"
206
- :clear="() => handleDeselect(item)"
207
- :key="`selected-values-${index}`"
237
+ <div
238
+ class="px-0.5 gap-2 items-center flex-wrap"
239
+ :class="[
240
+ effectiveTagLimit ? 'flex w-full' : 'inline-flex',
241
+ tagsExpanded && effectiveTagLimit ? 'max-h-32 overflow-y-auto scrollbar--thin' : '',
242
+ ]"
208
243
  >
209
- <EUITag
210
- class="capitalize"
211
- :disabled="disabled"
212
- closeIcon
213
- size="md"
214
- color="secondary"
215
- @remove="handleTagRemove(item)"
244
+ <slot
245
+ v-for="(item, index) in visibleSelectedValues"
246
+ name="selection"
247
+ :item="item"
248
+ :clear="() => handleDeselect(item)"
249
+ :key="`selected-values-${index}`"
216
250
  >
217
- {{ startCase(item[itemText] || item) }}
218
- </EUITag>
219
- </slot>
251
+ <EUITag
252
+ class="capitalize"
253
+ :disabled="disabled"
254
+ :closeIcon="!disabled"
255
+ :size="tagSize"
256
+ color="secondary"
257
+ @remove="handleTagRemove(item)"
258
+ >
259
+ {{ startCase(item[itemText] || item) }}
260
+ </EUITag>
261
+ </slot>
262
+ <button
263
+ v-if="canExpandTags && !tagsExpanded"
264
+ type="button"
265
+ class="text-xs font-medium text-violet-600 hover:text-violet-700"
266
+ @click="tagsExpanded = true"
267
+ >
268
+ +{{ selectedValues.length - effectiveTagLimit }} more
269
+ </button>
270
+ </div>
271
+ <button
272
+ v-if="canExpandTags && tagsExpanded"
273
+ type="button"
274
+ class="text-xs font-medium text-gray-600 hover:text-gray-700"
275
+ @click="tagsExpanded = false"
276
+ >
277
+ Show less
278
+ </button>
220
279
  </div>
221
280
  <div
222
281
  class="absolute top-0 px-2 text-xs text-white rounded right-6"
@@ -278,6 +337,8 @@ interface Props {
278
337
  searchable?: boolean;
279
338
  backendPagination?: boolean;
280
339
  hideNoData?: boolean;
340
+ /** Show a loading state while options are being fetched. */
341
+ loading?: boolean;
281
342
  disabled?: boolean;
282
343
  tag?: string;
283
344
  tagColor?: "None" | "Success" | "Error";
@@ -289,6 +350,13 @@ interface Props {
289
350
  ignoreDeselect?: boolean;
290
351
  popperClasses?: string;
291
352
  showCount?: boolean;
353
+ /** Show an All row when search has results; it selects every item in `items`. Enabled by default in multiple mode. */
354
+ showSelectAll?: boolean;
355
+ selectAllLabel?: string;
356
+ /** Maximum selected tags shown before the More/Less control; 0 shows all. */
357
+ maxVisibleTags?: number;
358
+ tagSize?: "sm" | "md" | "lg";
359
+ dropdownWidth?: string,
292
360
  }
293
361
  const props = withDefaults(defineProps<Props>(), {
294
362
  items: () => [],
@@ -302,6 +370,7 @@ const props = withDefaults(defineProps<Props>(), {
302
370
  searchable: true,
303
371
  backendPagination: false,
304
372
  hideNoData: false,
373
+ loading: false,
305
374
  disabled: false,
306
375
  tag: "",
307
376
  tagColor: "None",
@@ -314,6 +383,11 @@ const props = withDefaults(defineProps<Props>(), {
314
383
  ignoreDeselect: false,
315
384
  popperClasses: "",
316
385
  showCount: false,
386
+ showSelectAll: true,
387
+ selectAllLabel: "All",
388
+ maxVisibleTags: 0,
389
+ tagSize: "md",
390
+ dropdownWidth: ''
317
391
  });
318
392
 
319
393
 
@@ -336,7 +410,8 @@ const showDropDown = ref(false);
336
410
  const options = ref<HTMLElement>();
337
411
  const searchInputRef = ref<any>();
338
412
  const componentRef = ref<HTMLElement>();
339
- const isOpening = ref(false);
413
+ const tagsExpanded = ref(false);
414
+ let focusTimer: ReturnType<typeof setTimeout> | undefined;
340
415
 
341
416
  // Computed properties
342
417
  const selectedValues = computed({
@@ -362,6 +437,37 @@ const selected = computed(() => {
362
437
  };
363
438
  });
364
439
 
440
+ const sameItem = (a: any, b: any) =>
441
+ a != null &&
442
+ b != null &&
443
+ getPropertyFromItem(a, props.itemText) ===
444
+ getPropertyFromItem(b, props.itemText);
445
+
446
+ const allItemsSelected = computed(
447
+ () => props.items.length > 0 && props.items.every((item) => selected.value(item))
448
+ );
449
+ const someItemsSelected = computed(() =>
450
+ props.items.some((item) => selected.value(item))
451
+ );
452
+
453
+ const effectiveTagLimit = computed(() =>
454
+ Number.isFinite(props.maxVisibleTags) && props.maxVisibleTags > 0
455
+ ? Math.floor(props.maxVisibleTags)
456
+ : 0
457
+ );
458
+ const canExpandTags = computed(
459
+ () => effectiveTagLimit.value > 0 && selectedValues.value.length > effectiveTagLimit.value
460
+ );
461
+ const visibleSelectedValues = computed(() =>
462
+ canExpandTags.value && !tagsExpanded.value
463
+ ? selectedValues.value.slice(0, effectiveTagLimit.value)
464
+ : selectedValues.value
465
+ );
466
+
467
+ watch(canExpandTags, (canExpand) => {
468
+ if (!canExpand) tagsExpanded.value = false;
469
+ });
470
+
365
471
  export type SelectItemKey =
366
472
  | string
367
473
  | (string | number)[]
@@ -434,8 +540,7 @@ const filteredItems = computed(() => {
434
540
  });
435
541
 
436
542
  const handleDeselect = (item: any) => {
437
- // console.log("called with:", item, selectedValues.value);
438
- if (props.ignoreDeselect) return;
543
+ if (props.ignoreDeselect || props.disabled) return;
439
544
  let index = -1;
440
545
  if (typeof item === "object" && item[props.itemText]) {
441
546
  index = selectedValues.value.findIndex(
@@ -464,10 +569,26 @@ const handleDeselect = (item: any) => {
464
569
  };
465
570
 
466
571
  const handleClearAll = () => {
572
+ if (props.disabled) return;
467
573
  emit("update:modelValue", []);
468
574
  emit("toggle");
469
575
  };
470
576
 
577
+ const handleSelectAll = (checked: boolean) => {
578
+ if (props.disabled || props.selectType !== "multiple") return;
579
+ const next = checked
580
+ ? [
581
+ ...selectedValues.value,
582
+ ...props.items.filter((item) => !selected.value(item)),
583
+ ]
584
+ : selectedValues.value.filter(
585
+ (value: any) => !props.items.some((item) => sameItem(value, item))
586
+ );
587
+ emit("update:modelValue", next);
588
+ emit("on-change", next);
589
+ emit("toggle", next);
590
+ };
591
+
471
592
  const handleTagRemove = (item: any) => {
472
593
  handleDeselect(item);
473
594
  };
@@ -499,6 +620,7 @@ const handleUpdate = (items: any) => {
499
620
  };
500
621
 
501
622
  const handleSelect = (item: any) => {
623
+ if (props.disabled) return;
502
624
  let newSelection: any;
503
625
 
504
626
  if (props.selectType === "multiple" && typeof item === "object") {
@@ -537,25 +659,28 @@ const handleSelect = (item: any) => {
537
659
  };
538
660
 
539
661
  const handleOpen = () => {
540
- isOpening.value = true;
662
+ if (props.disabled || showDropDown.value) return;
541
663
 
542
664
  // Only clear search for single select, preserve it for multiple select
543
665
  if (props.selectType === "single") {
544
666
  internalSearch.value = "";
545
667
  }
546
668
  showDropDown.value = true;
669
+ window.addEventListener("scroll", handleScroll, true);
547
670
  emit("open");
548
671
 
549
672
  nextTick(() => {
550
- setTimeout(() => {
673
+ if (!showDropDown.value) return;
674
+ focusTimer = setTimeout(() => {
675
+ focusTimer = undefined;
676
+ if (!showDropDown.value) return;
551
677
  internalSearch.value = "";
552
- isOpening.value = false;
553
678
  if (searchInputRef.value?.searchInput) {
554
- searchInputRef.value.searchInput.focus();
679
+ searchInputRef.value.searchInput.focus({ preventScroll: true });
555
680
  } else if (searchInputRef.value?.$el) {
556
681
  const inputElement = searchInputRef.value.$el.querySelector("input");
557
682
  if (inputElement) {
558
- inputElement.focus();
683
+ inputElement.focus({ preventScroll: true });
559
684
  }
560
685
  }
561
686
  }, 50);
@@ -563,6 +688,11 @@ const handleOpen = () => {
563
688
  };
564
689
 
565
690
  const handleClose = () => {
691
+ window.removeEventListener("scroll", handleScroll, true);
692
+ if (focusTimer) {
693
+ clearTimeout(focusTimer);
694
+ focusTimer = undefined;
695
+ }
566
696
  if (showDropDown.value) {
567
697
  emit("blur");
568
698
  emit("close");
@@ -581,30 +711,25 @@ const handleSearchFocus = () => {
581
711
 
582
712
  // Document click handler for outside clicks
583
713
  const handleDocumentClick = (event: MouseEvent) => {
584
- // Don't close if we're currently opening
585
- if (isOpening.value) return;
586
-
587
- const target = event.target as Node;
588
-
589
- // Check if click is outside the component and popper
590
- if (showDropDown.value && componentRef.value) {
591
- // Check if click is outside the main component
592
- const isOutsideComponent = !componentRef.value.contains(target);
593
-
594
- // Check if click is outside any popper content
595
- const popperElements = document.querySelectorAll(
596
- '[data-popper-placement], .popper-defaultstyle, [role="tooltip"]'
597
- );
598
- const isOutsidePopper = Array.from(popperElements).every(
599
- (popper) => !popper.contains(target)
600
- );
601
-
602
- if (isOutsideComponent && isOutsidePopper) {
603
- handleClose();
604
- }
714
+ // The trigger can be replaced during this click. composedPath retains its
715
+ // original ancestors, so an inside click stays inside after that DOM update.
716
+ if (
717
+ showDropDown.value &&
718
+ componentRef.value &&
719
+ !event.composedPath().includes(componentRef.value)
720
+ ) {
721
+ handleClose();
605
722
  }
606
723
  };
607
724
 
725
+ const handleScroll = (event: Event) => {
726
+ if (!showDropDown.value) return;
727
+ const target = event.target;
728
+ // Keep the options usable; scrolling any ancestor or the page closes them.
729
+ if (target instanceof Node && componentRef.value?.contains(target)) return;
730
+ handleClose();
731
+ };
732
+
608
733
  // Lifecycle hooks
609
734
  onMounted(() => {
610
735
  document.addEventListener("click", handleDocumentClick);
@@ -612,6 +737,8 @@ onMounted(() => {
612
737
 
613
738
  onUnmounted(() => {
614
739
  document.removeEventListener("click", handleDocumentClick);
740
+ window.removeEventListener("scroll", handleScroll, true);
741
+ if (focusTimer) clearTimeout(focusTimer);
615
742
  });
616
743
 
617
744
  // Watchers
@@ -619,13 +746,6 @@ watch(internalSearch, (value: string) => {
619
746
  emit("search", value);
620
747
  });
621
748
 
622
- watch(
623
- () => props.modelValue,
624
- (newValue, oldValue) => {
625
- console.log("changed from:", oldValue, "to:", newValue);
626
- },
627
- { deep: true }
628
- );
629
749
  </script>
630
750
 
631
751
  <style lang="scss">
@@ -22,6 +22,7 @@
22
22
  :class="['search-input', hasError ? 'error-state' : '']"
23
23
  :placeholder="placeholder"
24
24
  :value="modelValue"
25
+ :disabled="disabled"
25
26
  @input="emitInput"
26
27
  @focus="onFocus"
27
28
  @blur="onBlur"