@weni/unnnic-system 3.12.8 → 3.13.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 (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{es-62c1e8d3.mjs → es-41fceca9.mjs} +1 -1
  3. package/dist/{index-ef197fd5.mjs → index-cca96b43.mjs} +81802 -81503
  4. package/dist/index.d.ts +660 -20
  5. package/dist/{pt-br-198b147b.mjs → pt-br-a3088529.mjs} +1 -1
  6. package/dist/style.css +1 -1
  7. package/dist/unnnic.mjs +142 -140
  8. package/dist/unnnic.umd.js +35 -35
  9. package/package.json +1 -1
  10. package/src/components/Input/BaseInput.vue +25 -5
  11. package/src/components/Input/Input.scss +2 -1
  12. package/src/components/Input/Input.vue +24 -1
  13. package/src/components/Input/TextInput.vue +64 -25
  14. package/src/components/Input/__test__/TextInput.spec.js +1 -1
  15. package/src/components/Input/__test__/__snapshots__/Input.spec.js.snap +5 -1
  16. package/src/components/Input/__test__/__snapshots__/TextInput.spec.js.snap +7 -1
  17. package/src/components/Select/__tests__/Select.spec.js +422 -0
  18. package/src/components/Select/__tests__/SelectItem.spec.js +330 -0
  19. package/src/components/Select/__tests__/__snapshots__/Popover.spec.js.snap +8 -0
  20. package/src/components/Select/__tests__/__snapshots__/Select.spec.js.snap +71 -0
  21. package/src/components/Select/__tests__/__snapshots__/SelectItem.spec.js.snap +15 -0
  22. package/src/components/Select/__tests__/__snapshots__/SelectOption.spec.js.snap +25 -0
  23. package/src/components/Select/__tests__/__snapshots__/SelectPopover.spec.js.snap +8 -0
  24. package/src/components/Select/index.vue +308 -0
  25. package/src/components/index.ts +4 -4
  26. package/src/components/ui/popover/PopoverOption.vue +4 -0
  27. package/src/locales/en.json +3 -1
  28. package/src/locales/es.json +3 -1
  29. package/src/locales/pt_br.json +3 -1
  30. package/src/stories/Input.mdx +3 -0
  31. package/src/stories/Select.stories.js +161 -0
@@ -0,0 +1,308 @@
1
+ <template>
2
+ <div
3
+ class="unnnic-select"
4
+ @keydown="handleKeyDown"
5
+ >
6
+ <Popover
7
+ :open="openPopover"
8
+ @update:open="openPopover = $event"
9
+ >
10
+ <PopoverTrigger class="w-full">
11
+ <UnnnicInput
12
+ ref="inputRef"
13
+ :modelValue="inputValue"
14
+ class="unnnic-select__input"
15
+ readonly
16
+ useFocusProp
17
+ :focus="openPopover"
18
+ :size="props.size"
19
+ :placeholder="props.placeholder"
20
+ :label="props.label"
21
+ :errors="props.errors"
22
+ :message="props.message"
23
+ :iconRight="openPopover ? 'keyboard_arrow_up' : 'keyboard_arrow_down'"
24
+ :disabled="props.disabled"
25
+ :showClear="!!selectedItem"
26
+ @clear="emit('update:modelValue', '')"
27
+ />
28
+ </PopoverTrigger>
29
+ <PopoverContent
30
+ align="start"
31
+ :class="'h-full'"
32
+ :style="popoverContentCustomStyles"
33
+ :width="inputWidthString"
34
+ >
35
+ <div class="unnnic-select__content">
36
+ <UnnnicInput
37
+ v-if="props.enableSearch"
38
+ class="unnnic-select__input-search"
39
+ :modelValue="props.search"
40
+ :placeholder="$t('search')"
41
+ iconLeft="search"
42
+ @update:model-value="handleSearch"
43
+ />
44
+ <p
45
+ v-if="filteredOptions.length === 0"
46
+ class="unnnic-select__content-no-results"
47
+ >
48
+ {{ $t('without_results') }}
49
+ </p>
50
+ <PopoverOption
51
+ v-for="(option, index) in filteredOptions"
52
+ v-else
53
+ :key="option[props.itemValue]"
54
+ :data-option-index="index"
55
+ data-testid="select-option"
56
+ :label="option[props.itemLabel]"
57
+ :active="
58
+ option[props.itemValue] === selectedItem?.[props.itemValue]
59
+ "
60
+ :focused="focusedOptionIndex === index"
61
+ :disabled="option.disabled"
62
+ @click="handleSelectOption(option)"
63
+ />
64
+ </div>
65
+ </PopoverContent>
66
+ </Popover>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { computed, ref, watch, nextTick } from 'vue';
72
+ import { useElementSize } from '@vueuse/core';
73
+
74
+ import UnnnicInput from '../Input/Input.vue';
75
+
76
+ import {
77
+ Popover,
78
+ PopoverTrigger,
79
+ PopoverContent,
80
+ PopoverOption,
81
+ } from '../ui/popover/index';
82
+
83
+ import UnnnicI18n from '../../mixins/i18n';
84
+
85
+ defineOptions({
86
+ name: 'UnnnicSelect',
87
+ mixins: [UnnnicI18n],
88
+ });
89
+
90
+ interface SelectProps {
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ options: Array<{ [key: string]: any }>;
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ modelValue: any;
95
+ returnObject?: boolean;
96
+ itemLabel?: string;
97
+ itemValue?: string;
98
+ placeholder?: string;
99
+ label?: string;
100
+ type?: 'normal' | 'error';
101
+ errors?: string | Array<string>;
102
+ message?: string;
103
+ size?: 'sm' | 'md';
104
+ optionsLines?: number;
105
+ enableSearch?: boolean;
106
+ search?: string;
107
+ locale?: string;
108
+ disabled?: boolean;
109
+ }
110
+
111
+ const props = withDefaults(defineProps<SelectProps>(), {
112
+ size: 'md',
113
+ type: 'normal',
114
+ placeholder: '',
115
+ optionsLines: 5,
116
+ returnObject: false,
117
+ itemLabel: 'label',
118
+ itemValue: 'value',
119
+ locale: 'en',
120
+ enableSearch: false,
121
+ disabled: false,
122
+ label: '',
123
+ errors: '',
124
+ message: '',
125
+ search: '',
126
+ });
127
+
128
+ const emit = defineEmits<{
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ 'update:modelValue': [value: any];
131
+ 'update:search': [value: string];
132
+ }>();
133
+
134
+ const openPopover = ref(false);
135
+ const inputRef = ref<HTMLInputElement | null>(null);
136
+ const { width: inputWidth } = useElementSize(inputRef);
137
+
138
+ const inputWidthString = computed(() => {
139
+ return `${inputWidth.value}px`;
140
+ });
141
+
142
+ watch(openPopover, () => {
143
+ if (!openPopover.value) {
144
+ handleSearch('');
145
+ } else {
146
+ focusedOptionIndex.value = -1;
147
+ }
148
+
149
+ if (openPopover.value && props.modelValue) {
150
+ const selectedOptionIndex = props.options.findIndex(
151
+ (option) =>
152
+ option[props.itemValue] === selectedItem.value?.[props.itemValue],
153
+ );
154
+ scrollToOption(selectedOptionIndex, 'instant', 'center');
155
+ }
156
+ });
157
+
158
+ const handleKeyDown = (event) => {
159
+ const { key } = event;
160
+ const validKeys = ['ArrowUp', 'ArrowDown', 'Enter'];
161
+
162
+ if (validKeys.includes(key)) {
163
+ event.preventDefault();
164
+ if (key === 'ArrowUp') {
165
+ if (focusedOptionIndex.value === 0) return;
166
+ focusedOptionIndex.value--;
167
+ scrollToOption(focusedOptionIndex.value);
168
+ }
169
+ if (key === 'ArrowDown') {
170
+ if (focusedOptionIndex.value === filteredOptions.value.length - 1) return;
171
+ focusedOptionIndex.value++;
172
+ scrollToOption(focusedOptionIndex.value);
173
+ }
174
+ if (key === 'Enter' && focusedOptionIndex.value !== -1) {
175
+ handleSelectOption(filteredOptions.value[focusedOptionIndex.value]);
176
+ }
177
+ }
178
+ };
179
+
180
+ const focusedOptionIndex = ref<number>(-1);
181
+
182
+ const scrollToOption = (
183
+ index: number,
184
+ behavior: 'smooth' | 'instant' = 'smooth',
185
+ block: 'center' | 'start' | 'end' | 'nearest' = 'center',
186
+ ) => {
187
+ nextTick(() => {
188
+ const option = document.querySelector(`[data-option-index="${index}"]`);
189
+ if (option) {
190
+ option.scrollIntoView?.({ behavior, block });
191
+ }
192
+ });
193
+ };
194
+
195
+ const calculatedPopoverHeight = computed(() => {
196
+ if (!props.options || props.options.length === 0) return 'unset';
197
+ const popoverPadding = 32;
198
+ const popoverGap = 4;
199
+ // 37 = 21px (height) + 16px (padding)
200
+ const fieldsHeight = 37 * props.optionsLines;
201
+ const gapsCompensation = props.enableSearch ? 1 : 2;
202
+
203
+ const size =
204
+ fieldsHeight +
205
+ popoverPadding +
206
+ (popoverGap * props.optionsLines - gapsCompensation);
207
+
208
+ return `${props.enableSearch ? size + 45 + 1 : size}px`;
209
+ });
210
+
211
+ const popoverContentCustomStyles = computed(() => {
212
+ const emptyFilteredOptions = filteredOptions.value?.length === 0;
213
+ return {
214
+ overflow: 'auto',
215
+ display: 'flex',
216
+ flexDirection: 'column',
217
+ minHeight: calculatedPopoverHeight.value,
218
+ maxHeight: emptyFilteredOptions ? 'unset' : calculatedPopoverHeight.value,
219
+ height: emptyFilteredOptions ? calculatedPopoverHeight.value : 'unset',
220
+ };
221
+ });
222
+
223
+ const selectedItem = computed(() => {
224
+ if (props.returnObject) return props.modelValue;
225
+
226
+ return props.options.find(
227
+ (option) => option[props.itemValue] === props.modelValue,
228
+ );
229
+ });
230
+
231
+ const inputValue = computed(() => {
232
+ return selectedItem.value?.[props.itemLabel];
233
+ });
234
+
235
+ const handleSelectOption = (option) => {
236
+ if (
237
+ option[props.itemValue] === selectedItem.value?.[props.itemValue] ||
238
+ option.disabled
239
+ )
240
+ return;
241
+
242
+ emit(
243
+ 'update:modelValue',
244
+ props.returnObject ? option : option[props.itemValue],
245
+ );
246
+ openPopover.value = false;
247
+ };
248
+
249
+ const handleSearch = (value: string) => {
250
+ emit('update:search', value);
251
+ };
252
+
253
+ const filteredOptions = computed(() => {
254
+ if (!props.enableSearch || !props.search) return props.options;
255
+
256
+ return props.options.filter(
257
+ (option) =>
258
+ option[props.itemLabel]
259
+ .toLowerCase()
260
+ .includes(props.search?.toLowerCase()) ||
261
+ option[props.itemValue]
262
+ .toLowerCase()
263
+ .includes(props.search?.toLowerCase()),
264
+ );
265
+ });
266
+ </script>
267
+
268
+ <style lang="scss" scoped>
269
+ @use '@/assets/scss/unnnic' as *;
270
+
271
+ :deep(.unnnic-select__input) {
272
+ cursor: pointer;
273
+ }
274
+
275
+ :deep(.unnnic-select__input-search) {
276
+ > .icon-left {
277
+ color: $unnnic-color-fg-base;
278
+ }
279
+ }
280
+
281
+ :deep(.unnnic-select__input) {
282
+ > .icon-right {
283
+ color: $unnnic-color-fg-base;
284
+ }
285
+ }
286
+
287
+ .unnnic-select {
288
+ &__content {
289
+ display: flex;
290
+ flex-direction: column;
291
+ padding: 0;
292
+ margin: 0;
293
+ gap: $unnnic-space-1;
294
+
295
+ height: -webkit-fill-available;
296
+
297
+ &-no-results {
298
+ margin: 0;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ height: 100%;
303
+ font: $unnnic-font-emphasis;
304
+ color: $unnnic-color-fg-muted;
305
+ }
306
+ }
307
+ }
308
+ </style>
@@ -34,7 +34,7 @@ import modal from './Modal/Modal.vue';
34
34
  import modalUpload from './ModalUpload/ModalUpload.vue';
35
35
  import { callAlert, callModal } from '../utils/call';
36
36
  import selectSmart from './SelectSmart/SelectSmart.vue';
37
- // import select from './Select/Select.vue';
37
+ import select from './Select/index.vue';
38
38
  import selectItem from './Select/SelectItem.vue';
39
39
  // import selectListItem from './SelectListItem/SelectListItem.vue';
40
40
  import multiSelect from './MultiSelect/MultiSelect.vue';
@@ -156,7 +156,7 @@ export const components: ComponentsMap = {
156
156
  unnnicModalDialog: ModalDialog,
157
157
  unnnicModalUpload: modalUpload,
158
158
  unnnicSelectSmart: selectSmart,
159
- // unnnicSelect: select,
159
+ unnnicSelect: select,
160
160
  unnnicSelectItem: selectItem,
161
161
  // unnnicSelectListItem: selectListItem,
162
162
  unnnicMultiSelect: multiSelect,
@@ -271,7 +271,7 @@ export const unnnicModalDialog = ModalDialog;
271
271
  export const unnnicModalNext = ModalNext;
272
272
  export const unnnicModalUpload = modalUpload;
273
273
  export const unnnicSelectSmart = selectSmart as VueComponent;
274
- // export const unnnicSelect = select;
274
+ export const unnnicSelect = select as VueComponent;
275
275
  export const unnnicSelectItem = selectItem;
276
276
  // export const unnnicSelectListItem = selectListItem;
277
277
  export const unnnicMultiSelect = multiSelect as VueComponent;
@@ -385,7 +385,7 @@ export const UnnnicModalDialog = ModalDialog;
385
385
  export const UnnnicModalNext = ModalNext;
386
386
  export const UnnnicModalUpload = modalUpload;
387
387
  export const UnnnicSelectSmart = selectSmart as VueComponent;
388
- // export const UnnnicSelect = select;
388
+ export const UnnnicSelect = select as VueComponent;
389
389
  export const UnnnicSelectItem = selectItem;
390
390
  // export const UnnnicSelectListItem = selectListItem;
391
391
  export const UnnnicMultiSelect = multiSelect as VueComponent;
@@ -98,6 +98,10 @@ const schemeColor = computed(() => {
98
98
  }
99
99
 
100
100
  &__label {
101
+ overflow: hidden;
102
+ text-overflow: ellipsis;
103
+ white-space: nowrap;
104
+
101
105
  @each $name, $color in $unnnic-scheme-colors {
102
106
  &.unnnic-popover-option__label--#{$name} {
103
107
  color: $color;
@@ -44,5 +44,7 @@
44
44
  "flags": "Flags",
45
45
  "custom": "Custom"
46
46
  }
47
- }
47
+ },
48
+ "search": "Search",
49
+ "without_results": "No results found"
48
50
  }
@@ -44,5 +44,7 @@
44
44
  "flags": "Banderas",
45
45
  "custom": "Personalizados"
46
46
  }
