v-uixy 1.3.1 → 1.4.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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/templates/assets/css/main.css +4 -0
  3. package/templates/components/Avatar/Avatar.component.vue +7 -3
  4. package/templates/components/Avatar/Avatar.styles.ts +3 -3
  5. package/templates/components/Button/Button.styles.ts +3 -3
  6. package/templates/components/Calendar/Calendar.component.vue +26 -15
  7. package/templates/components/Calendar/Calendar.styles.ts +11 -3
  8. package/templates/components/Calendar/Calendar.types.ts +1 -0
  9. package/templates/components/Checkbox/Checkbox.styles.ts +1 -1
  10. package/templates/components/ColorPicker/ColorPicker.component.vue +4 -4
  11. package/templates/components/Command/Command.component.vue +6 -6
  12. package/templates/components/Command/Command.types.ts +1 -0
  13. package/templates/components/Input/Input.component.vue +27 -27
  14. package/templates/components/Input/Input.styles.ts +5 -5
  15. package/templates/components/Input/Input.types.ts +1 -0
  16. package/templates/components/Modal/Modal.component.vue +28 -4
  17. package/templates/components/Modal/Modal.types.ts +3 -1
  18. package/templates/components/Popover/Popover.component.vue +33 -2
  19. package/templates/components/Popover/Popover.types.ts +1 -0
  20. package/templates/components/Select/Select.component.vue +253 -108
  21. package/templates/components/Select/Select.styles.ts +6 -6
  22. package/templates/components/Select/Select.types.ts +12 -0
  23. package/templates/components/Select/SelectTreeItem.vue +154 -0
  24. package/templates/components/Select/composables/useSelect.composable.ts +260 -42
  25. package/templates/components/Tabs/Tabs.component.vue +11 -2
  26. package/templates/components/Tabs/Tabs.types.ts +1 -0
  27. package/templates/components/Textarea/Textarea.component.vue +23 -32
  28. package/templates/components/Textarea/Textarea.styles.ts +4 -4
  29. package/templates/components/Textarea/Textarea.types.ts +1 -0
  30. package/templates/components/TimePicker/TimePicker.component.vue +21 -18
  31. package/templates/components/TimePicker/TimePicker.styles.ts +1 -1
  32. package/templates/components/TimePicker/TimePicker.types.ts +1 -0
  33. package/templates/components.json +1 -1
  34. package/templates/composables/usePosition.ts +17 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "v-uixy",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "author": {
6
6
  "name": "Alan Haber",
@@ -156,6 +156,10 @@
156
156
  @apply bg-transparent;
157
157
  }
158
158
 
159
+ .no-scrollbar::-webkit-scrollbar {
160
+ display: none;
161
+ }
162
+
159
163
  .slider::-webkit-slider-thumb {
160
164
  @apply relative z-10 h-5 w-5 cursor-pointer appearance-none rounded-full border-2 border-solid border-black bg-gray-100 dark:border-gray-100 dark:bg-black;
161
165
  }
@@ -47,7 +47,11 @@
47
47
  show.value = false;
48
48
  };
49
49
 
50
- onMounted(() => {
51
- show.value = !!props.src;
52
- });
50
+ watch(
51
+ () => props.src,
52
+ (src) => {
53
+ show.value = !!src;
54
+ },
55
+ { immediate: true },
56
+ );
53
57
  </script>
@@ -8,11 +8,11 @@ export const avatarWrapperStyles = styles(
8
8
  squared: "rounded-2",
9
9
  default: "",
10
10
  },
11
- }
11
+ },
12
12
  );
13
13
 
