@weni/unnnic-system 3.1.2 → 3.1.3

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.1.2",
3
+ "version": "3.1.3",
4
4
  "type": "commonjs",
5
5
  "files": [
6
6
  "dist",
@@ -50,9 +50,8 @@
50
50
  "vue": "^3.4.8"
51
51
  },
52
52
  "dependencies": {
53
- "@emoji-mart/data": "^1.1.2",
54
53
  "@vueuse/components": "^10.4.1",
55
- "emoji-mart": "^5.5.2",
54
+ "emoji-mart-vue-fast": "^15.0.5",
56
55
  "lodash": "^4.17.21",
57
56
  "mime": "^4.0.1",
58
57
  "mitt": "^3.0.1",
@@ -99,4 +98,4 @@
99
98
  "vue-eslint-parser": "^9.4.2",
100
99
  "vue-tsc": "^3.0.5"
101
100
  }
102
- }
101
+ }
@@ -1,64 +1,112 @@
1
1
  <template>
2
2
  <div
3
- ref="pickerContainer"
3
+ ref="emojiPickerRef"
4
4
  :class="['emoji-picker', `emoji-picker--${position}`]"
5
5
  @click.stop
6
6
  @keypress.enter.stop
7
- />
7
+ >
8
+ <Picker
9
+ :data="emojiIndex"
10
+ set="apple"
11
+ theme="light"
12
+ :preview="false"
13
+ :search="true"
14
+ nav-position="bottom"
15
+ no-results-emoji="cry"
16
+ :max-frequent-rows="3"
17
+ :i18n="translations"
18
+ :color="accentColor"
19
+ @select="onEmojiSelect"
20
+ />
21
+ </div>
8
22
  </template>
9
23
 
