vueless 1.0.1 → 1.0.2-beta.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.0.1",
3
+ "version": "1.0.2-beta.0",
4
4
  "license": "MIT",
5
5
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
6
6
  "keywords": [
@@ -72,6 +72,11 @@ const emit = defineEmits([
72
72
  * Triggers when content pasted to the input.
73
73
  */
74
74
  "paste",
75
+
76
+ /**
77
+ * Triggers when a key is pressed down while the input is focused.
78
+ */
79
+ "keydown",
75
80
  ]);
76
81
 
77
82
  const VALIDATION_RULES_REG_EX = {
@@ -164,6 +169,10 @@ function onCopy(event: ClipboardEvent) {
164
169
  emit("copy", event);
165
170
  }
166
171
 
172
+ function onKeydown(event: KeyboardEvent) {
173
+ emit("keydown", event);
174
+ }
175
+
167
176
  /**
168
177
  * This trick prevents default browser autocomplete behavior.
169
178
  * @param toggleState { boolean }
@@ -295,6 +304,7 @@ const {
295
304
  @click="onClick"
296
305
  @paste="onPaste"
297
306
  @copy="onCopy"
307
+ @keydown="onKeydown"
298
308
  />
299
309
 
300
310
  <div
@@ -51,6 +51,11 @@ const emit = defineEmits([
51
51
  * Triggers when the search input value changes.
52
52
  */
53
53
  "searchChange",
54
+
55
+ /**
56
+ * Triggers when the search input loses focus.
57
+ */
58
+ "searchBlur",
54
59
  ]);
55
60
 
56
61
  const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
@@ -220,6 +225,14 @@ function onSearchChange(value: string) {
220
225
  emit("searchChange", value);
221
226
  }
222
227
 
228
+ function onKeydownUp() {
229
+ wrapperRef.value?.focus();
230
+ }
231
+
232
+ function onKeydownDown() {
233
+ wrapperRef.value?.focus();
234
+ }
235
+
223
236
  function isMetaKey(key: string) {
224
237
  return ["isSubGroup", "groupLabel", "level", "isHidden", "onClick", "divider"].includes(key);
225
238
  }
@@ -307,6 +320,12 @@ function onClickOption(rawOption: Option) {
307
320
  emit("clickOption", option);
308
321
  }
309
322
 