14
14
  export const avatarImageStyles = styles(
15
- "flex aspect-square items-center overflow-hidden text-center text-[0]",
15
+ "flex aspect-square object-cover items-center overflow-hidden text-center text-[0]",
16
16
  {
17
17
  show: {
18
18
  true: "",
@@ -23,5 +23,5 @@ export const avatarImageStyles = styles(
23
23
  squared: "rounded-2",
24
24
  default: "",
25
25
  },
26
- }
26
+ },
27
27
  );
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const buttonStyles = styles(
4
- "relative cursor-pointer h-8 font-500 transition-all duration-200 ease-in-out hover:shadow-xs flex items-center justify-center gap-2 disabled:pointer-events-none",
4
+ "relative cursor-pointer h-8 font-500 transition-all duration-200 ease-in-out hover:shadow-xs flex items-center justify-center gap-2 disabled:pointer-events-none border border-transparent",
5
5
  {
6
6
  variant: {
7
7
  primary:
@@ -9,7 +9,7 @@ export const buttonStyles = styles(
9
9
  secondary:
10
10
  "bg-gray-300 hover:bg-gray-200 dark:bg-gray-900 dark:hover:bg-gray-700 dark:text-gray-100 text-black dark:disabled:bg-gray-600 dark:disabled:text-gray-700 disabled:text-gray-500",
11
11
  tertiary:
12
- "bg-transparent border border-solid border-gray-300 dark:border-gray-800 hover:bg-gray-200 dark:hover:bg-gray-800 text-black dark:text-gray-100 dark:disabled:border-gray-900 dark:disabled:text-gray-500 dark:disabled:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500 disabled:border-gray-400",
12
+ "bg-transparent border-solid border-gray-300 dark:border-gray-800 hover:bg-gray-200 dark:hover:bg-gray-800 text-black dark:text-gray-100 dark:disabled:border-gray-900 dark:disabled:text-gray-500 dark:disabled:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500 disabled:border-gray-400",
13
13
  quaternary:
14
14
  "dark:hover:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-800 disabled:text-gray-700",
15
15
  },
@@ -22,7 +22,7 @@ export const buttonStyles = styles(
22
22
  true: "rounded-full",
23
23
  false: "rounded-1",
24
24
  },
25
- }
25
+ },
26
26
  );
27
27
 
28
28
  export const iconStyles = styles("h-5 w-5 shrink-0", {
@@ -7,7 +7,7 @@
7
7
  size="sm"
8
8
  variant="tertiary"
9
9
  />
10
- <p class="w-[160px] text-center text-sm font-500">
10
+ <p class="w-40 text-center text-sm font-500">
11
11
  {{ MONTHS[selectedMonth] }} {{ selectedYear }}
12
12
  </p>
13
13
  <uixy-icon-button
@@ -22,7 +22,7 @@
22
22
  class="grid w-full grid-cols-7 text-xs text-gray-400 dark:text-gray-700"
23
23
  >
24
24
  <div v-for="day in adjustedDays" :key="day" class="py-2 text-center">
25
- {{ day.slice(0, 2) }}
25
+ {{ day?.slice(0, 2) }}
26
26
  </div>
27
27
  </div>
28
28
 
@@ -36,9 +36,18 @@
36
36
  selected: model?.toDateString?.() === day.date.toDateString(),
37
37
  disabled:
38
38
  !!props.disabled || day.fromPreviousMonth || day.fromNextMonth,
39
+ greyedOut:
40
+ !!day.fromCurrentMonth &&
41
+ !!props.isDateDisabled &&
42
+ props.isDateDisabled(day.date),
43
+ today: today.toDateString() === day.date.toDateString(),
39
44
  })
40
45
  "
41
- :disabled="day.fromPreviousMonth || day.fromNextMonth"
46
+ :disabled="
47
+ day.fromPreviousMonth ||
48
+ day.fromNextMonth ||
49
+ (!!props.isDateDisabled && props.isDateDisabled(day.date))
50
+ "
42
51
  @click="handleDayClick(day.date)"
43
52
  >
44
53
  {{ day.day }}
@@ -82,11 +91,13 @@
82
91
  "Saturday",
83
92
  ];
84
93
 
