All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.component.tabs.Tabs Maven / Gradle / Ivy

There is a newer version: 24.5.4
Show newest version
/*
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.vaadin.flow.component.tabs;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Stream;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.HasEnabled;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.Synchronize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.shared.HasThemeVariant;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.shared.Registration;

/**
 * Tabs are used to organize and group content into sections that the user can
 * navigate between. Use Tabs when you want to allow in-place navigation within
 * a certain part of the UI, instead of showing everything at once or forcing
 * the user to navigate between different views.
 * 

* {@link Tab} components can be added to this component with the * {@link #add(Tab...)} method or the {@link #Tabs(Tab...)} constructor. The Tab * components added to it can be selected with the * {@link #setSelectedIndex(int)} or {@link #setSelectedTab(Tab)} methods. The * first added {@link Tab} component will be automatically selected, firing a * {@link SelectedChangeEvent}, unless autoselection is explicitly disabled with * {@link #Tabs(boolean, Tab...)}, or {@link #setAutoselect(boolean)}. Removing * the selected tab from the component changes the selection to the next * available tab. *

* Note: Adding or removing Tab components via the Element API, * eg. {@code tabs.getElement().insertChild(0, tab.getElement()); }, doesn't * update the selected index, so it may cause the selected tab to change * unexpectedly. * * @author Vaadin Ltd. */ @Tag("vaadin-tabs") @NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.4.10") @JsModule("@vaadin/polymer-legacy-adapter/style-modules.js") @JsModule("@vaadin/tabs/src/vaadin-tabs.js") @NpmPackage(value = "@vaadin/tabs", version = "24.4.10") public class Tabs extends Component implements HasEnabled, HasSize, HasStyle, HasThemeVariant { private static final String SELECTED = "selected"; private transient Tab selectedTab; private boolean autoselect = true; /** * The valid orientations of {@link Tabs} instances. */ public enum Orientation { HORIZONTAL, VERTICAL } /** * Constructs an empty new object with {@link Orientation#HORIZONTAL * HORIZONTAL} orientation. */ public Tabs() { setSelectedIndex(-1); getElement().addPropertyChangeListener(SELECTED, event -> { int oldIndex = selectedTab != null ? indexOf(selectedTab) : -1; int newIndex = getSelectedIndex(); if (newIndex >= getComponentCount()) { LoggerFactory.getLogger(getClass()).warn(String.format( "The selected index is out of range: %d. Reverting to the previous index: %d.", newIndex, oldIndex)); setSelectedIndex(oldIndex); return; } updateSelectedTab(event.isUserOriginated()); }); } /** * Constructs a new object enclosing the given tabs, with * {@link Orientation#HORIZONTAL HORIZONTAL} orientation. *

