Jemt / Fit.UI

Fit.UI is a JavaScript based UI framework built on Object Oriented principles
http://fitui.org
GNU Lesser General Public License v3.0
19 stars 7 forks source link

DropDown: Open + drag scrollbar + select item = OnBlur fired #164

Closed FlowIT-JIT closed 2 years ago

FlowIT-JIT commented 2 years ago

Consider the following example (code also attached below): https://jsfiddle.net/34q5n7xm/8/ EDIT: JSFiddle no longer triggers the bug since it runs on the most recent (and now fixed) version of Fit.UI! To reproduce the bug, Fit.UI version 2.11.7 or earlier is required!

1) Open the dropdown control 2) Scroll the content by dragging the scrollbar (do not use scroll wheel!) 3) Select an item

Notice how OnBlur fires - this is not supposed to happen!

<div id="Focused" style="background: yellow; height: 40px;">Focused element</div>
<div id="Box" style="background: orange; height: 300px;" tabindex="2" ondblclick="this.innerHTML = '';">Log messages</div>

<div id="Value"><br><b>Users selected</b>:</div><br>
<div id="Control"></div>
body
{
    font-family: verdana;
    font-size: 14px;
    color: #333;
}

div#Focused,
div#Box
{
    font-size: 11px;
    overflow: hidden;
    white-space: nowrap;
    padding: 3px;
}
Fit.Events.OnReady(function()
{
    // Create Drop Down

    dd = new Fit.Controls.WSDropDown("WSDropDown1");
    dd.Url("https://fitui.org/demo/GetUsers.php");
    dd.JsonpCallback("JsonpCallback"); // Loading data from foreign domain
    dd.MultiSelectionMode(true);
    dd.Width(400);
    dd.DropDownMaxHeight(150);
    dd.InputEnabled(true);
    dd.OnRequest(function(sender, eventArgs)
    {
        eventArgs.Request.SetParameter("NoCache", Fit.Data.CreateGuid());
        eventArgs.Request.SetParameter("Search", dd.GetInputValue());
    });
    dd.OnChange(function(sender)
    {
        var val = document.querySelector("#Value");
        val.innerHTML = "<br><b>Users selected</b>:";

        Fit.Array.ForEach(dd.GetSelections(), function(sel)
        {
            val.innerHTML += "<br>" + sel.Title + " (" + sel.Value + ")";
        });
    });
    dd.Render(document.querySelector("#Control"));

    // Monitor focused element

    setInterval(function()
    {
        var html = Fit.Dom.GetFocused().outerHTML;
        var r =  /^<.*?>/g;
        html = r.exec(html)[0];

        document.querySelector("#Focused").innerHTML = Fit.String.EncodeHtml(html);
    }, 200);

    // Register FocusIn and FocusOut handlers

    window.elementToString = function(elm)
    {
        if (!elm) return elm + "";
        return Fit.String.EncodeHtml(elm.outerHTML.substring(0, 100));
    }

    window.log = function(type)
    {
        var box = document.querySelector("#Box");
        box.innerHTML = Fit.Date.Format(new Date(), "hh:mm:ss") + " " + type + "<br>" + box.innerHTML;
    }

    Fit.Events.AddHandler(dd.GetDomElement(), "focus", true, function(e) {
        log("Focus in");
        //log("Focus in<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>target</b> " + elementToString(e.target) + "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>related</b> " + elementToString(e.relatedTarget));
    });
    Fit.Events.AddHandler(dd.GetDomElement(), "blur", true, function(e) {
        log("Focus out");
        //log("Focus out<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>target</b> " + elementToString(e.target) + "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>related</b> " + elementToString(e.relatedTarget));
    });

    dd.OnFocus(function() { log("<b>OnFocus fired</b>") });
    dd.OnBlur(function() { log("<b>OnBlur fired</b>") });

    dd.GetTreeView().ExpandAll(3);
});
FlowIT-JIT commented 2 years ago

This bug is related to two issues:

First issue: https://github.com/Jemt/Fit.UI/blob/780b41ff958cff45d8e8f41f39bb0612f6799e2d/Controls/TreeView/TreeView.js#L374 image This code will cause focus to be moved back to DropDown's input field when an item is selected using click/touch in the picker control (TreeView). It was only intended to invoke FireOnFocusIn if the control was to lose focus to an outer container with tabIndex set. The browser will move focus to an element further up the DOM hierarchy when interacting with the scrollbar, if the parent is focusable. But due to the bug on line 391, FireOnFocusIn is always invoked. But we also invoke FireOnFocusIn when TreeView gains focus in general, which happens when clicking an item: https://github.com/Jemt/Fit.UI/blob/780b41ff958cff45d8e8f41f39bb0612f6799e2d/Controls/TreeView/TreeView.js#L1282 image So FireOnFocusIn gets invoked twice. This in turn results in onFocusIn and onFocusOut in ControlBase being invoked like in/out/out/in/in, rather than in/out/in/out/in as expected, and this results in two blur timers being scheduled to execute in onFocusOut, which is exactly what is causing the problem. When onFocusIn fires, it cancels any onBlur timer set. But if onFocusOut fires multiple times, prior onBlur timers are not canceled: https://github.com/Jemt/Fit.UI/blob/780b41ff958cff45d8e8f41f39bb0612f6799e2d/Core/ControlBase.js#L1134 image

Second issue: The second issue is touched upon in the last code reference above - the onFocusIn and OnFocusOut handlers in ControlBase. They only work as expected if they are fired in turn - like in/out/in/out rather than in/out/out/in/in. The following example demonstrates the problem using pure JavaScript: https://codepen.io/JemtDK/pen/abqRqNy?editors=0010 Can we make it more robust by canceling the onBlur timer in the onFocusOut handler, if it is already scheduled for execution?

FlowIT-JIT commented 2 years ago

Both issues mentioned have now been fixed.