94
+ const today = new Date();
95
+
85
96
  const selectedYear = ref(props.year ?? new Date().getFullYear());
86
97
  const selectedMonth = ref(props.month ?? new Date().getMonth());
87
98
 
88
99
  const adjustedDays = computed(() =>
89
- props.startOfWeek === "Monday" ? [...DAYS.slice(1), DAYS[0]] : DAYS
100
+ props.startOfWeek === "Monday" ? [...DAYS.slice(1), DAYS[0]] : DAYS,
90
101
  );
91
102
 
92
103
  const getDaysInMonth = (year: number, month: number) =>
@@ -101,11 +112,11 @@
101
112
  });
102
113
 
103
114
  const daysInCurrentMonth = computed(() =>
104
- getDaysInMonth(selectedYear.value, selectedMonth.value)
115
+ getDaysInMonth(selectedYear.value, selectedMonth.value),
105
116
  );
106
117
 
107
118
  const daysInPreviousMonth = computed(() =>
108
- getDaysInMonth(selectedYear.value, selectedMonth.value - 1)
119
+ getDaysInMonth(selectedYear.value, selectedMonth.value - 1),
109
120
  );
110
121
 
111
122
  const prevMonthDaysToShow = computed(() =>
@@ -114,11 +125,11 @@
114
125
  date: new Date(
115
126
  selectedYear.value,
116
127
  selectedMonth.value - 1,
117
- daysInPreviousMonth.value - i
128
+ daysInPreviousMonth.value - i,
118
129
  ),
119
130
  dayOfWeek: (firstDayOfMonth.value - 1 - i + 7) % 7,
120
131
  fromPreviousMonth: true,
121
- })).reverse()
132
+ })).reverse(),
122
133
  );
123
134
 
124
135
  const currentMonthDaysToShow = computed(() =>
@@ -128,18 +139,18 @@
128
139
  dayOfWeek: new Date(
129
140
  selectedYear.value,
130
141
  selectedMonth.value,
131
- i + 1
142
+ i + 1,
132
143
  ).getDay(),
133
144
  fromCurrentMonth: true,
134
- }))
145
+ })),
135
146
  );
136
147
 
137
148
  const lastDayOfMonth = computed(() =>
138
149
  new Date(
139
150
  selectedYear.value,
140
151
  selectedMonth.value,
141
- daysInCurrentMonth.value
142
- ).getDay()
152
+ daysInCurrentMonth.value,
153
+ ).getDay(),
143
154
  );
144
155
 
