@hmlr/govuk-react-components-library 1.0.2 → 1.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.
Files changed (34) hide show
  1. package/.stylelintrc.json +1 -1
  2. package/README.md +215 -1
  3. package/assets/uml-diagram.svg +731 -572
  4. package/bundle-analysis-main.html +2 -2
  5. package/bundle-analysis-pdfvc.html +1 -1
  6. package/dist/PDFViewerCanvas.cjs.css +4 -4
  7. package/dist/PDFViewerCanvas.cjs.js +91 -7
  8. package/dist/PDFViewerCanvas.cjs.js.map +1 -1
  9. package/dist/PDFViewerCanvas.esm.css +4 -4
  10. package/dist/PDFViewerCanvas.esm.js +91 -7
  11. package/dist/PDFViewerCanvas.esm.js.map +1 -1
  12. package/dist/index.cjs.css +13 -8
  13. package/dist/index.cjs.js +356 -60
  14. package/dist/index.cjs.js.map +1 -1
  15. package/dist/index.d.ts +104 -10
  16. package/dist/index.esm.css +13 -8
  17. package/dist/index.esm.js +357 -67
  18. package/dist/index.esm.js.map +1 -1
  19. package/dist/types/components/ExitThisPage/ExitThisPage.config.d.ts +1 -0
  20. package/dist/types/components/ExitThisPage/ExitThisPage.d.ts +4 -0
  21. package/dist/types/components/ExitThisPage/ExitThisPage.types.d.ts +17 -0
  22. package/dist/types/components/ExitThisPage/index.d.ts +2 -0
  23. package/dist/types/components/PDFViewer/ResolvePDFSource.d.ts +24 -1
  24. package/dist/types/components/Pagination/Pagination.types.d.ts +23 -13
  25. package/dist/types/components/Pagination/index.d.ts +2 -0
  26. package/dist/types/components/PasswordInput/PasswordInput.config.d.ts +1 -0
  27. package/dist/types/components/PasswordInput/PasswordInput.d.ts +4 -0
  28. package/dist/types/components/PasswordInput/PasswordInput.types.d.ts +49 -0
  29. package/dist/types/components/PasswordInput/index.d.ts +2 -0
  30. package/dist/types/components/index.d.ts +2 -0
  31. package/dist/types/utils/WithReference.types.d.ts +7 -2
  32. package/package.json +70 -66
  33. package/scripts/generateStories.mjs +433 -0
  34. package/dist/types/JestSetup.d.ts +0 -1
package/dist/index.cjs.js CHANGED
@@ -92,10 +92,10 @@ function ExtractAccordionConfigFromAttributes(remainingAttributes) {
92
92
  return accordionConfig;
93
93
  }
94
94
 
