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-anchor-1] More declarative syntax for simple cases #7757

Closed
tabatkins opened this issue Sep 16, 2022 · 16 comments
Closed

[css-anchor-1] More declarative syntax for simple cases #7757

tabatkins opened this issue Sep 16, 2022 · 16 comments

Comments

@tabatkins
Copy link
Member

In the minutes of the CSSWG discussion, @emilio brought up that handling fallback completely and correctly can require a decent amount of code, but in most cases the desired fallback behavior is very simple and straightforward. He suggests having a more declarative version of the fallback specification, which will let authors get this "basic" position fallback without having to think thru a lot of cases (or copy-paste from a good example).

This sounds reasonable to me. The current API is intentionally somewhat low-level because, as Ian said in the discussion, there are a lot of cases that want the complexity. But it's also true that a large fraction, possibly a majority, of cases want a very simple behavior that can be described in a common fashion.

Here's my suggestion: we add a auto side keyword to anchor(): anchor(--foo auto). This only works if the opposite property in the axis is auto (so the positioning is guaranteed to not have an effect on the element's size); if this isn't true it resolves to 0.

By default, it positions the specified edge of the element against the opposite edge of the anchor; top: anchor(--foo auto) puts your top edge against the bottom edge of the --foo anchor. It automatically falls back to the opposite set of edges (for both elements) if it needs to. (This doesn't change what property it's affecting - in the preceding example it would resolve top to a value that happens to place the element's bottom edge against the anchor's top edge, so 'transition: top 1s` still works to make it non-abrupt.) This applies to each axis independently; the other axis can also be auto-anchored, against the same or a different element, or be a fixed value or a non-auto anchor, whatever.

This would resolve the "lots of small popups, all need a separate @position-fallback rule" problem.

Emilio also mentioned a "sticky"-like behavior, where the element defaults to sticking to an edge like above, but if it would slightly overflow it just slides enough to avoid that, rather than flipping. We could do this as well with a sticky keyword that's subject to the same constraints as auto. I'm curious exactly what the constraints of this are, tho - does it stop sliding when the opposite edges are aligned? This would occur when the anchor is just about to slip off-screen entirely - is that what's desired?

Interaction between these and position-fallback need to be defined. Maybe we consider these as effectively a first fallback entry, and then use the actual position-fallback only if they still overflow in either position?

@tabatkins
Copy link
Member Author

Agenda+ to discuss adding the auto anchor-side keyword as described above: it requires the opposite inset property to be auto, and automatically positions the edge against the opposite edge of the anchor, falling back to using the opposite sides if needed.

That is, .popup { top: anchor(--target auto); bottom: auto; } will default to putting the top edge of the popup against the bottom edge of the target, but will flip to put popup's bottom edge against the target's top edge if needed to satisfy the fallback rules.


Also want to discuss the possible constraints of the proposed sticky keyword, that just slides to avoid leaving the screen rather than flipping all at once. I think @emilio mentioned Firefox UI having this behavior available - what are the exact constraints there, and is it good or should we do something different?

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed Declarative anchor fallback positioning.

The full IRC log of that discussion <TabAtkins> Topic: Declarative anchor fallback positioning
<TabAtkins> github: https://github.com//issues/7757
<fantasai> TabAtkins: At previous meeting when introduced anchor positioning
<fantasai> TabAtkins: Emilio brought up that XUL had similar functionality, and had a simple declarative way to express fallbacks
<fantasai> TabAtkins: This is good, current fallback is very complex
<fantasai> TabAtkins: I have a proposal in this thread for solving that
<fantasai> TabAtkins: Can still use full complex positioning if necessary, but this should solve common cases
<fantasai> TabAtkins: Not looking for resolution right now, just wanted to get attention for review and comment
<fantasai> iank_: Can you briefly describe this mode?
<fantasai> TabAtkins: The way you opt into it, rather than declaring a side to align to in the anchor
<fantasai> TabAtkins: Instead you say "auto", and it will automatically choose the opposite side of the property you're setting
<fantasai> TabAtkins: but if you would trigger fallback positioning, we flip around
<fantasai> TabAtkins: and align your bototm edge to the top edge
<fantasai> TabAtkins: so we can do this positoining without affecting layout
<fantasai> TabAtkins: then if neither works, we go to normal fallback rules
<fantasai> iank_: idk that you need auto on opposite side
<fantasai> iank_: how does this interact with maximum number of fallbacks?
<fantasai> TabAtkins: need auto on other side because if not, there's nothing to fall back to
<fantasai> TabAtkins: If you specify this for your top property, we can't switch to using bottom if your bottom is zero
<fantasai> iank_: ok
<fantasai> TabAtkins: wrt maximums, this will be part of that list
<fantasai> TabAtkins: effectively position fallback will have an extra entry, so this will be factored into that maximum
<fremy> q+
<fantasai> iank_: does this mean you need to set on both insets?
<fantasai> TabAtkins: no, set one to anchor and the other to auto
<fantasai> TabAtkins: it will be naturally sized
<fantasai> TabAtkins: we'll swap the properties if we need to for fallback reasons
<fantasai> [note that auto is the initial value]
<fantasai> iank_: You can embed an anchor inside of a calc() expression
<fantasai> iank_: so if you're not using the ?? anchor, what does that calc expression resolve to?
<fantasai> TabAtkins: after doing the flip?
<fantasai> TabAtkins: it resolves to the appropriate other edge
<fantasai> TabAtkins: Luckily because insets are specified in opposite directions
<Rossen_> ack dbaron
<fantasai> TabAtkins: if you use calc() to put you 10px from bottom edge, that same calc will work against the top
<fantasai> iank_: so you're taking the whole calc() and putting it in the other property, and the other property becomes auto?
<fantasai> TabAtkins: yes, we resolve exactly as if you'd done it on the other side
<fantasai> TabAtkins: just as position fallback already works, but we make it automatic for obvious placement
<fantasai> iank_: sounds fine, just concerned about magical swapping of computed values at layout time
<fantasai> TabAtkins: This is exactly position fallback, the same feature, we're just giving you one for free
<fantasai> iank_: not exactly, because you're taking e.g. top is auto, bottom is calc expression with anchor
<fantasai> iank_: the position fallback then is like the top becomes that calc function and bottom becomes auto
<fantasai> iank_: oh, and I see, you're saying that this will automatically populate a fallback position entry with those two things
<fantasai> iank_: okay, that makes sense
<fantasai> iank_: if you can write that as a note, this is how it should be implemented, that would be helpful
<fantasai> fremy: Small question
<fantasai> fremy: because I believe this would be common
<fantasai> fremy: what happens if you do it for top and left, and if so what happens?
<fantasai> TabAtkins: what exactly will happen is a great question, which I have not thought through the implications of!
<fantasai> TabAtkins: I don't have an answer for you yet.
<fantasai> TabAtkins: I don't think it's hugely important what the answer is... worst case maybe generate 3 fallbacks, generating vertical flip, horizontal flip, or both
<fantasai> TabAtkins: will think about it
<iank_> likely want the flips in the logical space vs the physical?
<fantasai> fremy: I guess maybe we can check what tooltips do on iOS/MacOS/Windows
<iank_> block flip vs. vertical flip
<fantasai> +1 iank
<Rossen_> ack fremy
<fantasai> TabAtkins: just wanted to get feedback, this was helpful
<TabAtkins> well, doesn't matter, point is you flip to the opposite side of wherever it's specified

@tabatkins
Copy link
Member Author

All right, anchor(auto) is added to the spec. For the issue raised by @FremyCompany in the minutes, I ended up specifying that if you use automatic anchoring in both axises, it adds three entries to the fallback list - one reversing the block axis, one the inline, and one doing both.

@xiaochengh
Copy link
Contributor

I don't see major issues with 3a60ac6, but some minor ones:

  1. What happens when we use anchor(auto) in a @position-fallback list?

I think the correct behavior is to insert some flipped fallback position entries immediately after the current entry.

  1. If the anchor(auto) is invalid (i.e. the other side is not auto), does it still add entries to the position fallback list?

I think it should not. It doesn't make much sense when the base style is {top: 100px; bottom: anchor(auto)} and then we try fallback position {top: anchor(auto); bottom: auto} -- it's not properly flipping the element.

  1. It doesn't work with how position fallback currently works. Currently, if an element uses position-fallback, then we never try to layout the element with its base (computed) style -- we start directly with the first fallback position. So in the example in the spec, the two are not really equivalent:
.foo {
  position: absolute;
  top: calc(.5em + anchor(--foo auto));
}
.foo {
  position: absolute;
  top: calc(.5em + anchor(--foo bottom));
  position-fallback: --flip;
}
@position-fallback --flip {
  @try {
    top: auto;
    bottom: calc(.5em + anchor(--foo top));
  }
}

In the second CSS, we'll always end up with the fallback position.

To make this example work, if anchor(auto) is used in the base style, we actually add two entries: a copy of the base style, followed by the flipped style; If it's used in both axes, we add four entries where the first is a copy of the base style.

Alternatively, we can change the fallback position application algorithm into: try to use the base style first, and then if it overflows, try each fallback position. Not sure if it's a good idea...

@tabatkins
Copy link
Member Author

What happens when we use anchor(auto) in a @position-fallback list?

I defined it in a later commit - it just selects the appropriate side as normal, but doesn't insert any entries. This seemed like the simplest behavior, and if you're already writing a fallback list, you can just write your fallback cases; the point of this is to let you avoid writing a fallback list entirely. But I'd be willing to make a change if necessary.

If the anchor(auto) is invalid (i.e. the other side is not auto), does it still add entries to the position fallback list?

It shouldn't, yeah. I meant the current text to express that, but I see I didn't properly hook things to the validity of the automatic anchor positioning. If you don't use the value correctly none of the effects should trigger, and it'll just be an invalid anchor function. I'll tweak to fix.

It doesn't work with how position fallback currently works. Currently, if an element uses position-fallback, then we never try to layout the element with its base (computed) style -- we start directly with the first fallback position.

...huh, I thought that I wrote it to start with the base styles and only do the position-fallback if it fails from the start. But reading it again, you're right, we do indeed just jump straight into the fallback list. And on consideration, that's almost certainly the most sensible behavior anyway.

So yeah, I'll need to add the base styles in. And also I probably want to change how this interacts with an existing fallback list - most likely I want to change it to only add entries if there is no list (for the same reason we don't add the base styles to the fallback list already). So you can either use auto positioning or use position-fallback, not both.