145
156
  const nextMonthDaysToShow = computed(() => {
@@ -190,8 +201,8 @@
190
201
  }
191
202
 
192
203
  function handleDayClick(date: Date) {
193
- if (!props.disabled) {
194
- model.value = date;
195
- }
204
+ if (props.disabled) return;
205
+ if (props.isDateDisabled?.(date)) return;
206
+ model.value = date;
196
207
  }
197
208
  </script>
@@ -1,7 +1,7 @@
1
1
  import styles from "~/utils/styles";
2
2
 
3
3
  export const calendarStyles = styles(
4
- "flex w-fit flex-col items-center rounded-2 border border-gray-300 bg-gray-100 p-3 shadow-xs dark:border-gray-900 dark:bg-black"
4
+ "flex w-fit flex-col items-center rounded-2 border border-gray-300 bg-gray-100 p-3 shadow-xs dark:border-gray-900 dark:bg-black",
5
5
  );
6
6
 
7
7
  export const calendarDayStyles = styles(
@@ -9,7 +9,7 @@ export const calendarDayStyles = styles(
9
9
  {
10
10
  currentMonth: {
11
11
  true: "dark:text-gray-300",
12
- false: "text-gray-500 dark:text-gray-600",
12
+ false: "text-gray-400 dark:text-gray-600",
13
13
  },
14
14
  selected: {
15
15
  true: "bg-black text-white hover:bg-black dark:bg-white dark:!text-black dark:hover:bg-white",
@@ -19,5 +19,13 @@ export const calendarDayStyles = styles(
19
19
  true: "cursor-default pointer-events-none",
20
20
  false: "cursor-pointer",
21
21
  },
22
- }
22
+ greyedOut: {
23
+ true: "opacity-35 cursor-not-allowed pointer-events-none",
24
+ false: "",
25
+ },
26
+ today: {
27
+ true: "ring-1 ring-gray-400 dark:ring-gray-600",
28
+ false: "",
29
+ },
30
+ },
23
31
  );
@@ -3,4 +3,5 @@ export interface UixyCalendarProps {
3
3
  month?: number;
4
4
  startOfWeek?: "Sunday" | "Monday";
5
5
  disabled?: boolean;
6
+ isDateDisabled?: (date: Date) => boolean;
6
7
  }
@@ -13,7 +13,7 @@ export const iconStyles = styles("h-3 w-3", {
13
13
  },
14
14
  });
15
15
 
16
- export const labelStyles = styles("text-xs", {
16
+ export const labelStyles = styles("text-sm", {
17
17
  disabled: {
18
18
  true: "text-gray-500 dark:text-gray-600",
19
19
  false: "",
@@ -2,7 +2,7 @@
2
2
  <uixy-card class="p-2 flex flex-col gap-4">
3
3
  <div
4
4
  ref="svRef"
5
- class="relative cursor-crosshair overflow-hidden rounded-1 w-64 h-48"
5
+ class="relative cursor-crosshair overflow-hidden rounded-1 min-w-64 h-48"
6
6
  :style="svBackgroundStyle"
7
7
  @mousedown="onSvMouseDown"
8
8
  @touchstart.prevent="onSvTouchStart"
@@ -69,11 +69,11 @@
69
69
 
70
70
  const rgbToHex8 = (r: number, g: number, b: number, a: number) =>
71
71
  `#${componentToHex(r)}${componentToHex(g)}${componentToHex(
72
- b
72
+ b,
73
73
  )}${componentToHex(clamp(a, 0, 255))}`;
74
74
 
75
75
  const hexToRgbA = (
76
- hex?: string
76
+ hex?: string,
77
77
  ): {
78
78
  r: number;
79
79
  g: number;
@@ -162,7 +162,7 @@
162
162
  const svRef = ref<HTMLElement | null>(null);
163
163
 
164
164
  const rgb = computed(() =>
165
- hsvToRgb(hue.value, sat.value / 100, val.value / 100)
165
+ hsvToRgb(hue.value, sat.value / 100, val.value / 100),
166
166
  );
167
167
 
168
168
  const updatingFromModel = ref(false);
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <uixy-modal v-model="open">
2
+ <uixy-modal v-model="open" :persistent="props.persistent">
3
3
  <uixy-card :class="commandStyles($attrs.class as string)">
4
4
  <div class="flex items-center gap-2 p-3">
5
5
  <label :for="id">
@@ -22,7 +22,7 @@
22
22
  />
23
23
  </div>
24
24
  <uixy-separator variant="horizontal" />
25
- <div class="scrollbar max-h-[320px] overflow-y-auto">
25
+ <div class="scrollbar max-h-80 overflow-y-auto">
26
26
  <template v-for="option in filteredOptions" :key="option.label">
27
27
  <template v-if="option.items.length > 0">
28
28
  <ul class="flex flex-col gap-2 p-2">
@@ -79,13 +79,13 @@
79
79
  props.options.map((option) => ({
80
80
  label: option.label,
81
81
  items: option.items.filter((item) =>
82
- item.label.toLowerCase().includes(input.value?.toLowerCase() ?? "")
82
+ item.label.toLowerCase().includes(input.value?.toLowerCase() ?? ""),
83
83
  ),
84
- }))
84
+ })),
85
85
  );
86
86
 
87
87
  const isEmpty = computed(() =>
88
- filteredOptions.value.every((option) => option.items.length === 0)
88
+ filteredOptions.value.every((option) => option.items.length === 0),
89
89
  );
90
90
 
91
91
  const handleClose = () => {
@@ -93,7 +93,7 @@
93
93
  };
94
94
 
95
95
  const placeholder = computed(
96
- () => props.placeholder || "Search or type a command..."
96
+ () => props.placeholder || "Search or type a command...",
97
97
  );
98
98
 
99
99
  const emptyText = computed(() => props.emptyText || "No results found.");
@@ -16,4 +16,5 @@ export interface UixyCommandProps {
16
16
  options: UixyCommandItems[];
17
17
  placeholder?: string;
18
18
  emptyText?: string;
19
+ persistent?: boolean;
19
20
  }
@@ -2,12 +2,12 @@
2
2
  <div class="flex flex-col gap-1">
3
3
  <div class="relative flex flex-col-reverse gap-1">
4
4
  <input
5
+ ref="inputRef"
5
6
  v-model="model"
6
7
  :type="props.type ?? 'text'"
7
8
  :id
8
9
  :disabled="props.disabled"
9
10
  :placeholder="props.placeholder"
10
- :auto-focus="props.autoFocus"
11
11
  :class="inputStyles({ status, icon }, $attrs.class as string)"
12
12
  v-bind="filteredAttrs"
13
13
  />
@@ -27,29 +27,19 @@
27
27
  {{ props.label }}
28
28
  </label>
29
29
  </div>
30
- <animate-presence mode="wait" :initial="false">
31
- <motion.div
32
- v-if="!!text"
33
- class="overflow-hidden"
34
- :initial="{ height: 0 }"
35
- :animate="{ height: 'auto' }"
36
- :exit="{ height: 0 }"
37
- :transition="{ duration: 0.1, ease: 'easeInOut' }"
38
- >
39
- <animate-presence mode="wait" :initial="false">
40
- <motion.p
41
- :key="status + text"
42
- :class="helperStyles({ status })"
43
- :initial="{ opacity: 0, y: -4 }"
44
- :animate="{ opacity: 1, y: 0 }"
45
- :exit="{ opacity: 0, y: 4 }"
46
- :transition="{ duration: 0.12, ease: 'easeInOut', delay: 0.1 }"
47
- >
48
- {{ text }}
49
- </motion.p>
50
- </animate-presence>
51
- </motion.div>
52
- </animate-presence>
30
+ <div v-if="!props.hideHelper" class="h-4">
31
+ <animate-presence mode="wait" :initial="false">
32
+ <motion.div
33
+ :initial="{ opacity: 0, y: '-4px' }"
34
+ :animate="{ opacity: 1, y: 0 }"
35
+ :exit="{ opacity: 0, y: '-4px' }"
36
+ :transition="{ duration: 0.125 }"
37
+ :key="status"
38
+ >
39
+ <p :class="helperStyles({ status })">{{ text }}</p>
40
+ </motion.div>
41
+ </animate-presence>
42
+ </div>
53
43
  </div>
54
44
  </template>
55
45
 
@@ -80,6 +70,8 @@
80
70
  return rest;
81
71
  });
82
72
 
73
+ const inputRef = ref<HTMLInputElement>();
74
+
83
75
  const model = defineModel<string>();
84
76
 
85
77
  const instance = getCurrentInstance();
@@ -89,7 +81,7 @@
89
81
  const hasIconClickEmit = computed(() => !!instance?.vnode.props?.onIconClick);
90
82
 
91
83
  const status = computed(() =>
92
- props.disabled ? "disabled" : props.status ?? "default"
84
+ props.disabled ? "disabled" : (props.status ?? "default"),
93
85
  );
94
86
 
95
87
  const text = computed(
@@ -99,10 +91,18 @@
99
91
  default: props.helperText,
100
92
  disabled: "",
101
93
  valid: "",
102
- }[props.status ?? "default"])
94
+ })[props.status ?? "default"],
103
95
  );