95
- const Accordion = ({ headingLevel: HeadingLevel = "h2", items, className, id, ...attributes }) => {
95
+ const Accordion = ({ headingLevel: HeadingLevel = "h2", items = [], className, id, ...attributes }) => {
96
96
  const remainingAttributes = { ...attributes };
97
97
  ExtractAccordionConfigFromAttributes(remainingAttributes);
98
- return (jsxRuntime.jsx("div", { ...remainingAttributes, id: id, className: `govuk-accordion ${className || ""}`, "data-module": "govuk-accordion", children: items.filter(Boolean).map((item, index) => (jsxRuntime.jsxs("div", { className: `govuk-accordion__section ${item.expanded ? "govuk-accordion__section--expanded" : ""}`, children: [jsxRuntime.jsxs("div", { className: "govuk-accordion__section-header", children: [jsxRuntime.jsx(HeadingLevel, { className: "govuk-accordion__section-heading", children: jsxRuntime.jsx("span", { className: "govuk-accordion__section-button", id: `${id}-heading-${index + 1}`, children: item.heading.children }) }), item.summary && (jsxRuntime.jsx("div", { className: "govuk-accordion__section-summary govuk-body", id: `${id}-summary-${index + 1}`, children: item.summary.children }))] }), jsxRuntime.jsx("div", { id: `${id}-content-${index + 1}`, className: "govuk-accordion__section-content", "aria-labelledby": `${id}-heading-${index + 1}`, children: item.content.children })] }, item.reactListKey || index))) }));
98
+ return (jsxRuntime.jsx("div", { ...remainingAttributes, id: id, className: `govuk-accordion ${className || ""}`, "data-module": "govuk-accordion", children: items.filter(Boolean).map((item, index) => (jsxRuntime.jsxs("div", { className: `govuk-accordion__section ${item.expanded ? "govuk-accordion__section--expanded" : ""}`, children: [jsxRuntime.jsxs("div", { className: "govuk-accordion__section-header", children: [jsxRuntime.jsx(HeadingLevel, { className: "govuk-accordion__section-heading", children: jsxRuntime.jsx("span", { className: "govuk-accordion__section-button", id: `${id}-heading-${index + 1}`, children: item.heading?.children }) }), item.summary && (jsxRuntime.jsx("div", { className: "govuk-accordion__section-summary govuk-body", id: `${id}-summary-${index + 1}`, children: item.summary.children }))] }), jsxRuntime.jsx("div", { id: `${id}-content-${index + 1}`, className: "govuk-accordion__section-content", "aria-labelledby": `${id}-heading-${index + 1}`, children: item.content?.children })] }, item.reactListKey || index))) }));
99
99
  };
100
100
 
101
101
  const LinkWithRef = ({ children, to, href, forwardedRef = null, ...attributes }) => {
@@ -831,6 +831,24 @@ function ConfigureOverallErrorSummary($scope, config) {
831
831
  govukFrontend.createAll(govukFrontend.ErrorSummary, config, $scope);
832
832
  }
833
833
 
834
+ const ExitThisPage = ({ children = "Exit this page", className, redirectUrl = "https://www.bbc.co.uk/weather", activatedText, timedOutText, pressTwoMoreTimesText, pressOneMoreTimeText, ...attributes }) => {
835
+ const i18nProps = {};
836
+ if (activatedText)
837
+ i18nProps["data-i18n.activated"] = activatedText;
838
+ if (timedOutText)
839
+ i18nProps["data-i18n.timed-out"] = timedOutText;
840
+ if (pressTwoMoreTimesText)
841
+ i18nProps["data-i18n.press-two-more-times"] = pressTwoMoreTimesText;
842
+ if (pressOneMoreTimeText)
843
+ i18nProps["data-i18n.press-one-more-time"] = pressOneMoreTimeText;
844
+ return (jsxRuntime.jsx("div", { ...attributes, ...i18nProps, className: `govuk-exit-this-page${className ? ` ${className}` : ""}`, "data-module": "govuk-exit-this-page", children: jsxRuntime.jsxs("a", { href: redirectUrl, role: "button", draggable: false, className: "govuk-button govuk-button--warning govuk-exit-this-page__button govuk-js-exit-this-page-button", "data-module": "govuk-button", rel: "nofollow noreferrer", children: [jsxRuntime.jsx("span", { className: "govuk-visually-hidden", children: "Emergency" }), " ", children] }) }));
845
+ };
846
+ ExitThisPage.displayName = "ExitThisPage";
847
+
848
+ function ConfigureOverallExitThisPage($scope) {
849
+ govukFrontend.createAll(govukFrontend.ExitThisPage, {}, $scope);
850
+ }
851
+
834
852
  const FileUpload = (props) => {
835
853
  const { className, errorMessage, formGroup, hint, label, "aria-describedby": describedBy, id, ...attributes } = props;
836
854
  let hintComponent;
@@ -955,6 +973,17 @@ const NotificationBanner = (props) => {
955
973
  return (jsxRuntime.jsxs("div", { className: `govuk-notification-banner ${typeClass} ${className || ""}`, role: roleAttribute, "aria-labelledby": titleId, "data-module": "govuk-notification-banner", ...(disableAutoFocus ? { "data-disable-auto-focus": "true" } : {}), ...attributes, children: [jsxRuntime.jsx("div", { className: "govuk-notification-banner__header", children: jsxRuntime.jsx(HeadingLevel, { className: "govuk-notification-banner__title", id: titleId, children: title }) }), jsxRuntime.jsx("div", { className: "govuk-notification-banner__content", children: typeof children === "string" ? (jsxRuntime.jsx("p", { className: "govuk-notification-banner__heading", children: children })) : (children) })] }));
956
974
  };
957
975
 
976
+ const SvgIcon = ({ type, ...props }) => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: 15, height: 13, "aria-hidden": "true", ...props, children: jsxRuntime.jsx("path", { d: type === "next"
977
+ ? "M8.107-.008L6.693 1.406 10.986 5.7H-2v2h12.896L6.71 11.676l1.378 1.449 6.744-6.406L8.107-.008z"
978
+ : "M6.594-.008-.133 6.72l6.744 6.406 1.377-1.449-4.185-3.977h12.896v-2H3.715l4.293-4.293L6.594-.008z" }) }));
979
+
980
+ const NavigationButton = ({ href, type, children, labelText, }) => {
981
+ const defaultText = type === "next" ? "Next" : "Previous";
982
+ const typeClass = type === "next" ? "next" : "prev";
983
+ const icon = (jsxRuntime.jsx(SvgIcon, { type: type, className: `govuk-pagination__icon govuk-pagination__icon--${typeClass}`, focusable: "false", "aria-hidden": "true" }));
984
+ return (jsxRuntime.jsxs("a", { className: "govuk-link govuk-pagination__link", href: href, rel: type, children: [type === "previous" && icon, jsxRuntime.jsxs("span", { className: "govuk-pagination__link-title", children: [children ?? defaultText, jsxRuntime.jsx("span", { className: "govuk-visually-hidden", children: " page" })] }), type === "next" && icon, labelText && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "govuk-visually-hidden", children: ":" }), jsxRuntime.jsx("span", { className: "govuk-pagination__link-label", children: labelText })] }))] }));
985
+ };
986
+
958
987
  const DOTS = "...";
