@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
@@ -2,20 +2,22 @@ import PProgressBar from '@squirrel/components/p-progress-bar/p-progress-bar.vue
2
2
  import { createWrapperFor } from '@tests/vitest.helpers';
3
3
 
4
4
  const items = [
5
- { vaLue: 50, label: 'bar1', color: 'rgb(204, 204, 204)' },
6
- { vaLue: 50, label: 'bar2', color: 'rgb(221, 221, 221)' },
5
+ { value: 50, label: 'bar1', color: 'rgb(204, 204, 204)' },
6
+ { value: 30, label: 'bar2', color: 'rgb(221, 221, 221)' },
7
7
  ];
8
8
 
9
9
  describe('PProgressBar.vue', () => {
10
10
  it('renders correctly', async () => {
11
- const wrapper = createWrapperFor(PProgressBar, { props: { total: 2, items } });
11
+ const wrapper = createWrapperFor(PProgressBar, { props: { total: 100, items } });
12
12
 
13
13
  const div = wrapper.find('div[role="progressbar"]');
14
14
 
15
15
  const bars = div.findAll('div.h-full');
16
16
 
17
17
  bars.forEach((bar, i) => {
18
- expect(bar.attributes('style')).toEqual(`background: ${items[i].color};`);
18
+ const expectedWidth = `${(items[i].value / 100) * 100}%`;
19
+ expect(bar.attributes('style')).toContain(`width: ${expectedWidth}`);
20
+ expect(bar.attributes('style')).toContain(`background: ${items[i].color}`);
19
21
  });
20
22
 
21
23
  expect(div.classes()).toEqual(['flex', 'justify-start', 'overflow-hidden', 'rounded', 'bg-p-blue-20']);
@@ -24,7 +26,7 @@ describe('PProgressBar.vue', () => {
24
26
  it('attrs fall through', async () => {
25
27
  const ParentComponent = {
26
28
  template: `
27
- <PProgressBar :total="2" :items="items" class="test-class" data-testattr="test attribute" />
29
+ <PProgressBar :total="100" :items="items" class="test-class" data-testattr="test attribute" />
28
30
  `,
29
31
  data() {
30
32
  return {
@@ -283,4 +283,108 @@ describe('PSelectBtn.vue', () => {
283
283
 
284
284
  expect(true).toBe(true);
285
285
  });
286
+
287
+ describe('Multiple selection', () => {
288
+ it('allows multiple items to be selected', async () => {
289
+ const wrapper = createWrapperFor(PSelectBtn, {
290
+ props: {
291
+ items,
292
+ itemValue: 'valueCustom',
293
+ itemText: 'textCustom',
294
+ modelValue: [items[0], items[1]], // Select first two items
295
+ multiple: true,
296
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
297
+ },
298
+ });
299
+
300
+ // Check that first two buttons are selected
301
+ const buttons = await wrapper.findAll('button');
302
+ expect(buttons[0].attributes()['aria-selected']).toBe('true');
303
+ expect(buttons[1].attributes()['aria-selected']).toBe('true');
304
+ expect(buttons[2].attributes()['aria-selected']).toBe('false');
305
+ });
306
+
307
+ it('adds items to selection when clicked in multiple mode', async () => {
308
+ const wrapper = createWrapperFor(PSelectBtn, {
309
+ props: {
310
+ items,
311
+ itemValue: 'valueCustom',
312
+ itemText: 'textCustom',
313
+ modelValue: [items[0]], // Start with first item selected
314
+ multiple: true,
315
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
316
+ },
317
+ });
318
+
319
+ // Click second item to add it to selection
320
+ await wrapper.findByText('Option 2', 'button').trigger('click');
321
+
322
+ const newModelValue = wrapper.props('modelValue');
323
+ expect(Array.isArray(newModelValue)).toBe(true);
324
+ expect(newModelValue).toHaveLength(2);
325
+ expect(newModelValue[0].valueCustom).toBe(1);
326
+ expect(newModelValue[1].valueCustom).toBe(2);
327
+ });
328
+
329
+ it('removes items from selection when clicked again in multiple mode', async () => {
330
+ const wrapper = createWrapperFor(PSelectBtn, {
331
+ props: {
332
+ items,
333
+ itemValue: 'valueCustom',
334
+ itemText: 'textCustom',
335
+ modelValue: [items[0], items[1]], // Start with first two selected
336
+ multiple: true,
337
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
338
+ },
339
+ });
340
+
341
+ // Click first item to remove it from selection
342
+ await wrapper.findByText('Option 1', 'button').trigger('click');
343
+
344
+ const newModelValue = wrapper.props('modelValue');
345
+ expect(Array.isArray(newModelValue)).toBe(true);
346
+ expect(newModelValue).toHaveLength(1);
347
+ expect(newModelValue[0].valueCustom).toBe(2);
348
+ });
349
+
350
+ it('creates new array when starting with non-array value in multiple mode', async () => {
351
+ const wrapper = createWrapperFor(PSelectBtn, {
352
+ props: {
353
+ items,
354
+ itemValue: 'valueCustom',
355
+ itemText: 'textCustom',
356
+ modelValue: null, // Start with null
357
+ multiple: true,
358
+ 'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
359
+ },
360
+ });
361
+
362
+ // Click first item
363
+ await wrapper.findByText('Option 1', 'button').trigger('click');
364
+
365
+ const newModelValue = wrapper.props('modelValue');
366
+ expect(Array.isArray(newModelValue)).toBe(true);
367
+ expect(newModelValue).toHaveLength(1);
368
+ expect(newModelValue[0].valueCustom).toBe(1);
369
+ });
370
+
371
+ it('handles isSelected correctly with non-array modelValue in multiple mode', async () => {
372
+ const wrapper = createWrapperFor(PSelectBtn, {
373
+ props: {
374
+ items,
375
+ itemValue: 'valueCustom',
376
+ itemText: 'textCustom',
377
+ modelValue: null, // Non-array value
378
+ multiple: true,
379
+ },
380
+ });
381
+
382
+ const buttons = await wrapper.findAll('button');
383
+
384
+ // All buttons should be unselected when modelValue is not an array
385
+ buttons.forEach((button) => {
386
+ expect(button.attributes()['aria-selected']).toBe('false');
387
+ });
388
+ });
389
+ });
286
390
  });
@@ -19,7 +19,7 @@
19
19
  ref="actionsContainer"
20
20
  class="flex flex-row justify-between text-xs font-semibold text-primary"
21
21
  >
22
- <p class="text-p-purple-60">{{ computedItems.length }} items</p>
22
+ <p class="text-p-purple-60">{{ $t('squirrel.select_list_items', computedItems.length) }}</p>
23
23
  <div class="flex flex-row">
24
24
  <a
25
25
  v-if="computedItems.length === internalItems.length"
@@ -28,18 +28,18 @@
28
28
  ]"
29
29
  @click="selectAll"
30
30
  >
31
- Select all
31
+ {{ $t('squirrel.select_list_select_all') }}
32
32
  </a>
33
33
  <a
34
34
  v-else
35
35
  :class="[computedInsideSelected ? 'pointer-events-none opacity-50' : 'cursor-pointer']"
36
36
  @click="selectAll"
37
37
  >
38
- Select all filtered
38
+ {{ $t('squirrel.select_list_select_all_filtered') }}
39
39
  </a>
40
40
  <span class="px-1 leading-none">.</span>
41
41
  <a :class="[selectedItems.length ? 'cursor-pointer' : 'pointer-events-none opacity-50']" @click="clearAll">
42
- Clear all
42
+ {{ $t('squirrel.select_list_clear_all') }}
43
43
  </a>
44
44
  </div>
45
45
  </div>
@@ -110,7 +110,9 @@
110
110
  </div>
111
111
  </div>
112
112
  <slot v-if="!computedItems.length" name="no-items">
113
- <div :class="['flex items-center justify-center', SIZES[size]]">No items found</div>
113
+ <div :class="['flex items-center justify-center', SIZES[size]]">
114
+ {{ $t('squirrel.select_list_no_items_found') }}
115
+ </div>
114
116
  </slot>
115
117
  </div>
116
118
  </div>
@@ -136,4 +136,118 @@ describe('PSelectPill.vue', () => {
136
136
  expect(button.text()).toContain(`subtext for ${items[i].text}`);
137
137
  });
138
138
  });
139
+
140
+ it('handles empty/null items array with default', async () => {
141
+ const wrapper = createWrapperFor(PSelectPill, {
142
+ props: {
143
+ items: null, // Test default items prop
144
+ },
145
+ });
146
+
147
+ const buttons = await wrapper.findAll('button');
148
+ expect(buttons).toHaveLength(0);
149
+ });
150
+
151
+ it('updates pill style when selection changes', async () => {
152
+ const wrapper = createWrapperFor(PSelectPill, {
153
+ props: {
154
+ items: selectItems,
155
+ modelValue: 1,
156
+ },
157
+ });
158
+
159
+ // Spy on the setPillStyle method
160
+ const setPillStyleSpy = vi.spyOn(wrapper.vm, 'setPillStyle');
161
+
162
+ // Trigger a prop change to test setPillStyle
163
+ await wrapper.setProps({ modelValue: 2 });
164
+
165
+ // Wait for the setTimeout delay (60ms + buffer)
166
+ await new Promise((resolve) => setTimeout(resolve, 80));
167
+
168
+ expect(setPillStyleSpy).toHaveBeenCalled();
169
+ });
170
+
171
+ it('handles setPillStyle when no active element exists', async () => {
172
+ const wrapper = createWrapperFor(PSelectPill, {
173
+ props: {
174
+ items: selectItems,
175
+ modelValue: 999, // Non-existent value
176
+ },
177
+ });
178
+
179
+ // Should not throw error when no active element exists
180
+ expect(() => wrapper.vm.setPillStyle()).not.toThrow();
181
+ });
182
+
183
+ it('handles responsive sizing with getOffsetValues fallback', async () => {
184
+ const wrapper = createWrapperFor(PSelectPill, {
185
+ props: {
186
+ items: selectItems,
187
+ modelValue: 1,
188
+ },
189
+ attachTo: document.body, // Ensure proper DOM attachment
190
+ });
191
+
192
+ // Mock offsetWidth to be 0 to trigger getOffsetValues fallback
193
+ const activeButton = wrapper.find('button.text-p-purple-60');
194
+ expect(activeButton.exists()).toBe(true);
195
+
196
+ // Mock the element's offsetWidth to be 0
197
+ Object.defineProperty(activeButton.element, 'offsetWidth', {
198
+ get: () => 0,
199
+ });
200
+ Object.defineProperty(activeButton.element, 'offsetLeft', {
201
+ get: () => 0,
202
+ });
203
+
204
+ // This should trigger the getOffsetValues fallback and not throw
205
+ expect(() => wrapper.vm.setPillStyle()).not.toThrow();
206
+
207
+ wrapper.unmount();
208
+ });
209
+
210
+ it('handles lifecycle events correctly', async () => {
211
+ const wrapper = createWrapperFor(PSelectPill, {
212
+ props: {
213
+ items: selectItems,
214
+ modelValue: 1,
215
+ },
216
+ attachTo: document.body,
217
+ });
218
+
219
+ // Test mounted lifecycle
220
+ expect(wrapper.vm.$refs.pill).toBeDefined();
221
+
222
+ // Test that pill ref exists and style can be set
223
+ if (wrapper.vm.$refs.pill instanceof HTMLElement) {
224
+ wrapper.vm.setPillStyle();
225
+ expect(wrapper.vm.$refs.pill.style.left).toBeDefined();
226
+ expect(wrapper.vm.$refs.pill.style.width).toBeDefined();
227
+ }
228
+
229
+ wrapper.unmount();
230
+ });
231
+
232
+ it('handles window resize events', async () => {
233
+ const wrapper = createWrapperFor(PSelectPill, {
234
+ props: {
235
+ items: selectItems,
236
+ modelValue: 1,
237
+ },
238
+ attachTo: document.body,
239
+ });
240
+
241
+ const setPillStyleSpy = vi.spyOn(wrapper.vm, 'setPillStyle');
242
+
243
+ // Simulate window resize
244
+ window.dispatchEvent(new Event('resize'));
245
+
246
+ // Wait for debounced resize handler
247
+ await new Promise((resolve) => setTimeout(resolve, 100));
248
+
249
+ expect(setPillStyleSpy).toHaveBeenCalled();
250
+
251
+ wrapper.unmount();
252
+ });
139
253
  });
@@ -1,7 +1,7 @@
1
1
  import { usePTableColResize } from '@squirrel/components/p-table/usePTableColResize';
2
2
  import { createApp, nextTick, ref } from 'vue';
3
3
 
4
- const withSetup = (composable) => {
4
+ const withSetup = (composable, returnApp = false) => {
5
5
  let result;
6
6
 
7
7
  const app = createApp({
@@ -9,23 +9,36 @@ const withSetup = (composable) => {
9
9
  result = composable();
10
10
  return () => {};
11
11
  },
12
+ template: '<div></div>',
12
13
  });
13
14
 
14
- app.mount(document.createElement('div'));
15
+ const element = document.createElement('div');
16
+ app.mount(element);
15
17
 
16
- return result;
18
+ return returnApp ? { result, app, element } : { result };
17
19
  };
18
20
 
19
21
  describe('usePTableColResize', () => {
22
+ let mockAddEventListener;
23
+ let mockRemoveEventListener;
24
+
25
+ beforeEach(() => {
26
+ mockAddEventListener = vi.spyOn(document, 'addEventListener');
27
+ mockRemoveEventListener = vi.spyOn(document, 'removeEventListener');
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
20
34
  it('should resize the column when colResize is called', () => {
21
35
  const options = {
22
36
  enabled: ref(true),
23
37
  ths: ref([{ offsetWidth: 100, getBoundingClientRect: () => ({ width: 100 }) }]),
24
38
  };
25
39
 
26
- const { isColResizing, colResizingIndex, colResizeHandleLeft, colResizingWidth, colResize } = withSetup(() =>
27
- usePTableColResize(options)
28
- );
40
+ const { result } = withSetup(() => usePTableColResize(options));
41
+ const { isColResizing, colResizingIndex, colResizeHandleLeft, colResizingWidth, colResize } = result;
29
42
 
30
43
  isColResizing.value = true;
31
44
  colResizingIndex.value = 0;
@@ -42,9 +55,8 @@ describe('usePTableColResize', () => {
42
55
  enabled: ref(true),
43
56
  ths: ref([{ offsetWidth: 100 }, { offsetWidth: 200 }]),
44
57
  };
45
- const { colResizeStart, isColResizing, colResizingWidth, colResizingIndex } = withSetup(() =>
46
- usePTableColResize(options)
47
- );
58
+ const { result } = withSetup(() => usePTableColResize(options));
59
+ const { colResizeStart, isColResizing, colResizingWidth, colResizingIndex } = result;
48
60
 
49
61
  const event = new MouseEvent('mousedown', { detail: 1 });
50
62
  colResizeStart(event, 1);
@@ -59,7 +71,23 @@ describe('usePTableColResize', () => {
59
71
  enabled: ref(true),
60
72
  ths: ref([{ offsetWidth: 100 }]),
61
73
  };
62
- const { colResizeStop, isColResizing } = withSetup(() => usePTableColResize(options));
74
+ const { result } = withSetup(() => usePTableColResize(options));
75
+ const { colResizeStop, isColResizing } = result;
76
+
77
+ colResizeStop();
78
+
79
+ expect(isColResizing.value).toBe(false);
80
+ });
81
+
82
+ it('should do nothing when colResizeStop is called and not resizing', () => {
83
+ const options = {
84
+ enabled: ref(true),
85
+ ths: ref([{ offsetWidth: 100 }]),
86
+ };
87
+ const { result } = withSetup(() => usePTableColResize(options));
88
+ const { colResizeStop, isColResizing } = result;
89
+
90
+ expect(isColResizing.value).toBe(false);
63
91
 
64
92
  colResizeStop();
65
93
 
@@ -77,7 +105,8 @@ describe('usePTableColResize', () => {
77
105
  },
78
106
  ]),
79
107
  };
80
- const { colResizeFitToData, isColResizing, colResizingWidth } = withSetup(() => usePTableColResize(options));
108
+ const { result } = withSetup(() => usePTableColResize(options));
109
+ const { colResizeFitToData, isColResizing, colResizingWidth } = result;
81
110
 
82
111
  colResizeFitToData(0);
83
112
 
@@ -86,4 +115,87 @@ describe('usePTableColResize', () => {
86
115
  expect(isColResizing.value).toBe(false);
87
116
  expect(colResizingWidth.value).toBe(150);
88
117
  });
118
+
119
+ it('should return early when colResizeFitToData cannot find cells', () => {
120
+ const options = {
121
+ enabled: ref(true),
122
+ ths: ref([
123
+ {
124
+ closest: () => null,
125
+ },
126
+ ]),
127
+ };
128
+ const { result } = withSetup(() => usePTableColResize(options));
129
+ const { colResizeFitToData, colResizingWidth } = result;
130
+
131
+ const initialWidth = colResizingWidth.value;
132
+
133
+ colResizeFitToData(0);
134
+
135
+ // Should not change width since no cells were found
136
+ expect(colResizingWidth.value).toBe(initialWidth);
137
+ });
138
+
139
+ it('should manage event listeners based on enabled state', async () => {
140
+ const enabled = ref(true);
141
+ const options = {
142
+ enabled,
143
+ ths: ref([{ offsetWidth: 100 }]),
144
+ };
145
+
146
+ withSetup(() => usePTableColResize(options));
147
+
148
+ // Should add listener on mount when enabled
149
+ expect(mockAddEventListener).toHaveBeenCalledWith('mouseup', expect.any(Function));
150
+
151
+ // Change enabled to false
152
+ enabled.value = false;
153
+ await nextTick();
154
+ expect(mockRemoveEventListener).toHaveBeenCalledWith('mouseup', expect.any(Function));
155
+
156
+ // Change enabled back to true
157
+ enabled.value = true;
158
+ await nextTick();
159
+ expect(mockAddEventListener).toHaveBeenCalledTimes(2);
160
+ });
161
+
162
+ it('should cleanup event listeners on unmount when enabled', () => {
163
+ const options = {
164
+ enabled: ref(true),
165
+ ths: ref([{ offsetWidth: 100 }]),
166
+ };
167
+
168
+ const { app } = withSetup(() => usePTableColResize(options), true);
169
+
170
+ // Trigger unmount
171
+ app.unmount();
172
+
173
+ expect(mockRemoveEventListener).toHaveBeenCalledWith('mouseup', expect.any(Function));
174
+ });
175
+
176
+ it('should not manage event listeners when disabled on mount', () => {
177
+ const options = {
178
+ enabled: ref(false),
179
+ ths: ref([{ offsetWidth: 100 }]),
180
+ };
181
+
182
+ withSetup(() => usePTableColResize(options));
183
+
184
+ // Should not add listener when disabled
185
+ expect(mockAddEventListener).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it('should not start resizing on double click', () => {
189
+ const options = {
190
+ enabled: ref(true),
191
+ ths: ref([{ offsetWidth: 100 }]),
192
+ };
193
+ const { result } = withSetup(() => usePTableColResize(options));
194
+ const { colResizeStart, isColResizing } = result;
195
+
196
+ const event = new MouseEvent('mousedown', { detail: 2 }); // Double click
197
+ colResizeStart(event, 0);
198
+
199
+ expect(isColResizing.value).toBe(false);
200
+ });
89
201
  });
@@ -6,7 +6,7 @@ import { defineComponent, useTemplateRef } from 'vue';
6
6
 
7
7
  // Mock ResizeObserver to capture the composable's callback
8
8
  let composableResizeCallback;
9
- const mockResizeObserver = vi.fn((callback) => {
9
+ const mockResizeObserver = vi.fn().mockImplementation(function (callback) {
10
10
  composableResizeCallback = callback;
11
11
  return {
12
12
  observe: vi.fn(),