@capillarytech/creatives-library 8.0.207 → 8.0.209
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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- package/v2Containers/Whatsapp/constants.js +26 -2
- package/v2Containers/Whatsapp/index.js +4 -1
- package/v2Containers/Whatsapp/tests/index.test.js +460 -18
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplitContainer - Smooth and functional resizable split panel
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Smooth dragging with proper event handling
|
|
6
|
+
* - Percentage-based sizing that works correctly
|
|
7
|
+
* - Proper constraints and boundaries
|
|
8
|
+
* - Clean visual feedback
|
|
9
|
+
* - Touch support for mobile devices
|
|
10
|
+
* - Memory of split position
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
14
|
+
import PropTypes from 'prop-types';
|
|
15
|
+
|
|
16
|
+
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
17
|
+
|
|
18
|
+
// Styles
|
|
19
|
+
import './_splitContainer.scss';
|
|
20
|
+
|
|
21
|
+
// Constants
|
|
22
|
+
const SPLIT_CONSTRAINTS = {
|
|
23
|
+
MIN_PERCENT: 10,
|
|
24
|
+
MAX_PERCENT: 90,
|
|
25
|
+
DEFAULT_MIN_SIZE: 200
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DIRECTIONS = {
|
|
29
|
+
HORIZONTAL: 'horizontal',
|
|
30
|
+
VERTICAL: 'vertical'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SplitContainer = ({
|
|
34
|
+
children,
|
|
35
|
+
className = '',
|
|
36
|
+
defaultSplit = 50,
|
|
37
|
+
minSize = SPLIT_CONSTRAINTS.DEFAULT_MIN_SIZE,
|
|
38
|
+
maxSize = null,
|
|
39
|
+
direction = DIRECTIONS.HORIZONTAL,
|
|
40
|
+
onSplitChange = null
|
|
41
|
+
}) => {
|
|
42
|
+
const [splitSize, setSplitSize] = useState(defaultSplit);
|
|
43
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
44
|
+
|
|
45
|
+
// Refs for drag handling
|
|
46
|
+
const containerRef = useRef(null);
|
|
47
|
+
const startPositionRef = useRef(0);
|
|
48
|
+
const startSplitRef = useRef(0);
|
|
49
|
+
|
|
50
|
+
const handleMouseDown = useCallback((e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
|
|
54
|
+
setIsDragging(true);
|
|
55
|
+
startPositionRef.current = direction === DIRECTIONS.HORIZONTAL ? e.clientX : e.clientY;
|
|
56
|
+
startSplitRef.current = splitSize;
|
|
57
|
+
|
|
58
|
+
// Add global styles for dragging
|
|
59
|
+
const { body } = document;
|
|
60
|
+
body.style.cursor = direction === DIRECTIONS.HORIZONTAL ? 'col-resize' : 'row-resize';
|
|
61
|
+
body.style.userSelect = 'none';
|
|
62
|
+
body.style.pointerEvents = 'none';
|
|
63
|
+
|
|
64
|
+
// Prevent text selection during drag
|
|
65
|
+
document.onselectstart = () => false;
|
|
66
|
+
|
|
67
|
+
// Re-enable pointer events on the container
|
|
68
|
+
if (containerRef.current) {
|
|
69
|
+
containerRef.current.style.pointerEvents = 'auto';
|
|
70
|
+
}
|
|
71
|
+
}, [direction, splitSize]);
|
|
72
|
+
|
|
73
|
+
const handleMouseMove = useCallback((e) => {
|
|
74
|
+
if (!isDragging || !containerRef.current) return;
|
|
75
|
+
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
|
|
79
|
+
const container = containerRef.current;
|
|
80
|
+
const containerRect = container.getBoundingClientRect();
|
|
81
|
+
const isHorizontal = direction === DIRECTIONS.HORIZONTAL;
|
|
82
|
+
const containerSize = isHorizontal ? containerRect.width : containerRect.height;
|
|
83
|
+
|
|
84
|
+
if (containerSize === 0) return; // Avoid division by zero
|
|
85
|
+
|
|
86
|
+
const currentPosition = isHorizontal ? e.clientX : e.clientY;
|
|
87
|
+
const containerStart = isHorizontal ? containerRect.left : containerRect.top;
|
|
88
|
+
const relativePosition = currentPosition - containerStart;
|
|
89
|
+
|
|
90
|
+
// Calculate new split percentage
|
|
91
|
+
let newSplit = (relativePosition / containerSize) * 100;
|
|
92
|
+
|
|
93
|
+
// Apply constraints
|
|
94
|
+
const minPercent = (minSize / containerSize) * 100;
|
|
95
|
+
const maxPercent = maxSize
|
|
96
|
+
? ((containerSize - maxSize) / containerSize) * 100
|
|
97
|
+
: SPLIT_CONSTRAINTS.MAX_PERCENT;
|
|
98
|
+
|
|
99
|
+
// Clamp the values
|
|
100
|
+
const { MIN_PERCENT, MAX_PERCENT } = SPLIT_CONSTRAINTS;
|
|
101
|
+
newSplit = Math.max(MIN_PERCENT, Math.min(MAX_PERCENT, newSplit)); // Overall limits
|
|
102
|
+
newSplit = Math.max(minPercent, Math.min(100 - minPercent, newSplit)); // Min size limits
|
|
103
|
+
|
|
104
|
+
if (maxSize) {
|
|
105
|
+
newSplit = Math.min(maxPercent, newSplit);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setSplitSize(newSplit);
|
|
109
|
+
onSplitChange?.(newSplit); // Optional chaining
|
|
110
|
+
}, [isDragging, direction, minSize, maxSize, onSplitChange]);
|
|
111
|
+
|
|
112
|
+
const handleMouseUp = useCallback(() => {
|
|
113
|
+
if (!isDragging) return;
|
|
114
|
+
|
|
115
|
+
setIsDragging(false);
|
|
116
|
+
|
|
117
|
+
// Reset global styles
|
|
118
|
+
const { body } = document;
|
|
119
|
+
body.style.cursor = '';
|
|
120
|
+
body.style.userSelect = '';
|
|
121
|
+
body.style.pointerEvents = '';
|
|
122
|
+
|
|
123
|
+
// Reset text selection prevention
|
|
124
|
+
document.onselectstart = null;
|
|
125
|
+
|
|
126
|
+
containerRef.current?.style && (containerRef.current.style.pointerEvents = '');
|
|
127
|
+
}, [isDragging]);
|
|
128
|
+
|
|
129
|
+
// Touch support - convert touch events to mouse events
|
|
130
|
+
const handleTouchStart = useCallback((e) => {
|
|
131
|
+
const touch = e.touches?.[0];
|
|
132
|
+
if (!touch) return;
|
|
133
|
+
|
|
134
|
+
const mouseEvent = new MouseEvent('mousedown', {
|
|
135
|
+
clientX: touch.clientX,
|
|
136
|
+
clientY: touch.clientY
|
|
137
|
+
});
|
|
138
|
+
handleMouseDown(mouseEvent);
|
|
139
|
+
}, [handleMouseDown]);
|
|
140
|
+
|
|
141
|
+
const handleTouchMove = useCallback((e) => {
|
|
142
|
+
if (!isDragging) return;
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
|
|
145
|
+
const touch = e.touches?.[0];
|
|
146
|
+
if (!touch) return;
|
|
147
|
+
|
|
148
|
+
const mouseEvent = new MouseEvent('mousemove', {
|
|
149
|
+
clientX: touch.clientX,
|
|
150
|
+
clientY: touch.clientY
|
|
151
|
+
});
|
|
152
|
+
handleMouseMove(mouseEvent);
|
|
153
|
+
}, [isDragging, handleMouseMove]);
|
|
154
|
+
|
|
155
|
+
const handleTouchEnd = useCallback(() => {
|
|
156
|
+
handleMouseUp();
|
|
157
|
+
}, [handleMouseUp]);
|
|
158
|
+
|
|
159
|
+
// Event listeners
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (isDragging) {
|
|
162
|
+
document.addEventListener('mousemove', handleMouseMove, { passive: false });
|
|
163
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
164
|
+
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
165
|
+
document.addEventListener('touchend', handleTouchEnd);
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
169
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
170
|
+
document.removeEventListener('touchmove', handleTouchMove);
|
|
171
|
+
document.removeEventListener('touchend', handleTouchEnd);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
|
|
175
|
+
|
|
176
|
+
// Prevent text selection during drag
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (isDragging) {
|
|
179
|
+
// Named handlers to prevent text selection and dragging during resize
|
|
180
|
+
const selectStartHandler = (e) => {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
return false;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const dragStartHandler = (e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
document.addEventListener('selectstart', selectStartHandler);
|
|
191
|
+
document.addEventListener('dragstart', dragStartHandler);
|
|
192
|
+
|
|
193
|
+
return () => {
|
|
194
|
+
document.removeEventListener('selectstart', selectStartHandler);
|
|
195
|
+
document.removeEventListener('dragstart', dragStartHandler);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}, [isDragging]);
|
|
199
|
+
|
|
200
|
+
// Cleanup effect to reset global styles on unmount
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
return () => {
|
|
203
|
+
// Reset global body styles if component unmounts while dragging
|
|
204
|
+
if (isDragging) {
|
|
205
|
+
const { body } = document;
|
|
206
|
+
body.style.cursor = '';
|
|
207
|
+
body.style.userSelect = '';
|
|
208
|
+
body.style.pointerEvents = '';
|
|
209
|
+
|
|
210
|
+
// Reset container pointer events if ref is still available
|
|
211
|
+
if (containerRef.current) {
|
|
212
|
+
containerRef.current.style.pointerEvents = '';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}, [isDragging]);
|
|
217
|
+
|
|
218
|
+
// Generate CSS classes
|
|
219
|
+
const containerClasses = [
|
|
220
|
+
'split-container',
|
|
221
|
+
`split-container--${direction}`,
|
|
222
|
+
isDragging ? 'split-container--dragging' : '',
|
|
223
|
+
className
|
|
224
|
+
].filter(Boolean).join(' ');
|
|
225
|
+
|
|
226
|
+
// Generate CSS custom properties for dynamic sizing
|
|
227
|
+
const containerStyle = {
|
|
228
|
+
'--split-size': `${splitSize}%`,
|
|
229
|
+
'--remaining-size': `${100 - splitSize}%`
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div
|
|
234
|
+
ref={containerRef}
|
|
235
|
+
className={containerClasses}
|
|
236
|
+
style={containerStyle}
|
|
237
|
+
>
|
|
238
|
+
<CapRow className="split-container__panel split-container__panel--first">
|
|
239
|
+
{children?.[0]}
|
|
240
|
+
</CapRow>
|
|
241
|
+
|
|
242
|
+
<div
|
|
243
|
+
className="split-container__splitter"
|
|
244
|
+
onMouseDown={handleMouseDown}
|
|
245
|
+
onTouchStart={handleTouchStart}
|
|
246
|
+
role="separator"
|
|
247
|
+
tabIndex={0}
|
|
248
|
+
aria-label={`Resize ${direction === DIRECTIONS.HORIZONTAL ? 'horizontal' : 'vertical'} split`}
|
|
249
|
+
aria-orientation={direction === DIRECTIONS.HORIZONTAL ? 'vertical' : 'horizontal'}
|
|
250
|
+
aria-valuenow={Math.round(splitSize)}
|
|
251
|
+
aria-valuemin={SPLIT_CONSTRAINTS.MIN_PERCENT}
|
|
252
|
+
aria-valuemax={SPLIT_CONSTRAINTS.MAX_PERCENT}
|
|
253
|
+
>
|
|
254
|
+
<div className="split-container__splitter-handle">
|
|
255
|
+
<div className="split-container__splitter-line" />
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<CapRow className="split-container__panel split-container__panel--second">
|
|
260
|
+
{children?.[1]}
|
|
261
|
+
</CapRow>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
SplitContainer.propTypes = {
|
|
267
|
+
children: PropTypes.arrayOf(PropTypes.node).isRequired,
|
|
268
|
+
className: PropTypes.string,
|
|
269
|
+
defaultSplit: PropTypes.number,
|
|
270
|
+
minSize: PropTypes.number,
|
|
271
|
+
maxSize: PropTypes.number,
|
|
272
|
+
direction: PropTypes.oneOf(Object.values(DIRECTIONS)),
|
|
273
|
+
onSplitChange: PropTypes.func
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export default SplitContainer;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplitContainer Component Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the SplitContainer component that provides resizable split panels.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import SplitContainer from '../SplitContainer';
|
|
11
|
+
|
|
12
|
+
describe('SplitContainer', () => {
|
|
13
|
+
const defaultProps = {
|
|
14
|
+
children: [
|
|
15
|
+
<div key="left" data-testid="left-panel">Left Panel</div>,
|
|
16
|
+
<div key="right" data-testid="right-panel">Right Panel</div>
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Setup window.getComputedStyle mock - returns a CSSStyleDeclaration-like object
|
|
22
|
+
window.getComputedStyle = jest.fn(() => ({
|
|
23
|
+
getPropertyValue: (prop) => {
|
|
24
|
+
const values = {
|
|
25
|
+
'width': '800px',
|
|
26
|
+
'height': '600px',
|
|
27
|
+
};
|
|
28
|
+
return values[prop] || '0px';
|
|
29
|
+
},
|
|
30
|
+
width: '800px',
|
|
31
|
+
height: '600px',
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// Mock getBoundingClientRect
|
|
35
|
+
Element.prototype.getBoundingClientRect = jest.fn(() => ({
|
|
36
|
+
width: 800,
|
|
37
|
+
height: 600,
|
|
38
|
+
left: 0,
|
|
39
|
+
top: 0,
|
|
40
|
+
right: 800,
|
|
41
|
+
bottom: 600
|
|
42
|
+
}));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
// Clean up any global state
|
|
47
|
+
document.body.innerHTML = '';
|
|
48
|
+
|
|
49
|
+
// Reset any global mocks
|
|
50
|
+
if (document.onselectstart) {
|
|
51
|
+
document.onselectstart = null;
|
|
52
|
+
}
|
|
53
|
+
if (document.ondragstart) {
|
|
54
|
+
document.ondragstart = null;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders with default horizontal layout', () => {
|
|
59
|
+
render(<SplitContainer {...defaultProps} />);
|
|
60
|
+
|
|
61
|
+
expect(screen.getByTestId('left-panel')).toBeInTheDocument();
|
|
62
|
+
expect(screen.getByTestId('right-panel')).toBeInTheDocument();
|
|
63
|
+
|
|
64
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
65
|
+
expect(container).toHaveClass('split-container--horizontal');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders with vertical layout when specified', () => {
|
|
69
|
+
render(<SplitContainer {...defaultProps} direction="vertical" />);
|
|
70
|
+
|
|
71
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
72
|
+
expect(container).toHaveClass('split-container--vertical');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('applies default 50/50 split', () => {
|
|
76
|
+
render(<SplitContainer {...defaultProps} />);
|
|
77
|
+
|
|
78
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
79
|
+
|
|
80
|
+
// Check CSS custom properties are set correctly
|
|
81
|
+
expect(container.style.getPropertyValue('--split-size')).toBe('50%');
|
|
82
|
+
expect(container.style.getPropertyValue('--remaining-size')).toBe('50%');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('applies custom default split', () => {
|
|
86
|
+
render(<SplitContainer {...defaultProps} defaultSplit={30} />);
|
|
87
|
+
|
|
88
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
89
|
+
|
|
90
|
+
// Check CSS custom properties are set correctly
|
|
91
|
+
expect(container.style.getPropertyValue('--split-size')).toBe('30%');
|
|
92
|
+
expect(container.style.getPropertyValue('--remaining-size')).toBe('70%');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('renders splitter handle', () => {
|
|
96
|
+
render(<SplitContainer {...defaultProps} />);
|
|
97
|
+
|
|
98
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
99
|
+
const handle = document.querySelector('.split-container__splitter-handle');
|
|
100
|
+
const line = document.querySelector('.split-container__splitter-line');
|
|
101
|
+
|
|
102
|
+
expect(splitter).toBeInTheDocument();
|
|
103
|
+
expect(handle).toBeInTheDocument();
|
|
104
|
+
expect(line).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('handles mouse down on splitter', () => {
|
|
108
|
+
render(<SplitContainer {...defaultProps} />);
|
|
109
|
+
|
|
110
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
111
|
+
|
|
112
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
113
|
+
|
|
114
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
115
|
+
expect(container).toHaveClass('split-container--dragging');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles mouse move during drag', () => {
|
|
119
|
+
render(<SplitContainer {...defaultProps} />);
|
|
120
|
+
|
|
121
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
122
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
123
|
+
|
|
124
|
+
// Start drag
|
|
125
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
126
|
+
|
|
127
|
+
// Move mouse
|
|
128
|
+
fireEvent.mouseMove(document, { clientX: 300, clientY: 300 });
|
|
129
|
+
|
|
130
|
+
// Should update split position via CSS custom properties
|
|
131
|
+
const splitSize = container.style.getPropertyValue('--split-size');
|
|
132
|
+
expect(splitSize).toBeTruthy();
|
|
133
|
+
expect(splitSize).toMatch(/^\d+(\.\d+)?%$/); // Should be a percentage
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles mouse up to end drag', () => {
|
|
137
|
+
render(<SplitContainer {...defaultProps} />);
|
|
138
|
+
|
|
139
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
140
|
+
|
|
141
|
+
// Start and end drag
|
|
142
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
143
|
+
fireEvent.mouseUp(document);
|
|
144
|
+
|
|
145
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
146
|
+
expect(container).not.toHaveClass('split-container--dragging');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('handles touch events', () => {
|
|
150
|
+
render(<SplitContainer {...defaultProps} />);
|
|
151
|
+
|
|
152
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
153
|
+
|
|
154
|
+
// Touch start
|
|
155
|
+
fireEvent.touchStart(splitter, {
|
|
156
|
+
touches: [{ clientX: 400, clientY: 300 }]
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
160
|
+
expect(container).toHaveClass('split-container--dragging');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('respects minimum size constraints', () => {
|
|
164
|
+
render(<SplitContainer {...defaultProps} minSize={100} />);
|
|
165
|
+
|
|
166
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
167
|
+
|
|
168
|
+
// Try to drag to a very small size
|
|
169
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
170
|
+
fireEvent.mouseMove(document, { clientX: 50, clientY: 300 }); // Very small
|
|
171
|
+
|
|
172
|
+
const firstPanel = screen.getByTestId('left-panel').closest('.split-container__panel--first');
|
|
173
|
+
const container = firstPanel.closest('.split-container');
|
|
174
|
+
|
|
175
|
+
// Read the CSS variable directly from the inline style
|
|
176
|
+
const splitSizeValue = container.style.getPropertyValue('--split-size');
|
|
177
|
+
const parsedPercent = parseFloat(splitSizeValue.trim().replace('%', ''));
|
|
178
|
+
|
|
179
|
+
// Should respect minimum size (100px out of 800px = 12.5%)
|
|
180
|
+
expect(parsedPercent).toBeGreaterThanOrEqual(12.5);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('respects maximum size constraints', () => {
|
|
184
|
+
render(<SplitContainer {...defaultProps} maxSize={100} />);
|
|
185
|
+
|
|
186
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
187
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
188
|
+
|
|
189
|
+
// Try to drag to a very large size
|
|
190
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
191
|
+
fireEvent.mouseMove(document, { clientX: 750, clientY: 300 }); // Very large
|
|
192
|
+
|
|
193
|
+
const splitSize = container.style.getPropertyValue('--split-size');
|
|
194
|
+
const widthPercent = parseFloat(splitSize);
|
|
195
|
+
|
|
196
|
+
// Should respect maximum size constraint
|
|
197
|
+
expect(widthPercent).toBeLessThanOrEqual(90); // Should be clamped
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('calls onSplitChange when split changes', () => {
|
|
201
|
+
const onSplitChange = jest.fn();
|
|
202
|
+
render(<SplitContainer {...defaultProps} onSplitChange={onSplitChange} />);
|
|
203
|
+
|
|
204
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
205
|
+
|
|
206
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
207
|
+
fireEvent.mouseMove(document, { clientX: 300, clientY: 300 });
|
|
208
|
+
|
|
209
|
+
expect(onSplitChange).toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('applies custom className', () => {
|
|
213
|
+
render(<SplitContainer {...defaultProps} className="custom-split" />);
|
|
214
|
+
|
|
215
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
216
|
+
expect(container).toHaveClass('custom-split');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('handles vertical direction correctly', () => {
|
|
220
|
+
render(<SplitContainer {...defaultProps} direction="vertical" />);
|
|
221
|
+
|
|
222
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
223
|
+
|
|
224
|
+
// In vertical mode, CSS custom properties are still used the same way
|
|
225
|
+
expect(container.style.getPropertyValue('--split-size')).toBe('50%');
|
|
226
|
+
expect(container.style.getPropertyValue('--remaining-size')).toBe('50%');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('handles drag in vertical direction', () => {
|
|
230
|
+
render(<SplitContainer {...defaultProps} direction="vertical" />);
|
|
231
|
+
|
|
232
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
233
|
+
const container = screen.getByTestId('left-panel').closest('.split-container');
|
|
234
|
+
|
|
235
|
+
// Start vertical drag
|
|
236
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
237
|
+
fireEvent.mouseMove(document, { clientX: 400, clientY: 200 }); // Move vertically
|
|
238
|
+
|
|
239
|
+
// Should update split position via CSS custom properties
|
|
240
|
+
const splitSize = container.style.getPropertyValue('--split-size');
|
|
241
|
+
expect(splitSize).toBeTruthy();
|
|
242
|
+
expect(splitSize).toMatch(/^\d+(\.\d+)?%$/); // Should be a percentage
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('prevents text selection during drag', () => {
|
|
246
|
+
render(<SplitContainer {...defaultProps} />);
|
|
247
|
+
|
|
248
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
249
|
+
|
|
250
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
251
|
+
|
|
252
|
+
// Should set document.onselectstart to prevent selection
|
|
253
|
+
expect(document.onselectstart).toBeDefined();
|
|
254
|
+
expect(document.onselectstart()).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('cleans up event listeners on unmount', () => {
|
|
258
|
+
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
|
|
259
|
+
|
|
260
|
+
const { unmount } = render(<SplitContainer {...defaultProps} />);
|
|
261
|
+
|
|
262
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
263
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
264
|
+
|
|
265
|
+
unmount();
|
|
266
|
+
|
|
267
|
+
// Should clean up listeners
|
|
268
|
+
expect(removeEventListenerSpy).toHaveBeenCalled();
|
|
269
|
+
|
|
270
|
+
removeEventListenerSpy.mockRestore();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('handles edge case with zero container size', () => {
|
|
274
|
+
// Mock zero-size container
|
|
275
|
+
Element.prototype.getBoundingClientRect = jest.fn(() => ({
|
|
276
|
+
width: 0,
|
|
277
|
+
height: 0,
|
|
278
|
+
left: 0,
|
|
279
|
+
top: 0,
|
|
280
|
+
right: 0,
|
|
281
|
+
bottom: 0
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
render(<SplitContainer {...defaultProps} />);
|
|
285
|
+
|
|
286
|
+
const splitter = document.querySelector('.split-container__splitter');
|
|
287
|
+
|
|
288
|
+
// Should not crash with zero-size container
|
|
289
|
+
fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
|
|
290
|
+
fireEvent.mouseMove(document, { clientX: 300, clientY: 300 });
|
|
291
|
+
|
|
292
|
+
expect(screen.getByTestId('left-panel')).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|