LOADING...

Preview

Pen ID
Unlock Campus Themeforest adv

 

Code

Needing a solution for an existing datepicker that had a readonly input field, opening a pop-up of a calendar only by clicking on the input, with no way to access any piece of it from getting to the input to navigating the calendar popup via keyboard alone, and having no feedback of the selection to screen reader applications... which broke a number of accessibility concerns.

Choose a Date

While there are additional accessibility considerations to address, such as including additional relevant aria states, this solution is a standalone jQuery plugin that does not rely on a third party library in order to function, is able to use a custom image for the calendar or by default uses a fully CSS-styled icon, and allows for direct input into the field in addition to fully supporting keyboard interactions.

For reference:http://www.webaxe.org/accessible-date-pickers/

CSS
* {
  box-sizing: border-box;
}
img {
  max-width: 100%;
}
.mvs-datepicker-cell {
  display: inline;
  position: relative;
}
.mvs-datepicker-cell.hasimage {
  margin-right: 35px;
}
button.mvs-datepicker-trigger {
  padding: 0;
  vertical-align: bottom;
  width: 30px;
  height: 30px;
  background-color: transparent;
  border: none;
  margin: auto;
}
.ui-cal {
  font-size: 1em;
  width: 100%;
  height: 100%;
  border-radius: 10%;
  overflow: hidden;
}
.ui-cal .ui-cal-header {
  width: 100%;
  height: 25%;
  background-color: red;
}
.ui-cal .ui-cal-body {
  width: 100%;
  height: 75%;
  border: 1px solid #bbb;
  border-top: 1px dashed #999;
}
#mvs-datepicker-calendar {
  position: absolute;
  left: 110%;
  top: -50%;
  z-index: 1;
  text-align: center;
  background-color: white;
  width: 220px;
  border: 2px solid #ddd;
  border-radius: 4px;
}
#mvs-datepicker-calendar table {
  width: 100%;
  table-layout: fixed;
  height: 175px;
  padding: 5px;
}
#mvs-datepicker-calendar .year-header {
  padding-top: 2px;
}
#mvs-datepicker-calendar .year-header,
#mvs-datepicker-calendar .month-header {
  background-color: #ddd;
}
#mvs-datepicker-calendar .year-header,
#mvs-datepicker-calendar .month-header,
#mvs-datepicker-calendar .cal-footer {
  padding-bottom: 4px;
}
#mvs-datepicker-calendar .highlighted {
  background-color: #580;
  color: #fff;
}
#mvs-datepicker-calendar .cal-nav {
  display: inline-block;
}
#mvs-datepicker-calendar div.cal-nav {
  width: 60%;
}
#mvs-datepicker-calendar button {
  border-radius: 2px;
  border-style: solid;
  border-width: 1px;
  background-color: #fff;
  color: #727272;
  font-weight: 100;
}
#mvs-datepicker-calendar button:focus,
#mvs-datepicker-calendar button:active {
  outline: none;
  box-shadow: 0px 0px 0 1px #006666 !important;
  border: 1px solid #117788;
}
#mvs-datepicker-calendar button:hover {
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
#mvs-datepicker-calendar button.cal-nav {
  width: 20%;
  max-width: 40px;
}
input[readonly] {
  border: 1px solid #aaa;
  background-color: #ccc;
  color: #777;
}
JS
const ESC_KEY = 27;

