vaadin / web-components

A set of high-quality standards based web components for enterprise web applications. Part of Vaadin 20+
https://vaadin.com/docs/latest/components
443 stars 83 forks source link

Option to make overlays non-modal #451

Open rolfsmeds opened 3 years ago

rolfsmeds commented 3 years ago

Overlays (vaadin-overlay) opened by various Vaadin components (e.g. combo-box, date-picker, etc) are effectively modal, although they render no modality curtain, as they set pointer-events:none; on the page body, which prevents the users from interacting with the rest of the page while the overlay is open.

E.g. while a combo-box dropdown is open, focus cannot be moved to another field by clicking it, as the initial click will only close the overlay, but not move focus to the clicked field.

While this is in line with native html elements like <select>, it may be undesired in some applications.

Solution proposal:

A simple solution, that would not require any additional API in components, could be to use a custom css property (with the currently hardcoded none as default value) for the pointer-events value set to the page body, so that it would be easy to override just by applying a single line of css.

E.g. if vaadin-overlay applied pointer-events: var(--vaadin-overlay-page-body-pointer-events); the modality could be removed from all overlays with a single line of global css: --vaadin-overlay-page-body-pointer-events: auto;

If a more scoped override is desired, e.g. to only remove modality from combo-boxes, this could be achieved by a css selector targeting combo-box-specific overlays.

yuriy-fix commented 3 years ago

Have quickly prototyped this option. There are several things that I would like to discuss. The major one is the css property name.

In the logic of entering modal state overlay is also applying pointer-events to other overlays and I guess we want to change that as well, so then word body in --vaadin-overlay-page-body-pointer-events won't represent the whole functionality. In addition, not sure what does word page represent 🤔

Another thing is the usage of custom css property for that purpose. I guess the reason is to avoid “any additional API in components”, but in reality from the options only auto and none would make sense (i.e. SVG options cannot be applied, initial doesn’t make difference, inherit would most probably confuse because of the contents teleportation logic)

If only two values makes sense we can continue usage of the modeless state and propagate it to the overlay from the component.

<vaadin-combo-box modeless placeholder="Placeholder"></vaadin-combo-box>

Pros:

  1. Would be more visible for the users as public API than custom css prop.
  2. Would not confuse with all other possible options, like fill / stroke / inherit / unset etc.
  3. Can be scoped to one specific component without classes/attributes/ids used on :host
  4. Will not change current behavior, but instead add simple functionality on top.

Cons:

  1. Cannot be applied to a group of overlays at once if not default (i.e. scoping all combo-boxes on the page or all overlays). That one should probably stay opt-in feature due to the reasons described here.
  2. Implementation is not difficult, but requires changes to all overlay components.
rolfsmeds commented 3 years ago

Yeah the name was just what first came to mind, as an example, not a proposal for the actual property name.

As for whehter a css property is appropriate, my assumption is that you'd specifically want to apply it to all components with invisibly modal overlays (i.e. overlay input fields, context-menu, menu-bar), as opposed to just one. The rationale being that you'd want all such overlays to work the same -- otherwise the behavior would be inconsistent between different component types and instances.

That being said, if there's a better way to achieve that, that would be avoid the issues mentioned, that would be even better. I just don't know what that would be.

rolfsmeds commented 3 years ago

Actually, one way to make the custom property approach less weird would be to hide the fact that it's using pointer-events, like so: --nonmodal-overlays: all; and pointer-events: var(--nonmodal-overlays, none);

That gives us: --nonmodal-overlays undefined => pointer-events: none; (fallback value used, no overlays are non-modal) --nonmodal-overlays: none; ==> pointer-events: none; (no overlays are non-modal) --nonmodal-overlays: alll; ==> pointer-events: all; (has no effect, falls back to auto, i.e. all overlays are non-modal)

rolfsmeds commented 6 months ago

And I just now realized that the CSS approach would probably not be satisfactory, as most components with overlays automatically set focus back to the non-overlay part of the component when the overlay changes (e.g. the date-picker returns focus to the <input> when its overlay closes). So if the user was able to focus another field (for example) while another field's overlay is open, that field would briefly get focus before its automatically set back to the one whose overlay just closed.

Thus, this does need a proper API that not only removes modality but also disables to focus-return in case focus has already moved to some other element.

robrez commented 6 months ago

Hi, I was asking about this on Discord yesterday

FWIW, I have been looking over some of the overlay-producing examples from WAI/ARIA and all of them are modeless.

In prior issues and discussions, some have cited that the native select is modeless. I wanted to add that this is not strictly true. It's true on desktop windows. It is not true on my cell phone.

Restating a realization I had in the discord thread: I was narrowly thinking of my user -- accessing a feature on a desktop/laptop and using mousevents. It seems reasonable that some users/contexts - especially on smaller devices, using touch events, will have a difficult time closing overlays without inadvertently interacting with "actions" (buttons, links, listbox/grid items) behind the overlay.

That started me down the trajectory of thinking of some goofy mechanism to provide separate modality for "narrow/touch" vs "large/point".. eg:

In particular I was thinking of those components which transition to a sheet. Anyhow, that all sounds complicated... so my after thought was.. "well I suppose I could use mediaMatch to conditionally toggle the modality"... but that sounds kind of tedious

rolfsmeds commented 6 months ago

The "modeless modal" as you call it could perhaps be called something like "device optimized" or something. I like the idea. In think in addition to viewport size, the mode could be decided also based on pointer: coarse.

Many of the overlay components (e.g. DatePicker, Select, ContextMenu, MenuBar) already have a distinct rendering mode for small viewports, where the overlay takes up most of the screen and is quite explicitly modal (with a modality curtain and everything), so this "device-conditional modality" would probably only affect those that don't have that, such as ComboBox and TimePicker (I'm not sure it would make sense to make those small-viewport renderings non-modal)