govuk_tech_docs 4.2.0 → 4.3.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/govuk_tech_docs.gemspec +1 -1
  4. data/lib/assets/stylesheets/_govuk_tech_docs.scss +3 -0
  5. data/lib/govuk_tech_docs/version.rb +1 -1
  6. data/node_modules/govuk-frontend/dist/govuk/all.bundle.js +508 -209
  7. data/node_modules/govuk-frontend/dist/govuk/all.bundle.mjs +505 -208
  8. data/node_modules/govuk-frontend/dist/govuk/all.mjs +3 -1
  9. data/node_modules/govuk-frontend/dist/govuk/all.scss +6 -0
  10. data/node_modules/govuk-frontend/dist/govuk/common/configuration.mjs +169 -0
  11. data/node_modules/govuk-frontend/dist/govuk/common/govuk-frontend-version.mjs +1 -1
  12. data/node_modules/govuk-frontend/dist/govuk/common/index.mjs +4 -87
  13. data/node_modules/govuk-frontend/dist/govuk/{govuk-frontend-component.mjs → component.mjs} +5 -5
  14. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js +161 -116
  15. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs +160 -115
  16. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.mjs +5 -8
  17. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.js +161 -116
  18. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.mjs +160 -115
  19. data/node_modules/govuk-frontend/dist/govuk/components/button/button.mjs +5 -8
  20. data/node_modules/govuk-frontend/dist/govuk/components/character-count/_index.scss +8 -0
  21. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js +187 -145
  22. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs +186 -144
  23. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.mjs +18 -17
  24. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js +9 -29
  25. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs +8 -28
  26. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs +2 -2
  27. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js +161 -116
  28. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs +160 -115
  29. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs +6 -8
  30. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js +161 -116
  31. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs +160 -115
  32. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs +5 -8
  33. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/_index.scss +167 -0
  34. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js +754 -0
  35. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs +746 -0
  36. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs +267 -0
  37. data/node_modules/govuk-frontend/dist/govuk/components/header/_index.scss +14 -10
  38. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.js +9 -29
  39. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.mjs +8 -28
  40. data/node_modules/govuk-frontend/dist/govuk/components/header/header.mjs +2 -2
  41. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js +161 -116
  42. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs +160 -115
  43. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs +6 -8
  44. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js +161 -117
  45. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs +160 -116
  46. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.mjs +5 -9
  47. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.js +9 -29
  48. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs +8 -28
  49. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.mjs +2 -2
  50. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js +9 -29
  51. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs +8 -28
  52. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs +2 -2
  53. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js +10 -30
  54. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs +9 -29
  55. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs +3 -3
  56. data/node_modules/govuk-frontend/dist/govuk/components/summary-list/_index.scss +12 -21
  57. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js +9 -29
  58. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs +8 -28
  59. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.mjs +2 -2
  60. data/node_modules/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss +1 -1
  61. data/node_modules/govuk-frontend/dist/govuk/errors/index.mjs +1 -1
  62. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.js +1 -1
  63. data/node_modules/govuk-frontend/dist/govuk/helpers/_colour.scss +2 -2
  64. data/node_modules/govuk-frontend/dist/govuk/init.mjs +28 -24
  65. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-organisations.scss +18 -5
  66. data/node_modules/govuk-frontend/dist/govuk/settings/_typography-responsive.scss +5 -10
  67. data/node_modules/govuk-frontend/dist/govuk-prototype-kit/init.scss +1 -1
  68. data/package-lock.json +8 -7
  69. data/package.json +1 -1
  70. metadata +12 -10
  71. data/node_modules/govuk-frontend/dist/govuk/common/normalise-dataset.mjs +0 -18
  72. data/node_modules/govuk-frontend/dist/govuk/common/normalise-string.mjs +0 -31