10
- <script>
11
- import i18n from '@emoji-mart/data/i18n/pt.json';
12
- import data from '@emoji-mart/data/sets/14/apple.json';
13
- import { Picker } from 'emoji-mart/';
14
-
15
- export default {
16
- name: 'UnnnicEmojiPicker',
17
- props: {
18
- returnName: {
19
- type: Boolean,
20
- default: false,
21
- },
22
- position: {
23
- type: String,
24
- default: 'top',
25
- validator: (position) => ['top', 'bottom'].includes(position),
26
- },
27
- },
28
- emits: ['close', 'emojiSelected'],
29
- computed: {
30
- emojiPickerPreferences() {
31
- return {
32
- data,
33
- set: 'apple',
34
- theme: 'light',
35
- previewPosition: 'none',
36
- searchPosition: 'none',
37
- navPosition: 'bottom',
38
- noResultsEmoji: 'cry',
39
- maxFrequentRows: 3,
40
- };
41
- },
42
- },
43
- mounted() {
44
- this.initPicker();
45
- },
46
- methods: {
47
- initPicker() {
48
- // eslint-disable-next-line no-new
49
- new Picker({
50
- i18n,
51
- parent: this.$refs.pickerContainer,
52
- onEmojiSelect: this.onEmojiSelect,
53
- onClickOutside: () => this.$emit('close'),
54
- ...this.emojiPickerPreferences,
55
- });
56
- },
57
- onEmojiSelect(emoji) {
58
- this.$emit('emojiSelected', this.returnName ? emoji.id : emoji.native);
59
- },
60
- },
61
- };
24
+ <script setup lang="ts">
25
+ import { computed, ref, onMounted, onUnmounted } from 'vue'
26
+ import { get } from 'lodash'
27
+ import i18n from '../../utils/plugins/i18n'
28
+ import { Picker, EmojiIndex } from 'emoji-mart-vue-fast/src'
29
+ import data from 'emoji-mart-vue-fast/data/all.json'
30
+ import 'emoji-mart-vue-fast/css/emoji-mart.css'
31
+ import UnnnicI18n from '../../mixins/i18n'
32
+
33
+ defineOptions({
34
+ mixins: [UnnnicI18n],
35
+ })
36
+
37
+ interface Emoji {
38
+ id: string
39
+ native: string
40
+ }
41
+
42
+ export interface Props {
43
+ returnName?: boolean
44
+ position?: 'top' | 'bottom'
45
+ locale?: string
46
+ }
47
+
48
+ const props = withDefaults(defineProps<Props>(), {
49
+ returnName: false,
50
+ position: 'top',
51
+ locale: 'pt-br'
52
+ })
53
+
54
+ const emit = defineEmits<{
55
+ close: []
56
+ emojiSelected: [emoji: string]
57
+ }>()
58
+
59
+ const emojiPickerRef = ref<HTMLElement>()
60
+ const emojiIndex = computed(() => new EmojiIndex(data))
61
+
62
+ const accentColor = '#00A49F' // $unnnic-color-weni-600
63
+
64
+ const currentLocale = computed(() => props.locale || 'pt-br')
65
+
66
+ const translation = (key: string) => {
67
+ const messages: Record<string, unknown> = i18n.global.messages as Record<string, unknown>
68
+ const loc = currentLocale.value
69
+ const localeMsgs = messages?.[loc] || messages?.[loc?.toLowerCase?.()] || messages?.[loc?.toUpperCase?.()]
70
+ const enMsgs = messages?.['en']
71
+ return get(localeMsgs, key) || get(enMsgs, key) || key
72
+ }
73
+
74
+ const translations = computed(() => ({
75
+ search: translation('emoji_picker.search'),
76
+ notfound: translation('emoji_picker.notfound'),
77
+ categories: {
78
+ search: translation('emoji_picker.categories.search'),
79
+ recent: translation('emoji_picker.categories.recent'),
80
+ smileys: translation('emoji_picker.categories.smileys'),
81
+ people: translation('emoji_picker.categories.people'),
82
+ nature: translation('emoji_picker.categories.nature'),
83
+ foods: translation('emoji_picker.categories.foods'),
84
+ activity: translation('emoji_picker.categories.activity'),
85
+ places: translation('emoji_picker.categories.places'),
86
+ objects: translation('emoji_picker.categories.objects'),
87
+ symbols: translation('emoji_picker.categories.symbols'),
88
+ flags: translation('emoji_picker.categories.flags'),
89
+ custom: translation('emoji_picker.categories.custom')
90
+ }
91
+ }))
92
+
93
+ const handleClickOutside = (event: Event) => {
94
+ if (emojiPickerRef.value && !emojiPickerRef.value.contains(event.target as Node)) {
95
+ emit('close')
96
+ }
97
+ }
98
+
99
+ onMounted(() => {
100
+ document.addEventListener('click', handleClickOutside)
101
+ })
102
+
103
+ onUnmounted(() => {
104
+ document.removeEventListener('click', handleClickOutside)
105
+ })
106
+
107
+ const onEmojiSelect = (emoji: Emoji) => {
108
+ emit('emojiSelected', props.returnName ? emoji.id : emoji.native)
109
+ }
62
110
  </script>
63
111
 
64
112
  <style lang="scss" scoped>
@@ -78,16 +126,35 @@ export default {
78
126
  animation-name: slideInDown;
79
127
  }
80
128
 
81
- :deep(em-emoji-picker) {
82
- // Most variables don't work here
83
- --border-radius: 16px;
84
- --font-family: Lato, sans-serif; // $unnnic-font-family
85
- --rgb-accent: 0, 164, 159; // $unnnic-color-weni-600
86
- --rgb-color: 59, 65, 77; // $unnnic-color-neutral-darkest
87
- --color-border: rgb(244, 246, 248);
88
- --shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.1);
89
-
129
+ :deep(.emoji-mart) {
130
+ border-radius: 16px;
131
+ font-family: Lato, sans-serif; // $unnnic-font-family
132
+ border: 1px solid rgb(244, 246, 248);
133
+ box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.1);
90
134
  cursor: default;
