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.6.0
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.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.HasOrderedComponents;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.Synchronize;
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. */ @SuppressWarnings("deprecation") public class Tabs extends GeneratedVaadinTabs implements HasOrderedComponents, HasSize { 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 tabs * the tabs to enclose */ public void add(Tab... tabs) { add((Component[]) tabs); } @Override public void add(Component... components) { boolean wasEmpty = getComponentCount() == 0; HasOrderedComponents.super.add(components); if (components.length == 0) { return; } if (wasEmpty && autoselect) { assert getSelectedIndex() == -1; setSelectedIndex(0); } else { updateSelectedTab(false); } } /** * {@inheritDoc} *

* Removing components 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. */ @Override public void remove(Component... components) { int lowerIndices = (int) Stream.of(components).map(this::indexOf) .filter(index -> index >= 0 && index < getSelectedIndex()) .count(); boolean isSelectedTab = Stream.of(components) .anyMatch(component -> component.equals(getSelectedTab())); HasOrderedComponents.super.remove(components); // 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); } } /** * {@inheritDoc} *

* This will reset the {@link #getSelectedIndex() selected index} to zero. */ @Override public void removeAll() { HasOrderedComponents.super.removeAll(); if (getSelectedIndex() > -1) { setSelectedIndex(-1); } else { updateSelectedTab(false); } } /** * {@inheritDoc} *

* Adding a component before the currently selected tab will increment the * {@link #getSelectedIndex() selected index} to avoid changing the selected * tab. */ @Override public void addComponentAtIndex(int index, Component component) { HasOrderedComponents.super.addComponentAtIndex(index, component); if (autoselect && getChildren().count() == 1) { setSelectedIndex(0); } else if (index <= getSelectedIndex()) { // Prevents changing the selected tab setSelectedIndex(getSelectedIndex() + 1); } } /** * {@inheritDoc} *

* Replacing the currently selected tab will make the new tab selected. */ @Override public void replace(Component oldComponent, Component newComponent) { HasOrderedComponents.super.replace(oldComponent, newComponent); updateSelectedTab(false); } /** * 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 fromClient * true for client-side events, * false otherwise. * * @deprecated use * {@link #SelectedChangeEvent(Tabs source, Tab previousTab, boolean fromClient)} * instead. */ @Deprecated public SelectedChangeEvent(Tabs source, boolean fromClient) { this(source, null, fromClient); } /** * 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; } Component selectedComponent = getComponentAt(selectedIndex); if (!(selectedComponent instanceof Tab)) { throw new IllegalStateException( "Illegal component inside Tabs: " + selectedComponent + "." + "Component should be an instance of Tab."); } return (Tab) selectedComponent; } /** * 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); } } /** * Adds theme variants to the component. * * @param variants * theme variants to add */ @Override public void addThemeVariants(TabsVariant... variants) { super.addThemeVariants(variants); } /** * Removes theme variants from the component. * * @param variants * theme variants to remove */ @Override public void removeThemeVariants(TabsVariant... variants) { super.removeThemeVariants(variants); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy