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
">
<label class="uol-form__input-label" for="input-one">
<span class="uol-form__input-label__text">Input label</span>
</label>
<div class="uol-form__input-wrapper
" data-field-invalid=false>
<!-- -->
<input class="uol-form__input " type="text" id="input-one" name="input-one" value="" autocomplete="off">
<!-- -->
</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-datepicker-background {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 11;
background-color: $color-black--dark;
opacity: 0.78;
animation: backgroundFade 0.15s ease-in-out;
&.uol-datepicker-background--fade-out {
animation: backgroundFade 0.15s ease-in-out;
animation-direction: reverse;
}
}
.uol-datepicker__controls-wrapper {
@include media(">=uol-media-xs") {
display: flex;
}
}
.uol-datepicker__input-wrapper {
position: relative;
width: 100%;
&::before {
content: "";
position: absolute;
bottom: 1px;
left: 1px;
border-bottom: 4px solid $color-focus-accent;
border-radius: 0 0 4px 4px;
width: 0;
z-index: 12;
transition: width 0.25s ease-in;
@include media(">=uol-media-xs") {
bottom: 1px;
}
}
&:focus-within {
&::before {
width: 100%;
}
}
[data-invalid] & {
&::before {
border-bottom-color: $color-alert--error;
}
}
}
.uol-datepicker__unified-input-wrapper {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
@include media(">=uol-media-s") {
width: 22.8125rem;
}
[data-invalid] & {
.uol-form__input__error {
flex-basis: auto;
order: 2;
@include media("<uol-media-s") {
max-width: 100%;
}
}
}
}
.uol-datepicker__input-group {
position: relative;
width: 100%;
.uol-form__input__error {
@include media("<uol-media-s") {
max-width: 100%;
}
}
@include media(">=uol-media-s") {
width: 22.8125rem;
}
}
.uol-datepicker__labels-wrapper {
display: flex;
[data-invalid] & {
order: 1;
}
}
.uol-datepicker__range-label {
@extend .uol-form__input-label;
padding-bottom: $spacing-3;
}
.uol-datepicker__range-label--start {
flex-basis: 8.5rem;
@include media(">=uol-media-xs") {
flex-basis: 9rem;
}
}
.uol-datepicker__range-label--end {
padding-left: $spacing-1;
}
.uol-datepicker__unified-input {
position: relative;
box-sizing: border-box;
display: flex;
width: 100%;
border: 1px solid $color-border;
border-radius: 6px;
[data-invalid] & {
order: 3;
border-color: $color-alert--error;
border-width: 2px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.12);
}
.no-csspositionsticky & {
height: 3.125rem;
}
@include media(">=uol-media-s") {
width: (22.8125rem - 3rem);
border-right: none;
border-radius: 6px 0 0 6px;
}
.no-js & {
border-right: 1px solid $color-border;
border-radius: 6px;
}
}
.uol-datepicker__input-wrapper--start,
.uol-datepicker__input-wrapper--end {
position: relative;
&::before {
content: "";
position: absolute;
bottom: -1px;
width: 0;
left: 1px;
border-top: 4px solid transparent;
border-radius: 0 0 0 3px;
transition: width 0.25s ease-in;
}
&:focus-within {
&::before {
border-color: $color-focus-accent;
width: 100%;
[data-invalid] & {
border-color: $color-alert--error;
}
}
}
}
.uol-datepicker__input-wrapper--start {
&::after {
content: "-";
position: absolute;
color: $color-font;
bottom: 50%;
transform: translateY(50%);
right: 0;
font-size: 1.25rem;
}
}
.uol-datepicker__input-wrapper--end {
flex-grow: 1;
}
.uol-datepicker__input {
@include font-size-responsive(1rem, 1.125rem, 1.25rem);
@include line-height-responsive(1.25, 1.25, 1.1);
box-sizing: border-box;
border: 0;
padding-left: $spacing-3;
height: 3.125rem;
width: 100%;
font-variant-numeric: lining-nums;
outline: transparent;
&::placeholder {
color: $color-font--light;
}
[data-single-selection] & {
border: 1px solid $color-border;
border-radius: 6px;
@include media(">=uol-media-xs") {
border-right: none;
border-radius: 6px 0 0 6px;
}
.no-js & {
border: 1px solid $color-border;
border-radius: 6px;
}
}
[data-single-selection][data-invalid] &{
border-width: 2px;
border-color: $color-alert--error;
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.15),
0 2px 4px rgba(0, 0, 0, 0.12);
}
[data-range-selection] & {
height: calc(3.125rem - 2px);
}
[data-range-selection][data-invalid] & {
height: calc(3.125rem - 4px);
}
}
.uol-datepicker__input--start {
[data-range-selection] & {
border-radius: 6px 0 0 6px;
width: 8.5rem;
}
}
.uol-datepicker__input--start,
.uol-datepicker__input--end, {
.no-csspositionsticky & {
&:focus {
outline: 3px solid $color-focus-accent;
background-color: $color-grey--light;
}
&::-ms-clear {
display: none;
}
}
}
.uol-datepicker__input--end {
border-radius: 0 6px 6px 0;
@include media(">=uol-media-xs") {
border-radius: 0;
}
.no-js & {
border-radius: 0 6px 6px 0;
}
}
.uol-datepicker__toggle-btn {
margin-top: $spacing-2;
padding: $spacing-3 $spacing-4;
min-width: 0;
max-width: 8.5rem;
> svg {
height: 1.25rem;
width: 1.25rem;
fill: $color-brand-2--dark;
@media (-ms-high-contrast: active) {
fill: ButtonText;
}
@media (forced-colors: active) {
fill: currentColor;
}
}
[data-range-selection][data-invalid] & {
order: 4;
}
@include media("<uol-media-xs") {
position: relative;
&::before {
content: "";
position: absolute;
top: -6px;
right: -6px;
bottom: -6px;
left: -6px;
border: 3px solid transparent;
border-radius: inherit;
transition: all 0.15s;
}
&:focus::before {
border-color: $color-focus-accent;
}
}
@include media(">=uol-media-xs") {
@include button_ripple($color-white);
align-self: flex-end;
margin-top: 0;
border-radius: 0 6px 6px 0;
padding: 0;
height: 3.125rem;
min-width: 3rem;
outline: transparent;
[data-range-selection] & {
position: absolute;
bottom: 0;
}
[data-invalid] & {
box-shadow:
0 3px 6px rgba(0, 0, 0, 0.15),
0 2px 4px rgba(0, 0, 0, 0.12);
}
.no-csspositionsticky & {
right: 0;
}
&:focus {
background-color: $color-focus-accent;
border-color: $color-focus-accent;
> svg {
fill: $color-white;
}
}
}
}
.uol-datepicker__btn-label {
vertical-align: text-top;
@include media(">=uol-media-xs") {
display: none;
}
}
.uol-datepicker {
position: fixed;
top: 50%;
left: 50%;
box-sizing: border-box;
margin-top: 0.15em;
border-radius: 8px;
padding: $spacing-5 $spacing-4;
max-height: 100%;
width: 20rem;
z-index: 12;
overflow: auto;
font-variant-numeric: lining-nums;
transform: translate(-50%, -50%);
opacity: 0;
background-color: $color-white;
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.15), 0 5px 10px rgba(0, 0, 0, 0.5),
0 5px 10px rgba($color-black--dark, 0.05);
@include media(">=uol-media-xs", "<uol-media-l") {
width: 24.625rem;
}
@include media(">=uol-media-l") {
font-family: $font-family-sans-serif--desktop;
}
@media (forced-colors: active), (-ms-high-contrast: active) {
border: 1px solid ButtonText;
}
}
.uol-datepicker__header {
border-radius: 8px 8px 0 0;
}
.uol-datepicker__toggle-container {
position: relative;
display: flex;
border: none;
border-radius: 3px;
padding: 0;
background-color: $color-grey--light;
@media (forced-colors: active), (-ms-high-contrast: active) {
border: 1px solid ButtonText;
}
&::before,
&::after {
content: "";
box-sizing: border-box;
position: absolute;
margin: 2px;
border-radius: 3px;
transition: all 200ms ease-in;
}
&::before {
top: 0;
left: 0;
border: 2px solid $color-brand-2--dark;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15), 0 3px 6px rgba(0, 0, 0, 0.12);
height: calc(100% - 4px);
width: calc(50% - 2px);
background: radial-gradient(circle, $color-white 50%, darken($color-white, 10%) 90%);
}
&::after {
top: 4px;
left: 12px;
border: 2px solid transparent;
height: calc(100% - 12px);
width: calc(50% - 10px);
@media (forced-colors: active), (-ms-high-contrast: active) {
display: none;
}
}
}
[data-toggle="start"] {
&::before {
left: 2px;
}
&::after {
left: 6px;
}
&:focus-within {
&::after {
border-color: $color-focus-accent;
}
}
}
[data-toggle="end"] {
&::before {
transform: translateX(100%);
}
&::after {
right: 4px;
transform: translateX(100%);
}
&:focus-within {
&::after {
border-color: $color-focus-accent;
}
}
}
.uol-datepicker__toggle-item {
position: relative;
width: 50%;
}
.uol-datepicker__toggle-radio {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
opacity: 0.0001;
color: $color-font;
cursor: pointer;
&:checked {
+ .uol-datepicker__toggle-label {
& .uol-datepicker__toggle-label--lower {
color: $color-brand-2--dark;
}
}
}
}
.uol-datepicker__toggle-label {
@include font-size-responsive(1rem, 1rem, 1.125rem);
display: block;
border-radius: 6px;
padding: calc(#{$spacing-4} + 2px) $spacing-4;
font-weight: 500;
}
.uol-datepicker__toggle-label--upper,
.uol-datepicker__toggle-label--lower {
display: block;
line-height: 1.2;
}
.uol-datepicker__toggle-label--lower {
padding-top: $spacing-2;
color: $color-font--light;
}
.uol-datepicker__nav {
display: flex;
justify-content: center;
padding: $spacing-5 $spacing-1 $spacing-3;
.uol-datepicker-container[data-single-selection] & {
padding-top: $spacing-2;
}
}
.uol-datepicker__grid-title {
@include font-size-responsive(1rem, 1rem, 1.125rem);
display: inline-block;
width: calc(18rem / 7 * 3.5);
margin: 0;
padding: 0;
font-weight: 500;
line-height: 1.625;
text-align: center;
color: $color-font--light;
@include media(">=uol-media-xs", "<uol-media-l") {
width: calc(21.625rem / 7 * 3.25);
line-height: 2;
}
@include media(">=uol-media-l") {
line-height: 1.3;
}
}
.uol-datepicker__prev-year,
.uol-datepicker__prev-month,
.uol-datepicker__next-month,
.uol-datepicker__next-year {
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 0;
width: 1.5rem;
height: 1.5rem;
font-size: 1.5rem;
background: none;
color: $color-black;
outline: transparent;
@include media(">=uol-media-xs", "<uol-media-l") {
height: 2rem;
width: 2rem;
}
& path {
height: 1.5rem;
width: 1.5rem;
@include media(">=uol-media-xs", "<uol-media-l") {
height: 2rem;
width: 2rem;
}
}
@media (forced-colors: active), (-ms-high-contrast: active) {
color: ButtonText;
}
&:focus,
&:hover {
&::before {
box-sizing: border-box;
content: "";
position: absolute;
top: -2px;
left: -2px;
height: 1.75rem;
width: 1.75rem;
border: 2px solid $color-focus-accent;
border-radius: 3rem;
@include media(">=uol-media-xs", "<uol-media-l") {
height: 2.25rem;
width: 2.25rem;
}
}
}
}
.uol-datepicker__prev-year {
margin-right: $spacing-4;
}
.uol-datepicker__next-year {
margin-left: $spacing-4;
}
.uol-datepicker__grid {
table-layout: fixed;
border-collapse: separate;
border-spacing: 0 4px;
width: 100%;
@include media(">=uol-media-xs", "<uol-media-l") {
padding: 0 $spacing-2;
}
}
.uol-datepicker__header-cell {
@include font-size-responsive(1rem, 1rem, 1.125rem);
height: 2.375rem;
line-height: 1.625;
font-weight: normal;
text-align: center;
color: $color-font--light;
}
.uol-datepicker__row--blank {
visibility: hidden;
}
.uol-datepicker__cell {
@include font-size-responsive(1rem, 1rem, 1.125rem);
position: relative;
box-sizing: border-box;
margin: 0;
border: none;
width: 2.575rem;
height: 2.575rem;
line-height: inherit;
text-align: center;
cursor: pointer;
outline: transparent;
&:not(.uol-datepicker__cell--unavailable) {
font-weight: 600;
}
&[tabindex="0"]:focus {
@media (-ms-high-contrast: active) {
border: 1px solid ButtonText;
}
@media (forced-colors: active) {
border: 1px solid currentColor;
}
}
&::before,
&::after {
content: "";
box-sizing: border-box;
position: absolute;
border: 2px solid transparent;
border-radius: 3rem;
z-index: 11;
@media (forced-colors: active), (-ms-high-contrast: active) {
display: none;
}
}
&::before {
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
}
&::after {
top: 2px;
right: 2px;
bottom: 2px;
left: 2px;
height: calc(100% - 4px);
width: calc(100% - 4px);
}
> span[aria-hidden="true"] {
position: relative;
display: inline-block;
height: 100%;
width: 100%;
line-height: 2.45;
z-index: 12;
color: $color-font;
pointer-events: none;
@include media(">=uol-media-xs", "<uol-media-l") {
line-height: 2.965;
}
@include media(">=uol-media-l") {
line-height: 2.1;
}
}
&:hover:not([data-selected]),
&[tabindex="0"]:not([data-selected]) {
color: $color-brand-2--dark;
@media (-ms-high-contrast: active) {
outline: 2px dotted ButtonText;
}
@media (forced-colors: active) {
outline: 2px dotted currentColor;
}
&::before {
background-color: $color-white;
border-color: $color-brand-2--dark;
}
}
&.uol-datepicker__cell--has-focus[tabindex="0"] {
&:not([data-selected]) {
&::before {
background-color: #699A9C;
}
&::after {
border-color: $color-white;
}
}
&[data-selected] {
&::after {
border-color: $color-white;
}
}
}
&[data-selected-start]:not([data-single-day]) {
background: linear-gradient(to right, $color-white 0%, $color-white 50%, #699A9C 50%, #699A9C 50%);
@media (-ms-high-contrast: active) {
border: 2px solid ButtonText;
border-radius: 1rem 0 0 1rem;
}
@media (forced-colors: active) {
border: 2px solid currentColor;
border-radius: 1rem 0 0 1rem;
}
}
&[data-selected-end]:not([data-single-day]),
&.uol-datepicker__cell--end-range {
background: linear-gradient(to right, #699A9C 0%, #699A9C 50%, $color-white 50%, $color-white 100%);
@media (-ms-high-contrast: active) {
border: 2px solid ButtonText;
border-radius: 0 1rem 1rem 0;
}
@media (forced-colors: active) {
border: 2px solid currentColor;
border-radius: 0 1rem 1rem 0;
}
}
&.uol-datepicker__cell--end-range:not([data-selected]) {
&.uol-datepicker__cell--has-focus {
&::before {
background-color: #699A9C;
}
&::after {
border-color: $color-white;
}
}
}
&[data-single-day] {
background-color: $color-white;
}
&[data-single-day],
&[data-selected-date] {
@media (-ms-high-contrast: active) {
border: 2px solid ButtonText;
border-radius: 1rem;
}
@media (forced-colors: active) {
border: 2px solid currentColor;
border-radius: 1rem;
}
}
&[data-selected-date],
&[data-single-day],
&[data-selected-start],
&[data-selected-end] {
span[aria-hidden="true"] {
color: $color-white;
}
&::before {
background-color: $color-brand-2--dark;
}
&:focus {
&::after {
border-color: $color-white;
}
}
}
&.uol-datepicker__cell--in-range,
&.uol-datepicker__cell--passthrough {
color: $color-font--dark;
background-color: #699A9C;
> span[aria-hidden="true"] {
&::before {
background-color: $color-white;
}
}
> span[aria-hidden="true"] {
color: $color-black--dark;
&::after {
background-color: $color-black--dark;
}
}
&[tabindex="0"],
&:focus {
&::before {
background-color: $color-white;
border-color: #699A9C;
}
> span[aria-hidden="true"] {
&::before {
background-color: $color-brand-2--dark;
}
}
}
&.uol-datepicker__cell--has-focus[tabindex="0"] {
&::before {
border-color: $color-white;
}
&::after {
border-color: $color-brand-2--dark;
}
}
}
}
.uol-datepicker__cell--empty {
cursor: not-allowed;
height: calc(18rem / 7);
@include media(">=uol-media-xs", "<uol-media-l") {
height: calc(21.625rem / 7);
}
&::before,
&::after {
display: none;
}
}
.uol-datepicker__cell--current-day {
span[aria-hidden="true"] {
&::before {
content: "";
position: absolute;
bottom: 6px;
left: 50%;
height: 4px;
width: 4px;
transform: translateX(-2px);
background-color: $color-brand-2--dark;
@media (-ms-high-contrast: active) {
background-color: ButtonText;
}
@media (forced-colors: active) {
background-color: currentColor;
}
@include media(">=uol-media-xs", "<uol-media-l") {
bottom: 8px;
}
}
}
&[tabindex="0"]:not([data-selected]) {
span[aria-hidden="true"] {
&::before {
background-color: $color-brand-2--dark;
}
}
}
&[data-selected],
&.uol-datepicker__cell--has-focus[tabindex="0"] {
> span[aria-hidden="true"] {
&::before {
background-color: $color-white;
}
}
}
}
.uol-datepicker__cell--unavailable[aria-disabled="true"] {
cursor: not-allowed;
&[tabindex="0"] {
&::before {
border-color: $color-alert--error;
}
}
&.uol-datepicker__cell--has-focus[tabindex="0"] {
[aria-hidden="true"] {
color: $color-white;
&::after {
background-color: $color-white;
}
}
&::before {
background-color: $color-alert--error;
}
}
[aria-hidden="true"] {
color: $color-font--x-light;
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
height: 100%;
width: 2px;
transform: translate(-50%, -50%) rotate(-45deg);
background-color: $color-font--x-light;
@media (-ms-high-contrast: active) {
background-color: ButtonText;
}
@media (forced-colors: active) {
background-color: CanvasText;
}
}
}
&.uol-datepicker__cell--in-range,
&.uol-datepicker__cell--passthrough {
&.uol-datepicker__cell--has-focus[tabindex="0"] {
&::after {
border-color: $color-alert--error
}
> [aria-hidden="true"] {
color: $color-white;
&::after {
background-color: $color-white;
}
}
}
> [aria-hidden="true"] {
color: $color-black--dark;
&::after {
background-color: $color-black--dark;
}
}
&:focus {
> [aria-hidden="true"] {
color: $color-black--light;
&::after {
background-color: $color-black--light;
}
}
}
}
&:focus,
&:hover {
&::before {
border-color: $color-alert--error;
}
}
&[data-selected-start],
&[data-selected-end],
&[data-single-day],
&[data-selected-date] {
> [aria-hidden="true"] {
&::after {
background-color: $color-white;
@media (forced-colors: active) {
background-color: CanvasText;
}
}
}
&::before {
background-color: $color-alert--error;
}
&:focus {
&::after {
border-color: $color-white;
}
}
}
}
.uol-datepicker__footer {
display: flex;
justify-content: flex-end;
margin: $spacing-3 0 $spacing-2;
padding: $spacing-4 0 0;
}
.uol-datepicker__cancel-btn,
.uol-datepicker__confirm-btn {
@include button_focus(-6px);
@include font-size-responsive(1rem, 1rem, 1.125rem);
border-radius: 4px;
padding: $spacing-4;
max-height: 3.125rem;
min-width: 0;
max-width: 7.75rem;
width: 7.75rem;
@include media(">=uol-media-l") {
padding: calc(#{$spacing-4} - 2px);
}
}
.uol-datepicker__cancel-btn {
margin-right: $spacing-3;
}
.uol-datepicker--fade-in {
animation: fadeIn 0.2s ease-in-out;
animation-fill-mode: forwards;
}
.uol-datepicker--fade-out {
animation: fadeOut 0.2s ease-in-out;
animation-fill-mode: backwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
visibility: visible;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
visibility: visible;
}
99% {
visibility: visible;
}
100% {
opacity: 0;
visibility: hidden;
}
}
@keyframes backgroundFade {
0% {
opacity: 0;
}
100% {
opacity: 0.78;
}
}
.uol-typeahead {
background: $color-white;
&[type="text"] {
padding-right: $spacing-6;
}
~ svg {
transition: transform 200ms ease-in-out;
}
&[aria-expanded="true"] {
border-radius: 6px 6px 0 0;
~ .uol-form__input__chevron {
cursor: pointer;
pointer-events: auto;
transform: rotate(180deg)
}
}
&[aria-expanded="false"] {
border-radius: 6px;
~ svg {
transform: rotate(0)
}
}
}
.uol-typeahead__list {
box-sizing: border-box;
position: absolute;
top: 1.85rem;
left: 0;
border: 1px solid $color-border;
padding: 0;
width: 100%;
max-height: 12rem;
list-style-type: none;
z-index: 11;
overflow-y: auto;
background-color: $color-white;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.12);
cursor: pointer;
&[data-panel-shown="false"] {
visibility: hidden;
}
&[data-panel-shown="true"] {
visibility: visible;
}
@include media(">=uol-media-m") {
max-height: 22rem;
}
@include media(">=uol-media-l") {
max-height: 24rem;
}
}
.uol-typeahead__item {
@extend %text-size-paragraph--small;
position: relative;
display: flex;
padding: 0 2.875rem 0 $spacing-3;
min-height: 2.875rem;
align-items: center;
font-family: $font-family-sans-serif;
font-weight: normal;
background: inherit;
color: $color-font;
&[data-item-active] {
background-color: $color-brand-2--light;
}
.uol-typeahead__list--multi-select & {
&::before {
content: "";
position: relative;
margin-right: $spacing-2;
min-width: 1.75rem;
height: 1.75rem;
width: 1.75rem;
border: 2px solid $color-brand-2--dark;
background-color: $color-white;
z-index: 2;
pointer-events: none;
}
&[aria-selected="true"] {
> svg {
fill: $color-brand-2--dark;
color: $color-brand-2--dark;
}
}
}
&:hover {
background-color: #699B9D;
cursor: pointer;
}
&[aria-selected="true"] {
> svg {
position: relative;
top: -1px;
left: $spacing-2;
fill: $color-black--dark;
color: $color-black--dark;
pointer-events: none;
.uol-typeahead__list--multi-select & {
position: absolute;
z-index: 3;
left: $spacing-4;
top: 50%;
transform: translateY(-50%);
}
@media screen and (forced-colors: active) {
fill: CanvasText;
}
}
}
&[data-filter-hidden] {
display: none;
}
@include media(">=uol-media-l") {
font-family: $font-family-sans-serif--desktop;
}
}
//Stop onload flash
.js {
.uol-form__input--select {
&[hidden] {
display: none;
}
// Enable next five lines to show native select
// &:not([data-native-select]) {
// display: block;
// height: 200px !important;
// margin-top: 250px !important;
// }
}
}
// Modifier for uol-chips, when appearing in input field they have associated boundary and other
.uol-chips--inInput {
padding-top: $spacing-3;
padding-left: 6px;
border: 1px solid $color-border;
border-bottom: none;
border-radius: 6px;
margin: 0 0 -8px 0;
background-color: #fff;
@include media(">=uol-media-s") {
max-width: calc(27.375rem - 8px);
}
}
.uol-typeahead--panelOpenChips {
// Increase specificity
&.uol-typeahead {
border-top: 0;
border-radius: 0;
}
}
.uol-typeahead--panelOpenNoChips {
// Increase specificity
&.uol-typeahead {
border-radius: 6px 6px 0 0;
}
&::before {
border-radius: 0;
}
}
.uol-typeahead--panelClosedChips {
// Increase specificity
&.uol-typeahead {
border-top: 0;
border-radius: 0 0 6px 6px;
}
&::before {
border-radius: 0;
}
}
.uol-typeahead--panelClosedNoChips {
// Increase specificity
&.uol-typeahead {
border-radius: 6px 6px 6px 6px;
}
}
.uol-typeahead__input--no-chips--panel-open {
// Important overwrites JS setting of values for generic (chips) version
padding-top: 50px !important;
height: 100px !important;
}
.uol-typeahead__input--no-chips--panel-closed {
// Important overwrites JS setting of values for generic (chips) version
padding-top: 50px !important;
height: 50px !important;
}
.uol-typeahead__select--no-chips--panel-open {
top: 80px;
}
.uol-typeahead__select--no-chips--panel-closed {
top: 30px;
}
.uol-typeahead__count-num--no-chips--panel-open {
border-bottom: 1px solid #51504c;
padding-bottom: 8px;
}
.uol-typeahead__count-num--no-chips--panel-closed {
border-bottom: none;
padding-bottom: 0;
}
.uol-form__input-wrapper__num-of-selected-message {
@extend .uol-typography-paragraph;
display: block;
width: calc(100% - #{$spacing-3});
padding-left: $spacing-3;
position: absolute;
left: 0;
top: 12px;
color: $color-font--light;
pointer-events: none;
@include media(">=uol-media-m") {
top: 10px;
}
@include media(">=uol-media-l") {
top: 8px;
}
}
// native select in search form
.uol-form__input-wrapper--search-typeahead {
.uol-form__input--select {
height: 44px;
padding-bottom: 0;
@include media("<=uol-media-m") {
margin-bottom: $spacing-6;
}
}
}
// Enable if want to see default multiselect
// .uol-form__input--select {
// margin-top: 200px;
// height: 10rem !important;
// }
// .uol-chips {
// margin: 0;
// padding: 0;
// }
// .uol-chips__item {
// display: inline-block;
// margin: 0 $spacing-3 $spacing-3;
// }
// .uol-chips__button {
// display: flex;
// align-items: center;
// justify-content: center;
// position: relative;
// border: none;
// border-radius: 6px;
// padding: $spacing-1 $spacing-7 $spacing-1 $spacing-2;
// background-color: #699B9D;
// cursor: pointer;
// &::before {
// content: "\00d7";
// box-sizing: border-box;
// position: absolute;
// right: $spacing-3;
// border: 1px solid $color-brand-2--dark;
// border-radius: 50%;
// height: 1.75rem;
// width: 1.75rem;
// font-size: 1.75rem;
// line-height: 0.7;
// color: $color-font--dark;
// background-color: $color-white;
// }
// }
.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;
}
}
}
const monthLabels = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
// Adds the button to trigger the datepicker
const datePickerAddButtons = (datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection')
const toggleBtnInnerText = `Calendar, choose ${isDateRange ? 'dates' : 'date'}`;
const button = document.createElement('button');
button.classList.add("uol-button");
button.setAttribute("type", "button");
button.classList.add("uol-button--secondary");
button.classList.add("uol-datepicker__toggle-btn");
button.innerHTML = `
<span class="hide-accessible uol-datepicker__toggle-btn--text">${toggleBtnInnerText}</span>
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1.75em" height="1.75em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none"/><path d="M7 11h2v2H7v-2zm14-5v14c0 1.1-.9 2-2 2H5c-1.11 0-2-.9-2-2l.01-14c0-1.1.88-2 1.99-2h1V2h2v2h8V2h2v2h1c1.1 0 2 .9 2 2zM5 8h14V6H5v2zm14 12V10H5v10h14zm-4-7h2v-2h-2v2zm-4 0h2v-2h-2v2z"/>
</svg>
<span class="uol-datepicker__btn-label" aria-hidden="true">Calendar</span>
`;
let inputWrapper;
if (isDateRange) {
inputWrapper = datePicker.querySelector('.uol-datepicker__unified-input-wrapper')
} else {
inputWrapper = datePicker.querySelector('.uol-datepicker__controls-wrapper')
}
if (inputWrapper) {
button.addEventListener( 'click', (event) => {
const inputComponent = event.target.closest(".uol-datepicker-container");
if (!datePicker.hasAttribute('data-modal-open')) {
datePickerOpenModal(inputComponent);
}
})
inputWrapper.appendChild(button)
}
}
/**
* Returns the modal outer to contain the date picker
* @returns {element} Modal element
*/
const datePickerModalOuter = (datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
const modal = document.createElement("div");
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
if (isDateRange) {
modal.setAttribute("aria-label", "Choose dates");
} else {
modal.setAttribute("aria-label", "Choose date");
}
modal.classList.add("uol-datepicker");
return modal;
};
const datePickerModalHeader = (datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
return `
<div class="uol-datepicker__header">
${
isDateRange
? datePickerModalStartEndToggle(datePicker.id)
: ""
}
${datePickerModalNav(datePicker.id)}
</div>
`;
}
const datePickerModalStartEndToggle = id => {
return `
<fieldset class="uol-datepicker__toggle-container" data-toggle="start">
<legend class="hide-accessible">Toggle start or end dates, selecting a start date will advance selection to end date</legend>
<span class="uol-datepicker__toggle-item">
<input class="uol-datepicker__toggle-radio" type="radio" id="${id}-start-toggle" name="toggle" value="start" checked>
<label class="uol-datepicker__toggle-label" for="${id}-start-toggle">
<span class="uol-datepicker__toggle-label--upper">Start date</span>
<span class="uol-datepicker__toggle-label--lower uol-datepicker__toggle-label--start">dd/mm/yyyy</span>
</label>
</span>
<span class="uol-datepicker__toggle-item">
<input class="uol-datepicker__toggle-radio" type="radio" id="${id}-end-toggle" name="toggle" value="end">
<label class="uol-datepicker__toggle-label" for="${id}-end-toggle">
<span class="uol-datepicker__toggle-label--upper">End date</span>
<span class="uol-datepicker__toggle-label--lower uol-datepicker__toggle-label--end">dd/mm/yyyy</span>
</label>
</span>
</fieldset>
`;
};
const datePickerModalNav = id => {
return `
<div class="uol-datepicker__nav">
<button type="button" class="uol-datepicker__prev-year" aria-label="previous year">
<svg style="width:24px; height:24px" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M18.41,7.41L17,6L11,12L17,18L18.41,16.59L13.83,12L18.41,7.41M12.41,7.41L11,6L5,12L11,18L12.41,16.59L7.83,12L12.41,7.41Z" />
</svg>
</button>
<button type="button" class="uol-datepicker__prev-month" aria-label="previous month">
<svg style="width:24px; height:24px" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" />
</svg>
</button>
<h2 id="${id}-title" class="uol-datepicker__grid-title" aria-live="polite"></h2>
<button type="button" class="uol-datepicker__next-month" aria-label="next month">
<svg style="width:24px;height:24px" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z" />
</svg>
</button>
<button type="button" class="uol-datepicker__next-year" aria-label="next year">
<svg style="width:24px;height:24px" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path fill="currentColor" d="M5.59,7.41L7,6L13,12L7,18L5.59,16.59L10.17,12L5.59,7.41M11.59,7.41L13,6L19,12L13,18L11.59,16.59L16.17,12L11.59,7.41Z" />
</svg>
</button>
</div>
`;
}
const datePickerModalCalendar = (datepickerID) => {
const dayLabels = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ];
const calendar = document.createElement("table");
const tHead = calendar.createTHead();
const tBody = calendar.createTBody();
calendar.classList.add("uol-datepicker__grid");
calendar.setAttribute("role", "grid");
calendar.setAttribute("aria-labelledby", `${datepickerID}-title`);
calendar.appendChild(tHead);
calendar.appendChild(tBody);
for (let tr = 0; tr < 7; tr++) {
const row = document.createElement('tr');
tHead.appendChild(row)
if (tr == 0) {
row.classList.add('uol-datepicker__header-row');
for (let th = 0; th < 7; th++) {
let headerCell = `
<th class="uol-datepicker__header-cell" scope="col">
<span aria-hidden="true">${dayLabels[th].substr(0,2)}</span>
<span class="hide-accessible">${dayLabels[th]}</span>
</th>`
row.insertAdjacentHTML('beforeend', headerCell);
}
}
if (tr > 0) {
tBody.classList.add('uol-datepicker__grid-body');
row.classList.add('uol-datepicker__row');
if (tr == 6) {
row.setAttribute('data-last-row', '');
}
for (let td = 0; td < 7; td++) {
const cell = document.createElement('td');
cell.tabIndex = -1;
cell.textContent = '-1';
cell.classList.add('uol-datepicker__cell');
row.appendChild(cell);
}
tBody.appendChild(row)
}
}
return calendar;
};
const datePickerModalFooter = `
<div class="uol-datepicker__footer">
<button type="button" class="uol-button uol-button--secondary uol-datepicker__cancel-btn" value="cancel">
Cancel
</button>
<button type="button" class="uol-button uol-button--primary uol-datepicker__confirm-btn" value="ok">
Confirm
</button>
<div class="hide-accessible uol-datepicker__announcement" aria-live="polite"></div>
</div>
`;
const datePickerOpenModal = (datePicker) => {
// Create modal
const modal = datePickerModalOuter(datePicker);
datePicker.setAttribute('data-modal-open', '');
// Add modal header including navigation and optional start end toggle
modal.innerHTML += datePickerModalHeader(datePicker);
// Add table
modal.appendChild(datePickerModalCalendar(datePicker.id));
// Add footer
modal.innerHTML += datePickerModalFooter;
modal.classList.add('uol-datepicker--fade-in');
datePicker.appendChild(modal);
attachRadioToggleEventListeners(modal, datePicker.id);
attachModalEventListeners(modal);
attachDateCellEventListeners(modal);
const wrapper = document.createElement('div');
wrapper.classList.add('uol-datepicker-background');
document.querySelector('.site-outer').appendChild(wrapper);
updateGrid(datePicker, getInitialFocusDay(datePicker));
setDateRange(datePicker);
if (datePicker.hasAttribute('data-range-selection')) {
setTimeout(() => {
if (datePicker.querySelector('.uol-datepicker__announcement')) {
datePicker.querySelector('.uol-datepicker__announcement').innerHTML = 'Toggle the start and end radio buttons to switch between start or end date selection';
}
}, 1000);
}
};
const attachRadioToggleEventListeners = (modal, id) => {
const isDateRange = modal.querySelector('fieldset');
if (isDateRange) {
const endToggle = modal.querySelector(`#${id}-end-toggle`);
const startToggle = modal.querySelector(`#${id}-start-toggle`);
startToggle.addEventListener('keydown', (evt) => {
handleRadioButtons(evt, modal)
});
startToggle.addEventListener('change', (evt) => {
handleRadioButtons(evt, modal)
});
endToggle.addEventListener('keydown', (evt) => {
handleRadioButtons(evt, modal)
});
endToggle.addEventListener('change', (evt) => {
handleRadioButtons(evt, modal)
});
modal.querySelector('fieldset').setAttribute('data-toggle', 'start');
startToggle.checked = true;
}
}
const attachDateCellEventListeners = (modal) => {
modal.querySelectorAll('.uol-datepicker__cell').forEach(cell => {
cell.addEventListener('keydown', (evt) => {
delegateDateCellEvents(evt, modal);
handleDaySelection(evt, modal);
});
cell.addEventListener('click', (evt) => {
handleDaySelection(evt, modal)
});
cell.addEventListener('mouseover', (evt) => {
handleRangeEvent(evt, modal)
});
cell.addEventListener('focus', (evt) => {
handleRangeEvent(evt, modal)
});
cell.addEventListener('blur', removeTemporaryFocusFromCell);
});
}
const attachModalEventListeners = (modal) => {
modal.querySelector(".uol-datepicker__nav").addEventListener('click', (evt) => {
delegateNavigationEvents(evt)
});
modal.querySelector(".uol-datepicker__nav").addEventListener('keydown', (evt) => {
delegateNavigationEvents(evt)
});
modal.addEventListener('keydown', handleEscapePress)
modal.querySelector('.uol-datepicker__confirm-btn').addEventListener('click', handleConfirmBtn);
modal.querySelector('.uol-datepicker__confirm-btn').addEventListener('keydown', handleConfirmBtn);
modal.querySelector('.uol-datepicker__cancel-btn').addEventListener('click', handleCancelButton);
modal.querySelector('.uol-datepicker__cancel-btn').addEventListener('keydown', handleCancelButton);
window.addEventListener('click', handleClickOutside);
}
const attachEventListenersToInputs = (datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
datePicker.querySelector('.uol-datepicker__input--start').addEventListener('blur', (evt) => {
setDateForButtonLabel(evt, datePicker)
});
if (isDateRange) {
datePicker.querySelector('.uol-datepicker__input--start').addEventListener('keydown', advanceToEndInput);
datePicker.querySelector('.uol-datepicker__input--end').addEventListener('blur', (evt) => {
setDateForButtonLabel(evt, datePicker)
});
}
}
// If inputs contain user input, sets the aria invalid if they have arrors, retains correct button name if they are valid
window.onload = event => {
document.querySelectorAll('.uol-datepicker-container').forEach(datePicker => {
let inputField;
if (datePicker.hasAttribute('data-range-selection')) {
inputField = datePicker.querySelector('.uol-datepicker__unified-input-wrapper');
} else {
inputField = datePicker.querySelector('.uol-datepicker__input--start');
}
if (datePicker.hasAttribute('data-invalid')) {
if (datePicker.hasAttribute('data-range-selection')) {
inputField.setAttribute('aria-invalid', true);
} else {
inputField.setAttribute('aria-invalid', true)
}
}
setDateForButtonLabel(event, datePicker);
})
}
const datePickerCloseModal = (event, clearDates) => {
let modal;
if (event.target.classList.contains('uol-datepicker-background')) {
modal = event.target.parentNode.querySelector('.uol-datepicker-container[data-modal-open]').querySelector('.uol-datepicker');
} else {
modal = event.target.closest(".uol-datepicker");
}
document.querySelector('.uol-datepicker-background').classList.add('uol-datepicker-background--fade-out');
modal.classList.add('uol-datepicker--fade-out');
modal.classList.remove('uol-datepicker--fade-in');
modal.closest('.uol-datepicker-container').removeAttribute('data-modal-open');
if (clearDates) {
cancelPickedSelection(modal);
}
modal.parentNode.querySelector('.uol-datepicker__toggle-btn').focus();
window.removeEventListener('click', handleClickOutside);
setTimeout(() => {
modal.remove();
document.querySelector('.uol-datepicker-background').remove();
}, 200);
}
// Compares 2 dates that are passed in, only testing the day, month and year part of the date object
const isSameDay = (day1, day2) => {
return (
day1.getFullYear() == day2.getFullYear() &&
day1.getMonth() == day2.getMonth() &&
day1.getDate() == day2.getDate()
);
};
// When the datepicker opens or moves to diffeerent months/years, applies the classes, attributes and correct dates to each cell
const updateGrid = (datePicker, focusOnDay) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
const days = datePicker.querySelectorAll(".uol-datepicker__cell");
let dayIsNotInCurrentMonth;
const monthAndYearToDisplay = focusOnDay;
const firstDayOfMonth = new Date(monthAndYearToDisplay.getFullYear(), monthAndYearToDisplay.getMonth(), 1);
const dayOfWeek = firstDayOfMonth.getDay();
firstDayOfMonth.setDate(firstDayOfMonth.getDate() - dayOfWeek);
const firstOfMonth = new Date(firstDayOfMonth);
if (datePicker.querySelector('.uol-datepicker__grid-title')) {
datePicker.querySelector('.uol-datepicker__grid-title').textContent = `${monthLabels[monthAndYearToDisplay.getMonth()]} ${monthAndYearToDisplay.getFullYear()}`;
}
const endDate = getEndDate(datePicker);
const startDate = getStartDate(datePicker);
days.forEach( (day, idx) => {
dayIsNotInCurrentMonth = firstOfMonth.getMonth() != monthAndYearToDisplay.getMonth();
updateDate(day, dayIsNotInCurrentMonth, firstOfMonth);
firstOfMonth.setDate(firstOfMonth.getDate() + 1);
if (day.hasAttribute("data-not-in-range")) {
day.classList.add("uol-datepicker__cell--unavailable");
day.setAttribute("aria-disabled", true);
}
highlightToday(day);
setEmptyLastRowToBlank(idx, dayIsNotInCurrentMonth, datePicker);
checkForExistingSelection(isDateRange, day, datePicker, startDate, endDate);
if (datePicker.hasAttribute('data-future-dates-only')) {
allowFutureDatesOnly(day);
}
});
if (datePicker.hasAttribute('data-unavailable-dates')) {
setUnavailableDates(datePicker);
}
setFocusDay(datePicker, focusOnDay);
days.forEach( day => {
appendCellAccessibleName(day);
})
};
// Sets aria-current to today and adds small acceent to cell
const highlightToday = (day) => {
if (day.getAttribute('data-date').split('/').reverse().join('-') == new Date().toISOString().replace(/T.*/,'')) {
day.classList.add('uol-datepicker__cell--current-day');
day.setAttribute('aria-current', 'date');
}
}
// Ensures the last row is blank, when the last day of current month is on the 5th row, to maintain consistent size - always 6 rows
const setEmptyLastRowToBlank = (idx, dayIsNotInCurrentMonth, datePicker) => {
if (idx === 35) {
const lastRowNode = datePicker.querySelector('[data-last-row]')
if (dayIsNotInCurrentMonth) {
lastRowNode.classList.add("uol-datepicker__row--blank");
} else {
lastRowNode.classList.remove("uol-datepicker__row--blank");
}
}
}
// If a user has previously entered dates or filled in inputs, sets the selections when the picker opens
const checkForExistingSelection = (isDateRange, day, datePicker, start, end) => {
if (isDateRange) {
deactivateCellsBeforeStart(day, datePicker);
if (day.getAttribute("data-date") == start) {
setStartCell(day, datePicker);
}
if (day.getAttribute("data-date") == end) {
setEndCell(day, datePicker);
}
setRangeCells(day, datePicker);
} else {
if (day.getAttribute("data-date") == start) {
setStartCell(day, datePicker);
}
}
}
// If there rae unavailable dates, this gets the string of ISO dates and matches against dates, disabling those dates
const setUnavailableDates = (datePicker) => {
datePicker.querySelectorAll('.uol-datepicker__cell').forEach(cell => {
let daysToDisable = datePicker.getAttribute('data-unavailable-dates');
if (daysToDisable.includes(cell.getAttribute('data-date').split('/').reverse().join('-'))) {
if (!cell.classList.contains('uol-datepicker__cell--empty')) {
cell.classList.add('uol-datepicker__cell--unavailable');
cell.setAttribute('aria-disabled', true);
cell.setAttribute('data-date-unavailable', '');
}
}
});
}
// If the datepicker should not let a user pick a date in the past, this disables all cells before the current day
const allowFutureDatesOnly = (day) => {
let today = new Date();
today.setHours(0,0,0,0);
if (getDateFromValue(day, true) < today) {
if (!day.classList.contains('uol-datepicker__cell--empty')) {
day.classList.add('uol-datepicker__cell--unavailable');
day.setAttribute('aria-disabled', true);
day.setAttribute('data-date-passed', '');
}
}
}
// The end date is set as a data-attribute on the datepicker, this function is called to check it has been set
const getEndDate = (datePicker) => {
if (datePicker.getAttribute('data-end-date') != 'false') {
return datePicker.getAttribute('data-end-date')
} else {
return false;
}
}
// The start date is set as a data-attribute on the datepicker, this function is called to check it has been set
const getStartDate = (datePicker) => {
if (datePicker.getAttribute('data-start-date') != 'false') {
return datePicker.getAttribute('data-start-date')
} else {
return false;
}
}
// This refreshes the grid, by stripping out all classes/attributes that are related to a specific date, prior to updateDate reapplying them
const updateDate = ( domNode, disable, day ) => {
let d = day.getDate().toString();
if (day.getDate() <= 9) { d = `0${d}` }
let m = day.getMonth() + 1;
if (day.getMonth() < 9) { m = `0${m}` }
domNode.tabIndex = -1;
domNode.removeAttribute('data-selected');
domNode.removeAttribute('data-selected-start');
domNode.removeAttribute('data-selected-end');
domNode.removeAttribute('data-selected-date');
domNode.removeAttribute('data-date-unavailable');
domNode.removeAttribute('data-date-passed');
domNode.removeAttribute('aria-current');
domNode.removeAttribute('data-not-in-range');
domNode.removeAttribute('aria-disabled');
domNode.removeAttribute('data-single-day')
domNode.classList.remove('uol-datepicker__cell--end-range');
domNode.classList.remove('uol-datepicker__cell--current-day');
domNode.classList.remove('uol-datepicker__cell--unavailable');
domNode.classList.remove('uol-datepicker__cell--in-range');
domNode.classList.remove('uol-datepicker__cell--passthrough');
domNode.classList.remove('uol-datepicker__cell--has-focus');
domNode.setAttribute('data-date', `${d}/${m}/${day.getFullYear()}`);
if (disable) {
domNode.classList.add('uol-datepicker__cell--empty');
domNode.textContent = '';
} else {
domNode.classList.remove('uol-datepicker__cell--empty');
domNode.innerHTML = `<span aria-hidden="true">${d.replace(/^0+/, '')}</span><span class="hide-accessible">${setAccessibleCellLabel(domNode.getAttribute('data-date'))}</span>`;
}
};
// moves focus to new date, based upon keyboard interactions or using nav buttons
const moveFocusToDay = (day, datePicker) => {
updateGrid(datePicker, day);
setFocusDay(datePicker, day);
};
// sets focus to cell with same day, when user changes month or year
const setFocusDay = (datePicker, focusOnDay) => {
datePicker.querySelectorAll('.uol-datepicker__cell').forEach(cell => {
if (isSameDay(getDateFromValue(cell, true), focusOnDay)) {
cell.tabIndex = 0;
cell.focus();
} else {
cell.tabIndex = -1;
}
})
};
// gets the currently focussed date and changes the calendar to the same day and month, in the following year
const moveToNextYear = (datePicker, focusOnDay) => {
focusOnDay.setFullYear(focusOnDay.getFullYear() + 1);
updateGrid(datePicker, focusOnDay);
};
// gets the currently focussed date and changes the calendar to the same day and month, in the previous year
const moveToPreviousYear = (datePicker, focusOnDay) => {
focusOnDay.setFullYear(focusOnDay.getFullYear() - 1);
updateGrid(datePicker, focusOnDay);
};
// gets the currently focussed date and changes the calendar to the same day, in the following month
const moveToNextMonth = (datePicker, focusOnDay) => {
focusOnDay.setMonth(focusOnDay.getMonth() + 1);
updateGrid(datePicker, focusOnDay);
};
// gets the currently focussed date and changes the calendar to the same day, in the previous month
const moveToPreviousMonth = (datePicker, focusOnDay) => {
focusOnDay.setMonth(focusOnDay.getMonth() - 1);
updateGrid(datePicker, focusOnDay);
};
// gets the currently focussed and moves focus to the next day, should a user press right arrow
const moveFocusToNextDay = (modal, focusOnDay) => {
focusOnDay.setDate(focusOnDay.getDate() + 1);
moveFocusToDay(focusOnDay, modal);
};
// gets the currently focussed day and moves focus to the next week, should a user press down arrow
const moveFocusToNextWeek = (modal, focusOnDay) => {
focusOnDay.setDate(focusOnDay.getDate() + 7);
moveFocusToDay(focusOnDay, modal);
};
// gets the currently focussed day and moves focus to the previous day, should a user press left arrow
const moveFocusToPreviousDay = (modal, focusOnDay) => {
focusOnDay.setDate(focusOnDay.getDate() - 1);
moveFocusToDay(focusOnDay, modal);
};
// gets the currently focussed day and moves focus to the next week, should a user press up arrow
const moveFocusToPreviousWeek = (modal, focusOnDay) => {
focusOnDay.setDate(focusOnDay.getDate() - 7);
moveFocusToDay(focusOnDay, modal);
};
// gets the currently focussed day and moves focus to the first day of the week should a user press Home
const moveFocusToFirstDayOfWeek = (modal, focusOnDay) => {
focusOnDay.setDate(focusOnDay.getDate() - focusOnDay.getDay());
moveFocusToDay(focusOnDay, modal);
};
// gets the currently focussed day and moves focus to the last day of the week should a user press End
const moveFocusToLastDayOfWeek = (modal, focusOnDay) => {
focusOnDay.setDate(focusOnDay.getDate() + (6 - focusOnDay.getDay()));
moveFocusToDay(focusOnDay, modal);
};
// Sets attributes and classes to the cell that contains the start date selected or input by a user
const setStartCell = (day, datePicker) => {
const isDateRange = day.closest('.uol-datepicker-container').hasAttribute('data-range-selection');
if (!day.hasAttribute('aria-disabled') && !day.classList.contains('uol-datepicker__cell--empty')) {
day.setAttribute('data-selected', '');
datePicker.setAttribute('data-start-date', day.getAttribute('data-date'));
if (isDateRange) {
day.setAttribute('data-selected-start', '');
} else {
day.setAttribute('data-selected-date', '');
}
}
}
// Sets attributes and classes to the cell that contains the end date selected or input by a user, also announces the selection
const setEndCell = (day, datePicker) => {
const hiddenMessage = datePicker.querySelector('.uol-datepicker__announcement');
const endDate = datePicker.getAttribute('data-end-date');
const startDate = datePicker.getAttribute('data-start-date');
if (!day.hasAttribute('aria-disabled') && !day.classList.contains('uol-datepicker__cell--empty')) {
if (getDateFromValue(endDate) >= getDateFromValue(startDate) || getDateFromValue(startDate) <= getDateFromValue(day, true) || !getStartDate(datePicker) && getEndDate(datePicker)) {
day.setAttribute('data-selected-end', '');
day.setAttribute('data-selected', '');
datePicker.setAttribute('data-end-date', day.getAttribute('data-date'))
}
if (getStartDate(datePicker) && getEndDate(datePicker)) {
if (getDateFromValue(startDate) < getDateFromValue(endDate)) {
hiddenMessage.innerHTML = `Your selected dates are ${getDateFromValue(startDate, false, true)} to ${getDateFromValue(endDate, false, true)}`
} else {
hiddenMessage.innerHTML = `please choose an end date, your end date cannot be before your start date`
}
} else if (!getStartDate(datePicker) && getEndDate(datePicker)) {
hiddenMessage.innerHTML = `Your selected end date is ${getDateFromValue(endDate, false, true)} please toggle the radio button and select a start date`;
}
}
}
// Sets attributes and classes on cells that are in range of the start and end dates
const setRangeCells = (day, datePicker) => {
const endDate = datePicker.getAttribute('data-end-date');
const startDate = datePicker.getAttribute('data-start-date');
if (!day.classList.contains('uol-datepicker__cell--empty')) {
if (getDateFromValue(day, true) > getDateFromValue(startDate) && getDateFromValue(day, true) < getDateFromValue(endDate)) {
day.setAttribute('data-selected', '');
day.classList.add('uol-datepicker__cell--in-range');
day.classList.remove('uol-datepicker__cell--passthrough');
} else {
day.classList.remove('uol-datepicker__cell--in-range');
}
}
}
// Aria-selected has poor support, this communicates selected dates by appending each selected cell's hidden labels
const appendCellAccessibleName = (day) => {
const hiddenAccessibleName = day.querySelector('.hide-accessible');
const accessibleName = setAccessibleCellLabel(day.getAttribute('data-date'));
if (!day.classList.contains('uol-datepicker__cell--empty')) {
if (!day.hasAttribute('data-date-unavailable')) {
if (day.hasAttribute('data-selected-start') && day.hasAttribute('data-selected-end')) {
hiddenAccessibleName.innerHTML = `${accessibleName} selected date`;
} else if (day.hasAttribute('data-selected-start')) {
hiddenAccessibleName.innerHTML = `${accessibleName} selected start date`;
} else if (day.hasAttribute('data-selected-end')) {
hiddenAccessibleName.innerHTML = `${accessibleName} selected end date`;
} else if (day.classList.contains('uol-datepicker__cell--in-range')) {
hiddenAccessibleName.innerHTML = `${accessibleName} selected, in range`;
} else if (day.hasAttribute('data-selected-date')) {
hiddenAccessibleName.innerHTML = `${accessibleName} selected date`;
} else if (day.hasAttribute('data-not-in-range')) {
hiddenAccessibleName.innerHTML = `${accessibleName}. End date cannot be before start date`;
} else {
hiddenAccessibleName.innerHTML = accessibleName;
}
} else {
hiddenAccessibleName.innerHTML = `${accessibleName} this date is unavailable`;
}
}
}
// Converts dd/mm/yyyy, into a date string, if the data is taken from a cell's data-date attribute, isDomNode should be true, if the order required is weekday, date, month, year, isDateString should be set to true
const getDateFromValue = (value, isDomNode, isDateString) => {
let parts;
if (isDomNode) {
parts = value.getAttribute('data-date').split('/');
} else {
parts = value.split('/');
}
if (isDateString) {
return new Date(parts[2], parseInt(parts[1]) - 1, parts[0]).toLocaleDateString("en-GB", {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'});
} else {
return new Date(parts[2], parseInt(parts[1]) - 1, parts[0]);
}
};
// Sets a hidden accessible name for a date cell, ensuring the first part of the date, the number matches the visible label of the cell
const setAccessibleCellLabel = (date) => {
const parts = date.split('/');
const options = {day: "numeric", month: "long", year: "numeric", weekday: "long"}
const dateLabel = new Date(`${parts[2]}-${parts[1]}-${parts[0]}`).toLocaleDateString("en-GB", options);
return `${dateLabel.split(' ')[1]} ${dateLabel.split(', ')[0]}`;
}
// The radio buttons show the currently selected dates, this adds the correct date/format to the radio label upon selection/input
const setRadioDateLabels = (domNode, modal) => {
const isDateRange = modal.closest('.uol-datepicker-container').hasAttribute('data-range-selection');
const startToggle = modal.querySelector('[value="start"]');
const endToggle = modal.querySelector('[value="end"]');
const startToggleLabel = modal.querySelector('.uol-datepicker__toggle-label--start');
const endToggleLabel = modal.querySelector('.uol-datepicker__toggle-label--end');
if (isDateRange) {
if (startToggle.checked) {
startToggleLabel.textContent = domNode.getAttribute('data-date');
} else if (endToggle.checked) {
endToggleLabel.textContent = domNode.getAttribute('data-date');
}
}
}
// Sets ranges when isDateRange is true and user enters or selects start dates
const setDateRange = (datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
const endDate = datePicker.getAttribute('data-end-date');
const startDate = datePicker.getAttribute('data-start-date');
datePicker.querySelectorAll('.uol-datepicker__cell').forEach((cell, idx) => {
if (getStartDate(datePicker) && getEndDate(datePicker)) {
if (startDate != endDate) {
cell.removeAttribute('data-single-day');
} else {
if (cell.getAttribute('data-date') == startDate && cell.getAttribute('data-date') == endDate) {
cell.setAttribute('data-single-day', '');
cell.removeAttribute('data-selected-start');
cell.removeAttribute('data-selected-end');
}
}
if (cell.getAttribute('data-date') == startDate) {
setStartCell(cell, datePicker);
}
if (cell.getAttribute('data-date') == endDate) {
setEndCell(cell, datePicker);
}
setRangeCells(cell, datePicker);
} else if (getStartDate(datePicker) && !getEndDate(datePicker)) {
if (cell.getAttribute('data-date') == startDate) {
setStartCell(cell, datePicker);
}
if (isDateRange) {
deactivateCellsBeforeStart(cell, datePicker);
advanceToggleToEndRadio(idx, datePicker);
}
}
})
}
// Prevents multiple start/end cells being selected by resetting the previous classes should a user click another cell
const preventMultipleSelection = (domNode, modal) => {
const startToggle = modal.querySelector('[value="start"]');
const endToggle = modal.querySelector('[value="end"]');
const datePicker = modal.closest('.uol-datepicker-container')
const isDateRange = datePicker.hasAttribute('data-range-selection');
modal.querySelectorAll('.uol-datepicker__cell').forEach((day) => {
const resetStartDate = (clearStartDay) => {
clearStartDay.removeAttribute('data-selected-start');
clearStartDay.removeAttribute('data-selected-date');
clearStartDay.classList.remove('uol-datepicker__cell--in-range');
clearStartDay.setAttribute('tabindex', '-1');
}
if (isDateRange) {
if ( startToggle.checked ) {
day === domNode ? setStartCell(day, datePicker) : resetStartDate(day);
} else if ( endToggle.checked ) {
if (day === domNode) {
setEndCell(day, datePicker)
} else {
day.removeAttribute('data-selected-end');
day.classList.remove('uol-datepicker__cell--end-range');
day.classList.remove('uol-datepicker__cell--in-range');
day.setAttribute('tabindex', '-1');
day.removeAttribute('data-selected');
}
}
} else {
day === domNode ? setStartCell(day, datePicker) : resetStartDate(day);
}
});
}
// When the picker is open, prevents a user selecting an end date, that is before the start date by disabling all cells that preceed the start date
const deactivateCellsBeforeStart = (cell, datePicker) => {
const endToggle = datePicker.querySelector('.uol-datepicker__toggle-radio[value="end"]');
const startDate = datePicker.getAttribute('data-start-date');
if (!cell.classList.contains('uol-datepicker__cell--empty')) {
if (getDateFromValue(cell, true) < getDateFromValue(startDate)) {
if (endToggle.checked) {
cell.classList.add('uol-datepicker__cell--unavailable');
cell.setAttribute('aria-disabled', 'true');
cell.setAttribute('data-not-in-range', '');
} else {
if (!cell.hasAttribute('data-date-unavailable') && !cell.hasAttribute('data-date-passed')) {
cell.classList.remove('uol-datepicker__cell--unavailable');
cell.removeAttribute('aria-disabled');
cell.removeAttribute('data-not-in-range');
}
}
}
}
}
// Validates user input, if a user selects an invalid date, it will open on the current day, as opposed to JS guessing what they meant
const validateDatesFromInputs = (input) => {
const parts = input.value.split('/');
let day = parseInt(parts[0]);
let month = parseInt(parts[1]);
const year = parseInt(parts[2]);
if (input.value.length == 10) {
if ( parts.length === 3 && Number.isInteger(day) && Number.isInteger(month) && Number.isInteger(year) ) {
let daysInMonth = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
if (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)) {
daysInMonth[1] = 29;
}
if (( day > 0) && day <= daysInMonth[month - 1]) {
if (day <= 9) { day = `0${day}` }
if (month <= 9) { month = `0${month}` }
return `${day}/${month}/${year}`;
} else {
return '';
}
}
} else {
return '';
}
}
// When the calendar opens, checks if valid dates are in the inputs and determines the focus day, else will open on the current date
const getInitialFocusDay = (datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
const startInput = datePicker.querySelector('.uol-datepicker__input--start');
const endInput = datePicker.querySelector('.uol-datepicker__input--end');
if (isDateRange && validateDatesFromInputs(startInput) && validateDatesFromInputs(endInput)) {
datePicker.querySelector('.uol-datepicker__toggle-label--end').textContent = endInput.value;
datePicker.querySelector('.uol-datepicker__toggle-label--start').textContent = startInput.value;
return getDateFromValue(validateDatesFromInputs(startInput));
} else if (validateDatesFromInputs(startInput)) {
if (isDateRange) {
datePicker.querySelector('.uol-datepicker__toggle-label--start').textContent = startInput.value;
datePicker.setAttribute('data-end-date', false);
}
return getDateFromValue(validateDatesFromInputs(startInput));
} else if (isDateRange && validateDatesFromInputs(endInput)) {
datePicker.querySelector('.uol-datepicker__toggle-label--end').textContent = endInput.value;
datePicker.setAttribute('data-start-date', false);
return getDateFromValue(validateDatesFromInputs(endInput));
} else {
return new Date();
}
}
// Sets the date(s), from the picker into the input(s)
const populateInputs = (datePicker) => {
const endDate = datePicker.getAttribute('data-end-date');
const startDate = datePicker.getAttribute('data-start-date');
if (getStartDate(datePicker)) {
datePicker.querySelector('.uol-datepicker__input--start').value = startDate;
}
if (getEndDate(datePicker)) {
datePicker.querySelector('.uol-datepicker__input--end').value = endDate;
}
}
// Handles updating the button's hidden label contain the selected dates, so a screen reader user would hear the dates as a string on the button
const setDateForButtonLabel = (event, datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
const buttonText = datePicker.querySelector('.uol-datepicker__toggle-btn--text');
const startInput = datePicker.querySelector('.uol-datepicker__input--start');
const endInput = datePicker.querySelector('.uol-datepicker__input--end');
const validStartDate = validateDatesFromInputs(startInput);
let validEndDate;
if (isDateRange) {
validEndDate = validateDatesFromInputs(endInput);
}
if (event && event.type == 'blur') {
if (event.target == startInput) {
if (validStartDate) {
datePicker.setAttribute('data-start-date', startInput.value);
} else {
datePicker.setAttribute('data-start-date', false);
}
} else if (validEndDate) {
datePicker.setAttribute('data-end-date', endInput.value);
} else {
datePicker.setAttribute('data-end-date', false);
}
}
if (event && event.type == 'load') {
datePicker.setAttribute('data-start-date', startInput.value);
if (isDateRange) {
datePicker.setAttribute('data-end-date', endInput.value)
}
}
if ( getStartDate(datePicker) && !isDateRange || (isDateRange && getStartDate(datePicker) && !getEndDate(datePicker))) {
if (isDateRange) {
buttonText.textContent = `Calendar, only start date chosen, ${getDateFromValue(validStartDate, false, true)}, please provide an end date`;
} else {
buttonText.textContent = `Calendar, chosen date ${getDateFromValue(validStartDate, false, true)}`;
}
} else if (isDateRange && getStartDate(datePicker) && getEndDate(datePicker)) {
buttonText.textContent = `Calendar, chosen dates ${getDateFromValue(validStartDate, false, true)} to ${getDateFromValue(validEndDate, false, true)}`;
} else if (isDateRange && !getStartDate(datePicker) && getEndDate(datePicker)) {
buttonText.textContent = `Calendar, only end date chosen, ${getDateFromValue(validEndDate, false, true)}, please provide a start date`
} else {
buttonText.textContent = `Calendar, choose ${isDateRange ? 'dates' : 'date'}`;
}
};
// Prevents selected dates populating inputs & radio labels, if user closes the modal without hitting confirm
const cancelPickedSelection = function (modal) {
const isDateRange = modal.closest('.uol-datepicker-container').hasAttribute('data-range-selection');
const startInput = modal.closest('.uol-datepicker-container').querySelector('.uol-datepicker__input--start');
const endInput = modal.closest('.uol-datepicker-container').querySelector('.uol-datepicker__input--end');
const hiddenMessage = modal.querySelector('.uol-datepicker__announcement');
const startToggleLabel = modal.querySelector('.uol-datepicker__toggle-label--start');
const endToggleLabel = modal.querySelector('.uol-datepicker__toggle-label--end');
const endDate = modal.closest('.uol-datepicker-container').getAttribute('data-end-date');
const startDate = modal.closest('.uol-datepicker-container').getAttribute('data-start-date');
if (startInput.value !== startDate) {
modal.closest('.uol-datepicker-container').setAttribute('data-start-date', startInput.value);
if (isDateRange) {
setTimeout(() => {
startToggleLabel.textContent = 'dd/mm/yyyy';
}, 100);
}
}
if (isDateRange && endInput.value !== endDate) {
modal.closest('.uol-datepicker-container').setAttribute('data-end-date', endInput.value);
setTimeout(() => {
endToggleLabel.textContent = 'dd/mm/yyyy';
}, 100);
}
hiddenMessage.innerHTML = '';
}
// handles auto-advancing the radio from Start to End, upon a user selecting a start date
const advanceToggleToEndRadio = (idx, datePicker) => {
const hiddenMessage = datePicker.querySelector('.uol-datepicker__announcement');
const endToggle = datePicker.querySelector('.uol-datepicker__toggle-radio[value="end"]');
const isDateRange = datePicker.hasAttribute('data-range-selection');
const startDate = datePicker.closest('.uol-datepicker-container').getAttribute('data-start-date');
if (isDateRange) {
if (idx === 0 && !getEndDate(datePicker)) {
hiddenMessage.innerHTML = `Selected start date ${getDateFromValue(startDate, false, true)}, please select an end date or toggle the radio button to edit your selected start date`;
}
endToggle.checked = true;
endToggle.closest('fieldset').setAttribute('data-toggle', 'end');
}
}
// Event listener functions
// Stops bubbling and default behaviour
const stopPropagationPreventDefault = (event) => {
event.stopPropagation();
event.preventDefault();
}
const handleEscapePress = (event) => {
if (event.key == 'Esc' || event.key == 'Escape') {
datePickerCloseModal(event, true);
}
}
// Handles advancing from start input to end, if user presses space or dash
const advanceToEndInput = (event) => {
const endInput = event.target.closest('.uol-datepicker-container').querySelector('.uol-datepicker__input--end')
if (event.target.value.length == 10 && endInput.value == '') {
if (event.key == ' ' || event.key == 'Minus' || event.keyCode == 109 || event.keyCode == 189) {
event.preventDefault()
endInput.focus();
}
}
}
// Determines the event/key and delegates to the correct function, such as nav clicks, focus trapping
const delegateNavigationEvents = (event) => {
const datePicker = event.target.closest('.uol-datepicker-container');
if (event.type == 'click') {
handleNavigationClick(event, datePicker)
}
if ((event.key == 'Tab' && event.shiftKey) && event.target == datePicker.querySelector('.uol-datepicker__prev-year')) {
handleNavigationFocusTrap(event, datePicker)
}
if (event.key == 'Tab' && !event.shiftKey && event.target == datePicker.querySelector('.uol-datepicker__next-year')) {
addTemporaryFocusToCell(event);
}
}
// Handles click events within the calendar navigation for next/prev year/month
const handleNavigationClick = (event, datePicker) => {
const focusOnDay = getDateFromValue(datePicker.querySelector('.uol-datepicker__cell[tabindex="0"]'), true);
if (event.target == datePicker.querySelector('.uol-datepicker__prev-month')) {
moveToPreviousMonth(datePicker, focusOnDay);
datePicker.querySelector('.uol-datepicker__prev-month').focus();
} else if (event.target == datePicker.querySelector('.uol-datepicker__next-month')) {
moveToNextMonth(datePicker, focusOnDay);
datePicker.querySelector('.uol-datepicker__next-month').focus();
} else if (event.target == datePicker.querySelector('.uol-datepicker__prev-year')) {
moveToPreviousYear(datePicker, focusOnDay);
datePicker.querySelector('.uol-datepicker__prev-year').focus();
} else if (event.target == datePicker.querySelector('.uol-datepicker__next-year')) {
moveToNextYear(datePicker, focusOnDay);
datePicker.querySelector('.uol-datepicker__next-year').focus();
}
}
// traps focus to the correct element, dependant on whether radio buttons are present
const handleNavigationFocusTrap = (event, datePicker) => {
const isDateRange = datePicker.hasAttribute('data-range-selection');
if (isDateRange) {
datePicker.querySelector('[value="start"]').checked ? datePicker.querySelector('[value="start"]').focus() : datePicker.querySelector('[value="end"]').focus();
} else {
datePicker.querySelector('.uol-datepicker__confirm-btn').focus();
}
stopPropagationPreventDefault(event);
}
// Handles selection of dates within the calendar, with keyboard or pointing device
const handleDaySelection = (event, modal) => {
if (event.type == 'click' || event.key == ' ' || event.key == 'Enter') {
const isDateRange = modal.closest('.uol-datepicker-container').hasAttribute('data-range-selection');
if (!event.currentTarget.hasAttribute('aria-disabled')) {
setRadioDateLabels(event.currentTarget, modal);
preventMultipleSelection(event.currentTarget, modal);
setDateRange(modal.closest('.uol-datepicker-container'));
if (!isDateRange) {
modal.querySelector('.uol-datepicker__announcement').innerHTML =
`Selected date ${getDateFromValue(event.target, true, true)}`;
}
}
modal.querySelectorAll('.uol-datepicker__cell').forEach((cell) => {
appendCellAccessibleName(cell);
if (!event.target.hasAttribute('aria-disabled')) {
if ( cell == event.target ) {
cell.setAttribute('tabindex', '0');
} else {
cell.setAttribute('tabindex', '-1')
}
} else {
if (cell.getAttribute('tabindex') == 0) {
cell.focus();
}
}
});
stopPropagationPreventDefault(event);
}
};
// Calls setHighlightStyleOnRangeCells() to set the background colour on range cells, when only start is set
const handleRangeEvent = (event, modal) => {
const isDateRange = modal.closest('.uol-datepicker-container').hasAttribute('data-range-selection');
const endToggle = modal.querySelector('.uol-datepicker__toggle-radio[value="end"]');
const datePicker = modal.closest('.uol-datepicker-container');
if (isDateRange && event.target.classList.contains('uol-datepicker__cell')) {
if ((getStartDate(datePicker) && !getEndDate(datePicker)) && endToggle.checked) {
setHighlightStyleOnRangeCells(event, modal)
}
}
}
// Sets the background color on cells, when only a start date is selected, if that cell is greater than start
const setHighlightStyleOnRangeCells = (event, modal) => {
const startDate = modal.closest('.uol-datepicker-container').getAttribute('data-start-date');
modal.querySelectorAll('.uol-datepicker__cell').forEach(cell => {
if (getDateFromValue(cell, true) < getDateFromValue(event.target, true) && getDateFromValue(cell, true) > getDateFromValue(startDate)) {
if (!cell.classList.contains('uol-datepicker__cell--empty')) {
cell.classList.add('uol-datepicker__cell--passthrough');
}
} else {
cell.classList.remove('uol-datepicker__cell--passthrough');
}
if (event.target == cell && getDateFromValue(event.target, true) > getDateFromValue(startDate)) {
if (!cell.classList.contains('uol-datepicker__cell--empty')) {
cell.classList.add('uol-datepicker__cell--end-range');
}
} else {
cell.classList.remove('uol-datepicker__cell--end-range');
}
})
}
// Handles keyboard navigation within the calendar
const delegateDateCellEvents = (event, modal) => {
const focusOnDay = getDateFromValue(modal.querySelector('.uol-datepicker__cell[tabindex="0"]'), true);
if (event.type == 'keydown') {
if (event.key == 'Tab') {
handleDayTabEvent(event, modal);
} else if (event.key == 'Esc'|| event.key == 'Escape') {
datePickerCloseModal(event, true);
} else {
handleDayNavigationEvents(event, modal, focusOnDay);
}
const cells = modal.querySelectorAll('.uol-datepicker__cell');
cells.forEach(cell => {
deactivateCellsBeforeStart(cell, modal.closest('.uol-datepicker-container'));
})
}
};
// Ensures focus acts correctly, should auser press tab or shift and tab
const handleDayTabEvent = (event, modal) => {
modal.querySelector('.uol-datepicker__cancel-btn').focus();
if (event.shiftKey) {
modal.querySelector('.uol-datepicker__next-year').focus();
}
stopPropagationPreventDefault(event);
}
// Handles keyboard navigation within the calendar
const handleDayNavigationEvents = (event, modal, focusOnDay) => {
if (event.key == 'Right' || event.key == 'ArrowRight') {
moveFocusToNextDay(modal, focusOnDay);
} else if (event.key == 'Left' || event.key == 'ArrowLeft') {
moveFocusToPreviousDay(modal, focusOnDay);
} else if (event.key == 'Down' || event.key == 'ArrowDown') {
moveFocusToNextWeek(modal, focusOnDay);
} else if (event.key == 'Up' || event.key == 'ArrowUp') {
moveFocusToPreviousWeek(modal, focusOnDay);
} else if (event.key == 'PageUp') {
event.shiftKey ? moveToPreviousYear(modal, focusOnDay) : moveToPreviousMonth(modal, focusOnDay);
setFocusDay(modal, focusOnDay);
} else if (event.key == 'PageDown') {
event.shiftKey ? moveToNextYear(modal, focusOnDay) : moveToNextMonth(modal, focusOnDay);
setFocusDay(modal, focusOnDay);
} else if (event.key == 'Home') {
moveFocusToFirstDayOfWeek(modal, focusOnDay);
} else if (event.key == 'End') {
moveFocusToLastDayOfWeek(modal, focusOnDay);
}
stopPropagationPreventDefault(event)
setDateRange(modal.closest('.uol-datepicker-container'));
}
// Adds a visual indicator to a cell, if keyboard focus moves to that cell from a nav element or the button, otherwise it was not clear where focus was
const addTemporaryFocusToCell = (event) => {
const focusCell = event.target.closest('.uol-datepicker').querySelector('.uol-datepicker__cell[tabindex="0"]');
if (!focusCell.hasAttribute('data-selected-start') || !focusCell.hasAttribute('data-selected-end') || !focusCell.hasAttribute('data-selected-single-day')) {
focusCell.classList.add('uol-datepicker__cell--has-focus');
}
}
// Removes the temporary focus style
const removeTemporaryFocusFromCell = (event) => {
event.target.classList.remove('uol-datepicker__cell--has-focus');
}
// Handles the radio toggles when there is a date range calendar
const handleRadioButtons = (event, modal) => {
const fieldset = modal.querySelector('fieldset');
if (event.type == 'keydown' && event.key == 'Tab' && event.shiftKey) {
modal.querySelector('.uol-datepicker__confirm-btn').focus();
stopPropagationPreventDefault(event);
}
if (event.type == 'change') {
if (fieldset.getAttribute('data-toggle') == 'start') {
fieldset.setAttribute('data-toggle', 'end');
} else {
fieldset.setAttribute('data-toggle', 'start');
}
modal.querySelectorAll('.uol-datepicker__cell').forEach(cell => {
deactivateCellsBeforeStart(cell, modal.closest('.uol-datepicker-container'))
})
}
};
// Handles setting a user's date(s) selection, by confirming their date(s)
const handleConfirmBtn = (event) => {
const datePicker = event.target.closest('.uol-datepicker-container');
const isDateRange = datePicker.hasAttribute('data-range-selection');
if (event.type == 'keydown' && event.key == 'Tab' && !event.shiftKey) {
if (!isDateRange) {
datePicker.querySelector('.uol-datepicker__prev-year').focus();
} else {
datePicker.querySelector('[value="start"]').checked ? datePicker.querySelector('[value="start"]').focus() : datePicker.querySelector('[value="end"]').focus();
}
stopPropagationPreventDefault(event);
} else if (event.type == 'keydown' && event.key == 'Tab' && event.shiftKey) {
datePicker.querySelector('.uol-datepicker__cancel-btn').focus();
stopPropagationPreventDefault(event);
}
if (event.type == 'click') {
datePickerCloseModal(event, false)
populateInputs(datePicker);
}
setDateForButtonLabel(event, datePicker);
};
// Handles closing the modal on clickin the cancel button
const handleCancelButton = (event) => {
if (event.type == 'click') {
datePickerCloseModal(event, true);
} else {
if (event.key == 'Tab' && event.shiftKey) {
addTemporaryFocusToCell(event)
}
}
};
// closes the modal if a user clicks outside
const handleClickOutside = (event) => {
if (document.querySelector('.uol-datepicker-background').contains(event.target)) {
datePickerCloseModal(event, true);
}
}
// Loops through all instances of datepickers so functions etc only apply to the correct instance
export const datePickerHelper = () => {
// find all of the date inputs
const datePickers = document.querySelectorAll(".uol-datepicker-container");
// add buttons and make any other dom changes and add listeners to all of the new date input buttons
datePickers.forEach((datePicker) => {
datePickerAddButtons(datePicker);
attachEventListenersToInputs(datePicker);
});
}
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;
}
// create a text input with all the associated ARIA etc
const createNewInput = (selectData, inputId, selectedValuesId) => {
// console.log('f:createNewInput');
return `<input
class="uol-form__input uol-typeahead"
type="text" id="${inputId}"
name="${inputId}-name"
role="combobox"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="${inputId}"
aria-label="${selectData.selectLabel}"
aria-autocomplete="list"
spellcheck="false"
autocomplete="off">`
}
const createChipsContainer = (chipsId) => {
// console.log('f:createChipsContainer');
return `
<ul class="uol-chips uol-chips--inInput" id="${chipsId}" tabindex="-1" aria-label="Label: Selected items"></ul>
`;
}
const createAriaLiveRegion = (selectedValuesId) => {
// console.log('f:createAriaLiveRegion');
return `
<div class="hide-accessible" id="${selectedValuesId}" aria-live="assertive"></div>
`;
}
const inputChanged = (selectData) => {
selectData.textFiltering = true;
const inputId = selectData.selectName + '-input-' + selectData.selectNum;
const inputText = document.getElementById(inputId).value.toLowerCase();
const lastKeyPressed = inputText.slice(-1);
// filter list if last key wasn't a space or not keyboard controlling
let filterList = (lastKeyPressed != ' ' || !selectData.keyboardControlling) ? true : false;
if (filterList) {
selectData.selectArray.forEach((arrayItem) => {
arrayItem.available = (arrayItem.name.toLowerCase().includes(inputText)) ? true : false;
});
openPanel(selectData);
removeSelectHighlights(selectData);
renderSelect(selectData);
}
selectData.optionCounter = selectData.topItem -1;
}
const itemSelected = (e, selectData, method, itemNum) => {
// console.log('f:itemSelected');
const isMultiSelect = selectData.isMultiSelect;
const selectArray = selectData.selectArray;
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const nativeSelect = document.getElementById(selectName.slice(0, -3) + '-' + selectNum);
const nativeItemNum = parseInt(itemNum) + 1;
const inputElement = document.getElementById(selectName + '-input-' + selectNum);
/*
if itemNum is a value other than -1 it will have been passed
via keyboard control ascertaining which item we are on
following two statements take itemNum from event object
whether from chip or select item
*/
if (method == "chipContainer") {
// chip selected. target element either button or element within button gets correct id string
let idStringArray = (e.target.nodeName == "BUTTON") ?
e.target.id.split("-"):
e.target.closest(".uol-chips__button").id.split("-")
;
itemNum = idStringArray[idStringArray.length - 1];
}
if (method == "select" && itemNum == -1) {
// custom select item 'pressed' (not keyboard enter)
if (e.target.nodeName == "path" || e.target.nodeName == "svg") {
// icon clicked
const closest = e.target.closest(".uol-typeahead__item");
const idStringArray = closest.id.split("-");
itemNum = idStringArray[idStringArray.length - 1];
} else {
// li clicked
const idStringArray = e.target.id.split("-");
itemNum = idStringArray[idStringArray.length - 1];
}
}
// give focus and set scroll to selected element
selectData.optionCounter = parseInt(itemNum);
// Multi
if (isMultiSelect) {
if (selectArray[itemNum].selected) {
selectData.selectArray[itemNum].selected = false;
selectData.anySelected = true;
nativeSelect[nativeItemNum].selected = false;
// update aria-live region for selection of option
document.querySelector('#' + selectData.selectId + '-aria-live').innerText = "De-selected " + selectArray[itemNum].name;
} else {
selectData.selectArray[itemNum].selected = true;
nativeSelect[nativeItemNum].selected = true;
// update aria-live region for selection of option
document.querySelector('#' + selectData.selectId + '-aria-live').innerText = "Selected " + selectArray[itemNum].name;
}
}
// Single
if (!isMultiSelect) {
// unselect all
for (let i = 0; i < selectArray.length; i++) {
selectData.selectArray[i].selected = false;
nativeSelect[i].selected = false;
}
// selected item
selectData.selectArray[itemNum].selected = true;
nativeSelect[parseInt(itemNum) + 1].selected = true;
nativeSelect[parseInt(itemNum) + 1].selectedIndex = parseInt(itemNum) + 1;
inputElement.value = selectArray[itemNum].name;
closePanel(selectData);
}
nativeSelect.addEventListener('change', () => {
// set selected for all items be false
selectArray.forEach((arrayItem) => {
arrayItem.selected = false;
});
// set options in native selected dependent on custom select selections
setNativeSelect(selectData);
});
// after selecting chip via filter/select list, make all available and set focus to input
if (method !== "chipContainer" && selectData.textFiltering) {
// if multiselect and selecting clear input
isMultiSelect && (inputElement.value = '');
for (let item of selectArray) {
item.available = true;
inputElement.focus();
}
}
selectData.textFiltering = false;
// Call number of selected items (to display in text)
if (isMultiSelect && selectData.noChipsVariant) {
selectData.itemsSelected = numberOfSelectedItems(selectData);
}
renderSelect(selectData);
}
const setNativeSelect = (selectData) => {
// console.log('f:setNativeSelect');
const selectArray = selectData.selectArray;
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const nativeSelect = document.getElementById(selectName.slice(0, -3) + '-' + selectNum);
// set select for any items selected in native to be true
nativeSelect.selectedOptions.forEach((selectOption) => {
let selectedItemText = selectOption.text;
selectArray.forEach((arrayItem) => {
if (arrayItem.name == selectedItemText) {
arrayItem.selected = true;
}
});
});
}
// Get number of selected items (to display in text)
const numberOfSelectedItems = (selectData) => {
let selectedItemsTotal = 0;
selectData.selectArray.forEach(item => {
if (item.selected) {
selectedItemsTotal++;
}
});
return selectedItemsTotal;
}
const removeSelectHighlights = (selectData) => {
// console.log('f:removeSelectHighlights');
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const selectArray = selectData.selectArray;
for (let i = 0; i < selectArray.length; i++) {
document.getElementById(selectName + '-' + selectNum + '-' + i).removeAttribute('data-item-active');
}
}
const highlightNextItem = (selectData, direction) => {
// console.log('f:highlightNextItem');
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const optionCounter = selectData.optionCounter;
const panelOpen = selectData.panelOpen;
const selectArray = selectData.selectArray;
const inputElement = document.getElementById(selectName + '-input-' + selectNum);
!panelOpen && openPanel(selectData);
// un highlight last selected item
if (optionCounter !== -1) {
document.getElementById(selectName + '-' + selectNum + '-' + (optionCounter)).removeAttribute('data-item-active');
}
// find next item to focus/highlight
for (let i = optionCounter + direction; i < selectArray.length; i = i + direction) {
// if attempting to go backwards from first item leave currently selected item
if (i < 0) {
break;
}
// if next item availbale specify new option counter
if (selectArray[i].available) {
selectData.optionCounter = i;
break;
}
}
// highlight latest selected item
let selectedItem = document.getElementById(selectName + '-' + selectNum + '-' + selectData.optionCounter);
selectedItem.setAttribute('data-item-active', 'true');
// Update input aria-activedescendant to the correct list ID
inputElement.setAttribute('aria-activedescendant', selectedItem.id)
selectedItem.focus();
selectedItem.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest"
});
// Update aria-live region to show "Select" or "De-select" prior to item name
let selectPrefectText = (selectArray[selectData.optionCounter].selected) ? "De-select " : "Select ";
document.querySelector('#' + selectData.selectId + '-aria-live').innerText = selectPrefectText + selectedItem.innerText;
if (selectData.optionCounter == selectArray.length - 1) {
// append aria region with (Last item)
document.querySelector('#' + selectData.selectId + '-aria-live').innerText += " (Last item)";
}
if (selectData.optionCounter == 0) {
// append aria region with (First item)
document.querySelector('#' + selectData.selectId + '-aria-live').innerText += " (First item)";
}
}
const clearHighlightedSelects = (selectData) => {
// console.log('f:clearHighlightedSelects');
for (let i = 0; i < selectData.selectArray.length; i++) {
if (selectData.selectArray[i].available) {
let selectedItem = document.getElementById(selectData.selectName + '-' + selectData.selectNum + '-' + i);
selectedItem.removeAttribute('data-item-active');
}
}
}
const eventListeners = (selectData) => {
// console.log('f:eventListeners');
// used in this function (extract from object)
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const inputId = selectData.inputId;
const selectId = selectData.selectId;
const chipsId = selectData.chipsId;
const selectArray = selectData.selectArray;
const selectElement = document.getElementById(selectId);
const inputElement = document.getElementById(inputId);
const selectOuter = inputElement.closest('.uol-form__input-wrapper');
const anySelected = selectData.anySelected;
// select options (one listener for ul element, not individual elements)
selectElement.addEventListener("click", (e) => itemSelected(e, selectData, "select", -1));
// input box
inputElement.addEventListener('focus', () => !selectData.panelOpen && openPanel(selectData));
inputElement.addEventListener("input", () => inputChanged(selectData));
inputElement.addEventListener('keydown', (e) => keyDown(e));
// key press listeners
const keyDown = (e) => {
// console.log("f:keyDown")
const optionCounter = selectData.optionCounter;
selectData.topItem = 0;
selectData.bottomItem = selectArray.length - 1;
// set keyboard controlling value to be true if going up and down list items
if (e.key == 'ArrowUp' || e.key == 'ArrowDown') {
e.preventDefault();
selectData.keyboardControlling = true;
// get first item in list
for (let j = 0; j < selectArray.length; j++) {
if (selectArray[j].available) {
selectData.topItem = j;
break;
}
}
// get last item in list
for (let k = selectArray.length - 1; k > -1; k--) {
if (selectArray[k].available) {
selectData.bottomItem = k;
break;
}
}
}
if ((e.key == 'ArrowUp') && (optionCounter > selectData.topItem)) {
highlightNextItem(selectData, -1);
}
if ((e.key == 'ArrowUp') && (optionCounter == selectData.topItem)) {
closePanel(selectData);
e.preventDefault();
}
(e.key == 'ArrowDown') && (optionCounter <= selectData.bottomItem) && highlightNextItem(selectData, 1);
(e.key == 'Escape') && closePanel(selectData);
// if keyboard controlling (up and down list items) and space pressed, select that item
if (e.key == ' ' && selectData.keyboardControlling) {
e.preventDefault();
itemSelected(e, selectData, "select", optionCounter);
}
if (e.key == 'Enter') {
e.preventDefault();
selectData.panelOpen ?
itemSelected(e, selectData, "select", optionCounter) :
openPanel(selectData);
}
if (e.key == 'Tab' && selectData.panelOpen) {
selectData.keyboardControlling = true;
if (!e.shiftKey) {
// tabbing forward
if (optionCounter == selectData.bottomItem) {
// off end of list (no preventDefault so goes to next element)
closePanel(selectData);
}
if (optionCounter < selectData.bottomItem) {
e.preventDefault();
if (optionCounter == selectData.bottomItem - 1) {
// last item
document.querySelector('#' + selectData.selectId + '-aria-live').innerText = ". Last item."
}
highlightNextItem(selectData, 1);
}
} else {
// tabbing backwards (shift key pressed)
// not at beginning of list, tab back one element
if (optionCounter > 0) {
highlightNextItem(selectData, -1);
e.preventDefault();
}
// tab back to input and close panel
if (optionCounter == 0) {
closePanel(selectData);
document.querySelector('#' + selectData.selectId + '-aria-live').innerText = "";
e.preventDefault();
}
// enable shift tab to work with default behaviour if not using tab for selecting
if (optionCounter == -1) {
closePanel(selectData);
}
}
}
}
// ascertain which item is clicked and action accordingly
document.addEventListener('mousedown', (e) => {
let itemClicked = '';
if ((e.target.id).includes(selectName + '-chip-' + selectNum)) {itemClicked = 'a chip';}
if (inputElement.contains(e.target)) {itemClicked = 'input';}
if (selectElement.contains(e.target)) {itemClicked = 'select item';}
if (selectOuter.contains(e.target) && e.target.classList.contains('uol-form__input__chevron')) {itemClicked = 'chevron';}
if (itemClicked == '') {
// empty area
selectData.panelOpen && closePanel(selectData);
}
if (itemClicked == 'input') {
// input area clicked
!selectData.panelOpen && openPanel(selectData);
}
if (itemClicked == 'chevron') {
// chevron clicked
togglePanelOpen(selectData);
}
if (itemClicked == 'a chip') {
// chip clicked
if (anySelected) {
// if there are chips remaining than focus on chips container
document.getElementById(chipsId).focus();
} else {
// if no chips remaining give focus to input element
inputElement.focus();
}
}
});
// native select
const nativeSelectId = selectName.slice(0, -3) + '-' + selectNum;
const nativeSelect = document.getElementById(nativeSelectId);
nativeSelect.addEventListener('change', () => {
// set selected for all items be false
selectData.selectArray.forEach((arrayItem) => {
arrayItem.selected = false;
});
// set options in native selected dependent on custom select selections
setNativeSelect(selectData);
// native select changed, reflect changes in custom
renderSelect(selectData);
});
// Listen for ajax loads and
// This object is located in a CMS team script. The file is loaded after bundle.js
// so we need wait wait for it to be loaded before accessing it.
if (typeof MICROSITENAME !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
searchAjaxLoad.registerListener((onload) => {
if (onload) {
// console.log("inputTypeahead running in searchAjaxLoad");
inputTypeahead();
}
});
});
}
}
const togglePanelOpen = (selectData) => {
// console.log('f:togglePanelOpen');
selectData.panelOpen ? closePanel(selectData) : openPanel(selectData);
}
const removePanelClasses = (selectData) => {
// console.log('f:removePanelClasses');
const inputElement = document.getElementById(selectData.inputId);
inputElement.classList.remove("uol-typeahead--panelOpenChips");
inputElement.classList.remove("uol-typeahead--panelOpenNoChips");
inputElement.classList.remove("uol-typeahead--panelClosedChips");
inputElement.classList.remove("uol-typeahead--panelClosedNoChips");
}
const openPanel = (selectData) => {
// console.log('f:openPanel');
const selectElement = document.getElementById(selectData.selectId);
const inputElement = document.getElementById(selectData.inputId);
const selectOuter = inputElement.closest('.uol-form__input-wrapper');
const anySelected = selectData.anySelected;
const noChipsVariant = selectData.noChipsVariant;
inputElement.focus();
// show panel
selectElement.setAttribute('data-panel-shown', 'true');
selectElement.ariaExpanded = true;
selectData.panelOpen = true;
// causes arrow icon to change to up
inputElement.ariaExpanded = true;
removePanelClasses(selectData);
// add open class to outer element
selectOuter.classList.add("uol-form__input-wrapper--panelOpen");
if (anySelected && !noChipsVariant) {
inputElement.classList.add("uol-typeahead--panelOpenChips");
} else {
inputElement.classList.add("uol-typeahead--panelOpenNoChips");
}
const countNumId = selectData.selectName + '-numSelected-' + selectData.selectNum;
if (noChipsVariant && anySelected && document.getElementById(countNumId)) {
// add classes to input, select list and "n options selected" for variant with no chips and panel open (remove panel closed classes)
// input
document.getElementById(selectData.inputId).classList.add('uol-typeahead__input--no-chips--panel-open');
document.getElementById(selectData.inputId).classList.remove('uol-typeahead__input--no-chips--panel-closed');
// select
document.getElementById(selectData.selectId).classList.add('uol-typeahead__select--no-chips--panel-open');
document.getElementById(selectData.selectId).classList.remove('uol-typeahead__select--no-chips--panel-closed');
// countNum
document.getElementById(countNumId).classList.add('uol-typeahead__count-num--no-chips--panel-open');
document.getElementById(countNumId).classList.remove('uol-typeahead__count-num--no-chips--panel-closed');
}
}
const closePanel = (selectData) => {
// console.log('f:closePanel');
if (selectData.isMultiSelect) {
document.getElementById(selectData.inputId).value = "";
}
const anySelected = selectData.anySelected;
const noChipsVariant = selectData.noChipsVariant;
const countNumId = selectData.selectName + '-numSelected-' + selectData.selectNum;
if (noChipsVariant && anySelected) {
// add classes to input, select list and "n options selected" for variant with no chips and panel open (remove panel closed classes)
// input
document.getElementById(selectData.inputId).classList.remove('uol-typeahead__input--no-chips--panel-open');
// select
document.getElementById(selectData.selectId).classList.remove('uol-typeahead__select--no-chips--panel-open');
document.getElementById(selectData.selectId).classList.add('uol-typeahead__select--no-chips--panel-closed');
// countNum
document.getElementById(countNumId).classList.remove('uol-typeahead__count-num--no-chips--panel-open');
document.getElementById(countNumId).classList.add('uol-typeahead__count-num--no-chips--panel-closed');
// if (noChipsVariant && document.getElementById(countNumId)) {
// console.log("HERE " + countNumId + " " + noChipsVariant);
// document.getElementById(countNumId).remove();
// }
}
const selectElement = document.getElementById(selectData.selectId);
const inputElement = document.getElementById(selectData.inputId);
const selectOuter = inputElement.closest('.uol-form__input-wrapper');
// remove class to outer element
selectOuter.classList.remove("uol-form__input-wrapper--panelOpen");
// close panel
selectElement.setAttribute('data-panel-shown', 'false');
selectElement.ariaExpanded = false;
selectData.panelOpen = false;
// set keyboardControlling back to false (to enable full text input including space key)
selectData.keyboardControlling = false;
// causes arrow icon to change to down
inputElement.ariaExpanded = false;
// set option counter so none are selected
selectData.optionCounter = -1;
inputElement.classList.remove("uol-typeahead--panelOpen");
removePanelClasses(selectData);
/*
noChipsVariant is if chips are set to not show in the config
anySelected is separate, this is where chips are set to show but there currently are none selected
*/
if (!noChipsVariant) {
if (anySelected) {
inputElement.classList.add("uol-typeahead--panelClosedChips");
} else {
inputElement.classList.add("uol-typeahead--panelClosedNoChips");
}
}
clearHighlightedSelects(selectData);
removeSelectHighlights(selectData);
}
const renderSelect = (selectData) => {
// console.log('f:renderSelect');
selectData.isMultiSelect && renderMultipleSelect(selectData);
!selectData.isMultiSelect && renderSingleSelect(selectData);
}
const renderSingleSelect = (selectData) => {
// console.log('f:renderSingleSelect');
const selectArray = selectData.selectArray;
const checkmark = `
<svg style="width:24px;height:24px" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>`;
for (let i = 0; i < selectArray.length; i++) {
let selectItem = document.getElementById(selectData.selectName + '-' + selectData.selectNum + '-' + i);
if (selectArray[i].selected) {
if (!selectItem.querySelector('svg')) {
selectItem.setAttribute("aria-selected", "true");
selectItem.insertAdjacentHTML('beforeend', checkmark);
}
} else {
// remove ticks from others
if (selectItem.querySelector('svg')) {
selectItem.setAttribute("aria-selected", "false");
selectItem.querySelector('svg').remove();
}
}
// whether item appears in drop down list
if (!selectData.selectArray[i].available) {
selectItem.setAttribute('data-filter-hidden', '');
} else {
selectItem.removeAttribute('data-filter-hidden');
}
}
}
const renderMultipleSelect = (selectData) => {
// console.log('f:renderMultipleSelect');
const selectOption = selectData.selectOption;
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const chipsId = selectData.chipsId;
const selectId = selectData.selectId;
const selectedValuesId = selectData.selectedValuesId;
const hintId = selectData.hintId;
const inputId = selectData.inputId;
const selectArray = selectData.selectArray;
const noChipsVariant = selectData.noChipsVariant;
const itemsSelected = selectData.itemsSelected;
const inputElement = document.getElementById(inputId);
const countNumId = selectData.selectName + '-numSelected-' + selectData.selectNum;
if (itemsSelected > 0) {selectData.anySelected = true;} else {selectData.anySelected = false;}
anySelected = selectData.anySelected;
// if "n options selected" text field doesn't exist create it
if (noChipsVariant && itemsSelected > 0 && !document.getElementById(countNumId)) {
const numberOfElement = document.createElement('span');
selectData.countNumId = countNumId;
numberOfElement.classList.add('uol-form__input-wrapper__num-of-selected-message');
numberOfElement.setAttribute('id', countNumId);
let spanToAdd;
spanToAdd = document.createTextNode(`Select option`);
// get input parent element and add new text node
const getMultiSelectWrapper = document.getElementById(inputId).parentElement;
numberOfElement.appendChild(spanToAdd);
getMultiSelectWrapper.prepend(numberOfElement);
// input
document.getElementById(inputId).classList.add('uol-typeahead__input--no-chips--panel-open');
document.getElementById(inputId).classList.remove('uol-typeahead__input--no-chips--panel-closed');
// select
document.getElementById(selectId).classList.add('uol-typeahead__select--no-chips--panel-open');
document.getElementById(selectId).classList.remove('uol-typeahead__select--no-chips--panel-closed');
// countNum
document.getElementById(countNumId).classList.add('uol-typeahead__count-num--no-chips--panel-open');
document.getElementById(countNumId).classList.remove('uol-typeahead__count-num--no-chips--panel-closed');
document.getElementById(selectData.inputId).focus();
}
// set text for "n options selected" field, with plural s for when more than one
if (noChipsVariant && itemsSelected > 0) {
const pluralS = (itemsSelected > 1) ? "s":"";
const optionsSelectedText = `${itemsSelected} option${pluralS} selected`;
document.getElementById(countNumId).innerHTML = optionsSelectedText;
}
// set removed classes inherit to "n options selected" field when non selected
if (noChipsVariant && itemsSelected == 0 && document.getElementById(countNumId)) {
document.getElementById(countNumId).remove();
// input
document.getElementById(selectData.inputId).classList.remove('uol-typeahead__input--no-chips--panel-open');
document.getElementById(selectData.inputId).classList.remove('uol-typeahead__input--no-chips--panel-closed');
// select
document.getElementById(selectData.selectId).classList.remove('uol-typeahead__select--no-chips--panel-open');
document.getElementById(selectData.selectId).classList.remove('uol-typeahead__select--no-chips--panel-closed');
}
const checkmark = `
<svg style="width:24px;height:24px" aria-hidden="true" focusable="false" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>`;
const chevron = selectOption.parentElement.querySelector('.uol-form__input__chevron');
chevron.style.display = 'block';
let anySelected = false;
let numSelected = 0;
let allChipsHtml = '';
let selectedValuesHtml = '';
for (let i = 0; i < selectArray.length; i++) {
let selectItem = document.getElementById(selectName + '-' + selectNum + '-' + i);
// add tick to selected item
if (selectArray[i].selected) {
selectItem.setAttribute("aria-selected", "true");
selectItem.insertAdjacentHTML('beforeend', checkmark);
}
// whether item appears in drop down list
selectArray[i].available ?
selectItem.removeAttribute('data-filter-hidden') :
selectItem.setAttribute('data-filter-hidden', '');
let individualChipId = selectName + '-chip-' + selectNum + '-' + i;
let chipName = selectArray[i].name;
if (selectArray[i].selected) {
anySelected = true;
numSelected++;
allChipsHtml += `
<li class="uol-chips__item" id="${individualChipId}-chip">
<button
id="${individualChipId}"
data-label="${chipName}"
aria-describedby="${individualChipId}"
class="uol-chips__button"
>
<span class="uol-chips__text" role="text">
<span class="hide-accessible">Cancel</span>
${chipName}
</span>
<span class="uol-chips__delete-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" focusable="false" aria-hidden="true">
<path fill="#000000" fill-rule="nonzero" 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"></path>
</svg>
</span>
</button>
</li>
`;
selectedValuesHtml += `${chipName}, `;
} else {
// Sete aria-sselected to false
selectItem.setAttribute("aria-selected", "false");
// Remove svg if present
if (selectItem.querySelector("svg")) {
selectItem.querySelector("svg").remove();
}
}
}
selectData.anySelected = anySelected;
if (anySelected) {
// At least one chip
removePanelClasses(selectData);
if (selectData.panelOpen ) {
if (!noChipsVariant) {
inputElement.classList.add("uol-typeahead--panelOpenChips");
}
} else {
if (!noChipsVariant) {
inputElement.classList.add("uol-typeahead--panelClosedChips");
}
}
// Create chips container if not already created and add event listener
if (!document.getElementById(chipsId) && !noChipsVariant) {
const selectOuter = selectOption.closest('.uol-form__input-wrapper');
selectOuter.insertAdjacentHTML("beforebegin", createChipsContainer(chipsId));
const chipElement = document.getElementById(chipsId);
chipElement.addEventListener("click", (e) => {
// if empty area of chip container pressed close panel, if chip run itemSelected
(e.target == chipElement) ?
closePanel(selectData):
itemSelected(e, selectData, "chipContainer", -1);
// give chip focus
(e.target.classList.contains('uol-chips__item')) && (selectData.itemsSelected > 0) && (chipElement.focus());
});
}
// Populate chips element
if (!noChipsVariant) document.getElementById(chipsId).innerHTML = allChipsHtml;
// Call number of selected items (to display in text)
if (noChipsVariant) numberOfSelectedItems(selectData);
} else {
// No chips
if (selectData.selectRendered) {
inputElement.focus();
}
// give input element one of no chips classes (different styling)
removePanelClasses(selectData);
(selectData.panelOpen) ?
inputElement.classList.add("uol-typeahead--panelOpenNoChips"):
inputElement.classList.add("uol-typeahead--panelClosedNoChips");
// if there was a chips container, remove parent div
(document.getElementById(chipsId)) && (document.getElementById(chipsId).remove());
}
// Populate hidden selectedValues container
selectedValuesHtml = (selectedValuesHtml.substring(selectedValuesHtml.length - 2) == ", ") ?
selectedValuesHtml.substring(0, selectedValuesHtml.length - 2) :
selectedValuesHtml;
if (document.getElementById(selectedValuesId)) {
document.getElementById(selectedValuesId).innerHTML = selectedValuesHtml;
}
// Change hint text depending on whether any selected or not
let hintTextElement = '';
if (document.getElementById(hintId)) {
hintTextElement = document.getElementById(hintId);
let selectedPlural = (numSelected > 1) ? 's' : '';
anySelected && (hintTextElement.innerText = `${selectData.hintText}: Item${selectedPlural} selected`);
!anySelected && (hintTextElement.innerText = selectData.hintText + ": Select items or type to filter");
}
selectData.itemsSelected = numSelected;
selectData.selectRendered = true;
}
const resetFiltersEvent = (selectData) => {
// console.log('f:resetFiltersEvent');
// if there's a filters panel
const filtersPanel = document.querySelector('.uol-filters-panel');
if (filtersPanel) {
// if it contains a clear fliters button
const clearFiltersBtn = document.querySelector('#clear_filter_panel')
if (clearFiltersBtn) {
// add event listener (can't do normal onclick as not in DOM initially)
clearFiltersBtn.addEventListener("click", (e) => {
// set any selected in array to be false and clear "n options selected"
selectData.anySelected = false;
const numSelectedId = selectData.selectName + '-numSelected-' + selectData.selectNum;
if (document.getElementById(numSelectedId)) {
document.getElementById(numSelectedId).innerText = '';
}
// clear input id
const inputId = selectData.inputId;
if (document.getElementById(inputId)) {
document.getElementById(inputId).value = '';
}
// hint text - only apply to multiselect, single option hint stays same
if (selectData.isMultiSelect) {
const hintId = selectData.selectName + '-hint-' + selectData.selectNum;
if (document.getElementById(hintId)) {
document.getElementById(hintId).innerText = selectData.hintText + ": Select items or type to filter";
}
}
// event in filters-panel-module clears DOM elements, this removes values in array
selectData.selectArray.forEach((arrayItem) => {
arrayItem.available = true;
arrayItem.selected = false;
});
});
}
}
}
/*
create custom select based on data from native select
*/
const buildCustomSelect = (selectData) => {
// console.log('f:buildCustomSelect');
const selectOption = selectData.selectOption;
const isMultiSelect = selectData.isMultiSelect;
const selectName = selectData.selectName;
const selectNum = selectData.selectNum;
const hintId = selectData.hintId;
const inputId = selectData.inputId;
const selectId = selectData.selectId;
const selectedValuesId = selectData.selectedValuesId;
const selectOuter = selectOption.closest('.uol-form__input-wrapper');
// Create aria announcements area if it doesn't already exist
!document.getElementById(selectedValuesId) && selectOuter.insertAdjacentHTML("afterbegin", createAriaLiveRegion(selectedValuesId));
// Hide native select
selectOption.hidden = true;
// Give native select uniqueId
selectData.selectOption.id += "-" + selectNum;
// Hint text, ensure has unique id
const originalHintTextId = selectName.slice(0, -3) + '-hint';
if (document.getElementById(originalHintTextId)) {
const hintText = document.getElementById(originalHintTextId).innerText;
document.getElementById(originalHintTextId).id = hintId;
selectData.hintText = hintText;
}
// create text input
selectOuter.insertAdjacentHTML('afterbegin', createNewInput(selectData, inputId, selectedValuesId));
// create outer ul panel element
const selectElement = document.createElement("ul");
selectElement.classList.add("uol-typeahead__list");
selectElement.id = selectId;
selectElement.setAttribute("role", "listbox");
selectElement.setAttribute("data-panel-shown", false);
selectElement.setAttribute("aria-label", selectData.selectLabel);
// if multiple add following class
isMultiSelect && selectElement.classList.add("uol-typeahead__list--multi-select");
/*
TO DO:
Look into, the follow line is a fix because appears select needs one item selected and takes the first
This stops our first item being falsely marked as selected
*/
selectOption[0].selected = true;
// loop through values in native and add to custom
for (let i = 1; i < selectOption.length; i++) {
// create new li element for custom select and add to ul
const liElement = document.createElement('li');
liElement.classList.add("uol-typeahead__item");
liElement.id = selectName + '-' + selectNum + '-' + (i - 1);
liElement.appendChild(document.createTextNode(selectOption[i].text));
liElement.setAttribute("role", "option");
let dataOptionValue = (selectOption[i].value) ? selectOption[i].value : '';
liElement.setAttribute('data-option-value', dataOptionValue);
selectElement.appendChild(liElement);
/*
add new object for each item to selectArray
available is set for true (when text input "rules out" a value this changes to false)
selected is whether an item is ticked/has a chip
if the native select has this as selected this is set to be true, otherwise false
the value contains a filter url that is used to retrieve the filtered results
*/
const selectItem = {
name: selectOption[i].text,
available: true,
selected: selectOption[i].hasAttribute('selected'),
value: selectOption[i].value
};
selectData.selectArray.push(selectItem);
// if single select and item specified as selected in config
if (!selectData.isMultiSelect && selectItem.selected) {
document.getElementById(inputId).value = selectOption[i].text;
}
}
selectOuter.appendChild(selectElement);
renderSelect(selectData);
eventListeners(selectData);
// Give label for attribute id of input
const labelId = selectData.selectName.slice(0, -3) + '-label';
const labelElement = document.getElementById(labelId);
labelElement.setAttribute("for", selectName + '-input-' + selectNum);
resetFiltersEvent(selectData);
}
/*
loop through any native selects on the page
initialise values and pass through as object to buildCustomSelect function
*/
export const inputTypeahead = () => {
const iOS = isIOS();
const androidDevice = isAndroid();
const firefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (!iOS && !androidDevice && !firefox) {
const selectOptions = document.querySelectorAll('.uol-form__input--select:not([data-native-select])');
let selectNum = 0;
let itemsSelected = 0;
let panelOpen = false;
let anySelected = false;
let optionCounter = -1;
let keyboardControlling = false;
let textFiltering = false;
selectOptions.forEach((selectOption) => {
let selectLabel = selectOption.closest('.uol-form__input-container').querySelector('label');
selectLabel = selectLabel ? selectLabel.innerText.replace(/(\r\n|\n|\r)/gm, " ") : "";
const selectName = `${selectOption.id}-js`;
const inputId = selectName + '-input-' + selectNum;
const chipsId = selectName + '-chips-' + selectNum;
const selectedValuesId = selectName + '-select-' + selectNum + '-aria-live';
const selectId = selectName + '-select-' + selectNum;
const hintId = selectName + '-hint-' + selectNum;
const selectArray = [];
const isMultiSelect = selectOption.hasAttribute('multiple');
const noChipsVariant = selectOption.hasAttribute('data-chips-hide');
const topItem = 0;
const bottomItem = 0;
const selectRendered = false;
const selectData = {
selectLabel,
selectOption,
selectName,
selectNum,
isMultiSelect,
noChipsVariant,
panelOpen,
anySelected,
selectArray,
optionCounter,
itemsSelected,
inputId,
chipsId,
selectedValuesId,
selectId,
hintId,
keyboardControlling,
textFiltering,
topItem,
bottomItem,
selectRendered
}
buildCustomSelect(selectData);
selectNum++;
});
} else {
// TODO: Do we still need this now we are using the 'hidden' attribute?
// Display native-select for IE
const selectOptions = document.querySelectorAll('.uol-form__input--select');
selectOptions.forEach((selectOption) => {
selectOption.style.display = "block";
});
}
if (firefox) {
const selectOptions = document.querySelectorAll('.uol-form__input--select');
selectOptions.forEach((selectOption) => {
if(selectOption.hasAttribute('multiple')) {
// use native select for select mutliple on firefox
selectOption.dataset.nativeSelect = "";
}
});
}
}
/**
* 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"
}