From 12c56e62f2616e7656bbb621fe44762d7599dfdb Mon Sep 17 00:00:00 2001 From: efeosasereOkoro Date: Tue, 2 Jun 2026 11:13:20 +0100 Subject: [PATCH] NSCP review fixes: scroll preservation, error auto-clear, flow restructure Addresses 22 review items from #23 across both reviewers. Framework (govbb-framework.js): - render() preserves scroll + focus when called for conditional re-renders; only navigates-to-top on real next/back/nav - Radio inputs now use 'change' event and restore checked state on re-render - New _clearFieldError() clears inline errors, aria-invalid, and matching summary list items as user fixes each field - _hidePreviousOnFirstStep() wired up in render() NSCP form (nscp-camper-registration.html): - Flow restructured: camp page split into Camp / Activities / Photos - Emergency-contact step always visible (step counter stable at 11) - Name fields split into First / Other (optional) / Surname for camper, guardian, and emergency contact - Meals: conditional dietary reveal when Yes; data-trigger-render added - DOB validation flags all 3 fields (Day/Month/Year), not just Day - First-aid consent now an explicit required checkbox on activities page - Medication Authorisation Form download link added - Confirmation page: email mention, print/save button, register another child - Check page: 'Sex' label, new activities + photos sections - Topics-of-interest hint clarifies at-least-one requirement - All 14 checkboxes now top-aligned with consistent spacing Co-Authored-By: Claude Opus 4.7 --- Prototypes/nscp-camper-registration.html | 275 ++++++++++++++++------- assets/govbb-framework.js | 86 ++++++- 2 files changed, 278 insertions(+), 83 deletions(-) diff --git a/Prototypes/nscp-camper-registration.html b/Prototypes/nscp-camper-registration.html index 18acb5f..3b6705f 100644 --- a/Prototypes/nscp-camper-registration.html +++ b/Prototypes/nscp-camper-registration.html @@ -61,6 +61,9 @@ if (s) s.remove(); document.querySelectorAll('.govbb-error-message').forEach(function(e){e.remove();}); document.querySelectorAll('[aria-invalid]').forEach(function(el){el.removeAttribute('aria-invalid');el.removeAttribute('aria-describedby');}); + if (document.title.indexOf('Error: ') === 0) { + document.title = document.title.substring('Error: '.length); + } }; GovBB.showErrors = function(errors) { GovBB.clearErrors(); @@ -89,6 +92,15 @@ msg.className='govbb-error-message'; msg.id=errId; msg.textContent=e.msg; var group = el.closest('.govbb-form-group')||el.closest('fieldset')||el.parentElement; if (group) group.appendChild(msg); + /* DOB highlight: if the error targets any of the three DOB parts, + mark all three as aria-invalid so the whole group shows the + error styling — not just "Day". */ + if (e.id === 'dob-day' || e.id === 'dob-month' || e.id === 'dob-year') { + ['dob-day','dob-month','dob-year'].forEach(function(id){ + var p = document.getElementById(id); + if (p) p.setAttribute('aria-invalid','true'); + }); + } }); if (!document.title.startsWith('Error:')) document.title = 'Error: ' + document.title; }; @@ -106,6 +118,8 @@ 'camper-details', 'camper-health', 'camp', + 'activities', + 'photos', 'nutrition', 'guardian-name', 'guardian-contact', @@ -231,15 +245,18 @@ } function camperFullName() { - return GovBB.D['camper-fullname'] || ''; + return [GovBB.D['camper-first-name'], GovBB.D['camper-other-names'], GovBB.D['camper-surname']] + .filter(Boolean).map(function(s){return String(s).trim();}).filter(Boolean).join(' '); } function guardianFullName() { - return GovBB.D['guardian-fullname'] || ''; + return [GovBB.D['guardian-first-name'], GovBB.D['guardian-other-names'], GovBB.D['guardian-surname']] + .filter(Boolean).map(function(s){return String(s).trim();}).filter(Boolean).join(' '); } function emergencyFullName() { - return GovBB.D['emerg-fullname'] || ''; + return [GovBB.D['emerg-first-name'], GovBB.D['emerg-other-names'], GovBB.D['emerg-surname']] + .filter(Boolean).map(function(s){return String(s).trim();}).filter(Boolean).join(' '); } function curriculumSummary() { @@ -308,9 +325,23 @@

