openui / open-ui

Maintain an open standard for UI and promote its adherence and adoption.
https://open-ui.org
Other
3.37k stars 182 forks source link

[invokers] invoke on focus but not hover #1030

Open flackr opened 2 months ago

flackr commented 2 months ago

The idea of using invokers came up for how to explain the behaviors of some of the carousel primitives. For scroll markers (https://github.com/flackr/carousel/issues/6) in particular, there are some complex requirements which I'm trying to navigate whether we could properly explain through some collection of the UI's here. The intended behavior of scroll markers is almost identical to radio buttons. On activation (which can be applied through arrow key presses in a manner similar to focus group focus), they scroll to their targeted element. The interest invokers proposal is almost this, except includes hover, which is very unlike radio-button interaction.

TLDR; I feel like there are a class of applications where we want some activation behavior on focus, but not hover. Would it make sense to say that for some elements, we apply the invoke behavior on focus? E.g. this may be the case for radio buttons.

lukewarlow commented 2 months ago

This is something that's come up for something before. We currently don't expose the type of interest (hover, focus, long press). Maybe we should do that so you could at least with a bit of JS decide whether to prevent default the event. But JS might not be the ideal here.

Pointer events while designed to be input agnostic do expose the type of input. So maybe this follows in the same theme?

keithamus commented 2 months ago

Interest invokers very specifically abstract away focus/hover to unify cross HID behaviours. I think if you want "interest invokers but just focus" then using the focus event is probably what you want. I'm not convinced we should extend interest invokers to facilitate exclusion or different semantics for a particular HID.

To more directly talk about your pattern though, a carousel is kind of a spicy tabpanel; it has a list of buttons that can be immediately activated and they will bring the matching panel to the front. They should also activate on click though. You could map the focus to a click, then use invokers to activate a custom element to switch the tab-panel. Given that, you might be better implementing a Carousel as tabpanels, and focusgroup might be a better fit for the tablist. In lieu of an actual tablist built-in, here's what I'd do:


<my-carousel id=carousel">
  <div role=tablist focusgroup slot=tablist>
    <button role=tab value=1 tabindex=-1 onfocus="this.onclick()" invoketarget="carousel">1</button>
    <button role=tab value=2 tabindex=-1 onfocus="this.onclick()" invoketarget="carousel">2</button>
    <button role=tab value=3 tabindex=-1 onfocus="this.onclick()" invoketarget="carousel">3</button>
  </div>
  <div role=tabpanel>1</div>
  <div role=tabpanel>2</div>
  <div role=tabpanel>3</div>
</my-carousel>
<script>
  customElements.define('my-carousel', class extends HTMLElement {
    connectedCallback() { this.addEventListener('invoke', this) }
    handleEvent(event) {
      const idx = event.target.closest('[role=tab]')?.value
      if (value) this.switchTo(idx)
    }
    switchTo(idx) {
      let i = 0;
      for(const panel of this.querySelectorAll('[role=tabpanel]')) {
        i += 1;
        panel.hidden = i != idx
      }
    }
  });
</script>
flackr commented 2 months ago

Yes, totally agree that clicking should invoke the action as well, and that mapping focus to the invoke action is a way to solve this. I'd like to have a way to do this declaratively, otherwise it doesn't really explain the thing that the pseudo-element scroll markers create. I think this could well be some attribute value for focusgroup which implies invoking on focus.

E.g.

<my-carousel id=carousel">
  <div role=tablist focusgroup="invoke" slot=tablist>
    <button role=tab value=1 tabindex=-1 invoketarget="tab1" invokeaction="scrollTo">1</button>
    <button role=tab value=2 tabindex=-1 invoketarget="tab2" invokeaction="scrollTo">2</button>
    <button role=tab value=3 tabindex=-1 invoketarget="tab3" invokeaction="scrollTo">3</button>
  </div>
  <div role=tabpanel id=tab1>1</div>
  <div role=tabpanel id=tab2>2</div>
  <div role=tabpanel id=tab3>3</div>
</my-carousel>

As an example of the scrolling interaction you can see the demo at https://flackr.github.io/carousel/examples/carousel/image/ . The advantage of using scrolling is that it automatically supports swiping to advance.

As described in #1031 this alone wouldn't explain how the active button also needs to be updated on scroll.