var MvsDatepicker = function($el, options) {
  $el.container = $el.closest('.mvs-datepicker-cell');
  $el.trigger;
  $el.calendar = false;
  
  this._vars = {
    triggerMarkup: '
' + '
' + '
25
' + '
', calWeekdayLabels: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], calWeekdayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], calMonthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] }; this._selectors = { mainSelector: '.mvs-datepicker', triggerClass: 'mvs-datepicker-trigger', calendarID: 'mvs-datepicker-calendar', }; // Default settings for the datepicker being created this._defaults = { buttonImage: false }; // Set tooltip options, based on defaults and overriding settings on the function call this._options = $.extend(true, {}, this._defaults, options); this.options = function(options) { return (options) ? $.extend(true, this._options, options) : this._options; }; // Method to set all base events on the selected datepicker elements this._createTrigger = function() { // console.info('inside: _createTrigger'); var $trigger = $('') .addClass(this._selectors.triggerClass) .attr('type', 'button'); if(this._options.buttonImage) { $trigger.append($('') .attr('src', this._options.buttonImage)); } else { $trigger.append( $(this._vars.triggerMarkup)); } $trigger.on('click.mvsdatepicker', function (e) { if( ! $(this).hasClass("expanded") ) { $el.mvsDatepicker('open'); } else { $el.mvsDatepicker('close'); } e.stopPropagation(); }); // Close on typing escape key, from anywhere within the datepicker element $trigger.on('keydown.mvsdatepicker', function (e) { if( e.which === ESC_KEY && $(this).hasClass('expanded') ) { // console.log('calling close from trigger logic'); $(this).mvsDatepicker('close'); } e.stopPropagation(); }); $el.after($trigger); $el.trigger = $trigger; }; this._generateCalendar = function( paramDate ) { // console.info('inside: _generateCalendar'); var inputDate = this._validateInputDate($el.val()); var calHtml; var $calObject; var calDate; var calDateDay; var calDateMonth; var calDateYear; var calFirstDay; var calStartingDay; var calMonthLength; var dayIterator = 1; var i = 0; var j = 0; this._removeCalendar(); // TODO: need to validate the input before going forward if( paramDate ) { if( typeof paramDate === "string" ) { calDate = new Date( paramDate ); } else if( typeof paramDate === "object" ) { calDate = new Date( paramDate.year, paramDate.month, paramDate.day ); } } else if ( inputDate ) { calDate = new Date(inputDate) != 'Invalid Date' ? new Date(inputDate) : new Date(); } else { calDate = new Date(); } if( ! calDate ) { // TODO: should revert invalid to current/default calDate = new Date(); } calDateDay = calDate.getDate(); calDateMonth = calDate.getMonth(); calDateYear = calDate.getFullYear(); calFirstDay = new Date(calDateYear, calDateMonth, 1); calStartingDay = calFirstDay.getDay(); calMonthLength = this._getDaysInMonth(calDateMonth + 1, calDateYear); // Start building the table, do the header calHtml = ''; $calObject = $(calHtml); // $calObject.on('click', 'button', $.proxy(this._handleCalNav, this)); $calObject.on('click.mvsdatepicker', 'button.cal-nav', $.proxy(this._handleCalNav, this)); $calObject.on('click.mvsdatepicker', 'button.cal-close', $.proxy(this.close, this)); $calObject.on('click.mvsdatepicker', '.calendar-day', $.proxy(this._selectCalDay, this)); $calObject.on('keydown.mvsdatepicker', $.proxy(this._handleCalendarKeyvents, this)); // TODO actually attach as a sibling to the input as a popup/dialog // $('.calendarzone').html($calObject); $el.parent().append($calObject); $el.calendar = $calObject; $calObject.find('.highlighted').focus(); }; this._removeCalendar = function() { if($el.calendar) { $el.calendar.remove(); $el.calendar = false; } } this._validateInputDate = function( dateString ) { // Could have unmet conditions return error objects to alert on page var parts, day, month, year, monthLength; // First check for the pattern if(!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(dateString)) return false; // Parse the date parts to integers parts = dateString.split("/"); day = parseInt(parts[1], 10); month = parseInt(parts[0], 10); year = parseInt(parts[2], 10); // Check the ranges of month and year if(month == 0 || month > 12) return false; monthLength = this._getDaysInMonth(month, year); // Check the range of the day if(day <= 0 || day > monthLength) return false; return dateString; } this._getDaysInMonth = function( month, year ) { // 1-based, so increment 0-based by 1 // console.info('inside: _getDaysInMonth'); return new Date(year, month, 0).getDate(); } this._getCalDate = function() { return new Date($el.calendar.data('date-year'), $el.calendar.data('date-month'), $el.calendar.data('date-day')); } this._handleCalNav = function(evt) { // console.info('inside: _handleCalNav'); // TODO this can certainly be optimized... this implementation is temporary var calnavObject = $(evt.target); var calnavDirection = calnavObject.data('calnav-direction'); var calnavScope = calnavObject.data('calnav-scope'); var calDay = $el.calendar.data('date-day'); var calMonth = $el.calendar.data('date-month'); var calYear = $el.calendar.data('date-year'); var calNavSelector = ('.cal-nav.' + calnavScope + '-' + calnavDirection); if(calnavDirection === "back") { if(calnavScope === "year") { calYear --; } else if (calnavScope === "month") { calMonth --; } } else if(calnavDirection === "forward") { if(calnavScope === "year") { calYear ++; } else if (calnavScope === "month") { calMonth ++; } } this._generateCalendar({year: calYear, month: calMonth, day: calDay}); // probably pass this into separate functions based on the handler directions, to account for which selector keeps focus, etc. $el.calendar.find(calNavSelector).focus(); } this._handleCalendarKeyvents = function(evt) { // console.info('inside: _handleCalendarKeyvents'); var which = evt.which; var target = evt.target; var $target = $(target); // console.log(which); // console.log(target); switch(which) { case 9: if(evt.shiftKey) { if($target.hasClass('year-back')) { evt.preventDefault(); $el.calendar.find('.cal-close').focus(); } } else { if($target.hasClass('cal-close')) { evt.preventDefault(); $el.calendar.find('.year-back').focus(); } } break; case 13: // console.info('case 13: enter'); if($target.hasClass('calendar-day')) { // evt.stopPropagation(); evt.preventDefault(); this._selectCalDay(evt); } break; case 27: // console.info('case 27: esc'); evt.stopPropagation(); this.close(); break; case 37: case 39: case 38: case 40: // console.info('arrow, code: ' + which); if($target.hasClass('calendar-day')) { evt.preventDefault(); this._handleArrowNav(evt); } break; } } this._handleArrowNav = function(evt) { // console.info('inside: _handleArrowNav'); var which = evt.which; var target = evt.target; var $target = $(target); var newDateDay = $target.data('cal-val'); // get current, will update based on which arrow var daysInMonth = this._getDaysInMonth($el.calendar.data('date-month') + 1, $el.calendar.data('date-year')); var getNewCal = false; switch(which) { case 37: newDateDay -= 1; break; case 38: newDateDay -= 7; break; case 39: newDateDay += 1; break; case 40: newDateDay += 7; break; } if( newDateDay < 1 || newDateDay > daysInMonth ) { this._generateCalendar({year: $el.calendar.data('date-year'), month: $el.calendar.data('date-month'), day: newDateDay}); } else { this._moveCalDay(newDateDay); } }; this._moveCalDay = function(moveDay) { // console.info('inside: _moveCalDay'); $el.calendar.find('.highlighted').removeClass('highlighted').attr('tabindex', '-1'); $el.calendar.find('[data-cal-val=' + moveDay + ']').addClass('highlighted').attr('tabindex', '0').focus(); $el.calendar.data('date-day', moveDay).attr('data-date-day', moveDay); } this._selectCalDay = function(evt) { // console.info('inside: _selectCalDay'); var selectedDay = $(evt.target).closest('.calendar-day'); var selectedDayVal = selectedDay.data('cal-val'); $el.calendar.find('.highlighted').removeClass('highlighted').attr('tabindex', '-1'); selectedDay.addClass('highlighted').attr('tabindex', '0'); $el.calendar.data('date-day', selectedDayVal).attr('data-date-day', selectedDayVal); this._setInputValByCal(); }; this._setInputValByCal = function(){ // console.info('inside: _setInputValByCal'); $el.val(($el.calendar.data('date-month') + 1) + '/' + $el.calendar.data('date-day') + '/' + $el.calendar.data('date-year')); this.close(); }; this.open = function() { // console.info("inside: open"); $(this._selectors.mainSelector).mvsDatepicker('close'); $el.trigger.addClass('expanded'); this._generateCalendar(); }; this.close = function() { // console.info("inside: close"); this._removeCalendar(); $el.trigger.removeClass('expanded'); $el.trigger.focus(); }; // Method to set all base events on the selected datepicker elements this._setEvents = function() { // console.info('inside: _setEvents'); // // event setting structure for the element itself... still thinking on this one... // $el.on('change.mvsdatepicker', ) }; // Method to unset all mvsdatepicker events this._unsetEvents = function() { // console.info('inside: _unsetEvents'); $el.off('.mvsdatepicker'); }; this.init = function() { // console.info($el.attr('id')); this._createTrigger(); // Finish init, set all events this._setEvents(); } this.init(); }; // Add as a jQuery function, specifying acceptable methods to be passed $.fn.mvsDatepicker = function(methodOrOptions) { var acceptableMethods = ['open', 'close', 'disable', 'enable'] var method = (typeof methodOrOptions === 'string') ? methodOrOptions : undefined; // console.info("method is: " + method); var options; var mvsDatepickers = []; var args = (arguments.length > 1) ? Array.prototype.slice.call(arguments, 1) : undefined; var results = []; function getMvsDatepicker() { var $el = $(this); var mvsDatepicker = $el.data('mvsDatepicker'); mvsDatepickers.push(mvsDatepicker); }; function applyMethod(index) { var mvsDatepicker = mvsDatepickers[index]; if (!mvsDatepicker) { throw new Error('$.mvsDatepicker not instantiated yet'); console.info(this); results.push(undefined); return; } if (typeof mvsDatepicker[method] === 'function') { var result = mvsDatepicker[method].apply(mvsDatepicker, args); results.push(result); } else { throw new Error('Method \'' + method + '\' not defined in $.mvsDatepicker'); } } function init() { var $el = $(this); var mvsDatepicker = new MvsDatepicker($el, options); $el.data('mvsDatepicker', mvsDatepicker); } if (method) { if( ! acceptableMethods.includes(method) ) { throw new Error('Method \'' + method + '\' may not be called from $.mvsDatepicker'); } this.each(getMvsDatepicker); this.each(applyMethod); return (results.length > 1) ? results : results[0]; } else { options = (typeof methodOrOptions === 'object') ? methodOrOptions : undefined; return this.each(init); } }; $($('.mvs-datepicker')[0]).mvsDatepicker(); $($('.mvs-datepicker')[1]).mvsDatepicker({'buttonImage': 'https://dequeuniversity.com/assets/images/calendar.png'});
Term
Wed, 12/27/2017 - 06:56

Related Codes

Pen ID
Pen ID
Pen ID
Square Adv