Sveltekit + Preline modal tab order is incorrect
Summary
When using a modal in a Preline + SvelteKit + TailwindCSS project set up as instructed by documentation, modal tab order is incorrect.
Steps to Reproduce
- Create new Sveltekit + Tailwind project per https://tailwindcss.com/docs/guides/sveltekit
- Install Preline with SvelteKit configurations per https://preline.co/docs/frameworks-svelte.html
- Install overlay and forms
- Create basic modal per https://preline.co/docs/modal.html
- Create basic input per https://preline.co/docs/input.html (with or without tabindex="0")
The example has been provided with focusable elements, however taborder is incorrect even without them.
If there are no focusable elements in the modal, then tab will only move to the Close button, with no ability to tab to the Close icon.
I believe the problem has something to do with these lines from preline.js:
onTab:
e.onTab = function(t, e) { if (!e.length) return !1; var n = t.element.overlay.querySelector(":focus") , o = Array.from(e).indexOf(n); o > -1 ? e[(o + 1) % e.length].focus() : e[0].focus() }
autoInit:
(e.autoInit = function () { window.$hsOverlayCollection || (window.$hsOverlayCollection = []), document .querySelectorAll('[data-hs-overlay]:not(.--prevent-on-load-init)') .forEach(function (t) { window.$hsOverlayCollection.find(function (e) { var n; return ( (null === (n = null == e ? void 0 : e.element) || void 0 === n ? void 0 : n.el) === t ); }) || new e(t); }), window.$hsOverlayCollection && document.addEventListener('keydown', function (t) { return e.accessibility(t); }); }),
In my tests, this function runs successfully initially for the element with the :focus pseudo-class and the correct index is returned, however, the document keydown event is initiated three times:
- From preline itself
- From the 'goto' event from afterNavigate
afterNavigate((result)=>console.log(result.type) - From the 'enter' event from afterNavigate
afterNavigate((result)=>console.log(result.type)
Removing the call to window.HSStaticMethods.autoInit(); in afterNavigate results in modals not working, and a call to either breaks the tab order again, so I haven't been able to develop a good workaround (short of removing all focusable elements in a modal besides the action buttons, which isn't an acceptable solution).
Please let me know if there is any other info I can provide to help nail this down!
Demo Link
https://stackblitz.com/~/github.com/fortserious/sveltekit-preline-modal-accessibility-bug
Expected Behavior
Tabbing through inputs proceeds in DOM order: Close (Icon), Input 1, Input 2, Close, Save Changes.
Actual Behavior
Tabbing through inputs proceeds in unusual order: Input 2, Close (Icon), Close (button), Input 1, Save Changes.
Screenshots
No response
I can confirm this issue also exists in Vue.js. The Tab key order is reversed within the forms shown in modals. The problem is on this method as @fortserious has pointed already: https://github.com/htmlstreamofficial/preline/blob/0c521a3c454f2ce345cad137473b77a89ed50a33/src/plugins/overlay/index.ts#L496
I can also confirm that this issue occurs with plain JavaScript when using htmx to load HTML from the server.
I don't believe the tab order is reversed. Instead, it seems that the window.HSStaticMethods.autoInit(); function is called on page load, and because we're dynamically loading additional HTML into the DOM later, we invoke window.HSStaticMethods.autoInit(); again.
https://github.com/htmlstreamofficial/preline/blob/c8f941a113556fb3e13a7c296be012f89c171c35/src/plugins/overlay/index.ts#L638C1-L644C4
If you examine the autoInit() function for the overlay plugin, you'll notice that it registers an event listener.
https://github.com/htmlstreamofficial/preline/blob/c8f941a113556fb3e13a7c296be012f89c171c35/src/plugins/overlay/index.ts#L400C3-L404C4
Since this function runs multiple times, the event listener is registered multiple times. This causes the onTab function to execute multiple times, depending on how often window.HSStaticMethods.autoInit(); is called.
As a result, when I press the Tab key, the function runs twice, causing it to skip over other inputs.
The Fix:
I modified the code by moving the event listener into its own function and removing the listener before adding it again. This resolved the issue for me.
....
if (window.$hsOverlayCollection) {
document.removeEventListener('keydown', onKeyDown)
document.addEventListener('keydown', onKeyDown);
}
}
...
const onKeyDown = (evt: KeyboardEvent) => {
HSOverlay.accessibility(evt)
}
...
I'm considering creating a pull request to fix this bug and two others. It would be great if you could verify whether this fix also resolves the issue in Svelte and Vue.js.
I have included the dist Preline folder as an attachment if you'd like to try my changes. It also contains fixes for two other bugs: allowing the use of Shift+Tab to navigate to the previous input, and preventing tabindex="-1" elements in overlays from being tab targets. Additionally, there are some console.log statements that I still need to remove.
@fortserious Hi, Thank you for your feedback. We will fix this behavior in the next update and inform you here.
Hey @olegpix
I believe I'm experiencing this issue using Vue. I have a Preline modal component and am finding that the tabbing skips a component inside of the modal. This seems identical to what @RobertHalfdanar has described. Just curious to know when a fix is due. Thanks.
@UnlockQA Hi, The update will be out soon, hopefully this week.
I'm considering creating a pull request to fix this bug and two others.
@RobertHalfdanar My attempt at fixing the underlying issue has been open for a while https://github.com/htmlstreamofficial/preline/pull/441. I did not observe wrong tab order or similar (but never really tested it either), only sluggishness, but I'm pretty sure it's basically all the same issue.
Hey everyone, the issue has been resolved in the latest v2.6.0 release. Thanks!