dhre-component-lib 0.0.1 → 0.0.2
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/coverage/clover.xml +260 -0
- package/coverage/coverage-final.json +19 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/components/Avatar/Avatar.tsx.html +163 -0
- package/coverage/lcov-report/components/Avatar/index.html +116 -0
- package/coverage/lcov-report/components/Badge/Badge.tsx.html +160 -0
- package/coverage/lcov-report/components/Badge/index.html +116 -0
- package/coverage/lcov-report/components/BreadCrumb/BreadCrumb.tsx.html +193 -0
- package/coverage/lcov-report/components/BreadCrumb/index.html +116 -0
- package/coverage/lcov-report/components/Button/Button.tsx.html +151 -0
- package/coverage/lcov-report/components/Button/index.html +116 -0
- package/coverage/lcov-report/components/Checkbox/Checkbox.tsx.html +190 -0
- package/coverage/lcov-report/components/Checkbox/index.html +116 -0
- package/coverage/lcov-report/components/CircularProgress/CircularProgress.tsx.html +196 -0
- package/coverage/lcov-report/components/CircularProgress/index.html +116 -0
- package/coverage/lcov-report/components/Divider/Divider.tsx.html +157 -0
- package/coverage/lcov-report/components/Divider/index.html +116 -0
- package/coverage/lcov-report/components/Enum.ts.html +142 -0
- package/coverage/lcov-report/components/InputTextField/InputTextField.tsx.html +229 -0
- package/coverage/lcov-report/components/InputTextField/index.html +116 -0
- package/coverage/lcov-report/components/Link/Link.tsx.html +184 -0
- package/coverage/lcov-report/components/Link/index.html +116 -0
- package/coverage/lcov-report/components/Modal/Modal.tsx.html +169 -0
- package/coverage/lcov-report/components/Modal/index.html +116 -0
- package/coverage/lcov-report/components/Notification/Notification.tsx.html +208 -0
- package/coverage/lcov-report/components/Notification/index.html +116 -0
- package/coverage/lcov-report/components/PdfView/PdfView.tsx.html +400 -0
- package/coverage/lcov-report/components/PdfView/index.html +116 -0
- package/coverage/lcov-report/components/Progress/Progress.tsx.html +190 -0
- package/coverage/lcov-report/components/Progress/index.html +116 -0
- package/coverage/lcov-report/components/RadioButton/RadioButton.tsx.html +214 -0
- package/coverage/lcov-report/components/RadioButton/index.html +116 -0
- package/coverage/lcov-report/components/Switch/Switch.tsx.html +199 -0
- package/coverage/lcov-report/components/Switch/index.html +116 -0
- package/coverage/lcov-report/components/Tag/Tag.tsx.html +160 -0
- package/coverage/lcov-report/components/Tag/index.html +116 -0
- package/coverage/lcov-report/components/Tooltip/Tooltip.tsx.html +187 -0
- package/coverage/lcov-report/components/Tooltip/index.html +116 -0
- package/coverage/lcov-report/components/index.html +116 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +371 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov.info +457 -0
- package/jest.config.ts +1 -1
- package/package.json +9 -6
- package/src/components/Avatar/Avatar.test.tsx +52 -0
- package/src/components/Avatar/__snapshots__/Avatar.test.tsx.snap +9 -0
- package/src/components/Badge/Badge.test.tsx +63 -0
- package/src/components/Badge/__snapshots__/Badge.test.tsx.snap +9 -0
- package/src/components/BreadCrumb/BreadCrumb.test.tsx +90 -0
- package/src/components/BreadCrumb/index.ts +1 -0
- package/src/components/Button/Button.test.tsx +62 -0
- package/src/components/Button/__snapshots__/Button.test.tsx.snap +9 -0
- package/src/components/Checkbox/Checkbox.test.tsx +71 -43
- package/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap +17 -0
- package/src/components/{Circular_Progress → CircularProgress}/CircularProgress.tsx +1 -1
- package/src/components/Divider/Divider.test.tsx +35 -10
- package/src/components/InputTextField/InputTextField.test.tsx +103 -33
- package/src/components/InputTextField/InputTextField.tsx +7 -5
- package/src/components/Link/Link.test.tsx +44 -36
- package/src/components/Link/Link.tsx +1 -1
- 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.css +26 -0
- package/src/components/Modal/Modal.test.tsx +53 -34
- package/src/components/Modal/Modal.tsx +15 -6
- package/src/components/PdfView/PdfView.css +70 -0
- package/src/components/PdfView/PdfView.test.tsx +27 -63
- package/src/components/PdfView/PdfView.tsx +53 -41
- package/src/components/Progress/Progress.test.tsx +34 -26
- package/src/components/Progress/Progress.tsx +2 -2
- package/src/components/RadioButton/RadioButton.test.tsx +59 -30
- package/src/components/Switch/Switch.test.tsx +83 -0
- package/src/components/Tag/Tag.test.tsx +57 -0
- package/src/components/Tag/__snapshots__/Tag.test.tsx.snap +9 -0
- package/src/components/Tooltip/Tooltip.test.tsx +73 -0
- package/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap +22 -0
- package/src/components/index.ts +5 -2
- package/src/typings.d.ts +1 -0
- package/tsconfig.json +17 -18
- /package/src/components/{Circular_Progress → CircularProgress}/CircularProgress.css +0 -0
- /package/src/components/{Circular_Progress → CircularProgress}/CircularProgress.test.tsx +0 -0
- /package/src/components/{Circular_Progress → CircularProgress}/index.ts +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface GetDirectionActionProps {
|
|
4
|
+
title: string;
|
|
5
|
+
titleClasses?: string;
|
|
6
|
+
location: string;
|
|
7
|
+
locationClasses?: string;
|
|
8
|
+
hours: string;
|
|
9
|
+
hoursClasses?: string;
|
|
10
|
+
directionsLink: string;
|
|
11
|
+
buttonName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const GetDirectionAction: React.FC<GetDirectionActionProps> = ({
|
|
15
|
+
title,
|
|
16
|
+
titleClasses,
|
|
17
|
+
location,
|
|
18
|
+
locationClasses,
|
|
19
|
+
hours,
|
|
20
|
+
hoursClasses,
|
|
21
|
+
directionsLink,
|
|
22
|
+
buttonName = 'Get Directions',
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<div className="direction-action-container">
|
|
26
|
+
<h3 className={titleClasses}>{title}</h3>
|
|
27
|
+
<p className={locationClasses}>{location}</p>
|
|
28
|
+
<p className={hoursClasses}>{hours}</p>
|
|
29
|
+
<a href={directionsLink} target="_blank" rel="noopener noreferrer" className="directions-link">
|
|
30
|
+
{buttonName}
|
|
31
|
+
</a>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default GetDirectionAction;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
GoogleMap,
|
|
4
|
+
Marker,
|
|
5
|
+
Circle,
|
|
6
|
+
DirectionsRenderer,
|
|
7
|
+
} from "@react-google-maps/api";
|
|
8
|
+
import { useGoogleMapsLoader } from "./GoogleMapsLoader";
|
|
9
|
+
import styles from './GoogleMap.module.scss'
|
|
10
|
+
export interface MapComponentProps {
|
|
11
|
+
containerClassName?: string;
|
|
12
|
+
zoom?: number;
|
|
13
|
+
mapOptions?: google.maps.MapOptions;
|
|
14
|
+
radius?: number;
|
|
15
|
+
googleMapsUrls: string[];
|
|
16
|
+
buttonText: string;
|
|
17
|
+
buttonClassName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MapComponent: React.FC<MapComponentProps> = ({
|
|
21
|
+
containerClassName = "map-container",
|
|
22
|
+
zoom = 18,
|
|
23
|
+
mapOptions = {
|
|
24
|
+
zoomControl: true,
|
|
25
|
+
tilt: 0,
|
|
26
|
+
gestureHandling: "auto",
|
|
27
|
+
mapTypeId: "roadmap",
|
|
28
|
+
},
|
|
29
|
+
radius = 5000,
|
|
30
|
+
googleMapsUrls,
|
|
31
|
+
buttonText,
|
|
32
|
+
buttonClassName,
|
|
33
|
+
}) => {
|
|
34
|
+
const [location, setLocation] = useState<{
|
|
35
|
+
latitude: number;
|
|
36
|
+
longitude: number;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
const [mapVisible, setMapVisible] = useState(false);
|
|
39
|
+
const [directionsResponse, setDirectionsResponse] =
|
|
40
|
+
useState<google.maps.DirectionsResult | null>(null);
|
|
41
|
+
const mapRef = useRef<google.maps.Map | null>(null);
|
|
42
|
+
const directionsService = useRef<google.maps.DirectionsService | null>(null);
|
|
43
|
+
const directionsRenderer = useRef<google.maps.DirectionsRenderer | null>(
|
|
44
|
+
null
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const handleLocationRequest = () => {
|
|
48
|
+
if ("geolocation" in navigator) {
|
|
49
|
+
navigator.geolocation.getCurrentPosition(
|
|
50
|
+
({ coords }) => {
|
|
51
|
+
const { latitude, longitude } = coords;
|
|
52
|
+
setLocation({ latitude, longitude });
|
|
53
|
+
setMapVisible(true);
|
|
54
|
+
},
|
|
55
|
+
() => {
|
|
56
|
+
alert("Failed to get location. Please try again.");
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
alert("Geolocation is not supported by this browser.");
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleMarkerClick = (lat: number, lng: number) => {
|
|
65
|
+
if (location) {
|
|
66
|
+
const directionsServiceInstance =
|
|
67
|
+
directionsService.current || new google.maps.DirectionsService();
|
|
68
|
+
const directionsRendererInstance =
|
|
69
|
+
directionsRenderer.current || new google.maps.DirectionsRenderer();
|
|
70
|
+
|
|
71
|
+
if (!directionsService.current)
|
|
72
|
+
directionsService.current = directionsServiceInstance;
|
|
73
|
+
if (!directionsRenderer.current)
|
|
74
|
+
directionsRenderer.current = directionsRendererInstance;
|
|
75
|
+
|
|
76
|
+
const directionsRequest: google.maps.DirectionsRequest = {
|
|
77
|
+
origin: { lat: location.latitude, lng: location.longitude },
|
|
78
|
+
destination: { lat, lng },
|
|
79
|
+
travelMode: google.maps.TravelMode.DRIVING,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
directionsService.current.route(directionsRequest, (result, status) => {
|
|
83
|
+
if (status === google.maps.DirectionsStatus.OK) {
|
|
84
|
+
setDirectionsResponse(result);
|
|
85
|
+
if (mapRef.current && directionsRenderer.current) {
|
|
86
|
+
directionsRenderer.current.setMap(mapRef.current);
|
|
87
|
+
directionsRenderer.current.setDirections(result);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
alert("Failed to get directions. Please try again.");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const extractCoordinatesFromUrl = (url: string) => {
|
|
97
|
+
const regex = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
|
|
98
|
+
const match = url.match(regex);
|
|
99
|
+
if (match) {
|
|
100
|
+
return {
|
|
101
|
+
lat: parseFloat(match[1]),
|
|
102
|
+
lng: parseFloat(match[2]),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const { isLoaded } = useGoogleMapsLoader();
|
|
110
|
+
|
|
111
|
+
useEffect(() => {}, [isLoaded]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {}, [mapVisible]);
|
|
114
|
+
|
|
115
|
+
if (!isLoaded) {
|
|
116
|
+
return <div>Loading map...</div>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (mapVisible && location) {
|
|
120
|
+
return (
|
|
121
|
+
<div className={containerClassName}>
|
|
122
|
+
<button onClick={handleLocationRequest} className={buttonClassName}>
|
|
123
|
+
{buttonText}
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
<GoogleMap
|
|
127
|
+
mapContainerStyle={styles.mapContainerStyle}
|
|
128
|
+
center={{ lat: location.latitude, lng: location.longitude }}
|
|
129
|
+
zoom={zoom}
|
|
130
|
+
options={mapOptions}
|
|
131
|
+
onLoad={(map) => {
|
|
132
|
+
mapRef.current = map;
|
|
133
|
+
directionsRenderer.current = new google.maps.DirectionsRenderer();
|
|
134
|
+
directionsRenderer.current.setMap(map);
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<Marker
|
|
138
|
+
position={{ lat: location.latitude, lng: location.longitude }}
|
|
139
|
+
icon="http://maps.google.com/mapfiles/ms/icons/red-dot.png"
|
|
140
|
+
/>
|
|
141
|
+
<Circle
|
|
142
|
+
center={{ lat: location.latitude, lng: location.longitude }}
|
|
143
|
+
radius={radius}
|
|
144
|
+
options={{
|
|
145
|
+
strokeColor: "#FF0000",
|
|
146
|
+
strokeOpacity: 0.8,
|
|
147
|
+
strokeWeight: 2,
|
|
148
|
+
fillColor: "#FF0000",
|
|
149
|
+
fillOpacity: 0,
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
{googleMapsUrls.map((url, index) => {
|
|
153
|
+
const coord = extractCoordinatesFromUrl(url);
|
|
154
|
+
if (!coord) return null;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Marker
|
|
158
|
+
key={index}
|
|
159
|
+
position={{ lat: coord.lat, lng: coord.lng }}
|
|
160
|
+
title={`Sample Location ${index + 1}`}
|
|
161
|
+
icon="http://maps.google.com/mapfiles/ms/icons/blue-dot.png"
|
|
162
|
+
onClick={() => handleMarkerClick(coord.lat, coord.lng)}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
{directionsResponse && (
|
|
167
|
+
<DirectionsRenderer
|
|
168
|
+
directions={directionsResponse}
|
|
169
|
+
options={{ suppressMarkers: true }}
|
|
170
|
+
/>
|
|
171
|
+
)}
|
|
172
|
+
</GoogleMap>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
return (
|
|
177
|
+
<div className={containerClassName}>
|
|
178
|
+
<button onClick={handleLocationRequest} className={buttonClassName}>
|
|
179
|
+
{buttonText}
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default MapComponent;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useJsApiLoader } from '@react-google-maps/api';
|
|
2
|
+
|
|
3
|
+
const GOOGLE_MAPS_API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAP_API as string; // Ensure you are using environment variable
|
|
4
|
+
|
|
5
|
+
export const useGoogleMapsLoader = () => {
|
|
6
|
+
return useJsApiLoader({
|
|
7
|
+
googleMapsApiKey: GOOGLE_MAPS_API_KEY, // Replace with your API key
|
|
8
|
+
libraries: ['places', 'drawing', 'geometry'], // Include all libraries you need
|
|
9
|
+
language: 'en',
|
|
10
|
+
region: 'US',
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -1,51 +1,70 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { render, screen } from
|
|
3
|
-
import Modal from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import Modal from './Modal';
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
|
|
5
|
+
describe('Modal Component', () => {
|
|
6
|
+
const onCloseMock = jest.fn();
|
|
7
|
+
const modalContentText = 'This is the modal content';
|
|
8
|
+
|
|
9
|
+
const renderModal = (isOpen: boolean, modalOverlayClassname?: string, modalContentClassname?: string) => {
|
|
7
10
|
render(
|
|
8
|
-
<Modal
|
|
9
|
-
|
|
11
|
+
<Modal
|
|
12
|
+
isOpen={isOpen}
|
|
13
|
+
onClose={onCloseMock}
|
|
14
|
+
modalOverlayClassname={modalOverlayClassname}
|
|
15
|
+
modalContentClassname={modalContentClassname}
|
|
16
|
+
>
|
|
17
|
+
<div>{modalContentText}</div>
|
|
10
18
|
</Modal>
|
|
11
19
|
);
|
|
20
|
+
};
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
14
24
|
});
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<div>Modal Content</div>
|
|
20
|
-
</Modal>
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
expect(screen.queryByText("Modal Content")).not.toBeInTheDocument();
|
|
26
|
+
it('renders the modal when isOpen is true', () => {
|
|
27
|
+
renderModal(true);
|
|
28
|
+
expect(screen.getByText(modalContentText)).toBeInTheDocument();
|
|
24
29
|
});
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
it('does not render the modal when isOpen is false', () => {
|
|
32
|
+
renderModal(false);
|
|
33
|
+
expect(screen.queryByText(modalContentText)).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
36
|
+
it('calls onClose when clicking on the overlay', () => {
|
|
37
|
+
renderModal(true);
|
|
38
|
+
const overlay = screen.getByText(modalContentText).closest('div.modalOverlay');
|
|
39
|
+
fireEvent.click(overlay!);
|
|
40
|
+
expect(onCloseMock).toHaveBeenCalled();
|
|
41
|
+
});
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
it('does not call onClose when clicking inside the modal content', () => {
|
|
44
|
+
renderModal(true);
|
|
45
|
+
const modalContent = screen.getByText(modalContentText);
|
|
46
|
+
fireEvent.click(modalContent);
|
|
47
|
+
expect(onCloseMock).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
it('applies the provided modalOverlayClassname', () => {
|
|
51
|
+
const overlayClass = 'customOverlayClass';
|
|
52
|
+
renderModal(true, overlayClass);
|
|
53
|
+
const overlay = screen.getByText(modalContentText).closest('div.modalOverlay');
|
|
54
|
+
expect(overlay).toHaveClass(overlayClass);
|
|
39
55
|
});
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
it('applies the provided modalContentClassname', () => {
|
|
58
|
+
const contentClass = 'customContentClass';
|
|
59
|
+
renderModal(true, undefined, contentClass);
|
|
60
|
+
const modalContent = screen.getByText(modalContentText).closest('div.modalContent');
|
|
61
|
+
expect(modalContent).toHaveClass(contentClass);
|
|
62
|
+
});
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
it('does not render modal when isOpen is false, even if modalOverlayClassname and modalContentClassname are provided', () => {
|
|
65
|
+
const overlayClass = 'customOverlayClass';
|
|
66
|
+
const contentClass = 'customContentClass';
|
|
67
|
+
renderModal(false, overlayClass, contentClass);
|
|
68
|
+
expect(screen.queryByText(modalContentText)).not.toBeInTheDocument();
|
|
50
69
|
});
|
|
51
70
|
});
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import
|
|
2
|
+
import "./Modal.css";
|
|
3
3
|
|
|
4
4
|
interface ModalProps {
|
|
5
5
|
isOpen: boolean;
|
|
6
6
|
onClose: () => void;
|
|
7
7
|
children: React.ReactNode;
|
|
8
|
-
|
|
8
|
+
modalOverlayClassname?: string;
|
|
9
|
+
modalContentClassname?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children,
|
|
12
|
+
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children, modalOverlayClassname, modalContentClassname }) => {
|
|
13
|
+
if (!isOpen) return null;
|
|
14
|
+
|
|
15
|
+
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
16
|
+
if (event.target === event.currentTarget) {
|
|
17
|
+
onClose();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
12
21
|
return (
|
|
13
|
-
<
|
|
14
|
-
<div className={
|
|
15
|
-
</
|
|
22
|
+
<div className={`modalOverlay ${modalOverlayClassname}`} onClick={handleOverlayClick}>
|
|
23
|
+
<div className={`modalContent ${modalContentClassname}`}>{children}</div>
|
|
24
|
+
</div>
|
|
16
25
|
);
|
|
17
26
|
};
|
|
18
27
|
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
.spinner-inner {
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
border-radius: 50%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.error-text {
|
|
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
|
+
|
|
33
|
+
.button-text {
|
|
34
|
+
background: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.button-outlined {
|
|
38
|
+
border: 1px solid currentColor;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.button-contained {
|
|
42
|
+
background-color: currentColor;
|
|
43
|
+
color: white;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.button-primary {
|
|
47
|
+
color: blue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.button-secondary {
|
|
51
|
+
color: gray;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.button-small {
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.button-medium {
|
|
59
|
+
font-size: 16px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.button-large {
|
|
63
|
+
font-size: 20px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes spin {
|
|
67
|
+
0% { transform: rotate(0deg); }
|
|
68
|
+
100% { transform: rotate(360deg); }
|
|
69
|
+
}
|
|
70
|
+
|
|
@@ -1,79 +1,43 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
|
-
import PdfView
|
|
5
|
-
|
|
6
|
-
// Mock the URL.createObjectURL and URL.revokeObjectURL functions
|
|
7
|
-
global.URL.createObjectURL = jest.fn();
|
|
8
|
-
global.URL.revokeObjectURL = jest.fn();
|
|
9
|
-
|
|
10
|
-
const defaultProps: PdfViewerProps = {
|
|
11
|
-
content: 'mockBase64Data',
|
|
12
|
-
buttonText: 'View PDF',
|
|
13
|
-
showLoadingSpinner: true, // Set this to true to test the spinner
|
|
14
|
-
loadingText: 'Loading...',
|
|
15
|
-
errorText: 'Failed to load PDF',
|
|
16
|
-
spinnerSize: 24,
|
|
17
|
-
spinnerColor: 'primary',
|
|
18
|
-
};
|
|
4
|
+
import PdfView from './PdfView';
|
|
19
5
|
|
|
20
6
|
describe('PdfView Component', () => {
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
// Dummy base64 content for testing
|
|
8
|
+
const validBase64Content = 'JVBERi0xLjQKJeLjz9MNCjEgMCBvYmoKPDwvTGluay9QYWdlcyAyIDAgUj4+CnN0YXJ0eHJlZgoyNCAwIFIKZW5kb2JqCjEgMCBvYmoKPDwvTGluay9QYWdlcyAyIDAgUj4+CnN0YXJ0eHJlZgo2IDAgUgo+';
|
|
9
|
+
const invalidBase64Content = 'invalid base64 content';
|
|
10
|
+
const errorText = 'Failed to load PDF';
|
|
11
|
+
const buttonText = 'View PDF';
|
|
12
|
+
const loadingText = 'Loading...';
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
// Mock URL.createObjectURL directly
|
|
16
|
+
global.URL.createObjectURL = jest.fn(() => 'mock-url');
|
|
23
17
|
});
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
// Restore the original implementation
|
|
21
|
+
delete global.URL.createObjectURL;
|
|
28
22
|
});
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
jest.spyOn(global, 'atob').mockImplementation(() => {
|
|
34
|
-
throw new Error('Error converting base64');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
render(<PdfView {...defaultProps} />);
|
|
38
|
-
|
|
39
|
-
// Click the button to trigger the error
|
|
40
|
-
fireEvent.click(screen.getByText('View PDF'));
|
|
41
|
-
|
|
42
|
-
// Wait for the error message to appear
|
|
43
|
-
await waitFor(() => expect(screen.getByText('Failed to load PDF')).toBeInTheDocument());
|
|
44
|
-
|
|
45
|
-
jest.restoreAllMocks();
|
|
24
|
+
it('renders correctly with default props', () => {
|
|
25
|
+
render(<PdfView content={validBase64Content} />);
|
|
26
|
+
expect(screen.getByText(buttonText)).toBeInTheDocument();
|
|
46
27
|
});
|
|
47
28
|
|
|
48
|
-
it('
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Simulate an error in base64 to blob conversion
|
|
52
|
-
jest.spyOn(global, 'atob').mockImplementation(() => {
|
|
53
|
-
throw new Error('Error converting base64');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
render(<PdfView {...defaultProps} onError={mockOnError} />);
|
|
57
|
-
|
|
58
|
-
// Click the button to trigger the error
|
|
59
|
-
fireEvent.click(screen.getByText('View PDF'));
|
|
60
|
-
|
|
61
|
-
// Wait for the error callback to be called
|
|
62
|
-
await waitFor(() => expect(mockOnError).toHaveBeenCalled());
|
|
63
|
-
|
|
64
|
-
jest.restoreAllMocks();
|
|
29
|
+
it('shows error message if content is invalid', () => {
|
|
30
|
+
render(<PdfView content={invalidBase64Content} />);
|
|
31
|
+
expect(screen.getByText(errorText)).toBeInTheDocument();
|
|
65
32
|
});
|
|
66
33
|
|
|
67
|
-
it('should open the PDF in a new tab when the button is clicked', async () => {
|
|
68
|
-
render(<PdfView {...defaultProps} />);
|
|
69
34
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Check if window.open was called
|
|
77
|
-
expect(global.URL.createObjectURL).toHaveBeenCalled();
|
|
35
|
+
|
|
36
|
+
it('applies the provided class names', () => {
|
|
37
|
+
const className = 'custom-class';
|
|
38
|
+
render(<PdfView content={validBase64Content} className={className} />);
|
|
39
|
+
expect(screen.getByText(buttonText).parentElement).toHaveClass(className);
|
|
78
40
|
});
|
|
41
|
+
|
|
42
|
+
// More tests for different button props can be added here...
|
|
79
43
|
});
|