@communitiesuk/svelte-component-library 0.1.19-beta.3 → 0.1.19-beta.34

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 (52) hide show
  1. package/README.md +7 -0
  2. package/dist/components/content/Tag.svelte +32 -0
  3. package/dist/components/content/Tag.svelte.d.ts +13 -0
  4. package/dist/components/data-vis/Histogram.svelte +287 -0
  5. package/dist/components/data-vis/Histogram.svelte.d.ts +75 -0
  6. package/dist/components/data-vis/axis/Axis.svelte +217 -34
  7. package/dist/components/data-vis/axis/Axis.svelte.d.ts +38 -30
  8. package/dist/components/data-vis/axis/Ticks.svelte +142 -78
  9. package/dist/components/data-vis/axis/Ticks.svelte.d.ts +28 -31
  10. package/dist/components/data-vis/line-chart/LineChart.svelte +51 -21
  11. package/dist/components/data-vis/line-chart/LineChart.svelte.d.ts +14 -6
  12. package/dist/components/data-vis/line-chart/ValueLabel.svelte +2 -1
  13. package/dist/components/data-vis/line-chart/ValueLabel.svelte.d.ts +2 -0
  14. package/dist/components/data-vis/position-chart/PositionChart.svelte +279 -122
  15. package/dist/components/data-vis/position-chart/PositionChart.svelte.d.ts +37 -5
  16. package/dist/components/data-vis/position-chart/PositionChartAxis.svelte +59 -48
  17. package/dist/components/data-vis/position-chart/PositionChartAxis.svelte.d.ts +4 -4
  18. package/dist/components/data-vis/table/Table.svelte +145 -95
  19. package/dist/components/data-vis/table/Table.svelte.d.ts +4 -4
  20. package/dist/components/layout/Footer.svelte +9 -0
  21. package/dist/components/layout/Footer.svelte.d.ts +1 -0
  22. package/dist/components/layout/PhaseBanner.svelte +10 -1
  23. package/dist/components/layout/PhaseBanner.svelte.d.ts +1 -0
  24. package/dist/components/layout/ServiceNavigation.svelte +19 -1
  25. package/dist/components/layout/ServiceNavigation.svelte.d.ts +2 -0
  26. package/dist/components/ui/Accordion.svelte +212 -66
  27. package/dist/components/ui/Accordion.svelte.d.ts +2 -0
  28. package/dist/components/ui/BasicMultiSelect.svelte +716 -0
  29. package/dist/components/ui/BasicMultiSelect.svelte.d.ts +18 -0
  30. package/dist/components/ui/Button.svelte +220 -104
  31. package/dist/components/ui/Button.svelte.d.ts +4 -0
  32. package/dist/components/ui/Card.svelte +48 -60
  33. package/dist/components/ui/Card.svelte.d.ts +26 -12
  34. package/dist/components/ui/CardHeader.svelte +46 -0
  35. package/dist/components/ui/CardHeader.svelte.d.ts +21 -0
  36. package/dist/components/ui/ChartExporter.svelte +142 -0
  37. package/dist/components/ui/ChartExporter.svelte.d.ts +16 -0
  38. package/dist/components/ui/CheckBox.svelte +1 -0
  39. package/dist/components/ui/Details.svelte +47 -8
  40. package/dist/components/ui/Details.svelte.d.ts +8 -10
  41. package/dist/components/ui/Masthead.svelte +44 -6
  42. package/dist/components/ui/Masthead.svelte.d.ts +6 -0
  43. package/dist/components/ui/RelatedContent.svelte +4 -1
  44. package/dist/components/ui/RelatedContent.svelte.d.ts +1 -0
  45. package/dist/components/ui/SearchAutocomplete.svelte +69 -44
  46. package/dist/components/ui/SearchAutocomplete.svelte.d.ts +1 -0
  47. package/dist/components/ui/Select.svelte +18 -7
  48. package/dist/components/ui/Tabs.svelte +192 -18
  49. package/dist/components/ui/Tabs.svelte.d.ts +1 -0
  50. package/dist/index.d.ts +5 -0
  51. package/dist/index.js +5 -0
  52. package/package.json +4 -1
