@weni/unnnic-system 3.12.8-alpha-teleports.0 → 3.12.8-alpha.1

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 (84) hide show
  1. package/.vscode/extensions.json +3 -0
  2. package/CHANGELOG.md +1114 -0
  3. package/README.md +1 -9
  4. package/dist/{es-2f6793d2.mjs → es-9d6892f7.mjs} +1 -1
  5. package/dist/{index-799af668.mjs → index-78117c0a.mjs} +9299 -8975
  6. package/dist/index.d.ts +870 -321
  7. package/dist/{pt-br-f5121b47.mjs → pt-br-89839646.mjs} +1 -1
  8. package/dist/style.css +1 -1
  9. package/dist/unnnic.mjs +156 -154
  10. package/dist/unnnic.umd.js +33 -33
  11. package/package.json +1 -1
  12. package/src/components/Alert/__tests__/__snapshots__/Alert.spec.js.snap +1 -1
  13. package/src/components/ChartFunnel/DefaultFunnel/ChartDefaultFunnelBase.vue +1 -2
  14. package/src/components/ChartFunnel/SvgFunnel/ChartFunnelTwoRows.vue +60 -61
  15. package/src/components/Checkbox/Checkbox.vue +9 -3
  16. package/src/components/CheckboxGroup/CheckboxGroup.vue +7 -5
  17. package/src/components/Chip/Chip.vue +1 -1
  18. package/src/components/DatePicker/DatePicker.vue +1 -11
  19. package/src/components/Drawer/Drawer.vue +20 -9
  20. package/src/components/Drawer/__tests__/Drawer.spec.js +11 -9
  21. package/src/components/Drawer/__tests__/__snapshots__/Drawer.spec.js.snap +9 -9
  22. package/src/components/EmojiPicker/EmojiPicker.vue +1 -1
  23. package/src/components/FormElement/FormElement.vue +97 -88
  24. package/src/components/Input/BaseInput.vue +25 -5
  25. package/src/components/Input/Input.scss +2 -1
  26. package/src/components/Input/Input.vue +26 -3
  27. package/src/components/Input/TextInput.vue +64 -25
  28. package/src/components/Input/__test__/TextInput.spec.js +1 -1
  29. package/src/components/Input/__test__/__snapshots__/Input.spec.js.snap +5 -1
  30. package/src/components/Input/__test__/__snapshots__/TextInput.spec.js.snap +7 -1
  31. package/src/components/InputDatePicker/InputDatePicker.vue +73 -68
  32. package/src/components/InputDatePicker/__test__/InputDatePicker.spec.js +24 -31
  33. package/src/components/ModalDialog/ModalDialog.vue +8 -1
  34. package/src/components/ModalDialog/__tests__/ModalDialog.spec.js +1 -31
  35. package/src/components/MultiSelect/MultSelectOption.vue +49 -0
  36. package/src/components/MultiSelect/__tests__/MultiSelect.spec.js +557 -0
  37. package/src/components/MultiSelect/__tests__/MultiSelectOption.spec.js +229 -0
  38. package/src/components/MultiSelect/__tests__/__snapshots__/MultiSelect.spec.js.snap +87 -0
  39. package/src/components/MultiSelect/__tests__/__snapshots__/MultiSelectOption.spec.js.snap +51 -0
  40. package/src/components/MultiSelect/index.vue +265 -0
  41. package/src/components/Radio/Radio.vue +13 -7
  42. package/src/components/Radio/__test__/Radio.spec.js +3 -1
  43. package/src/components/RadioGroup/RadioGroup.vue +18 -10
  44. package/src/components/Select/__tests__/Select.spec.js +422 -0
  45. package/src/components/Select/__tests__/SelectItem.spec.js +330 -0
  46. package/src/components/Select/__tests__/__snapshots__/Popover.spec.js.snap +8 -0
  47. package/src/components/Select/__tests__/__snapshots__/Select.spec.js.snap +71 -0
  48. package/src/components/Select/__tests__/__snapshots__/SelectItem.spec.js.snap +15 -0
  49. package/src/components/Select/__tests__/__snapshots__/SelectOption.spec.js.snap +25 -0
  50. package/src/components/Select/__tests__/__snapshots__/SelectPopover.spec.js.snap +8 -0
  51. package/src/components/Select/index.vue +308 -0
  52. package/src/components/Switch/Switch.vue +11 -4
  53. package/src/components/TemplatePreview/TemplatePreview.vue +30 -27
  54. package/src/components/TemplatePreview/TemplatePreviewModal.vue +11 -11
  55. package/src/components/TemplatePreview/types.d.ts +3 -3
  56. package/src/components/Toast/Toast.vue +13 -9
  57. package/src/components/Toast/ToastManager.ts +1 -4
  58. package/src/components/Toast/__tests__/ToastManager.spec.js +6 -10
  59. package/src/components/ToolTip/ToolTip.vue +1 -1
  60. package/src/components/index.ts +6 -6
  61. package/src/components/ui/dialog/DialogContent.vue +5 -5
  62. package/src/components/ui/drawer/DrawerContent.vue +2 -4
  63. package/src/components/ui/popover/PopoverContent.vue +2 -4
  64. package/src/components/ui/popover/PopoverOption.vue +4 -0
  65. package/src/components/ui/tooltip/TooltipContent.vue +2 -5
  66. package/src/components/ui/tooltip/TooltipTrigger.vue +4 -2
  67. package/src/index.ts +2 -9
  68. package/src/lib/layer-manager.ts +52 -24
  69. package/src/locales/en.json +3 -1
  70. package/src/locales/es.json +3 -1
  71. package/src/locales/pt_br.json +3 -1
  72. package/src/stories/Input.mdx +3 -0
  73. package/src/stories/LayerManager.docs.mdx +9 -9
  74. package/src/stories/LayerManager.stories.js +11 -54
  75. package/src/stories/ModalDialog.stories.js +0 -95
  76. package/src/stories/MultiSelect.stories.js +143 -45
  77. package/src/stories/Select.stories.js +161 -0
  78. package/src/stories/TemplatePreview.stories.js +27 -27
  79. package/src/stories/TemplatePreviewModal.stories.js +31 -31
  80. package/dist/apple-64.png +0 -0
  81. package/public/apple-64.png +0 -0
  82. package/src/components/MultiSelect/MultiSelect.vue +0 -297
  83. package/src/lib/__tests__/teleport-target.spec.ts +0 -73
  84. package/src/lib/teleport-target.ts +0 -46
