Keyboard controls for DatePicker inside bootstrap modal

1 Answer 311 Views
DatePicker TimePicker
Ben
Top achievements
Rank 1
Iron
Ben asked on 22 Aug 2023, 07:12 PM | edited on 22 Aug 2023, 08:11 PM

We currently have a telerik datepicker (and timepicker) displayed inside a bootstrap modal, and while it works (mostly) fine, I've discovered that accessibility keyboard controls (enabled via setting EnableAriaSupport and EnableKeyboardNavigation to True) do not work. The controls work fine in context of a datepicker outside of these modals.

Upon further investigation, it seems as though the telerik process is adding a new div to the dom when we open the datepicker calendar, which presumably is firing some low-level bootstrap event or process that I'm unaware of to force focus back to the modal... and in this case away from the datepicker that's appearing over it.

Does anyone know of a way to prioritize keyboard control focus to remain on the added datepicker calendar that appears instead of reverting back to the top of the modal?

Things I've tried include:

  • Setting the modal's data-keyboard and data-focus properties to false (which did nothing except disabling people to press escape to close the modal)
  • setting the date-picker's z-index arbitrarily high (despite it already rendering on top of the modal to begin with
  • setting some properties for bootstrap-adjacent things in the off chance that they might work (such as Vue.JS). These, predictably, did nothing.
  • fiddled with some raw JS events that might have impacted the process. These did not get hit upon loading the datepicker when test breakpoints were added to them
  • setting a hidden field on the datepicker's sub-controls with the ID "hasControlOnModal" to True. This was preexisting code from the 2009 version of the datepicker that I'm currently trying to get away from, and doesn't seem to do anything as far as I can tell.
  • confirming in VB code that the controls in and out of the modal are built the same way (they run through the same code)
  • Tried to catch the calendar opening and manually force focus in JS via the OnPopupOpening event (this doesn't work as the documentation points out this event is just BEFORE the calendar loads into the dom... I need it to be there so I can use jquery's .focus() on it.

It's worth noting that I do not have the option to swap away from the current way we build modals and use a telerik modal at this time.

The datepicker, opened with keyboard controls, as it appears outside the modal (note the black box around the calendar, denoting that the control is in-focus of the keyboard and is usable via arrow key navigation:

The modal with a datepicker attached to it. The focus is on the invisible "title" of the modal, which announces to screen readers that there's a modal open. hitting "tab" here shifts the focus to the "close" button.

Tabbing down to the datepicker works as intended, and upon hitting "enter" on the open calendar button, you're presented with this screen:

Notice that the calendar does not have the focused black box around it, as the focus has shifted back to the top of the bootstrap modal. Keyboard controls do not work. Hitting "tab" from here will once again move the focus to the modal's "close" button. There is no way for me to tab into the calendar control.

 

Rumen
Telerik team
commented on 25 Aug 2023, 02:35 PM

Hi Ben,

I spent some time playing with the reported scenario with the following setup:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>

<!DOCTYPE html>

<html lang="en-us" xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>test</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</head>
<body>
    <form id="form1" runat="server">


        <telerik:RadScriptManager ID="RadScriptManager1" runat="server"></telerik:RadScriptManager>

        <!-- Button trigger modal -->
        <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
            Launch demo modal
        </button>

        <!-- Modal -->
        <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                    </div>
                    <div class="modal-body">
                        <telerik:RadDatePicker ID="RadDatePicker2" runat="server" EnableAriaSupport="true" Calendar-EnableNavigationAnimation="false" ShowAnimation-Duration="0" EnableKeyboardNavigation="true"
                            ClientEvents-OnPopupOpening="OnPopupOpening">
                            <Calendar runat="server" EnableKeyboardNavigation="true"></Calendar>
                        </telerik:RadDatePicker>
                        <script>  
                            function OnPopupOpening(sender, args) {
                                setTimeout(function () {
                                    debugger
                                    document.activeElement.setAttribute("tabindex", "0");
                                    document.activeElement.blur();
                                    args.get_popupControl().CurrentViews[0].DomTable.focus();
                                    args.get_popupControl().CurrentViews[0].DomTable.tabIndex = 0;
                                    args.get_popupControl().get_element().style.border = "1px solid red";
                                  // console.log(document.activeElement)
                                }, 300);
                            }
                        </script>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary">Save changes</button>
                    </div>
                </div>
            </div>
        </div>

 
    </form>
</body>
</html>

where the 

args.get_popupControl().CurrentViews[0].DomTable.focus();

args.get_popupControl().CurrentViews[0].DomTable.tabIndex = 0;

lines are taken from the source code of the showPopup: function(x, y) {

...

        if ((this._calendar._enableKeyboardNavigation) && (!this._calendar._enableMultiSelect)) {
            this._calendar.CurrentViews[0].DomTable.tabIndex = 0;
            this._calendar.CurrentViews[0].DomTable.focus();
        }


}

to set the focus on the calendar table when the datepicker/timepicker popup gets opened.

The interesting part is that if I comment out the debugger in the example above the code focus the calendar in the dropdown (as you can see in the attached video), but if the debugger is commented the Bootstrap modal continues to steal the focus.

My research and conclusion show that the bootstrap modal steals the focus from the elements outside it as shown in my video demonstrating my test https://www.youtube.com/watch?v=3i-4fqzneNU and also noted in these online forums:

With regards to this, the tweak to stop this Boostrap behavior should be made in the Bootstrap source code but not in the Telerik one. This is a default Boostrap popup behavior and so far I am not aware of a working solution to stop it. What you can do as a workaround is to test with the Telerik Modal RadWindow, e.g.

    <style>
        .RadCalendarPopup {
            z-index: 100002 !important;
        }
    </style>
    <telerik:RadWindow RenderMode="Lightweight" ID="modalPopup" runat="server" Width="360px" Height="365px" Modal="true" OffsetElementID="main" OnClientShow="setCustomPosition" Style="z-index: 100001;">
        <ContentTemplate>
            <telerik:RadDatePicker ID="RadDatePicker2" runat="server" EnableAriaSupport="true"
                Calendar-EnableNavigationAnimation="false" ShowAnimation-Duration="0" EnableKeyboardNavigation="true">
                <Calendar Style="z-index: 10000;" runat="server" EnableKeyboardNavigation="true"></Calendar>
            </telerik:RadDatePicker>
        </ContentTemplate>
    </telerik:RadWindow>

Rumen
Telerik team
commented on 25 Aug 2023, 02:37 PM

A note about this point: Tried to catch the calendar opening and manually force focus in JS via the OnPopupOpening event (this doesn't work as the documentation points out this event is just BEFORE the calendar loads into the dom... I need it to be there so I can use jquery's .focus() on it.

You just need to set a timeout and you can grab the calendar popup in the OnPopupOpening event as shown in my example in the previous post.

1 Answer, 1 is accepted

Sort by
1
Accepted
Ben
Top achievements
Rank 1
Iron
answered on 30 Aug 2023, 01:12 PM

The solution I came up with was different, actually. I agree on the source of the problem being bootstrap (I found a lot of articles complaining that bootstrap modals were a pain to work with in general), and that the focus-lock code for modals was highly restrictive.

So I removed it.

Using the datepicker OnPopupOpening and OnPopupClosing events, I was able to fire a small jquery script that removes the focus-locking listeners for the duration of the calendar being open, and then re-enable them after it closes. A sample tidbit:

// These two methods are coupled to telerik datepicker events, and fire when the calendar opens or closes.
// When this happens, the calendar needs to have the ability to focus-trap the keyboard, but if the picker
// happens to be attached to a modal, the bootstrap code steals back focus, causing the calendar to be unable
// to trap the keyboard. As a result, we have to hack this process by manually removing all focus handlers,
// thus making space for the calendar's datepicker. Then, when the calendar closes, we can shove all the existing
// handlers back into the page, allowing the modal to once more operate normally.
// This is ONLY happening in modals, so these events DON'T need to fire if we aren't in a modal (which we handle
// simply by not setting these methods to fire in the server side.)
function OnPopupOpening(sender, args) {
  // collect and store all focusin handlers (which is how bootstrap modals focus-lock the keyboard)
  var focusinDelegates = jQuery._data($(document)[0]).events.focusin;
  for (var i = 0; i < focusinDelegates.length; i++) {
    focusinHandlers.push(focusinDelegates[i].handler);
  }
  // remove all the handlers so that the calendar can focus-lock the keyboard
  $(document).off('focusin');
}
function OnPopupClosing(sender, args) {
  // re-add all the handlers we've removed, since the calendar is closing.
  if (focusinHandlers) {
    for (var i = 0; i < focusinHandlers.length; i++) {
      $(document).on('focusin', focusinHandlers[i]);
    }
  }
  // reset the stored handlers since we don't need them anymore
  focusinHandlers = [];
}

 

And then we simply set the handlers when we construct the controls from the server side:

Private Sub SetModalHandlers(ByVal objDatePicker As RadDatePicker, ByVal blnOnModal As Boolean)
    ' These two delegates will ensure that we can disable/enable focus-locking on the modal when we open
    ' and close the calendar, since bootstrap modal tries to force modal focus-locking over the calendar
    ' control's focus-trapping.
    If blnOnModal Then
        objDatePicker.ClientEvents.OnPopupOpening = "OnPopupOpening"
        objDatePicker.ClientEvents.OnPopupClosing = "OnPopupClosing"
    End If
End Sub



Rumen
Telerik team
commented on 30 Aug 2023, 01:31 PM

This is great news! I am glad that you managed to resolved the problem! I converted your comment to an answer.
Ben
Top achievements
Rank 1
Iron
commented on 30 Aug 2023, 08:30 PM

Ah, thanks. Didn't even consider that until after I'd submitted the comment.
Tags
DatePicker TimePicker
Asked by
Ben
Top achievements
Rank 1
Iron
Answers by
Ben
Top achievements
Rank 1
Iron
Share this question
or