959
988
  const UsePagination = ({ totalCount, pageSize, siblingCount = 1, currentPage, }) => {
960
989
  const range = (start, end) => {
@@ -990,29 +1019,13 @@ const UsePagination = ({ totalCount, pageSize, siblingCount = 1, currentPage, })
990
1019
  return paginationRange;
991
1020
  };
992
1021
 
993
- const SvgIcon = ({ type, ...props }) => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: 15, height: 13, "aria-hidden": "true", ...props, children: jsxRuntime.jsx("path", { d: type === "next"
994
- ? "M8.107-.008L6.693 1.406 10.986 5.7H-2v2h12.896L6.71 11.676l1.378 1.449 6.744-6.406L8.107-.008z"
995
- : "M6.594-.008-.133 6.72l6.744 6.406 1.377-1.449-4.185-3.977h12.896v-2H3.715l4.293-4.293L6.594-.008z" }) }));
996
-
997
- const NavigationButton = ({ isDisabled, onClick, children, name, type, }) => {
998
- const typeClass = type === "next" ? type : "prev";
999
- const Icon = (jsxRuntime.jsx(SvgIcon, { type: type, className: `govuk-pagination__icon govuk-pagination__icon--${typeClass}`, focusable: "false" }));
1000
- return isDisabled ? (jsxRuntime.jsxs("div", { rel: type, children: [type === "previous" && Icon, jsxRuntime.jsx("span", { className: "govuk-pagination__link-title", children: name }), type === "next" && Icon, children && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [type === "previous" && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0" }), jsxRuntime.jsx("span", { className: "govuk-visually-hidden", children: ":" }), children, type === "next" && jsxRuntime.jsx(jsxRuntime.Fragment, { children: " \u00A0" })] }))] })) : (jsxRuntime.jsxs("a", { className: "govuk-link govuk-pagination__link", href: "#", rel: type, onClick: onClick, children: [type === "previous" && Icon, jsxRuntime.jsx("span", { className: "govuk-pagination__link-title", children: name }), type === "next" && Icon, children && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [type === "previous" && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0\u00A0" }), jsxRuntime.jsx("span", { className: "govuk-visually-hidden", children: ":" }), children, type === "next" && jsxRuntime.jsx(jsxRuntime.Fragment, { children: "\u00A0\u00A0" })] }))] }));
1001
- };
1002
-
1003
- const Pagination = ({ onPageChange, totalCount, siblingCount = 1, currentPage, pageSize, className, previousName = "Previous", nextName = "Next", previousChildren, nextChildren, }) => {
1004
- const paginationRange = UsePagination({
1005
- currentPage,
1006
- totalCount,
1007
- siblingCount,
1008
- pageSize,
1009
- });
1010
- if (!paginationRange || paginationRange.length < 2)
1022
+ const Pagination = ({ previous, next, items, className, }) => {
1023
+ if (!previous && !next && (!items || items.length === 0))
1011
1024
  return null;
1012
- const onNext = () => onPageChange(currentPage + 1);
1013
- const onPrevious = () => onPageChange(currentPage - 1);
1014
- const lastPage = paginationRange[paginationRange.length - 1];
1015
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "govuk-!-padding-bottom-2" }), jsxRuntime.jsx("div", { className: `govuk-grid-column-full ${className || ""}`, children: jsxRuntime.jsxs("nav", { className: "govuk-pagination", role: "navigation", "aria-label": "Pagination", children: [jsxRuntime.jsx("div", { className: "govuk-pagination__prev", children: jsxRuntime.jsx(NavigationButton, { isDisabled: currentPage === 1, onClick: onPrevious, name: previousName, type: "previous", children: previousChildren }) }), jsxRuntime.jsx("ul", { className: "govuk-pagination__list", children: paginationRange.map((pageNumber, index) => pageNumber === DOTS ? (jsxRuntime.jsx("li", { className: "govuk-pagination__item govuk-pagination__item--ellipses", children: DOTS }, index)) : (jsxRuntime.jsx("li", { className: `govuk-pagination__item ${pageNumber === currentPage ? "govuk-pagination__item--current" : ""}`, children: jsxRuntime.jsx("a", { className: "govuk-link govuk-pagination__link", href: "#", "aria-label": `Page ${pageNumber}`, "aria-current": pageNumber === currentPage ? "page" : undefined, onClick: () => onPageChange(pageNumber), children: pageNumber }) }, index))) }), jsxRuntime.jsx("div", { className: "govuk-pagination__next", children: jsxRuntime.jsx(NavigationButton, { isDisabled: currentPage === lastPage, onClick: onNext, name: nextName, type: "next", children: nextChildren }) })] }) }), jsxRuntime.jsx("div", { className: "govuk-!-padding-bottom-4" })] }));
1025
+ // Block layout is used when there are no numbered page items — govuk renders
1026
+ // the nav with an extra class and the link title with --decorated modifier.
1027
+ const isBlockLayout = !items || items.length === 0;
1028
+ return (jsxRuntime.jsxs("nav", { className: `govuk-pagination${isBlockLayout ? " govuk-pagination--block" : ""}${className ? ` ${className}` : ""}`, role: "navigation", "aria-label": "Pagination", children: [previous && (jsxRuntime.jsx("div", { className: "govuk-pagination__prev", children: jsxRuntime.jsx(NavigationButton, { type: "previous", ...previous }) })), items && items.length > 0 && (jsxRuntime.jsx("ul", { className: "govuk-pagination__list", children: items.map((item, index) => item.ellipsis ? (jsxRuntime.jsx("li", { className: "govuk-pagination__item govuk-pagination__item--ellipses", children: DOTS }, index)) : (jsxRuntime.jsx("li", { className: `govuk-pagination__item${item.current ? " govuk-pagination__item--current" : ""}`, children: jsxRuntime.jsx("a", { className: "govuk-link govuk-pagination__link", href: item.href ?? "#", "aria-label": `Page ${item.number}`, ...(item.current ? { "aria-current": "page" } : {}), children: item.number }) }, index))) })), next && (jsxRuntime.jsx("div", { className: "govuk-pagination__next", children: jsxRuntime.jsx(NavigationButton, { type: "next", ...next }) }))] }));
1016
1029
  };
