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

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.9",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -1,11 +1,15 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`UnnnicAlert.vue > matches the snapshot 1`] = `
4
- "<div data-v-284427ba="" class="unnnic-alert unnnic-alert-position--top-right"><span data-v-26446d8e="" data-v-284427ba="" class="material-symbols-rounded unnnic-icon-scheme--primary unnnic-icon-size--sm" data-testid="material-icon" translate="no" data-test="unnnic-icon">alert-icon</span>
5
- <div data-v-284427ba="" class="unnnic-alert__content">
6
- <div data-v-284427ba="" class="unnnic-alert__title">TEST ALERT</div>
7
- <div data-v-284427ba="" class="unnnic-alert__text">This is an alert message</div>
8
- </div>
9
- <div data-v-284427ba="" class="unnnic-alert__close-text unnnic--clickable">CLOSE</div>
10
- </div>"
4
+ "<transition-stub data-v-c3231c18="" name="toast-slide" appear="true" persisted="false" css="true" icon="alert-icon" position="top-right" closetext="Close">
5
+ <aside data-v-c3231c18="" class="unnnic-toast unnnic-toast--informational" role="status" aria-live="polite" data-testid="unnnic-toast">
6
+ <section data-v-c3231c18="" class="unnnic-toast__content">
7
+ <header data-v-c3231c18="" class="unnnic-toast__header"><span data-v-26446d8e="" data-v-c3231c18="" class="material-symbols-rounded unnnic-icon-scheme--blue-500 unnnic-icon-size--ant" data-testid="toast-type-icon" translate="no">info</span>
8
+ <h3 data-v-c3231c18="" class="unnnic-toast__title">Test Alert</h3><span data-v-26446d8e="" data-v-c3231c18="" class="material-symbols-rounded unnnic-icon-scheme--neutral-dark unnnic-icon-size--ant unnnic--clickable unnnic-toast__close" data-testid="toast-close-button" translate="no">close</span>
9
+ </header>
10
+ <!--v-if-->
11
+ </section>
12
+ <!--v-if-->
13
+ </aside>
14
+ </transition-stub>"
11
15
  `;
@@ -1,10 +1,10 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`Alert.vue > matches snapshot 1`] = `
4
- "<div data-v-fb94f284="" class="alert-container">
4
+ "<div data-v-fb94f284="" class="alert-container" version="1.0" linkhref="https://example.com" linktarget="_blank" linktext="Learn more">
5
5
  <div data-v-fb94f284="" class="alert alert--scheme-aux-green">
6
6
  <div data-v-fb94f284="" class="alert__progress"></div>
7
- <div data-v-fb94f284="" class="alert__text">Test Alert</div><a data-v-fb94f284="" class="alert__link" href="https://example.com" target="_blank">Learn more</a>
7
+ <div data-v-fb94f284="" class="alert__text">Test Alert</div>
8
8
  <div data-v-fb94f284="" class="alert__close">
9
9
  <unnnic-icon-stub data-v-fb94f284="" filled="false" next="false" icon="close-1" clickable="false" size="sm" scheme="neutral-white"></unnnic-icon-stub>
10
10
  </div>
@@ -16,7 +16,7 @@
16
16
  {
17
17
  'unnnic-data-table__header-cell--clickable': header.isSortable,
18
18
  'unnnic-data-table__header-cell--sorting':
19
- sort.header === header.title && sort.order !== '',
19
+ sortState.header === header.title && sortState.order !== '',
20
20
  },
21
21
  ]"
22
22
  @click.stop="handleClickHeader(header)"
@@ -31,12 +31,12 @@
31
31
  </template>
32
32
  <template v-if="header.isSortable">
33
33
  <IconArrowsDefault
34
- v-if="sort.header !== header.title"
34
+ v-if="sortState.header !== header.title"
35
35
  class="order-default-icon"
36
36
  data-testid="arrow-default-icon"
37
37
  />
38
38
  <Icon
39
- v-else-if="sort.order === 'asc'"
39
+ v-else-if="sortState.order === 'asc'"
40
40
  clickable
41
41
  size="ant"
42
42
  :icon="'switch_left'"
@@ -44,7 +44,7 @@
44
44
  data-testid="arrow-asc-icon"
45
45
  />
46
46
  <Icon
47
- v-else-if="sort.order === 'desc'"
47
+ v-else-if="sortState.order === 'desc'"
48
48
  clickable
49
49
  size="ant"
50
50
  :icon="'switch_left'"
@@ -183,6 +183,12 @@ type DataTableItem = {
183
183
  [key: string]: any;
184
184
  };
185
185
 
