Search code examples
htmlangularangular-materialjaws-screen-reader

Using mat-date-range-input but the JAWS reader first reads the end date with JAWS-e (next edit box)


I have a component with a mat-date-range-input. My client uses the JAWS reader and uses JAWS-e (next edit box) to navigate. When they do this JAWS first reads the 'end date' of the mat-date-range-input. The 'start date' is read when JAWS-e is pressed again. I had expected that the 'start date' would be read first.

Below is the part of the html with the mat-date-range-input. I first had the feeling that the span with aria-hidden=true could be the cause of the issue but removing it didn't fix it.

Has anyone experienced this before? Is there a solution?

Thanks&Regards, Nelleke

<div _ngcontent-ynn-c151="" class="ng-star-inserted" style="">
   <mat-form-field _ngcontent-ynn-c151="" appearance="fill" class="mat-form-field date-picker ng-tns-c64-23 mat-primary mat-form-field-type-mat-date-range-input mat-form-field-appearance-fill mat-form-field-can-float mat-form-field-has-label mat-form-field-hide-placeholder ng-star-inserted">
      <div class="mat-form-field-wrapper ng-tns-c64-23">
         <div class="mat-form-field-flex ng-tns-c64-23">
            <div class="mat-form-field-infix ng-tns-c64-23">
               <mat-date-range-input _ngcontent-ynn-c151="" role="group" class="mat-date-range-input ng-tns-c64-23" aria-labelledby="mat-form-field-label-37" data-mat-calendar="mat-datepicker-2">
                  <div cdkmonitorsubtreefocus="" class="mat-date-range-input-container">
                     <div class="mat-date-range-input-start-wrapper">
                        <input _ngcontent-ynn-c151="" type="text" matstartdate="" name="startDate" class="mat-start-date mat-date-range-input-inner startDate ng-touched ng-pristine ng-valid" placeholder="startdatum" id="mat-date-range-input-0" aria-haspopup="dialog" min="1800-01-01T00:00:00+00:19" max="2022-03-02T00:00:00+01:00">
                        <span aria-hidden="true" class="mat-date-range-input-mirror">startdatum</span>
                     </div>
                     <span class="mat-date-range-input-separator mat-date-range-input-separator-hidden">–</span>
                     <div class="mat-date-range-input-end-wrapper">
                        <input _ngcontent-ynn-c151="" type="text" matenddate="" name="endDate" class="mat-end-date mat-date-range-input-inner endDate ng-touched ng-pristine ng-valid" placeholder="einddatum" aria-haspopup="dialog" min="1800-01-01T00:00:00+00:19" max="2022-03-02T00:00:00+01:00">
                     </div>
                  </div>
               </mat-date-range-input>
               <mat-date-range-picker _ngcontent-ynn-c151="" class="ng-tns-c64-23"></mat-date-range-picker>
               <span class="mat-form-field-label-wrapper ng-tns-c64-23">
                  <label class="mat-form-field-label ng-tns-c64-23 mat-empty mat-form-field-empty ng-star-inserted" id="mat-form-field-label-37" for="mat-date-range-input-0" aria-owns="mat-date-range-input-0">
                     <mat-label _ngcontent-ynn-c151="" class="ng-tns-c64-23 ng-star-inserted">Datum (max 28 dagen)</mat-label>
                  </label>
               </span>
            </div>
            <div class="mat-form-field-suffix ng-tns-c64-23 ng-star-inserted">
               <mat-datepicker-toggle _ngcontent-ynn-c151="" matsuffix="" class="mat-datepicker-toggle ng-tns-c64-23" data-mat-calendar="mat-datepicker-2">
                  <button mat-icon-button="" type="button" class="mat-focus-indicator mat-icon-button mat-button-base" aria-haspopup="dialog" aria-label="Open calendar" tabindex="0">
                     <span class="mat-button-wrapper">
                        <mat-icon _ngcontent-ynn-c151="" role="img" matdatepickertoggleicon="" class="mat-icon notranslate material-icons mat-icon-no-color" aria-hidden="true" data-mat-icon-type="font">date_range</mat-icon>
                     </span>
                     <span matripple="" class="mat-ripple mat-button-ripple mat-button-ripple-round"></span>
                     <span class="mat-button-focus-overlay"></span>
                  </button>
               </mat-datepicker-toggle>
            </div>
         </div>
         <div class="mat-form-field-underline ng-tns-c64-23 ng-star-inserted">
            <span class="mat-form-field-ripple ng-tns-c64-23">
            </span>
         </div>
         <div class="mat-form-field-subscript-wrapper ng-tns-c64-23">
            <div class="mat-form-field-hint-wrapper ng-tns-c64-23 ng-trigger ng-trigger-transitionMessages ng-star-inserted" style="opacity: 1; transform: translateY(0%);">
               <div class="mat-form-field-hint-spacer ng-tns-c64-23"></div>
            </div>
         </div>
      </div>
   </mat-form-field>
</div>


Solution

  • This problem occurs no matter what screen reader you use, not just JAWS.

    Angular Material is mis-using the aria-owns attribute. You can test this on their "Date Range Selection" example. Their code is basically like this:

    <input placeholder="Start date" id="startdate">
    <input placeholder="End date">
    <label for="startdate" aria-owns="startdate">Enter a date range</label>
    

    Like all ARIA attributes, aria-owns is just a hint to the screen reader. ARIA attributes are supposed to help the screen reader understand the organization of the page. ARIA attributes do not affect the appearance of the page nor do they affect how the browser behaves. In this particular case, the ARIA attributes do not affect browser's tabbing order.

    So whether ARIA attributes exist or not, when you tab through the date range widget, the tab order (by default) is the same as the DOM order. In this case, the <input placeholder="Start date" id="startdate"> is first so it receives tab focus first. The next tab goes to the next DOM element which is the <input placeholder="End date">. The <label> is not a tab stop.

    This all works as expected.

    Where it gets funky is if you navigate using screen reader commands. A screen reader user can use the tab key as normal and they will hear things in the right order. But if the screen reader user tries to navigate the DOM (really the AOM - accessibility object model), whether via the E key (usually it's INS+E) or the downArrow key, then aria-owns will affect things, because ARIA attributes affect the screen reader.

    One other concept to understand is the AOM. The Accessibility Object Model is like the Document Object Model (DOM) except it's esentially a subset of the DOM. Not everything on the DOM will be in the AOM. A screen reader interacts with the AOM when you use screen reader commands.

    For example, if you have an element with display:none, that element still exists in the DOM. You can still call getObjectByName() on it. But the element will be removed from the AOM so that the screen reader user can no longer navigate to that element using screen reader commands.

    A similar thing happens with aria-hidden, as noted in the OP. Setting aria-hidden to true will remove that element from the AOM so the screen reader no longer thinks it's there.

    So, as mentioned earlier, the downArrow key (or E key specifically for input fields) can be used to walk the AOM. It lets the user walk the AOM as if it were a text document. Think of it like reading the HTML file in a text editor and you use the downArrow key to go to the next line in the editor.

    aria-owns changes the AOM order without physically changing the DOM. aria-owns lets you affect the parent/child relationship in the AOM and mimics an element being after another element even if it physically isn't in the HTML file.

    So in this case, even though the DOM physically looks like this:

    <input placeholder="Start date" id="startdate">
    <input placeholder="End date">
    <label for="startdate" aria-owns="startdate">Enter a date range</label>
    

    The AOM will look like this:

    <input placeholder="End date">
    <label for="startdate" aria-owns="startdate">Enter a date range</label>
    <input placeholder="Start date" id="startdate">
    

    because the aria-owns="startdate" on the <label> causes the startdate element to be after the label.

    Yes, it's complicated but once you see what's causing it, it's kind of simple.

    Is there a solution?

    No :-( other than contacting google to let them know they're not using aria-owns correctly.