@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 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
+ });