mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-25 02:19:12 -04:00
Keyboard users select a time slot by moving to the radio for that slot, using the arrow keys, and selecting it by pressing 'space' or 'enter', like a `<select>`. We allow this by listening for 'keydown' events from the 'enter' or 'space' keys on time slot radios that are checked. Browsers fire 'click' events alongside the 'keydown' event meaning it's possible for the code that makes the selection to be run twice. We currently guard against this by checking for the `pageX` property of the event object, reasoning that a click event fired by a key press won't have a cursor position. Most browsers we support set it to `0` but it isn't always the case: https://dom-event-test.glitch.me/results.html For those browsers, the `!event.pageX` condition resolves correctly so this works. Safari and versions of Internet Explorer before 11 however, set it to a positive number. In those browsers, moving the selection between radios using the arrow keys fired a 'click' event which, in Safari and IE<11, was treated as a mouse/touch event and so confirmed the selection. This made it impossible to select a later time. These changes replace the 'click' event on time slots with an artifical one that tracks mouse/trackpad clicks by listening for a 'mousedown' followed by a 'mouseup' on a time slot. This doesn't fire on key presses so avoids the problem.
189 lines
5.5 KiB
JavaScript
189 lines
5.5 KiB
JavaScript
(function(global) {
|
||
|
||
"use strict";
|
||
|
||
var Modules = global.GOVUK.Modules;
|
||
var Hogan = global.Hogan;
|
||
|
||
let states = {
|
||
'initial': Hogan.compile(`
|
||
<div class="radio-select-column">
|
||
<div class="multiple-choice js-multiple-choice">
|
||
<input checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
|
||
<label class="block-label js-block-label" for="{{name}}-0">Now</label>
|
||
</div>
|
||
</div>
|
||
<div class="radio-select-column">
|
||
{{#categories}}
|
||
<input type='button' class='js-category-button' value='{{.}}' />
|
||
{{/categories}}
|
||
</div>
|
||
`),
|
||
'choose': Hogan.compile(`
|
||
<div class="radio-select-column">
|
||
<div class="multiple-choice js-multiple-choice js-initial-option">
|
||
<input checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
|
||
<label for="{{name}}-0">Now</label>
|
||
</div>
|
||
</div>
|
||
<div class="radio-select-column">
|
||
{{#choices}}
|
||
<div class="multiple-choice js-multiple-choice js-option">
|
||
<input type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
|
||
<label for="{{id}}">{{label}}</label>
|
||
</div>
|
||
{{/choices}}
|
||
<input type='button' class='js-done-button js-done-button-block' value='Done' />
|
||
</div>
|
||
`),
|
||
'chosen': Hogan.compile(`
|
||
<div class="radio-select-column">
|
||
<div class="multiple-choice js-multiple-choice js-initial-option">
|
||
<input id="{{name}}-0" name="{{name}}" type="radio" value="">
|
||
<label for="{{name}}-0">Now</label>
|
||
</div>
|
||
</div>
|
||
<div class="radio-select-column">
|
||
{{#choices}}
|
||
<div class="multiple-choice js-multiple-choice">
|
||
<input checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
|
||
<label for="{{id}}">{{label}}</label>
|
||
</div>
|
||
{{/choices}}
|
||
</div>
|
||
<div class="radio-select-column">
|
||
<input type='button' class='category-link js-reset-button' value='Choose a different time' />
|
||
</div>
|
||
`)
|
||
};
|
||
|
||
let focusSelected = function() {
|
||
setTimeout(
|
||
() => $('[type=radio]:checked').next('label').blur().trigger('focus').addClass('selected'),
|
||
50
|
||
);
|
||
};
|
||
|
||
Modules.RadioSelect = function() {
|
||
|
||
this.start = function(component) {
|
||
|
||
let $component = $(component);
|
||
let render = (state, data) => {
|
||
$component.html(states[state].render(data));
|
||
};
|
||
let choices = $('label', $component).toArray().map(function(element) {
|
||
let $element = $(element);
|
||
return {
|
||
'id': $element.attr('for'),
|
||
'label': $.trim($element.text()),
|
||
'value': $element.prev('input').attr('value')
|
||
};
|
||
});
|
||
let categories = $component.data('categories').split(',');
|
||
let name = $component.find('input').eq(0).attr('name');
|
||
let mousedownOption = null;
|
||
const reset = () => {
|
||
render('initial', {
|
||
'categories': categories,
|
||
'name': name
|
||
});
|
||
};
|
||
const selectOption = (value) => {
|
||
render('chosen', {
|
||
'choices': choices.filter(
|
||
element => element.value == value
|
||
),
|
||
'name': name
|
||
});
|
||
focusSelected();
|
||
};
|
||
const trackMouseup = (event) => {
|
||
const parentNode = event.target.parentNode;
|
||
|
||
if (parentNode === mousedownOption) {
|
||
const value = $('input', parentNode).attr('value');
|
||
|
||
selectOption(value);
|
||
|
||
// clear tracking
|
||
mousedownOption = null;
|
||
$(document).off('mouseup', trackMouseup);
|
||
}
|
||
};
|
||
|
||
$component
|
||
.on('click', '.js-category-button', function(event) {
|
||
|
||
event.preventDefault();
|
||
let wordsInDay = $(this).attr('value').split(' ');
|
||
let day = wordsInDay[wordsInDay.length - 1].toLowerCase();
|
||
render('choose', {
|
||
'choices': choices.filter(
|
||
element => element.label.toLowerCase().indexOf(day) > -1
|
||
),
|
||
'name': name
|
||
});
|
||
focusSelected();
|
||
|
||
})
|
||
.on('mousedown', '.js-option', function(event) {
|
||
mousedownOption = this;
|
||
|
||
// mouseup on the same option completes the click action
|
||
$(document).on('mouseup', trackMouseup);
|
||
})
|
||
// space and enter, clicked on a radio confirm that option was selected
|
||
.on('keydown', 'input[type=radio]', function(event) {
|
||
|
||
// allow keypresses which aren’t enter or space through
|
||
if (event.which !== 13 && event.which !== 32) {
|
||
return true;
|
||
}
|
||
|
||
event.preventDefault();
|
||
let value = $(this).attr('value');
|
||
selectOption(value);
|
||
|
||
})
|
||
.on('click', '.js-done-button', function(event) {
|
||
|
||
event.preventDefault();
|
||
let $selection = $('input[type=radio]:checked', this.parentNode);
|
||
if ($selection.length) {
|
||
|
||
render('chosen', {
|
||
'choices': choices.filter(
|
||
element => element.value == $selection.eq(0).attr('value')
|
||
),
|
||
'name': name
|
||
});
|
||
|
||
} else {
|
||
|
||
reset();
|
||
|
||
}
|
||
focusSelected();
|
||
|
||
})
|
||
.on('click', '.js-reset-button', function(event) {
|
||
|
||
event.preventDefault();
|
||
reset();
|
||
|
||
});
|
||
|
||
render('initial', {
|
||
'categories': categories,
|
||
'name': name
|
||
});
|
||
|
||
$component.css({'height': 'auto'});
|
||
|
||
};
|
||
|
||
};
|
||
|
||
})(window);
|