@pequity/squirrel 8.4.5 → 8.5.1

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 (94) hide show
  1. package/README.md +31 -2
  2. package/dist/cjs/chunks/index.js +530 -179
  3. package/dist/cjs/chunks/p-alert.js +11 -16
  4. package/dist/cjs/chunks/p-btn.js +1 -1
  5. package/dist/cjs/chunks/p-input-percent.js +2 -2
  6. package/dist/cjs/chunks/p-table-header-cell.js +57 -0
  7. package/dist/cjs/index.js +41 -33
  8. package/dist/cjs/inputClasses.js +3 -3
  9. package/dist/cjs/p-icon.js +2 -1
  10. package/dist/cjs/p-loading.js +2 -2
  11. package/dist/cjs/p-modal.js +45 -43
  12. package/dist/cjs/p-table-header-cell.js +2 -116
  13. package/dist/cjs/p-table.js +2 -0
  14. package/dist/cjs/usePTableHeaderWrap.js +38 -0
  15. package/dist/es/chunks/index.js +530 -179
  16. package/dist/es/chunks/p-alert.js +11 -16
  17. package/dist/es/chunks/p-btn.js +2 -2
  18. package/dist/es/chunks/p-input-percent.js +2 -2
  19. package/dist/es/chunks/p-table-header-cell.js +58 -0
  20. package/dist/es/index.js +49 -41
  21. package/dist/es/inputClasses.js +4 -4
  22. package/dist/es/p-icon.js +2 -1
  23. package/dist/es/p-loading.js +2 -2
  24. package/dist/es/p-modal.js +45 -43
  25. package/dist/es/p-table-header-cell.js +2 -116
  26. package/dist/es/p-table.js +2 -0
  27. package/dist/es/usePTableHeaderWrap.js +38 -0
  28. package/dist/squirrel/components/index.d.ts +1 -2
  29. package/dist/squirrel/components/p-action-bar/p-action-bar.vue.d.ts +1 -1
  30. package/dist/squirrel/components/p-alert/p-alert.vue.d.ts +2 -2
  31. package/dist/squirrel/components/p-avatar/p-avatar.vue.d.ts +1 -1
  32. package/dist/squirrel/components/p-btn/p-btn.vue.d.ts +3 -3
  33. package/dist/squirrel/components/p-card/p-card.vue.d.ts +1 -1
  34. package/dist/squirrel/components/p-checkbox/p-checkbox.vue.d.ts +1 -1
  35. package/dist/squirrel/components/p-close-btn/p-close-btn.vue.d.ts +1 -1
  36. package/dist/squirrel/components/p-date-picker/p-date-picker.vue.d.ts +1 -1
  37. package/dist/squirrel/components/p-drawer/p-drawer.vue.d.ts +12 -12
  38. package/dist/squirrel/components/p-dropdown-select/p-dropdown-select.vue.d.ts +1 -1
  39. package/dist/squirrel/components/p-file-upload/p-file-upload.vue.d.ts +1 -1
  40. package/dist/squirrel/components/p-icon/p-icon.types.d.ts +1 -0
  41. package/dist/squirrel/components/p-icon/p-icon.vue.d.ts +1 -1
  42. package/dist/squirrel/components/p-info-icon/p-info-icon.vue.d.ts +1 -1
  43. package/dist/squirrel/components/p-inline-date-picker/p-inline-date-picker.vue.d.ts +1 -1
  44. package/dist/squirrel/components/p-input/p-input.vue.d.ts +1 -1
  45. package/dist/squirrel/components/p-input-percent/p-input-percent.vue.d.ts +1 -1
  46. package/dist/squirrel/components/p-input-search/p-input-search.vue.d.ts +1 -1
  47. package/dist/squirrel/components/p-link/p-link.vue.d.ts +1 -1
  48. package/dist/squirrel/components/p-loading/p-loading.vue.d.ts +1 -1
  49. package/dist/squirrel/components/p-modal/p-modal.vue.d.ts +5 -1
  50. package/dist/squirrel/components/p-pagination/p-pagination.vue.d.ts +1 -1
  51. package/dist/squirrel/components/p-pagination-info/p-pagination-info.vue.d.ts +1 -1
  52. package/dist/squirrel/components/p-progress-bar/p-progress-bar.vue.d.ts +1 -1
  53. package/dist/squirrel/components/p-ring-loader/p-ring-loader.vue.d.ts +1 -1
  54. package/dist/squirrel/components/p-select/p-select.vue.d.ts +1 -1
  55. package/dist/squirrel/components/p-select-btn/p-select-btn.vue.d.ts +1 -1
  56. package/dist/squirrel/components/p-select-list/p-select-list.vue.d.ts +1 -1
  57. package/dist/squirrel/components/p-steps/p-steps.vue.d.ts +1 -1
  58. package/dist/squirrel/components/p-table/p-table.types.d.ts +1 -0
  59. package/dist/squirrel/components/p-table/p-table.vue.d.ts +1 -1
  60. package/dist/squirrel/components/p-table/usePTableHeaderWrap.d.ts +4 -0
  61. package/dist/squirrel/components/p-table-header-cell/p-table-header-cell.vue.d.ts +14 -161
  62. package/dist/squirrel/components/p-table-loader/p-table-loader.vue.d.ts +1 -1
  63. package/dist/squirrel/components/p-table-sort/p-table-sort.vue.d.ts +1 -1
  64. package/dist/squirrel/components/p-table-td/p-table-td.vue.d.ts +1 -1
  65. package/dist/squirrel/components/p-tabs/p-tabs.vue.d.ts +1 -1
  66. package/dist/squirrel/components/p-tabs-pills/p-tabs-pills.vue.d.ts +1 -1
  67. package/dist/squirrel/components/p-textarea/p-textarea.vue.d.ts +1 -1
  68. package/dist/squirrel/components/p-toggle/p-toggle.vue.d.ts +1 -1
  69. package/dist/squirrel.css +22 -33
  70. package/package.json +23 -21
  71. package/squirrel/components/index.ts +0 -2
  72. package/squirrel/components/p-alert/p-alert.spec.js +4 -4
  73. package/squirrel/components/p-alert/p-alert.stories.js +19 -13
  74. package/squirrel/components/p-alert/p-alert.vue +9 -11
  75. package/squirrel/components/p-icon/p-icon.types.ts +1 -0
  76. package/squirrel/components/p-modal/p-modal-basic.spec.js +29 -3
  77. package/squirrel/components/p-modal/p-modal.vue +44 -33
  78. package/squirrel/components/p-table/p-table.spec.js +79 -10
  79. package/squirrel/components/p-table/p-table.types.ts +2 -0
  80. package/squirrel/components/p-table/p-table.vue +12 -5
  81. package/squirrel/components/p-table/usePTableHeaderWrap.spec.js +118 -0
  82. package/squirrel/components/p-table/usePTableHeaderWrap.ts +45 -0
  83. package/squirrel/components/p-table-header-cell/p-table-header-cell.spec.js +17 -9
  84. package/squirrel/components/p-table-header-cell/p-table-header-cell.vue +69 -83
  85. package/dist/cjs/p-table-filter-icon.js +0 -28
  86. package/dist/es/p-table-filter-icon.js +0 -29
  87. package/dist/squirrel/components/p-table-header-cell/p-table-filter-icon.vue.d.ts +0 -20
  88. package/squirrel/assets/filter-icon-active-hover.svg +0 -4
  89. package/squirrel/assets/filter-icon-active.svg +0 -4
  90. package/squirrel/assets/filter-icon-hover.svg +0 -7
  91. package/squirrel/assets/filter-icon.svg +0 -6
  92. package/squirrel/components/p-table-header-cell/p-filter-icon.spec.js +0 -20
  93. package/squirrel/components/p-table-header-cell/p-filter-icon.stories.js +0 -33
  94. package/squirrel/components/p-table-header-cell/p-table-filter-icon.vue +0 -41
