Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions l10n/en/cms/contact.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@

## Error messages

# Variables
# $field (string) The required form field that wasn't filled
contact-form-error-required-field = You must fill out the { $field } field.
contact-form-error-required = This field is required.
contact-form-error-sending = There was an error sending your message. Please try again.
contact-form-error-empty = Please fill out the form.
contact-form-submit = Submit
contact-form-select-option = Select an option
contact-form-correct-errors = Please correct the errors below
66 changes: 38 additions & 28 deletions media/css/cms/components/flare26-form-fields.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,42 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
display: flex;
flex-direction: column;
gap: var(--token-spacing-sm);
}

.fl-legend {
margin-block-end: var(--token-spacing-md);
}
.fl-field-wrap .fl-legend {
margin-block-end: var(--token-spacing-md);
}

.fl-field {
background-color: var(--theme-form-field-background);
border: 1px solid var(--theme-form-field-border-color);
border-radius: var(--token-border-radius-3xs);
color: var(--theme-form-color);
display: block;
inline-size: 100%;
margin: 0;
padding: var(--token-spacing-sm);
.fl-field-wrap .fl-field {
background-color: var(--theme-form-field-background);
border: 1px solid var(--theme-form-field-border-color);
border-radius: var(--token-border-radius-3xs);
color: var(--theme-form-color);
display: block;
inline-size: 100%;
margin: 0;
padding: var(--token-spacing-sm);
}

&:focus {
background-color: var(--theme-form-field-background-focus);
border-color: var(--theme-form-field-border-color-focus);
outline: 2px solid var(--theme-form-focus-outline-color);
}
}
.fl-field-wrap .fl-field:focus {
background-color: var(--theme-form-field-background-focus);
border-color: var(--theme-form-field-border-color-focus);
outline: 2px solid var(--theme-form-focus-outline-color);
}

select {
appearance: none;
background-image: url('/media/img/firefox/flare/2026/icons/desktop-16/arrows-and-chevrons/chevron-down-small-16.svg');
background-position: right var(--token-spacing-sm) center;
background-repeat: no-repeat;
background-size: 20px 20px;
padding-inline-end: calc(var(--token-spacing-sm) * 2 + 20px);
}
.fl-field-wrap select {
appearance: none;
background-image: url('/media/img/firefox/flare/2026/icons/desktop-16/arrows-and-chevrons/chevron-down-small-16.svg');
background-position: right var(--token-spacing-sm) center;
background-repeat: no-repeat;
background-size: 20px 20px;
padding-inline-end: calc(var(--token-spacing-sm) * 2 + 20px);
}

.fl-checkbox {
-webkit-appearance: none;
block-size: 19px;
border: 1px solid var(--token-color-light-purple);
border: 1px solid var(--theme-checkbox-border-color);
border-radius: 5px;
cursor: pointer;
flex-shrink: 0;
Expand All @@ -79,6 +79,16 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
gap: var(--token-spacing-sm);
}

