Datepicker
Datepicker
Turn on a screen reader to experience this example in action.
HTML Source Code
<label id="datepickerLabel" for="datepicker">Date (mm/dd/yyyy):</label><br>
<input type="text" id="datepicker">
<script>
var initialCalendarAriaLoop = setInterval(function() {
initializeUICalendarAria();
}, 1000);
/** calendar events **/
function initializeUICalendarAria() {
var uiCalendarTriggers = document.querySelectorAll('.ui-datepicker-trigger');
if(uiCalendarTriggers) {
[].slice.call(uiCalendarTriggers).forEach(function(eachCalendarTrigger) {
eachCalendarTrigger.addEventListener('click', triggerCalendarEvents);
});
}
function triggerCalendarEvents() {
var activeDateButtonElements = document.querySelectorAll('.ui-state-active');
[].slice.call(activeDateButtonElements).forEach(function(eachButton) {
eachButton.setAttribute('aria-current', 'date');
if(eachButton.getAttribute('aria-current') == 'date') {
console.log('cleared');
clearInterval(initialCalendarAriaLoop);
} else {
console.log('inerval');
}
});
function addAriaLayer_UIDate(e) {
var allButtons = document.querySelector('[data-calendar-id="' + e.currentTarget.getAttribute('data-parent-calendar-id') + '"]').querySelectorAll('.ui-state-default');
[].slice.call(allButtons).forEach(function(eachButton) {
eachButton.removeAttribute('aria-current');
});
e.currentTarget.setAttribute('aria-current', 'date');
}
var uiCalendars = document.querySelectorAll('.ui-datepicker-calendar');
var dataCalendarId = 0;
if(uiCalendars) {
[].slice.call(uiCalendars).forEach(function(eachUiCalendar) {
eachUiCalendar.setAttribute('data-calendar-id', dataCalendarId);
var allButtons = eachUiCalendar.querySelectorAll('.ui-state-default');
if(allButtons) {
[].slice.call(allButtons).forEach(function(eachButton) {
eachButton.setAttribute('data-parent-calendar-id', dataCalendarId);
eachButton.addEventListener('click', addAriaLayer_UIDate.bind(event));
});
}
dataCalendarId++;
});
}
}
}
/** end of calendar events **/
</script>
JavaScript Source Code
Dependencies:
- JQuery
- JQuery UI
$(function () {
$('#datepicker').datepicker({
showOn: 'button',
buttonImage: '/assets/images/calendar.png', // File (and file path) for the calendar image
buttonImageOnly: false,
buttonText: 'Calendar View',
dayNamesShort: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
showButtonPanel: true,
closeText: 'Close',
onClose: removeAria
});
// Add aria-describedby to the button referring to the label
$('.ui-datepicker-trigger').attr('aria-describedby', 'datepickerLabel');
dayTripper();
});
function dayTripper() {
$('.ui-datepicker-trigger').click(function () {
setTimeout(function () {
var today = $('.ui-datepicker-today a')[0];
if (!today) {
today = $('.ui-state-active')[0] ||
$('.ui-state-default')[0];
}
// Hide the entire page (except the date picker)
// from screen readers to prevent document navigation
// (by headings, etc.) while the popup is open
$("main").attr('id', 'dp-container');
// $("#dp-container").attr('aria-hidden','true');
$("#skipnav").attr('aria-hidden', 'true');
// Hide the "today" button because it doesn't do what
// you think it supposed to do
$(".ui-datepicker-current").hide();
today.focus();
datePickHandler();
$(document).on('click', '#ui-datepicker-div .ui-datepicker-close', function () {
closeCalendar();
});
}, 0);
});
}
function datePickHandler() {
var activeDate;
var container = document.getElementById('ui-datepicker-div');
var input = document.getElementById('datepicker');
if (!container || !input) {
return;
}
// $(container).find('table').first().attr('role', 'grid');
container.setAttribute('role', 'application');
container.setAttribute('aria-label', 'Calendar view date-picker');
// the top controls:
var prev = $('.ui-datepicker-prev', container)[0],
next = $('.ui-datepicker-next', container)[0];
// This is the line that needs to be fixed for use on pages with base URL set in head
next.href = 'javascript:void(0)';
prev.href = 'javascript:void(0)';
next.setAttribute('role', 'button');
next.removeAttribute('title');
prev.setAttribute('role', 'button');
prev.removeAttribute('title');
appendOffscreenMonthText(next);
appendOffscreenMonthText(prev);
// delegation won't work here for whatever reason, so we are
// forced to attach individual click listeners to the prev /
// next month buttons each time they are added to the DOM
$(next).on('click', handleNextClicks);
$(prev).on('click', handlePrevClicks);
monthDayYearText();
$(container).on('keydown', function calendarKeyboardListener(keyVent) {
var which = keyVent.which;
var target = keyVent.target;
var dateCurrent = getCurrentDate(container);
if (!dateCurrent) {
dateCurrent = $('a.ui-state-default')[0];
setHighlightState(dateCurrent, container);
}
if (27 === which) {
keyVent.stopPropagation();
return closeCalendar();
} else if (which === 9 && keyVent.shiftKey) { // SHIFT + TAB
keyVent.preventDefault();
if ($(target).hasClass('ui-datepicker-close')) { // close button
$('.ui-datepicker-prev')[0].focus();
} else if ($(target).hasClass('ui-state-default')) { // a date link
$('.ui-datepicker-close')[0].focus();
} else if ($(target).hasClass('ui-datepicker-prev')) { // the prev link
$('.ui-datepicker-next')[0].focus();
} else if ($(target).hasClass('ui-datepicker-next')) { // the next link
activeDate = $('.ui-state-highlight') ||
$('.ui-state-active')[0];
if (activeDate) {
activeDate.focus();
}
}
} else if (which === 9) { // TAB
keyVent.preventDefault();
if ($(target).hasClass('ui-datepicker-close')) { // close button
activeDate = $('.ui-state-highlight') ||
$('.ui-state-active')[0];
if (activeDate) {
activeDate.focus();
}
} else if ($(target).hasClass('ui-state-default')) {
$('.ui-datepicker-next')[0].focus();
} else if ($(target).hasClass('ui-datepicker-next')) {
$('.ui-datepicker-prev')[0].focus();
} else if ($(target).hasClass('ui-datepicker-prev')) {
$('.ui-datepicker-close')[0].focus();
}
} else if (which === 37) { // LEFT arrow key
// if we're on a date link...
if (!$(target).hasClass('ui-datepicker-close') && $(target).hasClass('ui-state-default')) {
keyVent.preventDefault();
previousDay(target);
}
} else if (which === 39) { // RIGHT arrow key
// if we're on a date link...
if (!$(target).hasClass('ui-datepicker-close') && $(target).hasClass('ui-state-default')) {
keyVent.preventDefault();
nextDay(target);
}
} else if (which === 38) { // UP arrow key
if (!$(target).hasClass('ui-datepicker-close') && $(target).hasClass('ui-state-default')) {
keyVent.preventDefault();
upHandler(target, container, prev);
}
} else if (which === 40) { // DOWN arrow key
if (!$(target).hasClass('ui-datepicker-close') && $(target).hasClass('ui-state-default')) {
keyVent.preventDefault();
downHandler(target, container, next);
}
} else if (which === 13) { // ENTER
if ($(target).hasClass('ui-state-default')) {
setTimeout(function () {
closeCalendar();
}, 100);
} else if ($(target).hasClass('ui-datepicker-prev')) {
handlePrevClicks();
} else if ($(target).hasClass('ui-datepicker-next')) {
handleNextClicks();
}
} else if (32 === which) {
if ($(target).hasClass('ui-datepicker-prev') || $(target).hasClass('ui-datepicker-next')) {
target.click();
}
} else if (33 === which) { // PAGE UP
moveOneMonth(target, 'prev');
} else if (34 === which) { // PAGE DOWN
moveOneMonth(target, 'next');
} else if (36 === which) { // HOME
var firstOfMonth = $(target).closest('tbody').find('.ui-state-default')[0];
if (firstOfMonth) {
firstOfMonth.focus();
setHighlightState(firstOfMonth, $('#ui-datepicker-div')[0]);
}
} else if (35 === which) { // END
var $daysOfMonth = $(target).closest('tbody').find('.ui-state-default');
var lastDay = $daysOfMonth[$daysOfMonth.length - 1];
if (lastDay) {
lastDay.focus();
setHighlightState(lastDay, $('#ui-datepicker-div')[0]);
}
}
$(".ui-datepicker-current").hide();
});
}
function closeCalendar() {
var container = $('#ui-datepicker-div');
$(container).off('keydown');
var input = $('#datepicker')[0];
$(input).datepicker('hide');
input.focus();
}
function removeAria() {
// make the rest of the page accessible again:
$("#dp-container").removeAttr('aria-hidden');
$("#skipnav").removeAttr('aria-hidden');
}
///////////////////////////////
//////////////////////////// //
///////////////////////// // //
// UTILITY-LIKE THINGS // // //
///////////////////////// // //
//////////////////////////// //
///////////////////////////////
function isOdd(num) {
return num % 2;
}
function moveOneMonth(currentDate, dir) {
var button = (dir === 'next')
? $('.ui-datepicker-next')[0]
: $('.ui-datepicker-prev')[0];
if (!button) {
return;
}
var ENABLED_SELECTOR = '#ui-datepicker-div tbody td:not(.ui-state-disabled)';
var $currentCells = $(ENABLED_SELECTOR);
var currentIdx = $.inArray(currentDate.parentNode, $currentCells);
button.click();
setTimeout(function () {
updateHeaderElements();
var $newCells = $(ENABLED_SELECTOR);
var newTd = $newCells[currentIdx];
var newAnchor = newTd && $(newTd).find('a')[0];
while (!newAnchor) {
currentIdx--;
newTd = $newCells[currentIdx];
newAnchor = newTd && $(newTd).find('a')[0];
}
setHighlightState(newAnchor, $('#ui-datepicker-div')[0]);
newAnchor.focus();
}, 0);
}
function handleNextClicks() {
setTimeout(function () {
updateHeaderElements();
prepHighlightState();
$('.ui-datepicker-next').focus();
$(".ui-datepicker-current").hide();
}, 0);
}
function handlePrevClicks() {
setTimeout(function () {
updateHeaderElements();
prepHighlightState();
$('.ui-datepicker-prev').focus();
$(".ui-datepicker-current").hide();
}, 0);
}
function previousDay(dateLink) {
var container = document.getElementById('ui-datepicker-div');
if (!dateLink) {
return;
}
var td = $(dateLink).closest('td');
if (!td) {
return;
}
var prevTd = $(td).prev(),
prevDateLink = $('a.ui-state-default', prevTd)[0];
if (prevTd && prevDateLink) {
setHighlightState(prevDateLink, container);
prevDateLink.focus();
} else {
handlePrevious(dateLink);
}
}
function handlePrevious(target) {
var container = document.getElementById('ui-datepicker-div');
if (!target) {
return;
}
var currentRow = $(target).closest('tr');
if (!currentRow) {
return;
}
var previousRow = $(currentRow).prev();
if (!previousRow || previousRow.length === 0) {
// there is not previous row, so we go to previous month...
previousMonth();
} else {
var prevRowDates = $('td a.ui-state-default', previousRow);
var prevRowDate = prevRowDates[prevRowDates.length - 1];
if (prevRowDate) {
setTimeout(function () {
setHighlightState(prevRowDate, container);
prevRowDate.focus();
}, 0);
}
}
}
function previousMonth() {
var prevLink = $('.ui-datepicker-prev')[0];
var container = document.getElementById('ui-datepicker-div');
prevLink.click();
// focus last day of new month
setTimeout(function () {
var trs = $('tr', container),
lastRowTdLinks = $('td a.ui-state-default', trs[trs.length - 1]),
lastDate = lastRowTdLinks[lastRowTdLinks.length - 1];
// updating the cached header elements
updateHeaderElements();
setHighlightState(lastDate, container);
lastDate.focus();
}, 0);
}
///////////////// NEXT /////////////////
/**
* Handles right arrow key navigation
* @param {HTMLElement} dateLink The target of the keyboard event
*/
function nextDay(dateLink) {
var container = document.getElementById('ui-datepicker-div');
if (!dateLink) {
return;
}
var td = $(dateLink).closest('td');
if (!td) {
return;
}
var nextTd = $(td).next(),
nextDateLink = $('a.ui-state-default', nextTd)[0];
if (nextTd && nextDateLink) {
setHighlightState(nextDateLink, container);
nextDateLink.focus(); // the next day (same row)
} else {
handleNext(dateLink);
}
}
function handleNext(target) {
var container = document.getElementById('ui-datepicker-div');
if (!target) {
return;
}
var currentRow = $(target).closest('tr'),
nextRow = $(currentRow).next();
if (!nextRow || nextRow.length === 0) {
nextMonth();
} else {
var nextRowFirstDate = $('a.ui-state-default', nextRow)[0];
if (nextRowFirstDate) {
setHighlightState(nextRowFirstDate, container);
nextRowFirstDate.focus();
}
}
}
function nextMonth() {
nextMon = $('.ui-datepicker-next')[0];
var container = document.getElementById('ui-datepicker-div');
nextMon.click();
// focus the first day of the new month
setTimeout(function () {
// updating the cached header elements
updateHeaderElements();
var firstDate = $('a.ui-state-default', container)[0];
setHighlightState(firstDate, container);
firstDate.focus();
}, 0);
}
/////////// UP ///////////
/**
* Handle the up arrow navigation through dates
* @param {HTMLElement} target The target of the keyboard event (day)
* @param {HTMLElement} cont The calendar container
* @param {HTMLElement} prevLink Link to navigate to previous month
*/
function upHandler(target, cont, prevLink) {
prevLink = $('.ui-datepicker-prev')[0];
var rowContext = $(target).closest('tr');
if (!rowContext) {
return;
}
var rowTds = $('td', rowContext),
rowLinks = $('a.ui-state-default', rowContext),
targetIndex = $.inArray(target, rowLinks),
prevRow = $(rowContext).prev(),
prevRowTds = $('td', prevRow),
parallel = prevRowTds[targetIndex],
linkCheck = $('a.ui-state-default', parallel)[0];
if (prevRow && parallel && linkCheck) {
// there is a previous row, a td at the same index
// of the target AND theres a link in that td
setHighlightState(linkCheck, cont);
linkCheck.focus();
} else {
// we're either on the first row of a month, or we're on the
// second and there is not a date link directly above the target
prevLink.click();
setTimeout(function () {
// updating the cached header elements
updateHeaderElements();
var newRows = $('tr', cont),
lastRow = newRows[newRows.length - 1],
lastRowTds = $('td', lastRow),
tdParallelIndex = $.inArray(target.parentNode, rowTds),
newParallel = lastRowTds[tdParallelIndex],
newCheck = $('a.ui-state-default', newParallel)[0];
if (lastRow && newParallel && newCheck) {
setHighlightState(newCheck, cont);
newCheck.focus();
} else {
// theres no date link on the last week (row) of the new month
// meaning its an empty cell, so we'll try the 2nd to last week
var secondLastRow = newRows[newRows.length - 2],
secondTds = $('td', secondLastRow),
targetTd = secondTds[tdParallelIndex],
linkCheck = $('a.ui-state-default', targetTd)[0];
if (linkCheck) {
setHighlightState(linkCheck, cont);
linkCheck.focus();
}
}
}, 0);
}
}
//////////////// DOWN ////////////////
/**
* Handles down arrow navigation through dates in calendar
* @param {HTMLElement} target The target of the keyboard event (day)
* @param {HTMLElement} cont The calendar container
* @param {HTMLElement} nextLink Link to navigate to next month
*/
function downHandler(target, cont, nextLink) {
nextLink = $('.ui-datepicker-next')[0];
var targetRow = $(target).closest('tr');
if (!targetRow) {
return;
}
var targetCells = $('td', targetRow),
cellIndex = $.inArray(target.parentNode, targetCells), // the td (parent of target) index
nextRow = $(targetRow).next(),
nextRowCells = $('td', nextRow),
nextWeekTd = nextRowCells[cellIndex],
nextWeekCheck = $('a.ui-state-default', nextWeekTd)[0];
if (nextRow && nextWeekTd && nextWeekCheck) {
// theres a next row, a TD at the same index of `target`,
// and theres an anchor within that td
setHighlightState(nextWeekCheck, cont);
nextWeekCheck.focus();
} else {
nextLink.click();
setTimeout(function () {
// updating the cached header elements
updateHeaderElements();
var nextMonthTrs = $('tbody tr', cont),
firstTds = $('td', nextMonthTrs[0]),
firstParallel = firstTds[cellIndex],
firstCheck = $('a.ui-state-default', firstParallel)[0];
if (firstParallel && firstCheck) {
setHighlightState(firstCheck, cont);
firstCheck.focus();
} else {
// lets try the second row b/c we didnt find a
// date link in the first row at the target's index
var secondRow = nextMonthTrs[1],
secondTds = $('td', secondRow),
secondRowTd = secondTds[cellIndex],
secondCheck = $('a.ui-state-default', secondRowTd)[0];
if (secondRow && secondCheck) {
setHighlightState(secondCheck, cont);
secondCheck.focus();
}
}
}, 0);
}
}
function onCalendarHide() {
closeCalendar();
}
// add an aria-label to the date link indicating the currently focused date
// (formatted identically to the required format: mm/dd/yyyy)
function monthDayYearText() {
var cleanUps = $('.amaze-date');
$(cleanUps).each(function (clean) {
// each(cleanUps, function (clean) {
clean.parentNode.removeChild(clean);
});
var datePickDiv = document.getElementById('ui-datepicker-div');
// in case we find no datepick div
if (!datePickDiv) {
return;
}
var dates = $('a.ui-state-default', datePickDiv);
$(dates).attr('role', 'button').on('keydown', function (e) {
if (e.which === 32) {
e.preventDefault();
e.target.click();
setTimeout(function () {
closeCalendar();
}, 100);
}
});
$(dates).each(function (index, date) {
var currentRow = $(date).closest('tr'),
currentTds = $('td', currentRow),
currentIndex = $.inArray(date.parentNode, currentTds),
headThs = $('thead tr th', datePickDiv),
dayIndex = headThs[currentIndex],
daySpan = $('span', dayIndex)[0],
monthName = $('.ui-datepicker-month', datePickDiv)[0].innerHTML,
year = $('.ui-datepicker-year', datePickDiv)[0].innerHTML,
number = date.innerHTML;
if (!daySpan || !monthName || !number || !year) {
return;
}
// AT Reads: {month} {date} {year} {day}
// "December 18 2014 Thursday"
var dateText = date.innerHTML + ' ' + monthName + ' ' + year + ' ' + daySpan.title;
// AT Reads: {date(number)} {name of day} {name of month} {year(number)}
// var dateText = date.innerHTML + ' ' + daySpan.title + ' ' + monthName + ' ' + year;
// add an aria-label to the date link reading out the currently focused date
date.setAttribute('aria-label', dateText);
});
}
// update the cached header elements because we're in a new month or year
function updateHeaderElements() {
var context = document.getElementById('ui-datepicker-div');
if (!context) {
return;
}
// $(context).find('table').first().attr('role', 'grid');
prev = $('.ui-datepicker-prev', context)[0];
next = $('.ui-datepicker-next', context)[0];
//make them click/focus - able
next.href = 'javascript:void(0)';
prev.href = 'javascript:void(0)';
next.setAttribute('role', 'button');
prev.setAttribute('role', 'button');
appendOffscreenMonthText(next);
appendOffscreenMonthText(prev);
$(next).on('click', handleNextClicks);
$(prev).on('click', handlePrevClicks);
// add month day year text
monthDayYearText();
}
function prepHighlightState() {
var highlight;
var cage = document.getElementById('ui-datepicker-div');
highlight = $('.ui-state-highlight', cage)[0] ||
$('.ui-state-default', cage)[0];
if (highlight && cage) {
setHighlightState(highlight, cage);
}
}
// Set the highlighted class to date elements, when focus is received
function setHighlightState(newHighlight, container) {
var prevHighlight = getCurrentDate(container);
// remove the highlight state from previously
// highlighted date and add it to our newly active date
$(prevHighlight).removeClass('ui-state-highlight');
$(prevHighlight).removeAttr('aria-current');
$(newHighlight).attr('aria-current', 'date');
$(newHighlight).addClass('ui-state-highlight');
}
// grabs the current date based on the highlight class
function getCurrentDate(container) {
var currentDate = $('.ui-state-highlight', container)[0];
return currentDate;
}
/**
* Appends logical next/prev month text to the buttons
* - ex: Next Month, January 2015
* Previous Month, November 2014
*/
function appendOffscreenMonthText(button) {
var buttonText;
var isNext = $(button).hasClass('ui-datepicker-next');
var months = [
'january', 'february',
'march', 'april',
'may', 'june', 'july',
'august', 'september',
'october',
'november', 'december'
];
var currentMonth = $('.ui-datepicker-title .ui-datepicker-month').text().toLowerCase();
var monthIndex = $.inArray(currentMonth.toLowerCase(), months);
var currentYear = $('.ui-datepicker-title .ui-datepicker-year').text().toLowerCase();
var adjacentIndex = (isNext) ? monthIndex + 1 : monthIndex - 1;
if (isNext && currentMonth === 'december') {
currentYear = parseInt(currentYear, 10) + 1;
adjacentIndex = 0;
} else if (!isNext && currentMonth === 'january') {
currentYear = parseInt(currentYear, 10) - 1;
adjacentIndex = months.length - 1;
}
buttonText = (isNext)
? 'Next Month, ' + firstToCap(months[adjacentIndex]) + ' ' + currentYear
: 'Previous Month, ' + firstToCap(months[adjacentIndex]) + ' ' + currentYear;
$(button).find('.ui-icon').html(buttonText);
}
// Returns the string with the first letter capitalized
function firstToCap(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
CSS Source Code
Dependencies:
- JQuery UI theme: Smoothness (including some images)
#datepicker {
margin: 0;
padding: 0;
height: 25px;
vertical-align: bottom;
outline-offset: 1.5px;
margin-right: 5px;
border: 1px #333 solid!important;
}
.ui-datepicker-trigger {
margin: 2px;
padding: 0;
vertical-align: bottom;
}
.ui-datepicker-trigger img{
width: 25px;
height: 25px;
}
.offscreen {
position: absolute;
width: 1px;
height: 1px;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
margin: 0;
padding: 0;
}
button.ui-datepicker-trigger {
background-color:transparent !important;
border:none !important;
margin:auto !important;
}
button.ui-datepicker-trigger:focus,
button.ui-datepicker-trigger:hover,
button.ui-datepicker-trigger:active {
border:none !important;
margin:auto !important;
outline:2px solid #588f0e !important;
background-color:#fdf6e7 !important;
}
Copy and Paste Full Page Example
License
This datepicker component is based on the date picker in Jquery UI. The license used by JQuery is the MIT license, which allows for commercial and non-commercial use of the component, as long as the copyright is left intact.