@planning-inspectorate/dynamic-forms 3.16.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +124 -189
  2. package/docs/Email Validation.md +86 -0
  3. package/docs/Getting Started.md +299 -0
  4. package/package.json +1 -1
  5. package/src/components/address/question.d.ts +1 -9
  6. package/src/components/address/question.d.ts.map +1 -1
  7. package/src/components/address/question.js +6 -17
  8. package/src/components/checkbox/question.d.ts +1 -21
  9. package/src/components/checkbox/question.d.ts.map +1 -1
  10. package/src/components/checkbox/question.js +38 -50
  11. package/src/components/checkbox/question.test.js +177 -26
  12. package/src/components/date/question.d.ts +0 -8
  13. package/src/components/date/question.d.ts.map +1 -1
  14. package/src/components/date/question.js +6 -14
  15. package/src/components/date-period/question.d.ts +1 -5
  16. package/src/components/date-period/question.d.ts.map +1 -1
  17. package/src/components/date-period/question.js +1 -1
  18. package/src/components/date-time/question.d.ts +0 -8
  19. package/src/components/date-time/question.d.ts.map +1 -1
  20. package/src/components/date-time/question.js +4 -12
  21. package/src/components/email/question.d.ts +0 -9
  22. package/src/components/email/question.d.ts.map +1 -1
  23. package/src/components/email/question.js +2 -13
  24. package/src/components/email/question.test.js +19 -0
  25. package/src/components/manage-list/answer-summary-list.njk +1 -1
  26. package/src/components/manage-list/question.d.ts +1 -5
  27. package/src/components/manage-list/question.d.ts.map +1 -1
  28. package/src/components/manage-list/question.js +1 -1
  29. package/src/components/multi-field-input/question.d.ts +4 -19
  30. package/src/components/multi-field-input/question.d.ts.map +1 -1
  31. package/src/components/multi-field-input/question.js +89 -23
  32. package/src/components/multi-field-input/question.test.js +441 -3
  33. package/src/components/number-entry/question.d.ts +0 -28
  34. package/src/components/number-entry/question.d.ts.map +1 -1
  35. package/src/components/number-entry/question.js +3 -20
  36. package/src/components/number-entry/question.test.js +17 -0
  37. package/src/components/radio/question.d.ts +1 -9
  38. package/src/components/radio/question.d.ts.map +1 -1
  39. package/src/components/radio/question.js +29 -46
  40. package/src/components/radio/question.test.js +139 -60
  41. package/src/components/select/question.d.ts +1 -15
  42. package/src/components/select/question.d.ts.map +1 -1
  43. package/src/components/select/question.js +4 -47
  44. package/src/components/single-line-input/question.d.ts +0 -4
  45. package/src/components/single-line-input/question.d.ts.map +1 -1
  46. package/src/components/single-line-input/question.js +1 -1
  47. package/src/components/text-entry/question.d.ts +0 -4
  48. package/src/components/text-entry/question.d.ts.map +1 -1
  49. package/src/components/text-entry/question.js +1 -1
  50. package/src/components/text-entry-redact/question.d.ts +2 -6
  51. package/src/components/text-entry-redact/question.d.ts.map +1 -1
  52. package/src/components/text-entry-redact/question.js +1 -1
  53. package/src/components/unit-option-entry/question.d.ts.map +1 -1
  54. package/src/components/unit-option-entry/question.js +3 -1
  55. package/src/components/unit-option-entry/question.test.js +15 -0
  56. package/src/components/utils/question-has-answer.js +1 -1
  57. package/src/components/utils/question-utils.d.ts +19 -1
  58. package/src/components/utils/question-utils.d.ts.map +1 -1
  59. package/src/components/utils/question-utils.js +31 -2
  60. package/src/components/utils/question-utils.test.js +89 -2
  61. package/src/questions/options-question.d.ts +4 -4
  62. package/src/questions/options-question.d.ts.map +1 -1
  63. package/src/questions/options-question.js +27 -2
  64. package/src/questions/options-question.test.js +106 -0
  65. package/src/questions/question.d.ts +16 -1
  66. package/src/questions/question.d.ts.map +1 -1
  67. package/src/questions/question.js +64 -9
  68. package/src/questions/question.test.js +86 -0
  69. package/test/journey.js +2 -0
  70. package/test/questions.js +50 -0
  71. package/test/snapshots/check-your-answers-not-started.html +22 -0
  72. package/test/snapshots/check-your-answers-with-answers.html +23 -1
  73. package/test/snapshots/favourite-activity-reason.html +54 -0
  74. package/test/snapshots/travel-requirements.html +48 -0
  75. package/test/utils/question-test-utils.js +38 -7
  76. package/typedefs/question-props.d.ts +33 -0
  77. package/typedefs/question-types.d.ts +4 -0