@@ -1,5 +1,5 @@
1
1
  import PModal from '@squirrel/components/p-modal/p-modal.vue';
2
- import { waitRAF } from '@tests/vitest.helpers';
2
+ import { waitNT, waitRAF } from '@tests/vitest.helpers';
3
3
  import { mount } from '@vue/test-utils';
4
4
 
5
5
  const createWrapperContainer = (componentArgs) => {
@@ -85,18 +85,44 @@ describe('Modal basic functionality', () => {
85
85
  await wrapper.setData({ showModal: true });
86
86
 
87
87
  expect(wrapper.find('[data-pm-id]').classes()).toEqual(
88
- 'pm relative flex flex-col rounded-2xl pb-6 cursor-default bg-surface shadow-xl'.split(' ')
88
+ 'pm relative flex flex-col rounded-2xl cursor-default bg-surface shadow-xl pb-6'.split(' ')
89
89
  );
90
90
 
91
91
  wrapper.unmount();
92
92
  });
93
93
 
94
+ it('sets the correct base modal class when modal-wrapper slot is used', async () => {
95
+ const wrapper = mount(PModal, {
96
+ attachTo: document.body,
97
+ global: {
98
+ stubs: {
99
+ transition: true,
100
+ teleport: true,
101
+ },
102
+ },
103
+ props: {
104
+ modelValue: true,
105
+ },
106
+ slots: {
107
+ 'modal-wrapper': '<div>Modal content goes here...</div>',
108
+ },
109
+ });
110
+
111
+ await waitNT(wrapper.vm);
112
+
113
+ const modalContent = wrapper.find('[data-pm-id]');
114
+
115
+ expect(modalContent.classes()).not.toContain('pb-6');
116
+
117
+ wrapper.unmount();
118
+ });
119
+
94
120
  it('passes the modalBaseClass prop to the modal', async () => {
95
121
  const wrapper = createWrapperContainer({ modalBaseClass: 'custom-class' });
96
122
 
97
123
  await wrapper.setData({ showModal: true });
98
124
 
99
- expect(wrapper.find('[data-pm-id]').classes()).toEqual(['custom-class']);
125
+ expect(wrapper.find('[data-pm-id]').classes()).toEqual(['custom-class', 'pb-6']);
100
126
 
101
127
  wrapper.unmount();
102
128
  });
@@ -29,40 +29,47 @@
29
29
  @click="overlayClick($event)"
30
30
  @keydown="keydown($event)"
31
31
  >
32
- <div ref="pm" :data-pm-id="id" :class="[modalBaseClass, modalClass]" :style="modalStyle">
33
- <slot name="title-wrapper">
34
- <div class="flex pb-4 pl-8 pr-4 pt-4">
35
- <h3 v-if="title" :id="`${id}-title`" class="mr-auto pt-4 text-xl font-semibold">
36
- {{ title }}
37
- </h3>
38
- <div class="ml-auto">
39
- <PCloseBtn
40
- :disabled="disabled"
41
- :class="{ invisible: !enableClose }"
42
- :aria-label="closeLabel"
43
- @click.prevent="close"
44
- />
32
+ <div
33
+ ref="pm"
34
+ :data-pm-id="id"
35
+ :class="[modalBaseClass, modalClass, { 'pb-6': !$slots['modal-wrapper'] }]"
36
+ :style="modalStyle"
37
+ >
38
+ <slot name="modal-wrapper">
39
+ <slot name="title-wrapper">
40
+ <div class="flex pb-4 pl-8 pr-4 pt-4">
41
+ <h3 v-if="title" :id="`${id}-title`" class="mr-auto pt-4 text-xl font-semibold">
42
+ {{ title }}
43
+ </h3>
44
+ <div class="ml-auto">
45
+ <PCloseBtn
46
+ :disabled="disabled"
47
+ :class="{ invisible: !enableClose }"
48
+ :aria-label="closeLabel"
49
+ @click.prevent="close"
50
+ />
51
+ </div>
45
52
  </div>
53
+ </slot>
54
+ <div v-if="errorMsg" class="mb-4 px-8">
55
+ <PAlert type="error">{{ errorMsg }}</PAlert>
46
56
  </div>
47
- </slot>
48
- <div v-if="errorMsg" class="mb-4 px-8">
49
- <PAlert type="error">{{ errorMsg }}</PAlert>
50
- </div>
51
- <slot name="content-wrapper">
52
- <div
53
- :id="`${id}-content`"
54
- :class="[
55
- 'relative grow overflow-y-auto overflow-x-hidden px-8',
56
- { 'pointer-events-none opacity-50': disabled },
57
- ]"
58
- >
59
- <slot></slot>
60
- </div>
61
- </slot>
62
- <slot name="footer-wrapper">
63
- <div v-if="$slots.footer" class="px-8 pt-6">
64
- <slot name="footer"></slot>
65
- </div>
57
+ <slot name="content-wrapper">
58
+ <div
59
+ :id="`${id}-content`"
60
+ :class="[
61
+ 'relative grow overflow-y-auto overflow-x-hidden px-8',
62
+ { 'pointer-events-none opacity-50': disabled },
63
+ ]"
64
+ >
65
+ <slot></slot>
66
+ </div>
67
+ </slot>
68
+ <slot name="footer-wrapper">
69
+ <div v-if="$slots.footer" class="px-8 pt-6">
70
+ <slot name="footer"></slot>
71
+ </div>
72
+ </slot>
66
73
  </slot>
