@pequity/squirrel 8.4.5 → 8.5.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 (81) hide show
  1. package/README.md +29 -0
  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/index.js +33 -27
  7. package/dist/cjs/inputClasses.js +3 -3
  8. package/dist/cjs/p-icon.js +2 -1
  9. package/dist/cjs/p-loading.js +2 -2
  10. package/dist/cjs/p-modal.js +45 -43
  11. package/dist/cjs/p-table-header-cell.js +3 -2
  12. package/dist/cjs/p-table.js +2 -0
  13. package/dist/cjs/usePTableHeaderWrap.js +38 -0
  14. package/dist/es/chunks/index.js +530 -179
  15. package/dist/es/chunks/p-alert.js +11 -16
  16. package/dist/es/chunks/p-btn.js +2 -2
  17. package/dist/es/chunks/p-input-percent.js +2 -2
  18. package/dist/es/index.js +39 -33
  19. package/dist/es/inputClasses.js +4 -4
  20. package/dist/es/p-icon.js +2 -1
  21. package/dist/es/p-loading.js +2 -2
  22. package/dist/es/p-modal.js +45 -43
  23. package/dist/es/p-table-header-cell.js +3 -2
  24. package/dist/es/p-table.js +2 -0
  25. package/dist/es/usePTableHeaderWrap.js +38 -0
  26. package/dist/squirrel/components/p-action-bar/p-action-bar.vue.d.ts +1 -1
  27. package/dist/squirrel/components/p-alert/p-alert.vue.d.ts +2 -2
  28. package/dist/squirrel/components/p-avatar/p-avatar.vue.d.ts +1 -1
  29. package/dist/squirrel/components/p-btn/p-btn.vue.d.ts +3 -3
  30. package/dist/squirrel/components/p-card/p-card.vue.d.ts +1 -1
  31. package/dist/squirrel/components/p-checkbox/p-checkbox.vue.d.ts +1 -1
  32. package/dist/squirrel/components/p-close-btn/p-close-btn.vue.d.ts +1 -1
  33. package/dist/squirrel/components/p-date-picker/p-date-picker.vue.d.ts +1 -1
  34. package/dist/squirrel/components/p-drawer/p-drawer.vue.d.ts +12 -12
  35. package/dist/squirrel/components/p-dropdown-select/p-dropdown-select.vue.d.ts +1 -1
  36. package/dist/squirrel/components/p-file-upload/p-file-upload.vue.d.ts +1 -1
  37. package/dist/squirrel/components/p-icon/p-icon.types.d.ts +1 -0
  38. package/dist/squirrel/components/p-icon/p-icon.vue.d.ts +1 -1
  39. package/dist/squirrel/components/p-info-icon/p-info-icon.vue.d.ts +1 -1
  40. package/dist/squirrel/components/p-inline-date-picker/p-inline-date-picker.vue.d.ts +1 -1
  41. package/dist/squirrel/components/p-input/p-input.vue.d.ts +1 -1
  42. package/dist/squirrel/components/p-input-percent/p-input-percent.vue.d.ts +1 -1
  43. package/dist/squirrel/components/p-input-search/p-input-search.vue.d.ts +1 -1
  44. package/dist/squirrel/components/p-link/p-link.vue.d.ts +1 -1
  45. package/dist/squirrel/components/p-loading/p-loading.vue.d.ts +1 -1
  46. package/dist/squirrel/components/p-modal/p-modal.vue.d.ts +5 -1
  47. package/dist/squirrel/components/p-pagination/p-pagination.vue.d.ts +1 -1
  48. package/dist/squirrel/components/p-pagination-info/p-pagination-info.vue.d.ts +1 -1
  49. package/dist/squirrel/components/p-progress-bar/p-progress-bar.vue.d.ts +1 -1
  50. package/dist/squirrel/components/p-ring-loader/p-ring-loader.vue.d.ts +1 -1
  51. package/dist/squirrel/components/p-select/p-select.vue.d.ts +1 -1
  52. package/dist/squirrel/components/p-select-btn/p-select-btn.vue.d.ts +1 -1
  53. package/dist/squirrel/components/p-select-list/p-select-list.vue.d.ts +1 -1
  54. package/dist/squirrel/components/p-steps/p-steps.vue.d.ts +1 -1
  55. package/dist/squirrel/components/p-table/p-table.types.d.ts +1 -0
  56. package/dist/squirrel/components/p-table/p-table.vue.d.ts +1 -1
  57. package/dist/squirrel/components/p-table/usePTableHeaderWrap.d.ts +4 -0
  58. package/dist/squirrel/components/p-table-header-cell/p-table-filter-icon.vue.d.ts +1 -1
  59. package/dist/squirrel/components/p-table-header-cell/p-table-header-cell.vue.d.ts +3 -3
  60. package/dist/squirrel/components/p-table-loader/p-table-loader.vue.d.ts +1 -1
  61. package/dist/squirrel/components/p-table-sort/p-table-sort.vue.d.ts +1 -1
  62. package/dist/squirrel/components/p-table-td/p-table-td.vue.d.ts +1 -1
  63. package/dist/squirrel/components/p-tabs/p-tabs.vue.d.ts +1 -1
  64. package/dist/squirrel/components/p-tabs-pills/p-tabs-pills.vue.d.ts +1 -1
  65. package/dist/squirrel/components/p-textarea/p-textarea.vue.d.ts +1 -1
  66. package/dist/squirrel/components/p-toggle/p-toggle.vue.d.ts +1 -1
  67. package/dist/squirrel.css +22 -22
  68. package/package.json +23 -21
  69. package/squirrel/components/p-alert/p-alert.spec.js +4 -4
  70. package/squirrel/components/p-alert/p-alert.stories.js +19 -13
  71. package/squirrel/components/p-alert/p-alert.vue +9 -11
  72. package/squirrel/components/p-icon/p-icon.types.ts +1 -0
  73. package/squirrel/components/p-modal/p-modal-basic.spec.js +29 -3
  74. package/squirrel/components/p-modal/p-modal.vue +44 -33
  75. package/squirrel/components/p-table/p-table.spec.js +51 -15
  76. package/squirrel/components/p-table/p-table.types.ts +2 -0
  77. package/squirrel/components/p-table/p-table.vue +7 -4
  78. package/squirrel/components/p-table/usePTableHeaderWrap.spec.js +118 -0
  79. package/squirrel/components/p-table/usePTableHeaderWrap.ts +45 -0
  80. package/squirrel/components/p-table-header-cell/p-table-header-cell.spec.js +5 -1
  81. package/squirrel/components/p-table-header-cell/p-table-header-cell.vue +2 -1
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@pequity/squirrel",
3
3
  "description": "Squirrel component library",