135
+ color: #3B4151; // $unnnic-color-neutral-darkest
136
+ }
137
+
138
+ :deep(.emoji-mart-emoji) {
139
+ cursor: pointer;
140
+ }
141
+
142
+ :deep(.emoji-mart-category .emoji-mart-emoji span) {
143
+ cursor: pointer;
144
+ }
145
+
146
+ :deep(.emoji-mart-anchor) {
147
+ cursor: pointer;
148
+ color: #858585;
149
+ }
150
+
151
+ :deep(.emoji-mart-anchor:hover),
152
+ :deep(.emoji-mart-anchor-selected) {
153
+ color: #00A49F; // $unnnic-color-weni-600
154
+ }
155
+
156
+ :deep(.emoji-mart-anchor-bar) {
157
+ background-color: #00A49F; // $unnnic-color-weni-600
91
158
  }
92
159
  }
93
160
 
@@ -1,16 +1,37 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
-
4
3
  import UnnnicEmojiPicker from '../EmojiPicker.vue';
5
4
 
6
- import { Picker } from 'emoji-mart/';
5
+ vi.mock('emoji-mart-vue-fast/src', () => ({
6
+ Picker: {
7
+ name: 'Picker',
8
+ props: ['data', 'set', 'theme', 'preview', 'search', 'navPosition', 'noResultsEmoji', 'maxFrequentRows', 'i18n', 'color'],
9
+ emits: ['select', 'click-outside'],
10
+ template: '<div class="emoji-mart-picker"></div>'
11
+ },
12
+ EmojiIndex: vi.fn().mockImplementation(() => ({}))
13
+ }));
14
+
15
+ vi.mock('emoji-mart-vue-fast/data/all.json', () => ({
16
+ default: {}
17
+ }));
7
18
 
8
- vi.mock('emoji-mart/', () => {
19
+ vi.mock('emoji-mart-vue-fast/css/emoji-mart.css', () => ({}));
20
+
21
+ vi.mock('../../utils/plugins/i18n', () => {
22
+ const current = 'pt-br'
9
23
  return {
10
- Picker: vi.fn(),
11
- };
24
+ default: {
25
+ global: {
26
+ get locale() { return current },
27
+ set locale(_v) {},
28
+ t: (key) => `${key}`,
29
+ },
30
+ },
31
+ }
12
32
  });
13
33
 
34
+
14
35
  describe('UnnnicEmojiPicker', () => {
15
36
  let wrapper;
16
37
 
@@ -21,59 +42,99 @@ describe('UnnnicEmojiPicker', () => {
21
42
  position: 'top',
22
43
  returnName: false,
23
44
  },
45
+ global: {
46
+ config: {
47
+ errorHandler: () => {},
48
+ warnHandler: () => {},
49
+ },
50
+ stubs: {
51
+ Picker: {
52
+ name: 'Picker',
53
+ props: ['data', 'set', 'theme', 'preview', 'search', 'navPosition', 'noResultsEmoji', 'maxFrequentRows', 'i18n', 'color'],
54
+ emits: ['select', 'click-outside'],
55
+ template: '<div class="emoji-mart-picker" @select="$emit(\'select\', $event)" @click-outside="$emit(\'click-outside\')"></div>'
56
+ }
57
+ }
58
+ }
24
59
  });
25
60
  });
26
61
 
27
62
  it('renders correctly with the correct position class', () => {
28
- const position = wrapper.vm.position;
29
63
  expect(wrapper.classes()).toContain('emoji-picker');
30
- expect(wrapper.classes()).toContain(`emoji-picker--${position}`);
64
+ expect(wrapper.classes()).toContain('emoji-picker--top');
31
65
  });
32
66
 
