Putting the focus on a specific element in a modal dialog

Hi all. I’ve been trying to figure out how to put the focus on a specific element when I open a modal dialog using uxpShowModal.

I’ve tried focus and autofocus attributes on the element and calling focus() on it either before calling uxpShowModal or in an event handler for the focus event on the dialog itself. I can’t find any evidence of there being a ‘show’ event as there is on panels.

I don’t see any element having focus when the dialog is opened.

Has anyone got this to work?

Testing in Photoshop 25.5 on Windows 11.

The only way I managed to make it work:

    const focusBySelector = (dialog: HTMLDialogElement, selector: string) => {
        dialog.querySelector<HTMLInputElement>(selector)?.focus()
    }

    document.body.appendChild(dialogElement);

    setTimeout(() => {
        focusBySelector(dialogElement, ".autofocus")
    }, 150)

    // @ts-ignore
    const response = await dialogElement.uxpShowModal(dialogOpts)
    dialogElement.remove()

You’ll need to clean up TS if not needed
But this isn’t really reliable depending on your dialog contents. In my case 150ms is enough to render all inputs, so it’s already there when it tries to focus

Similar to Karmalakas code. I call focus() of the target element in useRef.

The following code may not work as it is a simplified model, but this is how I achieve it.

// If using React, replace import statement with it
import { h } from 'preact' ;
import { useRef, useEffect } from 'preact/hooks' ;

export const App = (props) => {
  const focusRef = useRef(null) ;

  // Execute when first called
  useEffect(() => {
    // If the timing is too early, focus() will be ignored.
    // Waiting a little before executing can increase the success rate, but it is not perfect.
    setTimeout(() => {focusRef.current.focus() ;}, 300) ;
  }, []) ;

  return (
    <div>
      <sp-textfield
        ref={focusRef}
        value={"0"}
      ></sp-textfield>
    </div>
  ) ;
} ;
1 Like

You don’t need to use timeout. Instead you can add listener to “load” event in dialog element. It will trigger focus exactly when dialog is fully loaded so it can be focused.

1 Like

AFAIR it didn’t work for me at the time. Load event was trigerred before actual elements were rendered. Maybe it’s fixed now :thinking:

Thanks all. I’m finding that load is called slightly too early, but might give a better base for a timeout from than the original uxpShowModal? This works nicely for me:

      saveDialog.addEventListener('load', evt => {
        setTimeout(() => {
          document.getElementById('saveName').focus();
        }, 150)
      });
      
      const res = await saveDialog.uxpShowModal({   ...   });

Now to apply in all the other places that were bugging me!

I cannot tell for 100% it works every single time. I tried it only recently in one PS version. I tried it like 15 times and it always worked.

Also maybe “loaded” and “visible on the screen” could be two different things. I don’t know… it could also mean having everything ready for renderer.

how about the button element does button element has its own “load” event?

I just tried an event handler for load on the button, but it doesn’t seem to do anything, even with a 500ms delay.
A 150ms delay on load for the dialog is not always enough, but I’ve not seen any failures with 250ms. The downside of that is that you can actually see the dialog draw and then the focus highlighting being applies later. But that’s pretty minor, I think.
I’ve also tried adding two timeouts, one at 150ms and the second at 250ms, which looks a little smoother, but feels distinctly messy!

You could have 10ms interval to set focus and clear it once focus is set (or introduce endless loop if it is never set)
}:-D

Interesting, yes. I’ve just tried with this, which seems to work quite nicely:

const focusList = {};
function setFocusOnLoad(dlg, elem) {
  if (!focusList.hasOwnProperty(dlg.id)) {
    focusList[dlg.id] = 1;
    dlg.addEventListener('load', evt => {
      const startTime = Date.now();
      const focusInterval = setInterval(tryFocus, 20);
      function tryFocus(){
        elem.focus();
        const elapsed = Date.now() - startTime;
        if (elapsed > 500 || document.activeElement === elem) {
          clearInterval(focusInterval);
        }
      }
      elem.focus();
    });
  }
}

The messing around with focusList is because I was finding that the command entrypoint that opened this particular dialog suffered even more from multiple invocations than most other triggers in Photoshop. I was getting anything up to a dozen calls to the load event handler without adding that defence around it.

The other interesting thing I noticed is that when I dumped ‘elapsed’ into the console log I was usually seeing times between around 20 and 40ms. But my earlier attempts with a single call to focus sometimes failed with a 150ms delay. I don’t understand JS well enough to even hazard a guess at the reason for that!

2 Likes