4
- "version": "8.4.5",
5
- "packageManager": "pnpm@10.13.1",
4
+ "version": "8.5.0",
5
+ "packageManager": "pnpm@10.15.1",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "preinstall": "npx only-allow pnpm",
@@ -13,6 +13,7 @@
13
13
  "lint": "eslint \"**/*.{vue,ts,js}\"",
14
14
  "lint-fix": "eslint --fix \"**/*.{vue,ts,js}\"",
15
15
  "test:unit": "vitest --project=unit",
16
+ "test:unit-nowatch": "vitest run --project=unit",
16
17
  "test:unit-coverage": "vitest run --project=unit --coverage && make-coverage-badge",
17
18
  "typecheck": "vue-tsc --noEmit",
18
19
  "storybook": "storybook dev -p 6006",
@@ -49,57 +50,58 @@
49
50
  "devDependencies": {
50
51
  "@commitlint/cli": "^19.8.1",
51
52
  "@commitlint/config-conventional": "^19.8.1",
52
- "@pequity/eslint-config": "^2.0.2",
53
- "@playwright/test": "^1.54.1",
53
+ "@pequity/eslint-config": "^2.0.3",
54
+ "@playwright/test": "^1.55.0",
54
55
  "@semantic-release/changelog": "^6.0.3",
55
56
  "@semantic-release/git": "^10.0.1",
56
- "@storybook/addon-a11y": "^9.0.18",
57
- "@storybook/addon-docs": "^9.0.18",
58
- "@storybook/addon-links": "^9.0.18",
59
- "@storybook/addon-vitest": "^9.0.18",
60
- "@storybook/vue3-vite": "^9.0.18",
57
+ "@storybook/addon-a11y": "^9.1.4",
58
+ "@storybook/addon-docs": "^9.1.4",
59
+ "@storybook/addon-links": "^9.1.4",
60
+ "@storybook/addon-vitest": "^9.1.4",
61
+ "@storybook/vue3-vite": "^9.1.4",
61
62
  "@tanstack/vue-virtual": "3.13.12",
62
63
  "@types/jsdom": "^21.1.7",
63
64
  "@types/lodash-es": "^4.17.12",
64
- "@types/node": "^24.1.0",
65
+ "@types/node": "^24.3.0",
65
66
  "@vitejs/plugin-vue": "^6.0.1",
66
67
  "@vitest/browser": "3.2.4",
67
68
  "@vitest/coverage-v8": "^3.2.4",
68
- "@vue/compiler-sfc": "3.5.18",
69
+ "@vue/compiler-sfc": "3.5.21",
69
70
  "@vue/test-utils": "^2.4.6",
70
71
  "@vuepic/vue-datepicker": "11.0.2",
71
72
  "autoprefixer": "^10.4.21",
72
- "eslint": "^9.32.0",
73
- "eslint-plugin-storybook": "^9.0.18",
73
+ "eslint": "^9.34.0",
74
+ "eslint-plugin-storybook": "^9.1.4",
74
75
  "floating-vue": "5.2.2",
75
76
  "glob": "^11.0.3",
76
77
  "husky": "^9.1.7",
77
78
  "iconify-icon": "^3.0.0",
78
79
  "jsdom": "^26.1.0",
79
- "lint-staged": "^16.1.2",
80
+ "lint-staged": "^16.1.6",
80
81
  "lodash-es": "4.17.21",
81
82
  "make-coverage-badge": "^1.2.0",
82
- "playwright": "^1.54.1",
83
+ "playwright": "^1.55.0",
83
84
  "postcss": "^8.5.6",
84
85
  "prettier": "^3.6.2",
85
86
  "prettier-plugin-tailwindcss": "^0.6.14",
86
87
  "resolve-tspaths": "^0.8.23",
87
88
  "rimraf": "^6.0.1",
88
- "sass": "^1.89.2",
89
+ "sass": "^1.92.0",
89
90
  "semantic-release": "^24.2.7",
90
- "storybook": "^9.0.18",
91
+ "storybook": "^9.1.4",
91
92
  "svgo": "^4.0.0",
92
93
  "tailwindcss": "^3.4.17",
93
94
  "typescript": "5.8.3",
94
- "vite": "^7.0.6",
95
+ "vite": "^7.1.4",
95
96
  "vitest": "^3.2.4",
96
- "vue": "3.5.18",
97
+ "vue": "3.5.21",
97
98
  "vue-currency-input": "3.2.1",
98
99
  "vue-router": "4.5.1",
99
100
  "vue-toastification": "2.0.0-rc.5",
100
- "vue-tsc": "3.0.4"
101
+ "vue-tsc": "3.0.6"
101
102
  },
