SplitLayout: non-proportional splitter position
Describe your motivation
The splitter position is currently always proportional to the width of the SplitLayout, which means that, as the SplitLayout shrinks and grows along the split's axis, both sides of the split scale proportionately.
This is fine in many cases, but sometimes you would need one side to stay unchanged, so that only the other side of the split scales (until the SplitLayout's total size is smaller than the "fixed" side of course).
Describe the solution you'd like
A way to set the splitter position to be "fixed" in relation to one of the sides, e.g. through an API that lets you define either side as the "split position anchor".
(In order to really make this work properly in the Flow component, it would also need a split position setter overload that takes a non-percentual size as argument, e.g. setSplitterPosition("300px")).
Describe alternatives you've considered
No response
Additional context
No response
Some inspiration: there is a component for the angular framework that offers some advanced options for nesting layouts (https://angular-split.github.io/). Using that split-component you can easily do something like this:
Note: the middle (yellow) area has a minimum width, moving the left section will respect this one...
A prototype that implements the feature can be found on this branch: https://github.com/vaadin/web-components/tree/proto/split-layout/split-position-anchor.
The main difference is to set the flex-basis of the element to be used as the anchor with its initial size (width or height) alongside changing its flex-grow and flex-shrink to 0, to make it always have the defined size. That can be verified working in the example below:
https://github.com/vaadin/web-components/assets/262432/f4ba0656-670d-4bf8-8e77-a7c80803d77b
Some findings:
- It needs to be verified that having different
flex-grow/flex-shrinkvalues for the elements might cause some undesirable effects. Just as a reference, theangular-splitcomponent shared by @JayKayDeon here, definesflex-grow/flex-shrinkas0for elements with defined size and1, otherwise) - As it can be seen in the example if the SplitLayout size shrinks more than the anchor element size, it won't shrink with it, maintaining its size, which will cause content to be hidden by the overflow
- In some scenarios (like the one on the example, with a nested SplitLayout), the calculation of the anchor initial size might be off, so it needs to be defined how to calculate it properly in cases where the size is not explicitly defined.
Hi @JayKayDeon, a somewhat similar-looking layout can be produced with two split layouts using the following setup. Not sure if this fully covers your use case:
<vaadin-split-layout style="width: 900px; height: 400px; outline: 1px solid black">
<div>Left</div>
<vaadin-split-layout style="height: 100%">
<div style="min-width: 300px; background: lightyellow">Middle</div>
<div style="max-width: 300px">Right</div>
</vaadin-split-layout>
</vaadin-split-layout>
https://github.com/vaadin/web-components/assets/1222264/cb9d9dbe-3d68-40d2-b045-55d46f7f19e7
Update by @DiegoCardoso
The same example, but with Flow:
SplitLayout outerLayout = new SplitLayout();
SplitLayout innerLayout = new SplitLayout();
outerLayout.addToPrimary(new Span("left"));
outerLayout.addToSecondary(innerLayout);
innerLayout.addToPrimary(new Span("middle"));
innerLayout.addToSecondary(new Span("right"));
outerLayout.getStyle().setOutline("1px solid black");
outerLayout.setHeight("400px");
outerLayout.setWidth("900px");
innerLayout.setPrimaryStyle("background", "lightyellow");
// This is the important part
innerLayout.setPrimaryStyle("min-width", "300px");
innerLayout.setSecondaryStyle("max-width", "300px");
add(outerLayout);
Circling back to this now after
As today, the default for both elements is flex-basis:auto (i.e. based on size), flex-shrink:1, flex-grow:1 through styles set on slotted children.
If only one of the elements has fixed size, and setSplitPosition is not used:
- Set the fixed-size element to have
flex-grow:0so that it doesn't grow beyond that - Set the unsized element to
flex-basis:0(so that it doesn't force the fixed-sized element to shrink) - As the user moves the handle
- set the fixed-size element's
flex-basisto the new size, but keepflex-grow:0 - keep the unsized element unchanged
- set the fixed-size element's
If both or neither elements have fixed size, or setSplitPosition is used to set the split, use the current behavior.
WIth this approach, fixed-size elements can still shrink if needed to prevent overflow, but min-width/min-height can be used to prevent that if needed.
Also, in order to prevent either side from pushing the handle out of the layout bounds, we could consider setting max-width/max-height of both children to be 100% - split-handle-size.
- The default handle size is
var(--lumo-size-s), while the small variant has1pxand the minimal variant has0px, so I think the best way to handle this would be to set that size to an internal css property (e.g.--_handle-size) which can then be used to setmax-width: calc(100% - var(--_handle-size) - I think this won't prevent setting
max-width/max-heighton the elements since setting that directly on the elements should override whatever's coming through::slotted()styles from theSplitLayout.
(I've done some quick tinkering with this just through css, but haven't tried changing the handle-dragging behavior yet, so I might be missing something.)
Overall the approach from the above comment seems doable but might require some changes.
In particular, we need to remove hardcoded flex-grow from the web component which would override flex-grow: 0.
I'm actually not sure if we really have to set flex-grow: 1 and flex-shrink: 1 via style attribute as they can in theory use fallback values from the ::slotted() styles, maybe it's enough to only modify flex-basis dynamically?
Also, need to update layout when switching e.g. "one element with fixed size" -> "both elements with fixed size".
This seems to be a bit tricky unless we require users to explicitly call the APIs e.g. getPrimaryComponent().
So we might want to deprecated public setPrimaryStyle() and `setSecondaryStyle() as suggested in #6486.
Here's the approach that I was able to come up with, not sure if it's good as it modifies public API:
- Always wrap components passed to
addToPrimary()andaddToSecondary()into custom component, e.g. could be a plainDivthat implementsHasSizeand has overrides for some methods likesetWidth()andsetHeight() - When either of the overridden methods is called, check whether there's only one component has fixed size or both, and then call
getElement().getStyle().set("flex-grow", "0")as necessary, - On splitter dragend listener, ensure the
flex-growset above isn't overridden by1. Alternatively, consider modifying the low counterpart logic to only setflex-basisas proposed above for the web component.
Here's a partial prototype: https://github.com/vaadin/flow-components/tree/proto/split-layout-fixed-size
Man, the existing API and implementation are so messed up that I don't even know where to start. Apparently it already does wrap into a div if you pass multiple components to either side (and I guess that's why there's public API for setting each side's styles?)
Always wrapping the contents in divs will also be a much bigger breaking change, as it will affect custom styling with selectors that assume no wrappers, and probably also affect the default splitter position that is currently based on the size of the contents. Having the wrappers in shadow DOM would avoid the styling break, but then we need some API on the clientside for setting the sizes.
Maybe, instead of wrapping, we should instead just introduce new API for setting the size of primary and secondary?
-
setPrimarySize(String size)/setSecondarySize(String size)- sets the specified side's
width/height - sets the specified side's
flex-grow:0 - sets the other content's
flex: 1 1 0
- sets the specified side's
-
If both are set, or
setSplitPositionis called, removes all of that
WC/React could have corresponding primary-size / secondary-size properties.
I built a quick prototype with a slightly different API.
Prototype:
- https://github.com/vaadin/web-components/compare/proto/non-proportional-splitter
- https://github.com/vaadin/flow-components/compare/proto/non-proportional-splitter
Flow component API:
void setFlexArea(FlexArea.BOTH)– defaultvoid setFlexArea(FlexArea.PRIMARY)void setFlexArea(FlexArea.SECONDARY)FlexArea getFlexArea()void setPrimarySize(String size)– sets the primary element’s flex-basis to the given size, the secondary element’s flex-basis to auto, and resets splitterPosition to nullvoid setSecondarySize(String size)– same assetPrimarySize()but applies the size to the secondary element instead
Web component API:
flex-area='both'– defaultflex-area='primary'flex-area='secondary'setPrimarySize()– similar to the Flow componentsetSecondarySize()– similar to the Flow component
There are intentionally no getters for primary and secondary size, as they can be misleading when using FlexArea.BOTH: they would return the flex-basis value in pixels, which doesn't necessarily reflect the actual size of the elements.
I guess I'm just not sure I see the need for the FlexArea API. It seems to me that the side you set a specified size to is the side whose size you "care" about, and the other one is the flexible area.
Would @SebastianKuehnau or @JayKayDeon have opinions?
My two cents: API wise I'd like to just set "width", "minWidth" and "maxWidth" to control this behavior, everything else already feels like an "implementation detail".... See https://angular-split.github.io/, having exactly this "width-restrcitions" API... Feels most intuitive and behaves as expected (for me at least)...
Yeah, I'm now inclined to agree that FlexArea isn't a very intuitive API. Rolf's proposal seems to be closer to the angular-split API. The question is how the primary-width and secondary-width properties should behave when they are defined and the splitter position changes: would you expect them to reflect the new value or would you instead rely on the getSplitterPosition value? Even if we don't consider Java getters, the question is still relevant for the web component's properties because they act as both setters and getters by default.
When the splitter is moved, I'd assume that the undefined size remains undefined and the defined size is updated to reflect the new size, because that's the one that determines the position of the splitter.
In Flow, getSplitterPosition is a getter for the value set with setSplitterPosition – if you don't set it, it's null and should remain so. SplitterDragEndEvent should be used to get the realtime position on the server side.
I'm not sure whether the Flow getPrimaryWidth / getSecondaryWidth methods should return the user-modified size of the defined-size element, but the undefined size element should return null until explicitly set through the setter.