47
- }
47
+ },
48
+ "search": "Buscar",
49
+ "without_results": "No se encontró ningún resultado"
48
50
  }
@@ -44,5 +44,7 @@
44
44
  "flags": "Bandeiras",
45
45
  "custom": "Personalizados"
46
46
  }
47
- }
47
+ },
48
+ "search": "Buscar",
49
+ "without_results": "Nenhum resultado encontrado"
48
50
  }
@@ -42,6 +42,9 @@ The `Input` component is designed to provide an input field for users to enter t
42
42
  | placeholder | `String` of the native input placeholder | `String` | `''` |
43
43
  | iconLeft | `String` of the left icon | `String` | `undefined` |
44
44
  | iconRight | `String` of the right icon | `String` | `false` |
45
+ | useFocusProp | `Boolean` to enable use focus by prop (ignore native css) | `true` \| `false` | `false` |
46
+ | focus | `Boolean` to enable focus component status | `true` \| `false` | `false` |
47
+
45
48
 
46
49
  ## Example
47
50
  Some examples of uses of the `UnnnicInput`
@@ -0,0 +1,161 @@
1
+ import UnnnicSelect from '../components/Select/index.vue';
2
+
3
+ const options = [
4
+ { label: 'Option 1', value: 'option1', altValue: 'alt_value_option1' },
5
+ { label: 'Option 2', value: 'option2', altValue: 'alt_value_option2' },
6
+ { label: 'Option 3', value: 'option3', altValue: 'alt_value_option3' },
7
+ { label: 'Option 4', value: 'option4', altValue: 'alt_value_option4' },
8
+ { label: 'Option 5', value: 'option5', altValue: 'alt_value_option5' },
9
+ { label: 'Option 6', value: 'option6', altValue: 'alt_value_option6' },
10
+ { label: 'Option 7', value: 'option7', altValue: 'alt_value_option7' },
11
+ { label: 'Option 8', value: 'option8', altValue: 'alt_value_option8' },
12
+ { label: 'Option 9', value: 'option9', altValue: 'alt_value_option9' },
13
+ { label: 'Option 10', value: 'option10', disabled: true },
14
+ ];
15
+
16
+ export default {
17
+ title: 'Form/Select',
18
+ component: UnnnicSelect,
19
+ tags: ['autodocs'],
20
+ parameters: {
21
+ docs: {
22
+ description: {
23
+ component:
24
+ 'Select is designed to solve common problems related to option selection.',
25
+ },
26
+ },
27
+ },
28
+ argTypes: {
29
+ options: {
30
+ description:
31
+ 'List of items to be displayed in the options. If an item has a `disabled` key set to `true`, that item will be disabled. By default, the component looks for the `label` and `value` keys to display and update the model, but these are not required if you wish to customize them using the `itemLabel` and `itemValue` props, respectively.',
32
+ },
33
+ placeholder: {
34
+ description: 'Text to be displayed in the placeholder.',
35
+ },
36
+ label: {
37
+ description: 'Text to be displayed in the label.',
38
+ },
39
+ modelValue: {
40
+ description:
41
+ 'Model variable. Its type will always match the return type of the key specified by `itemValue`, or the entire options object when `returnObject` prop is set to `true`.',
42
+ },
43
+ returnObject: {
44
+ description:
45
+ 'Prop to indicate that the full option object should be returned in the v-model.',
46
+ },
47
+ itemLabel: {
48
+ description:
49
+ 'Field of the option item that should be used to render the option label.',
50
+ },
51
+ itemValue: {
52
+ description:
53
+ 'Field of the option item that should be used to return value to v-model.',
54
+ },
55
+ type: {
56
+ description: 'Select state type',
57
+ },
58
+ errors: {
59
+ description:
60
+ "Error message or messages. When it's an array, the messages will be separated by commas.",
61
+ },
62
+ message: {
63
+ description: 'Help or hint message.',
64
+ },
65
+ size: {
66
+ description: 'Select size',
67
+ },
68
+ optionsLines: {
69
+ description:
70
+ 'Number of options to display at most; if exceeded, scrolling will be enabled.',
71
+ },
72
+ enableSearch: {
73
+ description:
74
+ 'If true, enables a search field displayed inside the options popover. The `update:search` event is emitted on each input.',
75
+ },
76
+ search: {
77
+ description: 'Search value.',
78
+ },
79
+ locale: {
80
+ description: 'Locale for i18n translations.',
81
+ },
82
+ disabled: {
83
+ description: 'Disable the select.',
84
+ },
85
+ },
86
+ render: (args) => ({
87
+ components: { UnnnicSelect },
88
+ setup() {
89
+ const handleSearch = (value) => {
90
+ args.search = value;
91
+ };
92
+ return { args, handleSearch };
93
+ },
94
+ data() {
95
+ return {
96
+ exampleValue: null,
97
+ };
98
+ },
99
+ template: `
100
+ <div style="width: 300px;">
101
+ <p>modelValue: {{ exampleValue }}</p>
102
+ <unnnic-select v-model="exampleValue" v-bind="args" @update:search="handleSearch" />
103
+ </div>
104
+
105
+ `,
106
+ }),
107
+ };
108
+
109
+ export const Default = {
110
+ args: {
111
+ placeholder: 'Placeholder',
112
+ label: 'Label',
113
+ options,
114
+ },
115
+ };
116
+
117
+ export const ReturnObject = {
118
+ args: {
119
+ returnObject: true,
120
+ placeholder: 'Placeholder',
121
+ label: 'Label',
122
+ options,
123
+ },
124
+ };
125
+
126
+ export const AlternativeValueKey = {
127
+ args: {
128
+ itemValue: 'altValue',
129
+ placeholder: 'Placeholder',
130
+ label: 'Label',
131
+ options,
132
+ },
133
+ };
134
+
135
+ export const AlternativeValueLabel = {
136
+ args: {
137
+ itemLabel: 'altValue',
138
+ placeholder: 'Placeholder',
139
+ label: 'Label',
140
+ options,
141
+ },
142
+ };
143
+
144
+ export const Disabled = {
145
+ args: {
146
+ placeholder: 'Placeholder',
147
+ label: 'Label',
148
+ options,
149
+ disabled: true,
150
+ },
151
+ };
152
+
153
+ export const WithSearch = {
154
+ args: {
155
+ placeholder: 'Placeholder',
156
+ label: 'Label',
157
+ options,
158
+ enableSearch: true,
159
+ search: '',
160
+ },
161
+ };