@hmlr/govuk-react-components-library 1.0.1 → 1.0.3

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/dist/index.cjs.js CHANGED
@@ -504,7 +504,10 @@ const CardLink = /*#__PURE__*/React__namespace.forwardRef(({
504
504
  });
505
505
  CardLink.displayName = 'CardLink';
506
506
 
507
- var divWithClassName = (className => /*#__PURE__*/React__namespace.forwardRef((p, ref) => /*#__PURE__*/jsxRuntime.jsx("div", {
507
+ var divWithClassName = (className =>
508
+ /*#__PURE__*/
509
+ // eslint-disable-next-line react/display-name
510
+ React__namespace.forwardRef((p, ref) => /*#__PURE__*/jsxRuntime.jsx("div", {
508
511
  ...p,
509
512
  ref: ref,
510
513
  className: classNames(p.className, className)
@@ -1028,28 +1031,281 @@ function Slugify(str) {
1028
1031
  return str;
1029
1032
  }
1030
1033
 
1034
+ /**
1035
+ * Convert a base64 string to an ArrayBuffer
1036
+ */
1037
+ const _base64ToArrayBuffer = (base64) => {
1038
+ const binaryString = window.atob(base64);
1039
+ const len = binaryString.length;
1040
+ const bytes = new Uint8Array(len);
1041
+ for (let i = 0; i < len; i++) {
1042
+ bytes[i] = binaryString.charCodeAt(i);
1043
+ }
1044
+ return bytes.buffer;
1045
+ };
1046
+ /**
1047
+ * Very small heuristic to detect a plain base64 string
1048
+ */
1049
+ const isBase64 = (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test(value);
1050
+ /**
1051
+ * ResolvePDFSource
1052
+ *
1053
+ * Accepts a source that may be:
1054
+ * - a plain base64 string (PDF binary encoded as base64)
1055
+ * - a data: URL containing base64 (data:application/pdf;base64,...)
1056
+ * - an existing blob: URL (blob:...)
1057
+ * - a Blob/File object
1058
+ * - a regular URL string (http(s) or same-origin path)
1059
+ *
1060
+ * Returns a SourceDeterminer:
1061
+ * - { isBase64Source: true, source: string } when an object URL was created
1062
+ * - { isBase64Source: false, source: string } when source can be used directly
1063
+ *
1064
+ * If an object URL is created, callers should revoke it when no longer needed:
1065
+ * `URL.revokeObjectURL(source)`
1066
+ */
1067
+ const ResolvePDFSource = (source) => {
1068
+ // Nothing provided
1069
+ if (source === undefined || source === null || source === "") {
1070
+ return { isBase64Source: false, source: "" };
1071
+ }
1072
+ // If source is already a Blob (File), create an object URL
1073
+ if (typeof source.arrayBuffer === "function" ||
1074
+ source instanceof Blob) {
1075
+ const blob = source;
1076
+ const objectUrl = URL.createObjectURL(blob);
1077
+ return { isBase64Source: true, source: objectUrl };
1078
+ }
1079
+ // If source is a string:
1080
+ if (typeof source === "string") {
1081
+ // If it's already a blob URL, return as-is
1082
+ if (source.startsWith("blob:")) {
1083
+ return { isBase64Source: false, source };
1084
+ }
1085
+ // If it's a data URL (data:application/pdf;base64,....)
1086
+ if (source.startsWith("data:")) {
1087
+ // data:[<mediatype>][;base64],<data>
1088
+ const commaIndex = source.indexOf(",");
1089
+ if (commaIndex > -1) {
1090
+ const meta = source.substring(0, commaIndex);
1091
+ const dataPart = source.substring(commaIndex + 1);
1092
+ // If it contains base64 marker, decode accordingly
1093
+ if (meta.indexOf(";base64") !== -1) {
1094
+ try {
1095
+ const arrayBuffer = _base64ToArrayBuffer(dataPart);
1096
+ const blob = new Blob([arrayBuffer], { type: "application/pdf" });
1097
+ const objectUrl = URL.createObjectURL(blob);
1098
+ return { isBase64Source: true, source: objectUrl };
1099
+ }
1100
+ catch (err) {
1101
+ console.error("ResolvePDFSource: failed to convert data: URL to blob", err);
1102
+ // fallback to returning original data URL
1103
+ return { isBase64Source: false, source };
1104
+ }
1105
+ }
1106
+ else {
1107
+ // Not base64-encoded data URL — return as is
1108
+ return { isBase64Source: false, source };
1109
+ }
1110
+ }
1111
+ }
1112
+ // If it's a plain base64 string (no data: prefix), convert
1113
+ if (isBase64(source)) {
1114
+ try {
1115
+ const arrayBuffer = _base64ToArrayBuffer(source);
1116
+ const blob = new Blob([arrayBuffer], { type: "application/pdf" });
1117
+ const objectUrl = URL.createObjectURL(blob);
1118
+ return { isBase64Source: true, source: objectUrl };
1119
+ }
1120
+ catch (err) {
1121
+ console.error("ResolvePDFSource: failed to convert base64 to blob", err);
1122
+ return { isBase64Source: false, source };
1123
+ }
1124
+ }
1125
+ // Otherwise treat as a normal URL (http(s) or same-origin path)
1126
+ return { isBase64Source: false, source };
1127
+ }
1128
+ // Fallback: convert to string
1129
+ try {
1130
+ const asString = String(source);
1131
+ return { isBase64Source: false, source: asString };
1132
+ }
1133
+ catch (err) {
1134
+ console.error("ResolvePDFSource: failed to convert source to string", err);
1135
+ return { isBase64Source: false, source: "" };
1136
+ }
1137
+ };
1138
+
1031
1139
  const PDFViewer = (props) => {
1032
1140
  const viewerRef = React.useRef(null);
1033
- const { iframeId, src, viewerLocation, documentName, documentNameColour, backend, toolbar = "minimal", additionalBackendAttributes, ...attributes } = props;
1141
+ const { iframeId, src, viewerLocation, documentName, documentNameColour, backend, toolbar = "minimal", additionalBackendAttributes,
1142
+ // optional: lets callers skip client-side fetch for remote resources
1143
+ disableClientFetch = false, ...attributes } = props;
1034
1144
  const [loading, setLoading] = React.useState(false);
1145
+ // ref for object URL (either created by ResolvePDFSource or by us after fetching)
1146
+ const objectUrlRef = React.useRef(null);
1147
+ // did ResolvePDFSource create the object URL? (helps understanding where to revoke)
1148
+ const createdByResolveRef = React.useRef(false);
1149
+ // fetch abort controller for cross-origin remote fetches
1150
+ const fetchControllerRef = React.useRef(null);
1151
+ const isRemoteUrl = (url) => {
1152
+ if (!url)
1153
+ return false;
1154
+ // Check if it's an HTTP(S) URL
1155
+ return /^https?:\/\//i.test(url);
1156
+ };
1035
1157
  React.useEffect(() => {
1158
+ let mounted = true;
1036
1159
  setLoading(true);
1037
1160
  const element = viewerRef.current;
1038
- if (!element)
1039
- return; // Guard against null
1040
- const jsProps = {
1041
- iframeId,
1042
- source: src,
1043
- viewerLocation,
1044
- documentName,
1045
- documentNameColour,
1046
- element,
1047
- toolbar,
1048
- ...additionalBackendAttributes,
1161
+ if (!element) {
1162
+ setLoading(false);
1163
+ return;
1164
+ }
1165
+ // cleanup previous URL/fetch if any
1166
+ if (fetchControllerRef.current) {
1167
+ try {
1168
+ fetchControllerRef.current.abort();
1169
+ }
1170
+ catch (err) {
1171
+ console.error("PDFViewer: fetch abort error:", err);
1172
+ /* ignore */
1173
+ }
1174
+ fetchControllerRef.current = null;
1175
+ }
1176
+ if (objectUrlRef.current) {
1177
+ try {
1178
+ URL.revokeObjectURL(objectUrlRef.current);
1179
+ }
1180
+ catch (err) {
1181
+ console.error("PDFViewer: revokeObjectURL error:", err);
1182
+ }
1183
+ objectUrlRef.current = null;
1184
+ createdByResolveRef.current = false;
1185
+ }
1186
+ const init = async () => {
1187
+ let sourceToUse = src;
1188
+ // 1) Let ResolvePDFSource handle base64/data/blob inputs synchronously.
1189
+ try {
1190
+ const resolved = ResolvePDFSource(src);
1191
+ if (resolved && resolved.source) {
1192
+ sourceToUse = resolved.source;
1193
+ if (resolved.isBase64Source) {
1194
+ // ResolvePDFSource created an object URL for us (data/blob/base64)
1195
+ objectUrlRef.current = resolved.source;
1196
+ createdByResolveRef.current = true;
1197
+ }
1198
+ }
1199
+ }
1200
+ catch (err) {
1201
+ // If ResolvePDFSource throws, just ignore and fall back to src
1202
+ console.error("PDFViewer: ResolvePDFSource error:", err);
1203
+ }
1204
+ // 2) If ResolvePDFSource didn't create an object URL and the src is cross-origin HTTP(S),
1205
+ // attempt a client-side fetch to create an object URL (so viewer can load it safely).
1206
+ if (!objectUrlRef.current &&
1207
+ !disableClientFetch &&
1208
+ typeof src === "string" &&
1209
+ /^https?:\/\//i.test(src) &&
1210
+ isRemoteUrl(src)) {
1211
+ const controller = new AbortController();
1212
+ fetchControllerRef.current = controller;
1213
+ try {
1214
+ const resp = await fetch(src, {
1215
+ signal: controller.signal,
1216
+ credentials: "omit",
1217
+ });
1218
+ if (!resp.ok) {
1219
+ console.warn(`PDFViewer: fetch returned ${resp.status} ${resp.statusText} for ${src}`);
1220
+ // fallback: keep using original src
1221
+ }
1222
+ else {
1223
+ const blob = await resp.blob();
1224
+ if (blob && blob.size > 0) {
1225
+ const obj = URL.createObjectURL(blob);
1226
+ objectUrlRef.current = obj;
1227
+ createdByResolveRef.current = false; // we created it via fetch
1228
+ sourceToUse = obj;
1229
+ }
1230
+ else {
1231
+ console.warn("PDFViewer: fetched blob is empty - falling back to original src");
1232
+ }
1233
+ }
1234
+ }
1235
+ catch (err) {
1236
+ if (err?.name === "AbortError") {
1237
+ // aborted, nothing to do
1238
+ if (!mounted)
1239
+ return;
1240
+ }
1241
+ else {
1242
+ console.error("PDFViewer: error fetching cross-origin PDF:", err);
1243
+ }
1244
+ // fallback to source as-is (original remote URL)
1245
+ }
1246
+ }
1247
+ // 3) Prepare and call backend
1248
+ if (!mounted)
1249
+ return;
1250
+ const jsProps = {
1251
+ iframeId,
1252
+ source: sourceToUse,
1253
+ viewerLocation,
1254
+ documentName,
1255
+ documentNameColour,
1256
+ element,
1257
+ toolbar,
1258
+ ...additionalBackendAttributes,
1259
+ };
1260
+ try {
1261
+ backend(jsProps);
1262
+ }
1263
+ catch (err) {
1264
+ console.error("PDFViewer: backend threw an error:", err);
1265
+ }
1266
+ finally {
1267
+ if (mounted)
1268
+ setLoading(false);
1269
+ }
1270
+ }; // end init
1271
+ init();
1272
+ return () => {
1273
+ mounted = false;
1274
+ // cancel any ongoing fetch
1275
+ if (fetchControllerRef.current) {
1276
+ try {
1277
+ fetchControllerRef.current.abort();
1278
+ }
1279
+ catch (err) {
1280
+ console.error("PDFViewer: fetch abort error during cleanup:", err);
1281
+ }
1282
+ fetchControllerRef.current = null;
1283
+ }
1284
+ // revoke any object URL we created
1285
+ if (objectUrlRef.current) {
1286
+ try {
1287
+ URL.revokeObjectURL(objectUrlRef.current);
1288
+ }
1289
+ catch (err) {
1290
+ console.error("PDFViewer: fetch abort error during cleanup:", err);
1291
+ }
1292
+ objectUrlRef.current = null;
1293
+ createdByResolveRef.current = false;
1294
+ }
1295
+ // Note: PDFViewerBackend also attaches an iframe to the element. If backend does not
1296
+ // remove previous iframes, you might want to clear element.children here.
1049
1297
  };
1050
- backend(jsProps);
1051
- setLoading(false);
1052
- }, []);
1298
+ // re-run when these inputs change
1299
+ }, [
1300
+ src,
1301
+ iframeId,
1302
+ viewerLocation,
1303
+ documentName,
1304
+ documentNameColour,
1305
+ toolbar,
1306
+ backend,
1307
+ disableClientFetch,
1308
+ ]);
1053
1309
  return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [loading && jsxRuntime.jsx(Loading, { message: "Loading PDF Document" }), jsxRuntime.jsx("div", { ref: viewerRef, id: `viewer ${documentName ? Slugify(documentName) : ""}`, "data-testid": `viewer ${documentName ? Slugify(documentName) : ""}`, style: { width: "100%", height: "100%" }, ...attributes })] }));
1054
1310
  };
1055
1311
 
@@ -1063,27 +1319,6 @@ const ObjectToQueryString = (queryParameters) => {
1063
1319
  : "";
1064
1320
  };
1065
1321
 
1066
- const _base64ToArrayBuffer = (base64) => {
1067
- const binaryString = window.atob(base64);
1068
- const len = binaryString.length;
1069
- const bytes = new Uint8Array(len);
1070
- for (let i = 0; i < len; i++) {
1071
- bytes[i] = binaryString.charCodeAt(i);
1072
- }
1073
- return bytes.buffer;
1074
- };
1075
- const isBase64 = (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/.test(value);
1076
- const ResolvePDFSource = (source) => {
1077
- const isBase64Source = isBase64(source);
1078
- if (!isBase64Source) {
1079
- return { isBase64Source, source };
1080
- }
1081
- const blob = new Blob([_base64ToArrayBuffer(source)], {
1082
- type: "application/pdf",
1083
- });
1084
- return { isBase64Source, source: URL.createObjectURL(blob) };
1085
- };
1086
-
1087
1322
  const PDFViewerBackend = ({ iframeId, source, viewerLocation = "/pdfjs-4.4.168-dist/web/viewer.html", documentName, documentNameColour = "black", toolbar = "minimal", element, ...remaining }) => {
1088
1323
  const sourceDeterminer = ResolvePDFSource(source);
1089
1324
  const iframe = document.createElement("iframe");
@@ -1100,6 +1335,8 @@ const PDFViewerBackend = ({ iframeId, source, viewerLocation = "/pdfjs-4.4.168-d
1100
1335
  iframe.src = `${viewerLocation}${queryString}`;
1101
1336
  iframe.style.width = "100%";
1102
1337
  iframe.style.height = "94%";
1338
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-downloads");
1339
+ iframe.setAttribute("allow", "cross-origin-isolated");
1103
1340
  if (sourceDeterminer.isBase64Source) {
1104
1341
  iframe.addEventListener("beforeunload", (event) => {
1105
1342
  event.preventDefault();
@@ -1124,13 +1361,22 @@ const Radios = (props) => {
1124
1361
  const processedItems = items
1125
1362
  ? items.map((item) => {
1126
1363
  if (item) {
1127
- return {
1128
- ...item,
1129
- ...(value != null && { checked: item.value === value }),
1130
- ...(defaultValue != null && {
1364
+ // Only set checked/defaultChecked - not both
1365
+ if (value != null) {
1366
+ return {
1367
+ ...item,
1368
+ checked: item.value === value,
1369
+ defaultChecked: undefined, // Remove defaultChecked if value is provided
1370
+ };
1371
+ }
1372
+ else if (defaultValue != null) {
1373
+ return {
1374
+ ...item,
1131
1375
  defaultChecked: item.value === defaultValue,
1132
- }),
1133
- };
1376
+ checked: undefined, // Remove checked if defaultValue is provided
1377
+ };
1378
+ }
1379
+ return item;
1134
1380
  }
1135
1381
  return item;
1136
1382
  })