104
96
 
105
97
  const icon = computed(() =>
106
- props.icon ? props.iconPositon ?? "right" : "none"
98
+ props.icon ? (props.iconPositon ?? "right") : "none",
107
99
  );
100
+
101
+ onMounted(() => {
102
+ if (props.autoFocus) {
103
+ nextTick(() => inputRef.value?.focus({ preventScroll: true }));
104
+ }
105
+ });
106
+
107
+ defineExpose({ focus: () => inputRef.value?.focus({ preventScroll: true }) });
108
108
  </script>
@@ -17,7 +17,7 @@ export const inputStyles = styles(
17
17
  right: "pr-8",
18
18
  none: "",
19
19
  },
20
- }
20
+ },
21
21
  );
22
22
 
23
23
  export const iconStyles = styles(
@@ -41,11 +41,11 @@ export const iconStyles = styles(
41
41
  true: "pointer-events-auto cursor-pointer",
42
42
  false: "",
43
43
  },
44
- }
44
+ },
45
45
  );
46
46
 
47
47
  export const labelStyles = styles(
48
- "transition-colors ease-in-out duration-150 text-xs font-500",
48
+ "transition-colors ease-in-out duration-150 text-sm font-500",
49
49
  {
50
50
  status: {
51
51
  default: "",
@@ -53,7 +53,7 @@ export const labelStyles = styles(
53
53
  disabled: "text-gray-500 dark:text-gray-700",
54
54
  valid: "",
55
55
  },
56
- }
56
+ },
57
57
  );
