TabSheet - select and close tab

Hello,
In the code snippet below (Vaadin 24), we need to include the option to close a tab and we also need to select a tab programmatically, but we found code examples on how to do this.
Can you help us?

	TabSheet ts = new TabSheet();

	VerticalLayout l1 = new VerticalLayout();
	l1.add(new H1("First Tab"));
	Tab tab1 = new Tab(l1);

	VerticalLayout l2 = new VerticalLayout();
	l2.add(new H1("Second Tab"));
	Tab tab2 = new Tab(l2);

	ts.add("Fisrt", tab1);
	ts.add("Second", tab2);

	ts.setSelectedTab(tab2); // does not work

There is a misunderstanding - you don’t have to wrap the Tab’s content in a Tab. Using add(String, Component) is meant to be used with your vertical layout directly. The add call also returns the created tab to be stored / used in other methods (like select)

Hello,

Thanks for your feedback.

The tab selection issue worked very well with the information you gave me.

Now, I need help on adding the option to close each tab. Can you give me a code example for this?

I was able to implement the tab close button successfully. This works fine with the Tabs component. Extend Vaadin’s Tab class. Create a wrapper layout and add (an icon,) the caption and the close button.

About the JS event listener with e.stopPropagation():
This prevents that the Tab triggers a click event when the close button is clicked. When the tab is also clicked, the tab is selected by the TabSheet and when you got a Grid within the tab layout the data would be loaded. But we want the tab to be closed and do not need to open the tab within this roundtrip.

Here is the code:

public class V8Tab extends Tab {

	public Span caption = new Span();
	private String captionText = null;
	public Component icon = null;
	public Button closeButton = new Button(VaadinIcon.CLOSE_SMALL.create());
	private Div wrapper = new Div();

	private boolean closable = false;
	private String key;
	private String iconAltText;
	private String url;
	
	public V8Tab(String key, String caption, Component icon) {
		this.key = key;
		setCaption(caption);
		setIcon(icon);

		build();
	}

	private void build() {

		closeButton.setVisible(isClosable());
		closeButton.getElement().setAttribute("part", "v8-tab-close");
		closeButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE,
				ButtonVariant.LUMO_CONTRAST);
		closeButton.addClickListener(remove -> {
			fireEvent(new TabCloseEvent(this, remove.isFromClient()));
		});
		closeButton.getElement().executeJs("this.addEventListener('click', e => e.stopPropagation())");

		if (icon != null) {
			wrapper.add(icon);
		}
		caption.setClassName("v8-tab-caption");
		wrapper.add(caption, closeButton);
		wrapper.setClassName("v8-tab-wrapper");
		add(wrapper);

	}

	public Registration addTabCloseListener(ComponentEventListener<TabCloseEvent> listener) {
		return addListener(TabCloseEvent.class, listener);
	}

	public boolean isClosable() {
		return closable;
	}

	public void setClosable(boolean closable) {
		this.closable = closable;
		setCloseButton();
	}

	private void setCloseButton() {
		closeButton.setVisible(isClosable());
	}

	public boolean isEnabled() {
		return super.isEnabled();
	}

	public void setEnabled(boolean enabled) {
		super.setEnabled(enabled);
	}

	public void setCaption(String text) {
		captionText = text;
		this.caption.setText(text);
	}

	public String getCaption() {
		return captionText;
	}

	public Component getIcon() {
		return icon;
	}

	public void setIcon(Component icon) {
		removeIcon();
		if (icon != null) {
			this.icon = icon;
			addIcon();
		}

	}

	private void addIcon() {
		if (icon != null) {
			icon.getElement().setAttribute("part", "v8-tab-icon");
			wrapper.addComponentAtIndex(0, this.icon);
		}
	}

	public void removeIcon() {
		if (this.icon != null) {
			wrapper.remove(this.icon);
			icon.getElement().removeAttribute("part");
		}
		this.icon = null;
	}

	public void setIcon(Component icon, String iconAltText) {
		setIcon(icon);
		setIconAlternateText(iconAltText);
	}

	public String getIconAlternateText() {
		return iconAltText;
	}

	public void setIconAlternateText(String iconAltText) {
		this.iconAltText = iconAltText;
	}

	public static class TabCloseEvent extends ComponentEvent<V8Tab> {
		private static final long serialVersionUID = 3268493040165192509L;

		public TabCloseEvent(V8Tab source, boolean fromClient) {
			super(source, fromClient);
		}

	}

Here how to add:

TabSheet ts = new TabSheet();

V8Tab tab1 = new V8Tab("somekey", "First", null);
tab1.addCloseListener(cEvent -> ts.remove(tab1));
ts.add(tab1, yourLayout);

I have this

/**
 * @author rubn
 */
@Log4j2
@Getter
public class TabIconCreator {

    private final TabSheet tabSheet;

    public TabIconCreator(TabSheet tabSheet) {
        this.tabSheet = tabSheet;
        this.tabSheet.addThemeVariants(TabSheetVariant.LUMO_TABS_SMALL);
    }

    public Component createTabIcon() {
        return createTabIconInternal("User");
    }

    public void createTabIconWithCustomTreeLetters(final String customName) {
        String tabName = (customName == null || customName.isEmpty())
                ? "User "
                : customName;

        Component rowIcons = createTabIconInternal(tabName);

        // Añadir la pestaña al TabSheet
        var currentIndextTab = tabSheet.getSelectedIndex();
        log.info("Counter internal {}", currentIndextTab);

        tabSheet.getTabAt(currentIndextTab).removeAll();
        tabSheet.getTabAt(currentIndextTab).add(rowIcons);
    }