That then also makes me feel better about doing what you suggested about using it in a fallback list, where it adds 1-3 extra entries after itself. That way you can do some simple flipping followed by something more complicated as a last resort.


So in summary:

  1. I'll change it so that when used in a fallback list, it expands into 2 or 4 entries.
  2. I'll make it clear that all the behavior is contingent on using it correctly - if the opposite property isn't auto, it's invalid and does nothing.
  3. I'll make its use in normal styles only do something when there is no fallback list; if you already have a fallback list the base styles are ignored as normal.

@xiaochengh
Copy link
Contributor

There's still value if we allow anchor(auto) in position fallback list: to reduce verbosity of a complex position fallback list.

For example, the current position fallback list of <selectmenu>'s listbox is (assuming #8059 is fixed):

@position-fallback -internal-selectmenu-listbox-default-fallbacks {
  /* Below the anchor, left-aligned, no vertical scrolling */
  @try {
    inset-block-start: anchor(self-end);
    inset-inline-start: anchor(self-start);
  }
  /* Below the anchor, right-aligned, no vertical scrolling */
  @try {
    inset-block-start: anchor(self-end);
    inset-inline-end: anchor(self-end);
  }
  /* Over the anchor, left-aligned, no vertical scrolling */
  @try {
    inset-block-end: anchor(self-start);
    inset-inline-start: anchor(self-start);
  }
  /* Over the anchor, right-aligned, no vertical scrolling */
  @try {
    inset-block-end: anchor(self-start);
    inset-inline-end: anchor(self-end);
  }
  /* Below the anchor, left-aligned, fill up the available vertical space */
  @try {
    inset-block-start: anchor(self-end);
    block-size: -webkit-fill-available;
    inset-inline-start: anchor(self-start);
  }
  /* Below the anchor, right-aligned, fill up the available vertical space */
  @try {
    inset-block-start: anchor(self-end);
    block-size: -webkit-fill-available;
    inset-inline-end: anchor(self-end);
  }
  /* Over the anchor, left-aligned, fill up the available vertical space */
  @try {
    inset-block-end: anchor(self-start);
    block-size: -webkit-fill-available;
    inset-inline-start: anchor(self-start);
  }
  /* Over the anchor, right-aligned, fill up the available vertical space */
  @try {
    inset-block-end: anchor(self-start);
    block-size: -webkit-fill-available;
    inset-inline-end: anchor(self-end);
  }
}

With auto positioning, we can shrink its size in half (with some difference that shouldn't matter):

@position-fallback -internal-selectmenu-listbox-default-fallbacks {
  /* Below/above the anchor, left-aligned, no vertical scrolling */
  @try {
    inset-block-start: anchor(auto);
    inset-inline-start: anchor(self-start);
  }
  /* Below/above the anchor, right-aligned, no vertical scrolling */
  @try {
    inset-block-start: anchor(auto);
    inset-inline-end: anchor(self-end);
  }
  /* Below/above the anchor, left-aligned, fill up the available vertical space */
  @try {
    inset-block-start: anchor(auto);
    block-size: -webkit-fill-available;
    inset-inline-start: anchor(self-start);
  }
  /* Below/above the anchor, right-aligned, fill up the available vertical space */
  @try {
    inset-block-start: anchor(auto);
    block-size: -webkit-fill-available;
    inset-inline-end: anchor(self-end);
  }
}

(this also means that in the generated fallback positions, there should also be sizing and box alignment property values copied from the base style or the base @try rule)


Besides, the auto keyword only allows us to pick the opposite physical side.

What if we want to use the same side with automatic flipping? For example, what if we have left: anchor(auto-same-side) (sorry for bad at naming) that expands into left: anchor(left) and right: anchor(right) ? Then the selectmenu position fallback can be further simplified into:

@position-fallback -internal-selectmenu-listbox-default-fallbacks {
  /* Below/above the anchor, left/right-aligned, no vertical scrolling */
  @try {
    inset-block-start: anchor(auto);
    inset-inline-start: anchor(auto-same-side);
  }
  /* Below/above the anchor, left/right-aligned, fill up the available vertical space */
  @try {
    inset-block-start: anchor(auto);
    inset-inline-start: anchor(auto-same-side);
    block-size: -webkit-fill-available;
  }
}

@tabatkins
Copy link
Member Author

There's still value if we allow anchor(auto) in position fallback list

Right, that's why I said I should edit to allow it and expand in-place. ^_^

What if we want to use the same side with automatic flipping?

Oooh, I didn't even consider that "auto same side" could be useful, but you're right, it totally could be! I'll have to think of a better name, but consider it in the spec.

@xiaochengh
Copy link
Contributor

What happens if both axes use auto, while one is valid and the other is not? For example:

#foo {
  left: anchor(auto); /* invalid use of `auto` */
  right: 0;
  top: anchor(auto); /* valid use of `auto` */
  bottom: auto;
}

While the usage on the x-axis is invalid, I think it still makes sense to expand it on the y-axis.

@tabatkins
Copy link
Member Author

Yes, that follows from the (2) edit I said I needed to make:

I'll make it clear that all the behavior is contingent on using it correctly - if the opposite property isn't auto, it's invalid and does nothing.

tabatkins added a commit that referenced this issue Feb 8, 2023
…rides base styles, rather than using base styles as the first entry. #7757
@tabatkins
Copy link
Member Author

All right, landed a few commits today:

  • dd4a553 makes it clearer that all the automatic stuff only triggers if the auto keyword is used validly (with the opposite property being auto)
  • 503cb67 adds the auto-same keyword
  • 5b063f5 specifies that the auto keywords expand into multiple entries when used in @try
  • 15fcd3d correctly handles the interaction of base styles and position fallback when dealing with auto.

@xiaochengh
Copy link
Contributor

Thanks for the edits!

One more issue: what happens when the element's base style has both auto position fallback?

The current version has only specified two cases:

  1. The element has position-fallback: none (which means auto is used in base style)
  2. auto being used in a @try rule

@tabatkins
Copy link
Member Author

I can't quite parse your question, but I assume it's "what happens if you use auto in base styles and also in a @position-fallback"? Then the auto in base styles doesn't do anything.

@xiaochengh
Copy link
Contributor

Are you suggesting that using auto in both base style with a position fallback doesn't make much sense?

I was think of something else, but now I agree with that (the sentence above).

"what happens if you use auto in base styles and also in a @position-fallback"? Then the auto in base styles doesn't do anything.

So how about making the restriction even stronger: if the base style has anchor(auto) and uses position fallback (see example below), then the anchor(auto) in the base style doesn't expand into any @try rule.

.target {
  left: anchor(auto);
  position-fallback: --pf;
}

@position-fallback --pf {
 @try { top: ... }
 @try { bottom: ... }
}

Note that we may still need to evaluate a validanchor(auto) from the base style, in which case we just evaluate it as normal.

@tabatkins
Copy link
Member Author

Are you suggesting that using auto in both base style with a position fallback doesn't make much sense?

Per your earlier comment, base styles are ignored when using position fallback, so I'm matching that behavior.

So how about making the restriction even stronger: if the base style has anchor(auto) and uses position fallback (see example below), then the anchor(auto) in the base style doesn't expand into any @Try rule.

That's what the current spec already does.

@xiaochengh
Copy link
Contributor

That's what the current spec already does.

Yeah, and it's what confused me. Maybe worths adding a note?

tabatkins added a commit that referenced this issue Feb 10, 2023
…llback prevents anchor(auto) from creating fallback entries. #7757
@xiaochengh
Copy link
Contributor

I think this can be closed now?

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

4 participants