58
58
 
59
59
  export const helperStyles = styles(
@@ -65,5 +65,5 @@ export const helperStyles = styles(
65
65
  disabled: "text-gray-400 dark:text-gray-800",
66
66
  valid: "text-gray-600",
67
67
  },
68
- }
68
+ },
69
69
  );
@@ -11,6 +11,7 @@ export interface UixyInputProps {
11
11
  helperText?: string;
12
12
  errorText?: string;
13
13
  autoFocus?: boolean;
14
+ hideHelper?: boolean;
14
15
  }
15
16
 
16
17
  export interface UixyInputEmits {
@@ -11,7 +11,6 @@
11
11
  :class="modalStyles($attrs.class as string)"
12
12
  data-modal
13
13
  @click.self="handleClick"
14
- @keydown{esc}="handleClick"
15
14
  >
16
15
  <motion.div
17
16
  :initial="{ scale: 0.8 }"
@@ -32,7 +31,7 @@
32
31
  import { modalStyles } from "./Modal.styles";
33
32
  import { AnimatePresence, motion } from "motion-v";
34
33
 
35
- defineProps<UixyModalProps>();
34
+ const props = defineProps<UixyModalProps>();
36
35
 
37
36
  defineOptions({
38
37
  inheritAttrs: false,
@@ -41,14 +40,30 @@
41
40
  const open = defineModel<boolean>();
42
41
 
43
42
  const handleClick = () => {
43
+ if (props.persistent) return;
44
+
44
45
  open.value = false;
45
46
  };
46
47
 
48
+ const handleKeyDown = (e: KeyboardEvent) => {
49
+ if (e.key === "Escape") {
50
+ handleClick();
51
+ return;
52
+ }
53
+ if (e.key === "Enter") {
54
+ const target = e.target as HTMLElement;
55
+ if (target.tagName === "TEXTAREA" || target.tagName === "BUTTON") return;
56
+ const modalEl = document.querySelector("[data-modal]");
57
+ const form = modalEl?.querySelector("form");
58
+ form?.requestSubmit();
59
+ }
60
+ };
61
+
47
62
  const toggleSiblingsInert = (enable?: boolean) => {
48
63
  if (typeof document === "undefined") return;
49
64
 
50
65
  const modalEl = document.querySelector(
51
- "[data-modal]"
66
+ "[data-modal]",
52
67
  ) as HTMLElement | null;
53
68
 
54
69
  Array.from(document.body.children).forEach((el) => {
@@ -68,7 +83,16 @@
68
83
  open,
69
84
  (isOpen) => {
70
85
  toggleSiblingsInert(isOpen);
86
+ if (isOpen) {
87
+ window.addEventListener("keydown", handleKeyDown);
88
+ } else {
89
+ window.removeEventListener("keydown", handleKeyDown);
90
+ }
71
91
  },
72
- { immediate: true }
92
+ { immediate: true },
73
93
  );
94
+
95
+ onUnmounted(() => {
96
+ window.removeEventListener("keydown", handleKeyDown);
97
+ });
74
98
  </script>
@@ -1 +1,3 @@
1
- export interface UixyModalProps {}
1
+ export interface UixyModalProps {
2
+ persistent?: boolean;
3
+ }
@@ -27,12 +27,16 @@
27
27
 
28
28
  <script setup lang="ts">
29
29
  import { AnimatePresence, motion } from "motion-v";
30
+ import { v4 as uuidv4 } from "uuid";
30
31
  import { usePosition } from "~/composables";
31
32
  import { onWatcherCleanup } from "vue";
32
33
  import type { UixyPopoverProps } from "./Popover.types";
33
34
 
34
35
  const props = defineProps<UixyPopoverProps>();
35
36
 
37
+ const POPOVER_GROUP_EVENT = "uixy:popover-group-open";
38
+ const instanceId = uuidv4();
39
+
36
40
  const { active, handleOpen, handleLeave, styles, refElement } = usePosition({
37
41
  direction: props.direction ?? "bottom",
38
42
  });
@@ -45,22 +49,49 @@
45
49
  handleLeave();
46
50
  };
47
51
 
52
+ const handleKeyDown = (e: KeyboardEvent) => {
53
+ if (e.key === "Escape") handleLeave();
54
+ };
55
+
48
56
  watch(
49
57
  () => active.value,
50
- (_, prevVal) => {
58
+ (isActive, prevVal) => {
51
59
  if (prevVal) return;
52
60
 
61
+ if (isActive && props.group) {
62
+ window.dispatchEvent(
63
+ new CustomEvent(POPOVER_GROUP_EVENT, { detail: { id: instanceId } }),
64
+ );
65
+ }
66
+
53
67
  requestAnimationFrame(() => {
54
68
  window.addEventListener("click", handleClickOutside);
69
+ window.addEventListener("keydown", handleKeyDown);
55
70
  });
56
71
 
57
72
  onWatcherCleanup(() => {
58
73
  window.removeEventListener("click", handleClickOutside);
74
+ window.removeEventListener("keydown", handleKeyDown);
59
75
  });
60
- }
76
+ },
61
77
  );
62
78
 
79
+ const handleGroupOpen = (e: Event) => {
80
+ const event = e as CustomEvent<{ id: string }>;
81
+ if (event.detail.id !== instanceId) handleLeave();
82
+ };
83
+
84
+ onMounted(() => {
85
+ if (props.group) {
86
+ window.addEventListener(POPOVER_GROUP_EVENT, handleGroupOpen);
87
+ }
88
+ });
89
+
63
90
  onUnmounted(() => {
64
91
  window.removeEventListener("click", handleClickOutside);
92
+ window.removeEventListener("keydown", handleKeyDown);
93
+ if (props.group) {
94
+ window.removeEventListener(POPOVER_GROUP_EVENT, handleGroupOpen);
95
+ }
65
96
  });
66
97
  </script>
@@ -1,4 +1,5 @@
1
1
  export interface UixyPopoverProps {
2
2
  direction?: "top" | "bottom";
3
3
  closeOnClick?: boolean;
4
+ group?: string;
4
5
  }