if the form container has a title, the correct level of heading should be added to the form using the {‘title.heading_level’: ‘CORRECT_HEADING_LEVEL’} property, else this will default to a h2, which may not be correct in all scenarios
If the requires leading text, the {‘form.lead’: ‘STRING_OF_LEAD_TEXT’} should contain the lead of text
If the form requires text elements, lists, links or additional content to be placed before or after the form, the {‘form.additional_info_before’: ‘STRING_OF_RICH_TEXT_HTML’} or {‘form.additional_info_after’: ‘STRING_OF_RICH_TEXT_HTML’}, should be used. If the information is important, such as instructions, links to help pages or contacts etc, use the {‘form.additional_info_before’: ‘STRING_OF_RICH_TEXT_HTML’} value, as this will be discoverable for all users, if the content is inserted after, it appears after the submit button and some screen reader users may not be aware it is there. Only add the {‘form.additional_info_after’: ‘STRING_OF_RICH_TEXT_HTML’}, for search forms or forms with 1 or 2 inputs and if the content is not important.
If the form design has a button that is displayed adjacent to an input, the {‘form.button_inline’: true} would need to be set. This should only be used for single input forms, such as search forms
A form error is presented at the top of a form. It should be used in scenarios where a user’s input data passes validation constraints, but the data does not match records on the system. As an example, if a user attempts login and the credentials do not match, it may be helpful to direct them to some help pages or give them an email to contact the correct department e.g:
'form': {
'form_error': 'Your login credentails do not match, please email <a href="mailto:somebody@leeds.ac.uk">Somebody at Leeds</a>',
'form_error_id': 'formErrorId',
...
}
It is necessary to provide the {‘form_error_id’: ‘UNIQUE_ID_ON_THE_PAGE’} and when the page is sent back, the ID of the form error should be appended to the URL and the attribute tabindex=”-1” should be set to tabindex=”0”. this ensures that the message receives focus and can be consumed by the widest range of devices and assistive technologies.
Where a form needs centering within its container {‘form_centered’: true} should be set
When a form has errors, it is also best practice to prepend the page title with helpful error text, as an example
<title>Contact us | University of Leeds</title>
...
Would be more helpful to users if it were dynamically changed to
<title>3 errors on form submission - Contact us | University of Leeds</title>
This particularly benefits screen reader users, as the page title is the first thing read out on page load, it also benefits users that may become distracted, especially if they have multiple tabs open.
On request specific variants can be added to our form component. These could just be configuration, but in some instances may require further development work.
This variant presents two drop down lists before a search box and button. On screen sizes over 768px the two dropdowns are presented horizontally alongside each other and stack to one column for smaller sizes.
This is achieved via presenting these in the config within a “form-group” and setting “inline-fields” to be true to accommodate the required layout.
An example config (with reduced data for select components) for this is presented below.
"form": {
"heading_level": "h2",
"form_centered": true,
"action": "/example-form-action",
"title": null,
"lead": null,
"overflow": true,
"additional_info_before": null,
"button": {
"style": "primary",
"type": "submit",
"content": "Search"
},
"additional_info_after": "<p>Or <a href=\"#\">link to other site</a>.</p>",
"form_group": {
"inline_fields": true,
"fields": [
{
'type': 'select',
'label': 'Which subject matter does your event relate to?',
'id': 'cheeseList',
'name': 'selectName1',
"hint": "Select one type",
'options': [
{"label": "Brie", "value": "BRI"},
{"label": "Cashel Blue", "value": "CBL"},
],
},
{
'type': 'select',
'label': 'Which subject matter does your event relate to?',
'id': 'cheeseList',
'name': 'selectName1',
"hint": "Select one type",
'options': [
{"label": "Brie", "value": "BRI"},
{"label": "Cashel Blue", "value": "CBL"},
],
},
],
},
"fields": [
{
"type": "search",
"id": "inputId2",
"name": "searchCourses2",
"label": "Search by subject, course title or keyword",
"invalid": "false",
"autocomplete": "off",
"has_icon": true
},
],
"button_inline": true
}
{% if form %}
<div class="uol-form__container {{ 'uol-form-container--centered' if form.form_centered }} {{ 'uol-form__container--with-image' if form.img.src }} {{ 'uol-form-container--overflow' if form.overflow }}">
<div class="uol-form__inner-wrapper">
{% if form.title %}
<{{ form.heading_level if form.heading_level else 'h2' }} class="uol-form__title">{{ form.title }}</{{ form.heading_level if form.heading_level else 'h2' }}>
{% endif %}
{% if form.lead %}
<div class="uol-form__lead"><p>{{ form.lead | safe }}</p></div>
{% endif %}
{% if form.additional_info_before %}
<div class="uol-rich-text">
<div class="uol-form__additional-content uol-form__additional-content--before">
{{ form.additional_info_before | safe }}
</div>
</div>
{% endif %}
{% if form.form_error %}
{% render '@uol-form-error-msg', { form_error: form.form_error, form_error_id: form.form_error_id } %}
{% endif %}
<form class="uol-form" action="{{ form.action }}"
{% for field in form.fields %}
{{ 'role=search' if field.type == 'search' }}
{% endfor %}>
<div class="uol-form__input-group {{ 'uol-form__input-group--inline' if form.form_group.inline_fields else 'uol-form__input-group--block' }}">
{% if form.form_group %}
{% for field in form.form_group.fields %}
{% render '@uol-form-input', field %}
{% endfor %}
{% endif %}
</div>
<div class="{{ 'uol-form--button-inline' if form.button_inline else 'uol-form--button-block' }}">
<div class="uol-form__inputs-wrapper">
{% block formContent %}
{% for field in form.fields %}
{% render '@uol-form-input', field %}
{% endfor %}
{% endblock %}
</div>
{% if form.additional_info_before_submit_button %}
<div class="uol-rich-text">
<div class="uol-form__additional-content">
{{ form.additional_info_before_submit_button | safe }}
</div>
</div>
{% endif %}
{% if form.button %}
<div class="uol-form__button-wrapper">
{% render '@uol-button', form.button %}
</div>
{% endif %}
</div>
</form>
{% if form.additional_info_after %}
<div class="uol-rich-text">
<div class="uol-form__additional-content uol-form__additional-content--after">
{{ form.additional_info_after | safe }}
</div>
</div>
{% endif %}
</div>
{% if form.img.src %}
<figure class="uol-form__img-wrapper">
<img class="uol-form__img" src="{{ form.img.src }}" alt="{{ form.img.alt if form.img.alt else null }}">
</figure>
{% endif %}
</div>
{% endif %}
<div class="uol-form__container ">
<div class="uol-form__inner-wrapper">
<h2 class="uol-form__title">Form with lead</h2>
<div class="uol-form__lead">
<p>This is an optional lead sentence lorem ipsum dolor sit amet. consectetur adipiscing elit. Fusce in dui eleifend tortor gravida venenatis at non ligula. Donec consectetur ligula at velit feugiat, ac posuere elit luctus.</p>
</div>
<form class="uol-form" action="/example-form-action">
<div class="uol-form__input-group uol-form__input-group--block">
</div>
<div class="uol-form--button-block">
<div class="uol-form__inputs-wrapper">
</div>
</div>
</form>
</div>
</div>
.uol-form-container--centered {
@extend .uol-col;
@extend .uol-col-m-10;
@extend .uol-col-xl-8;
margin: 0 auto;
}
.uol-form__container {
border: 1px solid $color-border--light;
border-radius: 6px;
margin-bottom: $spacing-6;
&.uol-form-container--centered {
padding: 0;
}
.uol-side-nav-container--populated + .uol-homepage-content & {
.uol-form__inner-wrapper {
@include media(">=uol-media-l") {
flex-basis: 100%;
}
@include media(">=uol-media-xl") {
flex-basis: 55.555%;
}
}
.uol-form {
@include media(">=uol-media-xl") {
margin-right: $spacing-6;
}
}
.uol-form__img-wrapper {
display: none;
@include media(">=uol-media-xl") {
display: inline-flex;
flex-basis: 44.444%;
}
}
}
}
.uol-form__inner-wrapper {
padding: $spacing-5 $spacing-4 $spacing-6;
background-color: $color-grey--light;
@include media(">=uol-media-l") {
flex-basis: 58.333%;
padding: 2.5rem $spacing-6;
}
@include media(">=uol-media-xl") {
flex-basis: 50%;
}
/*
Note:
As element uses typography rich text, each paragraph element has spacing underneath
Here, the element is at the bottom of the form so we force last paragraph element
to have zero spacing
*/
p:last-child {
margin-bottom: 0 !important;
}
}
/*
Note:
Fix so blue line is in correct place for form group inputs
*/
.uol-form__input-group--inline {
.uol-form__input-wrapper:before {
bottom: 1px;
}
}
.uol-form__title {
color: $color-font;
font-size: 2rem;
line-height: 1.25;
font-family: $font-family-serif;
margin: 0;
padding-bottom: $spacing-2;
+ .uol-form {
padding-top: $spacing-2;
}
@include media(">=uol-media-m") {
font-size: 2.25rem;
line-height: 1.333;
}
@include media(">=uol-media-l") {
font-size: 2.625rem;
line-height: 1.238;
}
}
.uol-form__lead {
display: block;
color: $color-font;
font-size: 1.125rem;
line-height: 1.556;
font-family: $font-family-sans-serif;
margin: 0 0 $spacing-6;
font-weight: normal;
// @include media(">=uol-media-s") {
// max-width: 31.5rem;
// }
@include media(">=uol-media-m") {
max-width: 32rem;
}
@include media(">=uol-media-l") {
font-size: 1.25rem;
max-width: 41rem;
}
}
.uol-form {
flex-direction: row;
.uol-form--button-inline {
@include media(">=uol-media-m") {
display: flex;
}
}
.uol-form__input-group {
display: flex;
flex-wrap: wrap;
@include media(">=uol-media-m") {
column-gap: $spacing-4;
}
@include media(">=uol-media-l") {
column-gap: $spacing-5;
}
@include media(">=uol-media-xl") {
column-gap: $spacing-6;
}
.uol-form__input-container {
width: 100%;
@include media(">=uol-media-m") {
width: calc(50% - #{$spacing-2});
}
@include media(">=uol-media-l") {
width: calc(50% - #{$spacing-3});
}
@include media(">=uol-media-l") {
width: calc(50% - #{$spacing-4});
}
.uol-form__input-wrapper {
max-width: none;
}
}
}
.uol-form__input-group--inline {
display: flex;
flex-direction: column;
@include media(">=uol-media-m") {
flex-direction: row;
}
}
}
.uol-form__container--with-image {
@include media(">=uol-media-l") {
display: flex;
}
}
.uol-form__img-wrapper {
background-color: $color-grey--light;
position: relative;
display: none;
z-index: -2;
overflow: hidden;
@include media(">=uol-media-l") {
display: inline-flex;
flex-basis: 41.666%;
}
@include media(">=uol-media-xl") {
flex-basis: 50%;
}
}
.uol-form__img {
position: absolute;
min-width: 100%;
min-height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: -1;
}
.uol-form--button-inline {
.uol-form__inputs-wrapper {
flex: 1;
}
.uol-form__input-container {
margin-bottom: 0;
}
.uol-form__button-wrapper {
align-self: flex-end;
.uol-button {
@include button_focus(-6px);
}
@include media(">=uol-media-m") {
padding-left: $spacing-4;
}
@include media(">=uol-media-l") {
padding-left: $spacing-5;
}
@include media(">=uol-media-xl") {
padding-left: $spacing-6;
}
[class^="uol-button"] {
width: 100%;
@include media(">=uol-media-s") {
width: inherit;
}
height: 3.125rem;
line-height: 0.75;
}
}
}
.uol-form__button-wrapper {
.uol-form--button-block & {
@include media(">=uol-media-s") {
display: inline-block;
width: initial;
}
.uol-button {
width: 100%;
}
}
}
.uol-form__additional-content {
padding: 0;
margin: 0;
a {
@include link_focus();
}
}
.uol-form__additional-content--before {
.uol-rich-text & {
margin: $spacing-4 0;
> * {
margin-bottom: $spacing-4;
}
> *:last-child {
margin-bottom: $spacing-6;
}
}
}
.uol-form__additional-content--after {
.uol-rich-text & {
margin: $spacing-6 0 0;
> * {
margin-bottom: $spacing-4;
}
> *:last-child {
margin-bottom: 0;
}
}
}
// TODO: refactor this file
.uol-form__inner-wrapper {
.uol-form__custom-fieldset {
// Fiddle to allow container for buttons to extend over search button
@include media(">=uol-media-m") {
width: calc(100% + 160px + 16px);
}
@include media(">=uol-media-l") {
width: calc(100% + 160px + 24px);
}
}
.uol-form__custom__legend {
margin: 0 0 $spacing-3;
}
.uol-form__input-label {
padding-bottom: $spacing-3;
}
}
import { mdiConsoleNetwork } from "@mdi/js";
export const createAriaLiveRegion = () => {
if (document.querySelector('.uol-form__container')) {
const formContainer = document.querySelector('.uol-form__container');
if (!formContainer.querySelector('.uol-form__announcement')) {
const formAnnouncement = document.createElement('div');
formAnnouncement.setAttribute('aria-live', 'polite')
formAnnouncement.classList.add('uol-form__announcement')
formAnnouncement.classList.add('hide-accessible')
formContainer.appendChild(formAnnouncement);
}
}
}
export const preValidationChecks = () => {
const submitBtn = document.querySelector('[type="submit"]');
const passwordFields = document.querySelectorAll('.uol-form__input--password');
const toggleBtn = document.querySelector('.uol-form__input--password-toggle');
const checkboxGroups = document.querySelectorAll('[role="group"]');
if (submitBtn) {
submitBtn.addEventListener('click', () => {
if (passwordFields && submitBtn) {
passwordFields.forEach( (input) => {
if (input.getAttribute('type') == 'text') {
input.setAttribute('type', 'password');
toggleBtn.setAttribute('data-password-visible', false)
}
})
}
if (checkboxGroups) {
checkboxGroups.forEach( (group) => {
if (group.hasAttribute('data-checkboxes-required')) {
const numRequired = group.getAttribute('data-checkboxes-required');
const totalChecked = group.querySelectorAll("input:checked").length;
if (totalChecked >= numRequired) {
group.setAttribute('data-checkbox-group-invalid', false)
} else {
group.setAttribute('data-checkbox-group-invalid', true)
}
}
})
}
})
}
}
/*
This function changes the type of search input in a form based on radio button selection
It initially loads both inputs and then either shows the first input (on load)
Or changes the input type based on radio button selection
*/
export const formButtonInputSwitch = () => {
// Is iOS device check
const isIOS = () => {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
||
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
}
const isAndroid = () => {
const ua = navigator.userAgent.toLowerCase();
return ua.indexOf("android") > -1;
}
const iOS = isIOS();
const androidDevice = isAndroid();
const firefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
// If in form container
if (document.querySelector('.uol-form__container')) {
const customFieldSet = document.querySelector('.uol-form__custom-fieldset');
// The following attribute set if config has "changeInputType": true
if (customFieldSet && customFieldSet.hasAttribute("changeInputType")) {
// Loop through each radio button
document.querySelectorAll('.uol-form__input--radio').forEach((elem) => {
// Add event listener on to each radio
elem.addEventListener("change", function(event) {
// Hide all inputs when a change made
document.querySelectorAll('.uol-form__input-container--search').forEach((elem) => {
elem.style.display = 'none';
});
/*
in config, "changeInputTo" set to be "standard" or "singleTypeahead"
This value set as attribute
*/
const changeInputTo = event.target.getAttribute("changeInputTo");
const searchLabel = event.target.getAttribute("searchlabel");
let showInputId = event.target.getAttribute("showsearchid");
/*
Initial id of typeahead appended with -js-input-0
We want to target these id and then hide the parent (closest) container
Only run for browser which typeahead is available (not firefox android or IOS currently)
*/
if (!iOS && !androidDevice && !firefox) {
if (changeInputTo == "singleTypeahead") {
showInputId += "-js-input-0";
}
// Show parent container of input containing our id.
const containerElement = document.getElementById(showInputId).closest('.uol-form__input-container--search');
containerElement.style.display = "block";
} else {
// Always show search form for firefox, IOS and android
document.querySelector('.uol-form__input-container--search').style.display = "block";
}
// const searchLabel = document.querySelector('.uol-form__input-label');
// console.log("Changed" + searchLabel.innerHTML);
const nodeList = document.querySelectorAll(".uol-form__input-label__text");
for (let i = 0; i < nodeList.length; i++) {
nodeList[i].innerHTML = searchLabel;
}
});
});
// initially hide all inputs apart from the first one
document.querySelectorAll('.uol-form__input-container--search').forEach((elem, count) => {
if (count > 0) elem.style.display = 'none';
});
}
}
}
{
"form": {
"action": "/example-form-action",
"title": "Form with lead",
"heading_level": "h2",
"lead": "This is an optional lead sentence lorem ipsum dolor sit amet. consectetur adipiscing elit. Fusce in dui eleifend tortor gravida venenatis at non ligula. Donec consectetur ligula at velit feugiat, ac posuere elit luctus."
}
}