323
+ function onInputSearchBlur(event: FocusEvent) {
324
+ if (props.searchable) {
325
+ emit("searchBlur", event);
326
+ }
327
+ }
328
+
310
329
  defineExpose({
311
330
  /**
312
331
  * Allows setting the pointer to a specific index.
@@ -408,6 +427,9 @@ const {
408
427
  :debounce="debounce"
409
428
  v-bind="listboxInputAttrs"
410
429
  :data-test="getDataTest('search')"
430
+ @blur="onInputSearchBlur"
431
+ @keydown.self.down.prevent="onKeydownDown"
432
+ @keydown.self.up.prevent="onKeydownUp"
411
433
  @update:model-value="onSearchChange"
412
434
  />
413
435
  </div>
@@ -7,8 +7,6 @@ import UListbox from "../ui.form-listbox/UListbox.vue";
7
7
  import UBadge from "../ui.text-badge/UBadge.vue";
8
8
  import ULink from "../ui.button-link/ULink.vue";
9
9
 
10
- import { vClickOutside } from "../directives";
11
-
12
10
  import useUI from "../composables/useUI.ts";
13
11
  import { hasSlotContent } from "../utils/helper.ts";
14
12
  import { getDefaults } from "../utils/ui.ts";
@@ -198,10 +196,6 @@ const toggleIconName = computed(() => {
198
196
  return props.toggleIcon ? config.value.defaults.toggleIcon : "";
199
197
  });
200
198
 
201
- const clickOutsideOptions = computed(() => ({
202
- ignore: [labelComponentRef.value?.wrapperElement, labelComponentRef.value?.labelElement],
203
- }));
204
-
205
199
  watch(localValue, setLabelPosition, { deep: true });
206
200
 
207
201
  onMounted(() => {
@@ -216,16 +210,6 @@ function onSearchChange(query: string) {
216
210
  emit("searchChange", query);
217
211
  }
218
212
 
219
- function onListboxInteraction(event: MouseEvent) {
220
- const target = event.target as HTMLElement;
221
-
222
- if (target.closest("input")) {
223
- return;
224
- }
225
-
226
- event.preventDefault();
227
- }
228
-
229
213
  function onKeydownAddOption(event: KeyboardEvent) {
230
214
  if (!isOpen.value) return;
231
215
 
@@ -257,7 +241,7 @@ function deactivate() {
257
241
  return;
258
242
  }
259
243
 
260
- if (props.searchable) wrapperRef.value?.blur();
244
+ wrapperRef.value?.blur();
261
245
 
262
246
  isOpen.value = false;
263
247
 
@@ -283,9 +267,9 @@ function activate() {
283
267
  }
284
268
 
285
269
  function adjustPosition() {
286
- if (typeof window === "undefined" || !listboxRef.value || !wrapperRef.value) return;
270
+ if (!wrapperRef.value) return;
287
271
 
288
- const dropdownHeight = listboxRef.value.wrapperRef?.getBoundingClientRect().height || 0;
272
+ const dropdownHeight = listboxRef.value?.wrapperRef?.getBoundingClientRect().height || 0;
289
273
  const spaceAbove = wrapperRef.value.getBoundingClientRect().top;
290
274
  const spaceBelow = window.innerHeight - wrapperRef.value.getBoundingClientRect().bottom;
291
275
  const hasEnoughSpaceBelow = spaceBelow > dropdownHeight;
@@ -297,7 +281,7 @@ function adjustPosition() {
297
281
  }
298
282
  }
299
283
 
300
- function onWrapperBlur(event: FocusEvent) {
284
+ function deactivateOnBlur(event: FocusEvent) {
301
285
  const related = event.relatedTarget as HTMLElement | null;
302
286
 
303
287
  const isInsideWrapper = related && wrapperRef.value?.contains(related);
@@ -312,7 +296,19 @@ function onWrapperBlur(event: FocusEvent) {
312
296
  deactivate();
313
297
  }
314
298
 
315
- function onMouseDownClearItem(event: MouseEvent, option: Option) {
299
+ function onBlur(event: FocusEvent) {
300
+ deactivateOnBlur(event);
301
+ }
302
+
303
+ function onListboxBlur(event: FocusEvent) {
304
+ deactivateOnBlur(event);
305
+ }
306
+
307
+ function onListboxSearchBlur(event: FocusEvent) {
308
+ deactivateOnBlur(event);
309
+ }
310
+
311
+ function onClickClearItem(event: MouseEvent, option: Option) {
316
312
  if (props.disabled) return;
317
313
 
318
314
  const value = Array.isArray(props.modelValue)
@@ -325,25 +321,23 @@ function onMouseDownClearItem(event: MouseEvent, option: Option) {
325
321
  })
326
322
  : [];
327
323
 
324
+ if (isOpen.value) wrapperRef.value?.focus();
325
+
328
326
  emit("update:modelValue", value);
329
327
  emit("change", { value, options: props.options });
330
328
  emit("remove", option);
331
329
  }
332
330
 
333
- function onMouseDownClear() {
331
+ function onClickClear() {
334
332
  if (props.disabled) return;
335
333
 
336
- if (!props.clearable && !props.multiple) {
337
- deactivate();
338
-
339
- return;
340
- }
341
-
342
334
  const value = props.multiple ? [] : "";
343
335
 
344
336
  emit("update:modelValue", value);
345
337
  emit("change", { value, options: props.options });
346
338
  emit("remove", props.options);
339
+
340
+ deactivate();
347
341
  }
348
342
 
349
343
  useMutationObserver(leftSlotWrapperRef, (mutations) => mutations.forEach(setLabelPosition), {
@@ -475,25 +469,27 @@ const {
475
469
  v-bind="selectLabelAttrs"
476
470
  :data-test="getDataTest()"
477
471
  :tabindex="-1"
478
- @click="toggle"
479
472
  >
480
473
  <template #label>
481
- <!--
482
- @slot Use this to add custom content instead of the label.
483
- @binding {string} label
484
- -->
485
- <slot name="label" :label="label" />
474
+ <div @click="toggle" @mousedown.prevent>
475
+ <!--
476
+ @slot Use this to add custom content instead of the label.
477
+ @binding {string} label
478
+ -->
479
+ <slot name="label" :label="label">
480
+ {{ label }}
481
+ </slot>
482
+ </div>
486
483
  </template>
487
484
 
488
485
  <div
489
486
  ref="wrapper"
490
- v-click-outside="[deactivate, clickOutsideOptions]"
491
487
  :tabindex="searchable || disabled ? -1 : 0"
492
488
  role="combobox"
493
489
  :aria-owns="'listbox-' + elementId"
494
490
  v-bind="wrapperAttrs"
495
491
  @focus="activate"
496
- @blur="onWrapperBlur"
492
+ @blur="onBlur"
497
493
  @keydown.self.down.prevent="listboxRef?.pointerForward"
498
494
  @keydown.self.up.prevent="listboxRef?.pointerBackward"
499
495
  @keydown.enter.tab.stop.self="listboxRef?.addPointerElement()"
@@ -526,7 +522,7 @@ const {
526
522
  v-bind="toggleWrapperAttrs"
527
523
  :tabindex="-1"
528
524
  :data-test="getDataTest('toggle')"
529
- @mousedown.prevent.stop="toggle"
525
+ @click.stop="toggle"
530
526
  >
531
527
  <!--
532
528
  @slot Use it to add something instead of the toggle icon.
@@ -546,23 +542,22 @@ const {
546
542
  </slot>
547
543
  </div>
548
544
 
549
- <div
550
- v-if="!isMultipleListVariant && isLocalValue && clearable"
551
- v-bind="clearAttrs"
552
- :data-test="getDataTest('clear')"
553
- @mousedown="onMouseDownClear"
554
- >
545
+ <div v-if="!isMultipleListVariant && isLocalValue && clearable" v-bind="clearAttrs">
555
546
  <!--
556
547
  @slot Use it to add something instead of the clear icon.
557
548
  @binding {string} icon-name
549
+ @binding {function} clear
550
+ @binding {string} data-test
558
551
  -->
559
- <slot name="clear" :icon-name="config.defaults.clearIcon">
552
+ <slot name="clear" :icon-name="config.defaults.clearIcon" :clear="onClickClear">
560
553
  <UIcon
561
554
  interactive
562
555
  color="neutral"
563
556
  :disabled="disabled"
564
557
  :name="config.defaults.clearIcon"
565
558
  v-bind="clearIconAttrs"
559
+ :data-test="getDataTest('clear')"
560
+ @click.stop="onClickClear"
566
561
  />
567
562
  </slot>
568
563
  </div>
@@ -588,7 +583,7 @@ const {
588
583
  @binding {object} options
589
584
  -->
590
585
  <slot name="selected-options" :options="multiple ? selectedOptions.full : selectedOption">
591
- <span v-if="!multiple" v-bind="selectedLabelsAttrs" @mousedown.prevent="toggle">
586
+ <span v-if="!multiple" v-bind="selectedLabelsAttrs" @click.stop="toggle">
592
587
  <!--
593
588
  @slot Use it to customize selected option.
594
589
  @binding {string} label
@@ -669,7 +664,7 @@ const {
669
664
  :size="size"
670
665
  variant="subtle"
671
666
  v-bind="badgeLabelAttrs"
672
- @click="toggle"
667
+ @click.stop="toggle"
673
668
  >
674
669
  <div v-bind="selectedLabelTextAttrs">
675
670
  {{ option[labelKey] }}
@@ -682,7 +677,7 @@ const {
682
677
  :disabled="disabled"
683
678
  :name="config.defaults.badgeClearIcon"
684
679
  v-bind="badgeClearIconAttrs"
685
- @click="onMouseDownClearItem($event, option)"
680
+ @click.stop="onClickClearItem($event, option)"
686
681
  />
687
682
  </template>
688
683
  </UBadge>
@@ -735,9 +730,7 @@ const {
735
730
  :name="config.defaults.listClearIcon"
736
731
  :data-test="getDataTest('clear-item')"
737
732
  v-bind="listClearIconAttrs"
738
- @mousedown.prevent.capture
739
- @click.prevent.capture
740
- @mousedown="onMouseDownClearItem($event, option)"
733
+ @click.stop="onClickClearItem($event, option)"
741
734
  />
742
735
  </slot>
743
736
  </div>
@@ -767,8 +760,7 @@ const {
767
760
  :underlined="false"
768
761
  v-bind="listClearAllAttrs"
769
762
  :data-test="getDataTest('clear-all')"
770
- @mousedown.prevent.capture="onMouseDownClear"
771
- @click.prevent.capture
763
+ @click.stop="onClickClear"
772
764
  />
773
765
  </div>
774
766
  </template>
@@ -788,17 +780,19 @@ const {
788
780
  :size="size"
789
781
  :debounce="debounce"
790
782
  :visible-options="visibleOptions"
791
- :value-key="valueKey"
792
783
  :label-key="labelKey"
784
+ :value-key="valueKey"
785
+ :group-label-key="groupLabelKey"
786
+ :group-value-key="groupValueKey"
793
787
  :add-option="addOption"
794
788
  tabindex="-1"
795
789
  v-bind="listboxAttrs as KeyAttrsWithConfig<UListboxConfig>"
796
790
  :data-test="getDataTest()"
797
791
  @add="onAddOption"
798
792
  @focus="activate"
793
+ @blur="onListboxBlur"
794
+ @search-blur="onListboxSearchBlur"
799
795
  @update:model-value="onSearchChange"
800
- @mousedown.capture="onListboxInteraction"
801
- @click.capture="onListboxInteraction"
802
796
  >
803
797
  <template #before-option="{ option, index }">
804
798
  <!--
@@ -334,7 +334,7 @@ export const Slots: StoryFn<USelectArgs> = (args) => ({
334
334
  </template>
335
335
  </USelect>
336
336
 
337
- <URow>
337
+ <URow block>
338
338
  <USelect v-bind="args" v-model="args.beforeToggleModel" label="Slot before-toggle">
339
339
  <template #before-toggle>
340
340
  <UAvatar />
@@ -353,7 +353,7 @@ export const Slots: StoryFn<USelectArgs> = (args) => ({
353
353
  </USelect>
354
354
  </URow>
355
355
 
356
- <URow>
356
+ <URow block>
357
357
  <USelect v-bind="args" v-model="args.leftModel" label="Slot left">
358
358
  <template #left>
359
359
  <UAvatar />