Contact us

Tell us about your child
- + +
+ +
+
+ +
+ +
+ +
+
+ +
+
- +
@@ -433,6 +464,7 @@

Your child's m

Camp staff cannot give any medication without written permission from a parent or guardian. Download the form, sign it, and upload a scan or clear photo. Accepted formats: PDF, JPG, PNG. Maximum size: 10 MB.

+

Download the Medication Authorisation Form (PDF)

${(() => { const f = GovBB.D['upload-medication-auth']; if (f) { @@ -515,7 +547,7 @@

Your child's m

`, - /* ── Camp + curriculum + activities + photo permission ── */ + /* ── Camp: parish + venue + topics of interest ── */ 'camp': () => { const parish = GovBB.D['camp-parish'] || ''; const venueOptions = parish && CAMP_VENUES[parish] ? CAMP_VENUES[parish] : []; @@ -530,6 +562,8 @@

Your child's m ` : ''; + /* Inline override for checkbox layout (Fix 12): top-aligned with more gap. */ + const cbStyle = 'align-items:flex-start;gap:var(--spacing-s);margin-bottom:var(--spacing-s);'; return `

Ministry of Youth, Sports and Community Empowerment

@@ -549,40 +583,51 @@

Choose a camp<
Topics that interest your child -

Choose all that apply.

+

Choose at least one topic. This helps us assign your child to the right camp activities.

-
+
-
+
-
+
-
+
-
+
-
+
-
+
-

Activities

-

By registering, you give permission for trained camp staff to give first aid and seek emergency care for your child if we cannot reach you.

+
+ + +
+
`; + }, + + /* ── Activities and first aid ── */ + 'activities': () => ` +
+

Ministry of Youth, Sports and Community Empowerment

+

Activities and first aid

+
Are there any activities your child should not take part in?
@@ -604,8 +649,27 @@

Photos and videos

-

Photos and videos may be used in National Summer Camp Programme materials and communications.

+

First aid and emergency care

+
+ First aid consent +
+ + +
+
+ +
+ + +
+ `, + + /* ── Photos and videos ── */ + 'photos': () => ` +
+

Ministry of Youth, Sports and Community Empowerment

+

Photos and videos

+

Photos and videos may be used in National Summer Camp Programme materials and communications.

Do you give permission for photos or videos of your child during camp activities?
@@ -624,8 +688,7 @@

Previous

- `; - }, + `, /* ── Nutrition ── */ 'nutrition': () => ` @@ -638,31 +701,33 @@

Meals at camp< Will your child eat the meals provided at camp?
- +
- +

-
- -
- - -
-
-
+
- -
- + +
+ + +
+
+
+
+ +
+ +
@@ -680,9 +745,23 @@

Meals at camp<

About you, the parent or guardian

- + +
+ +
+
+ +
+
- + +
+
+ +
+ +
+
@@ -816,15 +895,45 @@

Ministry of Youth, Sports and Community Empowerment

+

Emergency contact details

+

You said you are the emergency contact. We will use your contact details above.

+

If this is wrong, change who the emergency contact is.

+
+ + +
+ `; + } + return `

Ministry of Youth, Sports and Community Empowerment

Emergency contact details

- +
- + +
+
+ +
+ +
+ +
+
+ +
+ +
+
@@ -859,7 +968,8 @@

Emergency cont

- `, + `; + }, /* ── Conduct ── */ 'conduct': () => ` @@ -871,27 +981,27 @@

Camper conduct
Conduct agreement -
+
-
+
-
+
-
+
-
+
-
+
@@ -917,7 +1027,7 @@

About the camp ${GovBB.summaryRow('Full name', camperFullName(), 'camper-details')} ${GovBB.summaryRow('Date of birth', fmtDate('dob'), 'camper-details')} ${GovBB.summaryRow('National ID Number', GovBB.D['nid'] || '', 'camper-details')} - ${GovBB.summaryRow('Gender', GovBB.D['gender'] || '', 'camper-details')} + ${GovBB.summaryRow('Sex', GovBB.D['gender'] || '', 'camper-details')} ${GovBB.summaryRow('Religion', GovBB.D['religion'] === 'Other' ? (GovBB.D['religion-other'] || 'Other') : (GovBB.D['religion'] || 'Prefer not to say'), 'camper-details')} @@ -944,14 +1054,23 @@

Camp

${GovBB.summaryRow('Parish of camp', GovBB.D['camp-parish'] || '', 'camp')} ${GovBB.summaryRow('Camp name', GovBB.D['camp-name'] || '', 'camp')} ${GovBB.summaryRow('Topics of interest', curriculumSummary(), 'camp')} - ${GovBB.summaryRow('Activity exemptions', GovBB.D['has-activity-exemptions'] === 'Yes' ? (GovBB.D['activity-exemptions'] || '') : 'None — can take part in all activities', 'camp')} - ${GovBB.summaryRow('Photo/video permission', GovBB.D['photo-permission'] || '', 'camp')} + + +

Activities and first aid

+
+ ${GovBB.summaryRow('Activity exemptions', GovBB.D['has-activity-exemptions'] === 'Yes' ? (GovBB.D['activity-exemptions'] || '') : 'None — can take part in all activities', 'activities')} + ${GovBB.summaryRow('First aid consent', GovBB.D['first-aid-consent'] ? 'Yes — staff may give first aid and seek emergency care' : 'Not given', 'activities')} +
+ +

Photos and videos

+
+ ${GovBB.summaryRow('Photo/video permission', GovBB.D['photo-permission'] || '', 'photos')}

Nutrition

${GovBB.summaryRow('Will eat camp meals?', GovBB.D['meals-provided'] || '', 'nutrition')} - ${GovBB.summaryRow('Dietary restrictions', GovBB.D['dietary'] === 'Other' ? (GovBB.D['dietary-other'] || 'Other') : (GovBB.D['dietary'] || ''), 'nutrition')} + ${GovBB.D['meals-provided'] === 'Yes' ? GovBB.summaryRow('Dietary restrictions', GovBB.D['dietary'] === 'Other' ? (GovBB.D['dietary-other'] || 'Other') : (GovBB.D['dietary'] || ''), 'nutrition') : ''}

Parent or guardian

@@ -1001,6 +1120,7 @@

Registration subm

What happens next

    +
  • We have sent a copy of your registration to ${escapeHtml(GovBB.D['contact-email'] || 'the email you provided')}.
  • We will review your child's registration and contact you within 5 working days to confirm their place at camp.
  • ${GovBB.D['upload-medication-auth'] ? 'We have received your Medication Authorisation Form. Camp staff will follow it for your child\'s medication.' @@ -1008,6 +1128,9 @@

    What happens n
  • If you do not hear from us within 5 working days, call +1 (246) 535-3835 or email mysce.youthprojects@barbados.gov.bb.

You will be contacted with the camp dates once your registration is confirmed.

+ +

+

Register another child

`; } @@ -1054,7 +1177,8 @@

