anklang icon indicating copy to clipboard operation
anklang copied to clipboard

Focus, shadow DOM, submenus, buttons and input elements

Open tim-janik opened this issue 4 months ago • 0 comments

Currently we have a number of focus related problems:

  • BUG: Clicking on menu button and then outside a popup menu causes the menu button to get stuck in pressed state. Also a pointer grab stays active so the UI becomes unusable. Pressing ESCAPE resolves the state. But from then on, the popup menu stays hidden and cannot never be shown again.
  • BLOCKER: The focus handling complexity is one major reason we do not support submenus yet, this important for plugin selection and some other cases.
  • MINOR: Arrow keys currently do not cycle through menus across start/end.
  • MINOR: Using tab inside a focus trap like a modal dialog escapes into the browser chrome.

Fixing focus/keyboard handling in the web platform is notoriously hard: Managing focus in the shadow DOM Dialogs and shadow DOM We have a lot of complex code in place for focus handling, but don't always catch all cases. The problems with a stuck grab may be related to the move to <dialog/> instead of our own focus/modal shield element, but this can also be caused by the menu items containing a focusable <button/> element.

Issus related to shadow DOM are manyfold.

  • Elements inside shaodow DOM cause issues in case they are focusable. Normally, shadow DOM is not an issue when <slot/> re-exposes child elements of a custom component to the light DOM tree. Instead of stashing focusable child elements into the shadowDOM, the component should simply derive from the focusable HTMLElement subclass to be focusable/editable (it just cannot be LitElement that way).
  • Handling element traversal (querySelector*) does not go into shadow DOMs, making any logic (like controlling the focus chain) that needs deep DOM inspection much more complicated.
  • CSS styling and visibility logic based on :focus, :focus-visible, :focus-within will hardly work as intended with focusable elements stashed away in shadow DOMs.
  • Controlling the focus order (like menu & submenu navigation via arrow keys need it) is hard to get right when considering (open) shadow DOM, inert, hidden elements, tabindex, custom components where focusable may depend on derivation from a specific HTMLElement subclass and potential future elements for which we don't know if they are focusable.
  • For closed shadow DOMs, it is impossible to control the focus chain.

Here is what we can do:

  • Keep all focusable elements out of shadow DOMs. Our LitComponent class could enforce this in __DEV__ mode by peeking inside the shadow DOMs created and ignoring external children that are assigned via <slot> elements. That means keeping all focusable elements in the light DOM, and in some cases where we have custom focusable components, these need to be implemented without Lit (since LitElement always derives from HTMLElement instead of any of its focusable subclasses).
  • Analysis; currently we have shadow DOMs for:
    • contextmenu.js - OK, has dialog in shadow DOM
    • menutitle.js - OK, but could be simplified to not use shadow DOM
    • editable.js - BAD, uses focussable input
    • menuitem.js - BAD, the major offender with <button/> inside the shadow DOM.
  • Port editable to derive from HTMLInputElement directly, getting rid of its LitElement heritage. (Good start, should be easier than menuitem)
  • Port menuitem to derive from HTMLButtonElement directly, getting rid of its LitElement heritage.
  • Leave visibility handling to CSS, i.e. we could display (via CSS) menus and submenus only if they have a focus child somewhere in the ancestry.
  • Implementing context menus can be a bit tricky via CSS visibility handling, as the context menu may need to be moved between multiple elements that can show the same context menu.
  • Just implement arrow key focussing, based on reducing our current focus logic to just the two functions suggested in the above article series: element.getNextTabbableElement() element.getPreviousTabbableElement()

tim-janik avatar Feb 29 '24 23:02 tim-janik