33
- it('calls `initPicker` on mount and passes the correct configuration to Picker', () => {
34
- expect(Picker).toHaveBeenCalledTimes(1);
35
- const pickerConfig = Picker.mock.calls[0][0];
36
- expect(pickerConfig).toMatchObject({
37
- i18n: expect.any(Object),
38
- parent: wrapper.vm.$refs.pickerContainer,
39
- onEmojiSelect: expect.any(Function),
40
- onClickOutside: expect.any(Function),
41
- data: expect.any(Object),
42
- set: 'apple',
43
- theme: 'light',
44
- previewPosition: 'none',
45
- searchPosition: 'none',
46
- navPosition: 'bottom',
47
- noResultsEmoji: 'cry',
48
- maxFrequentRows: 3,
49
- });
67
+ it('renders the Picker component with correct props', () => {
68
+ const picker = wrapper.findComponent({ name: 'Picker' });
69
+ expect(picker.exists()).toBe(true);
70
+ expect(picker.props('set')).toBe('apple');
71
+ expect(picker.props('theme')).toBe('light');
72
+ expect(picker.props('preview')).toBe(false);
73
+ expect(picker.props('search')).toBe(true);
74
+ expect(picker.props('navPosition')).toBe('bottom');
75
+ expect(picker.props('noResultsEmoji')).toBe('cry');
76
+ expect(picker.props('maxFrequentRows')).toBe(3);
77
+ expect(picker.props('color')).toBe('#00A49F');
78
+ expect(picker.props('i18n')).toBeDefined();
50
79
  });
51
80
 
52
- it('emits "emojiSelected" with the correct data when an emoji is selected', () => {
81
+ it('emits "emojiSelected" with the correct data when an emoji is selected', async () => {
53
82
  const emojiMock = { id: 'smile', native: '😊' };
54
- const onEmojiSelect = Picker.mock.calls[0][0].onEmojiSelect;
83
+ const picker = wrapper.findComponent({ name: 'Picker' });
55
84
 
56
- onEmojiSelect(emojiMock);
85
+ await picker.vm.$emit('select', emojiMock);
57
86
  expect(wrapper.emitted('emojiSelected')).toBeTruthy();
58
87
  expect(wrapper.emitted('emojiSelected')[0]).toEqual(['😊']);
59
88
  });
60
89
 
61
90
  it('emits "emojiSelected" with the name when `returnName` is true', async () => {
62
- await wrapper.setProps({ returnName: true });
91
+ const custom = mount(UnnnicEmojiPicker, {
92
+ props: { position: 'top', returnName: true },
93
+ global: {
94
+ stubs: {
95
+ Picker: {
96
+ name: 'Picker',
97
+ props: ['data', 'set', 'theme', 'preview', 'search', 'navPosition', 'noResultsEmoji', 'maxFrequentRows', 'i18n', 'color'],
98
+ emits: ['select', 'click-outside'],
99
+ template: '<div class="emoji-mart-picker" @select="$emit(\'select\', $event)"></div>'
100
+ }
101
+ }
102
+ }
103
+ });
63
104
 
64
105
  const emojiMock = { id: 'smile', native: '😊' };
65
- const onEmojiSelect = Picker.mock.calls[0][0].onEmojiSelect;
106
+ const picker = custom.findComponent({ name: 'Picker' });
66
107
 
67
- onEmojiSelect(emojiMock);
68
- expect(wrapper.emitted('emojiSelected')).toBeTruthy();
69
- expect(wrapper.emitted('emojiSelected')[0]).toEqual(['smile']);
108
+ await picker.vm.$emit('select', emojiMock);
109
+ expect(custom.emitted('emojiSelected')).toBeTruthy();
110
+ expect(custom.emitted('emojiSelected')[0]).toEqual(['smile']);
111
+ custom.unmount();
70
112
  });
71
113
 
72
- it('emits "close" when `onClickOutside` is triggered', () => {
73
- const onClickOutside = Picker.mock.calls[0][0].onClickOutside;
74
-
75
- onClickOutside();
114
+ it('emits "close" when clicking outside the component', async () => {
115
+ // Simula um click fora do componente
116
+ const outsideElement = document.createElement('div');
117
+ document.body.appendChild(outsideElement);
118
+
119
+ const clickEvent = new Event('click', { bubbles: true });
120
+ Object.defineProperty(clickEvent, 'target', { value: outsideElement });
121
+
122
+ document.dispatchEvent(clickEvent);
123
+
124
+ await wrapper.vm.$nextTick();
76
125
  expect(wrapper.emitted('close')).toBeTruthy();
126
+
127
+ // Cleanup
128
+ document.body.removeChild(outsideElement);
129
+ });
130
+
131
+ it('renders bottom position class when position is bottom', async () => {
132
+ const custom = mount(UnnnicEmojiPicker, {
133
+ props: { position: 'bottom' },
134
+ global: { stubs: { Picker: { name: 'Picker', props: ['data','set','theme','preview','search','navPosition','noResultsEmoji','maxFrequentRows','i18n','color'], template: '<div />' } } }
135
+ });
136
+ expect(custom.classes()).toContain('emoji-picker--bottom');
137
+ custom.unmount();
77
138
  });
78
139
 
79
140
  it('stops event propagation on click', async () => {
@@ -94,4 +155,19 @@ describe('UnnnicEmojiPicker', () => {
94
155
  });
95
156
  expect(stopPropagationSpy).toHaveBeenCalled();
96
157
  });
158
+
159
+ it('uses pt-br as default locale', () => {
160
+ const picker = wrapper.findComponent({ name: 'Picker' });
161
+ const i18nProp = picker.props('i18n');
162
+ expect(i18nProp).toBeDefined();
163
+ expect(i18nProp.search).toBeDefined();
164
+ });
165
+
166
+ it('uses custom locale when provided', async () => {
167
+ await wrapper.setProps({ locale: 'en' });
168
+ const picker = wrapper.findComponent({ name: 'Picker' });
169
+ const i18nProp = picker.props('i18n');
170
+ expect(i18nProp).toBeDefined();
171
+ expect(i18nProp.search).toBeDefined();
172
+ });
97
173
  });
@@ -22,5 +22,23 @@
22
22
  "image": "Image",
23
23
  "audio": "Audio message"
24
24
  }
25
+ },
26
+ "emoji_picker": {
27
+ "search": "Search",
28
+ "notfound": "No Emoji Found",
29
+ "categories": {
30
+ "search": "Search Results",
31
+ "recent": "Frequently Used",
32
+ "smileys": "Smileys & Emotion",
33
+ "people": "People & Body",
34
+ "nature": "Animals & Nature",
35
+ "foods": "Food & Drink",
36
+ "activity": "Activity",
37
+ "places": "Travel & Places",
38
+ "objects": "Objects",
39
+ "symbols": "Symbols",
40
+ "flags": "Flags",
41
+ "custom": "Custom"
42
+ }
25
43
  }
26
44
  }
@@ -22,5 +22,23 @@
22
22
  "image": "Imagen",
23
23
  "audio": "Mensaje de audio"
24
24
  }
