ABP Framework version: v7.3.1
UI type: MVC / Lepton-X 2.3.0
DB provider: EF Core
Tiered (MVC) or Identity Server Separated (Angular): yes
I'm following up on the previously asked question: https://support.abp.io/QA/Questions/4556/Lepton-x-keyboard-support-for-General-Settings
The Lepton-X theme still lacks support for keyboard navigation to the General Settings menu in the MVC project. There have been improvements made to tab navigation to other components, which is great, but it is still not possible to tab to the General Settings and use the enter key to show the options.
This is a high-priority feature for us, as our solution needs to meet accessibility standards. We have developed an accessible theme, but the application fails accessibility checks because users with accessibility requirements can't navigate to the General Settings menu to change to the accessible theme.
Can you provide a technical workaround, is this on your roadmap and what's the estimated release date?
Thanks Jamie
7 Answer(s)
-
0
Hello, sorry for the late reply.
I will create an internal issue on the subject.
I can suggest the following code as a workaround. It's not a workaround that will meet all your needs, but it still works:
1-) Create
menu.js
inwwwroot
folder as follow:$(function () { // Function to toggle the settings menu visibility function toggleSettingsMenu() { const settingsMenu = document.querySelector('[data-lpx-context-menu="settings-context-menu"]'); settingsMenu.classList.toggle('show'); } // Function to handle keyboard events on the settings menu item function handleSettingsMenuItemKeydown(event) { if (event.key === 'Enter') { event.preventDefault(); // Prevent default action for "Enter" key toggleSettingsMenu(); // Toggle the settings menu visibility } } // Function to add keyboard accessibility to the settings menu function addKeyboardAccessibilityToSettingsMenu() { const settingsMenuItem = document.getElementById('lpx-settings'); settingsMenuItem.setAttribute('tabindex', '0'); settingsMenuItem.addEventListener('keydown', handleSettingsMenuItemKeydown); settingsMenuItem.addEventListener('blur', () => { const settingsMenu = document.querySelector('[data-lpx-context-menu="settings-context-menu"]'); settingsMenu.classList.remove('show'); }); // Close the settings menu when focus is lost const settingIcons = document.querySelectorAll('.setting-icon'); settingIcons.forEach(item => { item.setAttribute('tabindex', '0'); item.addEventListener('keydown', handleSubMenuItemKeydown); }); } addKeyboardAccessibilityToSettingsMenu(); function handleMenuItemKeydown(event) { if (event.key === 'Enter') { event.preventDefault(); // Prevent default action for "Enter" key // Toggle the sub-menu visibility const subMenu = event.currentTarget.querySelector('.lpx-inner-menu'); if (subMenu) { subMenu.classList.toggle('show'); if (subMenu.classList.contains('show')) { const subMenuItems = subMenu.querySelectorAll('.lpx-menu-item-link'); subMenuItems.forEach(item => item.setAttribute('tabindex', '0')); } else { const subMenuItems = subMenu.querySelectorAll('.lpx-menu-item-link'); subMenuItems.forEach(item => item.removeAttribute('tabindex')); } } } else if (event.key === 'ArrowDown') { event.preventDefault(); // Prevent default action for "ArrowDown" key // Focus on the next menu item const nextMenuItem = event.currentTarget.nextElementSibling; if (nextMenuItem) { nextMenuItem.querySelector('.lpx-menu-item-link').focus(); } } else if (event.key === 'ArrowUp') { event.preventDefault(); // Prevent default action for "ArrowUp" key // Focus on the previous menu item const prevMenuItem = event.currentTarget.previousElementSibling; if (prevMenuItem) { prevMenuItem.querySelector('.lpx-menu-item-link').focus(); } } } // Function to handle keyboard events on sub-menu items function handleSubMenuItemKeydown(event) { if (event.key === 'Enter') { event.preventDefault(); // Prevent default action for "Enter" key // Perform the desired action for the sub-menu item (you can open sub-menus here) const linkElement = event.target.closest('.lpx-menu-item-link'); if (linkElement) { window.location.href = linkElement.getAttribute('href'); } } else if (event.key === 'ArrowDown') { event.preventDefault(); // Prevent default action for "ArrowDown" key // Focus on the next sub-menu item const nextSubMenuItem = event.currentTarget.nextElementSibling; if (nextSubMenuItem) { nextSubMenuItem.querySelector('.lpx-menu-item-link').focus(); } } else if (event.key === 'ArrowUp') { event.preventDefault(); // Prevent default action for "ArrowUp" key // Focus on the previous sub-menu item const prevSubMenuItem = event.currentTarget.previousElementSibling; if (prevSubMenuItem) { prevSubMenuItem.querySelector('.lpx-menu-item-link').focus(); } } } // Function to add keyboard accessibility to the menu function addKeyboardAccessibilityToMenu() { const menuItems = document.querySelectorAll('.lpx-menu-item'); menuItems.forEach(item => { item.setAttribute('tabindex', '0'); item.addEventListener('keydown', handleMenuItemKeydown); }); const subMenuItems = document.querySelectorAll('.lpx-inner-menu-item'); subMenuItems.forEach(item => { item.setAttribute('tabindex', '-1'); item.addEventListener('keydown', handleSubMenuItemKeydown); }); } // Call the function to enable keyboard accessibility for the menu addKeyboardAccessibilityToMenu(); });
2-) Configure
AbpBundlingOptions
in the module class of your web project as follow:Configure<AbpBundlingOptions>(options => { options.StyleBundles.Configure( LeptonXThemeBundles.Styles.Global, bundle => { ... } ); options.ScriptBundles.Configure( LeptonXThemeBundles.Styles.Global, bundle => { ... bundle.AddFiles("/menu.js"); ... }); });
-
0
Hi
Thank you for the technical solution.
Unfortunately, the menu doesn't behave as expected.
- Can tab to the General settings menu (all options outlined)
- Pressing enter opens the General Settings drop-down menu
- Pressing tab once the menu is showing, tabs through the individual items on the top (not to the drop-down menu)
- It's not possible to tab to the drop-down menu and press enter to change a setting
Can you address these issues?
As an alternative, I thought it would be possible to move the appearance and language menus to the top toolbar like this:
I've got it partially working but have run into some issues due to the javascript for the general settings menu. I don't have much insight into what's happening under the hood, but I'm getting an exception which prevents the user menu from loading.
Uncaught TypeError: Cannot read properties of null (reading 'parentElement') at t.initChildren (lepton-x.bundle.min.js?_v=638261408440000000:2:43721) at new t (lepton-x.bundle.min.js?_v=638261408440000000:2:41474) at t.create (lepton-x.bundle.min.js?_v=638261408440000000:2:41563) at t.createSettingGroupWithMenu (lepton-x.bundle.min.js?_v=638261408440000000:2:86551) at e [as constructor] (lepton-x.bundle.min.js?_v=638261408440000000:2:68837) at new e (lepton-x.bundle.min.js?_v=638261408440000000:2:71613) at e.create (lepton-x.bundle.min.js?_v=638261408440000000:2:71708) at t.createSettingGroups (lepton-x.bundle.min.js?_v=638261408440000000:2:73132) at t.create (lepton-x.bundle.min.js?_v=638261408440000000:2:72833) at NodeList.forEach (<anonymous>)
If the tabbing/enter issue in the general settings cannot be addressed, can you kindly assist in providing a technical solution for implementing two independent components on the toolbar to change the appearance and language?
Thanks
-
0
As an alternative, I thought it would be possible to move the appearance and language menus to the top toolbar like this:
Hello, I have examined your problem in detail, the alternative solution looks good, but I am sorry for the error you have received. We are already aware of the problem and will work to solve it in future versions.
To briefly talk about the problem,
LeptonX
traverses theDOM
elements in order while it is initializing, and if it cannot find an element it expects while it is traversing, it gives an error. So, you may have removed an element in the existing structure, for example, removingGeneral Settings
menu will cause this error. Likewise, removingAppearance
underGeneral Settings
menu or changing its order will cause an error. If you are removing an element from the DOM, instead of removing it, I can recommend you set the visible property to hidden in the style as a workaround for now. -
0
Hello again, you stated that you encountered the following error while customizing:
I've got it partially working but have run into some issues due to the javascript for the general settings menu. I don't have much insight into what's happening under the hood, but I'm getting an exception which prevents the user menu from loading.
Uncaught TypeError: Cannot read properties of null (reading 'parentElement') at t.initChildren (lepton-x.bundle.min.js?_v=638261408440000000:2:43721) at new t (lepton-x.bundle.min.js?_v=638261408440000000:2:41474) at t.create (lepton-x.bundle.min.js?_v=638261408440000000:2:41563) at t.createSettingGroupWithMenu (lepton-x.bundle.min.js?_v=638261408440000000:2:86551) at e [as constructor] (lepton-x.bundle.min.js?_v=638261408440000000:2:68837) at new e (lepton-x.bundle.min.js?_v=638261408440000000:2:71613) at e.create (lepton-x.bundle.min.js?_v=638261408440000000:2:71708) at t.createSettingGroups (lepton-x.bundle.min.js?_v=638261408440000000:2:73132) at t.create (lepton-x.bundle.min.js?_v=638261408440000000:2:72833) at NodeList.forEach ()
We have intuitive guesses as to where this error might be, but we want to reproduce the error just to be sure. Could you please tell us the steps by which we can reproduce the error?
-
0
can you pls write the recent status of this problem
-
0
Hi
The supplied javascript did not work (as described earlier).
The approach I tried using separate components for the appearance and language on the LeptonX\Top Menu\Main Header resolved the keyboard issues but resulted in an exception in the browser that prevented the User Menu from functioning.
lepton-x.bundle.min.js?_v=638270199920000000:2 Uncaught TypeError: Cannot read properties of null (reading 'parentElement')
at t.initChildren (lepton-x.bundle.min.js?_v=638270199920000000:2:43721)
at new t (lepton-x.bundle.min.js?_v=638270199920000000:2:41474)
at t.create (lepton-x.bundle.min.js?_v=638270199920000000:2:41563)
at t.createSettingGroupWithMenu (lepton-x.bundle.min.js?_v=638270199920000000:2:86551)
at e [as constructor] (lepton-x.bundle.min.js?_v=638270199920000000:2:68837)
at new e (lepton-x.bundle.min.js?_v=638270199920000000:2:71613)
at e.create (lepton-x.bundle.min.js?_v=638270199920000000:2:71708)
at t.createSettingGroups (lepton-x.bundle.min.js?_v=638270199920000000:2:73132)
at t.create (lepton-x.bundle.min.js?_v=638270199920000000:2:72833)
at NodeList.forEach (<anonymous>)
I was unable to resolve the issues and have reverted to the framework code for the menus and we have flagged this as a framework issue to our clients.
Here is the approach I took that you can use to recreate the exception ( using 7.3.2) in the Web.Public MVC project:
- Create a new application with abp suite
- Create a new Toolbar Appearance Component (in Components\Toolbar\Appearance)
- Update the Main Header to invoke the Appearance component (in LeptonX\Components\Top Menu\Main Menu)
- Comment out the General Settings cshtml code (in LeptonX\Components\Top Menu\Main Menu)
Toolbar Appearance Component (...\Components\Toolbar\Appearance\AppearanceViewComponent.cs):
public class AppearanceViewComponent : AbpViewComponent { public virtual IViewComponentResult Invoke() { return View("~/Components/Toolbar/Appearance/Default.cshtml"); } }
\Components\Toolbar\Appearance\Default.cshtml:
@using Microsoft.AspNetCore.Mvc.Localization @using Acs.Cts.Portal.Localization @using Microsoft.Extensions.Localization; @using Microsoft.Extensions.Options @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX @using Volo.Abp.LeptonX.Shared; @using Volo.Abp.Users @inject IOptions<LeptonXThemeOptions> Options @inject IStringLocalizerFactory LocalizerFactory @inject IHtmlLocalizer<PortalResource> L @inject ICurrentUser CurrentUser <li class="outer-menu-item" id="lpx-settings"> <div class="dropdown"> <a href="#" class="lpx-menu-item-link lpx-menu-item" role="button" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-lpx-setting-group="appearance"> <span class="lpx-menu-item-icon" data-lpx-setting-icon="appearance" data-lpx-setting-id="settings-context-menu"> <i class="lpx-icon bi bi-palette-fill" aria-hidden="true"></i> </span> <span class="lpx-menu-item-text">@L["Appearance"]</span> </a> <div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuLink" data-lpx-context-menu="settings-context-menu"> <ul class="lpx-nav-menu lpx-inner-menu hidden-in-hover-trigger collapsed" style="align-items:start; display: block;" data-id="appearance"> @foreach (var item in Options.Value.Styles) { <li class="lpx-inner-menu-item"> <a href="javascript:void(0)" class="lpx-menu-item-link lpx-menu-item" data-lpx-setting="@item.Key"> <span class="lpx-menu-item-icon"> <i class="lpx-icon @item.Value.Icon" aria-hidden="true"></i> </span> <span class="lpx-menu-item-text hidden-in-hover-trigger">@item.Value.DisplayName.Localize(LocalizerFactory).Value</span> </a> </li> } </ul> </div> </div> </li>
<br> Main Header (\Themes\LeptonX\Components\TopMenu\MainHeader\Default.cshtml):
@using Acs.Cts.Portal.Web.Components.Toolbar.Appearance; @using Acs.Cts.Portal.Web.Components.Toolbar.Language; @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.Common.MainHeaderBranding @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.TopMenu.MainHeaderToolbar @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.TopMenu.MainMenu <header> <div class="lpx-header-top"> @await Component.InvokeAsync(typeof(MainHeaderBrandingViewComponent)) <nav class="lpx-nav me-auto"> <ul class="lpx-nav-menu"> @await Component.InvokeAsync(typeof(AppearanceViewComponent)) </ul> </nav> @await Component.InvokeAsync(typeof(MainHeaderToolbarViewComponent)) </div> <div class="lpx-header-bottom"> <nav class="lpx-nav"> <ul class="lpx-nav-menu"> @await Component.InvokeAsync(typeof(MainMenuViewComponent)) </ul> </nav> </div> </header>
General Settings (...\Themes\LeptonX\Components\Common\GeneralSettings\Default.cshtml): @using Microsoft.Extensions.Localization @using Microsoft.Extensions.Options @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX @using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Themes.LeptonX.Components.SideMenu.Toolbar.LanguageSwitch @using Volo.Abp.LeptonX.Shared @using Volo.Abp.LeptonX.Shared.Localization @inject ThemeLanguageInfoProvider ThemeLanguageInfoProvider @inject IOptions<LeptonXThemeOptions> Options @inject IOptions<LeptonXThemeMvcOptions> MvcOptions @inject IStringLocalizer<LeptonXResource> L @inject IStringLocalizerFactory LocalizerFactory @{ var languageModel = await ThemeLanguageInfoProvider.GetLanguageSwitchViewComponentModel(); } @*<div class="lpx-settings" id="lpx-settings"> COMMENTED OUT </div>*@ Hope that helps. Jamie
<br>
-
0
Hello, first of all, thank you very much for the information you have provided. I have conveyed to my related teammates how they can reproduce the problem.
Thank you again for your understanding.