@@ -0,0 +1,46 @@
1
+ <script>
2
+ let {
3
+ text = "Card header",
4
+ textSize = "1.5rem",
5
+ textColor = "#1D70B8",
6
+ backgroundColor = "white",
7
+ href = undefined,
8
+ subtitle = undefined,
9
+ } = $props();
10
+ </script>
11
+
12
+ <a
13
+ class="link govuk-heading-m"
14
+ {href}
15
+ style="font-size: {textSize}; color: {textColor}; background-color: {backgroundColor}; margin: {subtitle ??
16
+ 0}"
17
+ >
18
+ {text}
19
+ <svg
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ width="1em"
22
+ height="1em"
23
+ viewBox="0 0 10 17"
24
+ aria-hidden="true"
25
+ >
26
+ <path
27
+ d="M6.21622 8.5L0 2.36667L1.89189 0.5L10 8.5L1.89189 16.5L0 14.6333L6.21622 8.5Z"
28
+ fill="currentColor"
29
+ ></path>
30
+ </svg>
31
+ </a>
32
+ {#if subtitle !== undefined}
33
+ <p>{subtitle}</p>{/if}
34
+
35
+ <style>
36
+ .link {
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: center;
40
+ width: 100%;
41
+ gap: 0.5rem;
42
+ }
43
+ p {
44
+ color: #666;
45
+ }
46
+ </style>
@@ -0,0 +1,21 @@
1
+ export default CardHeader;
2
+ type CardHeader = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const CardHeader: import("svelte").Component<{
7
+ text?: string;
8
+ textSize?: string;
9
+ textColor?: string;
10
+ backgroundColor?: string;
11
+ href?: any;
12
+ subtitle?: any;
13
+ }, {}, "">;
14
+ type $$ComponentProps = {
15
+ text?: string;
16
+ textSize?: string;
17
+ textColor?: string;
18
+ backgroundColor?: string;
19
+ href?: any;
20
+ subtitle?: any;
21
+ };
@@ -0,0 +1,142 @@
1
+ <script lang="ts">
2
+ // src/lib/utils/exportChartPNG.ts
3
+ export async function exportChartPNG(
4
+ node: HTMLElement,
5
+ {
6
+ fileName = "chart.png",
7
+ margin = 16,
8
+ backgroundColor = "#ffffff",
9
+ pixelRatio = Math.max(2, window.devicePixelRatio || 1),
10
+ filter,
11
+ }: {
12
+ fileName?: string;
13
+ margin?: number; // padding applied during export to avoid edges clipping
14
+ backgroundColor?: string;
15
+ pixelRatio?: number;
16
+ filter?: (node: Element) => boolean;
17
+ },
18
+ ): Promise<string> {
19
+ const { toPng } = await import("html-to-image");
20
+
21
+ // Wait for web fonts to settle (prevents text reflow/misalignment)
22
+ if ((document as any).fonts?.ready) {
23
+ try {
24
+ await (document as any).fonts.ready;
25
+ } catch {
26
+ /* ignore */
27
+ }
28
+ }
29
+
30
+ // Measure the true content size (includes overflow)
31
+ const exportWidth = Math.ceil(node.scrollWidth + margin * 2);
32
+ const exportHeight = Math.ceil(node.scrollHeight + margin * 2);
33
+
34
+ const dataUrl = await toPng(node, {
35
+ cacheBust: false,
36
+ backgroundColor,
37
+ pixelRatio,
38
+ width: exportWidth,
39
+ height: exportHeight,
40
+ style: {
41
+ // Apply padding inside the cloned element
42
+ padding: `${margin}px`,
43
+ boxSizing: "border-box",
44
+ // Ensure no clipping
45
+ overflow: "visible",
46
+ },
47
+ filter,
48
+ });
49
+
50
+ const name = fileName.toLowerCase().endsWith(".png")
51
+ ? fileName
52
+ : `${fileName}.png`;
53
+ const link = document.createElement("a");
54
+ link.download = name;
55
+ link.href = dataUrl;
56
+ link.click();
57
+
58
+ return dataUrl;
59
+ }
60
+
61
+ // Props
62
+ let {
63
+ node = null,
64
+ fileName = "chart",
65
+ }: { node: HTMLElement | null; fileName?: string } = $props();
66
+
67
+ // Local state for optional external triggering
68
+ let isExporting = $state(false);
69
+
70
+ async function exportNow() {
71
+ if (!node) return;
72
+
73
+ isExporting = true;
74
+
75
+ // Measure width to force correct clone sizing
76
+ const rect = node.getBoundingClientRect();
77
+ const measuredWidth = Math.ceil(rect.width);
78
+
79
+ // ------- Clone -------
80
+ const clone = node.cloneNode(true) as HTMLElement;
81
+ Object.assign(clone.style, {
82
+ boxSizing: "border-box",
83
+ width: `${measuredWidth}px`,
84
+ height: "auto",
85
+ transform: "none",
86
+ overflow: "visible",
87
+ fontFamily: "GDS Transport",
88
+ });
89
+
90
+ // ------- Hide tooltips (optional) -------
91
+ const hideSelectors = ['[role="tooltip"]'];
92
+ clone
93
+ .querySelectorAll<HTMLElement>(hideSelectors.join(","))
94
+ .forEach((el) => {
95
+ el.style.display = "none";
96
+ });
97
+
98
+ // ------- Sandbox offscreen -------
99
+ const sandbox = document.createElement("div");
100
+ Object.assign(sandbox.style, {
101
+ position: "fixed",
102
+ left: "-10000px",
103
+ top: "0",
104
+ pointerEvents: "none",
105
+ });
106
+
107
+ sandbox.appendChild(clone);
108
+ document.body.appendChild(sandbox);
109
+
110
+ try {
111
+ await exportChartPNG(clone, {
112
+ fileName,
113
+ margin: 16,
114
+ backgroundColor: "#ffffff",
115
+ pixelRatio: Math.max(2, window.devicePixelRatio || 1),
116
+ filter: (el: Element) => {
117
+ if (!(el instanceof HTMLElement)) return true;
118
+
119
+ // Exclude tooltips only
120
+ if (el.matches(hideSelectors.join(","))) return false;
121
+
122
+ return true;
123
+ },
124
+ });
125
+ } finally {
126
+ sandbox.remove();
127
+ isExporting = false;
128
+ }
129
+ }
130
+
131
+ // We export the function so parent can trigger it manually
132
+ export { exportNow };
133
+ </script>
134
+
135
+ <!-- Default button (can be hidden if user triggers manually) -->
136
+ <button
137
+ class="govuk-button govuk-button--secondary"
138
+ on:click={exportNow}
139
+ disabled={isExporting}
140
+ >
141
+ {isExporting ? "Exporting…" : "Download this chart"}
142
+ </button>
@@ -0,0 +1,16 @@
1
+ type $$ComponentProps = {
2
+ node: HTMLElement | null;
3
+ fileName?: string;
4
+ };
5
+ declare const ChartExporter: import("svelte").Component<$$ComponentProps, {
6
+ exportChartPNG: (node: HTMLElement, { fileName, margin, backgroundColor, pixelRatio, filter, }: {
7
+ fileName?: string;
8
+ margin?: number;
9
+ backgroundColor?: string;
10
+ pixelRatio?: number;
11
+ filter?: (node: Element) => boolean;
12
+ }) => Promise<string>;
13
+ exportNow: () => Promise<void>;
14
+ }, "">;
15
+ type ChartExporter = ReturnType<typeof ChartExporter>;
16
+ export default ChartExporter;
@@ -58,6 +58,7 @@
58
58
  // Modify toggleCheckbox to handle non-JS scenarios
59
59
  function toggleCheckbox(option: CheckboxOption) {
60
60
  // If JS/modern features aren't supported, let the native checkbox behavior work
61
+
61
62
  if (!isSupported) return;
62
63
 
63
64
  if (option.exclusive) {
@@ -1,12 +1,51 @@
1
- <script>
2
- let { summaryText, detailedText } = $props();
1
+ <script lang="ts">
2
+ let {
3
+ summaryText,
4
+ detailedText,
5
+ renderStringAsHTML = false,
6
+ noInset = false,
7
+ overlapBelow = false,
8
+ expanded = $bindable(false),
9
+ groupName = undefined,
10
+ } = $props();
3
11
  </script>
4
12
 
5
- <details class="govuk-details">
6
- <summary class="govuk-details__summary">
7
- <span class="govuk-details__summary-text">{summaryText}</span>
8
- </summary>
9
- <div class="govuk-details__text">
10
- {detailedText}
13
+ <details class="govuk-details" open={expanded} name={groupName}>
14
+ {#if renderStringAsHTML}
15
+ <summary class="govuk-details__summary-text">
16
+ {@html summaryText}
17
+ </summary>
18
+ {:else}
19
+ <summary class="govuk-details__summary-text">{summaryText}</summary>
20
+ {/if}
21
+
22
+ <div
23
+ class={`govuk-details__text ${noInset === true ? "no-inset" : ""} ${overlapBelow === true ? "overlap-below" : ""}`}
24
+ >
25
+ {#if typeof detailedText === "string"}
26
+ {#if renderStringAsHTML}
27
+ {@html detailedText}
28
+ {:else}
29
+ {detailedText}
30
+ {/if}
31
+ {:else if detailedText}
32
+ {@render detailedText()}
33
+ {/if}
11
34
  </div>
12
35
  </details>
36
+
37
+ <style>
38
+ .no-inset {
39
+ padding: 0;
40
+ border: 0;
41
+ }
42
+
43
+ details {
44
+ position: relative;
45
+ }
46
+
47
+ .overlap-below {
48
+ position: absolute;
49
+ background-color: white;
50
+ }
51
+ </style>
@@ -1,13 +1,11 @@
1
- export default Details;
2
- type Details = {
3
- $on?(type: string, callback: (e: any) => void): () => void;
4
- $set?(props: Partial<$$ComponentProps>): void;
5
- };
6
1
  declare const Details: import("svelte").Component<{
7
2
  summaryText: any;
8
3
  detailedText: any;
9
- }, {}, "">;
10
- type $$ComponentProps = {
11
- summaryText: any;
12
- detailedText: any;
13
- };
4
+ renderStringAsHTML?: boolean;
5
+ noInset?: boolean;
6
+ overlapBelow?: boolean;
7
+ expanded?: boolean;
8
+ groupName?: any;
9
+ }, {}, "expanded">;
10
+ type Details = ReturnType<typeof Details>;
11
+ export default Details;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import homepageIllustration from "./../../assets/images/homepage-illustration.svg";
3
+ import type { Snippet } from "svelte";
3
4
 
4
5
  // Define component props with types and default values
5
6
  let {
@@ -12,6 +13,11 @@
12
13
  imageAlt = "",
13
14
  backgroundColor = "#1d70b8", // GOV.UK blue by default
14
15
  textColor = "#FFFFFF",
16
+ imgMarginTop = "15px",
17
+ paddingTop = "30px",
18
+ titlePaddingTop = false,
19
+ paddingBottom = "30px",
20
+ contentSnippet = undefined,
15
21
  } = $props<{
16
22
  title?: string;
17
23
  description?: string;
@@ -22,18 +28,32 @@
22
28
  imageAlt?: string;
23
29
  backgroundColor?: string;
24
30
  textColor?: string;
31
+ imgMarginTop?: string;
32
+ paddingTop?: string;
33
+ titlePaddingTop?: boolean;
34
+ paddingBottom?: string;
35
+ contentSnippet?: Snippet;
25
36
  }>();
37
+
38
+ let arg = "hello";
26
39
  </script>
27
40
 
28
41
  <div
29
42
  class="app-masthead"
30
- style="background-color: {backgroundColor}; border-bottom-color: {backgroundColor}; --masthead-text-color: {textColor};"
43
+ style="background-color: {backgroundColor}; border-bottom-color: {backgroundColor}; --masthead-text-color: {textColor}; --padding-top: {paddingTop}; --padding-bottom: {paddingBottom};"
31
44
  >
32
45
  <div class="govuk-width-container">
33
46
  <div class="govuk-grid-row">
34
- <div class="govuk-grid-column-two-thirds-from-desktop">
47
+ <div
48
+ class="govuk-grid-column-two-thirds-from-desktop {titlePaddingTop
49
+ ? 'custom-padding'
50
+ : ''}"
51
+ >
35
52
  <h1 class="govuk-heading-xl app-masthead__title">{@html title}</h1>
36
53
  <p class="app-masthead__description">{description}</p>
54
+ {#if contentSnippet}
55
+ {@render contentSnippet()}
56
+ {/if}
37
57
  {#if includeButton === true}
38
58
  <a
39
59
  href={buttonHref}
@@ -61,6 +81,7 @@
61
81
  <div class="govuk-grid-column-one-third-from-desktop">
62
82
  <img
63
83
  class="app-masthead__image"
84
+ style="--img-margin-top: {imgMarginTop}"
64
85
  src={imageSrc}
65
86
  alt={imageAlt}
66
87
  role="presentation"
@@ -88,8 +109,8 @@
88
109
  @media (min-width: 40.0625em) {
89
110
  .app-masthead.app-masthead {
90
111
  /* Responsive spacing unit 6: 30px on large screens */
91
- padding-top: 30px;
92
- padding-bottom: 30px;
112
+ padding-top: var(--padding-top);
113
+ padding-bottom: var(--padding-bottom);
93
114
  }
94
115
  }
95
116
 
@@ -128,12 +149,19 @@
128
149
  }
129
150
 
130
151
  /* @include govuk-media-query($from: desktop) - Desktop breakpoint is 769px */
131
- @media (min-width: 48.0625em) {
152
+ @media (min-width: 53em) {
132
153
  .app-masthead .app-masthead__image.app-masthead__image {
133
154
  display: block;
134
155
  width: 100%;
135
156
  /* margin-top: govuk-spacing(3); - Static spacing unit 3 is 15px */
136
- margin-top: 15px;
157
+ margin-top: var(--img-margin-top);
158
+ }
159
+ }
160
+
161
+ @media (max-width: 52.9375em) {
162
+ .govuk-grid-column-two-thirds-from-desktop {
163
+ width: 100%;
164
+ float: none;
137
165
  }
138
166
  }
139
167
 
@@ -276,4 +304,14 @@
276
304
  .app-masthead__description {
277
305
  color: var(--masthead-text-color);
278
306
  }
307
+
308
+ .custom-padding {
309
+ padding-top: 32px;
310
+ }
311
+
312
+ @media (max-width: 820px) {
313
+ .custom-padding {
314
+ padding-top: 16px;
315
+ }
316
+ }
279
317
  </style>
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from "svelte";
1
2
  type $$ComponentProps = {
2
3
  title?: string;
3
4
  description?: string;
@@ -8,6 +9,11 @@ type $$ComponentProps = {
8
9
  imageAlt?: string;
9
10
  backgroundColor?: string;
10
11
  textColor?: string;
12
+ imgMarginTop?: string;
13
+ paddingTop?: string;
14
+ titlePaddingTop?: boolean;
15
+ paddingBottom?: string;
16
+ contentSnippet?: Snippet;
11
17
  };
12
18
  declare const Masthead: import("svelte").Component<$$ComponentProps, {}, "">;
13
19
  type Masthead = ReturnType<typeof Masthead>;
@@ -30,11 +30,13 @@
30
30
  headingLevel = 2 as 1 | 2 | 3 | 4 | 5 | 6, // Main heading level (used by first 'main' section)
31
31
  listTruncateThreshold = 5, // Default threshold, can be overridden per section
32
32
  disableGa4 = false,
33
+ marginBottom = "60px",
33
34
  } = $props<{
34
35
  sections?: RelatedContentSection[];
35
36
  headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
36
37
  listTruncateThreshold?: number; // Default threshold
37
38
  disableGa4?: boolean;
39
+ marginBottom?: string;
38
40
  }>();
39
41
 
40
42
  // Helper to check if a link is external
@@ -229,6 +231,7 @@
229
231
  class="gem-c-related-navigation govuk-!-display-none-print {hasJavaScript
230
232
  ? 'govuk-frontend-supported'
231
233
  : ''}"
234
+ style="margin-bottom: {marginBottom};"
232
235
  role="complementary"
233
236
  >
234
237
  {#if mainSection && mainSection.title}
@@ -304,7 +307,7 @@
304
307
  <style>
305
308
  .gem-c-related-navigation {
306
309
  border-top: 2px solid #1d70b8;
307
- margin-bottom: 60px;
310
+ /* margin-bottom: 60px; */
308
311
  color: #0b0c0c;
309
312
  }
310
313
 
@@ -24,6 +24,7 @@ type $$ComponentProps = {
24
24
  headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
25
25
  listTruncateThreshold?: number;
26
26
  disableGa4?: boolean;
27
+ marginBottom?: string;
27
28
  };
28
29
  declare const RelatedContent: import("svelte").Component<$$ComponentProps, {}, "">;
29
30
  type RelatedContent = ReturnType<typeof RelatedContent>;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from "svelte";
3
3
  import { clsx } from "clsx";
4
+ import { on } from "svelte/events";
4
5
  import Search from "./Search.svelte"; // Base component
5
6
  import "accessible-autocomplete/dist/accessible-autocomplete.min.css";
6
7
  import { browser } from "$app/environment";
@@ -55,6 +56,7 @@
55
56
  hideHint?: boolean; // Hide the hint input element when autoselect is true
56
57
  prefixMatchOnly?: boolean; // Only show suggestions that start with the query (better for hint behavior)
57
58
  autoFocusSubmitOnSelection?: boolean; // Auto-focus submit button when selection is confirmed
59
+ allowFreeTextSubmission?: boolean; // Treat free-typed input as confirmed when submitted (works with or without form)
58
60
  };
59
61
 
60
62
  let {
@@ -93,6 +95,7 @@
93
95
  hideHint = false, // Default to false - show hint by default
94
96
  prefixMatchOnly = false, // Default to false - show all matches
95
97
  autoFocusSubmitOnSelection = false, // Default to false - don't auto-focus by default
98
+ allowFreeTextSubmission = false, // Default to false - only confirmed suggestions submit
96
99
  ...restSearchProps // Other props for the base Search component
97
100
  }: Props = $props();
98
101
 
@@ -105,8 +108,6 @@
105
108
  let currentSourceKey = $state<string | undefined>(undefined);
106
109
  let currentSourceProperty = $state<string | undefined>(undefined);
107
110
 
108
-
109
-
110
111
  // --- Derived Values ---
111
112
  const wrapperClasses = $derived(
112
113
  clsx(
@@ -408,55 +409,41 @@
408
409
  // Define confirm function
409
410
  let isSubmitting = false; // Prevent double submit
410
411
  const handleConfirm = (confirmedValue: Suggestion | undefined) => {
411
- console.log('handleConfirm called with:', confirmedValue, 'isSubmitting:', isSubmitting); // Debug log
412
-
413
- if (confirmedValue === undefined) return;
414
-
415
- // Reset submitting flag at the start of each new confirmation
416
- isSubmitting = false;
412
+ if (confirmedValue === undefined || isSubmitting) return;
413
+
414
+ isSubmitting = true;
417
415
 
418
- // Re-assign selectedValue before any form-based guard checks (!form) so bindings still update
419
- // (e.g. when no <form> exists around the component usage) and search component value is being used clienside without a page reload
416
+ // Update selectedValue
420
417
  selectedValue =
421
418
  typeof confirmedValue === "string"
422
419
  ? confirmedValue
423
420
  : confirmedValue.value;
424
421
 
425
- // Type assertion needed here
422
+ // Mark as accepted to prevent form submit handler from processing again
426
423
  const inputElement =
427
424
  autocompleteInstance?.inputElement as HTMLInputElement;
428
- const form = containerElement?.closest("form");
429
- const submitButton = containerElement?.querySelector('button[type="submit"]') as HTMLButtonElement;
430
-
431
- console.log('Submit button found:', !!submitButton); // Debug log
432
-
433
- // Always focus the submit button first, regardless of form presence (if feature is enabled)
434
- if (autoFocusSubmitOnSelection && submitButton) {
435
- console.log('Focusing submit button'); // Debug log
436
- // Use requestAnimationFrame to ensure the focus happens after DOM updates
437
- requestAnimationFrame(() => {
438
- submitButton.focus();
439
- console.log('Submit button focused, document.activeElement:', document.activeElement === submitButton);
440
- });
425
+ if (inputElement) {
426
+ inputElement.value = inputValueTemplate(confirmedValue);
427
+ inputElement.dataset.autocompleteAccepted = "true";
441
428
  }
442
429
 
443
- // Handle form submission separately
444
- if (inputElement && form) {
445
- isSubmitting = true;
446
- inputElement.value = inputValueTemplate(confirmedValue);
447
- inputElement.dataset.autocompleteAccepted = "true"; // Set tracking attribute
448
-
449
- // Submit form immediately
450
- console.log('Submitting form'); // Debug log
451
- if (form.requestSubmit) {
452
- form.requestSubmit();
453
- } else {
454
- form.submit(); // Fallback for older browsers
430
+ // Auto-focus submit button if enabled
431
+ if (autoFocusSubmitOnSelection) {
432
+ const submitButton = containerElement?.querySelector(
433
+ 'button[type="submit"]',
434
+ ) as HTMLButtonElement | null;
435
+ if (submitButton) {
436
+ requestAnimationFrame(() => submitButton.focus());
455
437
  }
456
-
457
- // Reset flag after submission
458
- isSubmitting = false;
459
438
  }
439
+
440
+ // Submit form if present
441
+ const form = containerElement?.closest("form");
442
+ if (form) {
443
+ form.requestSubmit?.() ?? form.submit();
444
+ }
445
+
446
+ isSubmitting = false;
460
447
  };
461
448
 
462
449
  // Initialise accessible-autocomplete
@@ -505,15 +492,15 @@
505
492
  ".gem-c-search-with-autocomplete__menu",
506
493
  );
507
494
  // Listen for input changes on the autocomplete field
508
- autocompleteInputElement.addEventListener("input", () => {
495
+ on(autocompleteInputElement, "input", () => {
509
496
  const val = autocompleteInputElement.value;
510
-
497
+
511
498
  // Reset isSubmitting flag when user starts typing again
512
499
  if (isSubmitting) {
513
- console.log('User typing, resetting isSubmitting flag');
500
+ console.log("User typing, resetting isSubmitting flag");
514
501
  isSubmitting = false;
515
502
  }
516
-
503
+
517
504
  // Remove any existing 'too-short' warning before adding a new one to ensure we don't accumulate multiple warning items.
518
505
  suggestionsMenu
519
506
  ?.querySelector(
@@ -544,7 +531,7 @@
544
531
  // autocompleteInputElement.classList.add("autocomplete__input"); // Add specific class if needed
545
532
 
546
533
  // Add Enter key workaround from original JS
547
- autocompleteInputElement.addEventListener("keydown", (e) => {
534
+ on(autocompleteInputElement, "keydown", (e) => {
548
535
  if (isSubmitting) return; // Don't interfere if already submitting
549
536
  const dropdownVisible =
550
537
  autocompleteInputElement.getAttribute("aria-expanded") === "true";
@@ -567,6 +554,44 @@
567
554
  // );
568
555
  }
569
556
 
557
+ // Handle free-text submission when allowFreeTextSubmission is enabled
558
+ // handleConfirm is only called by the library when selecting from dropdown
559
+ // We need to catch: (1) form submit events, (2) button clicks outside forms
560
+ if (allowFreeTextSubmission) {
561
+ const updateFreeText = () => {
562
+ if (!autocompleteInputElement) return;
563
+
564
+ const wasAccepted =
565
+ autocompleteInputElement.dataset.autocompleteAccepted === "true";
566
+ const value = autocompleteInputElement.value?.trim();
567
+
568
+ if (value && !wasAccepted) {
569
+ selectedValue = value;
570
+ }
571
+
572
+ // Reset flag after submission
573
+ setTimeout(() => {
574
+ if (autocompleteInputElement) {
575
+ autocompleteInputElement.dataset.autocompleteAccepted = "false";
576
+ }
577
+ }, 100);
578
+ };
579
+
580
+ // Handle form submission
581
+ const form = containerElement?.closest("form");
582
+ if (form) {
583
+ on(form, "submit", updateFreeText);
584
+ }
585
+
586
+ // Handle button clicks (for non-form usage)
587
+ const submitButton = containerElement?.querySelector(
588
+ 'button[type="submit"]',
589
+ ) as HTMLButtonElement | null;
590
+ if (submitButton && !form) {
591
+ on(submitButton, "click", updateFreeText);
592
+ }
593
+ }
594
+
570
595
  // IMPORTANT: Remove the original Search.svelte input, as accessible-autocomplete replaces it.
571
596
  // We render it initially so accessible-autocomplete can grab its id, name, value.
572
597
  if (searchInput) {
@@ -40,6 +40,7 @@ type Props = {
40
40
  hideHint?: boolean;
41
41
  prefixMatchOnly?: boolean;
42
42
  autoFocusSubmitOnSelection?: boolean;
43
+ allowFreeTextSubmission?: boolean;
43
44
  };
44
45
  declare const SearchAutocomplete: import("svelte").Component<Props, {}, "selectedValue">;
45
46
  type SearchAutocomplete = ReturnType<typeof SearchAutocomplete>;