25
+ },
26
+ "emoji_picker": {
27
+ "search": "Buscar",
28
+ "notfound": "No se encontró ningún emoji",
29
+ "categories": {
30
+ "search": "Resultados de búsqueda",
31
+ "recent": "Usados frecuentemente",
32
+ "smileys": "Caritas y Emociones",
33
+ "people": "Personas y Cuerpo",
34
+ "nature": "Animales y Naturaleza",
35
+ "foods": "Comidas y Bebidas",
36
+ "activity": "Actividades",
37
+ "places": "Viajes y Lugares",
38
+ "objects": "Objetos",
39
+ "symbols": "Símbolos",
40
+ "flags": "Banderas",
41
+ "custom": "Personalizados"
42
+ }
25
43
  }
26
44
  }
@@ -22,5 +22,23 @@
22
22
  "image": "Imagem",
23
23
  "audio": "Mensagem de áudio"
24
24
  }
25
+ },
26
+ "emoji_picker": {
27
+ "search": "Buscar",
28
+ "notfound": "Nenhum emoji encontrado",
29
+ "categories": {
30
+ "search": "Resultados da busca",
31
+ "recent": "Usados frequentemente",
32
+ "smileys": "Carinhas & Pessoas",
33
+ "people": "Pessoas e Corpo",
34
+ "nature": "Animais e Natureza",
35
+ "foods": "Comidas e Bebidas",
36
+ "activity": "Atividades",
37
+ "places": "Viagens e Lugares",
38
+ "objects": "Objetos",
39
+ "symbols": "Símbolos",
40
+ "flags": "Bandeiras",
41
+ "custom": "Personalizados"
42
+ }
25
43
  }
26
44
  }