What happens n } if (pageId === 'camper-details') { - if (!D['camper-fullname'] || !D['camper-fullname'].trim()) errors.push({ id: 'camper-fullname', msg: 'Enter your child\'s full name' }); + if (!D['camper-first-name'] || !D['camper-first-name'].trim()) errors.push({ id: 'camper-first-name', msg: 'Enter your child\'s first name' }); + if (!D['camper-surname'] || !D['camper-surname'].trim()) errors.push({ id: 'camper-surname', msg: 'Enter your child\'s surname' }); if (!D['dob-day'] || !D['dob-month'] || !D['dob-year']) { errors.push({ id: 'dob-day', msg: 'Date of birth – Enter a complete date of birth' }); } else { @@ -1107,22 +1231,33 @@

What happens n const anyTopic = D['curriculum-arts'] || D['curriculum-innovation'] || D['curriculum-youth'] || D['curriculum-health'] || D['curriculum-self'] || D['curriculum-finance'] || D['curriculum-agriculture']; if (!anyTopic) errors.push({ id: 'curriculum-arts', msg: 'Choose at least one topic that interests your child' }); + } + + if (pageId === 'activities') { if (!D['has-activity-exemptions']) errors.push({ id: 'has-activity-exemptions-no', msg: 'Tell us whether your child should be exempt from any activities' }); if (D['has-activity-exemptions'] === 'Yes' && (!D['activity-exemptions'] || !D['activity-exemptions'].trim())) errors.push({ id: 'activity-exemptions', msg: 'Describe the activities your child should not take part in' }); + if (!D['first-aid-consent']) + errors.push({ id: 'first-aid-consent', msg: 'Tick the box to give permission for first aid and emergency care' }); + } + + if (pageId === 'photos') { if (!D['photo-permission']) errors.push({ id: 'photo-permission-yes', msg: 'Select yes or no for photo and video permission' }); } if (pageId === 'nutrition') { if (!D['meals-provided']) errors.push({ id: 'meals-provided-yes', msg: 'Select yes or no for camp meals' }); - if (!D['dietary']) errors.push({ id: 'dietary', msg: 'Select a dietary option' }); - if (D['dietary'] === 'Other' && (!D['dietary-other'] || !D['dietary-other'].trim())) - errors.push({ id: 'dietary-other', msg: 'Dietary restrictions – Describe the dietary restriction' }); + if (D['meals-provided'] === 'Yes') { + if (!D['dietary']) errors.push({ id: 'dietary', msg: 'Select a dietary option' }); + if (D['dietary'] === 'Other' && (!D['dietary-other'] || !D['dietary-other'].trim())) + errors.push({ id: 'dietary-other', msg: 'Dietary restrictions – Describe the dietary restriction' }); + } } if (pageId === 'guardian-name') { - if (!D['guardian-fullname'] || !D['guardian-fullname'].trim()) errors.push({ id: 'guardian-fullname', msg: 'Enter your full name' }); + if (!D['guardian-first-name'] || !D['guardian-first-name'].trim()) errors.push({ id: 'guardian-first-name', msg: 'Enter your first name' }); + if (!D['guardian-surname'] || !D['guardian-surname'].trim()) errors.push({ id: 'guardian-surname', msg: 'Enter your surname' }); if (!D['guardian-relationship']) errors.push({ id: 'guardian-relationship', msg: 'Select your relationship to the camper' }); if (D['guardian-relationship'] === 'Other' && (!D['guardian-relationship-other'] || !D['guardian-relationship-other'].trim())) errors.push({ id: 'guardian-relationship-other', msg: 'Describe your relationship to the camper' }); @@ -1140,7 +1275,11 @@

What happens n } if (pageId === 'emergency-contact') { - if (!D['emerg-fullname'] || !D['emerg-fullname'].trim()) errors.push({ id: 'emerg-fullname', msg: 'Enter the emergency contact\'s full name' }); + /* If the guardian is the emergency contact, the emergency-contact page + is a confirmation-only view — nothing to validate. */ + if (D['guardian-is-emergency'] === 'Yes') return errors; + if (!D['emerg-first-name'] || !D['emerg-first-name'].trim()) errors.push({ id: 'emerg-first-name', msg: 'Enter the emergency contact\'s first name' }); + if (!D['emerg-surname'] || !D['emerg-surname'].trim()) errors.push({ id: 'emerg-surname', msg: 'Enter the emergency contact\'s surname' }); if (!D['emerg-relationship']) errors.push({ id: 'emerg-relationship', msg: 'Select the emergency contact\'s relationship to the camper' }); if (D['emerg-relationship'] === 'Other' && (!D['emerg-relationship-other'] || !D['emerg-relationship-other'].trim())) errors.push({ id: 'emerg-relationship-other', msg: 'Describe their relationship to the camper' }); @@ -1167,25 +1306,9 @@

What happens n /* ───────── Init ───────── */ GovBB.init({ formName: FORM_NAME, - getFlow: function() { - if (GovBB.D['guardian-is-emergency'] === 'Yes') { - return FLOW.filter(p => p !== 'emergency-contact'); - } - return FLOW; - }, + flow: FLOW, pages: PAGES, - validate: validate, - onRadioChange: function(name) { - const triggers = [ - 'guardian-is-emergency', - 'has-activity-exemptions', - 'illness', - 'medication', - 'disability', - 'allergies' - ]; - if (triggers.indexOf(name) !== -1) GovBB.render(); - } + validate: validate }); diff --git a/assets/govbb-framework.js b/assets/govbb-framework.js index 41c8526..307b297 100644 --- a/assets/govbb-framework.js +++ b/assets/govbb-framework.js @@ -44,7 +44,7 @@ }; /* ── Navigation ── */ - GovBB.render = function () { + GovBB.render = function (preserveScroll) { var flow = _getFlow ? _getFlow() : _flow; var pageId = flow[_current]; var el = document.getElementById(_appEl); @@ -53,14 +53,12 @@ if (!fn) { el.innerHTML = '

Page not found: ' + pageId + '

'; return; } el.innerHTML = fn(); _injectProgressIndicator(pageId, flow); - /* Previous button is shown on every form step (including the first) so - users can navigate back to the start page if they want to re-read the - intro or eligibility. */ _bindInputs(); _bindRadios(); _bindCheckboxes(); _initSignaturePads(); - window.scrollTo(0, 0); + _hidePreviousOnFirstStep(pageId, flow); + if (!preserveScroll) window.scrollTo(0, 0); }; /* Pages that are not counted as form steps for the progress indicator. */ @@ -187,17 +185,91 @@ // Restore saved value if (GovBB.D[field] !== undefined) { if (el.type === 'checkbox') el.checked = !!GovBB.D[field]; + else if (el.type === 'radio') el.checked = (GovBB.D[field] === el.value); else el.value = GovBB.D[field]; } - var ev = (el.tagName === 'SELECT' || el.type === 'checkbox') ? 'change' : 'input'; + var ev = (el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio') ? 'change' : 'input'; el.addEventListener(ev, function () { if (el.type === 'checkbox') GovBB.D[field] = el.checked; + else if (el.type === 'radio') { if (el.checked) GovBB.D[field] = el.value; } else GovBB.D[field] = el.value; - if (el.getAttribute('data-trigger-render')) GovBB.render(); + _clearFieldError(el); + if (el.getAttribute('data-trigger-render')) { + var focusId = el.id; + GovBB.render(true); + if (focusId) { + try { + var refocus = document.getElementById(focusId); + if (refocus && refocus.focus) refocus.focus({ preventScroll: true }); + } catch (e) {} + } + } }); }); } + /* Clear inline + summary errors for a field once the user has provided + input. Handles radio groups by clearing errors targeting any radio in + the same group. */ + function _clearFieldError(el) { + var ids = []; + if (el.type === 'radio' && el.name) { + var group = document.querySelectorAll('input[type="radio"][name="' + el.name + '"]'); + group.forEach(function (r) { if (r.id) ids.push(r.id); }); + } else if (el.id) { + ids.push(el.id); + } + if (!ids.length) return; + /* Clear inline messages + aria-invalid on every input that matches an + id (so all radios in the group lose the invalid styling at once). */ + ids.forEach(function (id) { + var node = document.getElementById(id); + if (!node) return; + if (node.getAttribute('aria-invalid') === 'true') { + node.removeAttribute('aria-invalid'); + node.removeAttribute('aria-describedby'); + } + /* The inline `

` is appended to the + closest form-group / fieldset / parent. Remove any matching one + with id `err-`. */ + var errMsg = document.getElementById('err-' + id); + if (errMsg && errMsg.parentNode) errMsg.parentNode.removeChild(errMsg); + /* Remove the corresponding error-summary list item, if any. */ + var summary = document.getElementById('error-summary'); + if (summary) { + var link = summary.querySelector('a[href="#' + id + '"]'); + if (link) { + var li = link.closest('li'); + if (li && li.parentNode) li.parentNode.removeChild(li); + } + } + }); + /* Also strip any stray .govbb-error-message left next to the input + (in case it doesn't carry an id). */ + var group = el.closest('.govbb-form-group') || el.closest('fieldset') || el.parentElement; + if (group) { + var stray = group.querySelectorAll('.govbb-error-message'); + stray.forEach(function (m) { + /* Only remove messages whose id matches one of our cleared ids + (or has no id at all and the input has no remaining error). */ + if (!m.id || ids.indexOf(m.id.replace(/^err-/, '')) !== -1) { + if (m.parentNode) m.parentNode.removeChild(m); + } + }); + } + /* If the summary list is now empty, remove the whole summary block. */ + var summary2 = document.getElementById('error-summary'); + if (summary2) { + var remaining = summary2.querySelectorAll('li'); + if (!remaining.length) { + if (summary2.parentNode) summary2.parentNode.removeChild(summary2); + if (document.title.indexOf('Error: ') === 0) { + document.title = document.title.substring('Error: '.length); + } + } + } + } + /* ── Radio binding ── */ function _bindRadios() { var radios = document.querySelectorAll('[role="radio"]');