Screen reader doesn't read cell value when moving accros the datepicker grid

115 Views Asked by At

I have the following datepicker. The problem is that when running screen reader (NVDA), when moving the selection on the datepicker grid to different cells using keyboard, the screen reader just says "blank". (However, if I hover over the cells with mouse, it does read the number of the day correctly.)

$(document).ready(function () {
    $('#ui-datepicker-div').addClass("month-calendar-widget");
    $(".month-calendar input").datepicker({
        showOtherMonths: true,
        minDate: 0,
        beforeShow: function () {
          $('#ui-datepicker-div').addClass("month-calendar-widget");
        }
    }).datepicker("setDate", convertDateToUTC(new Date()));
});

What I was hoping was to fix this issue by adding aria-label attribute to each cell of the grid using beforeShowDay like below:

  beforeShowDay: function(date) {
    var dateString = $.datepicker.formatDate('MM d, yy', date);
    return [true, '', dateString + "" + 'aria-label="' + dateString + '" role="gridcell" aria-selected="false"'];
  }

However, this results in the follwing html which is wrong:

<td class=" " data-event="click" data-handler="selectDay" data-month="1" data-year="2024" title="February 16, 2024aria-label=&quot;February 16, 2024&quot; role=&quot;gridcell&quot; aria-selected=&quot;false&quot;">
    <a class="ui-state-default" href="#">16</a>
</td>

What am I missing?

Update 1:

To further clarify, here is my goal:

  • Run NVDA (just google nvda download and download and run it)
  • Navigate to, for example, the datepicker Diego provided in his answer below, then using keyboard move to different cells in the grid: enter image description here
  • I am expecting NVDA say the date, but it just says "blank".

update 2

Use this jsfiddle to play with Diego's answer

update 3

After further investigation, I think there is a fundamental issue with regard to accessibility of this JQuery datepicker: basically, imagine you cannot visually see the page and you tab into that field. Now there are two simultaneous options to enter the date: both by typing and moving across the datepicker grid. This is also probably why the screen-reader is confused too. I think the two options should have separate triggers: i.e. we have a text box where user can enter text, AND a button that is when clicked, opens the date picker where the user can now select a date. I looked some other implementations of the date-picker and they all seem to have implemented it this way. I also found this github issue for the same: https://github.com/jquery/jquery-ui/issues/2155

1

There are 1 best solutions below

14
Diego D On

Since beforeShowDay supports only creating the value of the title attribute, and since the trick of closing the quote to add your further attribute may not be enough because it's addressing a td and not the interactive element, you may just use a different strategy to populate the aria-label attribute on the actual anchor.

You can rely on the option beforeShow that will run the function given just one moment before the datepicker is actually shown:

https://api.jqueryui.com/datepicker/#option-beforeShow

beforeShow Type: Function( Element input, Object inst )

A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed.

Such function will iterate over each interactive element inside the calendar containing every single day the user can pick (anchor element) and will add an aria-label attribute to it with the value coming from the title attribute on its parent cell (set by beforeShowDay as you did).

This is an example of a day cell being processed:

<td
  class="ui-datepicker-days-cell-over ui-datepicker-current-day ui-datepicker-today"
  title="February 9, 2024"
  data-handler="selectDay"
  data-event="click"
  data-month="1"
  data-year="2024">
    <a
      class="ui-state-default ui-state-highlight ui-state-active" href="#"
      aria-label="February 9, 2024">9</a>
</td>

There was the option of crafting the date by picking its parts from each component like this, but it was overkill:

const year = parentCell.attr('data-year');
const month = parentCell.attr('data-month');
const day = $(this).text();

const paddedMonth =
  (parseInt(month, 10) + 1).toString().padStart(2, '0'); 
const paddedDay = day.padStart(2, '0'); 

//previously grab day info and format the correponding date
const formattedDate = `${paddedDay}/${paddedMonth}/${year}`;

//populate the day anchor with the aria-label containing dd/mm/yyyy
$(this).attr('aria-label', formattedDate);

As a further action I also dealt with the onSelect event that will add the aria-label attribute also to the input element once a date was selected inside the datepicker. It could be not needed since probably screen readers already can read the value of an input element.

Edit: as you found out with a library specifically designed to solve this particular problem, the datepicker doesn't move the focus while navigating over the calendar and it instead remains locked on the input field. That's why despite the 'aria-label` was set, it couldn't be read if the screenreader couldn't see the focus moving.

So the solution I provided here, so far, cannot fully address the scenario because it's missing the logics to deal with the focus.

That library I see instead is using a strategy based on the tab-index attribute and aria-select correctly achieving that goal.

I also used the option showOn as button so that the datepicker won't show up when the text input gained focus but only when the dedicated button is clicked.

https://api.jqueryui.com/datepicker/#option-showOn

When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both").

$(document).ready(function () {

  /*
  $("#pickdate").click(function(){
    $(".month-calendar input").datepicker("show");
  });
  */

  $(".month-calendar input").datepicker({
    showOtherMonths: true,
    minDate: 0,
    //prevent from showing on focus and use the default button instead
    //if you wish instead a custom button, uncomment both html and js related
    //and here use the option "none"
    showOn: "button",
    
    //on date selected
    onSelect: function(dateText, inst) {
      const value = "Selected date: " + dateText;
      //set the aria-label of the input element with the date picked
      $(this).attr("aria-label", value);
    },
    
    //before showing each day in the datepicker grid
    beforeShowDay: function(date) {
      //return a value used to populate the title attr of the day td
      const dateString = $.datepicker.formatDate('MM d, yy', date);
      return [true, '', dateString];
    },
    
    //before showing the days grid
    beforeShow: function(input, inst){
      //set timeout is to wait for the ui to be rendered
      setTimeout(function() {
        //select the calendar element
        const daysGrid = $('#ui-datepicker-div .ui-datepicker-calendar');
        //fetch all the anchors being interactive days
        const interactiveDays = $(daysGrid).find('td a');
        
        //for each one of them
        interactiveDays.each(function(){
          const parentCell = $(this).closest('td');                                    
          //set the aria-label as the same value on the title attr of its parent
          $(this).attr('aria-label', $(parentCell).attr('title'));
          
          //...other aria attributes could be set at this point...
        });
      }); 
    }

  }).datepicker("setDate", new Date());
});
button{
  cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

<div class="month-calendar">
  <input type="text" id="datepicker">
  <!--
  <button id="pickdate">datepicker</button>
  -->
</div>