@abstraks-dev/ui-library 2.2.0 → 2.3.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.
- package/README.md +53 -0
- package/dist/__tests__/Modal.test.js +480 -0
- package/dist/components/Modal.js +195 -0
- package/dist/index.js +56 -0
- package/dist/styles/main.css +218 -0
- package/dist/styles/main.css.map +1 -1
- package/dist/styles/modal.css +332 -0
- package/dist/styles/modal.css.map +1 -0
- package/dist/styles/modal.scss +253 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Header,
|
|
19
19
|
Hero,
|
|
20
20
|
Alert,
|
|
21
|
+
Modal,
|
|
21
22
|
} from '@abstraks-dev/ui-library';
|
|
22
23
|
import '@abstraks-dev/ui-library/dist/styles/main.css';
|
|
23
24
|
|
|
@@ -179,6 +180,58 @@ Alert messages with different types.
|
|
|
179
180
|
<Alert type="info" message="New features available!" />
|
|
180
181
|
```
|
|
181
182
|
|
|
183
|
+
#### Modal
|
|
184
|
+
|
|
185
|
+
Accessible modal dialog component with portal rendering.
|
|
186
|
+
|
|
187
|
+
```jsx
|
|
188
|
+
import { useState } from 'react';
|
|
189
|
+
import { Modal, Button } from '@abstraks-dev/ui-library';
|
|
190
|
+
|
|
191
|
+
function MyComponent() {
|
|
192
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<>
|
|
196
|
+
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
|
|
197
|
+
<Modal
|
|
198
|
+
isOpen={isOpen}
|
|
199
|
+
onClose={() => setIsOpen(false)}
|
|
200
|
+
title='Confirm Action'
|
|
201
|
+
size='md'
|
|
202
|
+
footer={
|
|
203
|
+
<>
|
|
204
|
+
<Button variant='ghost' onClick={() => setIsOpen(false)}>
|
|
205
|
+
Cancel
|
|
206
|
+
</Button>
|
|
207
|
+
<Button variant='primary' onClick={handleConfirm}>
|
|
208
|
+
Confirm
|
|
209
|
+
</Button>
|
|
210
|
+
</>
|
|
211
|
+
}
|
|
212
|
+
>
|
|
213
|
+
<p>Are you sure you want to proceed with this action?</p>
|
|
214
|
+
</Modal>
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Available sizes: `sm`, `md`, `lg`, `xl`, `full`
|
|
221
|
+
|
|
222
|
+
Props:
|
|
223
|
+
|
|
224
|
+
- `isOpen` - Controls modal visibility
|
|
225
|
+
- `onClose` - Callback when modal requests to close
|
|
226
|
+
- `title` - Modal title
|
|
227
|
+
- `children` - Modal body content
|
|
228
|
+
- `footer` - Custom footer content (typically buttons)
|
|
229
|
+
- `size` - Modal width size (default: `md`)
|
|
230
|
+
- `closeOnBackdropClick` - Allow closing by clicking backdrop (default: `true`)
|
|
231
|
+
- `closeOnEscape` - Allow closing with Escape key (default: `true`)
|
|
232
|
+
- `showCloseButton` - Show X close button in header (default: `true`)
|
|
233
|
+
- `preventBodyScroll` - Lock body scroll when open (default: `true`)
|
|
234
|
+
|
|
182
235
|
#### Error
|
|
183
236
|
|
|
184
237
|
Error message component.
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _react = _interopRequireDefault(require("react"));
|
|
4
|
+
var _react2 = require("@testing-library/react");
|
|
5
|
+
var _userEvent = _interopRequireDefault(require("@testing-library/user-event"));
|
|
6
|
+
var _Modal = require("../components/Modal");
|
|
7
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
9
|
+
// Mock createPortal to render in the same container for testing
|
|
10
|
+
jest.mock('react-dom', () => ({
|
|
11
|
+
...jest.requireActual('react-dom'),
|
|
12
|
+
createPortal: node => node
|
|
13
|
+
}));
|
|
14
|
+
describe('Modal Component', () => {
|
|
15
|
+
// Helper function to set up tests
|
|
16
|
+
const defaultProps = {
|
|
17
|
+
isOpen: true,
|
|
18
|
+
onClose: jest.fn(),
|
|
19
|
+
title: 'Test Modal',
|
|
20
|
+
children: /*#__PURE__*/_react.default.createElement("p", null, "Modal content")
|
|
21
|
+
};
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Clean up body styles
|
|
27
|
+
document.body.style.overflow = '';
|
|
28
|
+
document.body.style.paddingRight = '';
|
|
29
|
+
});
|
|
30
|
+
describe('Rendering', () => {
|
|
31
|
+
test('renders modal when isOpen is true', () => {
|
|
32
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
33
|
+
expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
|
|
34
|
+
expect(_react2.screen.getByText('Test Modal')).toBeInTheDocument();
|
|
35
|
+
expect(_react2.screen.getByText('Modal content')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
test('does not render when isOpen is false', () => {
|
|
38
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
39
|
+
isOpen: false
|
|
40
|
+
})));
|
|
41
|
+
expect(_react2.screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
test('renders with custom className', () => {
|
|
44
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
45
|
+
className: "custom-modal"
|
|
46
|
+
})));
|
|
47
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
48
|
+
expect(modal).toHaveClass('modal', 'custom-modal');
|
|
49
|
+
});
|
|
50
|
+
test('renders without title', () => {
|
|
51
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
52
|
+
title: undefined
|
|
53
|
+
})));
|
|
54
|
+
expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
|
|
55
|
+
expect(_react2.screen.queryByText('Test Modal')).not.toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
test('renders with footer content', () => {
|
|
58
|
+
const footer = /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("button", null, "Cancel"), /*#__PURE__*/_react.default.createElement("button", null, "Confirm"));
|
|
59
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
60
|
+
footer: footer
|
|
61
|
+
})));
|
|
62
|
+
expect(_react2.screen.getByText('Cancel')).toBeInTheDocument();
|
|
63
|
+
expect(_react2.screen.getByText('Confirm')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
test('renders with custom header content', () => {
|
|
66
|
+
const headerContent = /*#__PURE__*/_react.default.createElement("div", null, "Custom Header");
|
|
67
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
68
|
+
headerContent: headerContent
|
|
69
|
+
})));
|
|
70
|
+
expect(_react2.screen.getByText('Custom Header')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
test('renders close button by default', () => {
|
|
73
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
74
|
+
const closeButton = _react2.screen.getByLabelText('Close modal');
|
|
75
|
+
expect(closeButton).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
test('does not render close button when showCloseButton is false', () => {
|
|
78
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
79
|
+
showCloseButton: false
|
|
80
|
+
})));
|
|
81
|
+
expect(_react2.screen.queryByLabelText('Close modal')).not.toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('Size Variants', () => {
|
|
85
|
+
test('applies default medium size class', () => {
|
|
86
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
87
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
88
|
+
expect(modal).toHaveClass('modal--md');
|
|
89
|
+
});
|
|
90
|
+
test('applies small size class', () => {
|
|
91
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
92
|
+
size: "sm"
|
|
93
|
+
})));
|
|
94
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
95
|
+
expect(modal).toHaveClass('modal--sm');
|
|
96
|
+
});
|
|
97
|
+
test('applies large size class', () => {
|
|
98
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
99
|
+
size: "lg"
|
|
100
|
+
})));
|
|
101
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
102
|
+
expect(modal).toHaveClass('modal--lg');
|
|
103
|
+
});
|
|
104
|
+
test('applies extra large size class', () => {
|
|
105
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
106
|
+
size: "xl"
|
|
107
|
+
})));
|
|
108
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
109
|
+
expect(modal).toHaveClass('modal--xl');
|
|
110
|
+
});
|
|
111
|
+
test('applies full size class', () => {
|
|
112
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
113
|
+
size: "full"
|
|
114
|
+
})));
|
|
115
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
116
|
+
expect(modal).toHaveClass('modal--full');
|
|
117
|
+
});
|
|
118
|
+
test('applies centered class by default', () => {
|
|
119
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
120
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
121
|
+
expect(modal).toHaveClass('modal--centered');
|
|
122
|
+
});
|
|
123
|
+
test('does not apply centered class when centered is false', () => {
|
|
124
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
125
|
+
centered: false
|
|
126
|
+
})));
|
|
127
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
128
|
+
expect(modal).not.toHaveClass('modal--centered');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('Accessibility', () => {
|
|
132
|
+
test('has correct ARIA attributes', () => {
|
|
133
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
134
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
135
|
+
expect(modal).toHaveAttribute('aria-modal', 'true');
|
|
136
|
+
expect(modal).toHaveAttribute('aria-label', 'Test Modal');
|
|
137
|
+
});
|
|
138
|
+
test('uses aria-labelledby when title is provided', () => {
|
|
139
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
140
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
141
|
+
const titleId = modal.getAttribute('aria-labelledby');
|
|
142
|
+
expect(titleId).toBeTruthy();
|
|
143
|
+
const title = document.getElementById(titleId);
|
|
144
|
+
expect(title).toHaveTextContent('Test Modal');
|
|
145
|
+
});
|
|
146
|
+
test('uses custom aria-label when provided', () => {
|
|
147
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
148
|
+
"aria-label": "Custom label"
|
|
149
|
+
})));
|
|
150
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
151
|
+
expect(modal).toHaveAttribute('aria-label', 'Custom label');
|
|
152
|
+
});
|
|
153
|
+
test('uses custom aria-describedby when provided', () => {
|
|
154
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
155
|
+
"aria-describedby": "custom-description"
|
|
156
|
+
})));
|
|
157
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
158
|
+
expect(modal).toHaveAttribute('aria-describedby', 'custom-description');
|
|
159
|
+
});
|
|
160
|
+
test('supports custom role', () => {
|
|
161
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
162
|
+
role: "alertdialog"
|
|
163
|
+
})));
|
|
164
|
+
expect(_react2.screen.getByRole('alertdialog')).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('Close Behavior', () => {
|
|
168
|
+
test('calls onClose when close button is clicked', async () => {
|
|
169
|
+
const onClose = jest.fn();
|
|
170
|
+
const user = _userEvent.default.setup();
|
|
171
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
172
|
+
onClose: onClose
|
|
173
|
+
})));
|
|
174
|
+
const closeButton = _react2.screen.getByLabelText('Close modal');
|
|
175
|
+
await user.click(closeButton);
|
|
176
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
test('calls onClose when Escape key is pressed', () => {
|
|
179
|
+
const onClose = jest.fn();
|
|
180
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
181
|
+
onClose: onClose
|
|
182
|
+
})));
|
|
183
|
+
_react2.fireEvent.keyDown(document, {
|
|
184
|
+
key: 'Escape'
|
|
185
|
+
});
|
|
186
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
test('does not call onClose on Escape when closeOnEscape is false', () => {
|
|
189
|
+
const onClose = jest.fn();
|
|
190
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
191
|
+
onClose: onClose,
|
|
192
|
+
closeOnEscape: false
|
|
193
|
+
})));
|
|
194
|
+
_react2.fireEvent.keyDown(document, {
|
|
195
|
+
key: 'Escape'
|
|
196
|
+
});
|
|
197
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
test('calls onClose when backdrop is clicked', async () => {
|
|
200
|
+
const onClose = jest.fn();
|
|
201
|
+
const user = _userEvent.default.setup();
|
|
202
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
203
|
+
onClose: onClose
|
|
204
|
+
})));
|
|
205
|
+
const overlay = _react2.screen.getByRole('dialog').parentElement;
|
|
206
|
+
await user.click(overlay);
|
|
207
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
208
|
+
});
|
|
209
|
+
test('does not call onClose when backdrop is clicked and closeOnBackdropClick is false', async () => {
|
|
210
|
+
const onClose = jest.fn();
|
|
211
|
+
const user = _userEvent.default.setup();
|
|
212
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
213
|
+
onClose: onClose,
|
|
214
|
+
closeOnBackdropClick: false
|
|
215
|
+
})));
|
|
216
|
+
const overlay = _react2.screen.getByRole('dialog').parentElement;
|
|
217
|
+
await user.click(overlay);
|
|
218
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
test('does not call onClose when modal content is clicked', async () => {
|
|
221
|
+
const onClose = jest.fn();
|
|
222
|
+
const user = _userEvent.default.setup();
|
|
223
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
224
|
+
onClose: onClose
|
|
225
|
+
})));
|
|
226
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
227
|
+
await user.click(modal);
|
|
228
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
test('handles missing onClose gracefully', () => {
|
|
231
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
232
|
+
onClose: undefined
|
|
233
|
+
})));
|
|
234
|
+
expect(() => {
|
|
235
|
+
_react2.fireEvent.keyDown(document, {
|
|
236
|
+
key: 'Escape'
|
|
237
|
+
});
|
|
238
|
+
}).not.toThrow();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('Body Scroll Lock', () => {
|
|
242
|
+
test('locks body scroll when modal opens with preventBodyScroll true', () => {
|
|
243
|
+
const {
|
|
244
|
+
rerender
|
|
245
|
+
} = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
246
|
+
isOpen: false,
|
|
247
|
+
preventBodyScroll: true
|
|
248
|
+
})));
|
|
249
|
+
expect(document.body.style.overflow).not.toBe('hidden');
|
|
250
|
+
rerender(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
251
|
+
isOpen: true,
|
|
252
|
+
preventBodyScroll: true
|
|
253
|
+
})));
|
|
254
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
255
|
+
});
|
|
256
|
+
test('restores body scroll when modal closes', () => {
|
|
257
|
+
const originalOverflow = document.body.style.overflow;
|
|
258
|
+
const {
|
|
259
|
+
rerender
|
|
260
|
+
} = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
261
|
+
isOpen: true,
|
|
262
|
+
preventBodyScroll: true
|
|
263
|
+
})));
|
|
264
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
265
|
+
rerender(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
266
|
+
isOpen: false,
|
|
267
|
+
preventBodyScroll: true
|
|
268
|
+
})));
|
|
269
|
+
expect(document.body.style.overflow).toBe(originalOverflow);
|
|
270
|
+
});
|
|
271
|
+
test('does not lock body scroll when preventBodyScroll is false', () => {
|
|
272
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
273
|
+
isOpen: true,
|
|
274
|
+
preventBodyScroll: false
|
|
275
|
+
})));
|
|
276
|
+
expect(document.body.style.overflow).not.toBe('hidden');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe('Focus Management', () => {
|
|
280
|
+
test('stores and restores focus when modal closes', async () => {
|
|
281
|
+
const button = document.createElement('button');
|
|
282
|
+
document.body.appendChild(button);
|
|
283
|
+
button.focus();
|
|
284
|
+
expect(document.activeElement).toBe(button);
|
|
285
|
+
const {
|
|
286
|
+
rerender
|
|
287
|
+
} = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
288
|
+
isOpen: true
|
|
289
|
+
})));
|
|
290
|
+
rerender(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
291
|
+
isOpen: false
|
|
292
|
+
})));
|
|
293
|
+
await (0, _react2.waitFor)(() => {
|
|
294
|
+
expect(document.activeElement).toBe(button);
|
|
295
|
+
});
|
|
296
|
+
document.body.removeChild(button);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
describe('Keyboard Events', () => {
|
|
300
|
+
test('ignores non-Escape key presses', () => {
|
|
301
|
+
const onClose = jest.fn();
|
|
302
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
303
|
+
onClose: onClose
|
|
304
|
+
})));
|
|
305
|
+
_react2.fireEvent.keyDown(document, {
|
|
306
|
+
key: 'Enter'
|
|
307
|
+
});
|
|
308
|
+
_react2.fireEvent.keyDown(document, {
|
|
309
|
+
key: 'Tab'
|
|
310
|
+
});
|
|
311
|
+
_react2.fireEvent.keyDown(document, {
|
|
312
|
+
key: 'Space'
|
|
313
|
+
});
|
|
314
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
test('only closes when modal is open', () => {
|
|
317
|
+
const onClose = jest.fn();
|
|
318
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
319
|
+
isOpen: false,
|
|
320
|
+
onClose: onClose
|
|
321
|
+
})));
|
|
322
|
+
_react2.fireEvent.keyDown(document, {
|
|
323
|
+
key: 'Escape'
|
|
324
|
+
});
|
|
325
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
describe('Custom Props', () => {
|
|
329
|
+
test('passes through additional props to modal element', () => {
|
|
330
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
331
|
+
"data-testid": "custom-modal",
|
|
332
|
+
"data-custom": "test-value"
|
|
333
|
+
})));
|
|
334
|
+
const modal = _react2.screen.getByTestId('custom-modal');
|
|
335
|
+
expect(modal).toHaveAttribute('data-custom', 'test-value');
|
|
336
|
+
});
|
|
337
|
+
test('uses custom component name', () => {
|
|
338
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
339
|
+
componentName: "custom-modal"
|
|
340
|
+
})));
|
|
341
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
342
|
+
expect(modal).toHaveClass('custom-modal');
|
|
343
|
+
});
|
|
344
|
+
test('uses custom id', () => {
|
|
345
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
346
|
+
id: "custom-id"
|
|
347
|
+
})));
|
|
348
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
349
|
+
expect(modal).toHaveAttribute('id', 'custom-id');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
describe('Header Variations', () => {
|
|
353
|
+
test('renders header with title and close button', () => {
|
|
354
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps));
|
|
355
|
+
expect(_react2.screen.getByText('Test Modal')).toBeInTheDocument();
|
|
356
|
+
expect(_react2.screen.getByLabelText('Close modal')).toBeInTheDocument();
|
|
357
|
+
});
|
|
358
|
+
test('renders header with only close button when no title', () => {
|
|
359
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
360
|
+
title: undefined
|
|
361
|
+
})));
|
|
362
|
+
expect(_react2.screen.getByLabelText('Close modal')).toBeInTheDocument();
|
|
363
|
+
expect(_react2.screen.getByRole('dialog').querySelector('.modal__header')).toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
test('renders custom header content', () => {
|
|
366
|
+
const headerContent = /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("h2", null, "Custom Title"), /*#__PURE__*/_react.default.createElement("p", null, "Subtitle"));
|
|
367
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
368
|
+
headerContent: headerContent,
|
|
369
|
+
title: undefined
|
|
370
|
+
})));
|
|
371
|
+
expect(_react2.screen.getByText('Custom Title')).toBeInTheDocument();
|
|
372
|
+
expect(_react2.screen.getByText('Subtitle')).toBeInTheDocument();
|
|
373
|
+
});
|
|
374
|
+
test('does not render header section when no title, headerContent, or showCloseButton', () => {
|
|
375
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
376
|
+
title: undefined,
|
|
377
|
+
headerContent: undefined,
|
|
378
|
+
showCloseButton: false
|
|
379
|
+
})));
|
|
380
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
381
|
+
expect(modal.querySelector('.modal__header')).not.toBeInTheDocument();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
describe('Footer Variations', () => {
|
|
385
|
+
test('renders footer when provided', () => {
|
|
386
|
+
const footer = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("button", null, "Cancel"), /*#__PURE__*/_react.default.createElement("button", null, "Submit"));
|
|
387
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
388
|
+
footer: footer
|
|
389
|
+
})));
|
|
390
|
+
const modalFooter = _react2.screen.getByRole('dialog').querySelector('.modal__footer');
|
|
391
|
+
expect(modalFooter).toBeInTheDocument();
|
|
392
|
+
expect(_react2.screen.getByText('Cancel')).toBeInTheDocument();
|
|
393
|
+
expect(_react2.screen.getByText('Submit')).toBeInTheDocument();
|
|
394
|
+
});
|
|
395
|
+
test('does not render footer when not provided', () => {
|
|
396
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
397
|
+
footer: undefined
|
|
398
|
+
})));
|
|
399
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
400
|
+
expect(modal.querySelector('.modal__footer')).not.toBeInTheDocument();
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
describe('Content Rendering', () => {
|
|
404
|
+
test('renders simple text children', () => {
|
|
405
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps, "Simple text content"));
|
|
406
|
+
expect(_react2.screen.getByText('Simple text content')).toBeInTheDocument();
|
|
407
|
+
});
|
|
408
|
+
test('renders complex nested children', () => {
|
|
409
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, defaultProps, /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("p", null, "Paragraph 1"), /*#__PURE__*/_react.default.createElement("p", null, "Paragraph 2"), /*#__PURE__*/_react.default.createElement("ul", null, /*#__PURE__*/_react.default.createElement("li", null, "Item 1"), /*#__PURE__*/_react.default.createElement("li", null, "Item 2")))));
|
|
410
|
+
expect(_react2.screen.getByText('Paragraph 1')).toBeInTheDocument();
|
|
411
|
+
expect(_react2.screen.getByText('Paragraph 2')).toBeInTheDocument();
|
|
412
|
+
expect(_react2.screen.getByText('Item 1')).toBeInTheDocument();
|
|
413
|
+
expect(_react2.screen.getByText('Item 2')).toBeInTheDocument();
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
describe('Event Handling', () => {
|
|
417
|
+
test('prevents event propagation on Escape key', () => {
|
|
418
|
+
const onClose = jest.fn();
|
|
419
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
420
|
+
onClose: onClose
|
|
421
|
+
})));
|
|
422
|
+
const event = new KeyboardEvent('keydown', {
|
|
423
|
+
key: 'Escape',
|
|
424
|
+
bubbles: true,
|
|
425
|
+
cancelable: true
|
|
426
|
+
});
|
|
427
|
+
document.dispatchEvent(event);
|
|
428
|
+
expect(event.defaultPrevented).toBe(true);
|
|
429
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
test('backdrop click only triggers on overlay, not children', async () => {
|
|
432
|
+
const onClose = jest.fn();
|
|
433
|
+
const user = _userEvent.default.setup();
|
|
434
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
435
|
+
onClose: onClose
|
|
436
|
+
})));
|
|
437
|
+
|
|
438
|
+
// Click on modal content
|
|
439
|
+
const modalBody = _react2.screen.getByText('Modal content');
|
|
440
|
+
await user.click(modalBody);
|
|
441
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
442
|
+
|
|
443
|
+
// Click on overlay (parent of modal)
|
|
444
|
+
const overlay = _react2.screen.getByRole('dialog').parentElement;
|
|
445
|
+
await user.click(overlay);
|
|
446
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
describe('Edge Cases', () => {
|
|
450
|
+
test('handles rapid open/close toggles', () => {
|
|
451
|
+
const {
|
|
452
|
+
rerender
|
|
453
|
+
} = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
454
|
+
isOpen: true
|
|
455
|
+
})));
|
|
456
|
+
expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
|
|
457
|
+
rerender(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
458
|
+
isOpen: false
|
|
459
|
+
})));
|
|
460
|
+
expect(_react2.screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
461
|
+
rerender(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
462
|
+
isOpen: true
|
|
463
|
+
})));
|
|
464
|
+
expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
|
|
465
|
+
});
|
|
466
|
+
test('handles null children gracefully', () => {
|
|
467
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
468
|
+
children: null
|
|
469
|
+
})));
|
|
470
|
+
const modal = _react2.screen.getByRole('dialog');
|
|
471
|
+
expect(modal).toBeInTheDocument();
|
|
472
|
+
});
|
|
473
|
+
test('handles undefined title gracefully', () => {
|
|
474
|
+
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Modal.Modal, _extends({}, defaultProps, {
|
|
475
|
+
title: undefined
|
|
476
|
+
})));
|
|
477
|
+
expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|