luci-base: ui.js: rework dropdown focus behavior
authorJo-Philipp Wich <jo@mein.io>
Sun, 18 Feb 2024 22:25:55 +0000 (23:25 +0100)
committerPaul Donald <newtwen@gmail.com>
Thu, 7 Mar 2024 00:27:58 +0000 (01:27 +0100)
Drop mouse following focus behavior as it interferes with custom value inputs,
rely on CSS based hover effects instead.

Also slightly improve keyboard navigation by auto-focussing the custom value
input when entering the last dropdown choice via arrow down or tab key, and
by allowing to leave the text input again with the arrow up key.

Fixes: #6903
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
(cherry picked from commit 3980c192cda6985c2e2d2608e74677cf236005db)

modules/luci-base/htdocs/luci-static/resources/ui.js

index d7dcabe76a16cf9d7c481dff2064e33e58e324bb..c62df429db5652c6eb40a007c6313eb2b643f92f 100644 (file)
@@ -1187,8 +1187,6 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        window.addEventListener('touchstart', this.closeAllDropdowns);
                }
                else {
-                       sb.addEventListener('mouseover', this.handleMouseover.bind(this));
-                       sb.addEventListener('mouseout', this.handleMouseout.bind(this));
                        sb.addEventListener('focus', this.handleFocus.bind(this));
 
                        canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
@@ -1340,7 +1338,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                sb.insertBefore(pv, ul.nextElementSibling);
 
                li.forEach(function(l) {
-                       l.setAttribute('tabindex', 0);
+                       if (!l.hasAttribute('unselectable'))
+                               l.setAttribute('tabindex', 0);
                });
 
                sb.lastElementChild.setAttribute('tabindex', 0);
@@ -1581,20 +1580,6 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                elem.focus();
        },
 
-       /** @private */
-       handleMouseout: function(ev) {
-               var sb = ev.currentTarget;
-
-               if (!sb.hasAttribute('open'))
-                       return;
-
-               sb.querySelectorAll('.focus').forEach(function(e) {
-                       e.classList.remove('focus');
-               });
-
-               sb.querySelector('ul.dropdown').focus();
-       },
-
        /** @private */
        createChoiceElement: function(sb, value, label) {
                var tpl = sb.querySelector(this.options.create_template),
@@ -1826,7 +1811,12 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
 
                        case 40:
                                if (active && active.nextElementSibling) {
-                                       this.setFocus(sb, active.nextElementSibling);
+                                       var li = active.nextElementSibling;
+                                       this.setFocus(sb, li);
+                                       if (this.options.create && li == li.parentNode.lastElementChild) {
+                                               var input = li.querySelector('input');
+                                               if (input) input.focus();
+                                       }
                                        ev.preventDefault();
                                }
                                else if (document.activeElement === ul) {
@@ -1857,19 +1847,6 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                this.closeDropdown(sb, true);
        },
 
-       /** @private */
-       handleMouseover: function(ev) {
-               var sb = ev.currentTarget;
-
-               if (!sb.hasAttribute('open'))
-                       return;
-
-               var li = findParent(ev.target, 'li');
-
-               if (li && li.parentNode.classList.contains('dropdown'))
-                       this.setFocus(sb, li);
-       },
-
        /** @private */
        handleFocus: function(ev) {
                var sb = ev.currentTarget;
@@ -1888,7 +1865,8 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
        /** @private */
        handleCreateKeydown: function(ev) {
                var input = ev.currentTarget,
-                   sb = findParent(input, '.cbi-dropdown');
+                   li = findParent(input, 'li'),
+                   sb = findParent(li, '.cbi-dropdown');
 
                switch (ev.keyCode) {
                case 13:
@@ -1901,19 +1879,28 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
                        input.value = '';
                        input.blur();
                        break;
+
+               case 38:
+                       if (li.previousElementSibling) {
+                               this.handleCreateBlur(ev);
+                               this.setFocus(sb, li.previousElementSibling, true);
+                       }
+                       break;
                }
        },
 
        /** @private */
        handleCreateFocus: function(ev) {
                var input = ev.currentTarget,
-                   cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
+                   li = findParent(input, 'li'),
+                   cbox = li.querySelector('input[type="checkbox"]'),
                    sb = findParent(input, '.cbi-dropdown');
 
                if (cbox)
                        cbox.checked = true;
 
                sb.setAttribute('locked-in', '');
+               this.setFocus(sb, li, true);
        },
 
        /** @private */