Skip to content

Commit

Permalink
Merge branch 'main' into a11y/disclosure-menu-pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
whabanks committed May 13, 2024
2 parents fb09c46 + 5fde768 commit edf27b6
Show file tree
Hide file tree
Showing 41 changed files with 614 additions and 225 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_endpoints.yaml
Expand Up @@ -58,7 +58,7 @@ jobs:
echo "PYTHONPATH=/github/workspace/env/site-packages:${{ env.PYTHONPATH}}" >> $GITHUB_ENV
- name: Checks for new endpoints against AWS WAF rules
uses: cds-snc/notification-utils/.github/actions/waffles@0146718a423b0144566392ab3082d15779f4a73f # 52.5.0
uses: cds-snc/notification-utils/.github/actions/waffles@52.2.2
with:
app-loc: '/github/workspace'
app-libs: '/github/workspace/env/site-packages'
Expand Down
74 changes: 4 additions & 70 deletions app/assets/javascripts/branding_request.js
Expand Up @@ -19,34 +19,23 @@
*/
(function () {
"use strict";

const input_img = document.querySelector("input.file-upload-field");
const input_brandname = document.querySelector("#name");
const submit_button = document.querySelector("button[type=submit]");
const alt_en = document.getElementById("alt_text_en");
const alt_fr = document.getElementById("alt_text_fr");
const message = document.querySelector(".preview .message");
const image_slot = document.querySelector(".preview .img");
const preview_heading = document.querySelector("#preview_heading");
const preview_container = document.querySelector(".template_preview");
const brand_name_group = document
.querySelector("#name")
.closest(".form-group");
const image_label = document.getElementById("file-upload-label");

// init UI
input_img.style.opacity = 0;
input_img.addEventListener("change", updateImageDisplay);
submit_button.addEventListener("click", validateForm);
input_brandname.addEventListener("change", validateBrand);

// strings
let file_name = window.APP_PHRASES.branding_request_file_name;
let file_size = window.APP_PHRASES.branding_request_file_size;
let display_size = window.APP_PHRASES.branding_request_display_size;
let brand_error = window.APP_PHRASES.branding_request_brand_error;
let logo_error = window.APP_PHRASES.branding_request_logo_error;

// error message html
let brand_error_html = `<span id="name-error-message" data-testid="brand-error" class="error-message" data-module="track-error" data-error-type="${brand_error}" data-error-label="name">${brand_error}</span>`;
let image_error_html = `<span id="logo-error-message" data-testid="logo-error" class="error-message">${logo_error}</span>`;

/**
* Update email template preview based on the selected file
Expand All @@ -69,7 +58,7 @@
.getElementById("template_preview")
.shadowRoot.querySelector("img");
img.src = encodeURI(img_src);

img.alt = `${alt_en.value} / ${alt_fr.value}`;
img.onload = () => {
message.textContent = `${file_name} ${
file.name
Expand All @@ -79,7 +68,6 @@
preview_container.classList.remove("hidden");
preview_heading.focus();
};
validateLogo();
} else {
//remove file from input
input_img.value = "";
Expand All @@ -106,58 +94,4 @@
return `${(number / 1048576).toFixed(1)} MB`;
}
}

function validateForm(event) {
const brandName = input_brandname.value.trim();
const image = input_img.value.length > 0;

validateBrand();
validateLogo();

if (!brandName || !image) {
// set focus on the first input with an error
if (!brandName) {
input_brandname.focus();
} else {
input_img.focus();
}
if (event) {
event.preventDefault();
}
}
}

function validateBrand() {
const brandName = input_brandname.value.trim();

if (!brandName) {
if (!brand_name_group.classList.contains("form-group-error")) {
// dont display the error more than once
brand_name_group.classList.add("form-group-error");
input_brandname.insertAdjacentHTML("beforebegin", brand_error_html);
}
} else {
if (brand_name_group.classList.contains("form-group-error")) {
brand_name_group.classList.remove("form-group-error");
document.getElementById("name-error-message").remove();
}
}
}

function validateLogo() {
const image = input_img.value.length > 0;

if (!image) {
if (!image_label.classList.contains("form-group-error")) {
// dont display the error more than once
image_label.classList.add("form-group-error");
image_label.insertAdjacentHTML("beforebegin", image_error_html);
}
} else {
if (image_label.classList.contains("form-group-error")) {
image_label.classList.remove("form-group-error");
document.getElementById("logo-error-message").remove();
}
}
}
})();
2 changes: 1 addition & 1 deletion app/assets/javascripts/branding_request.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions app/assets/javascripts/button.js
@@ -0,0 +1,23 @@
/**
* Adds role and draggable attributes to all anchor elements with the class "button".
* Allows buttons to be triggered by pressing the space key.
*/
(function () {
"use strict";

/**
* Adds attributes and keydown listener
* @param {HTMLElement} button - The button element.
*/
document.querySelectorAll("a.button:not(.button-link)").forEach((button) => {
button.setAttribute("role", "button");
button.setAttribute("draggable", "false");

button.addEventListener("keydown", (event) => {
if (event.key === " ") {
event.preventDefault();
button.click();
}
});
});
})();
79 changes: 79 additions & 0 deletions app/assets/javascripts/formValidateRequired.js
@@ -0,0 +1,79 @@
/**
* This script is used to validate forms. It loops over any forms on the page with the `data-validate-required` attribute
* and validates each descendant input that has a `required` attribute.
*
* When a form is submitted, it ensures all fields marked `required` have values and prevents the form from being submitted
* if not. It also validates each field whenever its value changes.
**/
(function () {
"use strict";

// loop over all forms with the 'data-validate-required' attribute
const forms = document.querySelectorAll("form[data-validate-required]");
forms.forEach((form) => {
const inputs = form.querySelectorAll("[required]");

// Add a 'change' event listener to each input field to validate the field whenever it changes.
inputs.forEach((input) => {
input.addEventListener("change", () => _validateRequired(input.id));
});

// Listen for the form's submit event and validate the form when it's submitted.
const ids = Array.from(inputs).map((input) => input.id);
form.addEventListener("submit", (event) => _validateForm(event, ids));
});

/**
* Validates the form by looping over an array of field IDs and validating each field.
* If any field fails validation, it prevents the default event behavior and focuses on the first input with an error.
*
* @param {Event} event - The event object triggered by the form submission.
* @param {Array} ids - An array of field IDs to be validated.
*/
function _validateForm(event, ids) {
let validationResults = [];
// loop over array of ids
ids.forEach((id) => {
validationResults.push(_validateRequired(id));
});

// focus on the first input with an error (the field corresponding to the index of the first 'false' value in validationResults)
if (validationResults.includes(false)) {
if (event) {
event.preventDefault();
}
const firstErrorIndex = validationResults.indexOf(false);
document.getElementById(ids[firstErrorIndex]).focus();
}
}

/**
* Validates a field based on its ID.
*
* @param {string} id - The ID of the field to validate.
* @returns {boolean} - Returns true if the field is valid, false otherwise.
*/
function _validateRequired(id) {
let error_html = (id, error) =>
`<span id="${id}-error-message" data-testid="${id}-error" class="error-message" data-module="track-error" data-error-type="${error}" data-error-label="${id}">${error}</span>`;
const field = document.getElementById(id);
const value = field.value.trim();
const group = field.closest(".form-group");
const error_text = field.dataset.errorMsg;

if (!value) {
if (!group.classList.contains("form-group-error")) {
// dont display the error more than once
group.classList.add("form-group-error");
field.insertAdjacentHTML("beforebegin", error_html(id, error_text));
}
return false;
} else {
if (group.classList.contains("form-group-error")) {
group.classList.remove("form-group-error");
document.getElementById(`${id}-error-message`).remove();
}
return true;
}
}
})();
1 change: 1 addition & 0 deletions app/assets/javascripts/formValidateRequired.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions app/assets/javascripts/sessionRedirect.js
@@ -0,0 +1,20 @@
/**
* Redirects the user after a specified period of time.
*/
(function () {
const REDIRECT_LOCATION = "/sign-in?timeout=true";
const SESSION_TIMEOUT_MS = 7 * 60 * 60 * 1000 + 55 * 60 * 1000; // 7 hours 55 minutes

redirectCountdown(REDIRECT_LOCATION, SESSION_TIMEOUT_MS); // 7 hours 55 minutes

/**
* Redirects to the specified location after a given period of time.
* @param {string} redirectLocation - The URL to redirect to.
* @param {number} period - The period of time (in milliseconds) before redirecting.
*/
function redirectCountdown(redirectLocation, period) {
setTimeout(function () {
window.location.href = redirectLocation;
}, period);
}
})();
1 change: 1 addition & 0 deletions app/assets/javascripts/sessionRedirect.min.js
@@ -0,0 +1 @@
setTimeout((function(){window.location.href="/sign-in?timeout=true"}),285e5);
2 changes: 1 addition & 1 deletion app/assets/stylesheets/index.css

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion app/assets/stylesheets/tailwind/components/buttons.css
Expand Up @@ -8,7 +8,6 @@
a.button:active {
padding: 0.55em 1em 0.45em;
box-shadow: 0 2px 0 theme("colors.blue.border");
cursor: pointer;
@apply no-underline inline-flex items-center text-white text-smaller leading-tight align-top bg-blue min-h-target;
}
.button.shadow-none,
Expand Down Expand Up @@ -86,6 +85,11 @@
@apply opacity-50 cursor-default;
}

a.button:not(.button-link) {
user-select: none;
cursor: default;
}

.button-link,
a.button-link,
a.button-link:visited,
Expand All @@ -101,5 +105,6 @@
a.button-link:focus {
@apply bg-yellow text-blue outline-none border-white no-underline;
}

/*! purgecss end ignore */
}
19 changes: 19 additions & 0 deletions app/assets/stylesheets/tailwind/components/textbox.css
Expand Up @@ -39,5 +39,24 @@
@apply ml-2 inline-block align-top;
}
}

