diff --git a/core/assets/styles/overrides/_all.scss b/core/assets/styles/overrides/_all.scss index e8af924447..7c234ca02c 100644 --- a/core/assets/styles/overrides/_all.scss +++ b/core/assets/styles/overrides/_all.scss @@ -1,5 +1,6 @@ @import "button"; @import "checkboxes"; +@import "fieldset"; @import "file-upload"; @import "form-group"; @import "form-input"; diff --git a/core/assets/styles/overrides/_fieldset.scss b/core/assets/styles/overrides/_fieldset.scss new file mode 100644 index 0000000000..cd68a420a0 --- /dev/null +++ b/core/assets/styles/overrides/_fieldset.scss @@ -0,0 +1,11 @@ +fieldset { + .govuk-fieldset__legend--xl { + margin-bottom: 30px; + } + + @media (min-width: 40.0625em) { + .govuk-fieldset__legend--xl { + margin-bottom: 50px; + } + } +} diff --git a/core/common/forms.py b/core/common/forms.py index 09eed70245..3bcb35e768 100644 --- a/core/common/forms.py +++ b/core/common/forms.py @@ -2,8 +2,10 @@ from crispy_forms_gds.choices import Choice from crispy_forms_gds.layout import ( Div, + Fieldset, HTML, Layout, + Size, Submit, ) from django import forms @@ -62,6 +64,30 @@ def get_layout_actions(self): ] +class FieldsetForm(BaseForm): + """This is a suitable layout for a single question form. By using a +
and it ensures that related inputs are grouped together + with a common label to enable users to easily identify the group, as + covered by WCAG Technique H71. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper.layout = Layout( + Fieldset( + *self.get_layout_fields(), + legend=self.get_title(), + legend_size=Size.EXTRA_LARGE, + legend_tag="h1", + ), + Div( + *self.get_layout_actions(), + css_class="govuk-button-group", + ), + ) + + class MultipleFileInput(forms.ClearableFileInput): allow_multiple_selected = True diff --git a/exporter/applications/forms/hcsat.py b/exporter/applications/forms/hcsat.py index e9c1b02d26..a3747a9c2a 100644 --- a/exporter/applications/forms/hcsat.py +++ b/exporter/applications/forms/hcsat.py @@ -1,5 +1,5 @@ from crispy_forms_gds.helper import FormHelper -from crispy_forms_gds.layout import Field, Layout, Submit, HTML +from crispy_forms_gds.layout import Field, Fieldset, Layout, Size, Submit, HTML from django.urls import reverse_lazy from django import forms @@ -7,6 +7,9 @@ class HCSATminiform(forms.Form): + class Layout: + TITLE = "Overall, how would you rate your experience with the 'apply for a standard individual export licence (SIEL)' service today?" + RECOMMENDATION_CHOICES = [ ("VERY_DISSATISFIED", "Very dissatisfied"), ("DISSATISFIED", "Dissatisfied"), @@ -23,11 +26,19 @@ class HCSATminiform(forms.Form): error_messages={"required": "Star rating is required"}, ) + def get_title(self): + return self.Layout.TITLE + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.layout = Layout( - StarRadioSelect("satisfaction_rating"), + Fieldset( + StarRadioSelect("satisfaction_rating"), + legend=self.get_title(), + legend_size=Size.MEDIUM, + legend_tag="h2", + ), Submit("submit", "Submit and continue"), ) diff --git a/exporter/applications/forms/parties.py b/exporter/applications/forms/parties.py index e7be51a22c..86c7c28748 100644 --- a/exporter/applications/forms/parties.py +++ b/exporter/applications/forms/parties.py @@ -1,13 +1,13 @@ from core.helpers import remove_non_printable_characters from crispy_forms_gds.choices import Choice from crispy_forms_gds.helper import FormHelper -from crispy_forms_gds.layout import Layout, Submit, HTML +from crispy_forms_gds.layout import Fieldset, Layout, Size, Submit, HTML from django import forms from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, URLValidator from django.urls import reverse_lazy -from core.common.forms import BaseForm +from core.common.forms import BaseForm, FieldsetForm from core.forms.layouts import ConditionalRadios, ConditionalRadiosQuestion from core.forms.widgets import Autocomplete from exporter.core.constants import CaseTypes, FileUploadFileTypes @@ -133,10 +133,9 @@ def new_party_form_group(request, application, strings, back_url, clearance_opti return FormGroup(forms) -class PartyReuseForm(BaseForm): +class PartyReuseForm(FieldsetForm): class Layout: TITLE = "Do you want to reuse an existing party?" - TITLE_AS_LABEL_FOR = "reuse_party" reuse_party = forms.ChoiceField( choices=( @@ -154,7 +153,7 @@ def get_layout_fields(self): return ("reuse_party",) -class PartySubTypeSelectForm(BaseForm): +class PartySubTypeSelectForm(FieldsetForm): """ This form needs to be instantiated with a Layout.TITLE for the type of party whose data is being set as per the BaseForm. @@ -200,13 +199,11 @@ def clean(self): class EndUserSubTypeSelectForm(PartySubTypeSelectForm): class Layout: TITLE = "Select the type of end user" - TITLE_AS_LABEL_FOR = "sub_type" class ConsigneeSubTypeSelectForm(PartySubTypeSelectForm): class Layout: TITLE = "Select the type of consignee" - TITLE_AS_LABEL_FOR = "sub_type" class PartyNameForm(BaseForm): @@ -395,15 +392,19 @@ def __init__(self, *args, **kwargs): self.helper = FormHelper() self.helper.layout = Layout( - HTML.h1(self.title), - HTML.p(self.text_p1), - HTML.p(self.text_p2), - HTML.p(self.text_p3), - HTML.p(self.text_p4), - ConditionalRadios( - "end_user_document_available", - "Yes", - ConditionalRadiosQuestion("No", "end_user_document_missing_reason"), + Fieldset( + HTML.p(self.text_p1), + HTML.p(self.text_p2), + HTML.p(self.text_p3), + HTML.p(self.text_p4), + ConditionalRadios( + "end_user_document_available", + "Yes", + ConditionalRadiosQuestion("No", "end_user_document_missing_reason"), + ), + legend=self.title, + legend_size=Size.EXTRA_LARGE, + legend_tag="h1", ), Submit("submit", "Continue"), ) diff --git a/exporter/applications/views/goods/goods.py b/exporter/applications/views/goods/goods.py index 2fd21257e4..2bb9d86514 100644 --- a/exporter/applications/views/goods/goods.py +++ b/exporter/applications/views/goods/goods.py @@ -354,7 +354,10 @@ def get_product_type(self): def get_context_data(self, form, **kwargs): context = super().get_context_data(form, **kwargs) - context["title"] = form.title + if hasattr(form, "Layout") and hasattr(form.Layout, "TITLE"): + context["title"] = form.Layout.TITLE + else: + context["title"] = form.title # The back_link_url is used for the first form in the sequence. For subsequent forms, # the wizard automatically generates the back link to the previous form. context["back_link_url"] = reverse("applications:goods", kwargs={"pk": self.kwargs["pk"]}) diff --git a/exporter/core/organisation/forms.py b/exporter/core/organisation/forms.py index 82e50f1529..2565667eb2 100644 --- a/exporter/core/organisation/forms.py +++ b/exporter/core/organisation/forms.py @@ -5,7 +5,7 @@ from crispy_forms_gds.layout import HTML -from core.common.forms import BaseForm, TextChoice +from core.common.forms import BaseForm, FieldsetForm, TextChoice from exporter.core.services import get_countries from .validators import validate_phone from exporter.core.organisation.services import validate_registration_number @@ -27,7 +27,7 @@ def get_layout_fields(self): return () -class RegistrationTypeForm(BaseForm): +class RegistrationTypeForm(FieldsetForm): class Layout: TITLE = "Select the type of organisation" @@ -58,7 +58,7 @@ def get_layout_fields(self): return ("type",) -class RegistrationUKBasedForm(BaseForm): +class RegistrationUKBasedForm(FieldsetForm): class Layout: TITLE = "Where is your organisation based?" diff --git a/exporter/goods/forms/common.py b/exporter/goods/forms/common.py index b99ff58152..cf0314dbfc 100644 --- a/exporter/goods/forms/common.py +++ b/exporter/goods/forms/common.py @@ -16,7 +16,7 @@ ) from core.forms.utils import coerce_str_to_bool -from core.common.forms import BaseForm +from core.common.forms import BaseForm, FieldsetForm from exporter.core.forms import CustomErrorDateInputField from exporter.core.services import ( get_control_list_entries, @@ -52,7 +52,7 @@ def get_layout_fields(self): ) -class ProductControlListEntryForm(BaseForm): +class ProductControlListEntryForm(FieldsetForm): class Layout: TITLE = "Do you know the product's control list entry?" @@ -120,7 +120,7 @@ def clean(self): return cleaned_data -class ProductPVGradingForm(BaseForm): +class ProductPVGradingForm(FieldsetForm): class Layout: TITLE = "Does the product have a government security grading or classification?" @@ -148,7 +148,7 @@ def get_layout_fields(self): ) -class ProductPVGradingDetailsForm(BaseForm): +class ProductPVGradingDetailsForm(FieldsetForm): class Layout: TITLE = "What is the security grading or classification?" @@ -275,7 +275,7 @@ def clean(self): return cleaned_data -class ProductDocumentAvailabilityForm(BaseForm): +class ProductDocumentAvailabilityForm(FieldsetForm): class Layout: TITLE = "Do you have a document that shows what your product is and what it's designed to do?" @@ -324,7 +324,7 @@ def clean(self): return cleaned_data -class ProductDocumentSensitivityForm(BaseForm): +class ProductDocumentSensitivityForm(FieldsetForm): class Layout: TITLE = "Is the document rated above Official-sensitive?" @@ -354,7 +354,7 @@ def get_layout_fields(self): ) -class ProductDocumentUploadForm(BaseForm): +class ProductDocumentUploadForm(FieldsetForm): class Layout: TITLE = "Upload a document that shows what your product is designed to do" @@ -404,7 +404,7 @@ def get_layout_fields(self): return layout_fields -class ProductOnwardExportedForm(BaseForm): +class ProductOnwardExportedForm(FieldsetForm): class Layout: TITLE = "Is the product going to any ultimate end-users?" @@ -431,7 +431,7 @@ def get_layout_fields(self): ) -class ProductOnwardAlteredProcessedForm(BaseForm): +class ProductOnwardAlteredProcessedForm(FieldsetForm): class Layout: TITLE = "Will the item be altered or processed before it is exported again?" @@ -482,7 +482,7 @@ def clean(self): return cleaned_data -class ProductOnwardIncorporatedForm(BaseForm): +class ProductOnwardIncorporatedForm(FieldsetForm): class Layout: TITLE = "Will the product be incorporated into another item before it is onward exported?" diff --git a/exporter/goods/forms/firearms.py b/exporter/goods/forms/firearms.py index eaab0f5f97..589e90d13a 100644 --- a/exporter/goods/forms/firearms.py +++ b/exporter/goods/forms/firearms.py @@ -16,7 +16,7 @@ ) from core.forms.utils import coerce_str_to_bool -from core.common.forms import BaseForm, TextChoice +from core.common.forms import BaseForm, FieldsetForm, TextChoice from exporter.core.forms import ( CustomErrorDateInputField, PotentiallyUnsafeClearableFileInput, @@ -100,7 +100,7 @@ def get_layout_fields(self): return ("calibre",) -class FirearmReplicaForm(BaseForm): +class FirearmReplicaForm(FieldsetForm): class Layout: TITLE = "Is the product a replica firearm?" @@ -192,7 +192,7 @@ def get_layout_fields(self): ) -class FirearmRegisteredFirearmsDealerForm(BaseForm): +class FirearmRegisteredFirearmsDealerForm(FieldsetForm): class Layout: TITLE = "Are you a registered firearms dealer?" @@ -267,7 +267,7 @@ def get_layout_fields(self): ) -class FirearmFirearmAct1968Form(BaseForm): +class FirearmFirearmAct1968Form(FieldsetForm): class Layout: TITLE = "Which section of the Firearms Act 1968 is the product covered by?" @@ -462,7 +462,7 @@ def get_layout_fields(self): return ("is_covered_by_section_5",) -class FirearmMadeBefore1938Form(BaseForm): +class FirearmMadeBefore1938Form(FieldsetForm): class Layout: TITLE = "Was the product made before 1938?" @@ -503,7 +503,7 @@ def get_layout_fields(self): return ("year_of_manufacture",) -class FirearmIsDeactivatedForm(BaseForm): +class FirearmIsDeactivatedForm(FieldsetForm): class Layout: TITLE = "Has the product been deactivated?" @@ -599,7 +599,7 @@ def clean(self): return cleaned_data -class FirearmSerialIdentificationMarkingsForm(BaseForm): +class FirearmSerialIdentificationMarkingsForm(FieldsetForm): class Layout: TITLE = "Will each product have a serial number or other identification marking?" diff --git a/exporter/goods/forms/goods.py b/exporter/goods/forms/goods.py index e94f69cbbf..43362db1de 100644 --- a/exporter/goods/forms/goods.py +++ b/exporter/goods/forms/goods.py @@ -13,7 +13,7 @@ from core.forms.layouts import ConditionalRadiosQuestion, ConditionalRadios, summary_list from core.forms.utils import coerce_str_to_bool -from core.common.forms import TextChoice +from core.common.forms import FieldsetForm, TextChoice from exporter.core.constants import ( ProductSecurityFeatures, ProductDeclaredAtCustoms, @@ -332,8 +332,9 @@ def has_valid_section_five_certificate(application): return False -class GroupTwoProductTypeForm(forms.Form): - title = CreateGoodForm.FirearmGood.ProductType.TITLE +class GroupTwoProductTypeForm(FieldsetForm): + class Layout: + TITLE = CreateGoodForm.FirearmGood.ProductType.TITLE type = forms.TypedChoiceField( choices=( @@ -352,21 +353,14 @@ class GroupTwoProductTypeForm(forms.Form): label="", ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.helper = FormHelper() - self.helper.layout = Layout( - HTML.h1(self.title), - "type", - Submit("submit", CreateGoodForm.SUBMIT_BUTTON), - ) - def clean(self): cleaned_data = super().clean() cleaned_data["product_type_step"] = True return cleaned_data + def get_layout_fields(self): + return ("type",) + class FirearmsNumberOfItemsForm(forms.Form): title = "Number of items" diff --git a/exporter/goods/views.py b/exporter/goods/views.py index 9f10107aff..b885088054 100644 --- a/exporter/goods/views.py +++ b/exporter/goods/views.py @@ -102,7 +102,11 @@ class GoodCommonMixin: def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["title"] = self.get_form().title + form = self.get_form() + if hasattr(form, "Layout") and hasattr(form.Layout, "TITLE"): + context["title"] = form.Layout.TITLE + else: + context["title"] = form.title return context @@ -543,7 +547,7 @@ def get_initial(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["form_title"] = self.form_class.title + context["form_title"] = self.form_class.Layout.TITLE return context def form_valid(self, form): diff --git a/exporter/templates/applications/application-submit-success.html b/exporter/templates/applications/application-submit-success.html index 5aeefd22c4..5157ecd134 100644 --- a/exporter/templates/applications/application-submit-success.html +++ b/exporter/templates/applications/application-submit-success.html @@ -51,7 +51,6 @@

What happens next

{% endfor %}
-

Overall, how would you rate your experience with the 'apply for a standard individual export licence (SIEL)' service today?

{% crispy form %} {% endblock %} diff --git a/lite_forms/templates/components.html b/lite_forms/templates/components.html index 773236a262..8f4df99e81 100644 --- a/lite_forms/templates/components.html +++ b/lite_forms/templates/components.html @@ -24,11 +24,6 @@ {{ question.description|safe }} {% endif %} -{% if question.description %} - - {% endif %} {% if question.accessible_description %}