@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.
- package/README.md +7 -0
- package/dist/components/content/Tag.svelte +32 -0
- package/dist/components/content/Tag.svelte.d.ts +13 -0
- package/dist/components/data-vis/Histogram.svelte +287 -0
- package/dist/components/data-vis/Histogram.svelte.d.ts +75 -0
- package/dist/components/data-vis/axis/Axis.svelte +217 -34
- package/dist/components/data-vis/axis/Axis.svelte.d.ts +38 -30
- package/dist/components/data-vis/axis/Ticks.svelte +142 -78
- package/dist/components/data-vis/axis/Ticks.svelte.d.ts +28 -31
- package/dist/components/data-vis/line-chart/LineChart.svelte +51 -21
- package/dist/components/data-vis/line-chart/LineChart.svelte.d.ts +14 -6
- package/dist/components/data-vis/line-chart/ValueLabel.svelte +2 -1
- package/dist/components/data-vis/line-chart/ValueLabel.svelte.d.ts +2 -0
- package/dist/components/data-vis/position-chart/PositionChart.svelte +279 -122
- package/dist/components/data-vis/position-chart/PositionChart.svelte.d.ts +37 -5
- package/dist/components/data-vis/position-chart/PositionChartAxis.svelte +59 -48
- package/dist/components/data-vis/position-chart/PositionChartAxis.svelte.d.ts +4 -4
- package/dist/components/data-vis/table/Table.svelte +145 -95
- package/dist/components/data-vis/table/Table.svelte.d.ts +4 -4
- package/dist/components/layout/Footer.svelte +9 -0
- package/dist/components/layout/Footer.svelte.d.ts +1 -0
- package/dist/components/layout/PhaseBanner.svelte +10 -1
- package/dist/components/layout/PhaseBanner.svelte.d.ts +1 -0
- package/dist/components/layout/ServiceNavigation.svelte +19 -1
- package/dist/components/layout/ServiceNavigation.svelte.d.ts +2 -0
- package/dist/components/ui/Accordion.svelte +212 -66
- package/dist/components/ui/Accordion.svelte.d.ts +2 -0
- package/dist/components/ui/BasicMultiSelect.svelte +716 -0
- package/dist/components/ui/BasicMultiSelect.svelte.d.ts +18 -0
- package/dist/components/ui/Button.svelte +220 -104
- package/dist/components/ui/Button.svelte.d.ts +4 -0
- package/dist/components/ui/Card.svelte +48 -60
- package/dist/components/ui/Card.svelte.d.ts +26 -12
- package/dist/components/ui/CardHeader.svelte +46 -0
- package/dist/components/ui/CardHeader.svelte.d.ts +21 -0
- package/dist/components/ui/ChartExporter.svelte +142 -0
- package/dist/components/ui/ChartExporter.svelte.d.ts +16 -0
- package/dist/components/ui/CheckBox.svelte +1 -0
- package/dist/components/ui/Details.svelte +47 -8
- package/dist/components/ui/Details.svelte.d.ts +8 -10
- package/dist/components/ui/Masthead.svelte +44 -6
- package/dist/components/ui/Masthead.svelte.d.ts +6 -0
- package/dist/components/ui/RelatedContent.svelte +4 -1
- package/dist/components/ui/RelatedContent.svelte.d.ts +1 -0
- package/dist/components/ui/SearchAutocomplete.svelte +69 -44
- package/dist/components/ui/SearchAutocomplete.svelte.d.ts +1 -0
- package/dist/components/ui/Select.svelte +18 -7
- package/dist/components/ui/Tabs.svelte +192 -18
- package/dist/components/ui/Tabs.svelte.d.ts +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- 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;
|
|
@@ -1,12 +1,51 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
let {
|
|
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
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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:
|
|
92
|
-
padding-bottom:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
422
|
+
// Mark as accepted to prevent form submit handler from processing again
|
|
426
423
|
const inputElement =
|
|
427
424
|
autocompleteInstance?.inputElement as HTMLInputElement;
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
//
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
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(
|
|
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
|
|
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>;
|