.localized-field-fields .form-group {
@apply w-full mb-0 flex flex-col;
}
.localized-field-fields .form-group label,
.localized-field-fields .form-group .error-message {
padding: 0 4px 4px;
@apply border-l-2 border-gray-300 m-0 flex-grow;
}
.localized-field-fields .form-group-error {
@apply border-0 pl-0 mr-0;
}
.localized-field-fields .form-group-error label,
.localized-field-fields .form-group-error .error-message {
@apply border-red border-l-2;
}
.localized-field-fields .form-group:not(:last-of-type) input {
@apply border-r-0;
}
/*! purgecss end ignore */
}
6 changes: 4 additions & 2 deletions app/assets/stylesheets/tailwind/elements/form-validation.css
Expand Up @@ -5,11 +5,13 @@
@apply mb-4;
}

.form-group-error {
.form-group-error,
.localized-field:has(.form-group-error) {
@apply mr-4 pl-4 border-l-4 border-red;
}
@screen md {
.form-group-error {
.form-group-error,
.localized-field:has(.form-group-error) {
border-left-width: 5px;
@apply pl-gutterHalf;
}
Expand Down
5 changes: 4 additions & 1 deletion app/assets/stylesheets/tailwind/elements/reset.css
Expand Up @@ -131,6 +131,9 @@
*/

legend {
@apply box-border max-w-full table overflow-hidden;
@apply box-border max-w-full table overflow-hidden float-left;
}
legend + * {
@apply clear-both;
}
}
14 changes: 14 additions & 0 deletions app/main/forms.py
Expand Up @@ -1185,12 +1185,24 @@ class ServiceUpdateEmailBranding(StripWhitespaceForm):
],
)
organisation = RadioField("Select an organisation", choices=[])
alt_text_en = StringField("Alternative text for English logo")
alt_text_fr = StringField("Alternative text for French logo")

def validate_name(form, name):
op = request.form.get("operation")
if op == "email-branding-details" and not form.name.data:
raise ValidationError("This field is required")

def validate_alt_text_en(form, alt_text_en):
op = request.form.get("operation")
if op == "email-branding-details" and not form.alt_text_en.data:
raise ValidationError("This field is required")

def validate_alt_text_fr(form, alt_text_fr):
op = request.form.get("operation")
if op == "email-branding-details" and not form.alt_text_fr.data:
raise ValidationError("This field is required")


class SVGFileUpload(StripWhitespaceForm):
file = FileField_wtf(
Expand Down Expand Up @@ -1809,6 +1821,8 @@ class BrandingRequestForm(StripWhitespaceForm):
"""

name = StringField(label=_l("Name of logo"), validators=[DataRequired(message=_l("Enter the name of the logo"))])
alt_text_en = StringField(label=_l("English"), validators=[DataRequired(message=_l("This cannot be empty"))])
alt_text_fr = StringField(label=_l("French"), validators=[DataRequired(message=_l("This cannot be empty"))])
file = FileField_wtf(
label=_l("Prepare your logo"),
validators=[
Expand Down

0 comments on commit edf27b6

Please sign in to comment.