fieldset {
fieldset.fl-field-wrap {
border: 0 none;
}

.fl-field-error {
--theme-form-field-border-color: var(--theme-form-field-border-color-error);
--theme-checkbox-border-color: var(--theme-form-field-border-color-error);
}

.fl-field-error-message {
color: var(--theme-form-error-message-color);
font-size: var(--fl-theme-label-sm);
}
2 changes: 2 additions & 0 deletions media/css/cms/variables/flare26-dark-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,7 @@
--theme-form-field-background: var(--token-color-white);
--theme-form-field-background-focus: var(--token-color-white);
--theme-form-field-border-color: transparent;
--theme-form-field-border-color-error: var(--token-color-secondary-red);
--theme-form-field-border-color-focus: var(--token-color-light-purple);
--theme-form-focus-outline-color: var(--token-color-light-purple-60);
--theme-form-error-message-color: var(--token-color-secondary-red);
3 changes: 3 additions & 0 deletions media/css/cms/variables/flare26-light-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@
--theme-form-field-background: var(--token-color-soft-purple);
--theme-form-field-background-focus: var(--token-color-white);
--theme-form-field-border-color: transparent;
--theme-form-field-border-color-error: var(--token-color-secondary-red-2);
--theme-form-field-border-color-focus: var(--token-color-light-purple);
--theme-form-color: var(--token-color-black-4);
--theme-form-focus-outline-color: var(--token-color-soft-purple-3);
--theme-checkbox-border-color: var(--token-color-light-purple);
--theme-form-error-message-color: var(--token-color-secondary-red-2);
20 changes: 20 additions & 0 deletions springfield/cms/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
from urllib.parse import parse_qsl, urlparse
from uuid import uuid4

from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
from django.forms.widgets import CheckboxSelectMultiple
from django.urls import Resolver404, resolve
from django.utils import translation
from django.utils.translation import gettext_lazy as _

from product_details import product_details
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock
from wagtail.models import Page
Expand Down Expand Up @@ -3197,3 +3199,21 @@ class Meta:
template = "cms/blocks/form_fields/hidden_field.html"
label = "Hidden Field"
label_format = "Hidden - {label}"


class CountrySelectFieldBlock(BaseField):
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
request = parent_context.get("request") if parent_context else None
locale = (getattr(request, "locale", None) or settings.LANGUAGE_CODE) if request else settings.LANGUAGE_CODE
countries = sorted(
((code.upper(), name) for code, name in product_details.get_regions(locale).items()),
key=lambda item: item[1],
)
context["countries"] = countries
return context

class Meta:
template = "cms/blocks/form_fields/country_select_field.html"
label = "Country Select Field"
label_format = "Country Select - {label}"
4 changes: 2 additions & 2 deletions springfield/cms/fixtures/contact_page_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ def get_form_field_variants() -> list[dict]:
"id": "select-field",
},
{
"type": "text_field",
"type": "country_select_field",
"value": {
"internal_identifier": "country",
"label": "Country",
"required": True,
},
"id": "text-field-country",
"id": "country-select-field",
},
{
"type": "textarea_field",
Expand Down
27 changes: 14 additions & 13 deletions springfield/cms/models/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
CheckboxFieldBlock,
CheckboxGroupFieldBlock,
CodeBlock,
CountrySelectFieldBlock,
DownloadSupportBlock,
EmailFieldBlock,
FeaturedImageSectionBlock,
Expand Down Expand Up @@ -1779,6 +1780,7 @@ class ContactPage(AbstractSpringfieldCMSPage):
("checkbox_field", CheckboxFieldBlock()),
("checkbox_group_field", CheckboxGroupFieldBlock()),
("hidden_field", HiddenFieldBlock()),
("country_select_field", CountrySelectFieldBlock()),
],
blank=True,
null=True,
Expand Down Expand Up @@ -1863,9 +1865,7 @@ def clean(self):

def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
form_errors = getattr(request, "form_errors", None)
if form_errors:
context["form_errors"] = form_errors
context["form_errors"] = getattr(request, "form_errors", {})
if getattr(request, "form_success", False):
context["form_success"] = True
context["form_data"] = getattr(request, "form_data", {})
Expand Down Expand Up @@ -1899,7 +1899,7 @@ def serve(self, request, *args, **kwargs):
f"Basket API returned {api_response.status_code} for path {self.basket_api_path}",
level="error",
)
request.form_errors = [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]
request.form_errors = {"__all__": [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]}
request.form_data = self._get_form_data_for_context(request.POST)
response = super().serve(request, *args, **kwargs)
add_never_cache_headers(response)
Expand All @@ -1912,7 +1912,7 @@ def serve(self, request, *args, **kwargs):
f"Basket API request failed for path {self.basket_api_path}",
level="error",
)
request.form_errors = [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]
request.form_errors = {"__all__": [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]}
request.form_data = self._get_form_data_for_context(request.POST)
response = super().serve(request, *args, **kwargs)
add_never_cache_headers(response)
Expand All @@ -1928,7 +1928,7 @@ def serve(self, request, *args, **kwargs):
"Failed to send contact form email",
level="error",
)
request.form_errors = [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]
request.form_errors = {"__all__": [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]}
request.form_data = self._get_form_data_for_context(request.POST)
response = super().serve(request, *args, **kwargs)
add_never_cache_headers(response)
Expand Down Expand Up @@ -1981,12 +1981,14 @@ def _get_form_data_for_context(self, post_data):
def validate_form_data(self, post_data):
"""Validate submitted form data against the field configuration.

Returns a list of error messages. An empty list means the data is valid.
Returns a dict matching Django's ErrorDict shape:
{identifier: [msg], ..., "__all__": [global_msg]}
An empty dict means the data is valid.
"""
if post_data.get("office_fax", ""):
return [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]
return {"__all__": [ftl_lazy("contact-form-error-sending", ftl_files=self.ftl_files)]}