186
+ type SortState = {
187
+ header: string;
188
+ itemKey: string;
189
+ order: string;
190
+ };
191
+
186
192
  interface Props {
187
193
  headers: DataTableHeader[];
188
194
  items: DataTableItem[];
@@ -198,6 +204,7 @@ interface Props {
198
204
  pageTotal?: number;
199
205
  pageInterval?: number;
200
206
  locale?: string;
207
+ sort?: SortState;
201
208
  }
202
209
 
203
210
  defineOptions({
@@ -225,6 +232,7 @@ const props = withDefaults(defineProps<Props>(), {
225
232
  pageTotal: 0,
226
233
  pageInterval: 5,
227
234
  locale: 'en',
235
+ sort: undefined,
228
236
  });
229
237
 
230
238
  const defaultTranslations = {
@@ -251,12 +259,16 @@ const headersItemsKeys: ComputedRef<string[]> = computed(() => {
251
259
  return props.headers.map((header) => header.itemKey);
252
260
  });
253
261
 
254
- const sort = ref({
262
+ const internalSort = ref({
255
263
  header: '',
256
264
  itemKey: '',
257
265
  order: '',
258
266
  });
259
267
 
268
+ const sortState = computed(() => {
269
+ return props.sort !== undefined ? props.sort : internalSort.value;
270
+ });
271
+
260
272
  const getHeaderColumnSize = (header: DataTableHeader): string => {
261
273
  return typeof header.size === 'number'
262
274
  ? `${header.size || 1}fr`
@@ -271,9 +283,12 @@ const gridTemplateColumns: ComputedRef<string> = computed(() => {
271
283
  return columnSizes.join(' ');
272
284
  });
273
285
 
274
- const handleSort = (header: typeof sort.value, order: string) => {
275
- sort.value = { ...header, order };
276
- emit('update:sort', sort.value);
286
+ const handleSort = (header: SortState, order: string) => {
287
+ if (props.sort === undefined) {
288
+ internalSort.value = { ...header, order };
289
+ }
290
+
291
+ emit('update:sort', { ...header, order });
277
292
  };
278
293
 
279
294
  const handleClickHeader = (header: DataTableHeader) => {
@@ -286,9 +301,9 @@ const handleClickHeader = (header: DataTableHeader) => {
286
301
  };
287
302
 
288
303
  const nextSort =
289
- header.title !== sort.value.header
304
+ header.title !== sortState.value.header
290
305
  ? 'asc'
291
- : nextSortOrderMapper[sort.value.order];
306
+ : nextSortOrderMapper[sortState.value.order];
292
307
 
293
308
  handleSort(
294
309
  nextSort === ''
@@ -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(() => {
@@ -330,3 +330,63 @@ export const Loading = {
330
330
  isLoading: true,
331
331
  },
332
332
  };
333
+
334
+ export const ControlledSort = {
335
+ args: { headers, items },
336
+ render: (args) => ({
337
+ components: {
338
+ UnnnicDataTable,
339
+ },
340
+ setup() {
341
+ let sortState = {
342
+ header: 'ID',
343
+ itemKey: 'id',
344
+ order: 'asc',
345
+ };
346
+
347
+ const handleSort = ({ order, header, itemKey }) => {
348
+ action('update:sort')({ order, header, itemKey });
349
+ sortState = { header, itemKey, order };
350
+
351
+ if (order === 'asc') {
352
+ args.items = [...args.items].sort((a, b) => {
353
+ if (itemKey === 'id') return a.id - b.id;
354
+ return a[itemKey] > b[itemKey] ? 1 : -1;
355
+ });
356
+ } else if (order === 'desc') {
357
+ args.items = [...args.items].sort((a, b) => {
358
+ if (itemKey === 'id') return b.id - a.id;
359
+ return a[itemKey] < b[itemKey] ? 1 : -1;
360
+ });
361
+ }
362
+ };
363
+
364
+ const updatePage = (page) => {
365
+ action('update:page')(page);
366
+ args.page = page;
367
+ };
368
+
369
+ const itemClick = (item) => {
370
+ action('itemClick')(item);
371
+ };
372
+
373
+ return { args, sortState, handleSort, updatePage, itemClick };
374
+ },
375
+ template: `
376
+ <div>
377
+ <UnnnicDataTable
378
+ v-bind="args"
379
+ :headers="args.headers"
380
+ :items="args.items"
381
+ :pageTotal="125"
382
+ :pageInterval="5"
383
+ v-model:sort="sortState"
384
+ @update:sort="handleSort"
385
+ @update:page="updatePage"
386
+ @itemClick="itemClick"
387
+ >
388
+ </UnnnicDataTable>
389
+ </div>
390
+ `,
391
+ }),
392
+ };