@pequity/squirrel 10.0.3 → 10.1.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 (59) hide show
  1. package/README.md +20 -1
  2. package/dist/cjs/chunks/p-action-bar.js +17 -14
  3. package/dist/cjs/chunks/p-date-picker.js +3 -1
  4. package/dist/cjs/chunks/p-dropdown-select.js +27 -26
  5. package/dist/cjs/chunks/p-inline-date-picker.js +3 -1
  6. package/dist/cjs/chunks/p-pagination-info.js +2 -2
  7. package/dist/cjs/chunks/p-pagination.js +13 -11
  8. package/dist/cjs/chunks/p-tabs-pills.js +8 -8
  9. package/dist/cjs/index.js +101 -48
  10. package/dist/cjs/p-drawer.js +4 -4
  11. package/dist/cjs/p-input-search.js +5 -4
  12. package/dist/cjs/p-modal.js +3 -3
  13. package/dist/es/chunks/p-action-bar.js +18 -15
  14. package/dist/es/chunks/p-date-picker.js +3 -1
  15. package/dist/es/chunks/p-dropdown-select.js +27 -26
  16. package/dist/es/chunks/p-inline-date-picker.js +3 -1
  17. package/dist/es/chunks/p-pagination-info.js +2 -2
  18. package/dist/es/chunks/p-pagination.js +13 -11
  19. package/dist/es/chunks/p-tabs-pills.js +8 -8
  20. package/dist/es/index.js +102 -49
  21. package/dist/es/p-drawer.js +4 -4
  22. package/dist/es/p-input-search.js +5 -4
  23. package/dist/es/p-modal.js +3 -3
  24. package/dist/squirrel/index.d.ts +1 -0
  25. package/dist/squirrel/plugin/index.d.ts +11 -0
  26. package/dist/squirrel.css +40 -40
  27. package/package.json +28 -25
  28. package/squirrel/components/p-action-bar/p-action-bar.vue +4 -1
  29. package/squirrel/components/p-btn/p-btn.spec.js +0 -1
  30. package/squirrel/components/p-checkbox/p-checkbox.stories.js +2 -2
  31. package/squirrel/components/p-date-picker/p-date-picker.vue +3 -2
  32. package/squirrel/components/p-drawer/p-drawer.spec.js +364 -0
  33. package/squirrel/components/p-drawer/p-drawer.vue +8 -2
  34. package/squirrel/components/p-dropdown/p-dropdown.spec.js +252 -55
  35. package/squirrel/components/p-dropdown-select/p-dropdown-select.vue +16 -12
  36. package/squirrel/components/p-file-upload/p-file-upload.spec.js +0 -1
  37. package/squirrel/components/p-file-upload/p-file-upload.vue +26 -9
  38. package/squirrel/components/p-inline-date-picker/p-inline-date-picker.vue +3 -1
  39. package/squirrel/components/p-input-search/p-input-search.vue +2 -2
  40. package/squirrel/components/p-modal/p-modal.vue +1 -1
  41. package/squirrel/components/p-pagination/p-pagination.vue +3 -3
  42. package/squirrel/components/p-pagination-info/p-pagination-info.vue +2 -2
  43. package/squirrel/components/p-progress-bar/{p-progess-bar.spec.js → p-progress-bar.spec.js} +7 -5
  44. package/squirrel/components/p-select-btn/p-select-btn.spec.js +104 -0
  45. package/squirrel/components/p-select-list/p-select-list.vue +7 -5
  46. package/squirrel/components/p-select-pill/p-select-pill.spec.js +114 -0
  47. package/squirrel/components/p-table/usePTableColResize.spec.js +123 -11
  48. package/squirrel/components/p-table/usePTableHeaderWrap.spec.js +1 -1
  49. package/squirrel/components/p-table/usePTableRowVirtualizer.spec.js +207 -0
  50. package/squirrel/components/p-table-header-cell/p-table-header-cell.stories.js +3 -0
  51. package/squirrel/components/p-table-sort/p-table-sort.vue +4 -4
  52. package/squirrel/components/p-tabs-pills/p-tabs-pills.vue +1 -1
  53. package/squirrel/index.spec.js +5 -0
  54. package/squirrel/index.ts +1 -0
  55. package/squirrel/locales/en-US.json +47 -0
  56. package/squirrel/locales/fr-CA.json +47 -0
  57. package/squirrel/plugin/index.spec.ts +140 -0
  58. package/squirrel/plugin/index.ts +54 -0
  59. package/squirrel/utils/listKeyboardNavigation.spec.js +58 -0