package/test/questions.js CHANGED
@@ -195,6 +195,54 @@ export const questionProps = {
195
195
  question: 'Travel Companion Email',
196
196
  fieldName: 'travelCompanionEmail',
197
197
  url: 'travel-companion-email'
198
+ },
199
+ // Test questions for conditional reveal functionality
200
+ favouriteActivityReason: {
201
+ type: COMPONENT_TYPES.RADIO,
202
+ title: 'Favourite Activity Reason',
203
+ question: 'Why do you want to do your favourite activity?',
204
+ fieldName: 'favouriteActivityReason',
205
+ url: 'favourite-activity-reason',
206
+ label: 'Select a reason',
207
+ options: [
208
+ {
209
+ value: 'relaxation',
210
+ text: 'Relaxation',
211
+ conditional: {
212
+ question: 'How do you like to relax?',
213
+ type: 'text',
214
+ fieldName: 'relaxationDetails',
215
+ label: 'Details:'
216
+ }
217
+ },
218
+ { value: 'adventure', text: 'Adventure' },
219
+ { value: 'culture', text: 'Culture' }
220
+ ]
221
+ },
222
+ travelRequirements: {
223
+ type: COMPONENT_TYPES.CHECKBOX,
224
+ title: 'Travel Requirements',
225
+ question: 'What special requirements do you have?',
226
+ fieldName: 'travelRequirements',
227
+ url: 'travel-requirements',
228
+ label: 'Select all that apply',
229
+ options: [
230
+ {
231
+ value: 'dietary',
232
+ text: 'Dietary requirements',
233
+ conditional: {
234
+ question: 'Please describe your dietary requirements',
235
+ type: 'text',
236
+ fieldName: 'dietaryDetails',
237
+ label: 'Dietary details:'
238
+ }
239
+ },
240
+ {
241
+ value: 'mobility',
242
+ text: 'Mobility assistance'
243
+ },
244
+ { value: 'medical', text: 'Medical needs' }
245
+ ]
198
246
  }
199
247
  };
200
248
 
@@ -216,6 +264,8 @@ export const questionsInOrder = [
216
264
  questionProps.hotelAddress,
217
265
  questionProps.luggageWeight,
218
266
  questionProps.contactEmail,
267
+ questionProps.favouriteActivityReason,
268
+ questionProps.travelRequirements,
219
269
  questionProps.travelCompanions
220
270
  ];
221
271
 
@@ -196,6 +196,28 @@
196
196
  <a class="govuk-link" href="questions/contact-email">Answer<span class="govuk-visually-hidden"> What is your email address for booking confirmations?</span></a>
197
197
  </dd>
198
198
  </div>
199
+ <div class="govuk-summary-list__row">
200
+ <dt class="govuk-summary-list__key">
201
+ Favourite Activity Reason
202
+ </dt>
203
+ <dd class="govuk-summary-list__value">
204
+ Not started
205
+ </dd>
206
+ <dd class="govuk-summary-list__actions">
207
+ <a class="govuk-link" href="questions/favourite-activity-reason">Answer<span class="govuk-visually-hidden"> Why do you want to do your favourite activity?</span></a>
208
+ </dd>
209
+ </div>
210
+ <div class="govuk-summary-list__row">
211
+ <dt class="govuk-summary-list__key">
212
+ Travel Requirements
213
+ </dt>
214
+ <dd class="govuk-summary-list__value">
215
+ Not started
216
+ </dd>
217
+ <dd class="govuk-summary-list__actions">
218
+ <a class="govuk-link" href="questions/travel-requirements">Answer<span class="govuk-visually-hidden"> What special requirements do you have?</span></a>
219
+ </dd>
220
+ </div>
199
221
  <div class="govuk-summary-list__row">
200
222
  <dt class="govuk-summary-list__key">
201
223
  Travel Companions
@@ -146,7 +146,7 @@
146
146
  Travel Companions
147
147
  </dt>
148
148
  <dd class="govuk-summary-list__value">
