@communitiesuk/svelte-component-library 0.1.17

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 (217) hide show
  1. package/README.md +188 -0
  2. package/dist/assets/css/govuk-frontend.min.css +2 -0
  3. package/dist/assets/css/govuk-frontend.min.css.map +1 -0
  4. package/dist/assets/fonts/bold-affa96571d-v2.woff +0 -0
  5. package/dist/assets/fonts/bold-b542beb274-v2.woff2 +0 -0
  6. package/dist/assets/fonts/light-94a07e06a1-v2.woff2 +0 -0
  7. package/dist/assets/fonts/light-f591b13f7d-v2.woff +0 -0
  8. package/dist/assets/govuk_publishing_components/images/icon-autocomplete-search-suggestion.svg +4 -0
  9. package/dist/assets/govuk_publishing_components/images/icon-close.svg +1 -0
  10. package/dist/assets/images/favicon.ico +0 -0
  11. package/dist/assets/images/favicon.svg +1 -0
  12. package/dist/assets/images/govuk-crest-2x.png +0 -0
  13. package/dist/assets/images/govuk-crest.png +0 -0
  14. package/dist/assets/images/govuk-crest.svg +1 -0
  15. package/dist/assets/images/govuk-icon-180.png +0 -0
  16. package/dist/assets/images/govuk-icon-192.png +0 -0
  17. package/dist/assets/images/govuk-icon-512.png +0 -0
  18. package/dist/assets/images/govuk-icon-mask.svg +1 -0
  19. package/dist/assets/images/govuk-opengraph-image.png +0 -0
  20. package/dist/assets/images/homepage-illustration.svg +1 -0
  21. package/dist/assets/images/homepage.svg +44 -0
  22. package/dist/assets/images/masthead-illustration.svg +123 -0
  23. package/dist/assets/images/oflog_crest_black.png +0 -0
  24. package/dist/assets/images/oflog_crest_white.png +0 -0
  25. package/dist/assets/images/undraw_approved-wireframe_odf4.svg +1 -0
  26. package/dist/assets/images/undraw_collaborators_rgw4.svg +1 -0
  27. package/dist/assets/images/undraw_content-creator_vuqg.svg +1 -0
  28. package/dist/assets/images/undraw_online-media_opxh.svg +1 -0
  29. package/dist/assets/images/undraw_pull-request_zlsu.svg +1 -0
  30. package/dist/assets/images/undraw_reviewed-docs_g0cg.svg +1 -0
  31. package/dist/components/FilterPanel/codeBlocks.d.ts +3 -0
  32. package/dist/components/FilterPanel/codeBlocks.js +418 -0
  33. package/dist/components/content/InsetText.svelte +21 -0
  34. package/dist/components/content/InsetText.svelte.d.ts +7 -0
  35. package/dist/components/content/WarningText.svelte +27 -0
  36. package/dist/components/content/WarningText.svelte.d.ts +8 -0
  37. package/dist/components/data-vis/axis/Axis.svelte +51 -0
  38. package/dist/components/data-vis/axis/Axis.svelte.d.ts +33 -0
  39. package/dist/components/data-vis/axis/Ticks.svelte +113 -0
  40. package/dist/components/data-vis/axis/Ticks.svelte.d.ts +33 -0
  41. package/dist/components/data-vis/line-chart/Line.svelte +150 -0
  42. package/dist/components/data-vis/line-chart/Line.svelte.d.ts +85 -0
  43. package/dist/components/data-vis/line-chart/LineChart.svelte +249 -0
  44. package/dist/components/data-vis/line-chart/LineChart.svelte.d.ts +73 -0
  45. package/dist/components/data-vis/line-chart/Lines.svelte +138 -0
  46. package/dist/components/data-vis/line-chart/Lines.svelte.d.ts +57 -0
  47. package/dist/components/data-vis/line-chart/Marker.svelte +61 -0
  48. package/dist/components/data-vis/line-chart/Marker.svelte.d.ts +37 -0
  49. package/dist/components/data-vis/line-chart/SeriesLabel.svelte +67 -0
  50. package/dist/components/data-vis/line-chart/SeriesLabel.svelte.d.ts +43 -0
  51. package/dist/components/data-vis/line-chart/ValueLabel.svelte +50 -0
  52. package/dist/components/data-vis/line-chart/ValueLabel.svelte.d.ts +25 -0
  53. package/dist/components/data-vis/map/Map.svelte +392 -0
  54. package/dist/components/data-vis/map/Map.svelte.d.ts +47 -0
  55. package/dist/components/data-vis/map/MapLegend.svelte +41 -0
  56. package/dist/components/data-vis/map/MapLegend.svelte.d.ts +15 -0
  57. package/dist/components/data-vis/map/NonStandardControls.svelte +42 -0
  58. package/dist/components/data-vis/map/NonStandardControls.svelte.d.ts +13 -0
  59. package/dist/components/data-vis/map/Tooltip.svelte +41 -0
  60. package/dist/components/data-vis/map/Tooltip.svelte.d.ts +19 -0
  61. package/dist/components/data-vis/map/colorbrewer.d.ts +337 -0
  62. package/dist/components/data-vis/map/colorbrewer.js +1523 -0
  63. package/dist/components/data-vis/map/colors.d.ts +13 -0
  64. package/dist/components/data-vis/map/colors.js +65 -0
  65. package/dist/components/data-vis/map/dataJoin.d.ts +2 -0
  66. package/dist/components/data-vis/map/dataJoin.js +27 -0
  67. package/dist/components/data-vis/map/fullTopo.json +1 -0
  68. package/dist/components/data-vis/map/jenks.d.ts +1 -0
  69. package/dist/components/data-vis/map/jenks.js +51 -0
  70. package/dist/components/data-vis/map/lad2023.json +1 -0
  71. package/dist/components/data-vis/map/mapUtils.d.ts +5 -0
  72. package/dist/components/data-vis/map/mapUtils.js +86 -0
  73. package/dist/components/data-vis/map/topo.json +1 -0
  74. package/dist/components/data-vis/table/Table.svelte +247 -0
  75. package/dist/components/data-vis/table/Table.svelte.d.ts +19 -0
  76. package/dist/components/layout/Breadcrumbs.svelte +191 -0
  77. package/dist/components/layout/Breadcrumbs.svelte.d.ts +24 -0
  78. package/dist/components/layout/Footer.svelte +171 -0
  79. package/dist/components/layout/Footer.svelte.d.ts +30 -0
  80. package/dist/components/layout/Header.svelte +43 -0
  81. package/dist/components/layout/Header.svelte.d.ts +7 -0
  82. package/dist/components/layout/InternalHeader.svelte +628 -0
  83. package/dist/components/layout/InternalHeader.svelte.d.ts +15 -0
  84. package/dist/components/layout/PhaseBanner.svelte +28 -0
  85. package/dist/components/layout/PhaseBanner.svelte.d.ts +9 -0
  86. package/dist/components/layout/ServiceNavigation.svelte +143 -0
  87. package/dist/components/layout/ServiceNavigation.svelte.d.ts +13 -0
  88. package/dist/components/layout/SideNavigation.svelte +345 -0
  89. package/dist/components/layout/SideNavigation.svelte.d.ts +25 -0
  90. package/dist/components/layout/service-navigation-nested-mobile/HeaderNav.svelte +91 -0
  91. package/dist/components/layout/service-navigation-nested-mobile/HeaderNav.svelte.d.ts +15 -0
  92. package/dist/components/layout/service-navigation-nested-mobile/MobileNav.svelte +233 -0
  93. package/dist/components/layout/service-navigation-nested-mobile/MobileNav.svelte.d.ts +27 -0
  94. package/dist/components/layout/service-navigation-nested-mobile/ServiceNavigationNestedMobile.svelte +70 -0
  95. package/dist/components/layout/service-navigation-nested-mobile/ServiceNavigationNestedMobile.svelte.d.ts +11 -0
  96. package/dist/components/layout/service-navigation-nested-mobile/SideNav.svelte +276 -0
  97. package/dist/components/layout/service-navigation-nested-mobile/SideNav.svelte.d.ts +22 -0
  98. package/dist/components/ui/Accordion.svelte +244 -0
  99. package/dist/components/ui/Accordion.svelte.d.ts +23 -0
  100. package/dist/components/ui/Breadcrumbs.svelte +198 -0
  101. package/dist/components/ui/Breadcrumbs.svelte.d.ts +24 -0
  102. package/dist/components/ui/Button.svelte +96 -0
  103. package/dist/components/ui/Button.svelte.d.ts +17 -0
  104. package/dist/components/ui/CheckBox.svelte +198 -0
  105. package/dist/components/ui/CheckBox.svelte.d.ts +27 -0
  106. package/dist/components/ui/ContentsList.svelte +1117 -0
  107. package/dist/components/ui/ContentsList.svelte.d.ts +25 -0
  108. package/dist/components/ui/DateInput.svelte +255 -0
  109. package/dist/components/ui/DateInput.svelte.d.ts +59 -0
  110. package/dist/components/ui/Details.svelte +12 -0
  111. package/dist/components/ui/Details.svelte.d.ts +13 -0
  112. package/dist/components/ui/FilterPanel.svelte +588 -0
  113. package/dist/components/ui/FilterPanel.svelte.d.ts +74 -0
  114. package/dist/components/ui/Footer.svelte +171 -0
  115. package/dist/components/ui/Footer.svelte.d.ts +30 -0
  116. package/dist/components/ui/Header.svelte +43 -0
  117. package/dist/components/ui/Header.svelte.d.ts +7 -0
  118. package/dist/components/ui/Masthead.svelte +267 -0
  119. package/dist/components/ui/Masthead.svelte.d.ts +12 -0
  120. package/dist/components/ui/NavigationExample.svelte +117 -0
  121. package/dist/components/ui/NavigationExample.svelte.d.ts +3 -0
  122. package/dist/components/ui/NotificationBanner.svelte +93 -0
  123. package/dist/components/ui/NotificationBanner.svelte.d.ts +15 -0
  124. package/dist/components/ui/Radios.svelte +176 -0
  125. package/dist/components/ui/Radios.svelte.d.ts +28 -0
  126. package/dist/components/ui/RelatedContent.svelte +596 -0
  127. package/dist/components/ui/RelatedContent.svelte.d.ts +29 -0
  128. package/dist/components/ui/Search.svelte +499 -0
  129. package/dist/components/ui/Search.svelte.d.ts +32 -0
  130. package/dist/components/ui/SearchAutocomplete.svelte +655 -0
  131. package/dist/components/ui/SearchAutocomplete.svelte.d.ts +37 -0
  132. package/dist/components/ui/Select.svelte +116 -0
  133. package/dist/components/ui/Select.svelte.d.ts +22 -0
  134. package/dist/components/ui/ServiceNavigation.svelte +143 -0
  135. package/dist/components/ui/ServiceNavigation.svelte.d.ts +13 -0
  136. package/dist/components/ui/SideNavigation.svelte +346 -0
  137. package/dist/components/ui/SideNavigation.svelte.d.ts +25 -0
  138. package/dist/components/ui/Tabs.svelte +306 -0
  139. package/dist/components/ui/Tabs.svelte.d.ts +18 -0
  140. package/dist/components/ui/WhatsNew.svelte +155 -0
  141. package/dist/components/ui/WhatsNew.svelte.d.ts +29 -0
  142. package/dist/config.d.ts +51 -0
  143. package/dist/config.js +44 -0
  144. package/dist/icons/DoubleChevronButton.svelte +62 -0
  145. package/dist/icons/DoubleChevronButton.svelte.d.ts +13 -0
  146. package/dist/icons/IconSearch.svelte +42 -0
  147. package/dist/icons/IconSearch.svelte.d.ts +6 -0
  148. package/dist/icons/SingleChevronButtonWithLabel.svelte +132 -0
  149. package/dist/icons/SingleChevronButtonWithLabel.svelte.d.ts +19 -0
  150. package/dist/index.d.ts +44 -0
  151. package/dist/index.js +45 -0
  152. package/dist/main.css +1 -0
  153. package/dist/package-wrapping/BaseInformation.svelte +82 -0
  154. package/dist/package-wrapping/BaseInformation.svelte.d.ts +15 -0
  155. package/dist/package-wrapping/BaseNameAndStatus.svelte +108 -0
  156. package/dist/package-wrapping/BaseNameAndStatus.svelte.d.ts +10 -0
  157. package/dist/package-wrapping/CodeBlock.svelte +62 -0
  158. package/dist/package-wrapping/CodeBlock.svelte.d.ts +12 -0
  159. package/dist/package-wrapping/ComponentDemo.svelte +114 -0
  160. package/dist/package-wrapping/ComponentDemo.svelte.d.ts +25 -0
  161. package/dist/package-wrapping/ComponentDemoTEMP.svelte +305 -0
  162. package/dist/package-wrapping/ComponentDemoTEMP.svelte.d.ts +21 -0
  163. package/dist/package-wrapping/ComponentDetails.svelte +123 -0
  164. package/dist/package-wrapping/ComponentDetails.svelte.d.ts +13 -0
  165. package/dist/package-wrapping/DividerLine.svelte +21 -0
  166. package/dist/package-wrapping/DividerLine.svelte.d.ts +17 -0
  167. package/dist/package-wrapping/InputForParameter.svelte +205 -0
  168. package/dist/package-wrapping/InputForParameter.svelte.d.ts +13 -0
  169. package/dist/package-wrapping/InputForParameterUpdated.svelte +222 -0
  170. package/dist/package-wrapping/InputForParameterUpdated.svelte.d.ts +17 -0
  171. package/dist/package-wrapping/InputForParameterUpdatedTEMP.svelte +203 -0
  172. package/dist/package-wrapping/InputForParameterUpdatedTEMP.svelte.d.ts +17 -0
  173. package/dist/package-wrapping/ListOfComponentStatuses.svelte +19 -0
  174. package/dist/package-wrapping/ListOfComponentStatuses.svelte.d.ts +11 -0
  175. package/dist/package-wrapping/OverlayAndComponentContainer.svelte +426 -0
  176. package/dist/package-wrapping/OverlayAndComponentContainer.svelte.d.ts +33 -0
  177. package/dist/package-wrapping/ParametersSection.svelte +235 -0
  178. package/dist/package-wrapping/ParametersSection.svelte.d.ts +19 -0
  179. package/dist/package-wrapping/ParsingErrorToastsContainer.svelte +50 -0
  180. package/dist/package-wrapping/ParsingErrorToastsContainer.svelte.d.ts +15 -0
  181. package/dist/package-wrapping/Pill.svelte +54 -0
  182. package/dist/package-wrapping/Pill.svelte.d.ts +25 -0
  183. package/dist/package-wrapping/PlaygroundDetails.svelte +106 -0
  184. package/dist/package-wrapping/PlaygroundDetails.svelte.d.ts +13 -0
  185. package/dist/package-wrapping/ScreenSizeRadio.svelte +24 -0
  186. package/dist/package-wrapping/ScreenSizeRadio.svelte.d.ts +11 -0
  187. package/dist/package-wrapping/ScreenSizeRadioUpdated.svelte +23 -0
  188. package/dist/package-wrapping/ScreenSizeRadioUpdated.svelte.d.ts +11 -0
  189. package/dist/package-wrapping/SidebarContainer.svelte +103 -0
  190. package/dist/package-wrapping/SidebarContainer.svelte.d.ts +23 -0
  191. package/dist/package-wrapping/WrapperDetailsUpdate.svelte +40 -0
  192. package/dist/package-wrapping/WrapperDetailsUpdate.svelte.d.ts +15 -0
  193. package/dist/package-wrapping/templates/Template.svelte +100 -0
  194. package/dist/package-wrapping/templates/Template.svelte.d.ts +25 -0
  195. package/dist/templates/ComponentPageTemplate.svelte +1 -0
  196. package/dist/templates/ComponentPageTemplate.svelte.d.ts +26 -0
  197. package/dist/utils/data-transformations/convertCSV.d.ts +2 -0
  198. package/dist/utils/data-transformations/convertCSV.js +22 -0
  199. package/dist/utils/data-transformations/getValueFromParametersArray.d.ts +1 -0
  200. package/dist/utils/data-transformations/getValueFromParametersArray.js +9 -0
  201. package/dist/utils/layoutNavHelpers.d.ts +70 -0
  202. package/dist/utils/layoutNavHelpers.js +129 -0
  203. package/dist/utils/package-wrapping-specific/addIndexAndInitialValue.d.ts +1 -0
  204. package/dist/utils/package-wrapping-specific/addIndexAndInitialValue.js +21 -0
  205. package/dist/utils/package-wrapping-specific/createBindableParametersValuesArray.d.ts +1 -0
  206. package/dist/utils/package-wrapping-specific/createBindableParametersValuesArray.js +12 -0
  207. package/dist/utils/package-wrapping-specific/createParametersObject.d.ts +1 -0
  208. package/dist/utils/package-wrapping-specific/createParametersObject.js +29 -0
  209. package/dist/utils/package-wrapping-specific/defineDefaultEventHandler.d.ts +1 -0
  210. package/dist/utils/package-wrapping-specific/defineDefaultEventHandler.js +14 -0
  211. package/dist/utils/package-wrapping-specific/trackVisibleParameters.d.ts +1 -0
  212. package/dist/utils/package-wrapping-specific/trackVisibleParameters.js +29 -0
  213. package/dist/utils/syntax-highlighting/shikiHighlight.d.ts +7 -0
  214. package/dist/utils/syntax-highlighting/shikiHighlight.js +76 -0
  215. package/dist/utils/text-string-conversion/textStringConversion.d.ts +9 -0
  216. package/dist/utils/text-string-conversion/textStringConversion.js +86 -0
  217. package/package.json +113 -0