102
103
  "dependencies": {
103
- "tailwind-variants": "^1.0.0"
104
+ "tailwind-merge": "^3.3.1",
105
+ "tailwind-variants": "^3.1.0"
104
106
  }
105
107
  }
@@ -3,10 +3,10 @@ import { createWrapperFor } from '@tests/vitest.helpers';
3
3
 
4
4
  describe('PAlert.vue', () => {
5
5
  it.each([
6
- ['info', ['bg-info', 'text-on-info'], 'streamline:information-circle-solid'],
7
- ['warning', ['bg-warning', 'text-on-warning'], 'streamline:warning-triangle-solid'],
8
- ['error', ['bg-error', 'text-on-error'], 'streamline:warning-octagon-solid'],
9
- ['success', ['bg-success', 'text-on-success'], 'streamline:check-square-solid'],
6
+ ['info', ['bg-info', 'text-on-info'], 'material-symbols:info-outline'],
7
+ ['warning', ['bg-warning', 'text-on-warning'], 'warning'],
8
+ ['error', ['bg-error', 'text-on-error'], 'cancel-circle'],
9
+ ['success', ['bg-success', 'text-on-success'], 'ok-circle'],
10
10
  ])('renders an alert of type %s', async (type, classes, iconName) => {
11
11
  const wrapper = createWrapperFor(PAlert, { props: { type }, global: { stubs: { PIcon: true } } });
12
12
 
@@ -5,6 +5,13 @@ export default {
5
5
  title: 'Components/PAlert',
6
6
  component: PAlert,
7
7
  tags: ['autodocs'],
8
+ render: (args) => ({
9
+ components: { PAlert },
10
+ setup() {
11
+ return { args };
12
+ },
13
+ template: `<PAlert v-bind="args">${args.default}</PAlert>`,
14
+ }),
8
15
  argTypes: {
9
16
  type: {
10
17
  control: { type: 'select' },
@@ -22,16 +29,10 @@ export default {
22
29
  };
23
30
 
24
31
  export const Info = {
25
- render: (args) => ({
26
- components: { PAlert },
27
- setup() {
28
- return { args };
29
- },
30
- template: `<PAlert v-bind="args">${args.default}</PAlert>`,
31
- }),
32
32
  args: {
33
33
  type: 'info',
34
- default: 'Lorem ipsum dolor sit amet',
34
+ default:
35
+ 'Your account has been updated with the latest security features.<br />Two-factor authentication is now enabled for enhanced protection.',
35
36
  },
36
37
  };
37
38
 
@@ -39,28 +40,33 @@ Info.play = async ({ canvasElement }) => {
39
40
  const canvas = within(canvasElement);
40
41
  const alertDiv = await canvas.getByRole('alert');
41
42
 
42
- await expect(alertDiv.innerText).toBe('Lorem ipsum dolor sit amet');
43
- await expect(alertDiv).toHaveClass('p-4 rounded text-xs font-semibold bg-info text-on-info');
43
+ await expect(alertDiv.innerText).toBe(
44
+ 'Your account has been updated with the latest security features.\nTwo-factor authentication is now enabled for enhanced protection.'
45
+ );
46
+ await expect(alertDiv).toHaveClass('p-4 rounded-lg text-xs font-semibold bg-info text-on-info');
44
47
  await expect(alertDiv).toHaveStyle('background-color: #e4edfa');
45
48
  };
46
49
 
47
50
  export const Success = {
48
51
  args: {
49
- ...Info.args,
50
52
  type: 'success',
53
+ default:
54
+ 'Your profile has been successfully updated and changes are now live.<br />Other team members can now see your updated information.',
51
55
  },
52
56
  };
53
57
 
54
58
  export const Warning = {
55
59
  args: {
56
- ...Info.args,
57
60
  type: 'warning',
61
+ default:
62
+ 'You have unsaved changes that will be lost if you navigate away from this page.<br />Make sure to save your work before continuing.',
58
63
  },
59
64
  };
60
65
 
61
66
  export const Error = {
62
67
  args: {
63
- ...Info.args,
64
68
  type: 'error',
69
+ default:
70
+ 'Failed to save changes. Please check your internet connection and try again.<br />If the problem persists, contact support at help@pequity.com.',
65
71
  },
66
72
  };
@@ -1,11 +1,9 @@
1
1
  <template>
2
- <div :class="['rounded p-4 text-xs font-semibold', ALERT_TYPES[props.type].classes]" role="alert">
3
- <div class="flex">
4
- <div class="pr-2">
5
- <slot name="icon">
6
- <PIcon :icon="ALERT_TYPES[props.type].icon" width="16" class="-mb-0.5" inline />
7
- </slot>
8
- </div>
2
+ <div :class="['rounded-lg p-4 text-xs font-semibold', ALERT_TYPES[props.type].classes]" role="alert">
3
+ <div class="flex items-center gap-4">
4
+ <slot name="icon">
5
+ <PIcon :icon="ALERT_TYPES[props.type].icon" width="20" />
6
+ </slot>
9
7
  <div>
10
8
  <slot></slot>
11
9
  </div>
@@ -18,10 +16,10 @@ import PIcon from '@squirrel/components/p-icon/p-icon.vue';
18
16
  import { type PropType } from 'vue';
19
17
 
20
18
  const ALERT_TYPES = {
21
- info: { classes: `bg-info text-on-info`, icon: 'streamline:information-circle-solid' },
22
- warning: { classes: `bg-warning text-on-warning`, icon: 'streamline:warning-triangle-solid' },
23
- error: { classes: `bg-error text-on-error`, icon: 'streamline:warning-octagon-solid' },
24
- success: { classes: `bg-success text-on-success`, icon: 'streamline:check-square-solid' },
19
+ info: { classes: `bg-info text-on-info`, icon: 'material-symbols:info-outline' },
20
+ warning: { classes: `bg-warning text-on-warning`, icon: 'warning' },
21
+ error: { classes: `bg-error text-on-error`, icon: 'cancel-circle' },
22
+ success: { classes: `bg-success text-on-success`, icon: 'ok-circle' },
25
23
  };
26
24
  </script>
27
25
 
@@ -35,6 +35,7 @@ export const P_ICON_ALIASES = {
35
35
  info: 'streamline:information-circle',
36
36
  help: 'ph:question',
37
37
  search: 'streamline:magnifying-glass-solid',
38
+ warning: 'ri:error-warning-line',
38
39
  } as const;
39
40
 
40
41
  export type PIconAlias = keyof typeof P_ICON_ALIASES;
@@ -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.
@@ -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, {
@@ -374,19 +425,4 @@ describe('PTable.vue', () => {
374
425
  expect(wrapper.find('table tbody tr:last-child > td').attributes().style).toEqual('height: 100px;');
375
426
  expect(wrapper.find('table tbody tr:last-child').classes()).toEqual([]);
376
427
  });
377
-
378
- it('sets th refs correctly', async () => {
379
- const cols = cloneDeep(columns);
380
- const wrapper = createWrapperFor(PTable, { props: { cols } });
381
-
382
- const thsRefs = wrapper.vm.ths;
383
-
384
- expect(thsRefs.length).toBe(3);
385
- thsRefs.forEach((thRef, i) => {
386
- const thClasses = [...thRef.classList];
387
-
388
- expect(thClasses).toContain(cols[i].thAttrs.class);
389
- expect(thRef instanceof HTMLTableCellElement).toBe(true);
390
- });
391
- });
392
428
  });
@@ -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,7 +33,7 @@
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-10': hasWrap }]">
37
37
  <slot :name="`prepend-header-cell-${kebabCase(col.name)}`" :col="col" />
38
38
  <PTableHeaderCell
39
39
  :text="col.title"
@@ -48,7 +48,7 @@
48
48
  </div>
49
49
  <div
50
50
  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"
51
+ 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
52
  :class="i === cols.length - 1 ? 'after:right-0.5' : 'after:right-0'"
53
53
  data-resize-handle
54
54
  @mousedown="colResizeStart($event, i)"
@@ -108,10 +108,11 @@ import {
108
108
  type TableCol,
109
109
  } from '@squirrel/components/p-table/p-table.types';
110
110
  import { usePTableColResize } from '@squirrel/components/p-table/usePTableColResize';
111
+ import { usePTableHeaderWrap } from '@squirrel/components/p-table/usePTableHeaderWrap';
111
112
  import PTableHeaderCell from '@squirrel/components/p-table-header-cell/p-table-header-cell.vue';
112
113
  import PTableTd from '@squirrel/components/p-table-td/p-table-td.vue';
113
114
  import { kebabCase } from 'lodash-es';
114
- import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue';
115
+ import { computed, onBeforeUnmount, onMounted, provide, ref, useTemplateRef, watch } from 'vue';
115
116
 
116
117
  type Props = {
117
118
  /**
@@ -209,6 +210,7 @@ provide(
209
210
 
210
211
  // Data
211
212
  const scrollWrapper = ref<HTMLElement | null>(null);
213
+ const theadRef = useTemplateRef('theadRef');
212
214
  const ths = ref<HTMLElement[]>([]);
213
215
  const {
214
216
  isColResizing,
@@ -222,6 +224,7 @@ const {
222
224
  enabled: computed(() => props.colsResizable),
223
225
  ths,
224
226
  });
227
+ const { hasWrap } = usePTableHeaderWrap(theadRef);
225
228
  const tbodyElement = ref<HTMLElement | null>(null);
226
229
 
227
230
  // 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
+ });