* The first added {@link Tab} component will be automatically selected. Any * selection change listener added afterwards will not be notified about the * auto-selected tab. * * @param tabs * the tabs to enclose */ public Tabs(Tab... tabs) { this(); add(tabs); } /** * Constructs a new object enclosing the given autoselect option and tabs, * with {@link Orientation#HORIZONTAL HORIZONTAL} orientation. *

* Unless auto-select is disabled, the first added {@link Tab} component * will be automatically selected. Any selection change listener added * afterwards will not be notified about the auto-selected tab. * * @param autoselect * {@code true} to automatically select the first added tab, * {@code false} to leave tabs unselected * @param tabs * the tabs to enclose */ public Tabs(boolean autoselect, Tab... tabs) { this(); this.autoselect = autoselect; add(tabs); } /** * Adds the given tabs to the component. *

* The first added {@link Tab} component will be automatically selected, * unless auto-selection is explicitly disabled with * {@link #Tabs(boolean, Tab...)}, or {@link #setAutoselect(boolean)}. If a * selection change listener has been added before adding the tabs, it will * be notified with the auto-selected tab. * * @param components * the tabs to enclose * @deprecated since 24.0, use {@link #add(Tab...)} instead. */ @Deprecated public void add(Component... components) { Objects.requireNonNull(components, "Tabs should not be null"); boolean allComponentsAreTabs = Arrays.stream(components).map( tab -> Objects.requireNonNull(tab, "Tab to add cannot be null")) .allMatch(component -> component instanceof Tab); if (!allComponentsAreTabs) { throw new IllegalArgumentException( "Adding a component other than a Tab is not supported."); } Tab[] tabs = Arrays.copyOf(components, components.length, Tab[].class); add(tabs); } /** * Adds the given tabs to the component. *

* The first added {@link Tab} component will be automatically selected, * unless auto-selection is explicitly disabled with * {@link #Tabs(boolean, Tab...)}, or {@link #setAutoselect(boolean)}. If a * selection change listener has been added before adding the tabs, it will * be notified with the auto-selected tab. * * @param tabs * the tabs to enclose */ public void add(Tab... tabs) { Objects.requireNonNull(tabs, "Tabs should not be null"); boolean wasEmpty = getComponentCount() == 0; Arrays.stream(tabs).map( tab -> Objects.requireNonNull(tab, "Tab to add cannot be null")) .map(Tab::getElement).forEach(getElement()::appendChild); if (tabs.length == 0) { return; } if (wasEmpty && autoselect) { assert getSelectedIndex() == -1; setSelectedIndex(0); } else { updateSelectedTab(false); } } /** * Removes the given child tabs from this component. * * @param components * the tabs to remove * @throws IllegalArgumentException * if there is a tab whose non {@code null} parent is not this * component *

* Removing tabs before the selected tab will decrease the * {@link #getSelectedIndex() selected index} to avoid changing * the selected tab. Removing the selected tab will select the * next available tab if autoselect is true, otherwise no tab * will be selected. * @deprecated since 24.0, use {@link #remove(Tab...)} instead. */ @Deprecated public void remove(Component... components) { Objects.requireNonNull(components, "Tabs should not be null"); boolean allComponentsAreTabs = Arrays.stream(components) .map(tab -> Objects.requireNonNull(tab, "Tab to remove cannot be null")) .allMatch(component -> component instanceof Tab); if (!allComponentsAreTabs) { throw new IllegalArgumentException( "Adding a component other than a Tab is not supported."); } remove((Tab[]) components); } /** * Removes the given child tabs from this component. * * @param tabs * the tabs to remove * @throws IllegalArgumentException * if there is a tab whose non {@code null} parent is not this * component *

* Removing tabs before the selected tab will decrease the * {@link #getSelectedIndex() selected index} to avoid changing * the selected tab. Removing the selected tab will select the * next available tab if autoselect is true, otherwise no tab * will be selected. */ public void remove(Tab... tabs) { int selectedIndex = getSelectedIndex(); int lowerIndices = (int) Stream.of(tabs).map(this::indexOf) .filter(index -> index >= 0 && index < selectedIndex).count(); Tab selectedTab = getSelectedTab(); boolean isSelectedTab = selectedTab == null || Stream.of(tabs).anyMatch(selectedTab::equals); doRemoveTabs(tabs); // Prevents changing the selected tab int newSelectedIndex = getSelectedIndex() - lowerIndices; // In case the last tab was removed if (newSelectedIndex > 0 && newSelectedIndex >= getComponentCount()) { newSelectedIndex = getComponentCount() - 1; } if (getComponentCount() == 0 || (isSelectedTab && !isAutoselect())) { newSelectedIndex = -1; } if (newSelectedIndex != getSelectedIndex()) { setSelectedIndex(newSelectedIndex); } else { updateSelectedTab(false); } } private void doRemoveTabs(Tab... tabs) { List toRemove = new ArrayList<>(tabs.length); for (Tab tab : tabs) { Objects.requireNonNull(tab, "Tab to remove cannot be null"); Element parent = tab.getElement().getParent(); if (parent == null) { LoggerFactory.getLogger(getClass()) .debug("Remove of a tab with no parent does nothing."); continue; } if (getElement().equals(parent)) { toRemove.add(tab); } else { throw new IllegalArgumentException("The given tab (" + tab + ") is not a child of this tab"); } } toRemove.stream().map(Tab::getElement) .forEach(getElement()::removeChild); } /** * Removes all tabs from this component. It also removes the children that * were added only at the client-side. *

* This will reset the {@link #getSelectedIndex() selected index} to zero. */ public void removeAll() { getElement().removeAllChildren(); if (getSelectedIndex() > -1) { setSelectedIndex(-1); } else { updateSelectedTab(false); } } /** * Adds the given tab as child of this tab at the specific index. *

* In case the specified tab has already been added to another parent, it * will be removed from there and added to this one. * * @param index * the index, where the tab will be added. The index must be * non-negative and may not exceed the children count * @param component * the tab to add, value should not be null *

* Adding a tab before the currently selected tab will increment * the {@link #getSelectedIndex() selected index} to avoid * changing the selected tab. * @deprecated since 24.0, use {@link #addTabAtIndex(int, Tab)} instead. */ @Deprecated public void addComponentAtIndex(int index, Component component) { Objects.requireNonNull(component, "Tab should not be null"); if (!(component instanceof Tab)) { throw new IllegalArgumentException( "Adding a component other than a Tab is not supported."); } addTabAtIndex(index, (Tab) component); } /** * Adds the given tab as child of this tab at the specific index. *

* In case the specified tab has already been added to another parent, it * will be removed from there and added to this one. * * @param index * the index, where the tab will be added. The index must be * non-negative and may not exceed the children count * @param tab * the tab to add, value should not be null *

* Adding a tab before the currently selected tab will increment * the {@link #getSelectedIndex() selected index} to avoid * changing the selected tab. */ public void addTabAtIndex(int index, Tab tab) { Objects.requireNonNull(tab, "Tab should not be null"); if (index < 0) { throw new IllegalArgumentException( "Cannot add a tab with a negative index"); } // The case when the index is bigger than the children count is handled // inside the method below getElement().insertChild(index, tab.getElement()); if (autoselect && getChildren().count() == 1) { setSelectedIndex(0); } else if (index <= getSelectedIndex()) { // Prevents changing the selected tab setSelectedIndex(getSelectedIndex() + 1); } } /** * Replaces the tab in the container with another one without changing * position. This method replaces tab with another one is such way that the * new tab overtakes the position of the old tab. If the old tab is not in * the container, the new tab is added to the container. If the both tabs * are already in the container, their positions are swapped. Tab attach and * detach events should be taken care as with add and remove. * * @param oldComponent * the old tab that will be replaced. Can be null, * which will make the newTab to be added to the layout without * replacing any other * * @param newComponent * the new tab to be replaced. Can be null, which * will make the oldTab to be removed from the layout without * adding any other *

* Replacing the currently selected tab will make the new tab * selected. * @deprecated since 24.0, use {@link #replace(Tab, Tab)} instead. */ @Deprecated public void replace(Component oldComponent, Component newComponent) { if (oldComponent != null && !(oldComponent instanceof Tab)) { throw new IllegalArgumentException( "Removing a component other than a Tab is not supported."); } if (newComponent != null && !(newComponent instanceof Tab)) { throw new IllegalArgumentException( "Adding a component other than a Tab is not supported."); } replace((Tab) oldComponent, (Tab) newComponent); } /** * Replaces the tab in the container with another one without changing * position. This method replaces tab with another one is such way that the * new tab overtakes the position of the old tab. If the old tab is not in * the container, the new tab is added to the container. If the both tabs * are already in the container, their positions are swapped. Tab attach and * detach events should be taken care as with add and remove. * * @param oldTab * the old tab that will be replaced. Can be null, * which will make the newTab to be added to the layout without * replacing any other * * @param newTab * the new tab to be replaced. Can be null, which * will make the oldTab to be removed from the layout without * adding any other *

* Replacing the currently selected tab will make the new tab * selected. */ public void replace(Tab oldTab, Tab newTab) { if (oldTab == null && newTab == null) { // NO-OP } else if (oldTab == null) { add(newTab); } else if (newTab == null) { remove(oldTab); } else { doReplace(oldTab, newTab); } updateSelectedTab(false); } private void doReplace(Tab oldTab, Tab newTab) { Element element = getElement(); int oldIndex = element.indexOfChild(oldTab.getElement()); int newIndex = element.indexOfChild(newTab.getElement()); if (oldIndex >= 0 && newIndex >= 0) { element.insertChild(oldIndex, newTab.getElement()); element.insertChild(newIndex, oldTab.getElement()); } else if (oldIndex >= 0) { element.setChild(oldIndex, newTab.getElement()); } else { add(newTab); } } /** * An event to mark that the selected tab has changed. */ public static class SelectedChangeEvent extends ComponentEvent { private final Tab selectedTab; private final Tab previousTab; private final boolean initialSelection; /** * Creates a new selected change event. * * @param source * The tabs that fired the event. * @param previousTab * The previous selected tab. * @param fromClient * true for client-side events, * false otherwise. */ public SelectedChangeEvent(Tabs source, Tab previousTab, boolean fromClient) { super(source, fromClient); this.selectedTab = source.getSelectedTab(); this.initialSelection = source.isAutoselect() && previousTab == null && !fromClient; this.previousTab = previousTab; } /** * Get selected tab for this event. Can be {@code null} when autoselect * is set to false. * * @return the selected tab for this event */ public Tab getSelectedTab() { return this.selectedTab; } /** * Get previous selected tab for this event. Can be {@code null} when * autoselect is set to false. * * @return the selected tab for this event */ public Tab getPreviousTab() { return this.previousTab; } /** * Checks if this event is initial tabs selection. * * @return true if the event is initial tabs selection, * false otherwise */ public boolean isInitialSelection() { return this.initialSelection; } } @Override protected void onAttach(AttachEvent attachEvent) { getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse( this, context -> ui.getPage() .executeJs("$0.addEventListener('items-changed', " + "function(){ this.$server.updateSelectedTab(true); });", getElement()))); } /** * Adds a listener for {@link SelectedChangeEvent}. * * @param listener * the listener to add, not null * @return a handle that can be used for removing the listener */ public Registration addSelectedChangeListener( ComponentEventListener listener) { return addListener(SelectedChangeEvent.class, listener); } /** * Gets the zero-based index of the currently selected tab. * * @return the zero-based index of the selected tab, or -1 if none of the * tabs is selected */ @Synchronize(property = SELECTED, value = "selected-changed") public int getSelectedIndex() { return getElement().getProperty(SELECTED, -1); } /** * Selects a tab based on its zero-based index. * * @param selectedIndex * the zero-based index of the selected tab, -1 to unselect all */ public void setSelectedIndex(int selectedIndex) { getElement().setProperty(SELECTED, selectedIndex); } /** * Gets the currently selected tab. * * @return the selected tab, or {@code null} if none is selected */ public Tab getSelectedTab() { int selectedIndex = getSelectedIndex(); if (selectedIndex < 0) { return null; } return getTabAt(selectedIndex); } /** * Selects the given tab. * * @param selectedTab * the tab to select, {@code null} to unselect all * @throws IllegalArgumentException * if {@code selectedTab} is not a child of this component */ public void setSelectedTab(Tab selectedTab) { if (selectedTab == null) { setSelectedIndex(-1); return; } int selectedIndex = indexOf(selectedTab); if (selectedIndex < 0) { throw new IllegalArgumentException( "Tab to select must be a child: " + selectedTab); } setSelectedIndex(selectedIndex); } /** * Gets the orientation of this tab sheet. * * @return the orientation */ public Orientation getOrientation() { String orientation = getElement().getProperty("orientation"); if (orientation != null) { return Orientation.valueOf(orientation.toUpperCase(Locale.ROOT)); } return Orientation.HORIZONTAL; } /** * Sets the orientation of this tab sheet. * * @param orientation * the orientation */ public void setOrientation(Orientation orientation) { getElement().setProperty("orientation", orientation.name().toLowerCase(Locale.ENGLISH)); } /** * Sets the flex grow property of all enclosed tabs. The flex grow property * specifies what amount of the available space inside the layout the * component should take up, proportionally to the other components. *

* For example, if all components have a flex grow property value set to 1, * the remaining space in the layout will be distributed equally to all * components inside the layout. If you set a flex grow property of one * component to 2, that component will take twice the available space as the * other components, and so on. *

* Setting to flex grow property value 0 disables the expansion of the * component. Negative values are not allowed. * * @param flexGrow * the proportion of the available space the enclosed tabs should * take up * @throws IllegalArgumentException * if {@code flexGrow} is negative */ public void setFlexGrowForEnclosedTabs(double flexGrow) { if (flexGrow < 0) { throw new IllegalArgumentException( "Flex grow property must not be negative"); } getChildren().forEach(tab -> ((Tab) tab).setFlexGrow(flexGrow)); } /** * Specify that the tabs should be automatically selected. When autoselect * is false, no tab will be selected when the component load and it will not * select any others tab when removing currently selected tab. The default * value is true. * * @param autoselect * {@code true} to autoselect tab, {@code false} to not. */ public void setAutoselect(boolean autoselect) { this.autoselect = autoselect; } /** * Gets whether the tabs should be automatically selected. The default value * is true. * * @return true if autoselect is active, false * otherwise * @see #setAutoselect(boolean) */ public boolean isAutoselect() { return this.autoselect; } @ClientCallable private void updateSelectedTab(boolean changedFromClient) { if (getSelectedIndex() < -1) { setSelectedIndex(-1); return; } Tab currentlySelected = getSelectedTab(); Tab previousTab = selectedTab; if (Objects.equals(currentlySelected, selectedTab)) { return; } if (currentlySelected == null || currentlySelected.getElement().getNode().isEnabledSelf()) { selectedTab = currentlySelected; getChildren().filter(Tab.class::isInstance).map(Tab.class::cast) .forEach(tab -> tab.setSelected(false)); if (selectedTab != null) { selectedTab.setSelected(true); } fireEvent(new SelectedChangeEvent(this, previousTab, changedFromClient)); } else { updateEnabled(currentlySelected); setSelectedTab(selectedTab); } } private void updateEnabled(Tab tab) { boolean enabled = tab.getElement().getNode().isEnabledSelf(); Serializable rawValue = tab.getElement().getPropertyRaw("disabled"); if (rawValue instanceof Boolean) { // convert the boolean value to a String to force update the // property value. Otherwise since the provided value is the same as // the current one the update don't do anything. tab.getElement().setProperty("disabled", enabled ? null : Boolean.TRUE.toString()); } else { tab.setEnabled(enabled); } } /** * Returns the index of the given tab. * * @param component * the tab to look up, can not be null * @return the index of the tab or -1 if the tab is not a child * @deprecated since 24.0, use {@link #indexOf(Tab)} instead. */ @Deprecated public int indexOf(Component component) { Objects.requireNonNull(component, "Tab should not be null"); if (!(component instanceof Tab)) { throw new IllegalArgumentException( "Adding a component other than a Tab is not supported."); } return indexOf((Tab) component); } /** * Returns the index of the given tab. * * @param tab * the tab to look up, can not be null * @return the index of the tab or -1 if the tab is not a child */ public int indexOf(Tab tab) { if (tab == null) { throw new IllegalArgumentException( "The 'tab' parameter cannot be null"); } Iterator it = getChildren().sequential().iterator(); int index = 0; while (it.hasNext()) { Component next = it.next(); if (tab.equals(next)) { return index; } index++; } return -1; } /** * Gets the number of children tabs. * * @return the number of tabs */ public int getComponentCount() { return (int) getChildren().count(); } /** * Returns the tab at the given position. * * @param index * the position of the tab, must be greater than or equals to 0 * and less than the number of children tabs * @return The tab at the given index * @throws IllegalArgumentException * if the index is less than 0 or greater than or equals to the * number of children tabs * @deprecated since 24.0, use {@link #getTabAt(int)} instead. */ @Deprecated public Component getComponentAt(int index) { return getTabAt(index); } /** * Returns the tab at the given position. * * @param index * the position of the tab, must be greater than or equals to 0 * and less than the number of children tabs * @return The tab at the given index * @throws IllegalArgumentException * if the index is less than 0 or greater than or equals to the * number of children tabs */ public Tab getTabAt(int index) { if (index < 0) { throw new IllegalArgumentException( "The 'index' argument should be greater than or equal to 0. It was: " + index); } return (Tab) getChildren().sequential().skip(index).findFirst() .orElseThrow(() -> new IllegalArgumentException( "The 'index' argument should not be greater than or equals to the number of children tabs. It was: " + index)); } /** * Adds the given tab as the first child of this component. *

* In case the specified tab has already been added to another parent, it * will be removed from there and added to this one. * * @param component * the tab to add, value should not be null * @deprecated since 24.0, use {@link #addTabAsFirst(Tab)} instead. */ @Deprecated public void addComponentAsFirst(Component component) { Objects.requireNonNull(component, "Tab should not be null"); if (!(component instanceof Tab)) { throw new IllegalArgumentException( "Adding a component other than a Tab is not supported."); } addTabAsFirst((Tab) component); } /** * Adds the given tab as the first child of this component. *

* In case the specified tab has already been added to another parent, it * will be removed from there and added to this one. * * @param tab * the tab to add, value should not be null */ public void addTabAsFirst(Tab tab) { addTabAtIndex(0, tab); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy