[go: nahoru, domu]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-positioning] position: popover proposal #5699

Open
claviska opened this issue Nov 5, 2020 · 21 comments
Open

[css-positioning] position: popover proposal #5699

claviska opened this issue Nov 5, 2020 · 21 comments

Comments

@claviska
Copy link
claviska commented Nov 5, 2020

As a custom element author, it's not uncommon to create an element that utilizes some sort of dynamic "popover." There are many examples of this, but a few common ones include:

  • Dialogs
  • Dropdowns
  • Combo boxes
  • Tooltips

In most cases, such components require a "panel" element that "pops over" the page content. The problem is the panel is subject to containment by its ancestors. For example, a containing <div> with overflow: hidden will [correctly] cause unwanted clipping.

CleanShot 2020-11-05 at 09 20 46

Traditional workarounds for this problem include:

  • "Hoisting" the panel element to another position in the DOM (e.g. before </body>)
  • Using a fixed position strategy to "break out" of the containing element

Neither of these solutions are elegant, nor do they guarantee the panel will be positioned as intended. The former relies on DOM modifications and arbitrary z-indexes while the latter requires that no ancestors have transform, perspective, or filter properties:

It is positioned relative to the initial containing block established by the viewport, except when one of its ancestors has a transform, perspective, or filter property set to something other than none (see the CSS Transforms Spec), in which case that ancestor behaves as the containing block. Source

In the world of web components, particularly when a shadow root is attached, the panel is commonly contained in the shadow root. As a result, it cannot be "hoisted" to another location in the DOM because its styles are encapsulated and slotted content will no longer work. Hoisting also makes accessibility difficult since id attributes can no longer be referenced once they're removed from the shadow root (not to mention potential conflicts with ids in the global scope). This leaves us with only one option — the fixed position strategy.

With the fixed position strategy, there's no way to guarantee a panel's position. You can try to identify its containing block by traversing the DOM and checking for relevant properties, but that's arduous. And if the containing block isn't viewport, "fixing it" will involve altering properties that may cause visible changes to unrelated ancestors.

The Proposal

I suspect this use case will become more and more common as web components become ubiquitous. After many weeks of experimentation, I've come to the conclusion that this could better be solved at the CSS level. Therefore, I would like to propose a new position property:

.my-panel {
  position: popover;
}

Just like position: fixed, an element with position: popover will be removed from the normal document flow, and no space is created for the element in the page layout. Unlike fixed, the element will be positioned relative to the viewport, not its containing block. This makes sense since popovers are pseudo-modal and seldom appear off-screen.

And since naming is hard, a few alternatives might be position: overlay, position: anchored, or position: viewport.

@ByteEater-pl
Copy link

I remember wanting a solution for use cases like this at least a few times. But is it really a new positioning scheme? I'd prefer something that works orthogonally to position. The goal is to allow the element to be hoisted for some parts of styling as much as possible, breaking from ancestors' context. I see two main differences from regular behaviour:

  • painting order: we want it on top of other stuff, even that in other stacking contexts;
  • overflow: the element should overflow its ancestors even if they normally don't allow it.

Of course this also needs working out the details. Probably contain should trump hoisting. Maybe there should also be a property preventing descendants from being hoisted further. Not necessarily a binary one – perhaps named hoisting contexts would be useful.

@claviska
Copy link
Author
claviska commented Nov 9, 2020

I think it makes sense and will be easier to implement as a positioning scheme. The way I see its behavior working is almost identical to position: fixed except without the containing block behavior.

I'm interested to get some feedback from others in the community.

@emilio
Copy link
Collaborator
emilio commented Nov 9, 2020

This would introduce the same issues that making transform / filter being fixed-pos containing blocks fixes. Transformed / filtered content should be painted atomically.

Should position: popover nodes be affected by ancestor transforms and filters? If so, how is that supposed to work?

@claviska
Copy link
Author
claviska commented Nov 9, 2020

Ideally, they wouldn't be affected. Opting in to this positioning scheme will effectively "hoist" an element visually for the purpose of breaking it out of overflows and avoiding all transforms/filters. Perhaps it makes sense to think of it as a new stacking context that gets drawn above everything else.

While this mostly makes sense in the context of an element living in a shadow DOM, it's not exclusive to that use case. The same behavior can benefit any sort of utility that needs to hoist elements programmatically for the purpose of breaking out of overflows.

Thinking about this more, an alternative might be a property that allows an element to "escape" any overflow that affects it.

.my-modal-thing {
  escape-overflow: x | y | all | none;
}

With this, we could rely on existing positioning schemes and achieve a similar effect. But personally, a positioning scheme feels more appropriate here.

Either way, I wanted to start the discussion and gather some feedback. I'm very open to alternative ideas that solve the same problem. 😄

@vmpstr
Copy link
Member
vmpstr commented Nov 9, 2020

I second that some things should not be escapable, such as contain or things like clips/bounds on iframes.

I'm worried that a feature that says "i escape all of my ancestors clips" can break some optimizations that we may have. For e.g. strict containment essentially says that the elements bounds contain all of the pixels of its contents, which means that if the element is off-screen then you don't need to paint any of its subtree. If something can escape it though, this is no longer true.

Did you have a specific set of things in mind that you think this property should ignore / escape?

@claviska
Copy link
Author

I'm worried that a feature that says "i escape all of my ancestors clips" can break some optimizations that we may have.

I'm not sure it would break optimizations, but it's a fair point to consider. In the case of position: popover, the affected element would be virtually hoisted so we can safely ignore it when ancestors are drawn. Similarly, changes to how the popover is drawn won't affect ancestors. Both can be drawn independently.

Did you have a specific set of things in mind that you think this property should ignore / escape?

I'm debating with myself whether display: none in an ancestor would cause a position: popover element to be hidden or not. I don't think I have a preference here.

However, it should break out of overflows and clips. Overflows are the most common cause of frustration, but I've seen clips used cleverly to achieve similar results and it would be difficult to guarantee the popover element is visible if they didn't break out.

@ByteEater-pl
Copy link
ByteEater-pl commented Nov 10, 2020

The main reason why I don't consider it a positioning scheme is that you provided no description of how any positioning properties would be interpreted differently for popover. And indeed in the considered examples, including the one in your picture, authors would rather like to relate to the element's context in the box tree in specifying its position.

We considered requirements for interactions and for lack thereof with different, as yet quite vague sets of properties. Let me take a stab at a simple spec taking into consideration only the most important ones: overflow (without separating the axes for now), clip-path, z-index and contain. I propose we start from there and see what's needed and achievable in the first level.

Name: popover (to bikeshed)
Value: # || icb | none
Edit: There is <selector> before the hash but GitHub swallows it.
Edit: Replaced && with ||, that's what I meant.
Initial: none
Applies to: all elements
Inherited: no
Computed value: as specified (or with <selector>s canonicalized)
Animation type: discrete

If the value is not none, let B be the nearest ancestor of the subject matching any of the given selectors. If there is none and the value contains the keyword icb, let B be the initial containing block. If B is still undefined or there is an element with contain other than none (but cf. ISSUE 3) between the subject (exclusive) and B (inclusive) in the subject's ancestor chain, the used value is none. Otherwise, disregard overflow and clip-path on the subject's ancestors which are descendants of B when rendering the subject. Additionally, if the subject is not a child of B, take the subject out of the stacking context in which it would participate and insert it into the stacking context in which A participates as if it were the following sibling of A, where A is the subject's ancestor which is a child of B.

Open issues

  1. A syntactic nit to iron out: icb and none are valid selectors. (Use parentheses?)
  2. Is there a need for a viewport value? Probably not, given this property's orthogonality to position.
  3. Add more refined (and potentially controllable) interaction with contain instead of fully trumping in all cases?
  4. Consider potential interactions with transform, filter, perspective, mask, mask-image and mask-border.

@saithis
Copy link
saithis commented Jan 1, 2021

I stumbled over the same problem just now while thinking about converting our angular combobox to a web component.

I think that dropdowns/tooltips and dialogs are different scenarios:

  • Dropdowns/tooltips are usually positioned relative to the trigger (like a position relative wrapper around both and position absolute on the dropdown/tooltip).
  • Dialogs on the other hand are usually positioned in the middle of the viewport.

So in my opinion it shouldn't be a new css position value. It would be more flexible for the developer to have a new css property which can be combined with position absolute/fixed.

About the transform question: At least for dropdown/tooltip implementations it would probably make sense if the transforms are applied.

For my usecase (dropdown of a combobox) it would be best, if it would behave the same as the dropdown of the native <select>.

@claviska
Copy link
Author
claviska commented Jan 4, 2021

Perhaps this issue should be renamed to reflect the use case, not my proposed solution. I'm happy it's got the discussion going, but I'm definitely open to alternative ideas whether it be a new property or an extension of an existing one. 😄

@LeaVerou
Copy link
Member
LeaVerou commented Jan 5, 2021

I have stumbled on this problem many times as well. However, in my use cases, the positioning scheme I needed was closer to absolute positioning than fixed.
It seems like indeed this is orthogonal to positioning, and it sounds more like what we need is the ability to override the default containing block with a different ancestor.
Does that sound right? If so, I could try to come up with some syntax ideas.

I would argue solving this is also an accessibility matter, since currently a lot of these solutions break accessibility (tabbing order), with the developer not always making up for it with script.

@claviska
Copy link
Author
claviska commented Jan 6, 2021

it sounds more like what we need is the ability to override the default containing block with a different ancestor.