149
- test<br>1<br>
149
+ test<br>1
150
150
  </dd>
151
151
  <dd class="govuk-summary-list__actions">
152
152
  <a class="govuk-link" href="questions/companions">Change<span class="govuk-visually-hidden"> Enter the names and ages of your travel companions.</span></a>
@@ -196,6 +196,28 @@
196
196
  <a class="govuk-link" href="questions/contact-email">Change<span class="govuk-visually-hidden"> What is your email address for booking confirmations?</span></a>
197
197
  </dd>
198
198
  </div>
199
+ <div class="govuk-summary-list__row">
200
+ <dt class="govuk-summary-list__key">
201
+ Favourite Activity Reason
202
+ </dt>
203
+ <dd class="govuk-summary-list__value">
204
+ Relaxation<br>Details: test conditional answer
205
+ </dd>
206
+ <dd class="govuk-summary-list__actions">
207
+ <a class="govuk-link" href="questions/favourite-activity-reason">Change<span class="govuk-visually-hidden"> Why do you want to do your favourite activity?</span></a>
208
+ </dd>
209
+ </div>
210
+ <div class="govuk-summary-list__row">
211
+ <dt class="govuk-summary-list__key">
212
+ Travel Requirements
213
+ </dt>
214
+ <dd class="govuk-summary-list__value">
215
+ Dietary requirements<br>Dietary details: test conditional answer
216
+ </dd>
217
+ <dd class="govuk-summary-list__actions">
218
+ <a class="govuk-link" href="questions/travel-requirements">Change<span class="govuk-visually-hidden"> What special requirements do you have?</span></a>
219
+ </dd>
220
+ </div>
199
221
  <div class="govuk-summary-list__row">
200
222
  <dt class="govuk-summary-list__key">
201
223
  Travel Companions
