@abstraks-dev/ui-library 2.2.1 → 2.4.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 +99 -0
- package/dist/__tests__/Footer.test.js +3 -3
- package/dist/__tests__/Modal.test.js +480 -0
- package/dist/__tests__/Prompt.test.js +211 -0
- package/dist/components/Modal.js +195 -0
- package/dist/index.js +16 -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,8 @@ import {
|
|
|
18
18
|
Header,
|
|
19
19
|
Hero,
|
|
20
20
|
Alert,
|
|
21
|
+
Modal,
|
|
22
|
+
Prompt,
|
|
21
23
|
} from '@abstraks-dev/ui-library';
|
|
22
24
|
import '@abstraks-dev/ui-library/dist/styles/main.css';
|
|
23
25
|
|
|
@@ -179,6 +181,103 @@ Alert messages with different types.
|
|
|
179
181
|
<Alert type="info" message="New features available!" />
|
|
180
182
|
```
|
|
181
183
|
|
|
184
|
+
#### Modal
|
|
185
|
+
|
|
186
|
+
Accessible modal dialog component with portal rendering.
|
|
187
|
+
|
|
188
|
+
```jsx
|
|
189
|
+
import { useState } from 'react';
|
|
190
|
+
import { Modal, Button } from '@abstraks-dev/ui-library';
|
|
191
|
+
|
|
192
|
+
function MyComponent() {
|
|
193
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<>
|
|
197
|
+
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
|
|
198
|
+
<Modal
|
|
199
|
+
isOpen={isOpen}
|
|
200
|
+
onClose={() => setIsOpen(false)}
|
|
201
|
+
title='Confirm Action'
|
|
202
|
+
size='md'
|
|
203
|
+
footer={
|
|
204
|
+
<>
|
|
205
|
+
<Button variant='ghost' onClick={() => setIsOpen(false)}>
|
|
206
|
+
Cancel
|
|
207
|
+
</Button>
|
|
208
|
+
<Button variant='primary' onClick={handleConfirm}>
|
|
209
|
+
Confirm
|
|
210
|
+
</Button>
|
|
211
|
+
</>
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
<p>Are you sure you want to proceed with this action?</p>
|
|
215
|
+
</Modal>
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Available sizes: `sm`, `md`, `lg`, `xl`, `full`
|
|
222
|
+
|
|
223
|
+
Props:
|
|
224
|
+
|
|
225
|
+
- `isOpen` - Controls modal visibility
|
|
226
|
+
- `onClose` - Callback when modal requests to close
|
|
227
|
+
- `title` - Modal title
|
|
228
|
+
- `children` - Modal body content
|
|
229
|
+
- `footer` - Custom footer content (typically buttons)
|
|
230
|
+
- `size` - Modal width size (default: `md`)
|
|
231
|
+
- `closeOnBackdropClick` - Allow closing by clicking backdrop (default: `true`)
|
|
232
|
+
- `closeOnEscape` - Allow closing with Escape key (default: `true`)
|
|
233
|
+
- `showCloseButton` - Show X close button in header (default: `true`)
|
|
234
|
+
- `preventBodyScroll` - Lock body scroll when open (default: `true`)
|
|
235
|
+
|
|
236
|
+
#### Prompt
|
|
237
|
+
|
|
238
|
+
Lightweight confirmation dialog for user actions like deletes or destructive operations.
|
|
239
|
+
|
|
240
|
+
```jsx
|
|
241
|
+
import { useState } from 'react';
|
|
242
|
+
import { Prompt, Button } from '@abstraks-dev/ui-library';
|
|
243
|
+
|
|
244
|
+
function MyComponent() {
|
|
245
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
246
|
+
|
|
247
|
+
const handleDelete = () => {
|
|
248
|
+
// Perform delete action
|
|
249
|
+
console.log('Item deleted');
|
|
250
|
+
setIsOpen(false);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<>
|
|
255
|
+
<Button variant='error' onClick={() => setIsOpen(true)}>
|
|
256
|
+
Delete Item
|
|
257
|
+
</Button>
|
|
258
|
+
<Prompt
|
|
259
|
+
message='Are you sure you want to delete this item? This action cannot be undone.'
|
|
260
|
+
open={isOpen}
|
|
261
|
+
onConfirm={handleDelete}
|
|
262
|
+
onCancel={() => setIsOpen(false)}
|
|
263
|
+
confirmText='Delete'
|
|
264
|
+
cancelText='Cancel'
|
|
265
|
+
/>
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Props:
|
|
272
|
+
|
|
273
|
+
- `message` **(required)** - The confirmation message to display
|
|
274
|
+
- `open` - Controls prompt visibility (default: `false`)
|
|
275
|
+
- `onConfirm` - Callback when user clicks confirm button
|
|
276
|
+
- `onCancel` - Callback when user clicks cancel button
|
|
277
|
+
- `confirmText` - Text for confirm button (default: `'Yes'`)
|
|
278
|
+
- `cancelText` - Text for cancel button (default: `'Cancel'`)
|
|
279
|
+
- `showCancel` - Whether to show cancel button (default: `true`)
|
|
280
|
+
|
|
182
281
|
#### Error
|
|
183
282
|
|
|
184
283
|
Error message component.
|
|
@@ -35,7 +35,7 @@ describe('Footer', () => {
|
|
|
35
35
|
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Footer.default, {
|
|
36
36
|
hasAuth: false
|
|
37
37
|
}));
|
|
38
|
-
expect(_react2.screen.getByText(/©
|
|
38
|
+
expect(_react2.screen.getByText(/© 2026 Abstraks. All rights reserved./)).toBeInTheDocument();
|
|
39
39
|
});
|
|
40
40
|
it('renders auth menu when hasAuth is true on mobile', () => {
|
|
41
41
|
// Mock mobile screen size so auth menu shows
|
|
@@ -110,7 +110,7 @@ describe('Footer', () => {
|
|
|
110
110
|
hasAuth: true,
|
|
111
111
|
authMenu: /*#__PURE__*/_react.default.createElement("li", null, "Menu")
|
|
112
112
|
}));
|
|
113
|
-
expect(_react2.screen.queryByText(/©
|
|
113
|
+
expect(_react2.screen.queryByText(/© 2026 Abstraks. All rights reserved./)).not.toBeInTheDocument();
|
|
114
114
|
});
|
|
115
115
|
it('renders with copyright when hasAuth is true but navigation props are provided', () => {
|
|
116
116
|
// Mock desktop screen size to test desktop behavior with navigation props
|
|
@@ -122,7 +122,7 @@ describe('Footer', () => {
|
|
|
122
122
|
authMenu: /*#__PURE__*/_react.default.createElement("li", null, "Menu"),
|
|
123
123
|
navigationOne: /*#__PURE__*/_react.default.createElement("li", null, "Nav One")
|
|
124
124
|
}));
|
|
125
|
-
expect(_react2.screen.queryByText(/©
|
|
125
|
+
expect(_react2.screen.queryByText(/© 2026 Abstraks. All rights reserved./)).toBeInTheDocument();
|
|
126
126
|
});
|
|
127
127
|
it('renders with custom className', () => {
|
|
128
128
|
(0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Footer.default, {
|
|
@@ -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
|
+
});
|