@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.
- package/README.md +31 -2
- package/dist/cjs/chunks/index.js +530 -179
- package/dist/cjs/chunks/p-alert.js +11 -16
- package/dist/cjs/chunks/p-btn.js +1 -1
- package/dist/cjs/chunks/p-input-percent.js +2 -2
- package/dist/cjs/chunks/p-table-header-cell.js +57 -0
- package/dist/cjs/index.js +41 -33
- package/dist/cjs/inputClasses.js +3 -3
- package/dist/cjs/p-icon.js +2 -1
- package/dist/cjs/p-loading.js +2 -2
- package/dist/cjs/p-modal.js +45 -43
- package/dist/cjs/p-table-header-cell.js +2 -116
- package/dist/cjs/p-table.js +2 -0
- package/dist/cjs/usePTableHeaderWrap.js +38 -0
- package/dist/es/chunks/index.js +530 -179
- package/dist/es/chunks/p-alert.js +11 -16
- package/dist/es/chunks/p-btn.js +2 -2
- package/dist/es/chunks/p-input-percent.js +2 -2
- package/dist/es/chunks/p-table-header-cell.js +58 -0
- package/dist/es/index.js +49 -41
- package/dist/es/inputClasses.js +4 -4
- package/dist/es/p-icon.js +2 -1
- package/dist/es/p-loading.js +2 -2
- package/dist/es/p-modal.js +45 -43
- package/dist/es/p-table-header-cell.js +2 -116
- package/dist/es/p-table.js +2 -0
- package/dist/es/usePTableHeaderWrap.js +38 -0
- package/dist/squirrel/components/index.d.ts +1 -2
- package/dist/squirrel/components/p-action-bar/p-action-bar.vue.d.ts +1 -1
- package/dist/squirrel/components/p-alert/p-alert.vue.d.ts +2 -2
- package/dist/squirrel/components/p-avatar/p-avatar.vue.d.ts +1 -1
- package/dist/squirrel/components/p-btn/p-btn.vue.d.ts +3 -3
- package/dist/squirrel/components/p-card/p-card.vue.d.ts +1 -1
- package/dist/squirrel/components/p-checkbox/p-checkbox.vue.d.ts +1 -1
- package/dist/squirrel/components/p-close-btn/p-close-btn.vue.d.ts +1 -1
- package/dist/squirrel/components/p-date-picker/p-date-picker.vue.d.ts +1 -1
- package/dist/squirrel/components/p-drawer/p-drawer.vue.d.ts +12 -12
- package/dist/squirrel/components/p-dropdown-select/p-dropdown-select.vue.d.ts +1 -1
- package/dist/squirrel/components/p-file-upload/p-file-upload.vue.d.ts +1 -1
- package/dist/squirrel/components/p-icon/p-icon.types.d.ts +1 -0
- package/dist/squirrel/components/p-icon/p-icon.vue.d.ts +1 -1
- package/dist/squirrel/components/p-info-icon/p-info-icon.vue.d.ts +1 -1
- package/dist/squirrel/components/p-inline-date-picker/p-inline-date-picker.vue.d.ts +1 -1
- package/dist/squirrel/components/p-input/p-input.vue.d.ts +1 -1
- package/dist/squirrel/components/p-input-percent/p-input-percent.vue.d.ts +1 -1
- package/dist/squirrel/components/p-input-search/p-input-search.vue.d.ts +1 -1
- package/dist/squirrel/components/p-link/p-link.vue.d.ts +1 -1
- package/dist/squirrel/components/p-loading/p-loading.vue.d.ts +1 -1
- package/dist/squirrel/components/p-modal/p-modal.vue.d.ts +5 -1
- package/dist/squirrel/components/p-pagination/p-pagination.vue.d.ts +1 -1
- package/dist/squirrel/components/p-pagination-info/p-pagination-info.vue.d.ts +1 -1
- package/dist/squirrel/components/p-progress-bar/p-progress-bar.vue.d.ts +1 -1
- package/dist/squirrel/components/p-ring-loader/p-ring-loader.vue.d.ts +1 -1
- package/dist/squirrel/components/p-select/p-select.vue.d.ts +1 -1
- package/dist/squirrel/components/p-select-btn/p-select-btn.vue.d.ts +1 -1
- package/dist/squirrel/components/p-select-list/p-select-list.vue.d.ts +1 -1
- package/dist/squirrel/components/p-steps/p-steps.vue.d.ts +1 -1
- package/dist/squirrel/components/p-table/p-table.types.d.ts +1 -0
- package/dist/squirrel/components/p-table/p-table.vue.d.ts +1 -1
- package/dist/squirrel/components/p-table/usePTableHeaderWrap.d.ts +4 -0
- package/dist/squirrel/components/p-table-header-cell/p-table-header-cell.vue.d.ts +14 -161
- package/dist/squirrel/components/p-table-loader/p-table-loader.vue.d.ts +1 -1
- package/dist/squirrel/components/p-table-sort/p-table-sort.vue.d.ts +1 -1
- package/dist/squirrel/components/p-table-td/p-table-td.vue.d.ts +1 -1
- package/dist/squirrel/components/p-tabs/p-tabs.vue.d.ts +1 -1
- package/dist/squirrel/components/p-tabs-pills/p-tabs-pills.vue.d.ts +1 -1
- package/dist/squirrel/components/p-textarea/p-textarea.vue.d.ts +1 -1
- package/dist/squirrel/components/p-toggle/p-toggle.vue.d.ts +1 -1
- package/dist/squirrel.css +22 -33
- package/package.json +23 -21
- package/squirrel/components/index.ts +0 -2
- package/squirrel/components/p-alert/p-alert.spec.js +4 -4
- package/squirrel/components/p-alert/p-alert.stories.js +19 -13
- package/squirrel/components/p-alert/p-alert.vue +9 -11
- package/squirrel/components/p-icon/p-icon.types.ts +1 -0
- package/squirrel/components/p-modal/p-modal-basic.spec.js +29 -3
- package/squirrel/components/p-modal/p-modal.vue +44 -33
- package/squirrel/components/p-table/p-table.spec.js +79 -10
- package/squirrel/components/p-table/p-table.types.ts +2 -0
- package/squirrel/components/p-table/p-table.vue +12 -5
- package/squirrel/components/p-table/usePTableHeaderWrap.spec.js +118 -0
- package/squirrel/components/p-table/usePTableHeaderWrap.ts +45 -0
- package/squirrel/components/p-table-header-cell/p-table-header-cell.spec.js +17 -9
- package/squirrel/components/p-table-header-cell/p-table-header-cell.vue +69 -83
- package/dist/cjs/p-table-filter-icon.js +0 -28
- package/dist/es/p-table-filter-icon.js +0 -29
- package/dist/squirrel/components/p-table-header-cell/p-table-filter-icon.vue.d.ts +0 -20
- package/squirrel/assets/filter-icon-active-hover.svg +0 -4
- package/squirrel/assets/filter-icon-active.svg +0 -4
- package/squirrel/assets/filter-icon-hover.svg +0 -7
- package/squirrel/assets/filter-icon.svg +0 -6
- package/squirrel/components/p-table-header-cell/p-filter-icon.spec.js +0 -20
- package/squirrel/components/p-table-header-cell/p-filter-icon.stories.js +0 -33
- 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
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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.
|
|
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('
|
|
429
|
+
it('emits col-resize event when column resizing stops', async () => {
|
|
379
430
|
const cols = cloneDeep(columns);
|
|
380
|
-
const wrapper = createWrapperFor(PTable, {
|
|
431
|
+
const wrapper = createWrapperFor(PTable, {
|
|
432
|
+
props: {
|
|
433
|
+
cols,
|
|
434
|
+
colsResizable: true,
|
|
435
|
+
data,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
381
438
|
|
|
382
|
-
const
|
|
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
|
-
|
|
385
|
-
thsRefs.forEach((thRef, i) => {
|
|
386
|
-
const thClasses = [...thRef.classList];
|
|
454
|
+
await resizeHandle.trigger('dblclick');
|
|
387
455
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
});
|
|
@@ -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="[
|
|
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
|
|
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: {
|
|
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(
|
|
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
|
-
'
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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];
|