@@ -0,0 +1,655 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { clsx } from "clsx";
4
+ import Search from "./Search.svelte"; // Base component
5
+ import "accessible-autocomplete/dist/accessible-autocomplete.min.css";
6
+ import { browser } from "$app/environment";
7
+ import suggestionIconUrl from "../../assets/govuk_publishing_components/images/icon-autocomplete-search-suggestion.svg?url";
8
+ import closeIconUrl from "../../assets/govuk_publishing_components/images/icon-close.svg?url"; // Import for the cancel button
9
+
10
+
11
+ // SSR-safe HTML sanitizer: no-op on server
12
+ let sanitize: (html: string) => string = (html) => html;
13
+
14
+ // --- Define Props ---
15
+ type SuggestionObject = { label: string; value: any };
16
+ type Suggestion = string | SuggestionObject;
17
+ type Props = {
18
+ // User can supply either an options array or an API source
19
+ options?: Suggestion[]; // Predefined suggestions list
20
+ source_url?: string; // Optional: URL for autocomplete suggestions
21
+ source_key?: string; // Optional: Key in the JSON response containing suggestions array
22
+ source_property?: string; // Property to extract from API objects
23
+ outerClasses?: string; // Optional classes for the outer wrapper
24
+ outerDataAttributes?: Record<string, string>; // Optional data attributes for the outer wrapper
25
+ // Add other expected props passed down (e.g., size, on_govuk_blue, id, name etc.)
26
+ size?: "large" | "";
27
+ on_govuk_blue?: boolean;
28
+ homepage?: boolean;
29
+ id?: string;
30
+ name?: string;
31
+ label_text?: string;
32
+ button_text?: string;
33
+ // Include any other props you expect to pass through
34
+ [key: string]: any; // Allow other props via rest spread, less type-safe
35
+ // --- Add new props for accessible-autocomplete config ---
36
+ minLength?: number;
37
+ confirmOnBlur?: boolean;
38
+ showNoOptionsFound?: boolean;
39
+ defaultValue?: string;
40
+ placeholder?: string;
41
+ required?: boolean;
42
+ tNoResults?: () => string; // Function prop
43
+ tAssistiveHint?: () => string; // Function prop
44
+ menuAttributes?: Record<string, any>; // Object prop
45
+ menuClasses?: string | null;
46
+ hint?: string; // Add hint prop
47
+ selectedValue?: any; // Bindable selected value, updated on selection
48
+ };
49
+
50
+ let {
51
+ options = undefined,
52
+ source_url = undefined,
53
+ source_key = undefined,
54
+ source_property = undefined,
55
+ size = "", // Default size from Search
56
+ on_govuk_blue = false,
57
+ homepage = false, // Added homepage prop handling
58
+ outerClasses = "",
59
+ outerDataAttributes = {},
60
+ id, // Pass down id
61
+ name = "q", // Pass down name or use default
62
+ label_text = "Search", // Example: Default label
63
+ button_text = "Search", // Pass down button text
64
+ // --- Destructure new props ---
65
+ minLength = 3, // Default from accessible-autocomplete
66
+ confirmOnBlur = false, // Default from accessible-autocomplete
67
+ showNoOptionsFound = true, // Default from accessible-autocomplete
68
+ defaultValue = "", // Default to empty string
69
+ placeholder = "",
70
+ required = false,
71
+ tNoResults = () => "No results found", // Default function
72
+ tAssistiveHint = () =>
73
+ "When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.", // Default function
74
+ menuAttributes = {},
75
+ menuClasses = "", // Default to empty string
76
+ hint = undefined, // Add hint destructuring
77
+ selectedValue = $bindable(), // Bindable prop for selected value
78
+ ...restSearchProps // Other props for the base Search component
79
+ }: Props = $props();
80
+
81
+ let containerElement: HTMLDivElement; // bind:this target for the outer div
82
+ let autocompleteInstance: { inputElement?: HTMLInputElement } | null = null; // To store instance if needed
83
+
84
+ // --- Derived Values ---
85
+ const wrapperClasses = $derived(
86
+ clsx(
87
+ "gem-c-search-with-autocomplete",
88
+ size === "large" && "gem-c-search-with-autocomplete--large",
89
+ (on_govuk_blue || homepage) &&
90
+ "gem-c-search-with-autocomplete--on-govuk-blue", // Apply blue style if either is true
91
+ // TODO: Replicate margin logic from Ruby template if needed using govuk utility classes
92
+ outerClasses,
93
+ ),
94
+ );
95
+
96
+ // Prepare props for the inner Search component
97
+ const searchComponentOptions = $derived({
98
+ ...restSearchProps,
99
+ label_text, // Pass down explicitly
100
+ button_text, // Pass down explicitly
101
+ id, // Pass id down
102
+ name, // Pass name down
103
+ inline_label: false, // Force separate label for autocomplete compatibility
104
+ size, // Pass size down
105
+ on_govuk_blue, // Pass blue status down
106
+ homepage, // Pass homepage status down
107
+ hint, // Pass the hint prop down
108
+ value: undefined, // Don't pass initial value, let autocomplete handle it
109
+ });
110
+
111
+ // custom "too-short" message
112
+ function tooShort(min: number) {
113
+ return `Enter ${min} or more characters for suggestions`;
114
+ }
115
+
116
+ // --- Lifecycle & Autocomplete Initialisation ---
117
+ onMount(async () => {
118
+ // console.log("SearchAutocomplete: onMount started.");
119
+
120
+ if (browser) {
121
+ // hook up DOMPurify sanitize in browser
122
+ const DOMPurify = (await import("dompurify")).default;
123
+ sanitize = DOMPurify.sanitize;
124
+
125
+ // now load autocomplete
126
+ const accessibleAutocomplete = (await import("accessible-autocomplete"))
127
+ .default;
128
+
129
+ // Assert type for querySelector results
130
+ const targetInputWrapper = containerElement?.querySelector(
131
+ ".js-search-input-wrapper",
132
+ ) as HTMLDivElement | null;
133
+ const searchInput = containerElement?.querySelector(
134
+ 'input[type="search"]',
135
+ ) as HTMLInputElement | null; // Assert as HTMLInputElement
136
+
137
+ if (!targetInputWrapper || !searchInput) {
138
+ console.error(
139
+ "SearchAutocomplete: Could not find target elements for initialisation.",
140
+ );
141
+ return;
142
+ }
143
+
144
+ // --- Define Source Functions ---
145
+
146
+ // Source function for fetching from API
147
+ const getResultsFromApi = (
148
+ query: string,
149
+ populateResults: (results: string[]) => void,
150
+ ) => {
151
+ if (!source_url || !source_key) {
152
+ console.error(
153
+ "SearchAutocomplete: source_url and source_key are required for API mode.",
154
+ );
155
+ populateResults([]);
156
+ return;
157
+ }
158
+ const url = new URL(source_url);
159
+ url.searchParams.set("q", query);
160
+ fetch(url, { headers: { Accept: "application/json" } })
161
+ .then((response) => {
162
+ if (!response.ok)
163
+ throw new Error(`HTTP error! status: ${response.status}`);
164
+ return response.json();
165
+ })
166
+ .then((data) => {
167
+ // console.log("SearchAutocomplete: Data fetched:", data);
168
+ const results = data[source_key] || [];
169
+ if (!Array.isArray(results)) {
170
+ console.error(
171
+ "SearchAutocomplete: Source key did not return an array.",
172
+ );
173
+ populateResults([]);
174
+ return;
175
+ }
176
+ // --- API MAPPING LOGIC ---
177
+ populateResults(
178
+ results.map((item: any) => {
179
+ if (typeof item === "string") {
180
+ return item;
181
+ } else if (item && typeof item === "object") {
182
+ // 1. Try the specified source_property if provided
183
+ if (source_property && source_property in item) {
184
+ return String(item[source_property]);
185
+ }
186
+ // 2. Fallback to 'label'
187
+ if ("label" in item) return String(item.label);
188
+ // 3. Fallback to 'postcode' (for backwards compatibility/common case)
189
+ if ("postcode" in item) return String(item.postcode);
190
+ // 4. If none found, stringify the object (might be undesirable)
191
+ console.warn(
192
+ "SearchAutocomplete: Could not find 'label', 'postcode', or specified 'source_property' in object:",
193
+ item,
194
+ );
195
+ }
196
+ // 5. Final fallback: stringify the item
197
+ return String(item ?? ""); // Ensure null/undefined becomes empty string
198
+ }),
199
+ );
200
+ })
201
+ .catch((error) => {
202
+ console.error("SearchAutocomplete: Error fetching results:", error);
203
+ populateResults([]);
204
+ });
205
+ };
206
+
207
+ // Source function for using provided options array
208
+ const getResultsFromOptions = (
209
+ query: string,
210
+ populateResults: (results: Suggestion[]) => void,
211
+ ) => {
212
+ if (!options) {
213
+ populateResults([]);
214
+ return;
215
+ }
216
+ const lowerQuery = query.toLowerCase();
217
+ const filtered = options.filter((option) => {
218
+ const label = typeof option === "string" ? option : option.label;
219
+ return label.toLowerCase().includes(lowerQuery);
220
+ });
221
+ populateResults(filtered);
222
+ };
223
+
224
+ // Determine which source to use
225
+ const useOptions = Array.isArray(options) && options.length > 0;
226
+ const sourceFunction = useOptions
227
+ ? getResultsFromOptions
228
+ : getResultsFromApi;
229
+
230
+ // Define suggestion template function (sanitize and highlight)
231
+ const suggestionTemplate = (result: Suggestion): string => {
232
+ const displayLabel = typeof result === "string" ? result : result.label;
233
+ // Basic sanitization
234
+ const scratch = document.createElement("div");
235
+ scratch.textContent = displayLabel;
236
+ const sanitizedResult = sanitize(scratch.innerHTML);
237
+
238
+ // Get the input value directly from the DOM input created by the library
239
+ const inputElement = containerElement?.querySelector(
240
+ ".gem-c-search-with-autocomplete__input",
241
+ ) as HTMLInputElement | null; // Find the specific input
242
+ const currentInputValue = inputElement?.value?.toLowerCase() || ""; // Get its value
243
+
244
+ const index = currentInputValue
245
+ ? sanitizedResult.toLowerCase().indexOf(currentInputValue)
246
+ : -1;
247
+ let html = sanitizedResult;
248
+
249
+ if (currentInputValue && index !== -1) {
250
+ const before = sanitizedResult.slice(0, index);
251
+ const match = sanitizedResult.slice(
252
+ index,
253
+ index + currentInputValue.length,
254
+ );
255
+ const after = sanitizedResult.slice(index + currentInputValue.length);
256
+ html = `${before}<mark class="gem-c-search-with-autocomplete__suggestion-highlight">${match}</mark>${after}`;
257
+ }
258
+
259
+ // Match the GOV.UK structure
260
+ return `
261
+ <div class="gem-c-search-with-autocomplete__option-wrapper">
262
+ <span class="gem-c-search-with-autocomplete__suggestion-icon"></span>
263
+ <span class="gem-c-search-with-autocomplete__suggestion-text">${html}</span>
264
+ </div>
265
+ `;
266
+ };
267
+
268
+ // Define inputValue template function (handles objects)
269
+ const inputValueTemplate = (result: Suggestion | undefined): string => {
270
+ if (result === undefined) return "";
271
+ // Use label for input value if it's an object, otherwise use the string
272
+ return typeof result === "string" ? result : result.label;
273
+ };
274
+
275
+ // Define confirm function
276
+ let isSubmitting = false; // Prevent double submit
277
+ const handleConfirm = (confirmedValue: Suggestion | undefined) => {
278
+ if (confirmedValue === undefined || isSubmitting) return;
279
+
280
+ // Re-assign selectedValue before any form-based guard checks (!form) so bindings still update
281
+ // (e.g. when no <form> exists around the component usage) and search component value is being used clienside without a page reload
282
+ selectedValue =
283
+ typeof confirmedValue === "string"
284
+ ? confirmedValue
285
+ : confirmedValue.value;
286
+
287
+ // Type assertion needed here
288
+ const inputElement =
289
+ autocompleteInstance?.inputElement as HTMLInputElement;
290
+ const form = containerElement?.closest("form");
291
+
292
+ if (!inputElement || !form) return;
293
+
294
+ isSubmitting = true;
295
+ inputElement.value = inputValueTemplate(confirmedValue);
296
+ inputElement.dataset.autocompleteAccepted = "true"; // Set tracking attribute
297
+
298
+ // Submit form
299
+ if (form.requestSubmit) {
300
+ form.requestSubmit();
301
+ } else {
302
+ form.submit(); // Fallback for older browsers
303
+ }
304
+ // Reset flag after a short delay in case submission fails/is prevented
305
+ setTimeout(() => {
306
+ isSubmitting = false;
307
+ }, 500);
308
+ };
309
+
310
+ // Initialise accessible-autocomplete
311
+ autocompleteInstance = accessibleAutocomplete({
312
+ element: targetInputWrapper, // Target the div *containing* the input
313
+ id: searchInput.id, // Use the ID from the *rendered* Search input
314
+ name: searchInput.name, // Use the name from the *rendered* Search input
315
+ inputClasses: searchInput.classList, // Pass original classes directly
316
+ source: sourceFunction,
317
+ minLength: minLength,
318
+ confirmOnBlur: confirmOnBlur,
319
+ showNoOptionsFound: showNoOptionsFound,
320
+ defaultValue: defaultValue,
321
+ placeholder: placeholder,
322
+ required: required,
323
+ tNoResults: tNoResults,
324
+ tAssistiveHint: tAssistiveHint,
325
+ menuAttributes: menuAttributes,
326
+ menuClasses: menuClasses,
327
+ tStatusQueryTooShort: tooShort,
328
+ displayMenu: "overlay",
329
+ cssNamespace: "gem-c-search-with-autocomplete",
330
+ onConfirm: handleConfirm,
331
+ templates: {
332
+ suggestion: suggestionTemplate,
333
+ inputValue: inputValueTemplate, // optional template to transform displayed value
334
+ // noOptionsFound: optional template
335
+ },
336
+ });
337
+
338
+ // Query the DOM for the input element *created* by the library
339
+ const autocompleteInputElement = containerElement?.querySelector(
340
+ ".gem-c-search-with-autocomplete__input",
341
+ ) as HTMLInputElement | null;
342
+ // console.log(
343
+ // "SearchAutocomplete: Input element queried from DOM:",
344
+ // autocompleteInputElement,
345
+ // ); // Updated log
346
+
347
+ // Post-initialisation tweaks
348
+ if (autocompleteInputElement) {
349
+ // Post-init: dynamically show a 'too-short' warning when the user types fewer than minLength characters
350
+ // Find the dropdown menu <ul> generated by accessible-autocomplete
351
+ const suggestionsMenu =
352
+ containerElement.querySelector<HTMLUListElement>(
353
+ ".gem-c-search-with-autocomplete__menu",
354
+ );
355
+ // Listen for input changes on the autocomplete field
356
+ autocompleteInputElement.addEventListener("input", () => {
357
+ const val = autocompleteInputElement.value;
358
+ // Remove any existing 'too-short' warning before adding a new one to ensure we don't accumulate multiple warning items.
359
+ suggestionsMenu
360
+ ?.querySelector(
361
+ ".gem-c-search-with-autocomplete__option--too-short",
362
+ )
363
+ ?.remove();
364
+ if (val.length < minLength) {
365
+ // Force the menu open so the warning is shown
366
+ autocompleteInputElement.setAttribute("aria-expanded", "true");
367
+ suggestionsMenu?.classList.add(
368
+ "gem-c-search-with-autocomplete__menu--visible",
369
+ );
370
+ suggestionsMenu?.classList.remove(
371
+ "gem-c-search-with-autocomplete__menu--hidden",
372
+ );
373
+ // Create a new <li> element with the warning text. Classes match the library's own suggestion items (so it looks and behaves consistently). Set role="option" and aria-disabled="true" for accessibility
374
+ const li = document.createElement("li");
375
+ li.className =
376
+ "gem-c-search-with-autocomplete__option gem-c-search-with-autocomplete__option--too-short";
377
+ li.setAttribute("role", "option");
378
+ li.setAttribute("aria-disabled", "true");
379
+ li.textContent = tooShort(minLength);
380
+ // Insert the warning at the top of the suggestions list menu
381
+ suggestionsMenu?.prepend(li);
382
+ }
383
+ });
384
+ autocompleteInputElement.setAttribute("type", "search"); // Ensure input type is search
385
+ // autocompleteInputElement.classList.add("autocomplete__input"); // Add specific class if needed
386
+
387
+ // Add Enter key workaround from original JS
388
+ autocompleteInputElement.addEventListener("keydown", (e) => {
389
+ if (isSubmitting) return; // Don't interfere if already submitting
390
+ const dropdownVisible =
391
+ autocompleteInputElement.getAttribute("aria-expanded") === "true";
392
+ if (dropdownVisible && e.key === "Enter") {
393
+ const form = containerElement?.closest("form");
394
+ if (form) {
395
+ isSubmitting = true;
396
+ if (form.requestSubmit) form.requestSubmit();
397
+ else form.submit();
398
+ setTimeout(() => {
399
+ isSubmitting = false;
400
+ }, 500);
401
+ }
402
+ }
403
+ });
404
+
405
+ // console.log(
406
+ // "Autocomplete input classes AFTER add:",
407
+ // Array.from(autocompleteInputElement.classList),
408
+ // );
409
+ }
410
+
411
+ // IMPORTANT: Remove the original Search.svelte input, as accessible-autocomplete replaces it.
412
+ // We render it initially so accessible-autocomplete can grab its id, name, value.
413
+ if (searchInput) {
414
+ searchInput.remove(); // Use remove() instead of hiding
415
+ }
416
+ // Also remove the original label if it's still present inside targetInputWrapper
417
+ const originalLabel = targetInputWrapper.querySelector(
418
+ "label",
419
+ ) as HTMLLabelElement | null;
420
+ if (originalLabel) {
421
+ originalLabel.remove(); // Use remove() instead of hiding
422
+ }
423
+ }
424
+ }); // End of onMount
425
+ </script>
426
+
427
+ <div
428
+ bind:this={containerElement}
429
+ class={wrapperClasses}
430
+ data-module="gem-search-with-autocomplete"
431
+ data-source-url={source_url}
432
+ data-source-key={source_key}
433
+ data-source-property={source_property}
434
+ {...outerDataAttributes}
435
+ style={`--suggestion-icon: url("${suggestionIconUrl}"); --cancel-icon: url("${closeIconUrl}")`}
436
+ >
437
+ <!-- Render the base Search component initially -->
438
+ <!-- accessible-autocomplete will enhance the input inside it -->
439
+ <Search {...searchComponentOptions} />
440
+ </div>
441
+
442
+ <style>
443
+ /* global to prevent css namespace conflict with original library*/
444
+ :global {
445
+ .gem-c-search-with-autocomplete__wrapper {
446
+ position: relative;
447
+ }
448
+
449
+ .gem-c-search-with-autocomplete__menu {
450
+ margin: 0;
451
+ padding: 0;
452
+ overflow-x: hidden;
453
+ background-color: #fff;
454
+ border: 1px solid #b1b4b6;
455
+ border-top: 0;
456
+ margin-right: -40px;
457
+ }
458
+
459
+ .gem-c-search-with-autocomplete__menu--visible {
460
+ display: block;
461
+ }
462
+
463
+ .gem-c-search-with-autocomplete__menu--hidden {
464
+ display: none;
465
+ }
466
+
467
+ .gem-c-search-with-autocomplete__menu--inline {
468
+ position: relative;
469
+ }
470
+
471
+ .gem-c-search-with-autocomplete__option {
472
+ display: block;
473
+ cursor: pointer;
474
+ font-family: "GDS Transport", arial, sans-serif;
475
+ -webkit-font-smoothing: antialiased;
476
+ -moz-osx-font-smoothing: grayscale;
477
+ font-weight: 400;
478
+ font-size: 1.1875rem;
479
+ line-height: 1.3157894737;
480
+ }
481
+
482
+ @media print {
483
+ .gem-c-search-with-autocomplete__option {
484
+ font-family: sans-serif;
485
+ }
486
+ }
487
+
488
+ @media print {
489
+ .gem-c-search-with-autocomplete__option {
490
+ font-size: 14pt;
491
+ line-height: 1.15;
492
+ }
493
+ }
494
+
495
+ .gem-c-search-with-autocomplete__option > * {
496
+ pointer-events: none;
497
+ }
498
+
499
+ .gem-c-search-with-autocomplete__option > span {
500
+ clip: rect(0 0 0 0);
501
+ clip-path: inset(50%);
502
+ height: 1px;
503
+ overflow: hidden;
504
+ position: absolute;
505
+ white-space: nowrap;
506
+ width: 1px;
507
+ }
508
+
509
+ .gem-c-search-with-autocomplete__option--focused,
510
+ .gem-c-search-with-autocomplete__option:hover,
511
+ .gem-c-search-with-autocomplete__option:focus-visible {
512
+ background-color: #f3f2f1;
513
+ outline: none;
514
+ text-decoration: underline;
515
+ text-decoration-thickness: max(1px, 0.0625rem);
516
+ text-underline-offset: 0.1578em;
517
+ text-decoration-thickness: max(3px, 0.1875rem, 0.12em);
518
+ -webkit-text-decoration-skip-ink: none;
519
+ text-decoration-skip-ink: none;
520
+ -webkit-text-decoration-skip: none;
521
+ text-decoration-skip: none;
522
+ }
523
+
524
+ .gem-c-search-with-autocomplete__option--focused
525
+ .gem-c-search-with-autocomplete__suggestion-icon,
526
+ .gem-c-search-with-autocomplete__option:hover
527
+ .gem-c-search-with-autocomplete__suggestion-icon,
528
+ .gem-c-search-with-autocomplete__option:focus-visible
529
+ .gem-c-search-with-autocomplete__suggestion-icon {
530
+ background-color: #0b0c0c;
531
+ }
532
+
533
+ .gem-c-search-with-autocomplete__option:focus-visible
534
+ .gem-c-search-with-autocomplete__suggestion-text {
535
+ background-color: #fd0;
536
+ }
537
+
538
+ .gem-c-search-with-autocomplete__option-wrapper {
539
+ display: flex;
540
+ align-items: center;
541
+ margin: 0 15px;
542
+ padding: 5px 0;
543
+ border-bottom: 1px solid #b1b4b6;
544
+ }
545
+
546
+ .gem-c-search-with-autocomplete__option:last-child
547
+ .gem-c-search-with-autocomplete__option-wrapper {
548
+ border-bottom: 0;
549
+ }
550
+
551
+ .gem-c-search-with-autocomplete__suggestion-icon {
552
+ width: 20px;
553
+ height: 40px;
554
+ margin-right: 10px;
555
+ flex: none;
556
+ mask-image: var(--suggestion-icon);
557
+ -webkit-mask-image: var(--suggestion-icon);
558
+ background-color: #505a5f;
559
+ }
560
+
561
+ .gem-c-search-with-autocomplete__suggestion-text {
562
+ font-weight: bold;
563
+ }
564
+
565
+ .gem-c-search-with-autocomplete__suggestion-highlight {
566
+ font-weight: normal;
567
+ background: none;
568
+ }
569
+
570
+ .gem-c-search-with-autocomplete.gem-c-search-with-autocomplete--large
571
+ .gem-c-search-with-autocomplete__menu {
572
+ margin-right: -50px;
573
+ }
574
+
575
+ .gem-c-search-with-autocomplete.gem-c-search-with-autocomplete--large
576
+ .gem-c-search-with-autocomplete__option {
577
+ min-height: 50px;
578
+ }
579
+
580
+ .gem-c-search-with-autocomplete.gem-c-search-with-autocomplete--on-govuk-blue
581
+ .gem-c-search-with-autocomplete__menu {
582
+ border-top: 1px solid #b1b4b6;
583
+ }
584
+
585
+ @media (forced-colors: active) {
586
+ .gem-c-search-with-autocomplete__menu {
587
+ border-color: FieldText;
588
+ }
589
+
590
+ .gem-c-search-with-autocomplete__option {
591
+ forced-color-adjust: none;
592
+ background-color: Field;
593
+ color: FieldText;
594
+ }
595
+
596
+ .gem-c-search-with-autocomplete__option--focused,
597
+ .gem-c-search-with-autocomplete__option:hover,
598
+ .gem-c-search-with-autocomplete__option:focus-visible {
599
+ background-color: Highlight;
600
+ color: HighlightText;
601
+ border-color: FieldText;
602
+ }
603
+
604
+ .gem-c-search-with-autocomplete__option--focused
605
+ .gem-c-search-with-autocomplete__suggestion-text,
606
+ .gem-c-search-with-autocomplete__option:hover
607
+ .gem-c-search-with-autocomplete__suggestion-text,
608
+ .gem-c-search-with-autocomplete__option:focus-visible
609
+ .gem-c-search-with-autocomplete__suggestion-text {
610
+ background: none;
611
+ }
612
+
613
+ .gem-c-search-with-autocomplete__option--focused
614
+ .gem-c-search-with-autocomplete__suggestion-highlight,
615
+ .gem-c-search-with-autocomplete__option:hover
616
+ .gem-c-search-with-autocomplete__suggestion-highlight,
617
+ .gem-c-search-with-autocomplete__option:focus-visible
618
+ .gem-c-search-with-autocomplete__suggestion-highlight {
619
+ color: HighlightText;
620
+ }
621
+
622
+ .gem-c-search-with-autocomplete__option--focused
623
+ .gem-c-search-with-autocomplete__suggestion-icon,
624
+ .gem-c-search-with-autocomplete__option:hover
625
+ .gem-c-search-with-autocomplete__suggestion-icon,
626
+ .gem-c-search-with-autocomplete__option:focus-visible
627
+ .gem-c-search-with-autocomplete__suggestion-icon {
628
+ background-color: HighlightText;
629
+ }
630
+
631
+ .gem-c-search-with-autocomplete__option:focus-visible:not(:hover) {
632
+ background-color: SelectedItem;
633
+ color: SelectedItemText;
634
+ }
635
+
636
+ .gem-c-search-with-autocomplete__option:focus-visible:not(:hover)
637
+ .gem-c-search-with-autocomplete__suggestion-highlight {
638
+ color: SelectedItemText;
639
+ }
640
+
641
+ .gem-c-search-with-autocomplete__option:focus-visible:not(:hover)
642
+ .gem-c-search-with-autocomplete__suggestion-icon {
643
+ background-color: SelectedItemText;
644
+ }
645
+
646
+ .gem-c-search-with-autocomplete__suggestion-highlight {
647
+ color: FieldText;
648
+ }
649
+
650
+ .gem-c-search-with-autocomplete__suggestion-icon {
651
+ background-color: FieldText;
652
+ }
653
+ }
654
+ }
655
+ </style>
@@ -0,0 +1,37 @@
1
+ import "accessible-autocomplete/dist/accessible-autocomplete.min.css";
2
+ type SuggestionObject = {
3
+ label: string;
4
+ value: any;
5
+ };
6
+ type Suggestion = string | SuggestionObject;
7
+ type Props = {
8
+ options?: Suggestion[];
9
+ source_url?: string;
10
+ source_key?: string;
11
+ source_property?: string;
12
+ outerClasses?: string;
13
+ outerDataAttributes?: Record<string, string>;
14
+ size?: "large" | "";
15
+ on_govuk_blue?: boolean;
16
+ homepage?: boolean;
17
+ id?: string;
18
+ name?: string;
19
+ label_text?: string;
20
+ button_text?: string;
21
+ [key: string]: any;
22
+ minLength?: number;
23
+ confirmOnBlur?: boolean;
24
+ showNoOptionsFound?: boolean;
25
+ defaultValue?: string;
26
+ placeholder?: string;
27
+ required?: boolean;
28
+ tNoResults?: () => string;
29
+ tAssistiveHint?: () => string;
30
+ menuAttributes?: Record<string, any>;
31
+ menuClasses?: string | null;
32
+ hint?: string;
33
+ selectedValue?: any;
34
+ };
35
+ declare const SearchAutocomplete: import("svelte").Component<Props, {}, "selectedValue">;
36
+ type SearchAutocomplete = ReturnType<typeof SearchAutocomplete>;
37
+ export default SearchAutocomplete;