[combo-box] Dropdown width automatically based on item labels
Description Vaadin-combo-box by default wrap the drop-down-list. while It would be better to have Combobox like vaadin 8. This is irritating having a wrapped drop-down list.
Expected outcome Unwrap and automatically adjust, until not specifically instruct to wrap (Should work like vaadin 8 combo box).
Actual outcome
Live Demo
Steps to reproduce
Browsers Affected
- [x] Chrome
- [x] Firefox
- [x] Safari
- [x] Edge
- [x] IE 11
- [ ] iOS Safari
- [ ] Android Chrome
Just to check if we understood this correctly: You want the labels of the items in the combo box dropdown to not wrap, and for the dropdown's width to be automatically set according to the width of the widest item label, correct?
As a workaround, from V14.3 the dropdown's width can be set to a custom width: https://github.com/vaadin/vaadin-combo-box/issues/529
Yes, but what I was saying if I have a longer string (which we have like [company name, City, Postal Code] now how wide I have to create the Combobox, while in Vaadin 8 the drop-down was working great. I don't know what changed. Or just me feeling that way migrating from GWT component from vaadin 8 to web comp. of vaadin 14?
The Vaadin 8 combobox dropdown was split into discrete pages, which made auto-adjusting its width based on the longest item on the page easy. The V10+ combobox dropdown is a single scrolling page, however, which, combined with lazy loading items, makes it difficult if not impossible to automatically adjust the width based on the content. Would it be based on the first "page" of items, and maybe then still be too narrow when more items are loaded? Or would the dropdown width fluctuate as you scroll up/down?
But it's clear what you're asking for, so I suggest changing the issue title to be a bit more descriptive, e.g. "Dropdown width automatically based on item labels" or something like that.
So, will this combo-box will remain like this forever? I mean, it is just wired (not for me) but for my clients, they need more info to identify or pick a specific person/or his/her detail. For example in laymen language: If I am working with three different garages with the same name but in a different location, in that case, I am loading my full name, garage name, city, postal code. And in no way, it is going to be in one line (in V10+ Combobox). I don't know, I just gave you the real scenario where I am stuck.
There are technical reasons why the items behave like that mentioned at https://github.com/vaadin/vaadin-combo-box/issues/529#issuecomment-330756964
we are using iron-list, and that positions the items absolutely, so the width of the items do not affect the width of the overlay. So we would still have to measure the width of the items to be able to make the overlay adjust to it.
We might fix that in a similar way as we did for grid, by adding auto-width property: https://github.com/vaadin/vaadin-grid/pull/1595
Thank you guys, hoping the new one will come out soon.
I need to clarify that this is currently not in our short-term roadmap. We would need to do some prototyping first.
Compared to grid, there is an additional challenge related to the fact that items are only rendered after combo-box is opened.
For example in laymen language: If I am working with three different garages with the same name but in a different location, in that case, I am loading my full name, garage name, city, postal code. And in no way, it is going to be in one line...
In these types of cases I think it would make a lot of sense to use a custom renderer where the location is in a separate string below the garage name, see e.g. https://vaadin.com/components/vaadin-combo-box/java-examples/presentation. Also from 14.3 onward you can set a custom width for the dropdown using the --vaadin-combo-box-overlay-width css variable.
Oh cool, will try to implement this one. Thanks for the suggestion.
With all the item labels in your hand, you can do something like this:
private void calculateComboboxOverlayWidth(ComboBox cmb, List<String> labels) {
labels.stream()
.mapToInt(String::length)
.max()
.ifPresent(width ->
cmb.getElement().getStyle().set("--vaadin-combo-box-overlay-width", width + 5 + "ch")
);
}
The solution mentioned by @nSharifA works perfectly also for MultiSelectComboBox in Vaadin 24.
I have custom components extends com.vaadin.flow.component.combobox.MultiSelectComboBox<T>
Then, one can overwrite the setItems() like so:
public void setItems(List<MyDto> items) {
super.setItems(items);
List<String> labels = items.stream().map(MyDto::getLabel).toList();
calculateComboboxOverlayWidth(labels);
}
private void calculateComboboxOverlayWidth(List<String> labels) {
labels.stream()
.mapToInt(String::length)
.max()
.ifPresent(width ->
this.getElement().getStyle().set("--vaadin-multi-select-combo-box-overlay-width", width + 5 + "ch")
);
}
Pay attention for the CSS variable, it depends on your base component and is mentioned in the docs or you find the name by your Browser tools. In my case, these are the docs: https://vaadin.com/docs/latest/components/multi-select-combo-box/styling
Here is a workaround, that does not necessarily need to know all items beforehand, but updates the overlay width on the fly based on the rendered items. It supports the single and multi select combo box.
Due to the current nature of the cb design, it needs the following css setup:
:is(vaadin-combo-box-overlay, vaadin-multi-select-combo-box-overlay)::part(overlay) {
width: auto;
}
vaadin-combo-box-item,
vaadin-multi-select-combo-box-item {
width: auto !important;
white-space: nowrap;
}
and the following script executed on the combobox:
public static void activateRecalculateWidth(Component component) {
String itemTag, scrollerTag, overlayTag;
if (component instanceof ComboBox<?>) {
itemTag = "vaadin-combo-box-item";
scrollerTag = "vaadin-combo-box-scroller";
overlayTag = "vaadin-combo-box-overlay";
} else if(component instanceof MultiSelectComboBox<?>) {
itemTag = "vaadin-multi-select-combo-box-item";
scrollerTag = "vaadin-multi-select-combo-box-scroller";
overlayTag = "vaadin-multi-select-combo-box-overlay";
} else {
throw new IllegalArgumentException("Component " + component.getClassName() + " is not " +
" a supported component type");
}
component.getElement().executeJs("""
const recalculateWidth = () => {
const o = document.querySelector($2);
let cbItems = [...o.querySelectorAll($0)];
let newWidth = Math.max(...cbItems.map(i => {
let computedStyle = window.getComputedStyle(i);
return i.getBoundingClientRect().width + parseInt(computedStyle.paddingLeft) + parseInt(computedStyle.paddingRight)
})) + "px";
o.$.content.style.width = newWidth;
}
this.addEventListener("opened-changed", event => {
if(event.detail.value){
setTimeout(() => recalculateWidth(), 50);
}
const o = document.querySelector($2);
let scroller = o.querySelector($1);
if(!scroller.__recalculateWidthOnScroll){
scroller.addEventListener("scroll", event => {
if(!scroller.__recalculateWidthOnScrollInterval) {
scroller.__recalculateWidthOnScrollInterval = setInterval(() => {
recalculateWidth();
}, 25);
}
if(scroller.__recalculateWidthOnScrollTimeout) {
clearTimeout(scroller.__recalculateWidthOnScrollTimeout);
}
scroller.__recalculateWidthOnScrollTimeout = setTimeout(() => {
clearInterval(scroller.__recalculateWidthOnScrollInterval);
delete scroller.__recalculateWidthOnScrollInterval;
delete scroller.__recalculateWidthOnScrollTimeout;
}, 50);
});
scroller.__recalculateWidthOnScroll = true;
}
});
""", itemTag, scrollerTag, overlayTag);
}
// setup your fields
ComboBox<String> comboBox = new ComboBox<>("", items);
comboBox.setWidth("10rem");
MultiSelectComboBox<String> multiSelectComboBox = new MultiSelectComboBox<>("", items);
multiSelectComboBox.setWidth("10rem");
// apply recalc
activateRecalculateWidth(comboBox);
activateRecalculateWidth(multiSelectComboBox);
The script basically takes the items, calculates their current width and applies the max one to the overlay. That is done on opening the overlay and when scrolling it. The scrolling update is debounced to only be executed every 25ms, so that the client is not overwhelmed by events. Feel free to change the values accordingly, but my observation was, that these values work very well. The initial timeout should not be too small to let the browser update everything before getting the values.
Also note, that when you use any renderers or components, the contents must not break lines, otherwise the browser will prefer that before increasing the width.
Please leave me some feedback, if it works or if there are any issues.
*edit: updated the workaround to also support MultiSelectComboBox
To have some ease in the width transition, you can add this snippet to your css (and modify it as needed)
:is(vaadin-combo-box-overlay, vaadin-multi-select-combo-box-overlay)::part(content) {
transition: width 100ms ease-out;
}