1017
1030
 
1018
1031
  const Panel = (props) => {
@@ -1021,6 +1034,49 @@ const Panel = (props) => {
1021
1034
  return (jsxRuntime.jsxs("div", { className: `govuk-panel govuk-panel--confirmation ${className || ""}`, ...attributes, children: [jsxRuntime.jsx(HeadingLevel, { className: "govuk-panel__title", children: titleChildren }), innerHtml] }));
1022
1035
  };
1023
1036
 
1037
+ const PasswordInput = React.forwardRef((props, ref) => {
1038
+ const { className, "aria-describedby": describedBy, errorMessage, formGroup, hint, label, name, id, autoComplete = "current-password", showPasswordText = "Show", hidePasswordText = "Hide", showPasswordAriaLabelText = "Show password", hidePasswordAriaLabelText = "Hide password", passwordShownAnnouncementText, passwordHiddenAnnouncementText, ...attributes } = props;
1039
+ let describedByValue = describedBy || "";
1040
+ let hintComponent = null;
1041
+ let errorMessageComponent = null;
1042
+ if (hint) {
1043
+ const hintId = `${id}-hint`;
1044
+ describedByValue += ` ${hintId}`;
1045
+ hintComponent = jsxRuntime.jsx(Hint, { ...hint, id: hintId });
1046
+ }
1047
+ if (errorMessage) {
1048
+ const errorId = id ? `${id}-error` : "";
1049
+ describedByValue += ` ${errorId}`;
1050
+ errorMessageComponent = jsxRuntime.jsx(ErrorMessage, { ...errorMessage, id: errorId });
1051
+ }
1052
+ // Build i18n data attributes for the govuk-frontend JS module
1053
+ const i18nProps = {};
1054
+ if (showPasswordText)
1055
+ i18nProps["data-i18n.show-password"] = showPasswordText;
1056
+ if (hidePasswordText)
1057
+ i18nProps["data-i18n.hide-password"] = hidePasswordText;
1058
+ if (showPasswordAriaLabelText)
1059
+ i18nProps["data-i18n.show-password-aria-label"] =
1060
+ showPasswordAriaLabelText;
1061
+ if (hidePasswordAriaLabelText)
1062
+ i18nProps["data-i18n.hide-password-aria-label"] =
1063
+ hidePasswordAriaLabelText;
1064
+ if (passwordShownAnnouncementText)
1065
+ i18nProps["data-i18n.password-shown-announcement"] =
1066
+ passwordShownAnnouncementText;
1067
+ if (passwordHiddenAnnouncementText)
1068
+ i18nProps["data-i18n.password-hidden-announcement"] =
1069
+ passwordHiddenAnnouncementText;
1070
+ // Label handles isPageHeading internally — no extra wrapper needed here
1071
+ const labelEl = jsxRuntime.jsx(Label, { ...label, htmlFor: id });
1072
+ return (jsxRuntime.jsxs("div", { className: `govuk-form-group${formGroup?.className ? ` ${formGroup.className}` : ""}${errorMessage ? " govuk-form-group--error" : ""} govuk-password-input`, "data-module": "govuk-password-input", ...i18nProps, children: [labelEl, hintComponent, errorMessageComponent, jsxRuntime.jsxs("div", { className: "govuk-input__wrapper govuk-password-input__wrapper", children: [jsxRuntime.jsx("input", { ref: ref, id: id, className: `govuk-input govuk-password-input__input govuk-js-password-input-input${className ? ` ${className}` : ""}${errorMessage ? " govuk-input--error" : ""}`, name: name || id, type: "password", spellCheck: false, autoComplete: autoComplete, autoCapitalize: "none", "aria-describedby": describedByValue.trim() || undefined, ...attributes }), jsxRuntime.jsx("button", { type: "button", className: "govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle", "data-module": "govuk-button", "aria-controls": id, "aria-label": showPasswordAriaLabelText, hidden: true, children: showPasswordText })] })] }));
1073
+ });
1074
+ PasswordInput.displayName = "PasswordInput";
1075
+
1076
+ function ConfigureOverallPasswordInput($scope) {
1077
+ govukFrontend.createAll(govukFrontend.PasswordInput, {}, $scope);
1078
+ }
1079
+
1024
1080
  function Slugify(str) {
1025
1081
  str = str.replace(/^\s+|\s+$/g, ""); // trim leading/trailing white space
1026
1082
  str = str.toLowerCase(); // convert string to lowercase
@@ -1031,28 +1087,281 @@ function Slugify(str) {
1031
1087
  return str;
1032
1088
  }
1033
1089
 
1090
+ /**
1091
+ * Convert a base64 string to an ArrayBuffer
1092
+ */
1093
+ const _base64ToArrayBuffer = (base64) => {
1094
+ const binaryString = window.atob(base64);
1095
+ const len = binaryString.length;
1096
+ const bytes = new Uint8Array(len);
1097
+ for (let i = 0; i < len; i++) {
1098
+ bytes[i] = binaryString.charCodeAt(i);
1099
+ }
1100
+ return bytes.buffer;
1101
+ };
1102
+ /**
1103
+ * Very small heuristic to detect a plain base64 string
1104
+ */
1105
+ 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);
1106
+ /**
1107
+ * ResolvePDFSource
1108
+ *
1109
+ * Accepts a source that may be:
1110
+ * - a plain base64 string (PDF binary encoded as base64)
1111
+ * - a data: URL containing base64 (data:application/pdf;base64,...)
1112
+ * - an existing blob: URL (blob:...)
1113
+ * - a Blob/File object
1114
+ * - a regular URL string (http(s) or same-origin path)
1115
+ *
1116
+ * Returns a SourceDeterminer:
1117
+ * - { isBase64Source: true, source: string } when an object URL was created
1118
+ * - { isBase64Source: false, source: string } when source can be used directly
1119
+ *
1120
+ * If an object URL is created, callers should revoke it when no longer needed:
1121
+ * `URL.revokeObjectURL(source)`
1122
+ */
1123
+ const ResolvePDFSource = (source) => {
1124
+ // Nothing provided
1125
+ if (source === undefined || source === null || source === "") {
1126
+ return { isBase64Source: false, source: "" };
1127
+ }
1128
+ // If source is already a Blob (File), create an object URL
1129
+ if (typeof source.arrayBuffer === "function" ||
1130
+ source instanceof Blob) {
1131
+ const blob = source;
1132
+ const objectUrl = URL.createObjectURL(blob);
1133
+ return { isBase64Source: true, source: objectUrl };
1134
+ }
1135
+ // If source is a string:
1136
+ if (typeof source === "string") {
1137
+ // If it's already a blob URL, return as-is
1138
+ if (source.startsWith("blob:")) {
1139
+ return { isBase64Source: false, source };
1140
+ }
1141
+ // If it's a data URL (data:application/pdf;base64,....)
1142
+ if (source.startsWith("data:")) {
1143
+ // data:[<mediatype>][;base64],<data>
1144
+ const commaIndex = source.indexOf(",");
1145
+ if (commaIndex > -1) {
1146
+ const meta = source.substring(0, commaIndex);
1147
+ const dataPart = source.substring(commaIndex + 1);
1148
+ // If it contains base64 marker, decode accordingly
1149
+ if (meta.indexOf(";base64") !== -1) {
1150
+ try {
1151
+ const arrayBuffer = _base64ToArrayBuffer(dataPart);
1152
+ const blob = new Blob([arrayBuffer], { type: "application/pdf" });
1153
+ const objectUrl = URL.createObjectURL(blob);
1154
+ return { isBase64Source: true, source: objectUrl };
1155
+ }
1156
+ catch (err) {
1157
+ console.error("ResolvePDFSource: failed to convert data: URL to blob", err);
1158
+ // fallback to returning original data URL
1159
+ return { isBase64Source: false, source };
1160
+ }
1161
+ }
1162
+ else {
1163
+ // Not base64-encoded data URL — return as is
1164
+ return { isBase64Source: false, source };
1165
+ }
1166
+ }
1167
+ }
1168
+ // If it's a plain base64 string (no data: prefix), convert
1169
+ if (isBase64(source)) {
1170
+ try {
1171
+ const arrayBuffer = _base64ToArrayBuffer(source);
1172
+ const blob = new Blob([arrayBuffer], { type: "application/pdf" });
1173
+ const objectUrl = URL.createObjectURL(blob);
1174
+ return { isBase64Source: true, source: objectUrl };
1175
+ }
1176
+ catch (err) {
1177
+ console.error("ResolvePDFSource: failed to convert base64 to blob", err);
1178
+ return { isBase64Source: false, source };
1179
+ }
1180
+ }
1181
+ // Otherwise treat as a normal URL (http(s) or same-origin path)
1182
+ return { isBase64Source: false, source };
1183
+ }
1184
+ // Fallback: convert to string
1185
+ try {
1186
+ const asString = String(source);
1187
+ return { isBase64Source: false, source: asString };
1188
+ }
1189
+ catch (err) {
1190
+ console.error("ResolvePDFSource: failed to convert source to string", err);
1191
+ return { isBase64Source: false, source: "" };
1192
+ }
1193
+ };
1194
+
1034
1195
  const PDFViewer = (props) => {
1035
1196
  const viewerRef = React.useRef(null);
1036
- const { iframeId, src, viewerLocation, documentName, documentNameColour, backend, toolbar = "minimal", additionalBackendAttributes, ...attributes } = props;
1197
+ const { iframeId, src, viewerLocation, documentName, documentNameColour, backend, toolbar = "minimal", additionalBackendAttributes,
1198
+ // optional: lets callers skip client-side fetch for remote resources
1199
+ disableClientFetch = false, ...attributes } = props;
1037
1200
  const [loading, setLoading] = React.useState(false);
1201
+ // ref for object URL (either created by ResolvePDFSource or by us after fetching)
1202
+ const objectUrlRef = React.useRef(null);
1203
+ // did ResolvePDFSource create the object URL? (helps understanding where to revoke)
1204
+ const createdByResolveRef = React.useRef(false);
1205
+ // fetch abort controller for cross-origin remote fetches
1206
+ const fetchControllerRef = React.useRef(null);
1207
+ const isRemoteUrl = (url) => {
1208
+ if (!url)
1209
+ return false;
1210
+ // Check if it's an HTTP(S) URL
1211
+ return /^https?:\/\//i.test(url);
1212
+ };
1038
1213
  React.useEffect(() => {
1214
+ let mounted = true;
1039
1215
  setLoading(true);
1040
1216
  const element = viewerRef.current;
1041
- if (!element)
1042
- return; // Guard against null
1043
- const jsProps = {
1044
- iframeId,
1045
- source: src,
1046
- viewerLocation,
1047
- documentName,
1048
- documentNameColour,
1049
- element,
1050
- toolbar,
1051
- ...additionalBackendAttributes,
1217
+ if (!element) {
1218
+ setLoading(false);
1219
+ return;
1220
+ }
1221
+ // cleanup previous URL/fetch if any
1222
+ if (fetchControllerRef.current) {
1223
+ try {
1224
+ fetchControllerRef.current.abort();
1225
+ }
1226
+ catch (err) {
1227
+ console.error("PDFViewer: fetch abort error:", err);
1228
+ /* ignore */
1229
+ }
1230
+ fetchControllerRef.current = null;
1231
+ }
1232
+ if (objectUrlRef.current) {
1233
+ try {
1234
+ URL.revokeObjectURL(objectUrlRef.current);
1235
+ }
1236
+ catch (err) {
1237
+ console.error("PDFViewer: revokeObjectURL error:", err);
1238
+ }
1239
+ objectUrlRef.current = null;
1240
+ createdByResolveRef.current = false;
1241
+ }
1242
+ const init = async () => {
1243
+ let sourceToUse = src;
1244
+ // 1) Let ResolvePDFSource handle base64/data/blob inputs synchronously.
1245
+ try {
1246
+ const resolved = ResolvePDFSource(src);
1247
+ if (resolved?.source) {
1248
+ sourceToUse = resolved.source;
1249
+ if (resolved.isBase64Source) {
1250
+ // ResolvePDFSource created an object URL for us (data/blob/base64)
1251
+ objectUrlRef.current = resolved.source;
1252
+ createdByResolveRef.current = true;
1253
+ }
1254
+ }
1255
+ }
1256
+ catch (err) {
1257
+ // If ResolvePDFSource throws, just ignore and fall back to src
1258
+ console.error("PDFViewer: ResolvePDFSource error:", err);
1259
+ }
1260
+ // 2) If ResolvePDFSource didn't create an object URL and the src is cross-origin HTTP(S),
1261
+ // attempt a client-side fetch to create an object URL (so viewer can load it safely).
1262
+ if (!objectUrlRef.current &&
1263
+ !disableClientFetch &&
1264
+ typeof src === "string" &&
1265
+ /^https?:\/\//i.test(src) &&
1266
+ isRemoteUrl(src)) {
1267
+ const controller = new AbortController();
1268
+ fetchControllerRef.current = controller;
1269
+ try {
1270
+ const resp = await fetch(src, {
1271
+ signal: controller.signal,
1272
+ credentials: "omit",
1273
+ });
1274
+ if (resp.ok) {
1275
+ const blob = await resp.blob();
1276
+ if (blob && blob.size > 0) {
1277
+ const obj = URL.createObjectURL(blob);
1278
+ objectUrlRef.current = obj;
1279
+ createdByResolveRef.current = false; // we created it via fetch
1280
+ sourceToUse = obj;
1281
+ }
1282
+ else {
1283
+ console.warn("PDFViewer: fetched blob is empty - falling back to original src");
1284
+ }
1285
+ }
1286
+ else {
1287
+ console.warn(`PDFViewer: fetch returned ${resp.status} ${resp.statusText} for ${src}`);
1288
+ // fallback: keep using original src
1289
+ }
1290
+ }
1291
+ catch (err) {
1292
+ if (err instanceof Error && err.name === "AbortError") {
1293
+ // aborted, nothing to do
1294
+ if (!mounted)
1295
+ return;
1296
+ }
1297
+ else {
1298
+ console.error("PDFViewer: error fetching cross-origin PDF:", err);
1299
+ }
1300
+ // fallback to source as-is (original remote URL)
1301
+ }
1302
+ }
1303
+ // 3) Prepare and call backend
1304
+ if (!mounted)
1305
+ return;
1306
+ const jsProps = {
1307
+ iframeId,
1308
+ source: sourceToUse,
1309
+ viewerLocation,
1310
+ documentName,
1311
+ documentNameColour,
1312
+ element,
1313
+ toolbar,
1314
+ ...additionalBackendAttributes,
1315
+ };
1316
+ try {
1317
+ backend(jsProps);
1318
+ }
1319
+ catch (err) {
1320
+ console.error("PDFViewer: backend threw an error:", err);
1321
+ }
1322
+ finally {
1323
+ if (mounted)
1324
+ setLoading(false);
1325
+ }
1326
+ }; // end init
1327
+ init();
1328
+ return () => {
1329
+ mounted = false;
1330
+ // cancel any ongoing fetch
1331
+ if (fetchControllerRef.current) {
1332
+ try {
1333
+ fetchControllerRef.current.abort();
1334
+ }
1335
+ catch (err) {
1336
+ console.error("PDFViewer: fetch abort error during cleanup:", err);
1337
+ }
1338
+ fetchControllerRef.current = null;
1339
+ }
1340
+ // revoke any object URL we created
1341
+ if (objectUrlRef.current) {
1342
+ try {
1343
+ URL.revokeObjectURL(objectUrlRef.current);
1344
+ }
1345
+ catch (err) {
1346
+ console.error("PDFViewer: fetch abort error during cleanup:", err);
1347
+ }
1348
+ objectUrlRef.current = null;
1349
+ createdByResolveRef.current = false;
1350
+ }
1351
+ // Note: PDFViewerBackend also attaches an iframe to the element. If backend does not
1352
+ // remove previous iframes, you might want to clear element.children here.
1052
1353
  };
1053
- backend(jsProps);
1054
- setLoading(false);
1055
- }, []);
1354
+ // re-run when these inputs change
1355
+ }, [
1356
+ src,
1357
+ iframeId,
1358
+ viewerLocation,
1359
+ documentName,
1360
+ documentNameColour,
1361
+ toolbar,
1362
+ backend,
1363
+ disableClientFetch,
1364
+ ]);
1056
1365
  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 })] }));
