dhre-component-lib 0.0.9 → 0.1.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/package.json +3 -2
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/Avatar/Avatar.test.tsx +53 -0
- package/src/components/Avatar/Avatar.tsx +28 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.module.scss +14 -0
- package/src/components/Badge/Badge.test.tsx +59 -0
- package/src/components/Badge/Badge.tsx +25 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BreadCrumb/BreadCrumb.test.tsx +90 -0
- package/src/components/BreadCrumb/BreadCrumb.tsx +45 -0
- package/src/components/BreadCrumb/Breadcrumb.module.scss +20 -0
- package/src/components/BreadCrumb/index.ts +1 -0
- package/src/components/Button/Button.module.scss +66 -0
- package/src/components/Button/Button.test.tsx +49 -0
- package/src/components/Button/Button.tsx +29 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.test.tsx +93 -0
- package/src/components/Checkbox/Checkbox.tsx +35 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CircularProgress/CircularProgress.module.scss +19 -0
- package/src/components/CircularProgress/CircularProgress.test.tsx +39 -0
- package/src/components/CircularProgress/CircularProgress.tsx +37 -0
- package/src/components/CircularProgress/index.ts +1 -0
- package/src/components/Divider/Divider.test.tsx +44 -0
- package/src/components/Divider/Divider.tsx +24 -0
- package/src/components/Divider/index.ts +1 -0
- package/src/components/Enum.ts +19 -0
- package/src/components/InputTextField/InputTextField.test.tsx +118 -0
- package/src/components/InputTextField/InputTextField.tsx +48 -0
- package/src/components/InputTextField/index.ts +1 -0
- package/src/components/Link/Link.test.tsx +55 -0
- package/src/components/Link/Link.tsx +33 -0
- package/src/components/Link/index.ts +1 -0
- package/src/components/Map/Directions.tsx +36 -0
- package/src/components/Map/GoogleMap.module.scss +5 -0
- package/src/components/Map/GoogleMap.tsx +186 -0
- package/src/components/Map/GoogleMapsLoader.tsx +12 -0
- package/src/components/Map/index.ts +2 -0
- package/src/components/Modal/Modal.module.scss +26 -0
- package/src/components/Modal/Modal.test.tsx +74 -0
- package/src/components/Modal/Modal.tsx +39 -0
- package/src/components/Modal/index.ts +1 -0
- package/src/components/Notification/Notification.module.scss +20 -0
- package/src/components/Notification/Notification.test.tsx +53 -0
- package/src/components/Notification/Notification.tsx +42 -0
- package/src/components/Notification/index.ts +1 -0
- package/src/components/OtpInput/OtpInput.module.scss +49 -0
- package/src/components/OtpInput/OtpInput.test.tsx +53 -0
- package/src/components/OtpInput/OtpInput.tsx +137 -0
- package/src/components/OtpInput/index.ts +1 -0
- package/src/components/PdfView/PdfView.module.scss +69 -0
- package/src/components/PdfView/PdfView.test.tsx +52 -0
- package/src/components/PdfView/PdfView.tsx +116 -0
- package/src/components/PdfView/index.ts +1 -0
- package/src/components/Progress/Progress.test.tsx +43 -0
- package/src/components/Progress/Progress.tsx +35 -0
- package/src/components/Progress/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.test.tsx +56 -0
- package/src/components/RadioButton/RadioButton.tsx +43 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/Switch/Switch.test.tsx +83 -0
- package/src/components/Switch/Switch.tsx +38 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tag/Tag.css +14 -0
- package/src/components/Tag/Tag.test.tsx +61 -0
- package/src/components/Tag/Tag.tsx +19 -0
- package/src/components/Tag/index.ts +1 -0
- package/src/components/Tooltip/Tooltip.module.scss +37 -0
- package/src/components/Tooltip/Tooltip.test.tsx +68 -0
- package/src/components/Tooltip/Tooltip.tsx +38 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/index.ts +15 -0
- package/src/index.ts +1 -0
- package/src/theme/Typography/typography.scss +117 -0
- package/src/theme/colors/colors.scss +22 -0
- package/src/theme/colors.ts +3 -0
- package/src/typings.d.ts +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.modalOverlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
background: rgba(0, 0, 0, 0.5);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 1000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.modalContent {
|
|
15
|
+
background: white;
|
|
16
|
+
position: relative;
|
|
17
|
+
width: auto;
|
|
18
|
+
height: auto;
|
|
19
|
+
max-width: 80vw;
|
|
20
|
+
max-height: 80vh;
|
|
21
|
+
overflow: scroll;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.modalContent::-webkit-scrollbar {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import Modal from './Modal';
|
|
4
|
+
|
|
5
|
+
describe('Modal Component', () => {
|
|
6
|
+
const onCloseMock = jest.fn();
|
|
7
|
+
const modalContentText = 'This is the modal content';
|
|
8
|
+
|
|
9
|
+
const renderModal = (
|
|
10
|
+
isOpen: boolean,
|
|
11
|
+
modalOverlayClassname?: string,
|
|
12
|
+
modalContentClassname?: string
|
|
13
|
+
) => {
|
|
14
|
+
render(
|
|
15
|
+
<Modal
|
|
16
|
+
isOpen={isOpen}
|
|
17
|
+
onClose={onCloseMock}
|
|
18
|
+
modalOverlayClassname={modalOverlayClassname}
|
|
19
|
+
modalContentClassname={modalContentClassname}
|
|
20
|
+
>
|
|
21
|
+
<div>{modalContentText}</div>
|
|
22
|
+
</Modal>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders the modal when isOpen is true', () => {
|
|
31
|
+
renderModal(true);
|
|
32
|
+
expect(screen.getByText(modalContentText)).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not render the modal when isOpen is false', () => {
|
|
36
|
+
renderModal(false);
|
|
37
|
+
expect(screen.queryByText(modalContentText)).not.toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('calls onClose when clicking on the overlay', () => {
|
|
41
|
+
renderModal(true);
|
|
42
|
+
const overlay = screen.getByText(modalContentText).closest('button');
|
|
43
|
+
fireEvent.click(overlay!);
|
|
44
|
+
expect(onCloseMock).toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('does not call onClose when clicking inside the modal content', () => {
|
|
48
|
+
renderModal(true);
|
|
49
|
+
const modalContent = screen.getByText(modalContentText);
|
|
50
|
+
fireEvent.click(modalContent);
|
|
51
|
+
expect(onCloseMock).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('applies the provided modalOverlayClassname', () => {
|
|
55
|
+
const overlayClass = 'customOverlayClass';
|
|
56
|
+
renderModal(true, overlayClass);
|
|
57
|
+
const overlay = screen.getByText(modalContentText).closest('button');
|
|
58
|
+
expect(overlay).toHaveClass(overlayClass);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('applies the provided modalContentClassname', () => {
|
|
62
|
+
const contentClass = 'customContentClass';
|
|
63
|
+
renderModal(true, undefined, contentClass);
|
|
64
|
+
const modalContent = screen.getByText(modalContentText);
|
|
65
|
+
expect(modalContent.parentElement).toHaveClass(contentClass);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not render modal when isOpen is false, even if modalOverlayClassname and modalContentClassname are provided', () => {
|
|
69
|
+
const overlayClass = 'customOverlayClass';
|
|
70
|
+
const contentClass = 'customContentClass';
|
|
71
|
+
renderModal(false, overlayClass, contentClass);
|
|
72
|
+
expect(screen.queryByText(modalContentText)).not.toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import styles from './Modal.module.scss';
|
|
3
|
+
|
|
4
|
+
interface ModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
modalOverlayClassname?: string;
|
|
9
|
+
modalContentClassname?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Modal: React.FC<ModalProps> = ({
|
|
13
|
+
isOpen,
|
|
14
|
+
onClose,
|
|
15
|
+
children,
|
|
16
|
+
modalOverlayClassname = '',
|
|
17
|
+
modalContentClassname = ''
|
|
18
|
+
}) => {
|
|
19
|
+
if (!isOpen) return null;
|
|
20
|
+
|
|
21
|
+
const handleOverlayClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
22
|
+
if (event.target === event.currentTarget) {
|
|
23
|
+
onClose();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
className={`${styles.modalOverlay} ${modalOverlayClassname}`}
|
|
30
|
+
onClick={handleOverlayClick}
|
|
31
|
+
>
|
|
32
|
+
<div className={`${styles.modalContent} ${modalContentClassname}`}>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
</button>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default Modal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Modal";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.notification {
|
|
2
|
+
position: fixed;
|
|
3
|
+
bottom: 20px;
|
|
4
|
+
left: 20px;
|
|
5
|
+
width: 300px;
|
|
6
|
+
padding: 16px;
|
|
7
|
+
border-radius: 4px;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
z-index: 9999;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.notification .close-button {
|
|
16
|
+
background: none;
|
|
17
|
+
border: none;
|
|
18
|
+
font-size: 20px;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import Notification, { NotificationProps } from './Notification';
|
|
4
|
+
|
|
5
|
+
describe('Notification Component', () => {
|
|
6
|
+
const defaultProps: NotificationProps = {
|
|
7
|
+
message: 'Test Message',
|
|
8
|
+
severity: 'info',
|
|
9
|
+
autoHideDuration: 6000,
|
|
10
|
+
onClose: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const renderNotification = (props: Partial<NotificationProps> = {}) => {
|
|
14
|
+
return render(<Notification {...defaultProps} {...props} />);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
test('renders notification with the correct message', () => {
|
|
18
|
+
render(<Notification message="Test Message" severity="info" />);
|
|
19
|
+
|
|
20
|
+
// Check if the element exists by querying it
|
|
21
|
+
const notificationElement = screen.getByText('Test Message');
|
|
22
|
+
expect(notificationElement).not.toBeNull(); // Equivalent to `toBeInTheDocument`
|
|
23
|
+
|
|
24
|
+
// Alternatively, check for the presence of the role or className
|
|
25
|
+
const alertElement = screen.getByRole('alert');
|
|
26
|
+
expect(alertElement.className).toContain('info'); // Check if className includes "info"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('calls onClose when the notification is clicked', () => {
|
|
30
|
+
const onClose = jest.fn();
|
|
31
|
+
renderNotification({ onClose });
|
|
32
|
+
|
|
33
|
+
fireEvent.click(screen.getByRole('alert'));
|
|
34
|
+
|
|
35
|
+
expect(onClose).toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('calls onClose when the close button is clicked', () => {
|
|
39
|
+
const onClose = jest.fn();
|
|
40
|
+
renderNotification({ onClose });
|
|
41
|
+
|
|
42
|
+
fireEvent.click(screen.getByLabelText('Close notification'));
|
|
43
|
+
|
|
44
|
+
expect(onClose).toHaveBeenCalledWith(expect.anything(), 'close');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('calls onClose after autoHideDuration expires', async () => {
|
|
48
|
+
const onClose = jest.fn();
|
|
49
|
+
renderNotification({ onClose, autoHideDuration: 1000 });
|
|
50
|
+
|
|
51
|
+
await waitFor(() => expect(onClose).toHaveBeenCalledWith(expect.anything(), 'timeout'), { timeout: 1500 });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import './Notification.module.scss';
|
|
3
|
+
export interface NotificationProps {
|
|
4
|
+
message: string;
|
|
5
|
+
severity?: 'error' | 'warning' | 'info' | 'success';
|
|
6
|
+
autoHideDuration?: number;
|
|
7
|
+
onClose?: (event: React.SyntheticEvent<any> | Event, reason?: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Notification: React.FC<NotificationProps> = ({
|
|
11
|
+
message,
|
|
12
|
+
severity = 'info',
|
|
13
|
+
autoHideDuration = 6000,
|
|
14
|
+
onClose,
|
|
15
|
+
}) => {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (autoHideDuration && onClose) {
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
onClose(new Event('timeout'), 'timeout');
|
|
20
|
+
}, autoHideDuration);
|
|
21
|
+
|
|
22
|
+
return () => clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
}, [autoHideDuration, onClose]);
|
|
25
|
+
|
|
26
|
+
const handleClose = (event: React.SyntheticEvent<any> | Event, reason?: string) => {
|
|
27
|
+
if (onClose) {
|
|
28
|
+
onClose(event, reason);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={`notification ${severity}`} onClick={(e) => handleClose(e)} role="alert">
|
|
34
|
+
<div className="notification-message">{message}</div>
|
|
35
|
+
<button className="close-button" onClick={(e) => handleClose(e, 'close')} aria-label="Close notification">
|
|
36
|
+
×
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default Notification;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./Notification";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@import '../../theme/colors/colors.scss';
|
|
2
|
+
|
|
3
|
+
.otpMainContainer {
|
|
4
|
+
display: flex;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
margin: 10px;
|
|
8
|
+
width: 343px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.otpInputDiv {
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
gap: 12px;
|
|
14
|
+
display: flex
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.otpInput {
|
|
18
|
+
width: 76px;
|
|
19
|
+
height: 48px;
|
|
20
|
+
text-align: center;
|
|
21
|
+
font-size: 18px;
|
|
22
|
+
font-weight: 400;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.resendContainer {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: row;
|
|
29
|
+
margin-top: 10px;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.errorText {
|
|
34
|
+
color: $border-brand-danger;
|
|
35
|
+
font-weight: 400;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.timerText {
|
|
40
|
+
color: $content-disabled;
|
|
41
|
+
font-weight: 400;
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
}
|
|
44
|
+
.resendText {
|
|
45
|
+
color: $content-disabled;
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent, screen } from '@testing-library/react';
|
|
3
|
+
import OTPInput from './OtpInput';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
|
|
6
|
+
describe('OTPInput Component', () => {
|
|
7
|
+
it('renders the correct number of input fields based on the length prop', () => {
|
|
8
|
+
render(
|
|
9
|
+
<OTPInput
|
|
10
|
+
length={4}
|
|
11
|
+
onChange={() => {}}
|
|
12
|
+
onResend={() => {}}
|
|
13
|
+
resendText="Resend OTP"
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
const inputs = screen.getAllByRole('textbox');
|
|
17
|
+
expect(inputs).toHaveLength(4);
|
|
18
|
+
});
|
|
19
|
+
it('calls onChange prop when input changes', () => {
|
|
20
|
+
const handleChange = jest.fn();
|
|
21
|
+
render(<OTPInput length={4} onChange={handleChange} onResend={() => {}} resendText="Resend OTP" />);
|
|
22
|
+
const inputs = screen.getAllByRole('textbox');
|
|
23
|
+
fireEvent.change(inputs[0], { target: { value: '1' } });
|
|
24
|
+
expect(handleChange).toHaveBeenCalledWith('1');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('calls onResend prop when the resend button is clicked', async () => {
|
|
28
|
+
const handleResend = jest.fn();
|
|
29
|
+
render(<OTPInput length={4} onChange={() => {}} onResend={handleResend} resendText="Resend OTP" resendDelay={0} />);
|
|
30
|
+
const resendButton = screen.getByRole('button', { name: /resend otp/i });
|
|
31
|
+
fireEvent.click(resendButton);
|
|
32
|
+
expect(handleResend).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
it('displays error text when the error prop is true', () => {
|
|
35
|
+
const errorMessage = 'Invalid OTP';
|
|
36
|
+
render(<OTPInput length={4} onChange={() => {}} onResend={() => {}} resendText="Resend OTP" error={true} errorText={errorMessage} />);
|
|
37
|
+
const errorElement = screen.getByText(errorMessage);
|
|
38
|
+
expect(errorElement).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
it('changes border color to red when the error prop is true', () => {
|
|
41
|
+
render(
|
|
42
|
+
<OTPInput
|
|
43
|
+
length={4}
|
|
44
|
+
onChange={() => {}}
|
|
45
|
+
onResend={() => {}}
|
|
46
|
+
resendText="Resend OTP"
|
|
47
|
+
error={true}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
const firstInput = screen.getByTestId('otp-input-0');
|
|
51
|
+
expect(firstInput).toHaveStyle('border: 1px solid #EB0542');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import "./OtpInput.module.scss";
|
|
3
|
+
interface OTPInputProps {
|
|
4
|
+
length?: number;
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
onChange: (otp: string) => void;
|
|
7
|
+
autoFocus?: boolean;
|
|
8
|
+
onResend: () => void;
|
|
9
|
+
resendDelay?: number;
|
|
10
|
+
error?: boolean;
|
|
11
|
+
errorText?: string;
|
|
12
|
+
resendText: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const OTPInput: React.FC<OTPInputProps> = ({
|
|
16
|
+
length = 4,
|
|
17
|
+
onChange,
|
|
18
|
+
autoFocus = false,
|
|
19
|
+
onResend,
|
|
20
|
+
resendDelay = 60,
|
|
21
|
+
error = false,
|
|
22
|
+
errorText,
|
|
23
|
+
resendText,
|
|
24
|
+
}) => {
|
|
25
|
+
const [otp, setOtp] = useState<string[]>(Array(length).fill(""));
|
|
26
|
+
const [timer, setTimer] = useState<number>(resendDelay);
|
|
27
|
+
const [isComplete, setIsComplete] = useState<boolean>(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const countdown = setInterval(() => {
|
|
31
|
+
setTimer((prevTimer) => (prevTimer > 0 ? prevTimer - 1 : 0));
|
|
32
|
+
}, 1000);
|
|
33
|
+
|
|
34
|
+
return () => clearInterval(countdown);
|
|
35
|
+
}, []);
|
|
36
|
+
const handleChange = (value: string, index: number) => {
|
|
37
|
+
if (isNaN(Number(value))) return;
|
|
38
|
+
|
|
39
|
+
const newOtp = [...otp];
|
|
40
|
+
newOtp[index] = value;
|
|
41
|
+
setOtp(newOtp);
|
|
42
|
+
onChange(newOtp.join(""));
|
|
43
|
+
|
|
44
|
+
if (value && index < length - 1) {
|
|
45
|
+
const nextInput = document.getElementById(
|
|
46
|
+
`otp-input-${index + 1}`
|
|
47
|
+
) as HTMLInputElement;
|
|
48
|
+
if (nextInput) {
|
|
49
|
+
nextInput.focus();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleKeyDown = (
|
|
55
|
+
e: React.KeyboardEvent<HTMLInputElement>,
|
|
56
|
+
index: number
|
|
57
|
+
) => {
|
|
58
|
+
if (e.key === "Backspace" && !otp[index] && index > 0) {
|
|
59
|
+
const prevInput = document.getElementById(
|
|
60
|
+
`otp-input-${index - 1}`
|
|
61
|
+
) as HTMLInputElement;
|
|
62
|
+
if (prevInput) {
|
|
63
|
+
prevInput.focus();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleResendClick = () => {
|
|
69
|
+
onResend();
|
|
70
|
+
setOtp(Array(length).fill(""));
|
|
71
|
+
setTimer(resendDelay);
|
|
72
|
+
setIsComplete(false);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
setIsComplete(otp.every((digit) => digit !== ""));
|
|
77
|
+
}, [otp]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (error) {
|
|
81
|
+
setTimer(0);
|
|
82
|
+
}
|
|
83
|
+
}, [error]);
|
|
84
|
+
|
|
85
|
+
function getReadableStatus() {
|
|
86
|
+
if (error) {
|
|
87
|
+
return "#EB0542";
|
|
88
|
+
}
|
|
89
|
+
return isComplete ? "#00B578" : "#A7A7A7";
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<div className="otpMainContainer">
|
|
93
|
+
<div className="otpInputDiv">
|
|
94
|
+
{Array(length)
|
|
95
|
+
.fill("")
|
|
96
|
+
.map((_, index) => (
|
|
97
|
+
<input
|
|
98
|
+
key={index}
|
|
99
|
+
type="text"
|
|
100
|
+
id={`otp-input-${index}`}
|
|
101
|
+
value={otp[index]}
|
|
102
|
+
data-testid={`otp-input-${index}`}
|
|
103
|
+
role="textbox"
|
|
104
|
+
onChange={(e) => handleChange(e.target.value, index)}
|
|
105
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
106
|
+
maxLength={1}
|
|
107
|
+
style={{ border: `1px solid ${getReadableStatus()}` }}
|
|
108
|
+
className="otpInput"
|
|
109
|
+
autoFocus={autoFocus && index === 0}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="resendContainer">
|
|
114
|
+
{error ? (
|
|
115
|
+
<div className="errorText">{errorText}</div>
|
|
116
|
+
) : (
|
|
117
|
+
<div className="timerText">
|
|
118
|
+
{timer > 0 ? `00:${timer} secs` : ""}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
<button
|
|
122
|
+
onClick={handleResendClick}
|
|
123
|
+
disabled={timer > 0}
|
|
124
|
+
style={{
|
|
125
|
+
border: "none",
|
|
126
|
+
background: "none",
|
|
127
|
+
cursor: timer > 0 ? "not-allowed" : "pointer",
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<div className="resendText">{resendText}</div>
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default OTPInput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./OtpInput";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.spinner {
|
|
9
|
+
border: 2px solid transparent;
|
|
10
|
+
border-radius: 50%;
|
|
11
|
+
border-top: 2px solid currentColor;
|
|
12
|
+
animation: spin 1s linear infinite;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.spinnerInner {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
border-radius: 50%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.errorText {
|
|
22
|
+
color: red;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.button {
|
|
26
|
+
padding: 8px 16px;
|
|
27
|
+
border: none;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
font-size: 16px;
|
|
31
|
+
|
|
32
|
+
&-text {
|
|
33
|
+
background: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&-outlined {
|
|
37
|
+
border: 1px solid currentColor;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&-contained {
|
|
41
|
+
background-color: currentColor;
|
|
42
|
+
color: white;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&-primary {
|
|
46
|
+
color: blue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&-secondary {
|
|
50
|
+
color: gray;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&-small {
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&-medium {
|
|
58
|
+
font-size: 16px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
&-large {
|
|
62
|
+
font-size: 20px;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes spin {
|
|
67
|
+
0% { transform: rotate(0deg); }
|
|
68
|
+
100% { transform: rotate(360deg); }
|
|
69
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import PdfView from './PdfView';
|
|
5
|
+
|
|
6
|
+
// Dummy base64 content for testing
|
|
7
|
+
const validBase64Content = 'JVBERi0xLjQKJeLjz9MNCjEgMCBvYmoKPDwvTGluay9QYWdlcyAyIDAgUj4+CnN0YXJ0eHJlZgoyNCAwIFIKZW5kb2JqCjEgMCBvYmoKPDwvTGluay9QYWdlcyAyIDAgUj4+CnN0YXJ0eHJlZgo2IDAgUgo+';
|
|
8
|
+
const invalidBase64Content = 'invalid base64 content';
|
|
9
|
+
const errorText = 'Failed to load PDF';
|
|
10
|
+
const buttonText = 'View PDF';
|
|
11
|
+
const loadingText = 'Loading...';
|
|
12
|
+
|
|
13
|
+
// Save the original implementation of createObjectURL
|
|
14
|
+
const originalCreateObjectURL = global.URL.createObjectURL;
|
|
15
|
+
|
|
16
|
+
describe('PdfView Component', () => {
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
// Mock URL.createObjectURL directly
|
|
19
|
+
global.URL.createObjectURL = jest.fn(() => 'mock-url');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
// Restore the original implementation
|
|
24
|
+
global.URL.createObjectURL = originalCreateObjectURL;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders correctly with default props', () => {
|
|
28
|
+
render(<PdfView content={validBase64Content} />);
|
|
29
|
+
expect(screen.getByText(buttonText)).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('shows error message if content is invalid', () => {
|
|
33
|
+
render(<PdfView content={invalidBase64Content} />);
|
|
34
|
+
expect(screen.getByText(errorText)).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies the provided class names', () => {
|
|
38
|
+
const className = 'custom-class';
|
|
39
|
+
render(<PdfView content={validBase64Content} className={className} />);
|
|
40
|
+
expect(screen.getByText(buttonText).parentElement).toHaveClass(className);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
it('does not show spinner or loading text if isLoading is false', () => {
|
|
46
|
+
render(<PdfView content={validBase64Content} />);
|
|
47
|
+
|
|
48
|
+
// The component should not be in a loading state initially
|
|
49
|
+
expect(screen.queryByTestId('spinnertest')).not.toBeInTheDocument();
|
|
50
|
+
expect(screen.queryByText(loadingText)).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
});
|