hybridsjs / hybrids

Extraordinary JavaScript UI framework with unique declarative and functional architecture
https://hybrids.js.org
MIT License
3.03k stars 85 forks source link

How to add focus when opening a form? #231

Closed Qsppl closed 7 months ago

Qsppl commented 7 months ago

Expected Behavior:

I click "Show the dialog". image Focus is set to the "input" element by default. image

example: https://codesandbox.io/p/sandbox/focus-managment-in-hybrids-ffc334?file=%2Findex.html%3A24%2C68

Problem: I can't set focus before rendering because the input doesn't exist yet.

smalluban commented 7 months ago

I think you can use built-in feature of the Shadow DOM, which is delegateFocus. You can utilize it with hybrids by extending the render function with options object:

render: Object.assign(() => { ... }, { delegateFocus: true })

According to the MDN:

If true, when a non-focusable part of the shadow DOM is clicked, or .focus() is called on the host element, the first focusable part is given focus, and the shadow host is given any available :focus styling.

I suppose that, if you then use myEl.focus() it will focus your first focusable element in the Shadow DOM.

Qsppl commented 7 months ago

I still can't call "myElement.focus()" because the element doesn't exist before rendering. After rendering, I can’t assign focus because there is no “afterRender” event/callback. I tried to do what you suggested in the example and it didn’t work: (https://codesandbox.io/p/sandbox/focus-managment-in-hybrids-ffc334?file=%2Fsrc%2Findex.ts%3A33%2C1)

smalluban commented 7 months ago

Ok, it's another case - I understood it differently. The delegateFocus won't help, as it just make a focus, when the whole element gets a "focus" event.

There is a section in docs, which explains how to reference internal elements when using render/ content - https://hybrids.js.org/#/component-model/structure?id=reference-internals

In short, instead of using host.shadowRoot.querySelector directly, use host.render().querySelector - in case if the observer is called before the render's one, it forces to re-render earlier - in the result - the content will be there.

In your example, just change L14 to:

if (value) host.render().querySelector("input").focus();

It is 100% performance safe, as the render internals are called only once per change, so the latter observer (from "render" itself) won't run - you just force to run it earlier because you need it.

Qsppl commented 7 months ago

Thanks, this solves the problem. I need to read the documentation more carefully)