@@ -4,16 +4,18 @@
4
4
  v-mask="mask"
5
5
  v-bind="attributes"
6
6
  :value="fullySanitize(modelValue)"
7
- :class="classes"
7
+ :class="[classes, { focus, 'use-focus-prop': useFocusProp }]"
8
8
  :type="nativeType"
9
+ :readonly="readonly"
9
10
  />
10
11
  <input
11
12
  v-else
12
13
  v-bind="attributes"
13
14
  :value="fullySanitize(modelValue)"
14
- :class="classes"
15
+ :class="[classes, { focus, 'use-focus-prop': useFocusProp }]"
15
16
  :type="nativeType"
16
17
  :maxlength="maxlength"
18
+ :readonly="readonly"
17
19
  />
18
20
  </template>
19
21
 
@@ -49,15 +51,25 @@ export default {
49
51
  },
50
52
  hasIconLeft: Boolean,
51
53
  hasIconRight: Boolean,
54
+ hasClearIcon: Boolean,
52
55
  maxlength: {
53
56
  type: Number,
54
57
  default: null,
55
58
  },
59
+ readonly: {
60
+ type: Boolean,
61
+ default: false,
62
+ },
63
+ useFocusProp: {
64
+ type: Boolean,
65
+ default: false,
66
+ },
67
+ focus: {
68
+ type: Boolean,
69
+ default: false,
70
+ },
56
71
  },
57
72
  emits: ['update:modelValue'],
