web-components icon indicating copy to clipboard operation
web-components copied to clipboard

[combo-box] Dropdown width automatically based on item labels

Open prashant7526 opened this issue 5 years ago • 14 comments

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

prashant7526 avatar Jun 24 '20 15:06 prashant7526

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

rolfsmeds avatar Jun 30 '20 10:06 rolfsmeds

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?

prashant7526 avatar Jul 01 '20 01:07 prashant7526

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.

rolfsmeds avatar Jul 01 '20 12:07 rolfsmeds

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.

prashant7526 avatar Jul 08 '20 14:07 prashant7526

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

web-padawan avatar Jul 08 '20 15:07 web-padawan

Thank you guys, hoping the new one will come out soon.

prashant7526 avatar Jul 08 '20 17:07 prashant7526

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.

web-padawan avatar Jul 08 '20 18:07 web-padawan

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.

rolfsmeds avatar Jul 20 '20 08:07 rolfsmeds

Oh cool, will try to implement this one. Thanks for the suggestion.

prashant7526 avatar Jul 21 '20 11:07 prashant7526

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") 
      );
  }

nSharifA avatar Mar 30 '22 12:03 nSharifA

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

dominik42 avatar Aug 22 '24 13:08 dominik42

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.

stefanuebe avatar May 07 '25 08:05 stefanuebe

*edit: updated the workaround to also support MultiSelectComboBox

stefanuebe avatar May 07 '25 08:05 stefanuebe

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;
}

stefanuebe avatar May 07 '25 13:05 stefanuebe