    private Component createTabIconInternal(String name) {
        final Span spanName = new Span(name);
        spanName.getStyle().setCursor(CURSOR_POINTER);
        HorizontalLayout rowIcons = this.createRowCloseIcons(spanName);
        this.configurePopover(spanName, rowIcons);
        return rowIcons;
    }

    private void configurePopover(Span spanName, HorizontalLayout rowIcons) {
        final Popover popover = new Popover();
        popover.setCloseOnEsc(false);
        popover.setOpenOnClick(false);
        popover.setTarget(rowIcons);
        popover.setPosition(PopoverPosition.TOP);
        popover.setModal(true);
        popover.setBackdropVisible(true);
        popover.setCloseOnOutsideClick(true);
        popover.setAutofocus(true);

        final TextField textField = createTextField(spanName, popover);
        textField.setValueChangeMode(ValueChangeMode.LAZY);
        popover.add(textField);

        spanName.addDoubleClickListener(event -> {
            Mono.just(textField)
                    .delayElement(Duration.ofMillis(100))
                    .subscribe(t -> {
                        t.getUI().ifPresent(ui -> ui.access(() -> {
                            popover.open();
                            t.focus();
                            t.setValue(spanName.getText());
                        }));
                    });
        });

        Shortcuts.addShortcutListener(popover, event -> popover.close(), Key.ENTER);
        Shortcuts.addShortcutListener(popover, event -> popover.close(), Key.ESCAPE);
    }

    private TextField createTextField(Span spanName, Popover popover) {
        TextField textField = new TextField();
        textField.setValue(spanName.getText());
        textField.setClearButtonVisible(true);
        textField.setPrefixComponent(VaadinIcon.PENCIL.create());
        textField.setPlaceholder("Change name");
        textField.setValueChangeMode(ValueChangeMode.ON_BLUR);

        textField.addValueChangeListener(event -> {
            if (event.getValue() != null && !event.getValue().isEmpty()) {
                spanName.setText(event.getValue());
                Component newIcons = this.createRowCloseIcons(spanName);
                tabSheet.getTabAt(tabSheet.getSelectedIndex()).removeAll();
                tabSheet.getTabAt(tabSheet.getSelectedIndex()).add(newIcons);
                popover.setTarget(newIcons);
            } else {
                event.getSource().blur();
            }
        });

        textField.addBlurListener(event -> popover.close());
        return textField;
    }

    private HorizontalLayout createRowCloseIcons(Span spanUserName) {
        Span closeSpanCross = new Span();
        closeSpanCross.addClassName("span-close-tab");
        Tooltip.forComponent(closeSpanCross).setText("Close");
        final Icon icon = new Icon("lumo", "cross");
        icon.setSize("15px");
        closeSpanCross.add(icon);

        closeSpanCross.addClickListener(event -> {

            var confirm = ConfirmDialogBuilder.showConfirmInformation("Quieres borrar esta pestaña ?");
            confirm.addConfirmListener(confirmEvent -> {
                int selectedTab = tabSheet.getSelectedIndex();
                tabSheet.remove(selectedTab);
            });

        });

        Icon userIcon = VaadinIcon.USER.create();
        userIcon.setSize("15px");

        spanUserName.getStyle().set("margin-right", "8px");
        spanUserName.getStyle().set("margin-left", "3px");

        HorizontalLayout rowTabForIcons = new HorizontalLayout(userIcon, spanUserName, closeSpanCross);
        rowTabForIcons.setSpacing(false);
        return rowTabForIcons;
    }
}
 plusButton.addClickListener(event -> {
         final TabIconCreator tabIconCreator = new TabIconCreator(tabSheet);
         tabSheet.add(tabIconCreator.createTabIcon(), this.createContentLayout());
         tabSheet.setSelectedTab(tabSheet.getTabAt(tabSheet.getSelectedIndex() + 1));
});

This part allows you to close the tab

closeSpanCross.addClickListener(event -> {

   var confirm = ConfirmDialogBuilder.showConfirmInformation("Do you want to delete this tab ?");
        confirm.addConfirmListener(confirmEvent -> {
            int selectedTab = tabSheet.getSelectedIndex();
            tabSheet.remove(selectedTab);
         });
});

image

Hello,

I am using Vaadin version 24 and I have implemented it a little differently, as shown in the code below. After removing the tab, the processing icon stays on continuously and does not stop.

The same behavior occurs when I use the suggested V8Tab implementation.
if (forms.containsKey(formClass.getName())) {
tabs.setSelectedTab(tabs.getTab(forms.get(formClass.getName())));
} else {
clazz = classLoader.loadClass(formClass.getName());
BMFormBase form = (BMFormBase) clazz.getDeclaredConstructor().newInstance();
tabs.add(“”, form);
Button closeButton = new Button(VaadinIcon.CLOSE_SMALL.create());
closeButton.setVisible(true);
closeButton.addThemeVariants(ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_TERTIARY_INLINE,
ButtonVariant.LUMO_CONTRAST);
closeButton.addClickListener(remove → {
tabs.remove(tabs.getTab(form));
forms.remove(formClass.getName());
});
closeButton.getElement().executeJs(“this.addEventListener(‘click’, e => e.stopPropagation())”);
caption.setText(form.getTituloTab());
wrapper.add(caption, closeButton);
tabs.getTab(form).add(wrapper);
tabs.setSelectedTab(tabs.getTab(form));
forms.put(formClass.getName(), form);
}

The same thing happened to me as to you.

And in the end, it is easier to remove the tab.

But if you have several tabs, you get this loading with whichever one you delete.

tabSheet.getSelectedIndex() is very usefull…

I am looking, and the same thing happens to me when I delete the last tab of the TabSheet.