errors = []
errors = {}
has_any_data = False

for field in self.form_fields:
Expand All @@ -1995,7 +1997,6 @@ def validate_form_data(self, post_data):

value = field.value
identifier = value["internal_identifier"]
label = value["label"]
is_required = value.get("required", False)

if field.block_type == "checkbox_group_field":
Expand All @@ -2007,10 +2008,10 @@ def validate_form_data(self, post_data):
has_any_data = True

if is_required and not submitted:
errors.append(ftl_lazy("contact-form-error-required-field", ftl_files=self.ftl_files, field=label))
errors[identifier] = [ftl_lazy("contact-form-error-required", ftl_files=self.ftl_files)]

if not has_any_data:
errors.append(ftl_lazy("contact-form-error-empty", ftl_files=self.ftl_files))
if not has_any_data and not errors:
errors.setdefault("__all__", []).append(ftl_lazy("contact-form-error-empty", ftl_files=self.ftl_files))

return errors

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap">
<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-checkbox-label" for="{{ value.internal_identifier }}">
<input class="fl-checkbox" id="{{ value.internal_identifier }}" type="checkbox" name="{{ value.internal_identifier }}" value="on"{% if form_data.get(value.internal_identifier) %} checked{% endif %}{% if value.required %} required aria-required="true"{% endif %}>
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<fieldset class="fl-field-wrap"{% if value.required %} aria-required="true"{% endif %}>
<fieldset class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}"{% if value.required %} aria-required="true"{% endif %}>
<legend class="fl-legend">{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}</legend>
{% for option in value.options %}
<label class="fl-checkbox-label" for="{{ value.internal_identifier }}-{{ option.value|slugify }}">
<input class="fl-checkbox" id="{{ value.internal_identifier }}-{{ option.value|slugify }}" type="checkbox" name="{{ value.internal_identifier }}" value="{{ option.value }}"{% if option.value in form_data.get(value.internal_identifier, []) %} checked{% endif %}>
{{ option.label }}
</label>
{% endfor %}
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</fieldset>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{#
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-label" for="{{ value.internal_identifier }}">
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
<select class="fl-field" id="{{ value.internal_identifier }}" name="{{ value.internal_identifier }}"{% if value.required %} required aria-required="true"{% endif %}>
<option value="">{{ ftl("contact-form-select-option") }}</option>
{% for code, name in countries %}
<option value="{{ code }}"{% if code == form_data.get(value.internal_identifier) %} selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap">
<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-label" for="{{ value.internal_identifier }}">
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
<input class="fl-field" type="email" id="{{ value.internal_identifier }}" name="{{ value.internal_identifier }}" value="{{ form_data.get(value.internal_identifier, '') }}"{% if value.required %} required aria-required="true"{% endif %}>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap">
<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-label" for="{{ value.internal_identifier }}">
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
<input class="fl-field" type="tel" id="{{ value.internal_identifier }}" name="{{ value.internal_identifier }}" value="{{ form_data.get(value.internal_identifier, '') }}"{% if value.required %} required aria-required="true"{% endif %}>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap">
<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-label" for="{{ value.internal_identifier }}">
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
Expand All @@ -14,4 +14,7 @@
<option value="{{ option.value }}"{% if option.value == form_data.get(value.internal_identifier) %} selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap">
<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-label" for="{{ value.internal_identifier }}">
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
<input class="fl-field" type="text" id="{{ value.internal_identifier }}" name="{{ value.internal_identifier }}" value="{{ form_data.get(value.internal_identifier, '') }}"{% if value.required %} required aria-required="true"{% endif %}>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
#}

<div class="fl-field-wrap">
<div class="fl-field-wrap{% if form_errors.get(value.internal_identifier) %} fl-field-error{% endif %}">
<label class="fl-label" for="{{ value.internal_identifier }}">
{{ value.label }}{% if value.required %} <span aria-hidden="true">*</span>{% endif %}
</label>
<textarea class="fl-field" id="{{ value.internal_identifier }}" name="{{ value.internal_identifier }}" rows="{{ value.rows }}"{% if value.required %} required aria-required="true"{% endif %}>{{ form_data.get(value.internal_identifier, '') }}</textarea>
{% for error in form_errors.get(value.internal_identifier, []) %}
<p class="fl-field-error-message">{{ error }}</p>
{% endfor %}
</div>
Loading
Loading