processwire-issues icon indicating copy to clipboard operation
processwire-issues copied to clipboard

Admin flyout menus are not touch-friendly

Open Toutouwai opened this issue 3 years ago • 6 comments

Short description of the issue

The admin flyout menus (those generated by a Process module's executeNavJSON() method) are not touch-friendly. So for example, in the "Add New" menu...

2021-08-22_160811

...none of the submenu items such as "Seed collection" are accessible on a touch device.

Instead you are navigated to the "Add New" page.

2021-08-22_160958

At least the same items that are in the flyout menu can still be accessed here, but it's not as nice because it requires an extra page load. And although it's common for a Process module to reproduce the items from its executeNavJSON() method separately on a page there's no strict requirement to do that, so there's the potential for some menu items to be totally unavailable on a touch device.

Incidentally, I would have liked to work around this problem by using the first level of dropdowns in a custom Process module...

2021-08-22_161832

...but the frustrating thing is that you can't set these submenu items dynamically within a Process module. So a bump for my 2018 request for that.

Optional: Suggestion for a possible fix

Could a touch on the caret icon be used to trigger the loading of the flyout menu so these menus are available on devices with no hover state?

2021-08-22_162214

Setup/Environment

  • ProcessWire version: 3.0.183

Toutouwai avatar Aug 22 '21 04:08 Toutouwai

@Toutouwai Sounds like a worthwhile feature to me. But I'm not sure how to support that at present. I'm assuming this would need a computer that has a touch screen, since mobile devices use the offcanvas navigation instead. I don't currently have access to any touch devices that would use this navigation. I thought the Chrome emulator might help simulate it, but it doesn't seem to, even with force touch enabled.

Regarding dynamic submenu items, that is reserved for the ajax-loaded dropdown nav within a particular module's menu items. That way ProcessWire doesn't have to load every module and ask it for menu items on every single page load, and it can cache the whole navigation tree (other than the ajax loaded things that are nested enough so as not to be regularly loaded unintentionally during navigation hovers, etc.).

ryancramerdesign avatar Aug 24 '21 11:08 ryancramerdesign

It looks like wire/templates-admin/scripts/main.js already has some code that supposed to solve this problem for touch devices. https://github.com/processwire/processwire/blob/e6e08ad3fb6a83ed0569bd3c0059facd09a3861a/wire/templates-admin/scripts/main.js#L394-L423 It's not working though, so I had a go at a fix. I made the changes below and it seems to be working well for me here on both iOS (late model iPad Pro) and Android (oldish tablet). I'll attach my modified main.js but also describe the changes below.

1. I removed the code relating to the touchClick() function. https://github.com/processwire/processwire/blob/e6e08ad3fb6a83ed0569bd3c0059facd09a3861a/wire/templates-admin/scripts/main.js#L392-L423 https://github.com/processwire/processwire/blob/e6e08ad3fb6a83ed0569bd3c0059facd09a3861a/wire/templates-admin/scripts/main.js#L428 https://github.com/processwire/processwire/blob/e6e08ad3fb6a83ed0569bd3c0059facd09a3861a/wire/templates-admin/scripts/main.js#L263-L265

2. I removed the code that stops event propagation when links are clicked. https://github.com/processwire/processwire/blob/e6e08ad3fb6a83ed0569bd3c0059facd09a3861a/wire/templates-admin/scripts/main.js#L431-L433 Not sure why that was there - everything seems to work fine without it.

3. I removed a line that causes submenus to be hidden whenever a link is clicked (a problem on a touch device where we want the first touch to trigger a hover state). https://github.com/processwire/processwire/blob/e6e08ad3fb6a83ed0569bd3c0059facd09a3861a/wire/templates-admin/scripts/main.js#L151 I don't understand the meaning of the code comment "hide nav when an item is selected to avoid the whole nav getting selected". But again, everything seems to work fine with that line removed.

4. I added new code to handle touch devices.

if($('body').hasClass('touch-device')) {
    $(document).on('click', 'a.pw-dropdown-toggle, a.pw-has-items', function() {
        $('a.first-touch').not(this).removeClass('first-touch');
        if(!$(this).hasClass('first-touch')) {
            $(this).addClass('first-touch');
            return false;
        }
    });
}

So the idea is that the first touch just adds a "first-touch" class and doesn't navigate to the link href or do anything else. But the second touch follows the link as normal.

5. But something else is needed for iOS, because it's smarter than Android in that when it detects that a link has a hover state that changes the CSS visibility of other items it doesn't trigger the click event on the first touch. In other words, the top menu dropdowns already work properly in iOS without needing any JS. But because we need JS code to handle Android it has the effect that in iOS the top menu requires three clicks to actually navigate to the link. To solve that I add the first-touch class on the touchstart event, with a delay to ensure that it doesn't add it before the click event has fired. So that block of code mentioned in 4 now looks like this:

if($('body').hasClass('touch-device')) {
    // Ensure that iOS adds first-touch class on first touch
    $(document).on('touchstart', 'a.pw-dropdown-toggle', function() {
        var $t = $(this);
        // Seems to need a fairly long delay to avoid Android adding this class before the click event
        setTimeout(function() {
            $t.addClass('first-touch');
        }, 500);
    });

    $(document).on('click', 'a.pw-dropdown-toggle, a.pw-has-items', function() {
        $('a.first-touch').not(this).removeClass('first-touch');
        if(!$(this).hasClass('first-touch')) {
            $(this).addClass('first-touch');
            return false;
        }
    });
}

6. Whether or not the user is working on a touch device is determined in ProcessLogin, using What Input. But I've found that this one-off determination isn't always reliable, especially when using a password manager such as LastPass which will automatically log you in. This seems to happen without any touch events being fired and so What Input can't do its magic.

Rather than only having this one-off test for touch devices, I think it would be good if PW loaded What Input throughout the admin (it's a very lightweight script). Then for this menu code, instead of activating according to the "touch-device" class on the body it could activate according to the "whatintent" data attribute on the html tag.

So actually the code in 5 would be:

// Ensure that iOS adds first-touch class on first touch
$(document).on('touchstart', 'a.pw-dropdown-toggle', function() {
    if($('html').data('whatintent') !== 'touch') return;
    var $t = $(this);
    // Seems to need a fairly long delay to avoid Android adding this class before the click event
    setTimeout(function() {
        $t.addClass('first-touch');
    }, 500);
});

// Menu clicks on touch devices
$(document).on('click', 'a.pw-dropdown-toggle, a.pw-has-items', function() {
    if($('html').data('whatintent') !== 'touch') return;
    $('a.first-touch').not(this).removeClass('first-touch');
    if(!$(this).hasClass('first-touch')) {
        $(this).addClass('first-touch');
        return false;
    }
});

And it would be good to update to the latest What Intent v5 too.

Thanks.

main.zip

Toutouwai avatar Aug 26 '21 01:08 Toutouwai

Regarding dynamic submenu items, that is reserved for the ajax-loaded dropdown nav within a particular module's menu items. That way ProcessWire doesn't have to load every module and ask it for menu items on every single page load, and it can cache the whole navigation tree (other than the ajax loaded things that are nested enough so as not to be regularly loaded unintentionally during navigation hovers, etc.).

@ryancramerdesign, in another related issue I tested by bypassing the caching the first level of dropdowns from the main menu and it seems that the caching has negligible performance benefit: https://github.com/processwire/processwire-requests/issues/268#issuecomment-464200684

Regarding needing to get the first level of dropdowns from modules on every page load, there are only a few core modules that even use that hard-coded first level: ProcessModule, ProcessRecentPages and ProcessCommentsManager. And would there be some way so that a module can either hard-code the submenu, or indicate that it has a dedicated method to dynamically return submenu items? That way there could be the best of both worlds.

Toutouwai avatar Aug 26 '21 01:08 Toutouwai

@Toutouwai Wow, thanks for doing all this work in figuring this out! This looks great. I will plan to go through this in more detail next week. I'm also going to be able to borrow a computer with a touchscreen next week (Chromebook), which will help me test.

ryancramerdesign avatar Aug 26 '21 10:08 ryancramerdesign

...some way so that a module can either hard-code the submenu, or indicate that it has a dedicated method to dynamically return submenu items? That way there could be the best of both worlds.

I'd be open to that. Could work kind of like the autoload moduleInfo option, which also accepts a callable. So $moduleInfo['nav'] could return a callable rather than an array. Since module info is cached, as is the nav, it's not as simple as it sounds, but I think it'll be doable.

ryancramerdesign avatar Aug 26 '21 11:08 ryancramerdesign

Any news on this one?

BernhardBaumrock avatar Feb 26 '22 14:02 BernhardBaumrock