@@ -1,81 +1,11 @@
1
1
  (function (global, factory) {
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
3
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = global.GOVUKFrontend || {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- const version = '5.7.1';
7
+ const version = '5.9.0';
8
8
 
9
- function normaliseString(value, property) {
10
- const trimmedValue = value ? value.trim() : '';
11
- let output;
12
- let outputType = property == null ? void 0 : property.type;
13
- if (!outputType) {
14
- if (['true', 'false'].includes(trimmedValue)) {
15
- outputType = 'boolean';
16
- }
17
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
18
- outputType = 'number';
19
- }
20
- }
21
- switch (outputType) {
22
- case 'boolean':
23
- output = trimmedValue === 'true';
24
- break;
25
- case 'number':
26
- output = Number(trimmedValue);
27
- break;
28
- default:
29
- output = value;
30
- }
31
- return output;
32
- }
33
-
34
- /**
35
- * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
36
- */
37
-
38
- function mergeConfigs(...configObjects) {
39
- const formattedConfigObject = {};
40
- for (const configObject of configObjects) {
41
- for (const key of Object.keys(configObject)) {
42
- const option = formattedConfigObject[key];
43
- const override = configObject[key];
44
- if (isObject(option) && isObject(override)) {
45
- formattedConfigObject[key] = mergeConfigs(option, override);
46
- } else {
47
- formattedConfigObject[key] = override;
48
- }
49
- }
50
- }
51
- return formattedConfigObject;
52
- }
53
- function extractConfigByNamespace(Component, dataset, namespace) {
54
- const property = Component.schema.properties[namespace];
55
- if ((property == null ? void 0 : property.type) !== 'object') {
56
- return;
57
- }
58
- const newObject = {
59
- [namespace]: ({})
60
- };
61
- for (const [key, value] of Object.entries(dataset)) {
62
- let current = newObject;
63
- const keyParts = key.split('.');
64
- for (const [index, name] of keyParts.entries()) {
65
- if (typeof current === 'object') {
66
- if (index < keyParts.length - 1) {
67
- if (!isObject(current[name])) {
68
- current[name] = {};
69
- }
70
- current = current[name];
71
- } else if (key !== namespace) {
72
- current[name] = normaliseString(value);
73
- }
74
- }
75
- }
76
- }
77
- return newObject[namespace];
78
- }
79
9
  function getFragmentFromUrl(url) {
80
10
  if (!url.includes('#')) {
81
11
  return undefined;
@@ -133,26 +63,6 @@
133
63
  }
134
64
  return $scope.classList.contains('govuk-frontend-supported');
135
65
  }
136
- function validateConfig(schema, config) {
137
- const validationErrors = [];
138
- for (const [name, conditions] of Object.entries(schema)) {
139
- const errors = [];
140
- if (Array.isArray(conditions)) {
141
- for (const {
142
- required,
143
- errorMessage
144
- } of conditions) {
145
- if (!required.every(key => !!config[key])) {
146
- errors.push(errorMessage);
147
- }
148
- }
149
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
150
- validationErrors.push(...errors);
151
- }
152
- }
153
- }
154
- return validationErrors;
155
- }
156
66
  function isArray(option) {
157
67
  return Array.isArray(option);
158
68
  }
@@ -162,46 +72,13 @@
162
72
  function formatErrorMessage(Component, message) {
163
73
  return `${Component.moduleName}: ${message}`;
164
74
  }
165
-
166
- /**
167
- * Schema for component config
168
- *
169
- * @typedef {object} Schema
170
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
171
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
172
- */
173
-
174
- /**
175
- * Schema property for component config
176
- *
177
- * @typedef {object} SchemaProperty
178
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
179
- */
180
-
181
- /**
182
- * Schema condition for component config
183
- *
184
- * @typedef {object} SchemaCondition
185
- * @property {string[]} required - List of required config fields
186
- * @property {string} errorMessage - Error message when required config fields not provided
187
- */
188
75
  /**
189
76
  * @typedef ComponentWithModuleName
190
77
  * @property {string} moduleName - Name of the component
191
78
  */
192
-
193
- function normaliseDataset(Component, dataset) {
194
- const out = {};
195
- for (const [field, property] of Object.entries(Component.schema.properties)) {
196
- if (field in dataset) {
197
- out[field] = normaliseString(dataset[field], property);
198
- }
199
- if ((property == null ? void 0 : property.type) === 'object') {
200
- out[field] = extractConfigByNamespace(Component, dataset, field);
201
- }
202
- }
203
- return out;
204
- }
79
+ /**
80
+ * @import { ObjectNested } from './configuration.mjs'
81
+ */
205
82
 
206
83
  class GOVUKFrontendError extends Error {
207
84
  constructor(...args) {
@@ -253,10 +130,10 @@
253
130
  }
254
131
  }
255
132
  /**
256
- * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
133
+ * @import { ComponentWithModuleName } from '../common/index.mjs'
257
134
  */
258
135
 
259
- class GOVUKFrontendComponent {
136
+ class Component {
260
137
  /**
261
138
  * Returns the root element of the component
262
139
  *
@@ -307,9 +184,172 @@
307
184
  */
308
185
 
309
186
  /**
310
- * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
187
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
188
+ */
189
+ Component.elementType = HTMLElement;
190
+
191
+ const configOverride = Symbol.for('configOverride');
192
+ class ConfigurableComponent extends Component {
193
+ [configOverride](param) {
194
+ return {};
195
+ }
196
+
197
+ /**
198
+ * Returns the root element of the component
199
+ *
200
+ * @protected
201
+ * @returns {ConfigurationType} - the root element of component
202
+ */
203
+ get config() {
204
+ return this._config;
205
+ }
206
+ constructor($root, config) {
207
+ super($root);
208
+ this._config = void 0;
209
+ const childConstructor = this.constructor;
210
+ if (!isObject(childConstructor.defaults)) {
211
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
212
+ }
213
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
214
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
215
+ }
216
+ }
217
+ function normaliseString(value, property) {
218
+ const trimmedValue = value ? value.trim() : '';
219
+ let output;
220
+ let outputType = property == null ? void 0 : property.type;
221
+ if (!outputType) {
222
+ if (['true', 'false'].includes(trimmedValue)) {
223
+ outputType = 'boolean';
224
+ }
225
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
226
+ outputType = 'number';
227
+ }
228
+ }
229
+ switch (outputType) {
230
+ case 'boolean':
231
+ output = trimmedValue === 'true';
232
+ break;
233
+ case 'number':
234
+ output = Number(trimmedValue);
235
+ break;
236
+ default:
237
+ output = value;
238
+ }
239
+ return output;
240
+ }
241
+ function normaliseDataset(Component, dataset) {
242
+ if (!isObject(Component.schema)) {
243
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
244
+ }
245
+ const out = {};
246
+ const entries = Object.entries(Component.schema.properties);
247
+ for (const entry of entries) {
248
+ const [namespace, property] = entry;
249
+ const field = namespace.toString();
250
+ if (field in dataset) {
251
+ out[field] = normaliseString(dataset[field], property);
252
+ }
253
+ if ((property == null ? void 0 : property.type) === 'object') {
254
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
255
+ }
256
+ }
257
+ return out;
258
+ }
259
+ function mergeConfigs(...configObjects) {
260
+ const formattedConfigObject = {};
261
+ for (const configObject of configObjects) {
262
+ for (const key of Object.keys(configObject)) {
263
+ const option = formattedConfigObject[key];
264
+ const override = configObject[key];
265
+ if (isObject(option) && isObject(override)) {
266
+ formattedConfigObject[key] = mergeConfigs(option, override);
267
+ } else {
268
+ formattedConfigObject[key] = override;
269
+ }
270
+ }
271
+ }
272
+ return formattedConfigObject;
273
+ }
274
+ function validateConfig(schema, config) {
275
+ const validationErrors = [];
276
+ for (const [name, conditions] of Object.entries(schema)) {
277
+ const errors = [];
278
+ if (Array.isArray(conditions)) {
279
+ for (const {
280
+ required,
281
+ errorMessage
282
+ } of conditions) {
283
+ if (!required.every(key => !!config[key])) {
284
+ errors.push(errorMessage);
285
+ }
286
+ }
287
+ if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
288
+ validationErrors.push(...errors);
289
+ }
290
+ }
291
+ }
292
+ return validationErrors;
293
+ }
294
+ function extractConfigByNamespace(schema, dataset, namespace) {
295
+ const property = schema.properties[namespace];
296
+ if ((property == null ? void 0 : property.type) !== 'object') {
297
+ return;
298
+ }
299
+ const newObject = {
300
+ [namespace]: {}
301
+ };
302
+ for (const [key, value] of Object.entries(dataset)) {
303
+ let current = newObject;
304
+ const keyParts = key.split('.');
305
+ for (const [index, name] of keyParts.entries()) {
306
+ if (isObject(current)) {
307
+ if (index < keyParts.length - 1) {
308
+ if (!isObject(current[name])) {
309
+ current[name] = {};
310
+ }
311
+ current = current[name];
312
+ } else if (key !== namespace) {
313
+ current[name] = normaliseString(value);
314
+ }
315
+ }
316
+ }
317
+ }
318
+ return newObject[namespace];
319
+ }
320
+ /**
321
+ * Schema for component config
322
+ *
323
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
324
+ * @typedef {object} Schema
325
+ * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties
326
+ * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions
327
+ */
328
+ /**
329
+ * Schema property for component config
330
+ *
331
+ * @typedef {object} SchemaProperty
332
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
333
+ */
334
+ /**
335
+ * Schema condition for component config
336
+ *
337
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
338
+ * @typedef {object} SchemaCondition
339
+ * @property {(keyof ConfigurationType)[]} required - List of required config fields
340
+ * @property {string} errorMessage - Error message when required config fields not provided
341
+ */
342
+ /**
343
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
344
+ * @typedef ChildClass
345
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
346
+ * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration
347
+ * @property {ConfigurationType} [defaults] - The default values of the configuration of the component
348
+ */
349
+ /**
350
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
351
+ * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
311
352
  */
312
- GOVUKFrontendComponent.elementType = HTMLElement;
313
353
 
314
354
  class I18n {
315
355
  constructor(translations = {}, config = {}) {
@@ -517,15 +557,15 @@
517
557
  * attribute, which also provides accessibility.
518
558
  *
519
559
  * @preserve
560
+ * @augments ConfigurableComponent<AccordionConfig>
520
561
  */
521
- class Accordion extends GOVUKFrontendComponent {
562
+ class Accordion extends ConfigurableComponent {
522
563
  /**
523
564
  * @param {Element | null} $root - HTML element to use for accordion
524
565
  * @param {AccordionConfig} [config] - Accordion config
525
566
  */
526
567
  constructor($root, config = {}) {
527
- super($root);
528
- this.config = void 0;
568
+ super($root, config);
529
569
  this.i18n = void 0;
530
570
  this.controlsClass = 'govuk-accordion__controls';
531
571
  this.showAllClass = 'govuk-accordion__show-all';
@@ -550,7 +590,6 @@
550
590
  this.$showAllButton = null;
551
591
  this.$showAllIcon = null;
552
592
  this.$showAllText = null;
553
- this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
554
593
  this.i18n = new I18n(this.config.i18n);
555
594
  const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
556
595
  if (!$sections.length) {
@@ -820,7 +859,7 @@
820
859
  */
821
860
 
822
861
  /**
823
- * @typedef {import('../../common/index.mjs').Schema} Schema
862
+ * @import { Schema } from '../../common/configuration.mjs'
824
863
  */
825
864
  Accordion.moduleName = 'govuk-accordion';
826
865
  Accordion.defaults = Object.freeze({
@@ -851,17 +890,16 @@
851
890
  * JavaScript enhancements for the Button component
852
891
  *
853
892
  * @preserve
893
+ * @augments ConfigurableComponent<ButtonConfig>
854
894
  */
855
- class Button extends GOVUKFrontendComponent {
895
+ class Button extends ConfigurableComponent {
856
896
  /**
857
897
  * @param {Element | null} $root - HTML element to use for button
858
898
  * @param {ButtonConfig} [config] - Button config
859
899
  */
860
900
  constructor($root, config = {}) {
861
- super($root);
862
- this.config = void 0;
901
+ super($root, config);
863
902
  this.debounceFormSubmitTimer = null;
864
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
865
903
  this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
866
904
  this.$root.addEventListener('click', event => this.debounce(event));
867
905
  }
@@ -898,7 +936,7 @@
898
936
  */
899
937
 
900
938
  /**
901
- * @typedef {import('../../common/index.mjs').Schema} Schema
939
+ * @import { Schema } from '../../common/configuration.mjs'
902
940
  */
903
941
  Button.moduleName = 'govuk-button';
904
942
  Button.defaults = Object.freeze({
@@ -928,22 +966,33 @@
928
966
  * of the available characters/words has been entered.
929
967
  *
930
968
  * @preserve
969
+ * @augments ConfigurableComponent<CharacterCountConfig>
931
970
  */
932
- class CharacterCount extends GOVUKFrontendComponent {
971
+ class CharacterCount extends ConfigurableComponent {
972
+ [configOverride](datasetConfig) {
973
+ let configOverrides = {};
974
+ if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
975
+ configOverrides = {
976
+ maxlength: undefined,
977
+ maxwords: undefined
978
+ };
979
+ }
980
+ return configOverrides;
981
+ }
982
+
933
983
  /**
934
984
  * @param {Element | null} $root - HTML element to use for character count
935
985
  * @param {CharacterCountConfig} [config] - Character count config
936
986
  */
937
987
  constructor($root, config = {}) {
938
988
  var _ref, _this$config$maxwords;
939
- super($root);
989
+ super($root, config);
940
990
  this.$textarea = void 0;
941
991
  this.$visibleCountMessage = void 0;
942
992
  this.$screenReaderCountMessage = void 0;
943
993
  this.lastInputTimestamp = null;
944
994
  this.lastInputValue = '';
945
995
  this.valueChecker = null;
946
- this.config = void 0;
947
996
  this.i18n = void 0;
948
997
  this.maxLength = void 0;
949
998
  const $textarea = this.$root.querySelector('.govuk-js-character-count');
@@ -955,15 +1004,6 @@
955
1004
  identifier: 'Form field (`.govuk-js-character-count`)'
956
1005
  });
957
1006
  }
958
- const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
959
- let configOverrides = {};
960
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
961
- configOverrides = {
962
- maxlength: undefined,
963
- maxwords: undefined
964
- };
965
- }
966
- this.config = mergeConfigs(CharacterCount.defaults, config, configOverrides, datasetConfig);
967
1007
  const errors = validateConfig(CharacterCount.schema, this.config);
968
1008
  if (errors[0]) {
969
1009
  throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
@@ -1149,8 +1189,8 @@
1149
1189
  */
1150
1190
 
1151
1191
  /**
1152
- * @typedef {import('../../common/index.mjs').Schema} Schema
1153
- * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
1192
+ * @import { Schema } from '../../common/configuration.mjs'
1193
+ * @import { TranslationPluralForms } from '../../i18n.mjs'
1154
1194
  */
1155
1195
  CharacterCount.moduleName = 'govuk-character-count';
1156
1196
  CharacterCount.defaults = Object.freeze({
@@ -1208,7 +1248,7 @@
1208
1248
  *
1209
1249
  * @preserve
1210
1250
  */
1211
- class Checkboxes extends GOVUKFrontendComponent {
1251
+ class Checkboxes extends Component {
1212
1252
  /**
1213
1253
  * Checkboxes can be associated with a 'conditionally revealed' content block
1214
1254
  * – for example, a checkbox for 'Phone' could reveal an additional form field
@@ -1316,16 +1356,15 @@
1316
1356
  * configuration.
1317
1357
  *
1318
1358
  * @preserve
1359
+ * @augments ConfigurableComponent<ErrorSummaryConfig>
1319
1360
  */
1320
- class ErrorSummary extends GOVUKFrontendComponent {
1361
+ class ErrorSummary extends ConfigurableComponent {
1321
1362
  /**
1322
1363
  * @param {Element | null} $root - HTML element to use for error summary
1323
1364
  * @param {ErrorSummaryConfig} [config] - Error summary config
1324
1365
  */
1325
1366
  constructor($root, config = {}) {
1326
- super($root);
1327
- this.config = void 0;
1328
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
1367
+ super($root, config);
1329
1368
  if (!this.config.disableAutoFocus) {
1330
1369
  setFocus(this.$root);
1331
1370
  }
@@ -1392,7 +1431,7 @@
1392
1431
  */
1393
1432
 
1394
1433
  /**
1395
- * @typedef {import('../../common/index.mjs').Schema} Schema
1434
+ * @import { Schema } from '../../common/configuration.mjs'
1396
1435
  */
1397
1436
  ErrorSummary.moduleName = 'govuk-error-summary';
1398
1437
  ErrorSummary.defaults = Object.freeze({
@@ -1410,15 +1449,15 @@
1410
1449
  * Exit this page component
1411
1450
  *
1412
1451
  * @preserve
1452
+ * @augments ConfigurableComponent<ExitThisPageConfig>
1413
1453
  */
1414
- class ExitThisPage extends GOVUKFrontendComponent {
1454
+ class ExitThisPage extends ConfigurableComponent {
1415
1455
  /**
1416
1456
  * @param {Element | null} $root - HTML element that wraps the Exit This Page button
1417
1457
  * @param {ExitThisPageConfig} [config] - Exit This Page config
1418
1458
  */
1419
1459
  constructor($root, config = {}) {
1420
- super($root);
1421
- this.config = void 0;
1460
+ super($root, config);
1422
1461
  this.i18n = void 0;
1423
1462
  this.$button = void 0;
1424
1463
  this.$skiplinkButton = null;
@@ -1439,7 +1478,6 @@
1439
1478
  identifier: 'Button (`.govuk-exit-this-page__button`)'
1440
1479
  });
1441
1480
  }
1442
- this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, this.$root.dataset));
1443
1481
  this.i18n = new I18n(this.config.i18n);
1444
1482
  this.$button = $button;
1445
1483
  const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
@@ -1605,7 +1643,7 @@
1605
1643
  */
1606
1644
 
1607
1645
  /**
1608
- * @typedef {import('../../common/index.mjs').Schema} Schema
1646
+ * @import { Schema } from '../../common/configuration.mjs'
1609
1647
  */
1610
1648
  ExitThisPage.moduleName = 'govuk-exit-this-page';
1611
1649
  ExitThisPage.defaults = Object.freeze({
@@ -1624,12 +1662,271 @@
1624
1662
  }
1625
1663
  });
1626
1664
 
1665
+ /**
1666
+ * File upload component
1667
+ *
1668
+ * @preserve
1669
+ * @augments ConfigurableComponent<FileUploadConfig>
1670
+ */
1671
+ class FileUpload extends ConfigurableComponent {
1672
+ /**
1673
+ * @param {Element | null} $root - File input element
1674
+ * @param {FileUploadConfig} [config] - File Upload config
1675
+ */
1676
+ constructor($root, config = {}) {
1677
+ super($root, config);
1678
+ this.$input = void 0;
1679
+ this.$button = void 0;
1680
+ this.$status = void 0;
1681
+ this.i18n = void 0;
1682
+ this.id = void 0;
1683
+ const $input = this.$root.querySelector('input');
1684
+ if ($input === null) {
1685
+ throw new ElementError({
1686
+ component: FileUpload,
1687
+ identifier: 'File inputs (`<input type="file">`)'
1688
+ });
1689
+ }
1690
+ if ($input.type !== 'file') {
1691
+ throw new ElementError(formatErrorMessage(FileUpload, 'File input (`<input type="file">`) attribute (`type`) is not `file`'));
1692
+ }
1693
+ this.$input = $input;
1694
+ this.$input.setAttribute('hidden', 'true');
1695
+ if (!this.$input.id) {
1696
+ throw new ElementError({
1697
+ component: FileUpload,
1698
+ identifier: 'File input (`<input type="file">`) attribute (`id`)'
1699
+ });
1700
+ }
1701
+ this.id = this.$input.id;
1702
+ this.i18n = new I18n(this.config.i18n, {
1703
+ locale: closestAttributeValue(this.$root, 'lang')
1704
+ });
1705
+ const $label = this.findLabel();
1706
+ if (!$label.id) {
1707
+ $label.id = `${this.id}-label`;
1708
+ }
1709
+ this.$input.id = `${this.id}-input`;
1710
+ const $button = document.createElement('button');
1711
+ $button.classList.add('govuk-file-upload-button');
1712
+ $button.type = 'button';
1713
+ $button.id = this.id;
1714
+ $button.classList.add('govuk-file-upload-button--empty');
1715
+ const ariaDescribedBy = this.$input.getAttribute('aria-describedby');
1716
+ if (ariaDescribedBy) {
1717
+ $button.setAttribute('aria-describedby', ariaDescribedBy);
1718
+ }
1719
+ const $status = document.createElement('span');
1720
+ $status.className = 'govuk-body govuk-file-upload-button__status';
1721
+ $status.setAttribute('aria-live', 'polite');
1722
+ $status.innerText = this.i18n.t('noFileChosen');
1723
+ $button.appendChild($status);
1724
+ const commaSpan = document.createElement('span');
1725
+ commaSpan.className = 'govuk-visually-hidden';
1726
+ commaSpan.innerText = ', ';
1727
+ commaSpan.id = `${this.id}-comma`;
1728
+ $button.appendChild(commaSpan);
1729
+ const containerSpan = document.createElement('span');
1730
+ containerSpan.className = 'govuk-file-upload-button__pseudo-button-container';
1731
+ const buttonSpan = document.createElement('span');
1732
+ buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload-button__pseudo-button';
1733
+ buttonSpan.innerText = this.i18n.t('chooseFilesButton');
1734
+ containerSpan.appendChild(buttonSpan);
1735
+ containerSpan.insertAdjacentText('beforeend', ' ');
1736
+ const instructionSpan = document.createElement('span');
1737
+ instructionSpan.className = 'govuk-body govuk-file-upload-button__instruction';
1738
+ instructionSpan.innerText = this.i18n.t('dropInstruction');
1739
+ containerSpan.appendChild(instructionSpan);
1740
+ $button.appendChild(containerSpan);
1741
+ $button.setAttribute('aria-labelledby', `${$label.id} ${commaSpan.id} ${$button.id}`);
1742
+ $button.addEventListener('click', this.onClick.bind(this));
1743
+ $button.addEventListener('dragover', event => {
1744
+ event.preventDefault();
1745
+ });
1746
+ this.$root.insertAdjacentElement('afterbegin', $button);
1747
+ this.$input.setAttribute('tabindex', '-1');
1748
+ this.$input.setAttribute('aria-hidden', 'true');
1749
+ this.$button = $button;
1750
+ this.$status = $status;
1751
+ this.$input.addEventListener('change', this.onChange.bind(this));
1752
+ this.updateDisabledState();
1753
+ this.observeDisabledState();
1754
+ this.$announcements = document.createElement('span');
1755
+ this.$announcements.classList.add('govuk-file-upload-announcements');
1756
+ this.$announcements.classList.add('govuk-visually-hidden');
1757
+ this.$announcements.setAttribute('aria-live', 'assertive');
1758
+ this.$root.insertAdjacentElement('afterend', this.$announcements);
1759
+ this.$button.addEventListener('drop', this.onDrop.bind(this));
1760
+ document.addEventListener('dragenter', this.updateDropzoneVisibility.bind(this));
1761
+ document.addEventListener('dragenter', () => {
1762
+ this.enteredAnotherElement = true;
1763
+ });
1764
+ document.addEventListener('dragleave', () => {
1765
+ if (!this.enteredAnotherElement && !this.$button.disabled) {
1766
+ this.hideDraggingState();
1767
+ this.$announcements.innerText = this.i18n.t('leftDropZone');
1768
+ }
1769
+ this.enteredAnotherElement = false;
1770
+ });
1771
+ }
1772
+
1773
+ /**
1774
+ * Updates the visibility of the dropzone as users enters the various elements on the page
1775
+ *
1776
+ * @param {DragEvent} event - The `dragenter` event
1777
+ */
1778
+ updateDropzoneVisibility(event) {
1779
+ if (this.$button.disabled) return;
1780
+ if (event.target instanceof Node) {
1781
+ if (this.$root.contains(event.target)) {
1782
+ if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
1783
+ if (!this.$button.classList.contains('govuk-file-upload-button--dragging')) {
1784
+ this.showDraggingState();
1785
+ this.$announcements.innerText = this.i18n.t('enteredDropZone');
1786
+ }
1787
+ }
1788
+ } else {
1789
+ if (this.$button.classList.contains('govuk-file-upload-button--dragging')) {
1790
+ this.hideDraggingState();
1791
+ this.$announcements.innerText = this.i18n.t('leftDropZone');
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ showDraggingState() {
1797
+ this.$button.classList.add('govuk-file-upload-button--dragging');
1798
+ }
1799
+ hideDraggingState() {
1800
+ this.$button.classList.remove('govuk-file-upload-button--dragging');
1801
+ }
1802
+
1803
+ /**
1804
+ * Handles user dropping on the component
1805
+ *
1806
+ * @param {DragEvent} event - The `dragenter` event
1807
+ */
1808
+ onDrop(event) {
1809
+ event.preventDefault();
1810
+ if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
1811
+ this.$input.files = event.dataTransfer.files;
1812
+ this.$input.dispatchEvent(new CustomEvent('change'));
1813
+ this.hideDraggingState();
1814
+ }
1815
+ }
1816
+ onChange() {
1817
+ const fileCount = this.$input.files.length;
1818
+ if (fileCount === 0) {
1819
+ this.$status.innerText = this.i18n.t('noFileChosen');
1820
+ this.$button.classList.add('govuk-file-upload-button--empty');
1821
+ } else {
1822
+ if (fileCount === 1) {
1823
+ this.$status.innerText = this.$input.files[0].name;
1824
+ } else {
1825
+ this.$status.innerText = this.i18n.t('multipleFilesChosen', {
1826
+ count: fileCount
1827
+ });
1828
+ }
1829
+ this.$button.classList.remove('govuk-file-upload-button--empty');
1830
+ }
1831
+ }
1832
+ findLabel() {
1833
+ const $label = document.querySelector(`label[for="${this.$input.id}"]`);
1834
+ if (!$label) {
1835
+ throw new ElementError({
1836
+ component: FileUpload,
1837
+ identifier: `Field label (\`<label for=${this.$input.id}>\`)`
1838
+ });
1839
+ }
1840
+ return $label;
1841
+ }
1842
+ onClick() {
1843
+ this.$input.click();
1844
+ }
1845
+ observeDisabledState() {
1846
+ const observer = new MutationObserver(mutationList => {
1847
+ for (const mutation of mutationList) {
1848
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
1849
+ this.updateDisabledState();
1850
+ }
1851
+ }
1852
+ });
1853
+ observer.observe(this.$input, {
1854
+ attributes: true
1855
+ });
1856
+ }
1857
+ updateDisabledState() {
1858
+ this.$button.disabled = this.$input.disabled;
1859
+ this.$root.classList.toggle('govuk-drop-zone--disabled', this.$button.disabled);
1860
+ }
1861
+ }
1862
+ FileUpload.moduleName = 'govuk-file-upload';
1863
+ FileUpload.defaults = Object.freeze({
1864
+ i18n: {
1865
+ chooseFilesButton: 'Choose file',
1866
+ dropInstruction: 'or drop file',
1867
+ noFileChosen: 'No file chosen',
1868
+ multipleFilesChosen: {
1869
+ one: '%{count} file chosen',
1870
+ other: '%{count} files chosen'
1871
+ },
1872
+ enteredDropZone: 'Entered drop zone',
1873
+ leftDropZone: 'Left drop zone'
1874
+ }
1875
+ });
1876
+ FileUpload.schema = Object.freeze({
1877
+ properties: {
1878
+ i18n: {
1879
+ type: 'object'
1880
+ }
1881
+ }
1882
+ });
1883
+ function isContainingFiles(dataTransfer) {
1884
+ const hasNoTypesInfo = dataTransfer.types.length === 0;
1885
+ const isDraggingFiles = dataTransfer.types.some(type => type === 'Files');
1886
+ return hasNoTypesInfo || isDraggingFiles;
1887
+ }
1888
+
1889
+ /**
1890
+ * @typedef {HTMLInputElement & {files: FileList}} HTMLFileInputElement
1891
+ */
1892
+
1893
+ /**
1894
+ * File upload config
1895
+ *
1896
+ * @see {@link FileUpload.defaults}
1897
+ * @typedef {object} FileUploadConfig
1898
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
1899
+ */
1900
+
1901
+ /**
1902
+ * File upload translations
1903
+ *
1904
+ * @see {@link FileUpload.defaults.i18n}
1905
+ * @typedef {object} FileUploadTranslations
1906
+ *
1907
+ * Messages used by the component
1908
+ * @property {string} [chooseFile] - The text of the button that opens the file picker
1909
+ * @property {string} [dropInstruction] - The text informing users they can drop files
1910
+ * @property {TranslationPluralForms} [multipleFilesChosen] - The text displayed when multiple files
1911
+ * have been chosen by the user
1912
+ * @property {string} [noFileChosen] - The text to displayed when no file has been chosen by the user
1913
+ * @property {string} [enteredDropZone] - The text announced by assistive technology
1914
+ * when user drags files and enters the drop zone
1915
+ * @property {string} [leftDropZone] - The text announced by assistive technology
1916
+ * when user drags files and leaves the drop zone without dropping
1917
+ */
1918
+
1919
+ /**
1920
+ * @import { Schema } from '../../common/configuration.mjs'
1921
+ * @import { TranslationPluralForms } from '../../i18n.mjs'
1922
+ */
1923
+
1627
1924
  /**
1628
1925
  * Header component
1629
1926
  *
1630
1927
  * @preserve
1631
1928
  */
1632
- class Header extends GOVUKFrontendComponent {
1929
+ class Header extends Component {
1633
1930
  /**
1634
1931
  * Apply a matchMedia for desktop which will trigger a state sync if the
1635
1932
  * browser viewport moves between states.
@@ -1710,16 +2007,15 @@
1710
2007
  * Notification Banner component
1711
2008
  *
1712
2009
  * @preserve
2010
+ * @augments ConfigurableComponent<NotificationBannerConfig>
1713
2011
  */
1714
- class NotificationBanner extends GOVUKFrontendComponent {
2012
+ class NotificationBanner extends ConfigurableComponent {
1715
2013
  /**
1716
2014
  * @param {Element | null} $root - HTML element to use for notification banner
1717
2015
  * @param {NotificationBannerConfig} [config] - Notification banner config
1718
2016
  */
1719
2017
  constructor($root, config = {}) {
1720
- super($root);
1721
- this.config = void 0;
1722
- this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, this.$root.dataset));
2018
+ super($root, config);
1723
2019
  if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
1724
2020
  setFocus(this.$root);
1725
2021
  }
@@ -1737,7 +2033,7 @@
1737
2033
  */
1738
2034
 
1739
2035
  /**
1740
- * @typedef {import('../../common/index.mjs').Schema} Schema
2036
+ * @import { Schema } from '../../common/configuration.mjs'
1741
2037
  */
1742
2038
  NotificationBanner.moduleName = 'govuk-notification-banner';
1743
2039
  NotificationBanner.defaults = Object.freeze({
@@ -1755,15 +2051,15 @@
1755
2051
  * Password input component
1756
2052
  *
1757
2053
  * @preserve
2054
+ * @augments ConfigurableComponent<PasswordInputConfig>
1758
2055
  */
1759
- class PasswordInput extends GOVUKFrontendComponent {
2056
+ class PasswordInput extends ConfigurableComponent {
1760
2057
  /**
1761
2058
  * @param {Element | null} $root - HTML element to use for password input
1762
2059
  * @param {PasswordInputConfig} [config] - Password input config
1763
2060
  */
1764
2061
  constructor($root, config = {}) {
1765
- super($root);
1766
- this.config = void 0;
2062
+ super($root, config);
1767
2063
  this.i18n = void 0;
1768
2064
  this.$input = void 0;
1769
2065
  this.$showHideButton = void 0;
@@ -1794,7 +2090,6 @@
1794
2090
  }
1795
2091
  this.$input = $input;
1796
2092
  this.$showHideButton = $showHideButton;
1797
- this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, this.$root.dataset));
1798
2093
  this.i18n = new I18n(this.config.i18n, {
1799
2094
  locale: closestAttributeValue(this.$root, 'lang')
1800
2095
  });
@@ -1874,8 +2169,7 @@
1874
2169
  */
1875
2170
 
1876
2171
  /**
1877
- * @typedef {import('../../common/index.mjs').Schema} Schema
1878
- * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
2172
+ * @import { Schema } from '../../common/configuration.mjs'
1879
2173
  */
1880
2174
  PasswordInput.moduleName = 'govuk-password-input';
1881
2175
  PasswordInput.defaults = Object.freeze({
@@ -1901,7 +2195,7 @@
1901
2195
  *
1902
2196
  * @preserve
1903
2197
  */
1904
- class Radios extends GOVUKFrontendComponent {
2198
+ class Radios extends Component {
1905
2199
  /**
1906
2200
  * Radios can be associated with a 'conditionally revealed' content block –
1907
2201
  * for example, a radio for 'Phone' could reveal an additional form field for
@@ -1984,7 +2278,7 @@
1984
2278
  *
1985
2279
  * @preserve
1986
2280
  */
1987
- class ServiceNavigation extends GOVUKFrontendComponent {
2281
+ class ServiceNavigation extends Component {
1988
2282
  /**
1989
2283
  * @param {Element | null} $root - HTML element to use for header
1990
2284
  */
@@ -2062,9 +2356,9 @@
2062
2356
  * Skip link component
2063
2357
  *
2064
2358
  * @preserve
2065
- * @augments GOVUKFrontendComponent<HTMLAnchorElement>
2359
+ * @augments Component<HTMLAnchorElement>
2066
2360
  */
2067
- class SkipLink extends GOVUKFrontendComponent {
2361
+ class SkipLink extends Component {
2068
2362
  /**
2069
2363
  * @param {Element | null} $root - HTML element to use for skip link
2070
2364
  * @throws {ElementError} when $root is not set or the wrong type
@@ -2115,7 +2409,7 @@
2115
2409
  *
2116
2410
  * @preserve
2117
2411
  */
2118
- class Tabs extends GOVUKFrontendComponent {
2412
+ class Tabs extends Component {
2119
2413
  /**
2120
2414
  * @param {Element | null} $root - HTML element to use for tabs
2121
2415
  */
@@ -2408,7 +2702,7 @@
2408
2702
  }
2409
2703
  return;
2410
2704
  }
2411
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
2705
+ const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [FileUpload, config.fileUpload], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
2412
2706
  const options = {
2413
2707
  scope: (_config$scope = config.scope) != null ? _config$scope : document,
2414
2708
  onError: config.onError
@@ -2427,11 +2721,11 @@
2427
2721
  *
2428
2722
  * Any component errors will be caught and logged to the console.
2429
2723
  *
2430
- * @template {CompatibleClass} T
2431
- * @param {T} Component - class of the component to create
2432
- * @param {T["defaults"]} [config] - Config supplied to component
2433
- * @param {OnErrorCallback<T> | Element | Document | CreateAllOptions<T> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
2434
- * @returns {Array<InstanceType<T>>} - array of instantiated components
2724
+ * @template {CompatibleClass} ComponentClass
2725
+ * @param {ComponentClass} Component - class of the component to create
2726
+ * @param {ComponentConfig<ComponentClass>} [config] - Config supplied to component
2727
+ * @param {OnErrorCallback<ComponentClass> | Element | Document | CreateAllOptions<ComponentClass> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
2728
+ * @returns {Array<InstanceType<ComponentClass>>} - array of instantiated components
2435
2729
  */
2436
2730
  function createAll(Component, config, createAllOptions) {
2437
2731
  let $scope = document;
@@ -2478,7 +2772,7 @@
2478
2772
  }).filter(Boolean);
2479
2773
  }
2480
2774
  /**
2481
- * @typedef {{new (...args: any[]): any, defaults?: object, moduleName: string}} CompatibleClass
2775
+ * @typedef {{new (...args: any[]): any, moduleName: string}} CompatibleClass
2482
2776
  */
2483
2777
  /**
2484
2778
  * Config for all components via `initAll()`
@@ -2489,22 +2783,21 @@
2489
2783
  * @property {CharacterCountConfig} [characterCount] - Character Count config
2490
2784
  * @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
2491
2785
  * @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
2786
+ * @property {FileUploadConfig} [fileUpload] - File Upload config
2492
2787
  * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
2493
2788
  * @property {PasswordInputConfig} [passwordInput] - Password input config
2494
2789
  */
2495
2790
  /**
2496
2791
  * Config for individual components
2497
2792
  *
2498
- * @typedef {import('./components/accordion/accordion.mjs').AccordionConfig} AccordionConfig
2499
- * @typedef {import('./components/accordion/accordion.mjs').AccordionTranslations} AccordionTranslations
2500
- * @typedef {import('./components/button/button.mjs').ButtonConfig} ButtonConfig
2501
- * @typedef {import('./components/character-count/character-count.mjs').CharacterCountConfig} CharacterCountConfig
2502
- * @typedef {import('./components/character-count/character-count.mjs').CharacterCountTranslations} CharacterCountTranslations
2503
- * @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
2504
- * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
2505
- * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
2506
- * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
2507
- * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
2793
+ * @import { AccordionConfig } from './components/accordion/accordion.mjs'
2794
+ * @import { ButtonConfig } from './components/button/button.mjs'
2795
+ * @import { CharacterCountConfig } from './components/character-count/character-count.mjs'
2796
+ * @import { ErrorSummaryConfig } from './components/error-summary/error-summary.mjs'
2797
+ * @import { ExitThisPageConfig } from './components/exit-this-page/exit-this-page.mjs'
2798
+ * @import { NotificationBannerConfig } from './components/notification-banner/notification-banner.mjs'
2799
+ * @import { PasswordInputConfig } from './components/password-input/password-input.mjs'
2800
+ * @import { FileUploadConfig } from './components/file-upload/file-upload.mjs'
2508
2801
  */
2509
2802
  /**
2510
2803
  * Component config keys, e.g. `accordion` and `characterCount`
@@ -2512,32 +2805,38 @@
2512
2805
  * @typedef {keyof Config} ConfigKey
2513
2806
  */
2514
2807
  /**
2515
- * @template {CompatibleClass} T
2808
+ * @template {CompatibleClass} ComponentClass
2809
+ * @typedef {ConstructorParameters<ComponentClass>[1]} ComponentConfig
2810
+ */
2811
+ /**
2812
+ * @template {CompatibleClass} ComponentClass
2516
2813
  * @typedef {object} ErrorContext
2517
2814
  * @property {Element} [element] - Element used for component module initialisation
2518
- * @property {T} [component] - Class of component
2519
- * @property {T["defaults"]} config - Config supplied to component
2815
+ * @property {ComponentClass} [component] - Class of component
2816
+ * @property {ComponentConfig<ComponentClass>} config - Config supplied to component
2520
2817
  */
2521
2818
  /**
2522
- * @template {CompatibleClass} T
2819
+ * @template {CompatibleClass} ComponentClass
2523
2820
  * @callback OnErrorCallback
2524
2821
  * @param {unknown} error - Thrown error
2525
- * @param {ErrorContext<T>} context - Object containing the element, component class and configuration
2822
+ * @param {ErrorContext<ComponentClass>} context - Object containing the element, component class and configuration
2526
2823
  */
2527
2824
  /**
2528
- * @template {CompatibleClass} T
2825
+ * @template {CompatibleClass} ComponentClass
2529
2826
  * @typedef {object} CreateAllOptions
2530
2827
  * @property {Element | Document} [scope] - scope of the document to search within
2531
- * @property {OnErrorCallback<T>} [onError] - callback function if error throw by component on init
2828
+ * @property {OnErrorCallback<ComponentClass>} [onError] - callback function if error throw by component on init
2532
2829
  */
2533
2830
 
2534
2831
  exports.Accordion = Accordion;
2535
2832
  exports.Button = Button;
2536
2833
  exports.CharacterCount = CharacterCount;
2537
2834
  exports.Checkboxes = Checkboxes;
2538
- exports.Component = GOVUKFrontendComponent;
2835
+ exports.Component = Component;
2836
+ exports.ConfigurableComponent = ConfigurableComponent;
2539
2837
  exports.ErrorSummary = ErrorSummary;
2540
2838
  exports.ExitThisPage = ExitThisPage;
2839
+ exports.FileUpload = FileUpload;
2541
2840
  exports.Header = Header;
2542
2841
  exports.NotificationBanner = NotificationBanner;
2543
2842
  exports.PasswordInput = PasswordInput;