@weni/unnnic-system 3.2.9-alpha.6 → 3.2.9-alpha.7

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": "@weni/unnnic-system",
3
- "version": "3.2.9-alpha.6",
3
+ "version": "3.2.9-alpha.7",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -5,6 +5,7 @@
5
5
  {
6
6
  'unnnic-select-option--disabled': props.disabled,
7
7
  'unnnic-select-option--active': props.active,
8
+ 'unnnic-select-option--focused': props.focused,
8
9
  },
9
10
  ]"
10
11
  >
@@ -21,11 +22,13 @@ interface SelectOptionProps {
21
22
  label: string;
22
23
  disabled?: boolean;
23
24
  active?: boolean;
25
+ focused?: boolean;
24
26
  }
25
27
 
26
28
  const props = withDefaults(defineProps<SelectOptionProps>(), {
27
29
  disabled: false,
28
30
  active: false,
31
+ focused: false,
29
32
  });
30
33
  </script>
31
34
 
@@ -43,6 +46,11 @@ const props = withDefaults(defineProps<SelectOptionProps>(), {
43
46
  padding: $unnnic-space-2 $unnnic-space-4;
44
47
  font: $unnnic-font-emphasis;
45
48
 
49
+ &:hover:not(&--active):not(&--disabled),
50
+ &--focused {
51
+ background-color: $unnnic-color-bg-soft;
52
+ }
53
+
46
54
  &--active {
47
55
  background-color: $unnnic-color-bg-active;
48
56
  color: $unnnic-color-fg-inverted;
@@ -237,13 +237,13 @@ describe('UnnnicSelect.vue', () => {
237
237
  describe('computed properties', () => {
238
238
  test('calculatedMaxHeight returns correct value', () => {
239
239
  const maxHeight = wrapper.vm.calculatedMaxHeight;
240
- expect(maxHeight).toBe('225px'); // (37 * 5) + 40 = 225
240
+ expect(maxHeight).toBe('235px');
241
241
  });
242
242
 
243
243
  test('calculatedMaxHeight includes search height when enabled', async () => {
244
244
  await wrapper.setProps({ enableSearch: true });
245
245
  const maxHeight = wrapper.vm.calculatedMaxHeight;
246
- expect(maxHeight).toBe('275px'); // (37 * 5) + 40 + 50 = 275
246
+ expect(maxHeight).toBe('289px');
247
247
  });
248
248
 
249
249
  test('calculatedMaxHeight returns unset when no options', async () => {
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div class="unnnic-select">
2
+ <div
3
+ class="unnnic-select"
4
+ @keydown="handleKeyDown"
5
+ >
3
6
  <UnnnicPopover
4
7
  v-model="openPopover"
5
8
  :popoverBalloonProps="{ maxHeight: calculatedMaxHeight }"
@@ -30,12 +33,14 @@
30
33
  @update:modelValue="handleSearch"
31
34
  />
32
35
  <UnnnicSelectOption
33
- v-for="option in filteredOptions"
36
+ v-for="(option, index) in filteredOptions"
34
37
  :key="option[props.itemValue]"
38
+ :data-option-index="index"
35
39
  :label="option[props.itemLabel]"
36
40
  :active="
37
41
  option[props.itemValue] === selectedItem?.[props.itemValue]
38
42
  "
43
+ :focused="focusedOptionIndex === index"
39
44
  :disabled="option.disabled"
40
45
  @click="handleSelectOption(option)"
41
46
  />
@@ -46,7 +51,7 @@
46
51
  </template>
47
52
 
48
53
  <script setup lang="ts">
49
- import { computed, ref, watch } from 'vue';
54
+ import { computed, ref, watch, nextTick } from 'vue';
50
55
  import UnnnicInput from '../Input/Input.vue';
51
56
  import UnnnicPopover from '../Popover/index.vue';
52
57
  import UnnnicSelectOption from './SelectOption.vue';
@@ -102,13 +107,64 @@ const openPopover = ref(false);
102
107
  watch(openPopover, () => {
103
108
  if (!openPopover.value) {
104
109
  handleSearch('');
110
+ } else {
111
+ focusedOptionIndex.value = -1;
112
+ }
113
+
114
+ if (openPopover.value && props.modelValue) {
115
+ const selectedOptionIndex = props.options.findIndex(
116
+ (option) =>
117
+ option[props.itemValue] === selectedItem.value[props.itemValue],
118
+ );
119
+ scrollToOption(selectedOptionIndex, 'instant', 'center');
105
120
  }
106
121
  });
107
122
 
123
+ const handleKeyDown = (event) => {
124
+ const { key } = event;
125
+ const validKeys = ['ArrowUp', 'ArrowDown', 'Enter'];
126
+ if (validKeys.includes(key)) {
127
+ event.preventDefault();
128
+ if (key === 'ArrowUp') {
129
+ if (focusedOptionIndex.value === 0) return;
130
+ focusedOptionIndex.value--;
131
+ scrollToOption(focusedOptionIndex.value);
132
+ }
133
+ if (key === 'ArrowDown') {
134
+ if (focusedOptionIndex.value === filteredOptions.value.length - 1) return;
135
+ focusedOptionIndex.value++;
136
+ scrollToOption(focusedOptionIndex.value);
137
+ }
138
+ if (key === 'Enter') {
139
+ handleSelectOption(filteredOptions.value[focusedOptionIndex.value]);
140
+ }
141
+ }
142
+ };
143
+
144
+ const focusedOptionIndex = ref<number>(-1);
145
+
146
+ const scrollToOption = (
147
+ index: number,
148
+ behavior: 'smooth' | 'instant' = 'smooth',
149
+ block: 'center' | 'start' | 'end' | 'nearest' = 'center',
150
+ ) => {
151
+ nextTick(() => {
152
+ const option = document.querySelector(`[data-option-index="${index}"]`);
153
+ if (option) {
154
+ option.scrollIntoView({ behavior, block });
155
+ }
156
+ });
157
+ };
158
+
108
159
  const calculatedMaxHeight = computed(() => {
109
160
  if (!props.options || props.options.length === 0) return 'unset';
110
- const fieldsHeight = 37 * props.optionsLines + 40;
111
- return `${props.enableSearch ? fieldsHeight + 50 : fieldsHeight}px`;
161
+ const popoverPadding = 32;
162
+ const popoverGap = 4;
163
+ // 37 = 21px (height) + 16px (padding)
164
+ const fieldsHeight = 37 * props.optionsLines;
165
+ const size =
166
+ fieldsHeight + popoverPadding + (popoverGap * props.optionsLines - 2);
167
+ return `${props.enableSearch ? size + 54 : size}px`;
112
168
  });
113
169
 
114
170
  const selectedItem = computed(() => {