58
- data() {
59
- return {};
60
- },
61
73
  computed: {
62
74
  attributes() {
63
75
  return {
@@ -77,6 +89,7 @@ export default {
77
89
  {
78
90
  'input--has-icon-left': this.hasIconLeft,
79
91
  'input--has-icon-right': this.hasIconRight,
92
+ 'input--has-clear-icon': this.hasClearIcon,
80
93
  },
81
94
  ];
82
95
  },
@@ -117,6 +130,13 @@ export default {
117
130
 
118
131
  &.input--has-icon-right {
119
132
  padding-right: $unnnic-space-10;
133
+ &.input--has-clear-icon {
134
+ padding-right: $unnnic-space-10 + $unnnic-space-6;
135
+ }
136
+ }
137
+
138
+ &.input--has-clear-icon {
139
+ padding-right: $unnnic-space-10;
120
140
  }
121
141
 
122
142
  &.error {
@@ -11,7 +11,8 @@
11
11
 
12
12
  transition: border-color 0.1s ease-in-out;
13
13
 
14
- &:focus {
14
+ &:focus:not(.use-focus-prop),
15
+ &.focus {
15
16
  border-color: $unnnic-color-border-active;
16
17
  }
17
18
 
@@ -13,6 +13,8 @@
13
13
  v-bind="$attrs"
14
14
  v-model="val"
15
15
  class="unnnic-form-input"
16
+ :useFocusProp="useFocusProp"
17
+ :focus="focus"
16
18
  :placeholder="placeholder"
17
19
  :iconLeft="iconLeft"
18
20
  :iconRight="iconRight"
@@ -25,6 +27,9 @@
25
27
  :nativeType="nativeType"
26
28
  :maxlength="maxlength"
27
29
  :disabled="disabled"
30
+ :readonly="readonly"
31
+ :showClear="showClear"
32
+ @clear="$emit('clear')"
28
33
  />
29
34
 
30
35
  <template
@@ -43,8 +48,8 @@ import UnnnicFormElement from '../FormElement/FormElement.vue';
43
48
 
44
49
  export default {
45
50
  name: 'UnnnicInput',
46
- components: {
47
- TextInput,
51
+ components: {
52
+ TextInput,
48
53
  UnnnicFormElement,
49
54
  },
50
55
  props: {
@@ -127,8 +132,26 @@ export default {
127
132
  type: Boolean,
128
133
  default: false,
129
134
  },
135
+ readonly: {
136
+ type: Boolean,
137
+ default: false,
138
+ },
139
+ useFocusProp: {
140
+ type: Boolean,
141
+ default: false,
142
+ },
143
+ focus: {
144
+ type: Boolean,
145
+ default: false,
146
+ },
147
+ showClear: {
148
+ type: Boolean,
149
+ default: false,
150
+ },
130
151
  },
131
- emits: ['update:modelValue'],
152
+
153
+ emits: ['update:modelValue', 'clear'],
154
+
132
155
  data() {
133
156
  return {
134
157
  val: this.modelValue,
@@ -12,9 +12,11 @@
12
12
  class="input-itself"
13
13
  :hasIconLeft="!!iconLeft"
14
14
  :hasIconRight="!!iconRight || allowTogglePassword"
15
+ :hasClearIcon="showClear"
15
16
  :maxlength="maxlength"
16
- @focus="onFocus"
17
- @blur="onBlur"
17
+ :readonly="readonly"
18
+ :useFocusProp="useFocusProp"
19
+ :focus="focus"
18
20
  />
19
21
 
20
22
  <UnnnicIcon
@@ -27,18 +29,28 @@
27
29
  @click="onIconLeftClick"
28
30
  />
29
31
 
30
- <UnnnicIcon
31
- v-if="iconRightSvg"
32
- :scheme="iconScheme"
33
- :icon="iconRightSvg"
34
- size="ant"
35
- :clickable="iconRightClickable || allowTogglePassword"
36
- :class="[
37
- 'icon-right',
38
- { clickable: iconRightClickable || allowTogglePassword },
39
- ]"
40
- @click="onIconRightClick"
41
- />
32
+ <section class="icon-right-container">
33
+ <UnnnicIcon
34
+ v-if="showClear"
35
+ class="icon-clear"
36
+ :scheme="iconScheme"
37
+ icon="close"
38
+ size="ant"
39
+ clickable
40
+ @click.stop="onClearClick"
41
+ />
42
+
43
+ <UnnnicIcon
44
+ v-if="iconRightSvg"
45
+ :scheme="iconScheme"
46
+ :icon="iconRightSvg"
47
+ size="ant"
48
+ :clickable="iconRightClickable || allowTogglePassword"
49
+ class="icon-right"
50
+ :class="{ clickable: iconRightClickable || allowTogglePassword }"
51
+ @click="onIconRightClick"
52
+ />
53
+ </section>
42
54
  </div>
43
55
  </template>
44
56
 
@@ -103,8 +115,24 @@ export default {
103
115
  type: Boolean,
104
116
  default: false,
105
117
  },
118
+ readonly: {
119
+ type: Boolean,
120
+ default: false,
121
+ },
122
+ useFocusProp: {
123
+ type: Boolean,
124
+ default: false,
125
+ },
126
+ focus: {
127
+ type: Boolean,
128
+ default: false,
129
+ },
130
+ showClear: {
131
+ type: Boolean,
132
+ default: false,
133
+ },
106
134
  },
107
- emits: ['icon-left-click', 'icon-right-click'],
135
+ emits: ['icon-left-click', 'icon-right-click', 'clear'],
108
136
  data() {
109
137
  return {
110
138
  isFocused: false,
@@ -128,7 +156,6 @@ export default {
128
156
  if (this.isDisabled) {
129
157
  return 'fg-muted';
130
158
  }
131
-
132
159
  return 'fg-base';
133
160
  },
134
161
 
@@ -138,7 +165,7 @@ export default {
138
165
  },
139
166
 
140
167
  methods: {
141
- focus() {
168
+ focusInput() {
142
169
  this.$refs['base-input'].$el.focus();
143
170
  },
144
171
 
@@ -154,6 +181,10 @@ export default {
154
181
  if (this.iconLeftClickable) this.$emit('icon-left-click');
155
182
  },
156
183
 
184
+ onClearClick() {
185
+ this.$emit('clear');
186
+ },
187
+
157
188
  onIconRightClick() {
158
189
  if (this.attributes.disabled) return;
159
190
  if (this.allowTogglePassword) this.showPassword = !this.showPassword;
@@ -171,25 +202,33 @@ export default {
171
202
  }
172
203
 
173
204
  .icon {
174
- &-left,
175
- &-right {
176
- &:not(.clickable) {
177
- pointer-events: none;
178
- }
179
- }
180
-
181
205
  &-left {
182
206
  position: absolute;
183
207
  top: 50%;
184
208
  transform: translateY(-50%);
185
209
  left: $unnnic-space-4;
210
+
211
+ &:not(.clickable) {
212
+ pointer-events: none;
213
+ }
186
214
  }
187
215
 
188
- &-right {
216
+ &-right-container {
189
217
  position: absolute;
190
218
  top: 50%;
191
219
  transform: translateY(-50%);
192
220
  right: $unnnic-space-4;
221
+
222
+ display: flex;
223
+ align-items: center;
224
+ gap: $unnnic-space-2;
225
+
226
+ .icon-clear {
227
+ cursor: pointer;
228
+ }
229
+ .icon-right:not(.clickable) {
230
+ pointer-events: none;
231
+ }
193
232
  }
194
233
  }
195
234
  </style>
@@ -129,7 +129,7 @@ describe('TextInput.vue', () => {
129
129
 
130
130
  test('focus method calls focus on base input element', () => {
131
131
  const focusSpy = vi.spyOn(wrapper.vm.$refs['base-input'].$el, 'focus');
132
- wrapper.vm.focus();
132
+ wrapper.vm.focusInput();
133
133
 
134
134
  expect(focusSpy).toHaveBeenCalled();
135
135
  focusSpy.mockRestore();
@@ -6,7 +6,11 @@ exports[`Input.vue > matches the snapshot 1`] = `
6
6
  <p data-v-7f222291="" class="unnnic-label__label">Sample Label</p>
7
7
  <!--v-if-->
8
8
  </section>
9
- <div data-v-a0d36167="" data-v-d890ad85="" class="text-input size--md unnnic-form-input" hascloudycolor="false" mask="####-####"><input data-v-86533b41="" data-v-a0d36167="" class="unnnic-form-input input-itself input size-md normal input--has-icon-left input--has-icon-right unnnic-form-input input-itself" hascloudycolor="false" placeholder="Enter text" iconleft="search" iconright="clear" iconleftclickable="true" iconrightclickable="true" type="text" value=""><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-left clickable" data-testid="material-icon" translate="no">search</span><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-right clickable" data-testid="material-icon" translate="no">clear</span></div>
9
+ <div data-v-a0d36167="" data-v-d890ad85="" class="text-input size--md unnnic-form-input" hascloudycolor="false" mask="####-####"><input data-v-86533b41="" data-v-a0d36167="" class="unnnic-form-input input-itself input size-md normal input--has-icon-left input--has-icon-right unnnic-form-input input-itself" hascloudycolor="false" placeholder="Enter text" iconleft="search" iconright="clear" iconleftclickable="true" iconrightclickable="true" showclear="false" type="text" value=""><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-left clickable" data-testid="material-icon" translate="no">search</span>
10
+ <section data-v-a0d36167="" class="icon-right-container">
11
+ <!--v-if--><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-right clickable" data-testid="material-icon" translate="no">clear</span>
12
+ </section>
13
+ </div>
10
14
  <section data-v-9f8d6c86="" class="unnnic-form-element__hints-container">
11
15
  <section data-v-9f8d6c86="" class="unnnic-form-element__message-container">
12
16
  <p data-v-9f8d6c86="" class="unnnic-form-element__message">Error message</p>
@@ -1,3 +1,9 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`TextInput.vue > matches the snapshot 1`] = `"<div data-v-a0d36167="" class="text-input size--md"><input data-v-86533b41="" data-v-a0d36167="" placeholder="Enter text" iconleft="search" iconright="clear" iconleftclickable="true" iconrightclickable="true" allowtogglepassword="false" class="input-itself input size-md normal input--has-icon-left input--has-icon-right input-itself" type="text" value=""><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-left clickable" data-testid="material-icon" translate="no">search</span><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-right clickable" data-testid="material-icon" translate="no">clear</span></div>"`;
3
+ exports[`TextInput.vue > matches the snapshot 1`] = `
4
+ "<div data-v-a0d36167="" class="text-input size--md"><input data-v-86533b41="" data-v-a0d36167="" placeholder="Enter text" iconleft="search" iconright="clear" iconleftclickable="true" iconrightclickable="true" allowtogglepassword="false" showclear="false" class="input-itself input size-md normal input--has-icon-left input--has-icon-right input-itself" type="text" value=""><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-left clickable" data-testid="material-icon" translate="no">search</span>
5
+ <section data-v-a0d36167="" class="icon-right-container">
6
+ <!--v-if--><span data-v-26446d8e="" data-v-a0d36167="" class="unnnic-icon material-symbols-rounded unnnic-icon-scheme--fg-base unnnic-icon-size--ant unnnic-icon__size--ant unnnic--clickable icon-right clickable" data-testid="material-icon" translate="no">clear</span>
7
+ </section>
8
+ </div>"
9
+ `;
@@ -1,61 +1,51 @@
1
1
  <template>
2
- <div :class="['dropdown', { 'fill-w': fillW }]">
3
- <UnnnicPopover v-model:open="isPopoverOpen">
4
- <UnnnicPopoverTrigger :asChild="true">
5
- <UnnnicInput
6
- data-testid="input"
7
- :class="['input', { 'date-picker-input-next': next }]"
8
- :size="size"
9
- :iconLeft="iconPosition === 'left' ? 'calendar_month' : undefined"
10
- :iconRight="iconPosition === 'right' ? 'calendar_month' : undefined"
11
- readonly
12
- :modelValue="overwrittenValue || filterText || ''"
13
- @click="openPopover"
14
- @focus="openPopover"
15
- />
16
- </UnnnicPopoverTrigger>
17
-
18
- <UnnnicPopoverContent
19
- width="auto"
20
- side="bottom"
21
- :align="popoverAlign"
22
- class="date-picker-popover-content"
23
- >
24
- <UnnnicDatePicker
25
- v-model:equivalentOption="overwrittenValue"
26
- data-testid="date-picker"
27
- variant="popover"
28
- :type="type"
29
- :clearLabel="clearText"
30
- :actionLabel="actionText"
31
- :months="months"
32
- :days="days"
33
- :options="options"
34
- :periodBaseDate="periodBaseDate"
35
- :initialStartDate="initialStartDate"
36
- :initialEndDate="initialEndDate"
37
- :minDate="minDate"
38
- :maxDate="maxDate"
39
- :disableClear="disableClear"
40
- @change="emitSelectDate"
41
- @submit="changeDate"
42
- />
43
- </UnnnicPopoverContent>
44
- </UnnnicPopover>
2
+ <div
3
+ ref="dropdown"
4
+ :class="['dropdown', { active: showCalendarFilter, 'fill-w': fillW }]"
5
+ >
6
+ <UnnnicInput
7
+ :class="['input', { 'date-picker-input-next': next }]"
8
+ :size="size"
9
+ :iconLeft="iconPosition === 'left' ? 'calendar_month' : undefined"
10
+ :iconRight="iconPosition === 'right' ? 'calendar_month' : undefined"
11
+ hasCloudyColor
12
+ readonly
13
+ :modelValue="overwrittenValue || filterText || ''"
14
+ @focus="showCalendarFilter = true"
15
+ />
16
+
17
+ <div
18
+ class="dropdown-data"
19
+ :style="{ [position]: '0' }"
20
+ >
21
+ <UnnnicDatePicker
22
+ v-if="showCalendarFilter"
23
+ v-model:equivalentOption="overwrittenValue"
24
+ :type="type"
25
+ :clearLabel="clearText"
26
+ :actionLabel="actionText"
27
+ :months="months"
28
+ :days="days"
29
+ :options="options"
30
+ :periodBaseDate="periodBaseDate"
31
+ :initialStartDate="initialStartDate"
32
+ :initialEndDate="initialEndDate"
33
+ :minDate="minDate"
34
+ :maxDate="maxDate"
35
+ :disableClear="disableClear"
36
+ @change="emitSelectDate"
37
+ @submit="changeDate"
38
+ />
39
+ </div>
45
40
  </div>
46
41
  </template>
47
42
 
48
43
  <script setup lang="ts">
49
- import { computed, ref } from 'vue';
44
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
50
45
  import moment from 'moment';
51
46
 
52
47
  import UnnnicInput from '../Input/Input.vue';
53
48
  import UnnnicDatePicker from '../DatePicker/DatePicker.vue';
54
- import {
55
- Popover as UnnnicPopover,
56
- PopoverContent as UnnnicPopoverContent,
57
- PopoverTrigger as UnnnicPopoverTrigger,
58
- } from '../ui/popover';
59
49
 
60
50
  defineOptions({
61
51
  name: 'UnnnicInputDatePicker',
@@ -122,11 +112,9 @@ const emit = defineEmits<{
122
112
  (e: 'selectDate', value: DateRangeValue): void;
123
113
  }>();
124
114
 
125
- const isPopoverOpen = ref(false);
115
+ const dropdown = ref<HTMLElement | null>(null);
116
+ const showCalendarFilter = ref(false);
126
117
  const overwrittenValue = ref('');
127
- const popoverAlign = computed<'start' | 'end'>(() =>
128
- props.position === 'right' ? 'end' : 'start',
129
- );
130
118
 
131
119
  const filterText = computed(() => {
132
120
  const dates: string[] = [];
@@ -172,12 +160,20 @@ function emitSelectDate(date: { startDate: string; endDate: string }) {
172
160
  emit('selectDate', formattedDates);
173
161
  }
174
162
 
163
+ function mouseout(event: MouseEvent | { target: EventTarget | null }) {
164
+ if (dropdown.value?.contains(event.target as Node)) {
165
+ return;
166
+ }
167
+
168
+ showCalendarFilter.value = false;
169
+ }
170
+
175
171
  function changeDate(value: { startDate: string; endDate: string }) {
176
172
  const startDate = value.startDate.replace(/(\d+)-(\d+)-(\d+)/, '$3-$1-$2');
177
173
 
178
174
  const endDate = value.endDate.replace(/(\d+)-(\d+)-(\d+)/, '$3-$1-$2');
179
175
 
180
- isPopoverOpen.value = false;
176
+ showCalendarFilter.value = false;
181
177
 
182
178
  emit('update:model-value', {
183
179
  start: startDate
@@ -187,30 +183,39 @@ function changeDate(value: { startDate: string; endDate: string }) {
187
183
  });
188
184
  }
189
185
 
190
- function openPopover() {
191
- isPopoverOpen.value = true;
192
- }
193
- </script>
194
-
195
- <style lang="scss">
196
- @use '@/assets/scss/unnnic' as *;
186
+ onMounted(() => {
187
+ window.document.body.addEventListener('click', mouseout as any);
188
+ });
197
189
 
198
- .date-picker-popover-content {
199
- overflow: hidden;
200
- border-radius: $unnnic-radius-2;
201
- padding: 0;
202
- }
203
- </style>
190
+ onBeforeUnmount(() => {
191
+ window.document.body.removeEventListener('click', mouseout as any);
192
+ });
193
+ </script>
204
194
 
205
195
  <style lang="scss" scoped>
206
196
  @use '@/assets/scss/unnnic' as *;
207
197
  .fill-w {
208
198
  width: 100%;
209
199
  }
210
-
211
200
  .dropdown {
201
+ position: relative;
212
202
  display: inline-block;
213
203
 
204
+ .dropdown-data {
205
+ position: absolute;
206
+ pointer-events: none;
207
+ display: none;
208
+ top: 100%;
209
+ z-index: 2;
210
+ margin-top: $unnnic-spacing-stack-nano;
211
+ width: max-content;
212
+ }
213
+
214
+ &.active .dropdown-data {
215
+ pointer-events: auto;
216
+ display: block;
217
+ }
218
+
214
219
  .input {
215
220
  display: inline-block;
216
221
  }
@@ -1,6 +1,5 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
2
+ import { beforeEach, describe, expect, it } from 'vitest';
4
3
  import InputDatePicker from '../InputDatePicker.vue';
5
4
 
6
5
  const factory = (props = {}) =>
@@ -12,7 +11,20 @@ const factory = (props = {}) =>
12
11
  },
13
12
  ...props,
14
13
  },
15
- attachTo: document.body,
14
+ global: {
15
+ stubs: {
16
+ UnnnicInput: {
17
+ name: 'UnnnicInput',
18
+ template:
19
+ '<input data-testid="input" v-bind="$attrs" @focus="$emit(\'focus\', $event)" />',
20
+ },
21
+ UnnnicDatePicker: {
22
+ name: 'UnnnicDatePicker',
23
+ props: ['minDate', 'maxDate', 'periodBaseDate', 'options'],
24
+ template: '<div data-testid="datepicker"></div>',
25
+ },
26
+ },
27
+ },
16
28
  });
17
29
 
18
30
  describe('InputDatePicker.vue', () => {
@@ -22,12 +34,6 @@ describe('InputDatePicker.vue', () => {
22
34
  wrapper = factory();
23
35
  });
24
36
 
25
- afterEach(() => {
26
- if (wrapper) {
27
- wrapper.unmount();
28
- }
29
- });
30
-
31
37
  it('renders input and does not show datepicker by default', () => {
32
38
  expect(wrapper.find('[data-testid="input"]').exists()).toBe(true);
33
39
  expect(wrapper.find('[data-testid="datepicker"]').exists()).toBe(false);
@@ -36,23 +42,13 @@ describe('InputDatePicker.vue', () => {
36
42
  it('opens datepicker when input receives focus', async () => {
37
43
  const input = wrapper.find('[data-testid="input"]');
38
44
  await input.trigger('focus');
39
- await wrapper.vm.$nextTick();
40
45
 
41
- expect(wrapper.findComponent({ name: 'UnnnicDatePicker' }).exists()).toBe(
42
- true,
43
- );
44
- expect(wrapper.vm.isPopoverOpen).toBe(true);
45
- });
46
-
47
- it('opens datepicker when input receives click', async () => {
48
- const input = wrapper.find('[data-testid="input"]');
49
- await input.trigger('click');
46
+ wrapper.vm.showCalendarFilter = true;
50
47
  await wrapper.vm.$nextTick();
51
48
 
52
49
  expect(wrapper.findComponent({ name: 'UnnnicDatePicker' }).exists()).toBe(
53
50
  true,
54
51
  );
55
- expect(wrapper.vm.isPopoverOpen).toBe(true);
56
52
  });
57
53
 
58
54
  it('computes filterText placeholder when there is no date selected', () => {
@@ -70,7 +66,6 @@ describe('InputDatePicker.vue', () => {
70
66
  });
71
67
 
72
68
  expect(withDates.vm.filterText).toBe('01-10-2025 ~ 01-20-2025');
73
- withDates.unmount();
74
69
  });
75
70
 
76
71
  it('computes initialStartDate and initialEndDate for DatePicker', () => {
@@ -83,11 +78,10 @@ describe('InputDatePicker.vue', () => {
83
78
 
84
79
  expect(withDates.vm.initialStartDate).toBe('01 10 2025');
85
80
  expect(withDates.vm.initialEndDate).toBe('01 20 2025');
86
- withDates.unmount();
87
81
  });
88
82
 
89
83
  it('emits selectDate with formatted dates when DatePicker emits change', async () => {
90
- wrapper.vm.isPopoverOpen = true;
84
+ wrapper.vm.showCalendarFilter = true;
91
85
  await wrapper.vm.$nextTick();
92
86
 
93
87
  const datePicker = wrapper.findComponent({ name: 'UnnnicDatePicker' });
@@ -110,7 +104,7 @@ describe('InputDatePicker.vue', () => {
110
104
  });
111
105
 
112
106
  it('emits update:model-value and closes dropdown when DatePicker emits submit', async () => {
113
- wrapper.vm.isPopoverOpen = true;
107
+ wrapper.vm.showCalendarFilter = true;
114
108
  await wrapper.vm.$nextTick();
115
109
 
116
110
  const datePicker = wrapper.findComponent({ name: 'UnnnicDatePicker' });
@@ -131,7 +125,7 @@ describe('InputDatePicker.vue', () => {
131
125
  end: '2025-01-20',
132
126
  });
133
127
 
134
- expect(wrapper.vm.isPopoverOpen).toBe(false);
128
+ expect(wrapper.vm.showCalendarFilter).toBe(false);
135
129
  });
136
130
 
137
131
  it('passes minDate, maxDate, options and periodBaseDate down to DatePicker', async () => {
@@ -143,7 +137,7 @@ describe('InputDatePicker.vue', () => {
143
137
  };
144
138
 
145
139
  wrapper = factory(props);
146
- wrapper.vm.isPopoverOpen = true;
140
+ wrapper.vm.showCalendarFilter = true;
147
141
  await wrapper.vm.$nextTick();
148
142
 
149
143
  const datePicker = wrapper.findComponent({ name: 'UnnnicDatePicker' });
@@ -153,14 +147,13 @@ describe('InputDatePicker.vue', () => {
153
147
  expect(dpProps.maxDate).toBe(props.maxDate);
154
148
  expect(dpProps.periodBaseDate).toBe(props.periodBaseDate);
155
149
  expect(dpProps.options).toEqual(props.options);
156
- expect(dpProps.variant).toBe('popover');
157
150
  });
158
151
 
159
- it('aligns the popover to the end when position is right', () => {
160
- const rightWrapper = factory({ position: 'right' });
152
+ it('closes dropdown on mouseout when clicking outside', () => {
153
+ wrapper.vm.showCalendarFilter = true;
161
154
 
162
- expect(rightWrapper.vm.popoverAlign).toBe('end');
155
+ wrapper.vm.mouseout({ target: document.createElement('div') });
163
156
 
164
- rightWrapper.unmount();
157
+ expect(wrapper.vm.showCalendarFilter).toBe(false);
165
158
  });
166
159
  });
@@ -178,10 +178,18 @@ export default {
178
178
  },
179
179
  };
180
180
  },
181
+ watch: {
182
+ modelValue(value) {
183
+ this.updateBodyOverflow(value);
184
+ },
185
+ },
181
186
  methods: {
182
187
  close() {
183
188
  this.$emit('update:modelValue', false);
184
189
  },
190
+ updateBodyOverflow(isHidden) {
191
+ document.body.style.overflow = isHidden ? 'hidden' : '';
192
+ },
185
193
  persistentHandler(event) {
186
194
  if (this.persistent) {
187
195
  event.preventDefault();
@@ -193,7 +201,6 @@ export default {
193
201
 
194
202
  <style lang="scss" scoped>
195
203
  @use '@/assets/scss/unnnic' as *;
196
-
197
204
  .unnnic-modal-dialog__container {
198
205
  &__left-sidebar {
199
206
  background-color: $unnnic-color-neutral-black;