Where a form collects user data it is necessary to provide the correct autocomplete name - value pair, where it exists in HTML 5.2’s specification, in order to satisfy Success Criterion 1.3.5 Identify Input purpose - Level AA.
The complete list of autocomplete values is available at MDN: The HTML autocomplete attribute and in every instance where an input is collecting personal data about an individual, including work etc, these attributes need to be set to the correct value, as set out on MDN. This enables users that either rely on autofill in the browser, on their operating system or by using specialist software to fill in a form using their personal data. Users who do not wish to use this, can enter the data manually, using whichever method they would ordinarily use.
Where a value is not listed in MDN’s reference, omit the value and this will be programmatically set to ‘off’ and browsers/software that obey the ‘off’ value will not autopopulate that field.
When a field is required, ‘required’ must be set to ‘true’. This will append the text “required” to the label and implement basic required field validation.
These should only be used where the expected length of the input data is absolute, for example, if a student ID is always 9 characters. Do not use for names, addresses or any other type of data where restricting the length may cause for users entering legitimate data.
Use only when the input data is required to meet a certain pattern, do not use on names, addresses or other fields where variable lengths or patterns may legitimately be entered.
For numeric fields other than telephone numbers use {‘type’: ‘text’} and {‘inputmode’: ‘numeric’}. This allows the use of numeric keyboards where available eg. on mobile devices.
Usage notes for the various input fields used in the design system
Where user inputs have a corresponding text label, the label and input need to be linked for accessibility compliance. Set the label ‘for’ tag value to be the same as the id of the corresponding input. Eg:
<label for="Search input">Enter search term</label>
<input id="Search input">...
Use input {‘type’: ‘text’} for collecting user data that contains alphabetical characters, alphanumeric characters or most numeric characters.
Exceptions: if collecting:
For all other text-based inputs, use {‘type’: ‘text’}. The design system does not support type = number or type = date at this stage, due to significant accessibility issues with user agents.
Do not set constraint validations for names and addresses etc. Names can range from 1 character to an unknown length. Only validate that the ‘required’ constraint has been met.
When collecting user data, it is necessary to supply the correct autocomplete value in the config data, please refer to the autocomplete section, for a detailed explanation.
Use {‘type’: ‘url’} when collecting a web address
Set {‘autocomplete’: ‘url’} in the config data, if the input is intended to collect a user’s own URL, for example the web address for the organisation they work at or their personal site. If it is for URL that is not related to the user, omit this key-value pair and only use {‘type’: ‘url’}.
Use {‘type’: ‘tel’} when collecting a phone number.
Set {‘autocomplete’: ‘tel’} in the config data if the input is to collect a user’s own phone number.
Use {‘type’: ‘email’} when collecting an email address
Set {‘autocomplete’: ‘email’} in the config data if the input is to collect a user’s own email address.
Use {‘type’: ‘password’} when allowing a user to login with an existing password or creating a new password
The password field has a toggle functionality button, which assists users in ensuring the password they have entered is correct and they can easily check this at any stage. For security purposes, on form submission, the password field will mask the characters, before submitting the form, this is to ensure password managers and browser autocompletes do not save plain text characters. Toggling the password visibility is announced to users of screen readers, via an aria-live region. The aria-live region is automatically rendered when needed.
If the input is for creating a new password, set {‘autocomplete’: ‘new-password’}. If it is for an existing password, set {‘autocomplete’: ‘current-password’}.
Where passwords are required to meet certain requirements, provide these requirements in place of a hint by adding each requirement to the ‘requirements’ array in the config data. eg:
{
'type': 'password',
'label': 'Password',
'requirements': [
{
'item': 'Must be at least 8 characters'
},
{
'item': 'Must contain at least 1 uppercase letter'
}
],
...
}
Each item will be visually listed above the input and they will be read out to users of screen readers upon the field receiving focus.
Use {‘type’: ‘time’} when collecting a time, omit the ‘autocomplete’ value for this input
When using multiple checkboxes, it is necessary to provide an ID for the grouping element, in order to provide the accessible name for the group. The value of ‘group_label_id’ should be unique within the page
When using multiple checkboxes where a user is required to check 1 or more, there is no existing HTML validation for this and this will need to be implemented server-side.
The value of ‘num_required’ should be set in the config data (e.g if at least 2 are required, set the value to 2), which will provide the necessary HTML data attribute to test validation against, with server-side logic.
{
'type': 'checkbox',
'legend': 'Checkboxes',
'num_required': 2,
...
}
When using radio buttons, it is necessary to provide an ID for the grouping element, in order to provide the accessible name. The value of ‘group_label_id’ should be unique within the page.
If a radio group requires a user to make a selection, set the value of ‘required’ to ‘true’, this will automatically check the first item.
{
'type': 'radio',
'legend': 'Radios',
'hint': 'Select one option.',
'required': true,
...
}
When implementing groups of radio buttons, aim to ensure that the first radio is for the least consequential commitment if possible, to avoid users inadvertently committing to something where they may have missed the other options.
Use where a single option is required or requested from the user and always use in place of radio buttons when there are more than 7 options.
A Select component will automatically be converted to a Typeahead component with JavaScript and will enhance the functionality for users, If the Typeahead functionality is not required for a list of options and there is good reason not to use it, it is possible to prevent the Select becoming a Typeahead by adding the following {'native_select': true}
to the configuration data, for the Select input, example:
{
'type': 'select',
'native_select': true,
'id': 'selectId',
'name': 'selectName',
'label': 'Select a country',
...
},
Working example: @uol-form-input–select
The Typeahead component is a JavaScript enhancement (where available) of the Select element. No additional configuration is required as JavaScript will visually and accessibly hide the Select input and add the Typeahead input in its place, with its additional functionality. A Typeahead allows a user to type within the input to filter through the visible options within the options panel or use a keyboard/mouse or any combination. Any selection made by a user will also become the value of the hidden original Select input.
Working example: @uol-form-input–select–typeahead
The user input for this component can be captured using the name
and value
attributes for the {‘type’: ‘select’} input, in the normal way. Any values marked as “selected”: true; Example:
{
'type': 'select',
'id': 'selectId',
'name': 'selectName',
'label': 'Cheese list',
'hint': 'Select a cheese',
'options': [
{"label": "Brie", "value": "BRI"},
{"label": "Cashel Blue", "value": "CBL"},
...
},
Use where multiple options are required or requested from the user and always use in place of checkboxes when there are more than 7 options.
A Multiselect component will automatically be converted to a Multiselect Typeahead component with JavaScript and will enhance the functionality for users, If the Typeahead functionality is not required for a list of options and there is good reason not to use it, it is possible to prevent the Multiselect becoming a Multiselect Typeahead by adding the following {'native_select': true}
to the configuration data, for the Multiselect input. Any values with “selected” true will be pre-selected. Example:
{
'type': 'select',
'label': 'Label',
'id': 'selectID2',
'name': 'selectName2',
'hint': 'Hint text',
'options': [
{"label": "Brie", "value": "BRI", "selected": true},
{"label": "Cashel Blue", "value": "CBL"},
{"label": "Cheddar", "value": "CHE", "selected": true},
...
},
Working example: @uol-form-input–multiselect
The Multiselect Typeahead component is a JavaScript enhancement (where available) of the Multiselect element. No additional configuration is required as JavaScript will visually and accessibly hide the Multiselect input and add the Multiselect Typeahead input in its place, with its additional functionality. A Multiselect Typeahead allows a user to type within the input to filter through the visible options within the options panel or use a keyboard/mouse or any combination. Any selection made by a user will also become the value of the hidden original Select input.
Working example: @uol-form-input–multiselect–typeahead
The user input for this component can be captured using the name
and value
attributes for the {‘type’: ‘select’} input, in the normal way. Example:
{
'type': 'select',
'label': 'Label',
'id': 'selectID3',
'name': 'selectName3',
'hint': 'Hint text',
...
},
The Multiselect Preselected component is a JavaScript enhancement (where available) of the Multiselect element. No additional configuration is required as JavaScript will visually and accessibly hide the Multiselect input and add the Multiselect Preselected input in its place, with its additional functionality. A Multiselect Preselected component allows for particular chips/select items (where specified) to be preselected in advance for the user.
Working example: @uol-form-input–multiselect–preselected
A chip/select item can be preselected by setting {“selected” : true} on an item, Example:
{
"label": "Basketball",
"value": "BK",
"selected": true
},
Textareas have 3 sizes, which are set with CSS by the expected or permitted length of the input’s value. The ‘maxlength’ key should be set to an appropriate value and the textarea will be sized appropriately. There is no functionality to hard limit the number of characters a user enters in a textarea. Consider adding the character count component, by setting ‘char_count’ to ‘true’ and ‘maxlength’ to the desired character limit in the config data. Eg:
{
'type': 'textarea',
'id': 'tAreaSm',
'name': 'tarea1',
'label': 'Event description (small)',
'char_count': true,
'maxlength': 100,
...
},
This will alert users via visual cues, a counter message and a visually hidden message for users of screen readers, that the length of characters has been exceeded.
Use {‘type’: ‘inputs-inline’} when collecting data that is a date or split fields. set the maxlength on each input to the expected number of characters and the CSS will size it accordingly. The CSS will size the input for 2, 3 or 4 characters. Please view the following example.
When using inline inputs, it is necessary to provide an ID for the grouping element, in order to provide the accessible name. The value of ‘group_label_id’ should be unique, within the page.
When requesting a date, set the ‘inputmode’ on each input to ‘numeric’ If asking for a user’s date of birth, it will be necessary to set the ‘autocomplete’ values on each input to:
Do not use this component where breaking up strings may differ from how a user memorises strings. For example: do not use for a National Insurance Number, as individual inputs in the format AB 12 34 56 C, may not match the way a user recalls the string and could cause additional cognitive overheads: Use a {‘type’: ‘text’} field instead.
Use { "type": "file" }
to add a file upload input.
The “multiple” boolean can be set to true { "multiple": true }
to accept multiple files from a single input. However, if a fixed number of individual files with different purposes is expected it may be more appropriate to provide multiple single file inputs.
You can limit the acceptable file types using a comma-separated list of unique file type specifiers, eg. {"accept": "image/png, image/jpeg"}
.
For details of file type specifiers, see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
The Datepicker is a JavaScript (JS) dependency and will only be available for users that have JS enabled. Users without JS enabled or users that choose not to interact with the Datepicker calendar widget can manually enter their date(s) in the input field(s).
A Datepicker should be used when we wish to collect dates from a user that they may not already know. An example would be booking a date for an event, where it would be helpful for the user to be able to select date(s) whilst being able to determine the day(s) of the week.
A Datepicker may also be used where there is a need to make some dates unavailable, as an example, if it would not be possible to book an event/meeting on the 25th December, the Datepicker can be configured to disallow specific dates, however, this should be used with caution, read Unavailable dates for further considerations.
A Datepicker should not be used where the date would be one that the user is expected to already know, such as their date of birth or the date of an event that has passed. There is little value in providing a Datepicker for historic dates, as in most instances, the day of the week will be irrelevant and this data could be captured without adding an extra layer of complexity by using @uol-form-input–inline-inputs-date instead.
A Datepicker must be used within a Form, to correctly capture the user’s data and to ensure the visual styling meets the designers’ expectations.
Within the Form Config data, the following basic properties and values should be set to implement a Datepicker
'form': {
'type': 'datepicker',
'id': 'UNIQUE_ID',
...
}
There are several configuration options which can also be set in the Form Config:
The Single Date Datepicker allows a user to select a single date from the Datepicker widget, which will populate the input with their selected date, should they make a selection within the calendar modal and confirm by selecting the Confirm button.
The following basic configuration data is required, within the Form Config, for a Single Date Datepicker:
'form': {
'type': 'datepicker',
'id': 'UNIQUE_ID',
'isDateRange': false,
'value': '',
...
}
The isDateRange
property should be set to false
, to display the Single Date Datepicker, setting this value to true
would display the Date range Datepicker, which does not allow a single date.
The value
property sets the value of the input if a date is passed in with the response object, in the format DD/MM/YYYY e.g. 25/12/2022 and will populate the input should a page reload occur if there is an error in the form.
For capturing the input’s value, the ID for the input takes the id
property and concatenates the value string with ‘-start-date’. As an example, if the value of the id
property is datepicker1
, the ID for the input would be datepicker1-start-date
.
Capturing and populating on page reload example:
'form': {
'type': 'datepicker',
'id': 'datepicker-id-1',
...
'isDateRange': false,
'value': '11/01/2022' // Will populate the input on reload
to capture the input, using the supplied ID for the Datepicker widget, the ID of the input would appear like so:
Datepicker date input: <input type="text" id="datepicker-id-1-start-date">
The Date Range Picker allows a user to set both a start and end date, thus creating a range. An example for usage of this would be where a user wanted to book a conference that spans more than one day, for instance, a conference that may start on a Monday and end on a Friday.
The following basic configuration data is required, within the Form config file:
'form': {
'type': 'datepicker',
'id': 'UNIQUE_ID',
'start_value': '',
'end_value': '',
'isDateRange': true,
...
}
The isDateRange
property’s value must be set to true
to provide users with the Date Range Picker.
2 inputs are provided when a Date Range Picker is set, although visually these appear to look like 1 unified input. The property start_value
can be set to populate the Start Date input field on a page reload, using the format DD/MM/YYYY e.g. 11/01/2022. The end_value
property populates the End Date using the same formats.
To capture user input on either the Start Date field or the End Date field the IDs are generated based upon the value of the id
, using string concatenation. As an example, if the id
has the value ‘datepicker2’, the inputs’ IDs become datepicker2-start-date
and datepicker2-end-date
for the Start Date and End Date, respectively.
implementation example:
'form': {
'type': 'datepicker',
'id': 'UNIQUE_ID',
...
'isDateRange': true
}
Capturing and populating on page reload example:
'form': {
'type': 'datepicker',
'id': 'datepicker-id-2',
...
'isDateRange': true,
'start_value': '11/01/2022', // Will populate the start input on reload
'end_value': '14/01/2022', // Will populate the end input on reload
}
to capture the inputs, using the supplied ID, the IDs of the inputs would appear like so:
<input type="text" id="datepicker-id-2-start-date">
<input type="text" id="datepicker-id-2-end-date">
Should there be a need to prevent a user selecting a specific day, unavailable_dates
can be set in the Form Config data. There is an added layer of complexity when setting unavailable_dates
, particularly when using the Date Range Picker. A user cannot use the picker to select a Start Date or and End Date that is set as unavailable from within the calendar, however, it is entirely possible for an unavailable date to be within the range of dates. As an example, Wednesday the 3rd of June may be set as unavailable, a user could create an event, starting on Monday the 1st and ending on Friday the 5th and this would valid, as neither the Start Date or End Date would be invalid. This would likely create confusion for users, as they may think creating a range that has an unavailable day is invalid and that may actually be the case.
The Single Date Datepicker can handle an unavailable date in a more user-friendly fashion, as if the user uses the picker, it is not possible to select an unavailable date.
Both varaints come with a caveat, in that it is entirely possible to bypass the Datepicker and put any date in the input(s) and as there is no client side validation on the inputs, an error would need to be sent back from the server. Not everybody would be comfortable using a Datepicker, as an example, a user that relies upon voice input, some screen reader users and people who disable JS, if those users or any others decide not to use the Datepicker, they would have no knowledge of unavailable dates, in advance. Unavailable dates should therefore be used with caution, as they can add additional cognitive load to some users ans some users may abandon their task, if it’s not clear which dates cannot be selected, without using the calendar widget.
unavailable_dates
is an array and accepts dates in ISO format (‘YYYY-YY-DD’) delimited with comma + space , e.g. `[‘2022-12-25’, ‘2022-12-26’, ‘2022-12-31’ … ].
implementation example:
'form': {
'type': 'datepicker',
'id': 'UNIQUE_ID',
'value': '',
'isDateRange': false,
...
'unavailable_dates': ['2022-12-25', '2022-12-26', '2022-12-31']
}
In most instances, it is not advisable to use a Datepicker for historic dates as these dates can be better captured using the {'type': 'inputs_inline'}
inputs instead. If the expected user input is to be a date or dates that are not in the past, then the following settings may be set in the Form Config data. this prevents a user from selecting any date that preceeds the current date:
'form': {
'type': 'datepicker',
'id': 'UNIQUE_ID',
...
'future_dates_only': true
}
If the Datepicker is a required field, then required
should be set to true
in the Form Config file. If this value is set for a Single Date Datepicker, the required
attribute is programmatically set to the HTML input field. If the Datepicker is a Date Range Picker, setting required
to true
will apply the required
HTML attribute to both the Start and End inputs, as 1 input cannot be required without the other, as a Single Date Datepicker should be used in those circumstances.
implementation example:
‘form’: { ‘type’: ‘datepicker’, ‘id’: ‘UNIQUE_ID’, … ‘required’: true }
#### Errors
As with all errors that are sent back in from the server, the errors must be specific and useful to the user, so they help them quickly identify the problem and explain how to resolve it.
Some examples of helpful error messages for the Datepicker are:
- This field is required
- Please enter a date in the valid format DD/MM/YYYY, e.g. 25/12/2022
- The 25/12/2022 is unavailable, please select another date
- The end date cannot be before the start date (this is only possible by bypassing the Datepicker, but still possible)
- The start date cannot be in the past, please enter a date that has not yet occurred
Implementation example:
‘form’: { ‘type’: ‘datepicker’, ‘id’: ‘datepickerId4’, ‘value’: ‘25/12/2022, … ‘error’: ‘The 25th December 2022 is unavailable, please select another date’ }
### File upload
Use `{ "type": "file" }` to add a file upload input.
The "multiple" boolean can be set to true `{ "multiple": true }`to accept multiple files from a single input. However, if a fixed number of individual files with different purposes is expected it may be more appropriate to provide multiple single file inputs.
You can limit the acceptable file types using a comma-separated list of unique file type specifiers, eg. `{"accept": "image/png, image/jpeg"}`.
For details of file type specifiers, see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers
## Inputs additional features
### Fieldset
The fieldset component groups logically related sections of a form in an accessible manner. For larger forms, there may be a section where it makes sense to group the elements together, as they form a distinct section of the form.
The fieldset exposes itself as a group to screen readers, so it should be used only when necessary. An entire form should not be wrapped in a fieldset, every section of a form should not ordinarily use a fieldset, as this may create additional 'noise' for screen reader users and in some circumstances cause confusion. If, however, the form is complex and is not spread over multiple pages and there are distinct sections, it may make sense to group these sections, to assist users in task completion, but consider whether it is appropriate to group all sections.<br>
Example:<br>
{ ‘type’: ‘fieldset’, ‘fieldset’: { ‘title’: ‘Group title’, ‘sub_title’: ‘Optional subtitle’, ‘fields’: [ { ‘type’: ‘textarea’, ‘id’: ‘tAreaSm’, … } … } }
### Input widths
Inputs should have a sensible width, which both matches the expected length of user input data and meets the design guidance. There are 4 widths of inputs, which can be set by setting the value of {'input_width': 'VALUE'} to one of the following appropriate values:
- 'large'
- 'medium'
- 'small'
- 'x-small'
### Input errors
Input errors need to be specific to the actual error for their related input field.<br>
Example:<br>
If a required field has not been filled out, the message would need to read "First name is required" or words to that effect.
If a field does not meet other validation constraints, such as a 'pattern' or 'length', then the error message must be specific enough to help a user easily understand the problem, for example "Your student ID must be 9 characters in length and all numbers"
Individual inputs provide visual cues that a field has an issue that requires attention. The design system uses thicker red borders, programmatically associated error messages and red focus styles to communicate a field is invalid.
{ ‘type’: ‘text’, ‘label’: ‘Student ID number’, ‘required’: true, ‘maxlength’: 9, ‘pattern’: ‘[0-9]{9}’, ‘invalid’: true, ‘error’: ‘Student ID must be 9 characters’, … },
### Hint text
Hint text should be concise and specific, it should where necessary provide the expected format of the data or details on where to locate the information, an example "Your student ID is 9 characters in length and they are all numbers, this ID is available in your welcome email"
### Form errors
Form errors, rather than input error described above, should use the form error functionality on the @uol-form component.
## Related reading
- [MDN inputmode](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode)
{% extends '@uol-form-input-container' %}
{% block input %}
{% if type == 'fieldset' %}
<fieldset class="uol-form__input--fieldset">
<legend class="uol-form__input--legend">
<{{ fieldset.heading_level if fieldset.heading_level else 'h3' }} class="uol-form__input--legend-title">{{
fieldset.title | safe }}</{{ fieldset.heading_level if fieldset.heading_level else 'h3' }}>
{% if fieldset.sub_title %}<span class="uol-form__input--legend-subtitle">{{ fieldset.sub_title | safe }}</span>{% endif %}
</legend>
{% for field in fieldset.fields %}
{% render '@uol-form-input', field %}
{% endfor %}
</fieldset>
{% elseif type == 'radio' %}
<div role="radiogroup"
aria-labelledby="{{ group_label_id }}{% if hint %} {{ group_label_id }}-hint{% endif %}{% if error %} {{ group_label_id }}-error{% endif %}"
class="uol-form__custom-fieldset"
{% if changeInputType %} changeInputType {% endif %}
{% if required %} aria-required="true" {% endif %}
>
<span id="{{ group_label_id }}" class="uol-form__custom__legend">{{ legend | safe }}{% if required %} (required){% endif %}</span>
{% if hint %}
<span class="uol-form__input-label__hint" id="{{ group_label_id }}-hint">{{ hint | safe }} </span>
{% endif %}
{% if error %}
{% render '@uol-form-input-error-msg', { error: error, id: group_label_id } %}
{% endif %}
<div class="uol-form__inputs-wrapper {{ 'uol-form__inputs-wrapper--inline' if group_inline }}">
{% for option in options %}
<div class="uol-form__input--radio-wrapper">
<input class="uol-form__input--radio" type="radio" id="{{ option.id }}" name="{{ option.name }}"
value="{{ option.value }}"
{{ 'checked' if option.checked or (required and loop.index==1) }}
{% if option.changeInputTo %}
changeInputTo="{{ option.changeInputTo }}"
showSearchId="{{ option.showSearchId }}"
hideSearchId="{{ option.hideSearchId }}"
searchLabel="{{ option.searchLabel }}"
{% endif %}
>
<label class="uol-form__input--radio__label" for="{{ option.id }}">{{ option.label }}</label>
<span class="uol-form__input--custom-radio" hidden>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="12" />
</svg>
</span>
</div>
{% endfor %}
</div>
</div>
{% elseif type == 'checkbox' %}
{% if options.length > 1 %}
<div role="group"
aria-labelledby="{{ group_label_id }}{% if hint %} {{ group_label_id }}-hint{% endif %}{% if error %} {{ group_label_id }}-error{% endif %}"
class="uol-form__custom-fieldset"
{% if required %} aria-required="true" {% endif %}
{% if num_required %} data-checkboxes-required="{{ num_required }}" {% endif %}>
<span id="{{ group_label_id }}" class="uol-form__custom__legend">{{ legend | safe }}{% if required %} (required){% endif %}</span>
{% if hint %}
<span class="uol-form__input-label__hint" id="{{ group_label_id }}-hint">{{ hint | safe }} </span>
{% endif %}
{% if error %}
{% render '@uol-form-input-error-msg', { error: error, id: group_label_id } %}
{% endif %}
{% endif %}
{% for option in options %}
{% if checkbox_link %}
<span class="uol-rich-text uol-form__input--checkbox-link">
<a href="{{ checkbox_link.url }}">{{ checkbox_link.text }}</a>
</span>
{% endif %}
{% if options.length == 1 %}
{% if option.error %}
{% render '@uol-form-input-error-msg', { error: option.error, id: option.id } %}
{% endif %}
{% endif %}
<div class="uol-form__input--checkbox-wrapper">
<input class="uol-form__input--checkbox"
type="checkbox"
id="{{ option.id }}"
name="{{ option.name }}"
value="{{ value }}"
{{ 'checked' if option.checked}}
{{ 'required' if option.required}}
{% if option.error %} aria-labelledby="{{ option.id }}-error {{ option.id }}-label" {% endif %}
{% if option.invalid %} aria-invalid="{{ option.invalid }}" {% endif %}>
<label class="uol-form__input--checkbox-label" for="{{ option.id }}" id="{{ option.id }}-label">{{ option.label | safe }}</label>
<span class="uol-form__input--checkbox-custom">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="38" height="38" focusable="false" aria-hidden="true">
<path fill="#000" fill-rule="nonzero" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path>
</svg>
</span>
</div>
{% endfor %}
{% if options.length > 1 %}
</div>
{% endif %}
{% elseif type == 'select' %}
<label class="uol-form__input-label" for="{{ id }}" id="{{ id }}-label">
<span class="uol-form__input-label__text">{{ label | safe }}{% if required %} (required){% endif %}</span>
{% if hint %}
<span class="uol-form__input-label__hint" id="{{ id }}-hint">{{ hint | safe }} </span>
{% endif %}
{% if error %}
{% render '@uol-form-input-error-msg', { error: error } %}
{% endif %}
</label>
<div class="uol-form__input-wrapper {{ 'uol-form__input-wrapper--search uol-form__input-wrapper--with-icon uol-form__input-wrapper--search-typeahead' if search_icon }}" {{ 'data-field-invalid=true' if invalid else 'data-field-invalid=false' }} >
<select class="uol-form__input uol-form__input--select {{ 'uol-form__input-container__sort-by' if sort_by }}"
name="{{ name }}"
id="{{ id }}"
aria-label="Select {{ label }}"
{{ 'data-native-select' if native_select }}
{{ 'required' if required}}
{% if invalid %} aria-invalid="{{ invalid }}" {% endif %}
{{ 'multiple ' if multiple }}
{{ 'data-chips-hide ' if chips_hide }}
>
<option value="" {% if not first-item-enabled %}disabled{% endif %}>Select an option</option>
{% for option in options %}
<option value="{{ option.value }}" {{ 'selected' if option.selected}}>{{ option.label | safe }}</option>
{% endfor %}
</select>
<svg class="uol-form__input__chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" focusable="false" aria-hidden="true">
<path fill="none" d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" />
</svg>
</div>
{% elseif type == 'textarea' %}
<label class="uol-form__input-label uol-form__input--teaxtarea-label" for="{{ id }}" id="{{ id }}-label">
<span class="uol-form__input-label__text">{{ label | safe }}{% if required %} (required){% endif %}</span>
{% if hint %}
<span class="uol-form__input-label__hint">{{ hint | safe }} </span>
{% endif %}
{% if error %}
{% render '@uol-form-input-error-msg', { error: error } %}
{% endif %}
</label>
<div class="uol-form__input--textarea-wrapper"
{{ 'data-field-invalid=true' if invalid else 'data-field-invalid=false' }}>
<textarea class="uol-form__input uol-form__input--textarea"
id="{{ id }}" name="{{ name }}"
data-character-input="true"
{{ 'required' if required }}
{% if invalid %} aria-invalid="{{ invalid }}" {%endif %}
data-textarea-height="{% if maxlength <= 100 %}small{% elseif maxlength > 200 %}large{% else %}medium{% endif %}"
{% if maxlength %} data-maxlength="{{ maxlength }}" {% endif %}
{% if char_count %} data-char-limit="true"
aria-labelledby="{{ id }}-label {{ id }}-char-count" {% endif %}>{{ value }}</textarea>
</div>
{% if char_count %}
{% render '@uol-form-input-char-count', { maxlength: maxlength, id: id } %}
{% endif %}
{% elseif type == 'inputs-inline' %}
<div class="uol-form__input__group-wrapper">
<div role="group" aria-labelledby="{{ group_label_id }}{% if hint %} {{ group_label_id }}-hint{% endif %}{% if error %} {{ group_label_id }}-error{% endif %}" class="uol-form__custom-fieldset">
<{{ heading_level if heading_level else 'h3' }} id="{{ group_label_id }}" class="uol-form__custom__legend">{{
legend | safe }}{% if required %} (required){% endif %}</{{ heading_level if heading_level else 'h3' }}>
{% if hint %}
<span class="uol-form__input-label__hint" id="{{ group_label_id }}-hint">{{ hint | safe }} </span>
{% endif %}
{% if error %}
{% render '@uol-form-input-error-msg', { error: error, id: group_label_id } %}
{% endif %}
<div class="uol-form__input--inline-wrapper">
{% for option in options %}
<div class="uol-form__input--inline-field-wrapper" {{ 'data-field-invalid=true' if option.invalid
else 'data-field-invalid=false' }}>
<label class="uol-form__input-label" for="{{ option.id }}">{{ option.label | safe }}</label>
<input class="uol-form__input uol-form__input--inline-field"
inputmode="{{ option.inputmode }}"
name="{{ option.name }}"
id="{{ option.id }}"
type="text"
{% if option.invalid %} aria-invalid="{{ option.invalid }}" {% endif %}
{% if option.pattern %} pattern="{{ option.pattern }}" {% endif %}
{% if option.minlength %} minlength="{{ option.minlength }}" {% endif %}
{% if option.autocomplete %} autocomplete="{{ option.autocomplete }}" {% endif %}
{% if option.maxlength %} maxlength="{{ option.maxlength }}" {% endif %}
{{ 'required' if option.required }}
value="{{ option.value }}">
</div>
{% endfor %}
</div>
</div>
</div>
{% elseif type == 'datepicker' %}
<div id="{{ id }}" class="uol-datepicker-container"
{{ 'data-future-dates-only' if future_dates_only }}
{{ 'data-range-selection' if isDateRange else 'data-single-selection' }}
{{ 'data-end-date=false' if isDateRange }}
data-start-date="false"
{% if unavailable_dates %} data-unavailable-dates="{% for date in unavailable_dates %}{{date}}{% if not loop.last%},{% endif %}{% endfor %}"{% endif %}
{{ 'data-invalid' if error }}>
{% if isDateRange %}
<div class="uol-datepicker__unified-input-wrapper" role="group" aria-labelledby="{{ id }}-group-label{% if error %} {{ id }}-error{% endif %}">
<h3 id="{{ id }}-group-label" class="hide-accessible">Date range, input start and end dates or select in the date picker</h3>
{% if error %}
{% render '@uol-form-input-error-msg', { error: error, id: id } %}
{% endif %}
<div class="uol-datepicker__labels-wrapper">
<label class="uol-datepicker__range-label uol-datepicker__range-label--start" for="{{ id }}-start-date">Start date<span class="hide-accessible"> format: dd/mm/yyyy</span></label>
<label class="uol-datepicker__range-label uol-datepicker__range-label--end" for="{{ id }}-end-date">End date<span class="hide-accessible"> format: dd/mm/yyyy</span></label>
</div>
<div class="uol-datepicker__unified-input">
<div class="uol-datepicker__input-wrapper--start">
<input class="uol-datepicker__input uol-datepicker__input--start" name="{{ start_name }}" type="text" placeholder="dd/mm/yyyy" id="{{ id }}-start-date" value="{{ start_value }}" {{ 'required' if required }} autocomplete="off">
</div>
<div class="uol-datepicker__input-wrapper--end">
<input class="uol-datepicker__input uol-datepicker__input--end" name="{{ end_name }}" type="text" placeholder="dd/mm/yyyy" id="{{ id }}-end-date" value="{{ end_value }}" {{ 'required' if required }} autocomplete="off">
</div>
</div>
</div>
{% else %}
<div class="uol-datepicker__input-group">
<label class="uol-datepicker__range-label" for="{{ id }}-start-date">Select date<span class="hide-accessible"> format: dd/mm/yyyy</span></label>
{% if error %}
{% render '@uol-form-input-error-msg', { error: error } %}
{% endif %}
<div class="uol-datepicker__controls-wrapper">
<div class="uol-datepicker__input-wrapper">
<input class="uol-datepicker__input uol-datepicker__input--start" name="{{ start_name }}" type="text" placeholder="dd/mm/yyyy" id="{{ id }}-start-date" value="{{ value }}" {{ 'required' if required }} autocomplete="off">
</div>
</div>
</div>
{% endif %}
</div>
{% else %}
<label class="uol-form__input-label" for="{{ id }}">
<span class="uol-form__input-label__text">{{ label | safe }}{% if required %} (required){% endif %}</span>
{% if hint %}
<span class="uol-form__input-label__hint">{{ hint | safe }} </span>
{% endif %}
{% if requirements %}
<span class="uol-form__input__requirements" aria-hidden="true">
{% for item in requirements %}
<span>{{ item.item }} </span>
{% endfor %}
</span>
<span class="hide-accessible">
{% for item in requirements %} {{ item.item }}, {% endfor %}
</span>
{% endif %}
{% if error %}
{% render '@uol-form-input-error-msg', { error: error } %}
{% endif %}
</label>
<div class="uol-form__input-wrapper
{% if type %} uol-form__input-wrapper--{{ type }}{% endif %}
{% if has_icon %} uol-form__input-wrapper--with-icon {% endif %}"
{{ 'data-field-invalid=true' if invalid == true else 'data-field-invalid=false' }}>
<!-- {% if type == 'number' %}
<button type="button" class="uol-form__input-number--decrement" hidden></button>
{% endif %} -->
<input class="uol-form__input {% if type %} uol-form__input--{{ type }}{% endif %}"
type="{{ type if type else 'text' }}"
id="{{ id }}"
name="{{ name }}"
{% if invalid %} aria-invalid="{{ invalid }}" {% endif %}
{% if type !=='file' %} value="{{ value }}" {% endif %}
{% if pattern %} pattern="{{ pattern }}" {% endif %}
{% if min %} min="{{ min }}" {% else %} {% endif %}
{% if max %} max="{{ max }}" {% endif %}
{% if aria-valuemin %} aria-valuemin="{{ min }}" {% else %} {% endif %}
{% if aria-valuemax %} aria-valuemax="{{ max }}" {% endif %} {%
if aria-valuenow %} aria-valuenow="{{ value }}" {% endif %}
{% if inputmode %} inputmode="{{ inputmode }}" {% endif %}
{% if minlength %} minlength="{{ minlength }}" {% endif %}
{% if maxlength %} maxlength="{{ maxlength }}" {% endif %}
{% if accept %} accept="{{ accept }}" {% endif %}
{% if type !=='file'%} autocomplete="{{ autocomplete if autocomplete else 'off' }}" {% endif %}
{{ 'required' if required }}
{{ 'multiple' if multiple}}
{{ 'readonly aria-readonly="true"' if readonly }}>
{% if type == 'password' %}
<button type="button" class="uol-form__input--password-toggle" aria-controls="{{ id }}"
data-password-visible="false" hidden></button>
{% endif %}
<!-- {% if type == 'number' %}
<button type="button" class="uol-form__input-number--increment" hidden></button>
{% endif %} -->
</div>
{% endif %}
{% endblock %}
<div class="uol-form__input-container
">
<div role="radiogroup" aria-labelledby="someGroupID" class="uol-form__custom-fieldset">
<span id="someGroupID" class="uol-form__custom__legend">Radio legend optional</span>
<div class="uol-form__inputs-wrapper ">
<div class="uol-form__input--radio-wrapper">
<input class="uol-form__input--radio" type="radio" id="radio-one" name="radio-group-name" value="radio-one">
<label class="uol-form__input--radio__label" for="radio-one">Label 1</label>
<span class="uol-form__input--custom-radio" hidden>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="12" />
</svg>
</span>
</div>
<div class="uol-form__input--radio-wrapper">
<input class="uol-form__input--radio" type="radio" id="radio-two" name="radio-group-name" value="radio-two">
<label class="uol-form__input--radio__label" for="radio-two">Label 2</label>
<span class="uol-form__input--custom-radio" hidden>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="12" />
</svg>
</span>
</div>
<div class="uol-form__input--radio-wrapper">
<input class="uol-form__input--radio" type="radio" id="radio-three" name="radio-group-name" value="radio-three">
<label class="uol-form__input--radio__label" for="radio-three">Label 3</label>
<span class="uol-form__input--custom-radio" hidden>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="12" />
</svg>
</span>
</div>
<div class="uol-form__input--radio-wrapper">
<input class="uol-form__input--radio" type="radio" id="radio-four" name="radio-group-name" value="radio-four">
<label class="uol-form__input--radio__label" for="radio-four">Label 4</label>
<span class="uol-form__input--custom-radio" hidden>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="12" />
</svg>
</span>
</div>
</div>
</div>
</div>
# TODO
- [x] For pseudo input use aria-labeledby and reference the ID of the original label + the id of a div containing a list of the selected comma separated items ie labeledby="[id of the original label] [id of yet to be created non-visible list of selected items]". Expected computed label example "Cheese list Select a cheese: Items selected Brie, Cheddar, Edam"
- [x] Handle spacebar keypress when navigating options so that pressing spacebar selects the current item. (Note, added parameter so space acts as select/de-select when keyboard navigating up and down list)
- [x] Handel shift tab when focus is in initial pseudo input
- [x] Aria live region - when keyboarding to a option that is already selected - text should read de-select [option name]
- [x] Aria live region - change "deselected" to 'de-selected'
- [x] Remove "text" from aria live region
- [x] Missing label for typeahead input - suggest using JS to change the for attribute on initial label to ref typeahead
- [x] Scrollable options are not keyboard accessible - this is due to the reliance on arrow keys - it's reasonable for a user to expect to be able to tab to the next checkbox. If a user does not want to tab through all teh options they can press escape. Solution add tabindex="0" to each item and close and return to input on escape
- [x] Lack of contrast for top of input container - need to agree a solution with Gemma - no longer needed
- [x] Add additional vertical padding on chips for screens less than or equal to 1024 (following feedback from Gemma)
- [x] On the 'Form: With Errors' pattern, the width of the error notices on the top two multi select components doesn't extend to the correct width
- [x] Fix aria-controls and aria-activedecendant to refer to correct id - mismatch in naming structure
- [x] Add useful aria-label to the UL listbox
- [x] Add uol-typeahead modifier to .uol-form__input-wrapper with js to allow for modification of the focus after
- [ ] Look at alternatives for issue of double focus (list items and input both having "focus" at the same time). Note, I initiated mechanism to switch focus between the input and list but this is confusing for the end user (sighted or unsighted). Having to escape the drop down list to get back to the input typeahead, and then use up and down arrows to get to the list again is too convoluted. Suggest we keep as is. Discussed with Mark and we are detailing this issue prior to testing.
- [x] Pressing enter in typeahead without panel open selects hidden option, change it to open panel if panel is closed
- [x] Set multiselect tick colour
- [x] Up arrow changes cursor position on input text
- [x] When text filtering a list, the up down arrow key limits are no longer constrained to the showing list so dissapear
- [x] Remove tiny white blob due to underline border radius when panel open
## TODO 30 June
- [x] When at top of list get up arrow to do same as shift and tab - done
- [x] WAVE error (Bug 30876) - Jonny looking at
- [x] Console log when keyboard navigating down list causing select fail (Bug 31102) - done
- [x] iPad selected issue (Bug 31148) - Jonny looking at
- [x] Bug 31043 to replicate issue
- [x] Bug 31044 to replicate issue
.uol-form__input-container {
/**
* Styling specific to all inputs that take text characters as input
*/
color: $color-font;
font-size: $base-font-size * 1.125;
font-variant-numeric: lining-nums;
margin-bottom: $spacing-6;
&:not(.uol-form__input-container--multiple) {
position: relative;
}
@include media(">=uol-media-l") {
margin-bottom: 2.5rem;
}
@include media(">=uol-media-m") {
flex-wrap: nowrap;
border-radius: 12px;
}
@include media(">=uol-media-l") {
border-radius: 12px;
font-size: $base-font-size * 1.25;
}
&[data-typeahead] {
@include media(">=uol-media-s") {
max-width: 27.375rem;
min-width: 27.375rem;
}
}
}
.uol-form__input-label {
display: block;
font-weight: $font-weight-bold--sans-serif;
}
.uol-form__input-label__text {
@extend %text-size-paragraph;
display: block;
max-width: 27.375rem;
}
.uol-form__input-label__hint {
@extend %text-size-paragraph--small;
display: block;
font-weight: $font-weight-regular--sans-serif;
max-width: 27.375rem;
}
.uol-form__input-label__text + .uol-form__input-label__hint {
padding-top: $spacing-1;
}
.uol-form__selected-items {
display: none;
}
.uol-form__input__requirements {
@extend %text-size-paragraph--small;
max-width: 47.5rem;
font-weight: 400;
display: block;
margin-top: $spacing-1;
> span {
display: block;
position: relative;
&::before {
content: "•";
position: relative;
padding: 0 $spacing-3 0 $spacing-2;
font-size: $base-font-size;
top: -0.1rem;
}
&:last-of-type {
margin-bottom: $spacing-3;
}
}
}
.uol-form__input-wrapper {
display: inline-block;
width: 100%;
position: relative;
&:not(.uol-form__input-wrapper--search):not(.uol-form__input-wrapper--file) {
@include media(">=uol-media-s") {
max-width: 27.375rem;
}
}
svg {
fill: windowText;
position: absolute;
top: 0.9rem;
left: 0.9rem;
}
&::before {
content: "";
position: absolute;
bottom: 1px;
left: 1px;
border-bottom: 4px solid $color-focus-accent;
width: 0;
border-radius: 0 0 4px 4px;
transition: width 0.25s ease-in;
[data-typeahead] & {
border-radius: 0;
}
}
&.uol-form__input-wrapper--typeahead--open {
&::before {
border-radius: 0;
}
}
&:focus-within {
&::before {
width: calc(100% - 2px);
}
}
&[data-field-invalid="true"] {
&::before {
border-bottom-color: $color-alert--error;
}
}
}
.uol-form__input-wrapper--search {
&::before{
@include media("<uol-media-m") {
margin-bottom: $spacing-6;
}
}
@include media("<uol-media-m") {
margin-bottom: 0;
.uol-form__input--search, .uol-typeahead {
& {
margin-bottom: $spacing-6;
}
}
}
}
.uol-form__input-wrapper--panelOpen {
&::before {
border-radius: 0;
}
}
// uol-form__input-container
.uol-form__input-container--x-small {
.uol-form__input__error,
[class].uol-form__input-wrapper,
.uol-form__input {
max-width: 100%;
@include media(">=uol-media-xs") {
max-width: 18.25rem;
}
}
.uol-form__input-label__hint {
max-width: 27.375rem;
}
}
.uol-form__input-container--small {
.uol-form__input__error,
[class].uol-form__input-wrapper,
.uol-form__input {
max-width: 100%;
@include media(">=uol-media-s") {
max-width: 22.8125rem;
}
}
.uol-form__input-label__hint {
max-width: 27.375rem;
}
}
.uol-form__input-container--medium {
.uol-form__input__error,
[class].uol-form__input-wrapper,
.uol-form__input {
max-width: 100%;
@include media(">=uol-media-s") {
max-width: 27.375rem;
}
}
.uol-form__input-label__hint {
max-width: 27.375rem;
}
}
.uol-form__input-container--large {
.uol-form__input__error,
[class].uol-form__input-wrapper,
.uol-form__input {
max-width: 100%;
@include media(">=uol-media-xs") {
max-width: 46.125rem;
}
}
.uol-form__input-label__hint {
max-width: 27.375rem;
}
}
.uol-form__input {
@include line-height-responsive(1.25, 1.25, 1.1);
@extend %text-size-paragraph;
box-sizing: border-box;
display: block;
border-radius: 6px;
width: 100%;
border: 1px solid $color-border;
padding: 0 $spacing-3;
background-color: $color-white;
font-variant-numeric: lining-nums;
background-color: $color-white;
&:not(.uol-form__input--textarea):not(.uol-form__input--file):not(.uol-form__input--select[multiple]) {
height: 3.125rem;
}
&[type="search"] {
-webkit-appearance: none;
appearance: none;
box-sizing: border-box;
}
&:focus {
outline: 3px dotted transparent;
}
.no-csspositionsticky & {
&:focus {
outline: 4px solid $color-blue--bright;
}
}
&::placeholder {
color: $color-font;
}
&[aria-invalid="true"] {
border: 2px solid $color-alert--error;
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.15),
0 2px 4px rgba(0, 0, 0, 0.12);
}
}
/**
* Styling for textarea component
*/
.uol-form__input--textarea-wrapper {
position: relative;
z-index: 2;
&::before {
content: "";
position: absolute;
box-sizing: border-box;
top: -5px;
right: 0;
bottom: 0;
left: -5px;
width: calc(100% + 10px);
height: calc(100% + 10px);
border: 3px solid transparent;
border-radius: 8px;
z-index: 3;
@include media(">=uol-media-l") {
max-width: calc(46.125rem + 10px);
}
@media (forced-colors: active) {
border: none;
}
@media (-ms-high-contrast: active) {
border: none;
}
}
&:focus-within::before {
border-color: $color-focus-accent;
}
&[data-field-invalid="true"] {
&:focus-within::before {
border-color: $color-alert--error;
}
.uol-form__input--textarea {
caret-color: $color-alert--error;
}
}
}
.uol-form__input--textarea {
position: relative;
min-height: 3.125rem;
max-height: 57.5rem;
box-sizing: border-box;
resize: vertical;
z-index: 3;
height: 11.875rem;
line-height: 1.556;
padding: $spacing-3;
@include media(">=uol-media-xs") {
height: 13.625rem;
}
@include media(">=uol-media-l") {
line-height: 1.6;
max-width: 46.125rem;
}
&[data-textarea-height="small"] {
@include media(">=uol-media-s") {
height: 6.625rem;
}
@include media(">=uol-media-l") {
height: 7.25rem;
}
}
&[data-textarea-height="medium"] {
@include media(">=uol-media-s") {
height: 8.375rem;
}
@include media(">=uol-media-l") {
height: 9.25rem;
}
}
&[data-textarea-height="large"] {
@include media(">=uol-media-s") {
height: 14.625rem;
}
@include media(">=uol-media-l") {
height: 15.25rem;
}
}
}
/**
* Styling specific to inputs that have an icon
*/
.uol-form__input-wrapper--with-icon {
svg {
fill: windowText;
position: absolute;
top: 0.9rem;
left: 0.9rem;
}
}
.uol-form__input-wrapper--search-typeahead {
&::before {
@include media("<uol-media-m") {
margin-bottom: 32px;
}
}
}
.uol-form__input-wrapper--search-typeahead {
&::before {
@include media("<uol-media-m") {
margin-bottom: 32px;
}
}
}
.uol-form__input {
.uol-form__input-wrapper--with-icon & {
padding-left: 2.875rem;
}
}
/**
* Styling for search inputs
*/
.uol-form__input--search, .uol-form__input-wrapper--search {
@include media("<uol-media-m") {
// margin-bottom: $spacing-6;
}
@include media(">=uol-media-m") {
margin-right: $spacing-4;
}
@include media(">=uol-media-l") {
margin-right: $spacing-5;
}
@include media(">=uol-media-xl") {
margin-right: $spacing-6;
}
}
.uol-form__input {
.uol-form__input-wrapper--with-icon & {
padding-left: 2.875rem;
}
}
.uol-form__input {
.uol-form__input-wrapper--with-icon & {
padding-left: 2.875rem;
}
}
.uol-form__additional-content {
padding-top: $spacing-3;
a {
@include link_focus();
}
}
/**
* Password field with Toggle password functionality
*/
input[type=password]::-ms-reveal,
input[type=password]::-ms-clear {
display: none;
}
.uol-form__input--password {
width: calc(100% - 3.875rem);
border-radius: 6px 0 0 6px;
border-right: none;
.no-js & {
border-radius: 6px;
border: 1px solid $color-border;
width: 100%;
}
}
.uol-form__input--password-toggle {
@include button_ripple($color-white);
display: inline-flex;
justify-content: center;
align-items: center;
position: absolute;
padding: 0 $spacing-3;
right: 0;
top: 0;
height: 100%;
border: 2px solid $color-brand-2--dark;
border-radius: 0 6px 6px 0;
transition: box-shadow 0.25s ease-in, background 0.5s ease;
min-width: 4rem;
outline: 0 dotted transparent;
outline-offset: 3px;
&:focus {
background-color: $color-focus-accent;
border-color: $color-focus-accent;
&[data-password-visible="true"] {
&::before {
border-bottom: 4px solid $color-white;
border-top: 2px solid $color-focus-accent;
}
}
> svg {
color: $color-white;
fill: $color-white;
}
@media (forced-colors: active) {
outline-color: LinkText;
outline-width: 3px;
}
@media (-ms-high-contrast: active) {
outline-color: -ms-hotlight;
outline-width: 3px;
}
}
&::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 0;
transform-origin: left top;
border-bottom: 4px solid $color-brand-2--dark;
border-top: 2px solid $color-white;
transform: rotate(45deg) translate(-50%, -50%);
transition: width 0.25s ease-in;
}
&[data-password-visible="true"] {
&::before {
width: 2.25rem;
}
}
&:hover {
box-shadow: inset 0 0 0 1px $color-brand-2--dark;
}
&:active {
background-color: darken($color-white, 10%);
}
> svg {
fill: $color-brand-2--dark;
height: 1.875rem;
position: initial;
margin-top: 0.125rem;
@media (forced-colors: active) {
fill: LinkText;
}
@media (-ms-high-contrast: active) {
fill: -ms-hotlight;
}
}
.no-js & {
display: none;
}
}
.uol-form__input--password__toggle-label {
@extend .hide-accessible;
}
/**
* Grouped inputs that are short fields displayed inline, DOB, date etc
*/
.uol-form__input--inline-wrapper {
display: flex;
.uol-form__input-container & {
position: static;
}
}
.uol-form__input--inline-field-wrapper {
position: relative;
&::before {
content: "";
position: absolute;
bottom: -4.5px;
left: -4px;
box-sizing: border-box;
border-radius: 10px;
border: 3px solid transparent;
width: calc(100% + 9px);
height: calc(3.125rem + 10px);
z-index: 0;
background-color: transparent;
transition: border-color 0.25s ease-in;
@include media(">=uol-media-m") {
bottom: -4.5px;;
}
@include media(">=uol-media-l") {
bottom: -5px;
}
@media (forced-colors: active) {
border-color: Canvas;
}
@media (-ms-high-contrast: active) {
border-color: Window;
}
}
&:focus-within {
&::before {
border-color: $color-focus-accent;
}
&[data-field-invalid="true"] {
&::before {
border-color: $color-alert--error;
}
}
}
&:not(:last-of-type) {
margin-right: $spacing-4;
}
// TODO: Bit hacky as the focus ring appeared offset find better solution
:first-of-type {
margin-left: 1px;
}
&:not(:first-of-type) {
margin-left: -1.5px;
}
}
.uol-form__input-label {
.uol-form__input--inline-field-wrapper & {
@extend %text-size-paragraph--small;
font-weight: 500;
color: $color-font--dark;
padding-bottom: $spacing-2;
}
}
.uol-form__input--inline-field {
position: relative;
z-index: 3;
box-sizing: border-box;
&[maxlength="2"] {
width: 3.375rem;
}
&[maxlength="3"] {
width: 4rem;
}
&[maxlength="4"] {
width: 4.625rem;
}
&[aria-invalid="true"] {
border: 2px solid $color-alert--error;
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.15),
0 2px 4px rgba(0, 0, 0, 0.12);
}
}
.uol-form__input--fieldset {
border: 0;
padding: 0;
font-size: 2rem;
display: block;
margin: 2.5rem 0 -2rem;
@include media(">=uol-media-l") {
margin: $spacing-7 0 -2.5rem;
}
}
.uol-form__input--legend {
border-top: 1px solid #979797;
margin-bottom: $spacing-5;
display: block;
width: 100%;
}
.uol-form__input--legend-title {
@extend %text-size-heading-2;
font-family: $font-family-serif;
margin: $spacing-5 0 $spacing-2;
color: $color-font;
@include media(">=uol-media-m") {
font-size: 2rem;
}
}
.uol-form__input--legend-subtitle {
color: $color-font;
font-weight: normal;
vertical-align: top;
font-size: 1.125rem;
line-height: 1.556;
font-family: $font-family-sans-serif;
display: inline-block;
@include media(">=uol-media-l") {
font-size: $base-font-size * 1.25;
}
}
// -------------------- Styling for Time inputs
.uol-form__input--time {
-webkit-appearance: none;
appearance: none;
line-height: 2.75;
background-color: $color-white;
color: $color-font;
text-align: left;
&[value=""] {
-webkit-text-fill-color: $color-font--x-light;
}
&:valid {
-webkit-text-fill-color: $color-font;
}
&::-webkit-calendar-picker-indicator {
filter: brightness(0) saturate(100%) invert(29%) sepia(34%) saturate(975%) hue-rotate(133deg) brightness(94%) contrast(87%);
height: $spacing-5;
width: $spacing-5;
cursor: pointer;
@media (forced-colors: active) {
filter: none;
}
@media (-ms-high-contrast: active) {
filter: none;
}
}
&::-webkit-date-and-time-value {
-webkit-appearance: none;
line-height: 3.1;
text-align: left;
@include media(">=uol-media-s") {
line-height: 2.75;
}
@include media(">=uol-media-m") {
line-height: 2.5;
}
}
&::-webkit-datetime-edit {
@include media(">=uol-media-l") {
line-height: 2.3;
}
}
&::-webkit-datetime-edit-hour-field,
&::-webkit-datetime-edit-minute-field {
padding: 0;
&:focus {
padding-bottom: 1px;
border-radius: 0;
background: $color-focus-accent;
-webkit-text-fill-color: $color-white;
}
}
}
@media screen and (-webkit-min-device-pixel-ratio:0) and (min-resolution:.001dpcm) {
line-height: 1;
}
/*
--- Below contains all of the styling for our non-text input elements, controls and grouping elements ---
- Custom radio buttons
- Custom checkboxes
- Toggle password visibility
- Select input
- Custom number spinner (currently shelved)
*/
/**
* Custom fieldset styling
*/
.uol-form__custom-fieldset {
border: none;
padding: 0;
margin-bottom: $spacing-6;
width: 100%;
}
// If radio group redcuced bottom margin as radios have their own bottom margin
[role="radiogroup"] {
margin-bottom: $spacing-6;
@include media(">=uol-media-s") {
margin-bottom: $spacing-4;
}
@include media(">=uol-media-m") {
margin-bottom: $spacing-6;
}
}
.uol-form__custom__legend {
@extend %text-size-paragraph;
display: block;
margin: 0 0 $spacing-3;
font-weight: 600;
}
// Styling specific to radio buttons
.uol-form__input--radio-wrapper {
position: relative;
margin-bottom: $spacing-4;
min-height: 2.625rem;
max-width: 27.375rem;
}
.uol-form__input--radio,
.uol-form__input--custom-radio {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
height: 2.625rem;
width: 2.625rem;
border-radius: 50%;
cursor: pointer;
transition: box-shadow 0.25s ease-in, border-color 0.25s ease-in;
background-color: $color-white;
}
.uol-form__input--radio {
opacity: 0.000001;
z-index: 2;
overflow: hidden;
&:hover {
~ .uol-form__input--custom-radio {
border-radius: 50%;
border-color: $color-brand-2--dark;
box-shadow: inset 0 0 0 1px $color-brand-2--dark;
}
~ .uol-form__input--radio__label {
color: $color-font--dark;
}
}
&:checked {
~ .uol-form__input--custom-radio {
border-color: $color-brand-2--dark;
box-shadow: inset 0 0 0 1px $color-brand-2--dark;
svg {
fill: $color-brand-2--dark;
transition: fill 0.25s ease-in;
@media (forced-colors: active) {
fill: LinkText;
}
@media (-ms-high-contrast: active) {
fill: -ms-hotlight;
}
}
}
}
}
.uol-form__input--radio__label {
display: inline-block;
margin-left: calc(2.625rem + #{$spacing-2});
cursor: pointer;
padding: $spacing-1 0;
color: $color-font;
line-height: 1.556;
font-weight: 400;
font-size: $base-font-size * 1.125;
@include media(">=uol-media-l") {
font-size: 1.25rem;
}
}
.uol-form__input--custom-radio {
display: inline-block;
border: 2px solid $color-brand-2--dark;
> svg {
margin: 50% 50%;
transform: translate(-50%, -50%);
fill: rgba($color-brand-2--dark, 0);
overflow: visible;
}
}
/**
* Styling specific to checkboxes
*/
.uol-form__input--checkbox-wrapper {
position: relative;
min-height: 2.625rem;
margin-bottom: $spacing-4;
transition: box-shadow 0.25s ease-in;
max-width: 27.375rem;
&:hover {
.uol-form__input--checkbox-custom {
box-shadow: inset 0 0 0 1px $color-brand-2--dark;
}
~ .uol-form__input--checkbox-label {
color: $color-font--dark;
}
}
}
.uol-form__input--checkbox-link {
a {
@include link_focus();
display: inline-block;
margin-bottom: $spacing-4;
}
}
.uol-form__input--checkbox,
.uol-form__input--checkbox-custom {
height: 2.625rem;
width: 2.625rem;
position: absolute;
left: 0;
top: 0;
cursor: pointer;
box-sizing: border-box;
}
.uol-form__input--checkbox-label {
padding: $spacing-2 0 $spacing-2 3.25rem;
cursor: pointer;
display: inline-block;
line-height: 1.556;
font-weight: 400;
@include media(">=uol-media-l") {
padding: 0.35rem 0 0.35rem 3.25rem;
font-size: 1.25rem;
}
}
.uol-form__input--checkbox {
opacity: 0.00001;
z-index: 1;
&:focus {
~ .uol-form__input--checkbox-custom {
box-shadow: inset 0 0 0 1px $color-brand-2--dark;
}
~ .uol-form__input--checkbox-label {
color: $color-font--dark;
}
}
&[aria-invalid="true"] {
&:focus,
&:hover {
~ .uol-form__input--checkbox-custom {
box-shadow: inset 0 0 0 1px $color-alert--error;
}
}
}
}
.uol-form__input--checkbox-custom {
border: 2px solid $color-brand-2--dark;
background-color: $color-white;
path {
fill: transparent;
height: 2.25rem;
width: 2.25rem;
position: initial;
margin: auto;
transition: color 0.25s ease-in;
}
}
.uol-form__input--checkbox:focus ~ .uol-form__input--checkbox-custom {
@media (forced-colors: active) {
outline: 3px solid LinkText;
outline-offset: 3px;
}
@media (-ms-high-contrast: active) {
outline: 4px solid -ms-hotlight;
}
}
.uol-form__input--checkbox[aria-invalid="true"] ~ .uol-form__input--checkbox-custom {
border-color: $color-alert--error;
}
.uol-form__input--checkbox:checked ~ .uol-form__input--checkbox-custom {
path {
fill: $color-brand-2--dark;
@media (-ms-high-contrast: active) {
fill: WindowText;
}
@media (forced-colors: active) {
fill: LinkText;
}
}
}
.uol-form__input--select {
// display: none;
-webkit-appearance: none;
appearance: none;
background-color: $color-white;
color: $color-font;
+ svg {
right: $spacing-3;
left: unset;
stroke: WindowText;
pointer-events: none;
path {
fill: WindowText;
}
.no-csspositionsticky & {
display: none !important;
}
}
@include media(">=uol-media-l") {
line-height: 1.25;
}
&[multiple] {
.no-js &,
&[data-native-select] {
min-height: 10rem;
padding: 0;
option {
padding: 0.5em 0.75em;
}
}
+ svg {
display: none;
}
}
&.uol-form__input--select--typeahead-hidden {
+ svg {
display: block;
}
}
}
.uol-form__input--file {
@extend .text-size-paragraph;
padding: 0;
border: none;
max-width: 25.5rem;
&:focus {
&::file-selector-button, // for Firefox and Safari shadow DOM
&::-webkit-file-upload-button { // for Chromium
border: 2px solid #045ccc;
}
}
&::file-selector-button, // for Firefox and Safari shadow DOM
&::-webkit-file-upload-button { // for Chromium
// Inheritance breaks on IE 11
// @extend .uol-button;
// @extend .uol-button--primary;
// TODO: Remove duplication of .uol-button when we drop IE11 support
@include font-size-responsive(1.125rem, 1.125rem, 1.25rem);
@include button_ripple($color-brand-2--dark);
line-height: 1;
box-sizing: border-box;
min-width: 10rem;
border: 0.125rem solid $color-brand-2--dark;
padding: 0.8em 1.8em;
border-radius: 6px;
color: $color-white;
background-position: center;
text-decoration: none;
transition: background 0.5s ease;
@include media(">=uol-media-l") {
padding-bottom: 11px;
}
@media (-ms-high-contrast: active) {
border: 1px solid WindowText;
}
&:hover,
&:active {
text-decoration: none;
box-shadow: 0 3px 6px 0 rgba($color-black, 0.15), 0 2px 4px 0 rgba($color-black, 0.12);
}
&:active {
background-color: lighten($color-brand-2--dark, 7%);
background-size: 100%;
transition: background 0s;
}
&:disabled {
color: lighten($color-font, 60%);
background: darken($color-white, 6%);
border: 0.125rem solid darken($color-white, 6%);
&:hover {
box-shadow: none;
cursor: not-allowed;
}
}
.js & {
&.uol-icon {
padding: 0.8em 2.2em;
svg {
margin-top: 0;
}
}
&.uol-icon--icon-only {
min-width: 0;
border-radius: 50%;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
width: 2.81rem;
height: 2.81rem;
min-height: 2.81rem;
@include media(">=uol-media-l") {
width: 3.12rem;
height: 3.12rem;
}
}
&.uol-icon--icon-only--large {
svg {
transform: scale(1.4);
}
}
}
font-family: inherit;
margin-right: $spacing-3;
}
}
.uol-form__input-wrapper--file {
&::before {
content: none;
}
}
.uol-form__files-list {
list-style: none;
margin: $spacing-3 0;
padding: 0;
max-width: 27.375rem;
@include media(">=uol-media-l") {
margin: $spacing-3 0 $spacing-5;
}
}
.uol-form__files-list__item {
@extend .uol-typography-paragraph;
position: relative;
padding: $spacing-3 $spacing-7 $spacing-3 0;
border-bottom: 1px solid $color-border;
.uol-form__files-list__item__name {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.uol-form__files-list__item__btn-delete {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
justify-content: center;
align-items: center;
width: 2em;
height: 2em;
background: transparent;
border: none;
border: 2px solid transparent;
border-radius: 50%;
&:hover {
border-color: $color-brand-2--dark;
}
svg {
position: absolute;
width: 1.25em;
height: 1.25em;
top: initial;
left: initial;
}
}
}
// Currently used for styling checkboxes and radios for the search filter
.uol-form__input-container--small {
@include media(">=uol-media-l") {
.uol-form__input--checkbox-wrapper {
min-height: 0;
margin-bottom: $spacing-3;
.uol-form__input--checkbox,
.uol-form__input--checkbox-custom {
top: 0.075em;
height: 1.5rem;
width: 1.5rem;
}
.uol-form__input--checkbox-label {
padding: 0 0 0 30px;
line-height: 1;
}
}
.uol-form__input--radio-wrapper {
margin-bottom: $spacing-2;
min-height: 0;
.uol-form__input--radio,
.uol-form__input--custom-radio {
height: 1.5rem;
width: 1.5rem;
svg {
transform: translate(-25%,-25%);
circle {
cx: 6px;
cy: 6px;
r: 6px;
}
}
}
.uol-form__input--custom-radio {
top: 0.15em;
}
.uol-form__input--radio__label {
margin-left: $spacing-6;
padding: 0;
}
}
}
}
// ------- Number spinners (shelved) Chrome/NVDA does not correctly announce values, Chrome bug ~ 4yrs old
// Custom number spinner
// input[type="number"]::-webkit-inner-spin-button,
// input[type="number"]::-webkit-outer-spin-button {
// .js & {
// -webkit-appearance: none;
// margin: 0;
// }
// }
// input[type="number"] {
// .js & {
// -moz-appearance: textfield;
// }
// }
// .uol-form__input-number--decrement.uol-icon {
// justify-content: center;
// align-items: center;
// display: inline-block;
// position: absolute;
// top: 0;
// left: 0;
// height: 100%;
// border: 2px solid $color-brand-2--dark;
// padding: $spacing-3;
// padding-right: $spacing-2;
// background-color: $color-white;
// border-radius: 6px 0 0 6px;
// transition: box-shadow 0.25s ease-in, background 0.5s ease;
// &:focus-visible {
// background-color: $color-focus-accent;
// border-color: $color-focus-accent;
// > svg {
// color: $color-white;
// stroke: $color-white;
// }
// }
// &:focus {
// @media (forced-colors: active) {
// outline: 3px solid LinkText;
// outline-offset: 3px;
// }
// @media (-ms-high-contrast: active) {
// outline: 4px solid -ms-hotlight;
// }
// }
// &:hover {
// box-shadow: inset 0px 0px 0px 1px $color-brand-2--dark;
// }
// &:active {
// background-color: darken($color-white, 10%);
// }
// > svg {
// stroke: $color-brand-2--dark;
// color: $color-brand-2--dark;
// height: 1.75rem;
// width: 1.75rem;
// position: initial;
// margin-top: -0.125rem;
// @media (forced-colors: active) {
// color: LinkText;
// stroke: LinkText;
// }
// @media (-ms-high-contrast: active) {
// color: -ms-hotlight;
// stroke: -ms-hotlight;
// }
// }
// .no-js & {
// display: none;
// }
// }
// .uol-form__input--number {
// text-align: center;
// width: calc(100% - 6.5rem);
// .js & {
// border-left: none;
// border-right: none;
// border-radius: 0;
// margin-left: 3.25rem;
// }
// }
// .uol-form__input-number--increment.uol-icon {
// display: inline-block;
// position: absolute;
// display: inline-flex;
// justify-content: center;
// align-items: center;
// top: 0;
// right: 0;
// height: 100%;
// border: 2px solid $color-brand-2--dark;
// padding: $spacing-3;
// padding-right: $spacing-2;
// background-color: $color-white;
// border-radius: 0 6px 6px 0;
// transition: box-shadow 0.25s ease-in, background 0.5s ease;
// &:focus-visible {
// background-color: $color-focus-accent;
// border-color: $color-focus-accent;
// > svg {
// color: $color-white;
// stroke: $color-white;
// }
// }
// &:focus {
// @media (forced-colors: active) {
// outline: 3px solid LinkText;
// outline-offset: 3px;
// }
// @media (-ms-high-contrast: active) {
// outline: 4px solid -ms-hotlight;
// }
// }
// &:hover {
// box-shadow: inset 0px 0px 0px 1px $color-brand-2--dark;
// }
// &:active {
// background-color: darken($color-white, 10%);
// }
// > svg {
// stroke: $color-brand-2--dark;
// color: $color-brand-2--dark;
// height: 1.75rem;
// width: 1.75rem;
// position: initial;
// margin-top: 0.125rem;
// @media (forced-colors: active) {
// color: LinkText;
// stroke: LinkText;
// fill: LinkText;
// }
// @media (-ms-high-contrast: active) {
// color: -ms-hotlight;
// stroke: -ms-hotlight;
// fill: LinkText;
// }
// }
// .no-js & {
// display: none;
// }
// }
.uol-form__inputs-wrapper--inline {
@include media(">=uol-media-s") {
display: flex;
flex-wrap: wrap;
flex-direction: row;
.uol-form__input--radio-wrapper {
flex-shrink: 0;
margin-right: $spacing-6;
}
}
}
/**
* Summary. Enhancements to input type file.
*
* Description. Adds editable file list below input when files are added
*
* @file This files exports the inputFileUploadDetails module.
*/
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)
}
/**
* Remove file from filelist DataTransfer
* @param {Element} input - DOM object
* @param {index} index - The index of the item to delete
*/
const removeFileFromFileList = (input, index) => {
const dataTransferInstance = new DataTransfer();
const { files } = input;
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (index !== i) dataTransferInstance.items.add(file); // here you exclude the file. thus removing it.
}
// Assign the updates list
input.files = dataTransferInstance.files;
};
/**
* Add file listing functionality to each input of type file
*/
export const inputFileUploadDetails = () => {
// Create Boolean for detection of DataTransfer constructor support
let dataTransferConstructorSupported = false;
try { new DataTransfer(), dataTransferConstructorSupported = true } catch {}
/**
* Exclude browsers that do not support DataTransfer constructor (ie IE, iOS < 14.5)
* and exclude all iOS/IPadOS devices as the file count does not update in mobile Safari
*/
if ( dataTransferConstructorSupported && !isIOS() ) {
// Select all the file upload inputs
const uploadInputs = document.querySelectorAll('input[type="file"]');
uploadInputs.forEach((uploadInput) => {
// Get input parent
const uploadsInputsParent = uploadInput.closest(
".uol-form__input-wrapper"
);
// Create file list container
const fileUploadsInfo = document.createElement("ul");
fileUploadsInfo.classList.add("uol-form__files-list");
uploadInput.addEventListener("change", (event) => {
let fileList = uploadInput.files;
if (uploadInput.files.length > 0) {
// Create sring to contain file details
let fileDetails = "";
Array.from(fileList).forEach((file) => {
// Add file details <li>
fileDetails += `
<li class="uol-form__files-list__item">
<span class="uol-form__files-list__item__name">${file.name}</span>
<button class="uol-form__files-list__item__btn-delete">
<svg aria-hidden="true" viewBox="0 0 24 24">
<path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
<span class="hide-accessible">Delete</span>
</button>
</li>`;
});
// Add file details to container
fileUploadsInfo.innerHTML = fileDetails;
// Append file list to input parent
uploadsInputsParent.appendChild(fileUploadsInfo);
// Select all the delete buttons
let fileDeleteBtns = uploadsInputsParent.querySelectorAll("button");
fileDeleteBtns.forEach((btn) => {
btn.onclick = () => {
// Find the index of the clicked button
const index = [
...uploadsInputsParent.querySelectorAll("li button"),
].indexOf(btn);
// select the buttons parent
const btnParent = btn.closest("li");
// Remove file for DataTransfer
removeFileFromFileList(uploadInput, index);
// Add updated set to DataTransfer
fileList = uploadInput.files;
// Remove th clicked button's <li>
btnParent.remove();
// If there are no file left, delete the list
if (uploadInput.files.length === 0) {
fileUploadsInfo.remove();
}
};
});
}
});
});
}
};
export const searchInputIcon = () => {
const searchInputWrappers = document.querySelectorAll('.uol-form__input-wrapper--search')
searchInputWrappers.forEach( (wrapper) => {
if (wrapper.classList.contains('uol-form__input-wrapper--with-icon')) {
const searchIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
searchIcon.setAttribute('aria-hidden', true)
searchIcon.setAttribute('focusable', false)
searchIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
searchIcon.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
searchIcon.setAttribute('version', '1.1')
searchIcon.setAttribute('width', '22')
searchIcon.setAttribute('height', '22')
searchIcon.setAttribute('viewBox', '0 0 24 24')
searchIcon.innerHTML = `
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" />
`
wrapper.appendChild(searchIcon)
}
})
}
export const togglePasswordVisibility = () => {
const eyeSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true">
<path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/>
<path xmlns="http://www.w3.org/2000/svg" d="M12 6c3.79 0 7.17 2.13 8.82 5.5C19.17 14.87 15.79 17 12 17s-7.17-2.13-8.82-5.5C4.83 8.13 8.21 6 12 6m0-2C7 4 2.73 7.11 1 11.5 2.73 15.89 7 19 12 19s9.27-3.11 11-7.5C21.27 7.11 17 4 12 4zm0 5c1.38 0 2.5 1.12 2.5 2.5S13.38 14 12 14s-2.5-1.12-2.5-2.5S10.62 9 12 9m0-2c-2.48 0-4.5 2.02-4.5 4.5S9.52 16 12 16s4.5-2.02 4.5-4.5S14.48 7 12 7z"/>
</svg>`
if (document.querySelector('.uol-form__input--password-toggle')) {
const passwordToggle = document.querySelector('.uol-form__input--password-toggle');
let passwordToggleAlert = document.querySelector('.uol-form__announcement');
let passwordInput = document.querySelector('.uol-form__input--password');
passwordToggle.innerHTML = eyeSvg + '<span class="uol-form__input--password__toggle-label">Show password</span>';
let passwordToggleLabel = document.querySelector('.uol-form__input--password__toggle-label');
passwordToggle.addEventListener('click', () => {
const buttonPressed = passwordToggle.getAttribute('data-password-visible')
if (buttonPressed == 'false') {
passwordToggle.setAttribute('data-password-visible', 'true');
passwordInput.setAttribute('type', 'text');
passwordToggleLabel.innerText = 'Hide password';
passwordToggleAlert.innerText = 'Your password is shown';
} else {
passwordToggle.setAttribute('data-password-visible', 'false');
passwordInput.setAttribute('type', 'password');
passwordToggleLabel.innerText = 'Show password';
passwordToggleAlert.innerText = 'Your password is hidden';
}
})
}
}
//
// SHELVED for the moment, due to accessibility bug in Chrome
//
// export const incrementDecrementNumber = () => {
// if (document.querySelector('.uol-form-input--number')) {
// const numDecrement = document.querySelector('.uol-form-input-number--decrement');
// const numIncrement = document.querySelector('.uol-form-input-number--increment');
// const numInput = document.querySelector('.uol-form-input--number');
// let numAnnouncement = document.querySelector('.uol-form__announcement');
// let numMinVal = numInput.getAttribute('min');
// let numMaxVal;
// if (numInput.hasAttribute('max')) {
// numMaxVal = numInput.getAttribute('max');
// }
// numInput.addEventListener('change', () => {
// numAnnouncement.innerText = parseInt(numInput.value);
// numInput.setAttribute('aria-valuenow', parseInt(numInput.value));
// console.log(numAnnouncement.innerText);
// })
// if (numDecrement) {
// console.log(parseInt(numInput.value));
// numDecrement.classList.add('uol-icon')
// numDecrement.classList.add('uol-icon--mdiMinus')
// numDecrement.addEventListener('click', () => {
// if (parseInt(numInput.value) > parseInt(numMinVal)) {
// numInput.value--
// numAnnouncement.innerText = numInput.value
// } else {
// return
// }
// })
// }
// if (numIncrement) {
// numIncrement.classList.add('uol-icon')
// numIncrement.classList.add('uol-icon--mdiPlus')
// numIncrement.addEventListener('click', () => {
// if (parseInt(numInput.value) < parseInt(numMaxVal)) {
// numInput.value++;
// numAnnouncement.innerText = numInput.value
// } else {
// return
// }
// })
// }
// }
// }
{
"id": "input-one",
"name": "input-one",
"label": "Input label",
"type": "radio",
"legend": "Radio legend optional",
"heading_level": "h3",
"group_label_id": "someGroupID",
"options": [
{
"id": "radio-one",
"name": "radio-group-name",
"value": "radio-one",
"label": "Label 1"
},
{
"id": "radio-two",
"name": "radio-group-name",
"value": "radio-two",
"label": "Label 2"
},
{
"id": "radio-three",
"name": "radio-group-name",
"value": "radio-three",
"label": "Label 3"
},
{
"id": "radio-four",
"name": "radio-group-name",
"value": "radio-four",
"label": "Label 4"
}
]
}