1057
1366
  };
1058
1367
 
@@ -1066,27 +1375,6 @@ const ObjectToQueryString = (queryParameters) => {
1066
1375
  : "";
1067
1376
  };
1068
1377
 
1069
- const _base64ToArrayBuffer = (base64) => {
1070
- const binaryString = window.atob(base64);
1071
- const len = binaryString.length;
1072
- const bytes = new Uint8Array(len);
1073
- for (let i = 0; i < len; i++) {
1074
- bytes[i] = binaryString.charCodeAt(i);
1075
- }
1076
- return bytes.buffer;
1077
- };
1078
- 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);
1079
- const ResolvePDFSource = (source) => {
1080
- const isBase64Source = isBase64(source);
1081
- if (!isBase64Source) {
1082
- return { isBase64Source, source };
1083
- }
1084
- const blob = new Blob([_base64ToArrayBuffer(source)], {
1085
- type: "application/pdf",
1086
- });
1087
- return { isBase64Source, source: URL.createObjectURL(blob) };
1088
- };
1089
-
1090
1378
  const PDFViewerBackend = ({ iframeId, source, viewerLocation = "/pdfjs-4.4.168-dist/web/viewer.html", documentName, documentNameColour = "black", toolbar = "minimal", element, ...remaining }) => {
1091
1379
  const sourceDeterminer = ResolvePDFSource(source);
1092
1380
  const iframe = document.createElement("iframe");
@@ -1103,6 +1391,8 @@ const PDFViewerBackend = ({ iframeId, source, viewerLocation = "/pdfjs-4.4.168-d
1103
1391
  iframe.src = `${viewerLocation}${queryString}`;
1104
1392
  iframe.style.width = "100%";
1105
1393
  iframe.style.height = "94%";
1394
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-downloads");
1395
+ iframe.setAttribute("allow", "cross-origin-isolated");
1106
1396
  if (sourceDeterminer.isBase64Source) {
1107
1397
  iframe.addEventListener("beforeunload", (event) => {
1108
1398
  event.preventDefault();
@@ -1326,11 +1616,14 @@ exports.ConfigureOverallAccordion = ConfigureOverallAccordion;
1326
1616
  exports.ConfigureOverallButton = ConfigureOverallButton;
1327
1617
  exports.ConfigureOverallCheckboxes = ConfigureOverallCheckboxes;
1328
1618
  exports.ConfigureOverallErrorSummary = ConfigureOverallErrorSummary;
1619
+ exports.ConfigureOverallExitThisPage = ConfigureOverallExitThisPage;
1329
1620
  exports.ConfigureOverallHeader = ConfigureOverallHeader;
1621
+ exports.ConfigureOverallPasswordInput = ConfigureOverallPasswordInput;
1330
1622
  exports.ConfigureOverallRadios = ConfigureOverallRadios;
1331
1623
  exports.ConfigureOverallSkipLink = ConfigureOverallSkipLink;
1332
1624
  exports.ConfigureOverallTabs = ConfigureOverallTabs;
1333
1625
  exports.CookieBanner = CookieBanner;
1626
+ exports.DOTS = DOTS;
1334
1627
  exports.DataNavigation = DataNavigation;
1335
1628
  exports.DateInput = DateInput;
1336
1629
  exports.Details = Details;
@@ -1338,6 +1631,7 @@ exports.DifferenceNavigation = DifferenceNavigation;
1338
1631
  exports.ErrorBoundary = ErrorBoundary;
1339
1632
  exports.ErrorMessage = ErrorMessage;
1340
1633
  exports.ErrorSummary = ErrorSummary;
1634
+ exports.ExitThisPage = ExitThisPage;
1341
1635
  exports.ExtractAccordionConfigFromAttributes = ExtractAccordionConfigFromAttributes;
1342
1636
  exports.Fieldset = Fieldset;
1343
1637
  exports.FileUpload = FileUpload;
@@ -1357,6 +1651,7 @@ exports.PDFViewer = PDFViewer;
1357
1651
  exports.PDFViewerBackend = PDFViewerBackend;
1358
1652
  exports.Pagination = Pagination;
1359
1653
  exports.Panel = Panel;
1654
+ exports.PasswordInput = PasswordInput;
1360
1655
  exports.PhaseBanner = PhaseBanner;
1361
1656
  exports.ProblemWithService = ProblemWithService;
1362
1657
  exports.Radios = Radios;
@@ -1369,6 +1664,7 @@ exports.Tabs = Tabs;
1369
1664
  exports.Tag = Tag;
1370
1665
  exports.TaskList = TaskList;
1371
1666
  exports.Textarea = Textarea;
1667
+ exports.UsePagination = UsePagination;
1372
1668
  exports.WarningInfo = WarningInfo;
1373
1669
  exports.WarningText = WarningText;
1374
1670
  //# sourceMappingURL=index.cjs.js.map