67
74
  </div>
68
75
  </div>
@@ -106,6 +113,10 @@ defineSlots<{
106
113
  * Default content slot for the modal body.
107
114
  */
108
115
  default?: () => unknown;
116
+ /**
117
+ * Custom modal wrapper content.
118
+ */
119
+ 'modal-wrapper'?: () => unknown;
109
120
  /**
110
121
  * Custom title wrapper content.
111
122
  */
@@ -209,7 +220,7 @@ const props = defineProps({
209
220
  */
210
221
  modalBaseClass: {
211
222
  type: [String, Object, Array] as PropType<StyleValue>,
212
- default: 'pm relative flex flex-col rounded-2xl pb-6 cursor-default bg-surface shadow-xl',
223
+ default: 'pm relative flex flex-col rounded-2xl cursor-default bg-surface shadow-xl',
213
224
  },
214
225
  /**
215
226
  * Additional CSS classes for the modal content.
@@ -302,7 +302,7 @@ describe('PTable.vue', () => {
302
302
  props: { cols },
303
303
  });
304
304
 
305
- const filterIcon = wrapper.find('div.cursor-pointer.filter.active');
305
+ const filterIcon = wrapper.findComponent({ name: 'PIcon' });
306
306
  await filterIcon.trigger('click');
307
307
 
308
308
  expect(wrapper.emitted()['click-filter-icon']).toBeTruthy();
@@ -354,6 +354,57 @@ describe('PTable.vue', () => {
354
354
  expect(wrapper.findAll('tbody div')[2].text()).toBe('true');
355
355
  });
356
356
 
357
+ it('renders correctly with column resizing enabled', async () => {
358
+ const cols = cloneDeep(columns);
359
+ const wrapper = createWrapperFor(PTable, {
360
+ props: { cols, colsResizable: true },
361
+ global: {
362
+ stubs: {
363
+ PTableHeaderCell: { template: `<div class="header-cell-stub">{{ text }}</div>`, props: { text: '' } },
364
+ },
365
+ },
366
+ });
367
+
368
+ // Should have resize handles for middle columns (not first, not last when isLastColFixed)
369
+ const resizeHandles = wrapper.findAll('[data-resize-handle]');
370
+ expect(resizeHandles.length).toBe(cols.length - 1); // All columns except first
371
+
372
+ // Should have extra th for column resizing when not isLastColFixed
373
+ const extraTh = wrapper.find('thead th:last-child');
374
+ expect(extraTh.classes()).toContain('min-w-[80px]');
375
+ expect(extraTh.classes()).toContain('bg-gradient-to-r');
376
+ });
377
+
378
+ it('renders correctly with subheader', async () => {
379
+ const cols = cloneDeep(columns);
380
+ const wrapper = createWrapperFor(PTable, {
381
+ props: { cols, subheader: true, isFirstColFixed: true, isLastColFixed: true },
382
+ slots: {
383
+ 'subheader-cell-first-column': `<div class="subheader-content">Subheader 1</div>`,
384
+ 'subheader-cell-third-column': `<div class="subheader-content">Subheader 3</div>`,
385
+ },
386
+ global: {
387
+ stubs: {
388
+ PTableHeaderCell: { template: `<div class="header-cell-stub">{{ text }}</div>`, props: { text: '' } },
389
+ },
390
+ },
391
+ });
392
+
393
+ // Check subheader divs exist
394
+ const subheaderDivs = wrapper.findAll('.subheader-content');
395
+ expect(subheaderDivs.length).toBe(2);
396
+ expect(subheaderDivs[0].text()).toBe('Subheader 1');
397
+ expect(subheaderDivs[1].text()).toBe('Subheader 3');
398
+
399
+ // Check subheader classes include th-shadow for fixed columns
400
+ cols.forEach((col, i) => {
401
+ const subheaderDiv = wrapper.find(`th[data-col-id="${col.id}"] > div:last-child`);
402
+ if (i === 0 || i === cols.length - 1) {
403
+ expect(subheaderDiv.classes()).toContain('th-shadow');
404
+ }
405
+ });
406
+ });
407
+
357
408
  it('shows additional rows when the virtualizer padding options are set', async () => {
358
409
  const cols = cloneDeep(columns);
359
410
  const wrapper = createWrapperFor(PTable, {
@@ -375,18 +426,36 @@ describe('PTable.vue', () => {
375
426
  expect(wrapper.find('table tbody tr:last-child').classes()).toEqual([]);
376
427
  });
377
428
 
378
- it('sets th refs correctly', async () => {
429
+ it('emits col-resize event when column resizing stops', async () => {
379
430
  const cols = cloneDeep(columns);
380
- const wrapper = createWrapperFor(PTable, { props: { cols } });
431
+ const wrapper = createWrapperFor(PTable, {
432
+ props: {
433
+ cols,
434
+ colsResizable: true,
435
+ data,
436
+ },
437
+ });
381
438
 
382
- const thsRefs = wrapper.vm.ths;
439
+ const resizeHandle = wrapper.find('[data-resize-handle]');
440
+
441
+ // Find the parent th element and mock its dimensions
442
+ const th = resizeHandle.element.closest('th');
443
+ if (th) {
444
+ const mockTds = [{ getBoundingClientRect: () => ({ width: 120 }) }];
445
+ const mockTable = {
446
+ querySelectorAll: vi.fn(() => mockTds),
447
+ };
448
+ Object.defineProperty(th, 'closest', {
449
+ value: vi.fn(() => mockTable),
450
+ configurable: true,
451
+ });
452
+ }
383
453
 
384
- expect(thsRefs.length).toBe(3);
385
- thsRefs.forEach((thRef, i) => {
386
- const thClasses = [...thRef.classList];
454
+ await resizeHandle.trigger('dblclick');
387
455
 
388
- expect(thClasses).toContain(cols[i].thAttrs.class);
389
- expect(thRef instanceof HTMLTableCellElement).toBe(true);
390
- });
456
+ expect(wrapper.emitted()['col-resize']).toBeTruthy();
457
+ expect(wrapper.emitted()['col-resize'][0]).toHaveLength(2);
458
+ expect(typeof wrapper.emitted()['col-resize'][0][0]).toBe('number');
459
+ expect(typeof wrapper.emitted()['col-resize'][0][1]).toBe('number');
391
460
  });
392
461
  });
@@ -26,3 +26,5 @@ export const isLastColFixedInjectionKey = Symbol('isLastColFixed');
26
26
  export const isColsResizableInjectionKey = Symbol('isColsResizable');
27
27
 
28
28
  export const MIN_WIDTH_COL_RESIZE = 80;
29
+
30
+ export const HEADER_CELL_ONE_LINE_HEIGHT = 20;
@@ -21,7 +21,7 @@
21
21
  ]"
22
22
  v-on="colsResizable ? { mousemove: colResize } : {}"
23
23
  >
24
- <thead>
24
+ <thead ref="theadRef">
25
25
  <tr>
26
26
  <th
27
27
  v-for="(col, i) in props.cols"
@@ -33,14 +33,18 @@
33
33
  class="bg-surface"
34
34
  >
35
35
  <div :class="thDivClasses(i)" :style="bgColorStyle(col)">
36
- <div class="flex">
36
+ <div :class="['flex', { 'h-8': hasWrap }]">
37
37
  <slot :name="`prepend-header-cell-${kebabCase(col.name)}`" :col="col" />
38
38
  <PTableHeaderCell
39
39
  :text="col.title"
40
40
  :filter-active="col.filterActive"
41
41
  :show-filter-icon="col.filterable || col.sortable"
42
42
  :tooltip-text="col.tooltip"
43
- :class="[{ 'pl-2': i === 1 && isFirstColFixed, 'pr-2': i === cols.length && isLastColFixed }, 'grow']"
43
+ :class="[
44
+ hasWrap ? 'leading-4' : 'leading-5',
45
+ { 'pl-2': i === 1 && isFirstColFixed, 'pr-2': i === cols.length && isLastColFixed },
46
+ 'grow',
47
+ ]"
44
48
  :text-color="headerCellTextColor(col)"
45
49
  v-bind="col.headerCellAttrs"
46
50
  @click-filter-icon="$emit('click-filter-icon', $event, col)"
@@ -48,7 +52,7 @@
48
52
  </div>
49
53
  <div
50
54
  v-if="colsResizable && i !== 0 && !(i === cols.length - 1 && isLastColFixed)"
51
- class="absolute bottom-2 right-0 z-110 h-5 w-2 cursor-col-resize after:absolute after:bottom-0 after:z-110 after:block after:h-full after:w-2 after:cursor-col-resize after:border-r-2 after:border-dashed after:border-p-gray-30"
55
+ class="absolute right-0 top-1/2 z-110 h-5 w-2 -translate-y-1/2 cursor-col-resize after:absolute after:bottom-0 after:z-110 after:block after:h-full after:w-2 after:cursor-col-resize after:border-r-2 after:border-dashed after:border-p-gray-30"
52
56
  :class="i === cols.length - 1 ? 'after:right-0.5' : 'after:right-0'"
53
57
  data-resize-handle
54
58
  @mousedown="colResizeStart($event, i)"
@@ -108,10 +112,11 @@ import {
108
112
  type TableCol,
109
113
  } from '@squirrel/components/p-table/p-table.types';
110
114
  import { usePTableColResize } from '@squirrel/components/p-table/usePTableColResize';
115
+ import { usePTableHeaderWrap } from '@squirrel/components/p-table/usePTableHeaderWrap';
111
116
  import PTableHeaderCell from '@squirrel/components/p-table-header-cell/p-table-header-cell.vue';
112
117
  import PTableTd from '@squirrel/components/p-table-td/p-table-td.vue';
113
118
  import { kebabCase } from 'lodash-es';
114
- import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
119
+ import { computed, onBeforeUnmount, onMounted, provide, ref, useTemplateRef, watch } from 'vue';
115
120
 
116
121
  type Props = {
117
122
  /**
@@ -209,6 +214,7 @@ provide(
209
214
 
210
215
  // Data
211
216
  const scrollWrapper = ref<HTMLElement | null>(null);
217
+ const theadRef = useTemplateRef('theadRef');
212
218
  const ths = ref<HTMLElement[]>([]);
213
219
  const {
214
220
  isColResizing,
@@ -222,6 +228,7 @@ const {
222
228
  enabled: computed(() => props.colsResizable),
223
229
  ths,
224
230
  });
231
+ const { hasWrap } = usePTableHeaderWrap(theadRef);
225
232
  const tbodyElement = ref<HTMLElement | null>(null);
226
233
 
227
234
  // Methods
@@ -0,0 +1,118 @@
1
+ import { HEADER_CELL_ONE_LINE_HEIGHT } from '@squirrel/components/p-table/p-table.types';
2
+ import { usePTableHeaderWrap } from '@squirrel/components/p-table/usePTableHeaderWrap';
3
+ import { waitNT } from '@tests/vitest.helpers';
4
+ import { mount } from '@vue/test-utils';
5
+ import { defineComponent, useTemplateRef } from 'vue';
6
+
7
+ // Mock ResizeObserver to capture the composable's callback
8
+ let composableResizeCallback;
9
+ const mockResizeObserver = vi.fn((callback) => {
10
+ composableResizeCallback = callback;
11
+ return {
12
+ observe: vi.fn(),
13
+ unobserve: vi.fn(),
14
+ disconnect: vi.fn(),
15
+ };
16
+ });
17
+
18
+ const createWrapper = (refName = 'theadRef') => {
19
+ const TestComponent = defineComponent({
20
+ setup() {
21
+ const theadRef = useTemplateRef('theadRef');
22
+ const { hasWrap } = usePTableHeaderWrap(theadRef);
23
+
24
+ return {
25
+ theadRef,
26
+ hasWrap,
27
+ };
28
+ },
29
+ template: `
30
+ <table>
31
+ <thead ref="${refName}" :data-has-wrap="hasWrap">
32
+ <tr>
33
+ <th>
34
+ <div data-p-table-header-text class="div-to-resize">Short</div>
35
+ </th>
36
+ <th>
37
+ <span data-p-table-header-text>Very Long Header That Could Potentially Wrap To Multiple Lines</span>
38
+ </th>
39
+ </tr>
40
+ </thead>
41
+ </table>
42
+ `,
43
+ });
44
+
45
+ return mount(TestComponent, {
46
+ attachTo: document.body,
47
+ });
48
+ };
49
+
50
+ describe('usePTableHeaderWrap', () => {
51
+ const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');
52
+ const originalGlobalResizeObserver = globalThis.ResizeObserver;
53
+
54
+ beforeAll(() => {
55
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
56
+ configurable: true,
57
+ value: HEADER_CELL_ONE_LINE_HEIGHT,
58
+ });
59
+ globalThis.ResizeObserver = mockResizeObserver;
60
+ });
61
+
62
+ afterAll(() => {
63
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight);
64
+ globalThis.ResizeObserver = originalGlobalResizeObserver;
65
+ });
66
+
67
+ beforeEach(() => {
68
+ mockResizeObserver.mockClear();
69
+ composableResizeCallback = null;
70
+ });
71
+
72
+ it('should have hasWrap false when all divs are single line height', async () => {
73
+ const wrapper = createWrapper();
74
+
75
+ // Trigger the composable's ResizeObserver callback
76
+ if (composableResizeCallback) {
77
+ composableResizeCallback();
78
+ await waitNT(wrapper.vm);
79
+ }
80
+
81
+ expect(wrapper.find('thead').attributes()['data-has-wrap']).toBe('false');
82
+
83
+ wrapper.unmount();
84
+ });
85
+
86
+ it('should have hasWrap true when one div is double line height', async () => {
87
+ // Override offsetHeight for the "div-to-resize" element
88
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
89
+ configurable: true,
90
+ get() {
91
+ if (this.classList.contains('div-to-resize')) {
92
+ return HEADER_CELL_ONE_LINE_HEIGHT * 2;
93
+ }
94
+ return HEADER_CELL_ONE_LINE_HEIGHT;
95
+ },
96
+ });
97
+
98
+ const wrapper = createWrapper();
99
+
100
+ // Trigger the composable's ResizeObserver callback
101
+ if (composableResizeCallback) {
102
+ composableResizeCallback();
103
+ await waitNT(wrapper.vm);
104
+ }
105
+
106
+ expect(wrapper.find('thead').attributes()['data-has-wrap']).toBe('true');
107
+
108
+ wrapper.unmount();
109
+ });
110
+
111
+ it('should handle null theadRef gracefully', () => {
112
+ const wrapper = createWrapper('nullRef');
113
+
114
+ expect(wrapper.find('thead').attributes()['data-has-wrap']).toBe('false');
115
+
116
+ wrapper.unmount();
117
+ });
118
+ });
@@ -0,0 +1,45 @@
1
+ import { HEADER_CELL_ONE_LINE_HEIGHT } from '@squirrel/components/p-table/p-table.types';
2
+ import { onBeforeUnmount, onMounted, type Ref, ref } from 'vue';
3
+
4
+ export const usePTableHeaderWrap = (theadRef: Ref<HTMLElement | null>) => {
5
+ let headerObserver: ResizeObserver | null = null;
6
+ const hasWrap = ref(false);
7
+
8
+ const setupObserver = () => {
9
+ if (!theadRef.value) return;
10
+
11
+ headerObserver = new ResizeObserver(() => {
12
+ if (theadRef.value) {
13
+ const textDivs = theadRef.value.querySelectorAll('[data-p-table-header-text]') as NodeListOf<HTMLElement>;
14
+
15
+ for (const div of textDivs) {
16
+ if (div.offsetHeight > HEADER_CELL_ONE_LINE_HEIGHT) {
17
+ hasWrap.value = true;
18
+ return;
19
+ }
20
+ }
21
+
22
+ hasWrap.value = false;
23
+ }
24
+ });
25
+
26
+ headerObserver.observe(theadRef.value);
27
+ };
28
+
29
+ const cleanupObserver = () => {
30
+ if (headerObserver) {
31
+ headerObserver.disconnect();
32
+ headerObserver = null;
33
+ }
34
+ };
35
+
36
+ onMounted(() => {
37
+ setupObserver();
38
+ });
39
+
40
+ onBeforeUnmount(() => {
41
+ cleanupObserver();
42
+ });
43
+
44
+ return { hasWrap };
45
+ };
@@ -1,4 +1,3 @@
1
- import PFilterIcon from '@squirrel/components/p-table-header-cell/p-table-filter-icon.vue';
2
1
  import PTableHeaderCell from '@squirrel/components/p-table-header-cell/p-table-header-cell.vue';
3
2
  import { createWrapperFor } from '@tests/vitest.helpers';
4
3
 
@@ -9,11 +8,16 @@ describe('PTableHeaderCell.vue', () => {
9
8
  text: 'Test text',
10
9
  textClass: 'test-class',
11
10
  },
12
- global: { stubs: { PInfoIcon: true } },
11
+ global: {
12
+ stubs: {
13
+ PInfoIcon: true,
14
+ PIcon: true,
15
+ },
16
+ },
13
17
  });
14
18
 
15
19
  const div = await wrapper.find('div.test-class');
16
- const icon = wrapper.findComponent(PFilterIcon);
20
+ const icon = wrapper.findComponent({ name: 'PIcon' });
17
21
  const tooltipIcon = await wrapper.findComponent({ name: 'PInfoIcon' });
18
22
 
19
23
  expect(wrapper.classes()).toEqual(['flex', 'items-center', 'overflow-hidden']);
@@ -23,9 +27,12 @@ describe('PTableHeaderCell.vue', () => {
23
27
  expect(div.attributes('title')).toBe('Test text');
24
28
  expect(div.classes()).toEqual([
25
29
  'text-xs',
26
- 'leading-5',
27
30
  'font-semibold',
28
- 'truncate',
31
+ 'line-clamp-2',
32
+ 'break-words',
33
+ 'hyphens-auto',
34
+ 'whitespace-normal',
35
+ 'max-h-10',
29
36
  'shrink',
30
37
  'test-class',
31
38
  'text-p-gray-60',
@@ -45,11 +52,10 @@ describe('PTableHeaderCell.vue', () => {
45
52
  });
46
53
 
47
54
  const div = wrapper.find('div.test-class');
48
- const icon = wrapper.findComponent(PFilterIcon);
55
+ const icon = wrapper.findComponent({ name: 'PIcon' });
49
56
 
50
- expect(icon.exists()).toBe(true);
51
57
  expect(icon.classes()).not.toContain('hidden');
52
- expect(icon.vm.active).toBe(true);
58
+ expect(icon.classes()).toContain('text-active-blue');
53
59
  expect(div.classes()).toContain('text-active-blue');
54
60
  });
55
61
 
@@ -79,7 +85,9 @@ describe('PTableHeaderCell.vue', () => {
79
85
  },
80
86
  });
81
87
 
82
- await wrapper.find('.filter').trigger('click');
88
+ const icon = wrapper.findComponent({ name: 'PIcon' });
89
+
90
+ await icon.trigger('click');
83
91
 
84
92
  const emittedEvent = wrapper.emitted()['click-filter-icon'][0];
85
93
  const mouseEvent = emittedEvent[0];