@@ -0,0 +1,54 @@
1
+ <div class="govuk-grid-row">
2
+ <div class="govuk-grid-column-two-thirds">
3
+ <form action="" method="post" novalidate>
4
+
5
+
6
+ <fieldset class="govuk-fieldset">
7
+ <legend class="govuk-fieldset__legend govuk-fieldset__legend--l">
8
+ <h1 class="govuk-fieldset__heading">
9
+ Why do you want to do your favourite activity?
10
+ </h1>
11
+ </legend>
12
+
13
+ <div class="govuk-form-group">
14
+ <div class="govuk-radios govuk-radios" data-module="govuk-radios">
15
+ <div class="govuk-radios__item">
16
+ <input class="govuk-radios__input" id="favouriteActivityReason" name="favouriteActivityReason" type="radio" value="relaxation" data-aria-controls="conditional-favouriteActivityReason" data-cy="answer-relaxation">
17
+ <label class="govuk-label govuk-radios__label" for="favouriteActivityReason">
18
+ Relaxation
19
+ </label>
20
+ </div>
21
+ <div class="govuk-radios__conditional govuk-radios__conditional--hidden" id="conditional-favouriteActivityReason">
22
+ <div class="govuk-form-group">
23
+ <label class="govuk-label" for="favouriteActivityReason_relaxationDetails">
24
+ How do you like to relax?
25
+ </label>
26
+ <input class="govuk-input" id="favouriteActivityReason_relaxationDetails" name="favouriteActivityReason_relaxationDetails" type="text" value="">
27
+ </div>
28
+ </div>
29
+ <div class="govuk-radios__item">
30
+ <input class="govuk-radios__input" id="favouriteActivityReason-2" name="favouriteActivityReason" type="radio" value="adventure" data-cy="answer-adventure">
31
+ <label class="govuk-label govuk-radios__label" for="favouriteActivityReason-2">
32
+ Adventure
33
+ </label>
34
+ </div>
35
+ <div class="govuk-radios__item">
36
+ <input class="govuk-radios__input" id="favouriteActivityReason-3" name="favouriteActivityReason" type="radio" value="culture" data-cy="answer-culture">
37
+ <label class="govuk-label govuk-radios__label" for="favouriteActivityReason-3">
38
+ Culture
39
+ </label>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+
45
+
46
+ </fieldset>
47
+
48
+ <button type="submit" class="govuk-button" data-module="govuk-button" data-cy="button-save-and-continue">
49
+ Continue
50
+ </button>
51
+
52
+ </form>
53
+ </div>
54
+ </div>
@@ -0,0 +1,48 @@
1
+ <div class="govuk-grid-row">
2
+ <div class="govuk-grid-column-two-thirds">
3
+ <form action="" method="post" novalidate>
4
+ <div class="govuk-form-group">
5
+ <fieldset class="govuk-fieldset">
6
+ <legend class="govuk-fieldset__legend govuk-fieldset__legend--l">
7
+ <h1 class="govuk-fieldset__heading">
8
+ What special requirements do you have?
9
+ </h1>
10
+ </legend>
11
+ <div class="govuk-checkboxes govuk-checkboxes" data-module="govuk-checkboxes">
12
+ <div class="govuk-checkboxes__item">
13
+ <input class="govuk-checkboxes__input" id="travelRequirements" name="travelRequirements" type="checkbox" value="dietary" data-aria-controls="conditional-travelRequirements" data-cy="answer-dietary">
14
+ <label class="govuk-label govuk-checkboxes__label" for="travelRequirements">
15
+ Dietary requirements
16
+ </label>
17
+ </div>
18
+ <div class="govuk-checkboxes__conditional govuk-checkboxes__conditional--hidden" id="conditional-travelRequirements">
19
+ <div class="govuk-form-group">
20
+ <label class="govuk-label" for="travelRequirements_dietaryDetails">
21
+ Please describe your dietary requirements
22
+ </label>
23
+ <input class="govuk-input" id="travelRequirements_dietaryDetails" name="travelRequirements_dietaryDetails" type="text" value="">
24
+ </div>
25
+ </div>
26
+ <div class="govuk-checkboxes__item">
27
+ <input class="govuk-checkboxes__input" id="travelRequirements-2" name="travelRequirements" type="checkbox" value="mobility" data-cy="answer-mobility">
28
+ <label class="govuk-label govuk-checkboxes__label" for="travelRequirements-2">
29
+ Mobility assistance
30
+ </label>
31
+ </div>
32
+ <div class="govuk-checkboxes__item">
33
+ <input class="govuk-checkboxes__input" id="travelRequirements-3" name="travelRequirements" type="checkbox" value="medical" data-cy="answer-medical">
34
+ <label class="govuk-label govuk-checkboxes__label" for="travelRequirements-3">
35
+ Medical needs
36
+ </label>
37
+ </div>
38
+ </div>
39
+ </fieldset>
40
+ </div>
41
+
42
+ <button type="submit" class="govuk-button" data-module="govuk-button" data-cy="button-save-and-continue">
43
+ Continue
44
+ </button>
45
+
46
+ </form>
47
+ </div>
48
+ </div>
@@ -101,9 +101,24 @@ export function mockAnswerBody(q) {
101
101
  switch (q.type) {
102
102
  case COMPONENT_TYPES.BOOLEAN:
103
103
  return { [q.fieldName]: BOOLEAN_OPTIONS.YES };
104
- case COMPONENT_TYPES.CHECKBOX:
105
- return { [q.fieldName]: [q.options[0].value] };
106
- case COMPONENT_TYPES.RADIO:
104
+ case COMPONENT_TYPES.CHECKBOX: {
105
+ const result = { [q.fieldName]: [q.options[0].value] };
106
+ // Include conditional field if present on the first option
107
+ if (q.options[0].conditional?.fieldName) {
108
+ const conditionalFieldName = `${q.fieldName}_${q.options[0].conditional.fieldName}`;
109
+ result[conditionalFieldName] = 'test conditional answer';
110
+ }
111
+ return result;
112
+ }
113
+ case COMPONENT_TYPES.RADIO: {
114
+ const result = { [q.fieldName]: q.options[0].value };
115
+ // Include conditional field if present on the first option
116
+ if (q.options[0].conditional?.fieldName) {
117
+ const conditionalFieldName = `${q.fieldName}_${q.options[0].conditional.fieldName}`;
118
+ result[conditionalFieldName] = 'test conditional answer';
119
+ }
120
+ return result;
121
+ }
107
122
  case COMPONENT_TYPES.SELECT:
108
123
  return { [q.fieldName]: q.options[0].value };
109
124
  case COMPONENT_TYPES.NUMBER:
@@ -181,9 +196,24 @@ export function mockAnswer(q) {
181
196
  switch (q.type) {
182
197
  case COMPONENT_TYPES.BOOLEAN:
183
198
  return BOOLEAN_OPTIONS.YES;
184
- case COMPONENT_TYPES.CHECKBOX:
185
- return q.options[0].text;
186
- case COMPONENT_TYPES.RADIO:
199
+ case COMPONENT_TYPES.CHECKBOX: {
200
+ const optionText = q.options[0].text;
201
+ // Include conditional value if present on the first option
202
+ if (q.options[0].conditional?.fieldName) {
203
+ const label = q.options[0].conditional.label ? `${q.options[0].conditional.label} ` : '';
204
+ return `${optionText}<br>${label}test conditional answer`;
205
+ }
206
+ return optionText;
207
+ }
208
+ case COMPONENT_TYPES.RADIO: {
209
+ const optionText = q.options[0].text;
210
+ // Include conditional value if present on the first option
211
+ if (q.options[0].conditional?.fieldName) {
212
+ const label = q.options[0].conditional.label ? `${q.options[0].conditional.label} ` : '';
213
+ return `${optionText}<br>${label}test conditional answer`;
214
+ }
215
+ return optionText;
216
+ }
187
217
  case COMPONENT_TYPES.SELECT:
188
218
  return q.options[0].text;
189
219
  case COMPONENT_TYPES.NUMBER:
@@ -205,7 +235,8 @@ export function mockAnswer(q) {
205
235
  for (const field of q.inputFields) {
206
236
  res += mockInputFieldValue(field) + '<br>';
207
237
  }
208
- return res;
238
+ // Remove trailing <br>
239
+ return res.slice(0, -4);
209
240
  }
210
241
  break;
211
242
  case COMPONENT_TYPES.ADDRESS: {
@@ -1,4 +1,27 @@
1
1
  import type { QuestionParameters } from './question-types.d.ts';
2
+ import type { Question } from '../src/questions/question.js';
3
+ import type { Journey } from '../src/journey/journey.js';
4
+
5
+ /**
6
+ * Context passed to the custom summary formatter
7
+ */
8
+ export interface SummaryFormatterContext<TAnswer = unknown> {
9
+ /** The raw answer value */
10
+ answer: TAnswer;
11
+ /** The default display value */
12
+ formattedAnswer: string;
13
+ /** The question instance */
14
+ question: Question;
15
+ /** The journey instance */
16
+ journey: Journey;
17
+ /** The section segment */
18
+ sectionSegment: string;
19
+ }
20
+
21
+ /**
22
+ * Custom function to format the summary display value
23
+ */
24
+ export type SummaryValueFormatter<TAnswer = unknown> = (context: SummaryFormatterContext<TAnswer>) => string;
2
25
 
3
26
  export type QuestionTypes =
4
27
  | 'boolean'
@@ -74,6 +97,7 @@ interface InputField {
74
97
  formatJoinString?: string; // used by formatAnswerForSummary (e.g. task list display), effective default to line break
75
98
  formatPrefix?: string; // used by formatAnswerForSummary (e.g. task list display), to prefix answer
76
99
  formatTextFunction?: (text: string) => string; // used to format the answer for display and value in question
100
+ formatSummaryValue?: (context: InputFieldSummaryContext) => string; // used to format the answer for summary display only (not escaped)
77
101
  attributes?: Record<string, string>; // used to add HTML attributes to the field
78
102
  suffix?: Affix; // used to add a suffix to the field
79
103
  prefix?: Affix; // used to add a prefix to the field
@@ -83,6 +107,15 @@ interface InputField {
83
107
  pattern?: string; // Used for backwards-compatibility for older iOS devices in numeric input fields
84
108
  }
85
109
 
110
+ /**
111
+ * Context passed to InputField.formatSummaryValue
112
+ * Extends the base SummaryFormatterContext with field-specific properties
113
+ */
114
+ interface InputFieldSummaryContext extends SummaryFormatterContext {
115
+ /** The input field configuration */
116
+ field: InputField;
117
+ }
118
+
86
119
  /*
87
120
  * UnitOptions are the options displayed in the radio format - in this case the value
88
121
  * represents the unit.
@@ -21,6 +21,10 @@ export interface QuestionParameters {
21
21
  editable?: boolean;
22
22
  // override the action link for this question
23
23
  actionLink?: ActionLink;
24
+ // custom function to format the summary display value
25
+ formatSummaryValue?: SummaryValueFormatter;
26
+ // whether to capitalise the first letter of the answer in summary (defaults to true)
27
+ capitaliseAnswer?: boolean;
24
28
  // static view data for this question
25
29
  viewData?: {
26
30
  /**