@@ -18,15 +18,29 @@ const createMockedKbdNavigationSvc = () => {
18
18
  };
19
19
  };
20
20
 
21
- const Popper = {
22
- template: `<div class="popper"><slot /></div>`,
23
- };
21
+ const createVDropdownStub = (options = {}) => {
22
+ const { popperMethods = {} } = options;
24
23
 
25
- const PopperContent = {
26
- template: `<div class="popper-content"><slot /></div>`,
27
- };
24
+ const Popper = {
25
+ template: '<div class="popper"><slot /></div>',
26
+ data() {
27
+ return {
28
+ isShown: false,
29
+ $_referenceNode: null,
30
+ };
31
+ },
32
+ methods: {
33
+ hide: vi.fn(),
34
+ $_detachPopperNode: vi.fn(),
35
+ $emit: vi.fn(),
36
+ ...popperMethods,
37
+ },
38
+ };
39
+
40
+ const PopperContent = {
41
+ template: '<div class="popper-content"><slot /></div>',
42
+ };
28
43
 
29
- const createVDropdownStub = () => {
30
44
  return {
31
45
  template: `
32
46
  <div ref="vPopper">
@@ -38,10 +52,7 @@ const createVDropdownStub = () => {
38
52
  </PopperContent>
39
53
  </div>
40
54
  `,
41
- components: {
42
- Popper,
43
- PopperContent,
44
- },
55
+ components: { Popper, PopperContent },
45
56
  mounted() {
46
57
  this.$emit('show');
47
58
  },
@@ -51,26 +62,40 @@ const createVDropdownStub = () => {
51
62
  };
52
63
  };
53
64
 
65
+ const createWrapper = (options = {}) => {
66
+ const { props = {}, slots = {}, attrs = {}, global = {}, stubOptions = {}, ...wrapperOptions } = options;
67
+
68
+ return createWrapperFor(PDropdown, {
69
+ props: {
70
+ enableArrowNavigation: false,
71
+ enableCloseOnEsc: false,
72
+ ...props,
73
+ },
74
+ slots: {
75
+ default: '<button>Open popper</button>',
76
+ popper: '<div>Dropdown content</div>',
77
+ ...slots,
78
+ },
79
+ attrs,
80
+ global: {
81
+ stubs: {
82
+ VDropdown: createVDropdownStub(stubOptions),
83
+ },
84
+ ...global,
85
+ },
86
+ ...wrapperOptions,
87
+ });
88
+ };
89
+
54
90
  describe('PDropdown.vue', () => {
55
91
  it('renders correctly', async () => {
56
- const wrapper = createWrapperFor(PDropdown, {
92
+ const wrapper = createWrapper({
57
93
  props: {
58
- enableArrowNavigation: false,
59
- enableCloseOnEsc: false,
60
94
  triggerStyle: {
61
95
  width: '200px',
62
96
  display: 'flex',
63
97
  },
64
98
  },
65
- slots: {
66
- default: `<button>Open popper</button>`,
67
- popper: `<div>Dropdown content</div>`,
68
- },
69
- global: {
70
- stubs: {
71
- VDropdown: createVDropdownStub(),
72
- },
73
- },
74
99
  });
75
100
 
76
101
  const popper = wrapper.find('.popper');
@@ -82,21 +107,7 @@ describe('PDropdown.vue', () => {
82
107
  });
83
108
 
84
109
  it(`sets the correct defaults`, async () => {
85
- const wrapper = createWrapperFor(PDropdown, {
86
- props: {
87
- enableArrowNavigation: false,
88
- enableCloseOnEsc: false,
89
- },
90
- slots: {
91
- default: `<button>Open popper</button>`,
92
- popper: `<div>Dropdown content</div>`,
93
- },
94
- global: {
95
- stubs: {
96
- VDropdown: createVDropdownStub(),
97
- },
98
- },
99
- });
110
+ const wrapper = createWrapper();
100
111
 
101
112
  const res = {
102
113
  triggers: 'click',
@@ -115,19 +126,9 @@ describe('PDropdown.vue', () => {
115
126
  it('initializes the keyboard navigation svc', async () => {
116
127
  setupListKeyboardNavigation.mockImplementation(() => createMockedKbdNavigationSvc());
117
128
 
118
- createWrapperFor(PDropdown, {
129
+ createWrapper({
119
130
  props: {
120
131
  enableArrowNavigation: true,
121
- enableCloseOnEsc: false,
122
- },
123
- slots: {
124
- default: `<button>Open popper</button>`,
125
- popper: `<div>Dropdown content</div>`,
126
- },
127
- global: {
128
- stubs: {
129
- VDropdown: createVDropdownStub(),
130
- },
131
132
  },
132
133
  });
133
134
 
@@ -137,24 +138,220 @@ describe('PDropdown.vue', () => {
137
138
  it('destroys the keyboard navigation svc', async () => {
138
139
  setupListKeyboardNavigation.mockImplementation(() => createMockedKbdNavigationSvc());
139
140
 
140
- const wrapper = createWrapperFor(PDropdown, {
141
+ const wrapper = createWrapper({
141
142
  props: {
142
143
  enableArrowNavigation: true,
143
- enableCloseOnEsc: false,
144
144
  },
145
+ });
146
+
147
+ wrapper.unmount();
148
+
149
+ expect(destroyFn).toHaveBeenCalled();
150
+ });
151
+
152
+ it.each([
153
+ [true, 'enables'],
154
+ [false, 'disables'],
155
+ ])('%s arrow navigation when enableArrowNavigation is %s', async (enableArrowNavigation, description) => {
156
+ setupListKeyboardNavigation.mockImplementation(() => createMockedKbdNavigationSvc());
157
+
158
+ createWrapper({
159
+ props: {
160
+ enableArrowNavigation,
161
+ },
162
+ });
163
+
164
+ if (enableArrowNavigation) {
165
+ expect(setupListKeyboardNavigation).toHaveBeenCalledTimes(1);
166
+ } else {
167
+ expect(setupListKeyboardNavigation).not.toHaveBeenCalled();
168
+ }
169
+ });
170
+
171
+ it('handles escape key when enableCloseOnEsc is true', async () => {
172
+ const mockHide = vi.fn();
173
+
174
+ const wrapper = createWrapper({
175
+ props: {
176
+ enableCloseOnEsc: true,
177
+ },
178
+ stubOptions: {
179
+ popperMethods: { hide: mockHide },
180
+ },
181
+ });
182
+
183
+ // Simulate escape key press
184
+ const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
185
+ document.dispatchEvent(escapeEvent);
186
+
187
+ expect(mockHide).toHaveBeenCalled();
188
+
189
+ wrapper.unmount();
190
+ });
191
+
192
+ it('does not handle escape key when enableCloseOnEsc is false', async () => {
193
+ const mockHide = vi.fn();
194
+
195
+ const wrapper = createWrapper({
196
+ stubOptions: {
197
+ popperMethods: { hide: mockHide },
198
+ },
199
+ });
200
+
201
+ // Simulate escape key press
202
+ const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
203
+ document.dispatchEvent(escapeEvent);
204
+
205
+ expect(mockHide).not.toHaveBeenCalled();
206
+
207
+ wrapper.unmount();
208
+ });
209
+
210
+ it('applies custom trigger styles', async () => {
211
+ const customStyle = {
212
+ width: '300px',
213
+ height: '50px',
214
+ backgroundColor: 'red',
215
+ };
216
+
217
+ const wrapper = createWrapper({
218
+ props: {
219
+ triggerStyle: customStyle,
220
+ },
221
+ slots: {
222
+ default: '<button>Custom styled trigger</button>',
223
+ },
224
+ });
225
+
226
+ const popper = wrapper.find('.popper');
227
+
228
+ expect(popper.attributes().style).toBe('width: 300px; height: 50px; background-color: red;');
229
+
230
+ wrapper.unmount();
231
+ });
232
+
233
+ it('uses default trigger style when not provided', async () => {
234
+ const wrapper = createWrapper({
145
235
  slots: {
146
- default: `<button>Open popper</button>`,
147
- popper: `<div>Dropdown content</div>`,
236
+ default: '<button>Default styled trigger</button>',
237
+ },
238
+ });
239
+
240
+ const popper = wrapper.find('.popper');
241
+
242
+ expect(popper.attributes().style).toBe('display: inline-block;');
243
+
244
+ wrapper.unmount();
245
+ });
246
+
247
+ it('reinitializes keyboard navigation on subsequent shows', async () => {
248
+ setupListKeyboardNavigation.mockImplementation(() => createMockedKbdNavigationSvc());
249
+
250
+ const createMultiShowStub = () => ({
251
+ template: `
252
+ <div ref="vPopper">
253
+ <Popper ref="popper">
254
+ <slot />
255
+ </Popper>
256
+ <PopperContent ref="popperContent">
257
+ <slot name="popper" />
258
+ </PopperContent>
259
+ </div>
260
+ `,
261
+ components: {
262
+ Popper: { template: '<div class="popper"><slot /></div>' },
263
+ PopperContent: { template: '<div class="popper-content"><slot /></div>' },
264
+ },
265
+ mounted() {
266
+ // Simulate multiple show events
267
+ this.$emit('show');
268
+ this.$emit('show');
269
+ },
270
+ beforeUnmount() {
271
+ this.$emit('hide');
272
+ },
273
+ });
274
+
275
+ createWrapper({
276
+ props: {
277
+ enableArrowNavigation: true,
148
278
  },
149
279
  global: {
150
280
  stubs: {
151
- VDropdown: createVDropdownStub(),
281
+ VDropdown: createMultiShowStub(),
152
282
  },
153
283
  },
154
284
  });
155
285
 
286
+ // First call creates the service, second call calls init
287
+ expect(setupListKeyboardNavigation).toHaveBeenCalledTimes(1);
288
+ expect(initFn).toHaveBeenCalledTimes(1);
289
+ });
290
+
291
+ it('updates reference element correctly', async () => {
292
+ const mockDetachPopperNode = vi.fn();
293
+ const mockEmit = vi.fn();
294
+
295
+ const wrapper = createWrapper({
296
+ stubOptions: {
297
+ popperMethods: {
298
+ $_detachPopperNode: mockDetachPopperNode,
299
+ $emit: mockEmit,
300
+ },
301
+ },
302
+ });
303
+
304
+ const newReference = document.createElement('button');
305
+ newReference.textContent = 'New reference';
306
+
307
+ // Should not throw error when updating reference
308
+ await expect(wrapper.vm.updateReference(newReference)).resolves.toBeUndefined();
309
+
310
+ expect(mockDetachPopperNode).toHaveBeenCalled();
311
+
156
312
  wrapper.unmount();
313
+ });
157
314
 
158
- expect(destroyFn).toHaveBeenCalled();
315
+ it('throws error when updateReference is called with invalid reference', async () => {
316
+ const wrapper = createWrapper();
317
+
318
+ // Should throw error when reference is null/undefined
319
+ await expect(wrapper.vm.updateReference(null)).rejects.toThrow('Reference element is required');
320
+ await expect(wrapper.vm.updateReference(undefined)).rejects.toThrow('Reference element is required');
321
+
322
+ wrapper.unmount();
323
+ });
324
+
325
+ it('forwards all attributes to VDropdown component', async () => {
326
+ const wrapper = createWrapper({
327
+ attrs: {
328
+ 'data-testid': 'custom-dropdown',
329
+ 'aria-label': 'Custom dropdown menu',
330
+ 'custom-prop': 'custom-value',
331
+ },
332
+ });
333
+
334
+ // Check that custom attributes are forwarded along with defaults
335
+ expect(wrapper.attributes()['data-testid']).toBe('custom-dropdown');
336
+ expect(wrapper.attributes()['aria-label']).toBe('Custom dropdown menu');
337
+ expect(wrapper.attributes()['custom-prop']).toBe('custom-value');
338
+
339
+ wrapper.unmount();
340
+ });
341
+
342
+ it('properly handles slot forwarding to VDropdown', async () => {
343
+ const wrapper = createWrapper({
344
+ slots: {
345
+ default: '<button class="trigger-button">Trigger Content</button>',
346
+ popper: '<div class="popper-content-test">Popper Content</div>',
347
+ },
348
+ });
349
+
350
+ expect(wrapper.find('.trigger-button').exists()).toBe(true);
351
+ expect(wrapper.find('.trigger-button').text()).toBe('Trigger Content');
352
+ expect(wrapper.find('.popper-content-test').exists()).toBe(true);
353
+ expect(wrapper.find('.popper-content-test').text()).toBe('Popper Content');
354
+
355
+ wrapper.unmount();
159
356
  });
160
357
  });
@@ -4,7 +4,7 @@
4
4
  :class="[{ hidden: $attrs.hidden }, $attrs.class]"
5
5
  :data-has-error="!!errorMsg"
6
6
  :style="style"
7
- aria-label="Dropdown select"
7
+ :aria-label="$t('squirrel.dropdown_select_aria_label')"
8
8
  >
9
9
  <label v-if="label" :class="labelClasses">{{ label }}</label>
10
10
  <PDropdown
@@ -48,7 +48,7 @@
48
48
  <button
49
49
  type="button"
50
50
  class="flex items-center justify-center text-p-gray-40 hover:text-p-gray-60"
51
- aria-label="Remove item"
51
+ :aria-label="$t('squirrel.dropdown_select_remove_item')"
52
52
  data-test="pill-remove"
53
53
  @click.stop="select($event, item[itemValue])"
54
54
  >
@@ -60,8 +60,8 @@
60
60
  {{
61
61
  multiple
62
62
  ? selectedItems.length === computedItems.length
63
- ? 'All options selected'
64
- : `${selectedItems.length} option${selectedItems.length > 1 ? 's' : ''} selected`
63
+ ? $t('squirrel.dropdown_select_all_options_selected')
64
+ : `${selectedItems.length} ${$t('squirrel.dropdown_select_options', selectedItems.length)} ${$t('squirrel.dropdown_select_selected')}`
65
65
  : selectedItems[0][itemText]
66
66
  }}
67
67
  </div>
@@ -71,7 +71,7 @@
71
71
  v-if="clearable && selectedItems.length"
72
72
  class="absolute right-9 flex h-6 items-center justify-center text-base text-p-gray-40 hover:text-p-gray-60"
73
73
  :class="[CLEAR_BUTTON_SPACING[size]]"
74
- aria-label="Clear selection"
74
+ :aria-label="$t('squirrel.dropdown_select_clear_selection')"
75
75
  @click.stop="clearAll"
76
76
  >
77
77
  <PIcon icon="fe:close" />
@@ -86,7 +86,9 @@
86
86
  ref="actionsContainer"
87
87
  class="flex flex-row justify-between text-xs font-semibold text-primary"
88
88
  >
89
- <p class="text-p-purple-60">{{ computedItems.length }} items</p>
89
+ <p class="text-p-purple-60">
90
+ {{ $t('squirrel.dropdown_select_items', computedItems.length) }}
91
+ </p>
90
92
  <div class="flex flex-row">
91
93
  <a
92
94
  v-if="computedItems.length === internalItems.length"
@@ -95,21 +97,21 @@
95
97
  ]"
96
98
  @click="selectAll"
97
99
  >
98
- Select all
100
+ {{ $t('squirrel.dropdown_select_select_all') }}
99
101
  </a>
100
102
  <a
101
103
  v-else
102
104
  :class="[computedInsideSelected ? 'pointer-events-none opacity-50' : 'cursor-pointer']"
103
105
  @click="selectAll"
104
106
  >
105
- Select all filtered
107
+ {{ $t('squirrel.dropdown_select_select_all_filtered') }}
106
108
  </a>
107
109
  <span class="px-1 leading-none">.</span>
108
110
  <a
109
111
  :class="[selectedItems.length ? 'cursor-pointer' : 'pointer-events-none opacity-50']"
110
112
  @click="clearAll"
111
113
  >
112
- Clear all
114
+ {{ $t('squirrel.dropdown_select_clear_all') }}
113
115
  </a>
114
116
  </div>
115
117
  </div>
@@ -184,11 +186,13 @@
184
186
  @click="handleCreate"
185
187
  >
186
188
  <PIcon icon="fe:plus-circle" />
187
- Add '{{ search }}'
189
+ {{ $t('squirrel.dropdown_select_add') }} '{{ search }}'
188
190
  </button>
189
191
  </template>
190
- <template v-else-if="creatable">No items found. Type to add</template>
191
- <template v-else>No items found</template>
192
+ <template v-else-if="creatable">
193
+ {{ $t('squirrel.dropdown_select_no_items_found_type_to_add') }}
194
+ </template>
195
+ <template v-else>{{ $t('squirrel.dropdown_select_no_items_found') }}</template>
192
196
  </div>
193
197
  </slot>
194
198
  </div>
@@ -1,7 +1,6 @@
1
1
  import PFileUpload from '@squirrel/components/p-file-upload/p-file-upload.vue';
2
2
  import { createWrapperFor } from '@tests/vitest.helpers';
3
3
  import { expect } from 'storybook/test';
4
- import { vi } from 'vitest';
5
4
 
6
5
  // Mocking the toastification library
7
6
  const toastError = vi.fn();
@@ -10,7 +10,7 @@
10
10
  errorMsg ? 'border-on-error' : 'border-p-gray-40 hover:border-primary',
11
11
  { 'pointer-events-none opacity-50': disabled },
12
12
  ]"
13
- aria-label="dropzone"
13
+ :aria-label="$t('squirrel.file_upload_dropzone')"
14
14
  @dragover.prevent.stop="isDraggingOver = true"
15
15
  @dragleave.prevent.stop="isDraggingOver = false"
16
16
  @drop.prevent.stop="onDrop"
@@ -20,12 +20,16 @@
20
20
  <div v-if="!isDraggingOver" class="flex flex-col items-center">
21
21
  <PIcon icon="upload" width="32" class="text-p-gray-50" />
22
22
  <div class="mt-2 text-p-gray-50">
23
- Drag or <span class="text-p-blue-60">select {{ fileWord }}</span>
23
+ <I18nT keypath="squirrel.file_upload_drag_or_select" scope="global">
24
+ <template #select>
25
+ <span class="text-p-blue-60">{{ $t('squirrel.file_upload_select', { fileWord }) }}</span>
26
+ </template>
27
+ </I18nT>
24
28
  </div>
25
29
  </div>
26
30
  <div v-else class="flex flex-col items-center">
27
31
  <PIcon icon="tdesign:drag-drop" width="32" class="text-primary" />
28
- <div class="mt-2 text-p-purple-60">Drop {{ fileWord }}</div>
32
+ <div class="mt-2 text-p-purple-60">{{ $t('squirrel.file_upload_drop', { fileWord }) }}</div>
29
33
  </div>
30
34
  <input
31
35
  ref="fileInputRef"
@@ -61,8 +65,15 @@
61
65
  <div v-show="errorMsg" :class="errorMsgClasses">{{ errorMsg }}</div>
62
66
  <!-- Info -->
63
67
  <div class="mt-1 text-xs text-p-gray-40">
64
- {{ multiple ? `Max ${maxNumberOfFiles}` : 'One' }} {{ acceptFileTypes }} {{ multiple ? 'files' : 'file' }} with
65
- size less than {{ formatBytes(maxSizeInBytes) }} {{ multiple ? 'each' : '' }}
68
+ {{ multiple ? $t('squirrel.file_upload_max', maxNumberOfFiles) : $t('squirrel.file_upload_one') }}
69
+ {{ acceptFileTypes }}
70
+ {{ $t('squirrel.file_upload_files', multiple ? 2 : 1) }}
71
+ {{
72
+ $t('squirrel.file_upload_with_size_less_than', {
73
+ count: multiple ? 2 : 1,
74
+ maxSize: formatBytes(maxSizeInBytes),
75
+ })
76
+ }}
66
77
  </div>
67
78
  </div>
68
79
  </template>
@@ -74,6 +85,7 @@ import PIcon from '@squirrel/components/p-icon/p-icon.vue';
74
85
  import { useInputClasses } from '@squirrel/composables/useInputClasses';
75
86
  import { uniq } from 'lodash-es';
76
87
  import { computed, onMounted, type PropType, ref, shallowRef } from 'vue';
88
+ import { useI18n } from 'vue-i18n';
77
89
  import { useToast } from 'vue-toastification';
78
90
 
79
91
  /**
@@ -185,10 +197,11 @@ const emit = defineEmits<{
185
197
  const fileInputRef = shallowRef<HTMLInputElement>();
186
198
  const isDraggingOver = ref(false);
187
199
  const toast = useToast();
200
+ const { t } = useI18n({ useScope: 'global' });
188
201
  const { labelClasses, errorMsgClasses } = useInputClasses(props);
189
202
 
190
203
  // Computed
191
- const fileWord = computed(() => (props.multiple ? 'files' : 'file'));
204
+ const fileWord = computed(() => t('squirrel.file_upload_files', props.multiple ? 2 : 1));
192
205
 
193
206
  const files = computed<FileUploadFile[]>({
194
207
  get() {
@@ -223,7 +236,9 @@ const validateFiles = (filesToUpload: FileUploadFile[]) => {
223
236
  if (res.length + files.value.length >= props.maxNumberOfFiles) {
224
237
  // Show a toast - except for the contradictory config (multiple: true with maxNumberOfFiles: 1)
225
238
  if (!(props.multiple && props.maxNumberOfFiles === 1)) {
226
- toast.error(`You can only upload a maximum of ${props.maxNumberOfFiles} ${fileWord.value}.`);
239
+ toast.error(
240
+ t('squirrel.file_upload_max_files_exceeded', { count: props.maxNumberOfFiles, fileWord: fileWord.value })
241
+ );
227
242
  }
228
243
 
229
244
  break;
@@ -248,14 +263,16 @@ const validateFiles = (filesToUpload: FileUploadFile[]) => {
248
263
  const isValidExtension = extension ? props.fileTypes.includes(extension) : false;
249
264
 
250
265
  if (!isValidExtension) {
251
- toast.error(`${extension} files are not allowed.`);
266
+ toast.error(t('squirrel.file_upload_files_not_allowed', { extension }));
252
267
  continue;
253
268
  }
254
269
  }
255
270
 
256
271
  // Check filesize
257
272
  if ((file.size || 0) > props.maxSizeInBytes) {
258
- toast.error(`File size of ${fileName} exceeds ${formatBytes(props.maxSizeInBytes)}.`);
273
+ toast.error(
274
+ t('squirrel.file_upload_file_size_exceeded', { fileName, maxSize: formatBytes(props.maxSizeInBytes) })
275
+ );
259
276
  continue;
260
277
  }
261
278
 
@@ -14,6 +14,7 @@
14
14
  import { useInputClasses } from '@squirrel/composables/useInputClasses';
15
15
  import VueDatePicker, { type VueDatePickerProps } from '@vuepic/vue-datepicker';
16
16
  import { computed, type StyleValue, useAttrs } from 'vue';
17
+ import { useI18n } from 'vue-i18n';
17
18
 
18
19
  /**
19
20
  * An inline date picker component that displays a calendar for date selection.
@@ -73,6 +74,7 @@ const props = withDefaults(defineProps<Props>(), {
73
74
  const model = defineModel<Date | string | null>({ default: '' });
74
75
 
75
76
  // Data
77
+ const { locale } = useI18n();
76
78
  const attrs = useAttrs();
77
79
  const { labelClasses, errorMsgClasses } = useInputClasses(props);
78
80
 
@@ -81,7 +83,7 @@ const datePickerProps = computed<VueDatePickerProps>(() => {
81
83
  const { modelValue: _, ...propsWithoutModelValue } = props;
82
84
  const { class: classes, style, ...attrsWithoutClassAndStyle } = attrs;
83
85
 
84
- return { ...propsWithoutModelValue, ...attrsWithoutClassAndStyle };
86
+ return { ...propsWithoutModelValue, ...attrsWithoutClassAndStyle, locale: locale.value };
85
87
  });
86
88
 
87
89
  const style = computed(() => {
@@ -16,14 +16,14 @@
16
16
  <template #suffix>
17
17
  <i
18
18
  v-if="query && showEnterIcon && showEnterIconOnFocus"
19
- v-tooltip.bottom="{ content: 'Press enter to search', delay: { show: 100, hide: 0 } }"
19
+ v-tooltip.bottom="{ content: $t('squirrel.input_search_press_enter_to_search'), delay: { show: 100, hide: 0 } }"
20
20
  class="enter absolute bg-no-repeat outline-none"
21
21
  :class="enterIconClasses[size]"
22
22
  ></i>
23
23
  <button
24
24
  v-if="query"
25
25
  role="button"
26
- aria-label="Clear search input"
26
+ :aria-label="$t('squirrel.input_search_clear_search_input')"
27
27
  class="absolute cursor-pointer"
28
28
  :class="clearIconClasses[size]"
29
29
  @click="clearSearch"
@@ -45,7 +45,7 @@
45
45
  <PCloseBtn
46
46
  :disabled="disabled"
47
47
  :class="{ invisible: !enableClose }"
48
- :aria-label="closeLabel"
48
+ :aria-label="closeLabel || $t('squirrel.close')"
49
49
  @click.prevent="close"
50
50
  />
51
51
  </div>
@@ -3,7 +3,7 @@
3
3
  <div v-if="!loading && pages.length > 1" class="flex text-p-gray-40">
4
4
  <div
5
5
  :class="[BTN_CLASS, Number(modelValue) <= 1 ? ARROW_INACTIVE_CLASS : ARROW_ACTIVE_CLASS]"
6
- aria-label="go to the previous page"
6
+ :aria-label="$t('squirrel.pagination_go_to_previous_page')"
7
7
  @click="setPage(Number(modelValue) - 1)"
8
8
  >
9
9
  <PIcon icon="chevron-left" width="24px" />
@@ -13,7 +13,7 @@
13
13
  <div
14
14
  v-if="page !== DOTS"
15
15
  :class="[BTN_CLASS, page === modelValue ? BTN_ACTIVE_CLASS : BTN_INACTIVE_CLASS]"
16
- :aria-label="`go to page ${page}`"
16
+ :aria-label="$t('squirrel.pagination_go_to_page', { page })"
17
17
  >
18
18
  {{ page }}
19
19
  </div>
@@ -22,7 +22,7 @@
22
22
  </div>
23
23
  <div
24
24
  :class="[BTN_CLASS, modelValue === pageCount ? ARROW_INACTIVE_CLASS : ARROW_ACTIVE_CLASS]"
25
- aria-label="go to the next page"
25
+ :aria-label="$t('squirrel.pagination_go_to_next_page')"
26
26
  @click="setPage(Number(modelValue) + 1)"
27
27
  >
28
28
  <PIcon icon="chevron-right" width="24px" />
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <PSkeletonLoader v-if="loading" class="h-6 w-56" />
3
3
  <div v-else class="text-sm font-medium text-p-gray-40">
4
- <template v-if="count">Showing {{ from }} to {{ to }} of {{ count }} results</template>
5
- <slot v-else name="no-results">No results found</slot>
4
+ <template v-if="count">{{ $t('squirrel.pagination_info_showing_results', { from, to, count }) }}</template>
5
+ <slot v-else name="no-results">{{ $t('squirrel.pagination_info_no_results_found') }}</slot>
6
6
  </div>
7
7
  </template>
8
8