This sounds like it would indeed allow us to use the fixed positioning technique more reliably. For this use case, I don't see a need to support arbitrary containing blocks aside from the initial one. 😄

@claviska
Copy link
Author

A new HTML element called <popup> was proposed yesterday. While the proposal doesn't provide the same level of flexibility as what we're discussing here, its goals largely parallel what we're trying to accomplish.

It seems to address most of my concerns for dropdowns and tooltips, so I thought I'd post it here for others to review and comment on.

@ByteEater-pl
Copy link

I know that in the times of HTML5 the tenet of separation between content and presentation isn't held very strongly, but the existence of such element, possibly with presentational hints, should not be an excuse to forgo it in CSS. If both are standardized, the HTML element should just have its presentation defined in the default stylesheet. And modifiable by authors if they wish (including the flexibility you wish for, @claviska). Of course, there's also the benefit of orthogonal specifications, enabling such styling in documents using something other than HTML.

@LeaVerou
Copy link
Member
LeaVerou commented Feb 8, 2021

After thinking about it a bit more, I wonder if the best way to go isn't necessarily to override the containing block, but to be able to declare an element as something that can "spill out" of an overflow: hidden container.

Something like overflow-behavior: normal | popup? (TBB)

One issue with that is that now overflow: hidden is no guarantee of no overflow anymore. We could also introduce a new strict modifier value for overflow that means "hide everything, even popups", (used like: overflow: hidden strict).

@vmpstr
Copy link
Member
vmpstr commented Feb 8, 2021

One issue with that is that now overflow: hidden is no guarantee of no overflow anymore. We could also introduce a new strict modifier value for overflow that means "hide everything, even popups", (used like: overflow: hidden strict).

One way to resolve this without introducing new keywords is to have popups respect things like contain: paint or overflow: clip or similar. It would add ambiguity since some properties would 'leak' popups and others would clip them, but I don't think that's worse that having something like overflow: hidden strict

@LeaVerou
Copy link
Member
LeaVerou commented Feb 8, 2021

What worries me is that if popups cannot be guaranteed to display, authors would still resort to the existing techniques (described in the first post).

@vmpstr
Copy link
Member
vmpstr commented Feb 8, 2021

There are less extreme ways to make this work though.

For example, if the popups are clipped by contain: paint, we can use overflow-clip-margin, which is a fairly new addition, to pad the clip imposed by paint containment. This means that the author can make padding 'large enough' for the popup but not infinite as to defeat any kind of browser optimizations that rely on skipping off-screen work.

overflow-clip-margin doesn't apply to overflow: hidden though, but maybe this can work together with the 'spill-over' behavior

@ShanonJackson
Copy link

Just make "dialog" attribute, using .show() method break out of overflow like it does for .showDialog().
I think this is actually missing in the dialog spec and I'm thinking of raising it.

breaks out of overflow parents when .showDialog() is called but not .show()

@kizu
Copy link
Member
kizu commented Sep 14, 2023

Stumbled upon this issue when researching stuff for anchor positioning. Now that we have a spec for it in progress — https://drafts.csswg.org/css-anchor-position-1/ (allowing attaching things to other elements, and escaping some containers via fixed anchor positioning), HTML Popover API (allowing moving the popover into the topmost layer), and there are separate issues about having a way to escape/reparent elements (#8588, #9107, for cases that Popover does not cover) — should this issue be closed in favor of these, or is there something else that could be salvaged from it?

@yisibl
Copy link
Contributor
yisibl commented Jan 5, 2024

overflow-clip-margin doesn't apply to overflow: hidden though

@vmpstr So we also need properties like overflow-hidden-margin.

@mayank99
Copy link
mayank99 commented Jun 19, 2024

This issue is worth revisiting now that the popover API has shipped in all browsers.

Today, you can put pretty much anything in the top layer using code similar to below:

<div id="p" popover="manual">
<script>
  p.showPopover();
</script>

This requires HTML and worse, it requires JavaScript.

Note: I'm talking about popover="manual" because it doesn't require any user interaction and doesn't have special behaviors (like closing on Esc keypress).


Why shouldn't it be possible to arbitrarily put things in top-layer declaratively using CSS (There was some discussion about this in #6965)

It would be immensely useful, and CSS feels like the correct place to do this. When migrating some of my old code over to popover, I found myself replacing a lot of CSS (z-index: 9999) with JavaScript, which is never a good thing.

There is even a UA-restricted overlay property that could be re-used for this purpose.

#p {
  overlay: manual;
}

One of the issues I can foresee is loops. There might be a selector that only matches elements that are in the top-layer (#7319), and authors could use that selector to unset the declaration that placed the element in the top-layer in the first place. But this issue can be avoided by restricting this pseudo-class to non-CSS triggered top-layer elements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

12 participants