aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnna Koskinen <Ansku@users.noreply.github.com>2021-08-13 17:05:50 +0300
committerGitHub <noreply@github.com>2021-08-13 17:05:50 +0300
commit54230dfa058c49bbe9e825c77cf82d9c0a170c4c (patch)
tree5cf04c471d838d521fc07b7832cd697f9b1b2a0e
parent5dd5cdca20b5ece095057c852b3a40f01bc95298 (diff)
downloadvaadin-framework-54230dfa058c49bbe9e825c77cf82d9c0a170c4c.tar.gz
vaadin-framework-54230dfa058c49bbe9e825c77cf82d9c0a170c4c.zip
Reworked and cleaned up client-side TabSheet and Accordion. (#12357)
- Added and corrected JavaDocs. - Deprecated unused public methods. - Fixed first tab style logic in TabSheet. - Fixed navigation focus logic in TabSheet. - Fixed tab width bookkeeping for scrolling TabSheet tabs. - Renamed private methods and variables for clarity. - Removed unnecessary or duplicated private methods. - Reworked some logic to clarify it and to better match my understanding of what's supposed to happen within those methods. - Updated some deprecated method calls to use currently recommended solutions. - Added and updated regression tests.
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VAccordion.java220
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VTabsheet.java1206
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VTabsheetBase.java95
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VTabsheetPanel.java33
-rw-r--r--client/src/main/java/com/vaadin/client/ui/accordion/AccordionConnector.java1
-rw-r--r--client/src/main/java/com/vaadin/client/ui/tabsheet/TabsheetBaseConnector.java23
-rwxr-xr-xuitest/reference-screenshots/chrome/TabKeyboardNavigationTest-testFocus_ANY_Chrome__scrolled-right-to-tab-12.pngbin30308 -> 30612 bytes
-rwxr-xr-xuitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.pngbin18297 -> 18536 bytes
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResize.java20
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResize.java14
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResizeTest.java31
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResizeTest.java148
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/tabsheet/TabSheetFocusedTabTest.java46
13 files changed, 1462 insertions, 375 deletions
diff --git a/client/src/main/java/com/vaadin/client/ui/VAccordion.java b/client/src/main/java/com/vaadin/client/ui/VAccordion.java
index c60c542f3e..a9b992cf4c 100644
--- a/client/src/main/java/com/vaadin/client/ui/VAccordion.java
+++ b/client/src/main/java/com/vaadin/client/ui/VAccordion.java
@@ -32,14 +32,23 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.VCaption;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler;
+import com.vaadin.client.ui.VAccordion.StackItem;
import com.vaadin.shared.ComponentConstants;
import com.vaadin.shared.ui.accordion.AccordionState;
import com.vaadin.shared.ui.tabsheet.TabState;
import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc;
import com.vaadin.shared.util.SharedUtil;
+/**
+ * Widget class for the Accordion component. Displays one child item's contents
+ * at a time.
+ *
+ * @author Vaadin Ltd
+ *
+ */
public class VAccordion extends VTabsheetBase {
+ /** Default classname for this widget. */
public static final String CLASSNAME = AccordionState.PRIMARY_STYLE_NAME;
private Set<Widget> widgets = new HashSet<>();
@@ -53,16 +62,19 @@ public class VAccordion extends VTabsheetBase {
private int tabulatorIndex;
+ /**
+ * Constructs a widget for an Accordion.
+ */
public VAccordion() {
super(CLASSNAME);
touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
}
+ @SuppressWarnings("deprecation")
@Override
public void renderTab(TabState tabState, int index) {
StackItem item;
- int itemIndex;
if (getWidgetCount() <= index) {
// Create stackItem and render caption
@@ -70,12 +82,9 @@ public class VAccordion extends VTabsheetBase {
if (getWidgetCount() == 0) {
item.addStyleDependentName("first");
}
- itemIndex = getWidgetCount();
add(item, getElement());
} else {
item = getStackItem(index);
-
- itemIndex = index;
}
item.updateCaption(tabState);
@@ -103,6 +112,12 @@ public class VAccordion extends VTabsheetBase {
updateStyleNames(style);
}
+ /**
+ * Updates the primary style name base for all stack items.
+ *
+ * @param primaryStyleName
+ * the new primary style name base
+ */
protected void updateStyleNames(String primaryStyleName) {
for (Widget w : getChildren()) {
if (w instanceof StackItem) {
@@ -114,6 +129,12 @@ public class VAccordion extends VTabsheetBase {
/**
* For internal use only. May be renamed or removed in a future release.
+ * <p>
+ * Sets the tabulator index for the active stack item. The active stack item
+ * represents the entire accordion in the browser's focus cycle (excluding
+ * any focusable elements within the content panel).
+ * <p>
+ * This value is delegated from the TabsheetState via AccordionState.
*
* @param tabIndex
* tabulator index for the open stack item
@@ -127,7 +148,12 @@ public class VAccordion extends VTabsheetBase {
}
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param itemIndex
+ * the index of the stack item to open
+ */
public void open(int itemIndex) {
StackItem item = (StackItem) getWidget(itemIndex);
boolean alreadyOpen = false;
@@ -148,7 +174,12 @@ public class VAccordion extends VTabsheetBase {
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param item
+ * the stack item to close
+ */
public void close(StackItem item) {
if (!item.isOpen()) {
return;
@@ -160,6 +191,12 @@ public class VAccordion extends VTabsheetBase {
}
+ /**
+ * Handle stack item selection.
+ *
+ * @param item
+ * the selected stack item
+ */
public void onSelectTab(StackItem item) {
final int index = getWidgetIndex(item);
@@ -182,6 +219,13 @@ public class VAccordion extends VTabsheetBase {
private Widget widget;
private String id;
+ /**
+ * Sets the height for this stack item's contents.
+ *
+ * @param height
+ * the height to set (in pixels), or {@code -1} to remove
+ * height
+ */
public void setHeight(int height) {
if (height == -1) {
super.setHeight("");
@@ -193,6 +237,12 @@ public class VAccordion extends VTabsheetBase {
}
}
+ /**
+ * Sets the identifier for this stack item.
+ *
+ * @param newId
+ * the identifier to set
+ */
public void setId(String newId) {
if (!SharedUtil.equals(newId, id)) {
if (id != null) {
@@ -205,15 +255,23 @@ public class VAccordion extends VTabsheetBase {
}
}
+ /**
+ * Returns the wrapped widget of this stack item.
+ *
+ * @return the widget
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ * Use {@link #getChildWidget()} instead.
+ */
+ @Deprecated
public Widget getComponent() {
- return widget;
- }
-
- @Override
- public void setVisible(boolean visible) {
- super.setVisible(visible);
+ return getChildWidget();
}
+ /**
+ * Queries the height from the wrapped widget and uses it to set this
+ * stack item's height.
+ */
public void setHeightFromWidget() {
Widget widget = getChildWidget();
if (widget == null) {
@@ -228,7 +286,8 @@ public class VAccordion extends VTabsheetBase {
/**
* Returns caption width including padding.
*
- * @return
+ * @return the width of the caption (in pixels), or zero if there is no
+ * caption element (not possible via the default implementation)
*/
public int getCaptionWidth() {
if (caption == null) {
@@ -241,6 +300,14 @@ public class VAccordion extends VTabsheetBase {
return captionWidth + padding;
}
+ /**
+ * Sets the width of the stack item, or removes it if given value is
+ * {@code -1}.
+ *
+ * @param width
+ * the width to set (in pixels), or {@code -1} to remove
+ * width
+ */
public void setWidth(int width) {
if (width == -1) {
super.setWidth("");
@@ -249,10 +316,24 @@ public class VAccordion extends VTabsheetBase {
}
}
+ /**
+ * Returns the offset height of this stack item.
+ *
+ * @return the height in pixels
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ * Use {@link #getOffsetHeight()} instead.
+ */
+ @Deprecated
public int getHeight() {
return getOffsetHeight();
}
+ /**
+ * Returns the offset height of the caption node.
+ *
+ * @return the height in pixels
+ */
public int getCaptionHeight() {
return captionNode.getOffsetHeight();
}
@@ -263,6 +344,11 @@ public class VAccordion extends VTabsheetBase {
private Element captionNode = DOM.createDiv();
private String styleName;
+ /**
+ * Constructs a stack item. The content widget should be set later when
+ * the stack item is opened.
+ */
+ @SuppressWarnings("deprecation")
public StackItem() {
setElement(DOM.createDiv());
caption = new VCaption(client);
@@ -295,14 +381,31 @@ public class VAccordion extends VTabsheetBase {
onSelectTab(this);
}
+ /**
+ * Returns the container element for the content widget.
+ *
+ * @return the content container element
+ */
+ @SuppressWarnings("deprecation")
public com.google.gwt.user.client.Element getContainerElement() {
return DOM.asOld(content);
}
+ /**
+ * Returns the wrapped widget of this stack item.
+ *
+ * @return the widget
+ */
public Widget getChildWidget() {
return widget;
}
+ /**
+ * Replaces the existing wrapped widget (if any) with a new widget.
+ *
+ * @param newWidget
+ * the new widget to wrap
+ */
public void replaceWidget(Widget newWidget) {
if (widget != null) {
widgets.remove(widget);
@@ -318,6 +421,9 @@ public class VAccordion extends VTabsheetBase {
}
+ /**
+ * Opens the stack item and clears any previous visibility settings.
+ */
public void open() {
add(widget, content);
open = true;
@@ -328,10 +434,20 @@ public class VAccordion extends VTabsheetBase {
getElement().setTabIndex(tabulatorIndex);
}
+ /**
+ * Hides the stack item content but does not close the stack item.
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ */
+ @Deprecated
public void hide() {
content.getStyle().setVisibility(Visibility.HIDDEN);
}
+ /**
+ * Closes this stack item and removes the wrapped widget from the DOM
+ * tree and this stack item.
+ */
public void close() {
if (widget != null) {
remove(widget);
@@ -346,6 +462,11 @@ public class VAccordion extends VTabsheetBase {
getElement().setTabIndex(-1);
}
+ /**
+ * Returns whether this stack item is open or not.
+ *
+ * @return {@code true} if open, {@code false} otherwise
+ */
public boolean isOpen() {
return open;
}
@@ -377,8 +498,17 @@ public class VAccordion extends VTabsheetBase {
onSelectTab(this);
}
+ /**
+ * Updates the caption to match the current tab state.
+ *
+ * @param tabState
+ * the state for this stack item
+ */
+ @SuppressWarnings("deprecation")
public void updateCaption(TabState tabState) {
- // TODO need to call this because the caption does not have an owner
+ // Need to call this because the caption does not have an owner, and
+ // cannot have an owner, because only the selected stack item's
+ // connector is sent to the client.
caption.setCaptionAsHtml(isTabCaptionsAsHtml());
caption.updateCaptionWithoutOwner(tabState.caption,
!tabState.enabled, hasAttribute(tabState.description),
@@ -394,10 +524,10 @@ public class VAccordion extends VTabsheetBase {
}
/**
- * Updates a tabs stylename from the child UIDL
+ * Updates the stack item's style name from the TabState.
*
- * @param uidl
- * The child UIDL of the tab
+ * @param newStyleName
+ * the new style name
*/
private void updateTabStyleName(String newStyleName) {
if (newStyleName != null && !newStyleName.isEmpty()) {
@@ -419,20 +549,55 @@ public class VAccordion extends VTabsheetBase {
}
}
+ /**
+ * Returns the offset width of the wrapped widget.
+ *
+ * @return the offset width in pixels, or zero if no widget is set
+ */
public int getWidgetWidth() {
- return DOM.getFirstChild(content).getOffsetWidth();
+ if (widget == null) {
+ return 0;
+ }
+ return widget.getOffsetWidth();
}
+ /**
+ * Returns whether the given container's widget is this stack item's
+ * wrapped widget. Does not check whether the given container's widget
+ * is a child of the wrapped widget.
+ *
+ * @param p
+ * the container whose widget to set
+ * @return {@code true} if the container's widget matches wrapped
+ * widget, {@code false} otherwise
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ */
+ @Deprecated
public boolean contains(ComponentConnector p) {
return (getChildWidget() == p.getWidget());
}
+ /**
+ * Returns whether the caption element is visible or not.
+ *
+ * @return {@code true} if visible, {@code false} otherwise
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ */
+ @Deprecated
public boolean isCaptionVisible() {
return caption.isVisible();
}
}
+ /**
+ * {@inheritDoc}
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ */
+ @Deprecated
@Override
protected void clearPaintables() {
clear();
@@ -474,15 +639,32 @@ public class VAccordion extends VTabsheetBase {
return null;
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param index
+ * the index of the stack item to get
+ * @return the stack item
+ */
public StackItem getStackItem(int index) {
return (StackItem) getWidget(index);
}
+ /**
+ * Returns an iterable over all the stack items.
+ *
+ * @return the iterable
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
public Iterable<StackItem> getStackItems() {
return (Iterable) getChildren();
}
+ /**
+ * Returns the currently open stack item.
+ *
+ * @return the open stack item, or {@code null} if one does not exist
+ */
public StackItem getOpenStackItem() {
return openTab;
}
diff --git a/client/src/main/java/com/vaadin/client/ui/VTabsheet.java b/client/src/main/java/com/vaadin/client/ui/VTabsheet.java
index 59084397cf..e994623d99 100644
--- a/client/src/main/java/com/vaadin/client/ui/VTabsheet.java
+++ b/client/src/main/java/com/vaadin/client/ui/VTabsheet.java
@@ -34,7 +34,6 @@ import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.dom.client.TableCellElement;
-import com.google.gwt.dom.client.TableElement;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -79,31 +78,60 @@ import com.vaadin.shared.ui.tabsheet.TabState;
import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc;
import com.vaadin.shared.ui.tabsheet.TabsheetState;
+/**
+ * Widget class for the TabSheet component. Displays one child item's contents
+ * at a time.
+ *
+ * @author Vaadin Ltd
+ *
+ */
public class VTabsheet extends VTabsheetBase
implements Focusable, SubPartAware {
private static final String PREV_SCROLLER_DISABLED_CLASSNAME = "Prev-disabled";
+ /**
+ * Event class for tab closing requests.
+ */
private static class VCloseEvent {
private Tab tab;
+ /**
+ * Construct a tab closing request event.
+ *
+ * @param tab
+ * the tab to close
+ */
VCloseEvent(Tab tab) {
this.tab = tab;
}
+ /**
+ * Returns the tab whose closing has been requested.
+ *
+ * @return the tab to close
+ */
public Tab getTab() {
return tab;
}
}
+ /**
+ * Handler interface for dealing with tab closing requests.
+ */
private interface VCloseHandler {
+ /**
+ * Handle a tab closing request.
+ *
+ * @param event
+ * the close event
+ */
public void onClose(VCloseEvent event);
}
/**
- * Representation of a single "tab" shown in the TabBar.
- *
+ * Representation of a single "tab" shown in the {@link TabBar}.
*/
public static class Tab extends SimplePanel implements HasFocusHandlers,
HasBlurHandlers, HasMouseDownHandlers, HasKeyDownHandlers {
@@ -162,25 +190,54 @@ public class VTabsheet extends VTabsheetBase
Id.of(tabCaption.getElement()));
}
+ /**
+ * Returns whether the tab is hidden on server (as opposed to simply
+ * hidden because it's scrolled out of view).
+ *
+ * @return {@code true} if hidden on server, {@code false} otherwise
+ */
public boolean isHiddenOnServer() {
return hiddenOnServer;
}
+ /**
+ * Set tab hidden state on server (as opposed to simply hidden because
+ * it's scrolled out of view).
+ *
+ * @param hiddenOnServer
+ * {@code true} if hidden on server, {@code false} otherwise
+ */
public void setHiddenOnServer(boolean hiddenOnServer) {
this.hiddenOnServer = hiddenOnServer;
Roles.getTabRole().setAriaHiddenState(getElement(), hiddenOnServer);
}
+ @SuppressWarnings("deprecation")
@Override
protected com.google.gwt.user.client.Element getContainerElement() {
// Attach caption element to div, not td
return DOM.asOld(div);
}
+ /**
+ * Returns whether the tab is enabled on server (there is no client-side
+ * disabling, but the naming convention matches
+ * {@link #isHiddenOnServer()}).
+ *
+ * @return {@code true} if enabled on server, {@code false} otherwise
+ */
public boolean isEnabledOnServer() {
return enabledOnServer;
}
+ /**
+ * Set tab enabled state on server (there is no client-side disabling,
+ * but the naming convention matches
+ * {@link #setHiddenOnServer(boolean)}).
+ *
+ * @param enabled
+ * {@code true} if enabled on server, {@code false} otherwise
+ */
public void setEnabledOnServer(boolean enabled) {
enabledOnServer = enabled;
Roles.getTabRole().setAriaDisabledState(getElement(), !enabled);
@@ -191,10 +248,26 @@ public class VTabsheet extends VTabsheetBase
}
}
+ /**
+ * Adds a {@link ClickEvent} handler to the tab caption.
+ *
+ * @param handler
+ * the click handler
+ */
public void addClickHandler(ClickHandler handler) {
tabCaption.addClickHandler(handler);
}
+ /**
+ * Sets the close handler for this tab. This handler should be called
+ * whenever closing of a tab is requested (by clicking the close button
+ * or pressing the close key).
+ *
+ * @param closeHandler
+ * the close handler
+ *
+ * @see VTabsheet#getCloseTabKey()
+ */
public void setCloseHandler(VCloseHandler closeHandler) {
this.closeHandler = closeHandler;
}
@@ -203,14 +276,32 @@ public class VTabsheet extends VTabsheetBase
* Toggles the style names for the Tab.
*
* @param selected
- * true if the Tab is selected
+ * {@code true} if the Tab is selected, {@code false}
+ * otherwise
* @param first
- * true if the Tab is the first visible Tab
+ * {@code true} if the Tab is the first visible Tab,
+ * {@code false} otherwise
*/
public void setStyleNames(boolean selected, boolean first) {
setStyleNames(selected, first, false);
}
+ /**
+ * Sets the style names for this tab according to the given parameters.
+ *
+ * @param selected
+ * {@code true} if the tab is selected, {@code false}
+ * otherwise
+ * @param first
+ * {@code true} if the tab is the first one from the left,
+ * {@code false} otherwise
+ * @param keyboardFocus
+ * {@code true} if the tab should display keyboard navigation
+ * focus styles, {@code false} otherwise -- the focus style
+ * name is used by the compatibility themes like
+ * {@code reindeer} ({@code valo} relies on {@code :focus}
+ * pseudo-class)
+ */
public void setStyleNames(boolean selected, boolean first,
boolean keyboardFocus) {
setStyleName(td, TD_FIRST_CLASSNAME, first);
@@ -222,18 +313,55 @@ public class VTabsheet extends VTabsheetBase
setStyleName(div, DIV_FOCUS_CLASSNAME, keyboardFocus);
}
+ /**
+ * Sets the index that represents the tab's position in the browser's
+ * focus cycle. Negative index means that this tab element is not
+ * reachable via tabulator navigation.
+ * <p>
+ * By default only the selected tab has a non-negative tabulator index,
+ * and represents the entire tab sheet. If there are any other navigable
+ * tabs in the same tab sheet those can be navigated into with
+ * next/previous buttons, which does not update the selection until
+ * confirmed with a selection key press.
+ *
+ * @param tabIndex
+ * the tabulator index
+ *
+ * @see VTabsheet#getNextTabKey()
+ * @see VTabsheet#getPreviousTabKey()
+ * @see VTabsheet#getSelectTabKey()
+ */
public void setTabulatorIndex(int tabIndex) {
getElement().setTabIndex(tabIndex);
}
+ /**
+ * Returns whether the tab can be closed or not.
+ *
+ * @return {@code true} if the tab is closable, {@code false} otherwise
+ *
+ * @see TabCaption#setClosable(boolean)
+ */
public boolean isClosable() {
return tabCaption.isClosable();
}
+ /**
+ * Handles a request to close this tab. Closability should be checked
+ * before calling this method. The close request will be delivered to
+ * the server, where the actual closing is handled.
+ *
+ * @see #isClosable()
+ */
public void onClose() {
closeHandler.onClose(new VCloseEvent(this));
}
+ /**
+ * Returns the tab sheet instance where this tab is attached to.
+ *
+ * @return the current tab sheet
+ */
public VTabsheet getTabsheet() {
return tabBar.getTabsheet();
}
@@ -272,10 +400,63 @@ public class VTabsheet extends VTabsheetBase
}
}
+ /**
+ * Recalculates the required caption width and sets it as the new width.
+ * Also updates the tab width bookkeeping of the tab bar if needed. The
+ * default implementation for the bookkeeping logic attempts to account
+ * for different margins and paddings in the first tab element and its
+ * caption element versus the same values in the next visible tab.
+ */
public void recalculateCaptionWidth() {
+ boolean visible = isVisible();
+ boolean first = td.hasClassName(Tab.TD_FIRST_CLASSNAME);
+ if (visible && !tabBar.firstAdjusted) {
+ if (first) {
+ tabBar.pendingTab = this;
+ } else if (tabBar.pendingTab != null) {
+ // the first visible tab usually has different styling than
+ // the rest, compare the styles against the second visible
+ // tab in order to adjust the saved width for the first tab
+ ComputedStyle tabStyle = new ComputedStyle(getElement());
+ ComputedStyle captionStyle = new ComputedStyle(
+ tabCaption.getElement());
+ ComputedStyle pendingTabStyle = new ComputedStyle(
+ tabBar.pendingTab.getElement());
+ ComputedStyle pendingCaptionStyle = new ComputedStyle(
+ tabBar.pendingTab.tabCaption.getElement());
+ double tabPadding = tabStyle.getPaddingWidth();
+ double tabMargin = tabStyle.getMarginWidth();
+ double captionPadding = captionStyle.getPaddingWidth();
+ double captionMargin = captionStyle.getMarginWidth();
+ double pendingTabPadding = pendingTabStyle
+ .getPaddingWidth();
+ double pendingTabMargin = pendingTabStyle.getMarginWidth();
+ double pendingCaptionPadding = pendingCaptionStyle
+ .getPaddingWidth();
+ double pendingCaptionMargin = pendingCaptionStyle
+ .getMarginWidth();
+ // update the adjuster
+ tabBar.firstTabWidthAdjuster = (int) Math.ceil(tabPadding
+ + tabMargin + captionPadding + captionMargin
+ - pendingTabPadding - pendingTabMargin
+ - pendingCaptionPadding - pendingCaptionMargin);
+ // update the pending tab
+ tabBar.tabWidths.put(tabBar.pendingTab,
+ tabBar.pendingTab.getOffsetWidth()
+ + tabBar.firstTabWidthAdjuster);
+ // mark adjusting done
+ tabBar.firstAdjusted = true;
+ tabBar.pendingTab = null;
+ }
+ }
tabCaption.setWidth(tabCaption.getRequiredWidth() + "px");
- if (isVisible()) {
- tabBar.tabWidths.put(this, getOffsetWidth());
+ if (visible) {
+ if (first) {
+ tabBar.tabWidths.put(this,
+ getOffsetWidth() + tabBar.firstTabWidthAdjuster);
+ } else {
+ tabBar.tabWidths.put(this, getOffsetWidth());
+ }
}
}
@@ -300,39 +481,73 @@ public class VTabsheet extends VTabsheetBase
return addDomHandler(handler, KeyDownEvent.getType());
}
+ /**
+ * Scrolls the tab into view and focuses it.
+ */
public void focus() {
getTabsheet().scrollIntoView(this);
FOCUS_IMPL.focus(td);
}
+ /**
+ * Removes focus from the tab.
+ */
public void blur() {
FOCUS_IMPL.blur(td);
}
+ /**
+ * Returns whether the tab caption has a configured tooltip or not.
+ *
+ * @return {@code true} if the tab caption has a tooltip, {@code false}
+ * otherwise
+ */
public boolean hasTooltip() {
return tabCaption.getTooltipInfo() != null;
}
+ /**
+ * Returns the tab caption's tooltip info if it has been configured.
+ *
+ * @return the tooltip info, or {@code null} if no tooltip configuration
+ * found
+ */
public TooltipInfo getTooltipInfo() {
return tabCaption.getTooltipInfo();
}
+ /**
+ * Sets the {@code aria-describedby} attribute for this tab element to
+ * the referenced id. This should be called when this tab receives focus
+ * and has a tooltip configured.
+ *
+ * @param descriptionId
+ * the unique id of the tooltip element
+ */
public void setAssistiveDescription(String descriptionId) {
Roles.getTablistRole().setAriaDescribedbyProperty(getElement(),
Id.of(descriptionId));
}
+ /**
+ * Removes the {@code aria-describedby} attribute from this tab element.
+ * This should be called when this tab loses focus.
+ */
public void removeAssistiveDescription() {
Roles.getTablistRole().removeAriaDescribedbyProperty(getElement());
}
}
+ /**
+ * Caption implementation for a {@link Tab}.
+ */
public static class TabCaption extends VCaption {
private boolean closable = false;
private Element closeButton;
private Tab tab;
+ @SuppressWarnings("deprecation")
TabCaption(Tab tab) {
super(tab.getTabsheet().connector.getConnection());
this.tab = tab;
@@ -351,10 +566,12 @@ public class VTabsheet extends VTabsheetBase
setTooltipInfo(null);
}
- // TODO need to call this instead of super because the caption does
- // not have an owner
+ // Need to call this because the caption does not have an owner, and
+ // cannot have an owner, because only the selected tab's connector
+ // is sent to the client.
String captionString = tabState.caption.isEmpty() ? null
: tabState.caption;
+ @SuppressWarnings("deprecation")
boolean ret = updateCaptionWithoutOwner(captionString,
!tabState.enabled, hasAttribute(tabState.description),
hasAttribute(tabState.componentError),
@@ -392,10 +609,23 @@ public class VTabsheet extends VTabsheetBase
}
}
+ /**
+ * Returns the tab this caption belongs to.
+ *
+ * @return the corresponding tab
+ */
public Tab getTab() {
return tab;
}
+ /**
+ * Adds or removes the button for closing the corresponding tab and the
+ * style name for a closable tab.
+ *
+ * @param closable
+ * {@code true} if the tab is closable, {@code false}
+ * otherwise
+ */
public void setClosable(boolean closable) {
this.closable = closable;
if (closable && closeButton == null) {
@@ -419,6 +649,11 @@ public class VTabsheet extends VTabsheetBase
}
}
+ /**
+ * Returns whether the corresponding tab is closable or not.
+ *
+ * @return {@code true} if the tab is closable, {@code false} otherwise
+ */
public boolean isClosable() {
return closable;
}
@@ -432,25 +667,64 @@ public class VTabsheet extends VTabsheetBase
return width;
}
+ /**
+ * Returns the close button if one exists.
+ *
+ * @return the close button, or {@code null} if not found
+ */
+ @SuppressWarnings("deprecation")
public com.google.gwt.user.client.Element getCloseButton() {
return DOM.asOld(closeButton);
}
}
+ /**
+ * Container widget that houses all {@link Tab} widgets of a single tab
+ * sheet. Only one tab can be selected at the same time, and the selected
+ * tab's assigned component is displayed within a {@link VTabsheetPanel}
+ * (outside of this tab bar).
+ * <p>
+ * If there are more tabs that can fit to be visible at the same time, those
+ * 'scrolled' out of view to the left are set temporarily hidden, although
+ * the elements are still in the DOM tree with {@code display: none;}. The
+ * excess tabs to the right don't get the same explicit hiding and are
+ * simply not shown because of {@code overflow: hidden;} in the tab
+ * container element (parent of this tab bar).
+ */
static class TabBar extends ComplexPanel implements VCloseHandler {
private final Element tr = DOM.createTR();
+ /**
+ * Spacer element for filling the gap to the right from the tabs and/or
+ * for reserving room for the scroller. By default hidden by Valo theme.
+ */
private final Element spacerTd = DOM.createTD();
private Tab selected;
private VTabsheet tabsheet;
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ * <p>
+ * Map for saving the closest approximation for how much width each of
+ * these tabs would add to this tab bar when made visible. The first
+ * visible tab usually has different styling, but as these values are
+ * only used in scrolling, there should always be a tab with those
+ * styles in view already. Therefore the width to save should
+ * approximate the width when the tab is not the first one.
+ */
private Map<Tab, Integer> tabWidths = new HashMap<Tab, Integer>();
+ /** Adjuster for countering the different styling for the first tab. */
+ private int firstTabWidthAdjuster = 0;
+ /** Has the first tab's different styling been adjusted. */
+ private boolean firstAdjusted = false;
+ /** First visible tab that is pending for saved width adjustment. */
+ private Tab pendingTab = null;
+
TabBar(VTabsheet tabsheet) {
this.tabsheet = tabsheet;
@@ -477,6 +751,7 @@ public class VTabsheet extends VTabsheetBase
getTabsheet().sendTabClosedEvent(tabIndex);
}
+ @SuppressWarnings("deprecation")
protected com.google.gwt.user.client.Element getContainerElement() {
return DOM.asOld(tr);
}
@@ -510,6 +785,11 @@ public class VTabsheet extends VTabsheetBase
getTabsheet().selectionHandler.registerTab(t);
t.setCloseHandler(this);
+
+ // Save the size that is expected to be needed if this tab is
+ // scrolled back to view after getting temporarily hidden. The tab
+ // hasn't been initialized from tab state yet so this value is a
+ // placeholder.
tabWidths.put(t, t.getOffsetWidth());
return t;
@@ -555,6 +835,21 @@ public class VTabsheet extends VTabsheetBase
return -1;
}
+ /**
+ * Selects the indicated tab, deselects the previously selected tab, and
+ * updates the style names, tabulator indices, and the
+ * {@code aria-selected} roles to match. Also recalculates the tab
+ * caption widths in case the addition or removal of the selection style
+ * changed them, and schedules a scroll for moving the newly selected
+ * tab into view (at the end of the event loop to allow for layouting).
+ * If the previously selected item is the same as the new one, nothing
+ * is done.
+ *
+ * @param index
+ * the index of the tab to select
+ *
+ * @see Tab#setTabulatorIndex(int)
+ */
public void selectTab(int index) {
final Tab newSelected = getTab(index);
final Tab oldSelected = selected;
@@ -562,18 +857,22 @@ public class VTabsheet extends VTabsheetBase
return;
}
- newSelected.setStyleNames(true, isFirstVisibleTab(index), true);
+ newSelected.setStyleNames(true, isFirstVisibleTabClient(index),
+ true);
newSelected.setTabulatorIndex(getTabsheet().tabulatorIndex);
Roles.getTabRole().setAriaSelectedState(newSelected.getElement(),
SelectedValue.TRUE);
- if (oldSelected != null && oldSelected != newSelected) {
+ if (oldSelected != null) {
oldSelected.setStyleNames(false,
- isFirstVisibleTab(getWidgetIndex(oldSelected)));
+ isFirstVisibleTabClient(getWidgetIndex(oldSelected)));
oldSelected.setTabulatorIndex(-1);
Roles.getTabRole().setAriaSelectedState(
oldSelected.getElement(), SelectedValue.FALSE);
+
+ // The unselected tab might need less (or more) space
+ oldSelected.recalculateCaptionWidth();
}
// Update the field holding the currently selected tab
@@ -581,13 +880,30 @@ public class VTabsheet extends VTabsheetBase
// The selected tab might need more (or less) space
newSelected.recalculateCaptionWidth();
- getTab(tabsheet.activeTabIndex).recalculateCaptionWidth();
// Scroll the tab into view if it is not already, after layout
Scheduler.get().scheduleFinally(() -> getTabsheet()
.scrollIntoView(getTab(tabsheet.activeTabIndex)));
}
+ /**
+ * Updates tab focus styles when navigating from one tab to another.
+ * <p>
+ * This method should be called when there is either a mouse click at
+ * the new tab (which should also trigger selection) or a next/previous
+ * key navigation event (which should not, unless confirmed with
+ * selection key).
+ *
+ * @param fromIndex
+ * the index of the previously selected tab
+ * @param toIndex
+ * the index of the tab that is getting navigated into
+ * @return the tab that gets navigated to
+ *
+ * @see VTabsheet#getNextTabKey()
+ * @see VTabsheet#getPreviousTabKey()
+ * @see VTabsheet#getSelectTabKey()
+ */
public Tab navigateTab(int fromIndex, int toIndex) {
Tab newNavigated = getTab(toIndex);
if (newNavigated == null) {
@@ -597,16 +913,29 @@ public class VTabsheet extends VTabsheetBase
Tab oldNavigated = getTab(fromIndex);
newNavigated.setStyleNames(newNavigated.equals(selected),
- isFirstVisibleTab(toIndex), true);
+ isFirstVisibleTabClient(toIndex), true);
if (oldNavigated != null && fromIndex != toIndex) {
oldNavigated.setStyleNames(oldNavigated.equals(selected),
- isFirstVisibleTab(fromIndex), false);
+ isFirstVisibleTabClient(fromIndex), false);
}
return newNavigated;
}
+ /**
+ * Removes a tab from this tab bar and updates the scroll position if
+ * needed. If there is no tab that corresponds with the given index,
+ * nothing is done.
+ * <p>
+ * Tab removal should always get triggered via the connector, even when
+ * a tab's close button is clicked. That ensures that the states stay in
+ * sync, and that logic such as selection change forced by tab removal
+ * only needs to be implemented once.
+ *
+ * @param i
+ * the index of the tab to remove
+ */
public void removeTab(int i) {
Tab tab = getTab(i);
if (tab == null) {
@@ -617,13 +946,16 @@ public class VTabsheet extends VTabsheetBase
tabWidths.remove(tab);
/*
- * If this widget was selected we need to unmark it as the last
- * selected
+ * If this widget was still selected we need to unselect it. This
+ * should only be necessary if there are no other tabs left that the
+ * selection could move to. Otherwise the server-side updates the
+ * selection when a component is removed from the tab sheet, and the
+ * connector handles that selection change before triggering tab
+ * removal.
*/
if (tab == selected) {
selected = null;
}
- // FIXME: Shouldn't something be selected instead?
int scrollerIndexCandidate = getTabIndex(
getTabsheet().scrollerPositionTabId);
@@ -631,7 +963,8 @@ public class VTabsheet extends VTabsheetBase
// The tab with id scrollerPositionTabId has been removed
scrollerIndexCandidate = getTabsheet().scrollerIndex;
}
- scrollerIndexCandidate = selectNewShownTab(scrollerIndexCandidate);
+ scrollerIndexCandidate = getNearestShownTabIndex(
+ scrollerIndexCandidate);
if (scrollerIndexCandidate >= 0
&& scrollerIndexCandidate < getTabCount()) {
getTabsheet().scrollIntoView(getTab(scrollerIndexCandidate));
@@ -645,11 +978,19 @@ public class VTabsheet extends VTabsheetBase
return 0;
}
- private int selectNewShownTab(int oldPosition) {
- // After removing a tab, find a new scroll position. In most
- // cases the scroll position does not change, but if the tab
- // at the scroll position was removed, need to find a nearby
- // tab that is visible.
+ /**
+ * After removing a tab, find a new scroll position. In most cases the
+ * scroll position does not change, but if the tab at the scroll
+ * position was removed, we need to find a nearby tab that is visible.
+ * The search is performed first to the right from the original tab
+ * (less need to scroll), then to the left.
+ *
+ * @param oldPosition
+ * the index to start the search from
+ * @return the index of the nearest shown tab, or {@code -1} if there
+ * are none
+ */
+ private int getNearestShownTabIndex(int oldPosition) {
for (int i = oldPosition; i < getTabCount(); i++) {
Tab tab = getTab(i);
if (!tab.isHiddenOnServer()) {
@@ -667,22 +1008,42 @@ public class VTabsheet extends VTabsheetBase
return -1;
}
- private boolean isFirstVisibleTab(int index) {
- return getFirstVisibleTab() == index;
+ /**
+ * Returns whether the given tab index matches the first visible tab on
+ * the client.
+ *
+ * @param index
+ * the index to check
+ * @return {@code true} if the given index matches the first visible tab
+ * that hasn't been scrolled out of view, {@code false}
+ * otherwise
+ */
+ private boolean isFirstVisibleTabClient(int index) {
+ return getNextVisibleTab(tabsheet.scrollerIndex - 1) == index;
}
/**
- * Returns the index of the first visible tab on the server
+ * Returns the index of the first visible tab on the server.
+ *
+ * @return the index, or {@code -1} if not found
*/
private int getFirstVisibleTab() {
return getNextVisibleTab(-1);
}
/**
- * Find the next visible tab. Returns -1 if none is found.
+ * Find the next tab that is visible on the server. Being scrolled out
+ * of view or clipped on the client does not make a difference. Returns
+ * -1 if none is found.
*
* @param i
- * @return
+ * the index to start the search from
+ * @return the index of the first visible tab to the right from the
+ * starting point, or {@code -1} if not found
+ *
+ * @see Tab#isHiddenOnServer()
+ * @see VTabsheet#scrolledOutOfView(int)
+ * @see VTabsheet#isClipped(Tab)
*/
private int getNextVisibleTab(int i) {
int tabs = getTabCount();
@@ -698,34 +1059,27 @@ public class VTabsheet extends VTabsheetBase
}
/**
- * Returns the index of the first visible tab in browser.
- */
- private int getFirstVisibleTabClient() {
- int tabs = getTabCount();
- int i = 0;
- while (i < tabs && !getTab(i).isVisible()) {
- i++;
- }
-
- if (i == tabs) {
- return -1;
- } else {
- return i;
- }
- }
-
- /**
- * Returns the index of the last visible tab on the server
+ * Returns the index of the last visible tab on the server.
+ *
+ * @return the index, or {@code -1} if not found
*/
private int getLastVisibleTab() {
return getPreviousVisibleTab(getTabCount());
}
/**
- * Find the previous visible tab. Returns -1 if none is found.
+ * Find the previous tab that is visible on the server. Being scrolled
+ * out of view or clipped on the client does not make a difference.
+ * Returns -1 if none is found.
*
* @param i
- * @return
+ * the index to start the search from
+ * @return the index of the first visible tab to the left from the
+ * starting point, or {@code -1} if not found
+ *
+ * @see Tab#isHiddenOnServer()
+ * @see VTabsheet#scrolledOutOfView(int)
+ * @see VTabsheet#isClipped(Tab)
*/
private int getPreviousVisibleTab(int i) {
do {
@@ -733,9 +1087,21 @@ public class VTabsheet extends VTabsheetBase
} while (i >= 0 && getTab(i).isHiddenOnServer());
return i;
-
}
+ /**
+ * Finds a plausible scroll position to the closest tab on the left that
+ * hasn't been set hidden on the server. If a suitable tab is found,
+ * also sets that tab visible and removes the first visible style from
+ * the previous tab. Does not update the scroller index or set the new
+ * first visible style, in case there are multiple calls in a row. Does
+ * not update any visibilities or styles if a suitable tab is not found.
+ *
+ * @param currentFirstVisible
+ * the index of the current first visible tab
+ * @return the index of the closest visible tab to the left from the
+ * starting point, or {@code -1} if not found
+ */
public int scrollLeft(int currentFirstVisible) {
int prevVisible = getPreviousVisibleTab(currentFirstVisible);
if (prevVisible < 0) {
@@ -745,10 +1111,29 @@ public class VTabsheet extends VTabsheetBase
Tab newFirst = getTab(prevVisible);
newFirst.setVisible(true);
newFirst.recalculateCaptionWidth();
+ Tab oldFirst = getTab(currentFirstVisible);
+ if (oldFirst != null) {
+ oldFirst.setStyleNames(
+ currentFirstVisible == tabsheet.activeTabIndex, false);
+ }
return prevVisible;
}
+ /**
+ * Finds a plausible scroll position to the closest tab on the right
+ * that hasn't been set hidden on the server. If a suitable tab is
+ * found, also sets the previous leftmost tab hidden and remove the
+ * first visible styles. Does not update the scroller index or set the
+ * new first visible style, in case there are multiple calls in a row.
+ * Does not update any visibilities or styles if a suitable tab is not
+ * found.
+ *
+ * @param currentFirstVisible
+ * the index of the current first visible tab
+ * @return the index of the closest visible tab to the right from the
+ * starting point, or {@code -1} if not found
+ */
public int scrollRight(int currentFirstVisible) {
int nextVisible = getNextVisibleTab(currentFirstVisible);
if (nextVisible < 0) {
@@ -756,10 +1141,16 @@ public class VTabsheet extends VTabsheetBase
}
Tab currentFirst = getTab(currentFirstVisible);
currentFirst.setVisible(false);
+ currentFirst.setStyleNames(
+ currentFirstVisible == tabsheet.activeTabIndex, false);
currentFirst.recalculateCaptionWidth();
return nextVisible;
}
+ /**
+ * Recalculates the caption widths for all tabs within this tab bar, and
+ * updates the tab width bookkeeping if necessary.
+ */
private void recalculateCaptionWidths() {
for (int i = 0; i < getTabCount(); ++i) {
getTab(i).recalculateCaptionWidth();
@@ -770,16 +1161,23 @@ public class VTabsheet extends VTabsheetBase
// TODO using the CLASSNAME directly makes primaryStyleName for TabSheet of
// very limited use - all use of style names should be refactored in the
// future
+ /** Default classname for this widget. */
public static final String CLASSNAME = TabsheetState.PRIMARY_STYLE_NAME;
+ /** Default classname for the element that contains tab bar and scroller. */
public static final String TABS_CLASSNAME = CLASSNAME + "-tabcontainer";
+ /** Default classname for the scroller element. */
public static final String SCROLLER_CLASSNAME = CLASSNAME + "-scroller";
+ /** Focus implementation for creating and manipulating tab sheet focus. */
private static final FocusImpl FOCUS_IMPL = FocusImpl
.getFocusImplForPanel();
- /** For internal use only. May be removed or replaced in the future. */
- // tabbar and 'scroller' container
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ * <p>
+ * Container element for tab bar and 'scroller'.
+ */
public final Element tabs;
/**
@@ -788,16 +1186,25 @@ public class VTabsheet extends VTabsheetBase
*/
int tabulatorIndex = 0;
- // tab-scroller element
+ /**
+ * Tab-scroller element, wrapper for the previous and next buttons. No
+ * scrollbars are involved, 'scrolling' happens by hiding tabs on the left.
+ */
private final Element scroller;
- // tab-scroller next button element
+ /**
+ * Tab-scroller next button element. If clicked when active, hides one more
+ * tab from the left, which moves more content in view from the right. Focus
+ * is moved to the first visible tab.
+ */
private final Element scrollerNext;
- // tab-scroller prev button element
- private final Element scrollerPrev;
-
/**
- * The index of the first visible tab (when scrolled)
+ * Tab-scroller prev button element. If clicked when active, shows one more
+ * tab from the left, which moves more content out of view from the right.
+ * Focus is moved to the first visible tab.
*/
+ private final Element scrollerPrev;
+
+ /** The index of the first visible tab (when scrolled). */
private int scrollerIndex = 0;
/**
* The id of the tab at position scrollerIndex. This is used for keeping the
@@ -809,21 +1216,55 @@ public class VTabsheet extends VTabsheetBase
*/
private String scrollerPositionTabId;
+ /** Tab bar widget that contains all {@link Tab}s and a spacer. */
final TabBar tb = new TabBar(this);
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ * <p>
+ * The content panel that contains the widget of the content component that
+ * has been assigned to the selected tab. There should be at most one tab's
+ * content widget added to the panel at the same time.
+ */
protected final VTabsheetPanel tabPanel = new VTabsheetPanel();
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ * <p>
+ * The content wrapper element around the content panel.
+ */
public final Element contentNode;
+ /**
+ * A decorator element at the bottom of the tab sheet, styled in different
+ * ways with different themes. The Valo implementation contains a loading
+ * animation positioned in the middle of the content panel area, only
+ * displayed while the contents are waiting to load, and otherwise the
+ * element has {@code display: none;}.
+ */
private final Element deco;
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ * <p>
+ * {@code true} if waiting for a server roundtrip to return after requesting
+ * selection change, {@code false} otherwise
+ */
public boolean waitingForResponse;
+ /**
+ * String representation of the currently used list of style names given to
+ * this tab sheet. Only used to check whether the list has changed in order
+ * to avoid unnecessary updating of all the element styles.
+ */
private String currentStyle;
/**
* For internal use only. May be renamed or removed in a future release.
+ * <p>
+ * Sets the tabulator index for the active tab of the tab sheet. The active
+ * tab represents the entire tab sheet in the browser's focus cycle
+ * (excluding any focusable elements within the content panel).
+ * <p>
+ * This value is delegated from the TabsheetState.
*
* @param tabIndex
* tabulator index for the active tab of the tab sheet
@@ -838,14 +1279,22 @@ public class VTabsheet extends VTabsheetBase
}
/**
- * @return Whether the tab could be selected or not.
+ * Returns whether the tab could be selected or not. In addition to 'usual'
+ * selection blockers like being disabled or hidden, if the tab sheet is
+ * already waiting for selection confirmation from the server, any further
+ * selections are blocked until the response has been received.
+ *
+ * @param tabIndex
+ * the index of the tab to check
+ *
+ * @return {@code true} if selectable, {@code false} otherwise
*/
private boolean canSelectTab(final int tabIndex) {
- Tab tab = tb.getTab(tabIndex);
if (getApplicationConnection() == null || disabled
|| waitingForResponse) {
return false;
}
+ Tab tab = tb.getTab(tabIndex);
if (!tab.isEnabledOnServer() || tab.isHiddenOnServer()) {
return false;
}
@@ -856,12 +1305,16 @@ public class VTabsheet extends VTabsheetBase
}
/**
- * Load the content of a tab of the provided index.
+ * Begin loading of the content of a tab of the provided index. The actual
+ * content widget will only be available later, after a server round-trip
+ * confirms the selection and switches to send the required child connector.
+ * If the tab in the given index is already active, nothing is done.
*
* @param tabIndex
* The index of the tab to load
*
- * @return true if the specified sheet gets loaded, otherwise false.
+ * @return {@code true} if loading of the specified sheet gets successfully
+ * initialized, {@code false} otherwise.
*/
public boolean loadTabSheet(int tabIndex) {
if (activeTabIndex != tabIndex && canSelectTab(tabIndex)) {
@@ -879,7 +1332,8 @@ public class VTabsheet extends VTabsheetBase
waitingForResponse = true;
- tb.getTab(tabIndex).focus(); // move keyboard focus to active tab
+ // Once upon a time it was necessary to re-establish the tab focus
+ // here. This should not be the case with modern browsers.
return true;
}
@@ -926,43 +1380,62 @@ public class VTabsheet extends VTabsheetBase
return getApplicationConnection().getVTooltip();
}
+ /**
+ * This should be triggered from an onload event within the given tab's
+ * caption to signal that icon contents have finished loading. The contents
+ * may have changed the tab's width. This might in turn require changes in
+ * the scroller (hidden tabs might need to be scrolled back into view), or
+ * even the width of the entire tab sheet if it has been configured to be
+ * dynamic.
+ *
+ * @param tab
+ * the tab whose size may have changed
+ */
public void tabSizeMightHaveChanged(Tab tab) {
// icon onloads may change total width of tabsheet
if (isDynamicWidth()) {
updateDynamicWidth();
}
updateTabScroller();
-
}
+ /**
+ * Informs the server that closing of a tab has been requested.
+ *
+ * @param tabIndex
+ * the index of the closed to close
+ */
void sendTabClosedEvent(int tabIndex) {
getRpcProxy().closeTab(tabKeys.get(tabIndex));
}
+ /**
+ * Constructs a widget for a TabSheet component.
+ */
public VTabsheet() {
super(CLASSNAME);
// Tab scrolling
getElement().getStyle().setOverflow(Overflow.HIDDEN);
tabs = DOM.createDiv();
- DOM.setElementProperty(tabs, "className", TABS_CLASSNAME);
+ tabs.setPropertyString("className", TABS_CLASSNAME);
Roles.getTablistRole().set(tabs);
Roles.getTablistRole().setAriaLiveProperty(tabs, LiveValue.OFF);
scroller = DOM.createDiv();
Roles.getTablistRole().setAriaHiddenState(scroller, true);
- DOM.setElementProperty(scroller, "className", SCROLLER_CLASSNAME);
+ scroller.setPropertyString("className", SCROLLER_CLASSNAME);
scrollerPrev = DOM.createButton();
scrollerPrev.setTabIndex(-1);
- DOM.setElementProperty(scrollerPrev, "className",
+ scrollerPrev.setPropertyString("className",
SCROLLER_CLASSNAME + "Prev");
Roles.getTablistRole().setAriaHiddenState(scrollerPrev, true);
DOM.sinkEvents(scrollerPrev, Event.ONCLICK | Event.ONMOUSEDOWN);
scrollerNext = DOM.createButton();
scrollerNext.setTabIndex(-1);
- DOM.setElementProperty(scrollerNext, "className",
+ scrollerNext.setPropertyString("className",
SCROLLER_CLASSNAME + "Next");
Roles.getTablistRole().setAriaHiddenState(scrollerNext, true);
DOM.sinkEvents(scrollerNext, Event.ONCLICK | Event.ONMOUSEDOWN);
@@ -977,9 +1450,8 @@ public class VTabsheet extends VTabsheetBase
deco = DOM.createDiv();
tb.setStyleName(CLASSNAME + "-tabs");
- DOM.setElementProperty(contentNode, "className",
- CLASSNAME + "-content");
- DOM.setElementProperty(deco, "className", CLASSNAME + "-deco");
+ contentNode.setPropertyString("className", CLASSNAME + "-content");
+ deco.setPropertyString("className", CLASSNAME + "-deco");
add(tb, tabs);
DOM.appendChild(scroller, scrollerPrev);
@@ -990,11 +1462,6 @@ public class VTabsheet extends VTabsheetBase
DOM.appendChild(getElement(), deco);
DOM.appendChild(tabs, scroller);
-
- // TODO Use for Safari only. Fix annoying 1px first cell in TabBar.
- // DOM.setStyleAttribute(DOM.getFirstChild(DOM.getFirstChild(DOM
- // .getFirstChild(tb.getElement()))), "display", "none");
-
}
@Override
@@ -1005,7 +1472,8 @@ public class VTabsheet extends VTabsheetBase
if (event.getTypeInt() == Event.ONCLICK) {
// Tab scrolling
- if (eventTarget == scrollerPrev || eventTarget == scrollerNext) {
+ if (scrollerPrev.equals(eventTarget)
+ || scrollerNext.equals(eventTarget)) {
scrollAccordingToScrollTarget(eventTarget);
event.stopPropagation();
@@ -1013,7 +1481,8 @@ public class VTabsheet extends VTabsheetBase
} else if (event.getTypeInt() == Event.ONMOUSEDOWN) {
- if (eventTarget == scrollerPrev || eventTarget == scrollerNext) {
+ if (scrollerPrev.equals(eventTarget)
+ || scrollerNext.equals(eventTarget)) {
// In case the focus was previously on a Tab, we need to cancel
// the upcoming blur on the Tab which will follow this mouse
// down event.
@@ -1026,9 +1495,11 @@ public class VTabsheet extends VTabsheetBase
super.onBrowserEvent(event);
}
- /*
- * Scroll the tab bar according to the last scrollTarget (the scroll button
- * pressed).
+ /**
+ * Scroll the tab bar according to the last scrollTarget.
+ *
+ * @param scrollTarget
+ * the scroll button that was pressed
*/
private void scrollAccordingToScrollTarget(
com.google.gwt.dom.client.Element scrollTarget) {
@@ -1039,23 +1510,48 @@ public class VTabsheet extends VTabsheetBase
int newFirstIndex = -1;
// Scroll left.
- if (isScrolledTabs() && scrollTarget == scrollerPrev) {
+ if (hasScrolledTabs() && scrollTarget == scrollerPrev) {
newFirstIndex = tb.scrollLeft(scrollerIndex);
// Scroll right.
- } else if (isClippedTabs() && scrollTarget == scrollerNext) {
+ } else if (hasClippedTabs() && scrollTarget == scrollerNext) {
newFirstIndex = tb.scrollRight(scrollerIndex);
}
if (newFirstIndex != -1) {
scrollerIndex = newFirstIndex;
- scrollerPositionTabId = tb.getTab(scrollerIndex).id;
+ Tab currentFirst = tb.getTab(newFirstIndex);
+ currentFirst.setStyleNames(scrollerIndex == activeTabIndex, true,
+ true);
+ scrollerPositionTabId = currentFirst.id;
updateTabScroller();
}
+ // scrolling updated first visible styles but only removed the previous
+ // focus style if the focused tab was also the first tab
+ if (selectionHandler.focusedTabIndex >= 0
+ && selectionHandler.focusedTabIndex != scrollerIndex) {
+ tb.getTab(selectionHandler.focusedTabIndex).setStyleNames(
+ selectionHandler.focusedTabIndex == activeTabIndex, false);
+ }
+
// For this to work well, make sure the method gets called only from
// user events.
selectionHandler.focusTabAtIndex(scrollerIndex);
+ /*
+ * Update the bookkeeping or the next keyboard navigation starts from
+ * the wrong tab.
+ *
+ * Note: unusually, this can move the focusedTabIndex to point to a
+ * disabled tab. We could add more logic that only focuses an
+ * unselectable first tab if there are no selectable tabs in view at
+ * all, but for now it's left like this for simplicity. Another option
+ * would be to put canSelectTab(scrollerIndex) around both of these
+ * lines, but that would have more impact on the experienced behavior
+ * (using only keyboard or only the arrow buttons seems more likely than
+ * mixing them up actively).
+ */
+ selectionHandler.focusedTabIndex = scrollerIndex;
}
/**
@@ -1063,21 +1559,28 @@ public class VTabsheet extends VTabsheetBase
* view (on the left side).
*
* @param index
- * @return
+ * the index of the tab to check
+ * @return {@code true} if the index is smaller than the first visible tab's
+ * index, {@code false} otherwise
*/
private boolean scrolledOutOfView(int index) {
return scrollerIndex > index;
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param state
+ * the state object for this component
+ */
public void handleStyleNames(AbstractComponentState state) {
// Add proper stylenames for all elements (easier to prevent unwanted
// style inheritance)
if (ComponentStateUtil.hasStyles(state)) {
final List<String> styles = state.styles;
- if (currentStyle == null
- || !currentStyle.equals(styles.toString())) {
- currentStyle = styles.toString();
+ String newStyles = styles.toString();
+ if (currentStyle == null || !currentStyle.equals(newStyles)) {
+ currentStyle = newStyles;
final String tabsBaseClass = TABS_CLASSNAME;
String tabsClass = tabsBaseClass;
final String contentBaseClass = CLASSNAME + "-content";
@@ -1090,29 +1593,36 @@ public class VTabsheet extends VTabsheetBase
contentClass += " " + contentBaseClass + "-" + style;
decoClass += " " + decoBaseClass + "-" + style;
}
- DOM.setElementProperty(tabs, "className", tabsClass);
- DOM.setElementProperty(contentNode, "className", contentClass);
- DOM.setElementProperty(deco, "className", decoClass);
+ tabs.setPropertyString("className", tabsClass);
+ contentNode.setPropertyString("className", contentClass);
+ deco.setPropertyString("className", decoClass);
}
} else {
tb.setStyleName(CLASSNAME + "-tabs");
- DOM.setElementProperty(tabs, "className", TABS_CLASSNAME);
- DOM.setElementProperty(contentNode, "className",
- CLASSNAME + "-content");
- DOM.setElementProperty(deco, "className", CLASSNAME + "-deco");
+ tabs.setPropertyString("className", TABS_CLASSNAME);
+ contentNode.setPropertyString("className", CLASSNAME + "-content");
+ deco.setPropertyString("className", CLASSNAME + "-deco");
}
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @see #isDynamicWidth()
+ */
public void updateDynamicWidth() {
// Find width consumed by tabs
- TableCellElement spacerCell = ((TableElement) tb.getElement().cast())
- .getRows().getItem(0).getCells().getItem(tb.getTabCount());
+ // spacer is a filler cell that covers the gap beside the tabs when
+ // the content is wider than the collective width of the tabs (also
+ // ensures there's room for the scroller element but that is usually
+ // hidden in dynamic width tab sheets), by default hidden by Valo
+ TableCellElement spacerCell = ((TableCellElement) tb.spacerTd.cast());
int spacerWidth = spacerCell.getOffsetWidth();
- DivElement div = (DivElement) spacerCell.getFirstChildElement();
+ DivElement spacerContent = (DivElement) spacerCell
+ .getFirstChildElement();
- int spacerMinWidth = spacerCell.getOffsetWidth() - div.getOffsetWidth();
+ int spacerMinWidth = spacerWidth - spacerContent.getOffsetWidth();
int tabsWidth = tb.getOffsetWidth() - spacerWidth + spacerMinWidth;
@@ -1120,22 +1630,24 @@ public class VTabsheet extends VTabsheetBase
Style style = tabPanel.getElement().getStyle();
String overflow = style.getProperty("overflow");
style.setProperty("overflow", "hidden");
+ // set temporary width to match the tab widths in case the content
+ // component is relatively sized and previously calculated width is now
+ // too wide
style.setPropertyPx("width", tabsWidth);
- boolean hasTabs = tabPanel.getWidgetCount() > 0;
+ boolean hasContent = tabPanel.getWidgetCount() > 0;
Style wrapperstyle = null;
- if (hasTabs) {
+ int contentWidth = 0;
+ if (hasContent) {
wrapperstyle = getCurrentlyDisplayedWidget().getElement()
.getParentElement().getStyle();
wrapperstyle.setPropertyPx("width", tabsWidth);
- }
- // Get content width from actual widget
- int contentWidth = 0;
- if (hasTabs) {
+ // Get content width from actual widget
contentWidth = getCurrentlyDisplayedWidget().getOffsetWidth();
}
+
style.setProperty("overflow", overflow);
// Set widths to max(tabs,content)
@@ -1147,7 +1659,7 @@ public class VTabsheet extends VTabsheetBase
tabs.getStyle().setPropertyPx("width", outerWidth);
style.setPropertyPx("width", tabsWidth);
- if (hasTabs) {
+ if (hasContent) {
wrapperstyle.setPropertyPx("width", tabsWidth);
}
@@ -1156,29 +1668,6 @@ public class VTabsheet extends VTabsheetBase
updateOpenTabSize();
}
- private boolean isAllTabsBeforeIndexInvisible() {
- boolean invisible = true;
- for (int i = 0; i < scrollerIndex; i++) {
- invisible = invisible & !tb.getTab(i).isVisible();
- }
- return invisible;
- }
-
- private boolean isScrollerPrevDisabled() {
- return scrollerPrev.getClassName()
- .contains(PREV_SCROLLER_DISABLED_CLASSNAME);
- }
-
- private boolean isScrollerHidden() {
- return scroller.getStyle().getDisplay()
- .equals(Display.NONE.getCssName());
- }
-
- private boolean isIndexSkippingHiddenTabs() {
- return isAllTabsBeforeIndexInvisible()
- && (isScrollerPrevDisabled() || isScrollerHidden());
- }
-
@Override
public void renderTab(final TabState tabState, int index) {
Tab tab = tb.getTab(index);
@@ -1187,29 +1676,58 @@ public class VTabsheet extends VTabsheetBase
}
tab.updateFromState(tabState);
- tab.setEnabledOnServer((!disabledTabKeys.contains(tabKeys.get(index))));
- tab.setHiddenOnServer(!tabState.visible);
+ tab.setEnabledOnServer(!disabledTabKeys.contains(tabKeys.get(index)));
- if (scrolledOutOfView(index) && !isIndexSkippingHiddenTabs()) {
- // Should not set tabs visible if they are scrolled out of view
- tab.setVisible(false);
- } else {
- // When the tab was hidden and then turned visible again
- // and there is space for it, it should be in view (#17096) (#17333)
- if (isTabSetVisibleBeforeScroller(tabState, index, tab)) {
+ boolean previouslyVisibleOnServer = !tab.isHiddenOnServer();
+ boolean serverVisibilityChanged = previouslyVisibleOnServer != tabState.visible;
+
+ if (serverVisibilityChanged) {
+ Tab activeTab = tb.selected;
+ boolean activeInView = activeTab != null
+ && !scrolledOutOfView(activeTabIndex)
+ && !isClipped(activeTab);
+
+ if (tabState.visible
+ && needsToScrollIntoViewIfBecomesVisible(index)) {
scrollerIndex = index;
- tab.setVisible(true);
- tab.setStyleNames(false, true);
+ scrollerPositionTabId = tab.id;
+ }
+ /*
+ * Technically the scroller position also needs to change if the
+ * currently updated tab was the first visible one and is now
+ * hidden, but that is dealt with at the end of the state change
+ * handling, when layouting is triggered for the whole tab sheet at
+ * once. It would be premature to do those calculations here, since
+ * the following tabs haven't got refreshed to match the current
+ * state yet.
+ */
+
+ tab.setHiddenOnServer(!tabState.visible);
+ tab.setVisible(tabState.visible && !scrolledOutOfView(index));
- // scroll to the currently selected tab if it got clipped
- // after making another tab visible
- if (isClippedTabs()) {
- scrollIntoView(getActiveTab());
+ if (activeInView && tab.isVisible() && index < activeTabIndex) {
+ // ensure the newly visible tab didn't push the active tab out
+ // of view
+ if (isClipped(activeTab)) {
+ scrollIntoView(activeTab);
}
- } else {
- tab.setVisible(tabState.visible);
}
+
+ tab.setStyleNames(activeTabIndex == index, scrollerIndex == index);
}
+ /*
+ * There is no need to update the tab visibility if the server
+ * visibility didn't change, because the scroller index can only have
+ * changed for two reasons while rendering previous tabs:
+ *
+ * 1) If all previously hidden tabs were also hidden on server, in which
+ * case the only tabs that could get automatically scrolled into view
+ * are ones that had their hiddenOnServer state updated.
+ *
+ * 2) If the active tab got clipped and needed to get scrolled into
+ * again, in which case the visibilities of all relevant tabs already
+ * got refreshed anyway.
+ */
/*
* Force the width of the caption container so the content will not wrap
@@ -1219,27 +1737,28 @@ public class VTabsheet extends VTabsheetBase
}
/**
- * Checks whether the tab has been set to visible and the scroller is at the
- * first visible tab. That means that the scroller has to be adjusted so
- * that the tab is visible again.
+ * If the tab bar was previously scrolled as far left as it can go, i.e.
+ * every scrolled out tab was also hidden on server, and the tab that is
+ * getting its visibility updated is among them, it should become the first
+ * visible tab instead. If the tab was not among those tabs, the scroller
+ * index doesn't need adjusting. If any visible-on-server tabs were already
+ * scrolled out of view, scroll position likewise doesn't need adjusting
+ * regardless of which side of the line this tab falls.
+ * <p>
+ * This check must be performed before the tab's hiddenOnServer state is
+ * updated, and only if the server visibility is changed from hidden to
+ * visible.
+ *
+ * @param index
+ * the index of the tab that is getting updated
+ * @return {@code true} if the given index should become the new scroller
+ * index, {@code false} otherwise
*/
- private boolean isTabSetVisibleBeforeScroller(TabState tabState, int index,
- Tab tab) {
- return isIndexSkippingHiddenTabs() && isScrollerAtFirstVisibleTab()
- && hasTabChangedVisibility(tabState, tab)
- && scrolledOutOfView(index);
- }
-
- /**
- * Checks whether the tab is visible on server but is not visible on client
- * yet.
- */
- private boolean hasTabChangedVisibility(TabState tabState, Tab tab) {
- return !tab.isVisible() && tabState.visible;
- }
-
- private boolean isScrollerAtFirstVisibleTab() {
- return tb.getFirstVisibleTabClient() == scrollerIndex;
+ private boolean needsToScrollIntoViewIfBecomesVisible(int index) {
+ // note that these methods use different definition for word 'scrolled',
+ // the first one accepts hidden-on-server tabs as scrolled while the
+ // second one only cares about tabs that end-user considers scrolled
+ return scrolledOutOfView(index) && !hasScrolledTabs();
}
/**
@@ -1248,6 +1767,8 @@ public class VTabsheet extends VTabsheetBase
*/
@Deprecated
public class PlaceHolder extends VLabel {
+ /** @deprecated This class is not used by the framework code anymore. */
+ @Deprecated
public PlaceHolder() {
super("");
}
@@ -1257,6 +1778,7 @@ public class VTabsheet extends VTabsheetBase
* Renders the widget content for a tab sheet.
*
* @param newWidget
+ * the content widget or {@code null} if there is none
*/
public void renderContent(Widget newWidget) {
assert tabPanel.getWidgetCount() <= 1;
@@ -1277,19 +1799,9 @@ public class VTabsheet extends VTabsheetBase
// There's never any other index than 0, but maintaining API for now
tabPanel.showWidget(0);
- VTabsheet.this.iLayout();
+ iLayout();
updateOpenTabSize();
- VTabsheet.this.removeStyleDependentName("loading");
- }
-
- /**
- * Recalculates the sizes of tab captions, causing the tabs to be rendered
- * the correct size.
- */
- private void updateTabCaptionSizes() {
- for (int tabIx = 0; tabIx < tb.getTabCount(); tabIx++) {
- tb.getTab(tabIx).recalculateCaptionWidth();
- }
+ removeStyleDependentName("loading");
}
/** For internal use only. May be removed or replaced in the future. */
@@ -1318,14 +1830,18 @@ public class VTabsheet extends VTabsheetBase
* Run internal layouting.
*/
public void iLayout() {
+ // reset the width adjuster, in case the styles have changed
+ tb.firstAdjusted = false;
+ tb.pendingTab = null;
+ tb.firstTabWidthAdjuster = 0;
updateTabScroller();
- updateTabCaptionSizes();
+ tb.recalculateCaptionWidths();
}
/**
- * Sets the size of the visible tab (component). As the tab is set to
- * position: absolute (to work around a firefox flickering bug) we must keep
- * this up-to-date by hand.
+ * Sets the size of the visible tab content (component). As the tab is set
+ * to position: absolute (to work around a firefox flickering bug) we must
+ * keep this up-to-date by hand.
* <p>
* For internal use only. May be removed or replaced in the future.
*/
@@ -1345,9 +1861,9 @@ public class VTabsheet extends VTabsheetBase
width = contentNode.getOffsetWidth() - getContentAreaBorderWidth();
} else {
/*
- * If the tabbar is wider than the content we need to use the tabbar
- * width as minimum width so scrollbars get placed correctly (at the
- * right edge).
+ * In case the tab bar happens to be wider than the content we need
+ * to use the tab bar width as minimum width to ensure scrollbars
+ * get placed correctly (at the right edge).
*/
minWidth = tb.getOffsetWidth() - getContentAreaBorderWidth();
}
@@ -1364,15 +1880,18 @@ public class VTabsheet extends VTabsheetBase
}
// Make sure scrollerIndex is valid
+ boolean changed = false;
if (scrollerIndex < 0 || scrollerIndex > tb.getTabCount()) {
scrollerIndex = tb.getFirstVisibleTab();
+ changed = true;
} else if (tb.getTabCount() > 0
&& tb.getTab(scrollerIndex).isHiddenOnServer()) {
scrollerIndex = tb.getNextVisibleTab(scrollerIndex);
+ changed = true;
}
- TableCellElement spacerCell = ((TableElement) tb.getElement().cast())
- .getRows().getItem(0).getCells().getItem(tb.getTabCount());
+ // This element is hidden by Valo, test with legacy themes.
+ TableCellElement spacerCell = ((TableCellElement) tb.spacerTd.cast());
if (scroller.getStyle().getDisplay() != "none") {
spacerCell.getStyle().setPropertyPx("minWidth",
scroller.getOffsetWidth());
@@ -1383,24 +1902,29 @@ public class VTabsheet extends VTabsheetBase
}
// check if hidden tabs need to be scrolled back into view
- int firstVisibleIndex = tb.getFirstVisibleTabClient();
- if (firstVisibleIndex != 0 && getTabCount() > 0
- && getLeftGap() + getRightGap() > 0) {
- int hiddenCount = tb.getTabCount();
- if (firstVisibleIndex > 0) {
- hiddenCount -= firstVisibleIndex;
- }
- int counter = 0;
- while ((getLeftGap() + getRightGap() > getFirstOutOfViewWidth())
- && counter < hiddenCount) {
- tb.scrollLeft(tb.getFirstVisibleTabClient());
- scrollerIndex = tb.getFirstVisibleTabClient();
- ++counter;
- }
+ while (hasScrolledTabs()
+ && (getLeftGap() + getRightGap() >= getFirstOutOfViewWidth())) {
+ scrollerIndex = tb.scrollLeft(scrollerIndex);
+ Tab currentFirst = tb.getTab(scrollerIndex);
+ scrollerPositionTabId = currentFirst.id;
+ // the styles might affect the next round of calculations, must
+ // update on every round
+ currentFirst.setStyleNames(scrollerIndex == activeTabIndex, true,
+ true);
+ currentFirst.recalculateCaptionWidth();
+ // everything up to date, can remove the check
+ changed = false;
}
- boolean scrolled = isScrolledTabs();
- boolean clipped = isClippedTabs();
+ if (changed) {
+ Tab currentFirst = tb.getTab(scrollerIndex);
+ currentFirst.setStyleNames(scrollerIndex == activeTabIndex, true,
+ true);
+ scrollerPositionTabId = currentFirst.id;
+ }
+
+ boolean scrolled = hasScrolledTabs();
+ boolean clipped = hasClippedTabs();
if (tb.getTabCount() > 0 && tb.isVisible() && (scrolled || clipped)) {
scroller.getStyle().clearDisplay();
scrollerPrev.setPropertyString("className", SCROLLER_CLASSNAME
@@ -1433,8 +1957,17 @@ public class VTabsheet extends VTabsheetBase
}
}
+ /**
+ * Returns the gap between the leftmost visible tab and the tab container
+ * edge. By default there should be no gap at all, unless the tabs have been
+ * right-aligned by styling (e.g. Valo style {@code right-aligned-tabs} or
+ * {@code centered-tabs}).
+ *
+ * @return the left gap (in pixels), or zero if no gap
+ */
private int getLeftGap() {
- int firstVisibleIndex = tb.getFirstVisibleTabClient();
+ int firstVisibleIndex = tb.getFirstVisibleTab() < 0 ? -1
+ : scrollerIndex;
int gap;
if (firstVisibleIndex < 0) {
// no tabs are visible, the entire empty space is returned
@@ -1449,6 +1982,13 @@ public class VTabsheet extends VTabsheetBase
return gap > 0 ? gap : 0;
}
+ /**
+ * Returns the gap between the rightmost visible tab and the tab container
+ * edge. If the tabs have been right-aligned by styling (e.g. Valo style
+ * {@code right-aligned-tabs}) there should be no gap at all.
+ *
+ * @return the right gap (in pixels), or zero if no gap
+ */
private int getRightGap() {
int lastVisibleIndex = tb.getLastVisibleTab();
Element tabContainer = tb.getElement().getParentElement();
@@ -1461,14 +2001,14 @@ public class VTabsheet extends VTabsheetBase
gap = tabContainer.getAbsoluteRight()
- lastVisibleTab.getAbsoluteLeft()
- lastVisibleTab.getOffsetWidth()
- - scroller.getOffsetWidth() - 2;
+ - scroller.getOffsetWidth();
}
return gap > 0 ? gap : 0;
}
private int getFirstOutOfViewWidth() {
- Tab firstTabOutOfView = tb.getTab(
- tb.getPreviousVisibleTab(tb.getFirstVisibleTabClient()));
+ Tab firstTabOutOfView = tb
+ .getTab(tb.getPreviousVisibleTab(scrollerIndex));
if (firstTabOutOfView != null) {
return tb.getLastKnownTabWidth(firstTabOutOfView);
}
@@ -1488,26 +2028,63 @@ public class VTabsheet extends VTabsheetBase
}
}
- private boolean isScrolledTabs() {
- return scrollerIndex > tb.getFirstVisibleTab();
+ /**
+ * Checks whether there are any tabs scrolled out of view that could be
+ * scrolled back into (not hidden on the server). If no such tabs are
+ * scrolled out, this check returns {@code false}. Disabled but
+ * visible-on-server tabs count as viewable.
+ *
+ * @return {@code true} if any viewable tabs are scrolled out of view,
+ * {@code false} otherwise
+ */
+ private boolean hasScrolledTabs() {
+ return scrollerIndex > 0 && scrollerIndex > tb.getFirstVisibleTab();
}
- private boolean isClippedTabs() {
+ /**
+ * Checks whether there are any tabs clipped out of view (hidden behind the
+ * scroller element or overflowing further) that could be scrolled into (not
+ * hidden on the server). If no such tabs are clipped, this check returns
+ * {@code false}. Disabled but visible-on-server tabs count as viewable.
+ *
+ * @return {@code true} if any viewable tabs are clipped, {@code false}
+ * otherwise
+ */
+ private boolean hasClippedTabs() {
+ // scroller should only be taken into account if some potentially
+ // visible tabs are already scrolled out of view
return (tb.getOffsetWidth() - getSpacerWidth()) > getOffsetWidth()
- - (isScrolledTabs() ? scroller.getOffsetWidth() : 0);
+ - (hasScrolledTabs() ? scroller.getOffsetWidth() : 0);
}
+ /**
+ * Checks whether the given tab is clipped out of view (hidden behind the
+ * scroller element or overflowing further). Does not check whether hiding
+ * the scroller element would bring this tab fully into view.
+ *
+ * @return {@code true} if the given tab is clipped, {@code false} otherwise
+ */
private boolean isClipped(Tab tab) {
return tab.getAbsoluteLeft() + tab.getOffsetWidth() > getAbsoluteLeft()
+ getOffsetWidth() - scroller.getOffsetWidth();
}
+ /**
+ * Returns the width of the spacer cell. Valo theme has the element hidden
+ * by default, in which case the this returns zero.
+ *
+ * @return the width of the spacer cell in pixels
+ */
private int getSpacerWidth() {
- int spacerWidth = ((Element) tb.getContainerElement().getLastChild()
- .cast()).getPropertyInt("offsetWidth");
- return spacerWidth;
+ return tb.spacerTd.getOffsetWidth();
}
+ /**
+ * {@inheritDoc}
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ */
+ @Deprecated
@Override
protected void clearPaintables() {
@@ -1524,7 +2101,11 @@ public class VTabsheet extends VTabsheetBase
return tabPanel.iterator();
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @return the horizontal width consumed by borders of the content area
+ */
public int getContentAreaBorderWidth() {
return WidgetUtil.measureHorizontalBorder(contentNode);
}
@@ -1560,12 +2141,23 @@ public class VTabsheet extends VTabsheetBase
getActiveTab().focus();
}
+ /**
+ * Removes focus from the active tab.
+ *
+ * @deprecated This method is not called by the framework code anymore.
+ */
+ @Deprecated
public void blur() {
getActiveTab().blur();
}
- /*
- * Gets the active tab.
+ /**
+ * Returns the active tab. This method uses
+ * {@link VTabsheetBase#activeTabIndex} to identify the tab, which usually
+ * matches the value saved to {@link TabBar#selected}, but the former has a
+ * default value and the latter doesn't.
+ *
+ * @return the active tab
*/
private Tab getActiveTab() {
return tb.getTab(activeTabIndex);
@@ -1583,20 +2175,20 @@ public class VTabsheet extends VTabsheetBase
*/
private FocusBlurManager focusBlurManager = new FocusBlurManager();
- /*
+ /**
* Generate the correct focus/blur events for the main TabSheet component
* (#14304).
*
* The TabSheet must fire one focus event when the user clicks on the tab
- * bar (i.e. inner TabBar class) containing the Tabs or when the focus is
- * provided to the TabSheet by any means. Also one blur event should be
- * fired only when the user leaves the tab bar. After the user focus on the
- * tab bar and before leaving it, no matter how many times he's pressing the
- * Tabs or the scroll buttons, the TabSheet component should not fire any of
- * those blur/focus events.
+ * bar (i.e. inner {@link TabBar} class) containing the Tabs or when the
+ * focus is provided to the TabSheet by any means. Also one blur event
+ * should be fired only when the user leaves the tab bar. After the user
+ * focus on the tab bar and before leaving it, no matter how many times the
+ * Tabs or the scroll buttons are pressed, the TabSheet component should not
+ * fire any of those blur/focus events.
*
* The only focusable elements contained in the tab bar are the Tabs (see
- * inner class Tab). The reason is the accessibility support.
+ * inner class {@link Tab}). The reason is the accessibility support.
*
* Having this in mind, the chosen solution path for our problem is to match
* a sequence of focus/blur events on the tabs, choose only the first focus
@@ -1612,27 +2204,33 @@ public class VTabsheet extends VTabsheetBase
*/
private static class FocusBlurManager {
- // The real tab with focus on it. If the focus goes to another element
- // in the page this will be null.
+ /**
+ * The real tab with focus on it. If the focus goes to another element
+ * in the page this will be null.
+ */
private Tab focusedTab;
- /*
- * Gets the focused tab.
+ /**
+ * Returns the tab that has the focus currently.
+ *
+ * @return the focused tab or {@code null} if one doesn't exist
*/
private Tab getFocusedTab() {
return focusedTab;
}
- /*
- * Sets the local field tracking the focused tab.
+ /**
+ * Sets the tab that has the focus currently.
+ *
+ * @param focusedTab
+ * the focused tab or {@code null} if no tab should be
+ * focused anymore
*/
private void setFocusedTab(Tab focusedTab) {
this.focusedTab = focusedTab;
}
- /*
- * The ultimate focus/blur event dispatcher.
- */
+ /** The ultimate focus/blur event dispatcher. */
private AbstractComponentConnector connector;
/**
@@ -1676,19 +2274,15 @@ public class VTabsheet extends VTabsheetBase
}
}
- /*
- * The last blur command to be executed.
- */
+ /** The last blur command to be executed. */
private BlurCommand blurCommand;
- /*
- * Execute the final blur command.
+ /**
+ * Command class for executing the final blur event.
*/
private class BlurCommand implements Command {
- /*
- * The blur source.
- */
+ /** The blur source. */
private Tab blurSource;
/**
@@ -1721,13 +2315,12 @@ public class VTabsheet extends VTabsheetBase
@Override
public void execute() {
-
- Tab focusedTab = getFocusedTab();
-
if (blurSource == null) {
return;
}
+ Tab focusedTab = getFocusedTab();
+
// The focus didn't change since this blur triggered, so
// the new focused element is not a tab.
if (focusedTab == blurSource) {
@@ -1744,8 +2337,11 @@ public class VTabsheet extends VTabsheetBase
}
}
- /*
+ /**
* Schedule a new blur event for a deferred execution.
+ *
+ * @param blurSource
+ * the source tab
*/
private void scheduleBlur(Tab blurSource) {
@@ -1795,7 +2391,7 @@ public class VTabsheet extends VTabsheetBase
nextBlurScheduleCancelled = true;
}
- /*
+ /**
* Flag that the next deferred command won't get executed. This is
* useful in case of IE where the user focus event don't fire and we're
* using the mouse down event to track the focus. But the mouse down
@@ -1806,19 +2402,20 @@ public class VTabsheet extends VTabsheetBase
}
- /*
- * The tabs selection handler instance.
- */
+ /** The tab selection handler instance. */
private final TabSelectionHandler selectionHandler = new TabSelectionHandler();
- /*
- * Handle the events for selecting the tabs.
+ /**
+ * Handler class for tab selection events.
*/
private class TabSelectionHandler implements FocusHandler, BlurHandler,
KeyDownHandler, ClickHandler, MouseDownHandler {
- /** For internal use only. May be removed or replaced in the future. */
- // The current visible focused index.
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ * <p>
+ * The current visible focused index.
+ */
private int focusedTabIndex = 0;
/**
@@ -1883,13 +2480,19 @@ public class VTabsheet extends VTabsheetBase
tb.navigateTab(focusedTabIndex, index);
+ // save the previous focus index in case the clicked tab isn't
+ // selectable
+ int previouslyFocusedTabIndex = focusedTabIndex;
+
focusedTabIndex = index;
if (!loadTabSheet(index)) {
-
- // This needs to be called at the end, as the activeTabIndex
- // is set in the loadTabSheet.
- focus();
+ // no loading attempted, return focus to the previous tab (which
+ // might be the current tab, if the same tab was clicked again)
+ if (focusedTabIndex != activeTabIndex) {
+ focusedTabIndex = previouslyFocusedTabIndex;
+ }
+ tb.getTab(focusedTabIndex).focus();
}
}
@@ -1912,11 +2515,11 @@ public class VTabsheet extends VTabsheetBase
if (!event.isAnyModifierKeyDown()) {
if (keycode == getPreviousTabKey()) {
- selectPreviousTab();
+ focusPreviousTab();
event.stopPropagation();
} else if (keycode == getNextTabKey()) {
- selectNextTab();
+ focusNextTab();
event.stopPropagation();
} else if (keycode == getCloseTabKey()) {
@@ -1936,10 +2539,13 @@ public class VTabsheet extends VTabsheetBase
}
}
- /*
- * Left arrow key selection.
+ /**
+ * Left arrow key focus move. Selection won't change until the selection
+ * key is pressed, but the target tab must be selectable. If no
+ * selectable tabs are found before currently focused tab, focus isn't
+ * moved.
*/
- private void selectPreviousTab() {
+ private void focusPreviousTab() {
int newTabIndex = focusedTabIndex;
// Find the previous visible and enabled tab if any.
do {
@@ -1947,14 +2553,17 @@ public class VTabsheet extends VTabsheetBase
} while (newTabIndex >= 0 && !canSelectTab(newTabIndex));
if (newTabIndex >= 0) {
- keySelectTab(newTabIndex);
+ keyFocusTab(newTabIndex);
}
}
- /*
- * Right arrow key selection.
+ /**
+ * Right arrow key focus move. Selection won't change until the
+ * selection key is pressed, but the target tab must be selectable. If
+ * no selectable tabs are found after currently focused tab, focus isn't
+ * moved.
*/
- private void selectNextTab() {
+ private void focusNextTab() {
int newTabIndex = focusedTabIndex;
// Find the next visible and enabled tab if any.
do {
@@ -1962,20 +2571,22 @@ public class VTabsheet extends VTabsheetBase
} while (newTabIndex < getTabCount() && !canSelectTab(newTabIndex));
if (newTabIndex < getTabCount()) {
- keySelectTab(newTabIndex);
+ keyFocusTab(newTabIndex);
}
}
- /*
- * Select the specified tab using left/right key.
+ /**
+ * Focus the specified tab using left/right key. Selection won't change
+ * until the selection key is pressed. Selectability should be checked
+ * before calling this method.
*/
- private void keySelectTab(int newTabIndex) {
+ private void keyFocusTab(int newTabIndex) {
Tab tab = tb.getTab(newTabIndex);
if (tab == null) {
return;
}
- // Focus the tab, otherwise the selected one will loose focus and
+ // Focus the tab, otherwise the selected one will lose focus and
// TabSheet will get blurred.
focusTabAtIndex(newTabIndex);
@@ -2001,18 +2612,20 @@ public class VTabsheet extends VTabsheetBase
}
/**
- * @return The key code of the keyboard shortcut that selects the previous
- * tab in a focused tabsheet.
+ * Returns the key code of the keyboard shortcut that focuses the previous
+ * tab in a focused tabsheet.
+ *
+ * @return the key to move focus to the previous tab
*/
protected int getPreviousTabKey() {
return KeyCodes.KEY_LEFT;
}
/**
- * Gets the key to activate the selected tab when navigating using
+ * Gets the key to select the focused tab when navigating using
* previous/next (left/right) keys.
*
- * @return the key to activate the selected tab.
+ * @return the key to select the focused tab.
*
* @see #getNextTabKey()
* @see #getPreviousTabKey()
@@ -2022,21 +2635,32 @@ public class VTabsheet extends VTabsheetBase
}
/**
- * @return The key code of the keyboard shortcut that selects the next tab
- * in a focused tabsheet.
+ * Returns the key code of the keyboard shortcut that focuses the next tab
+ * in a focused tabsheet.
+ *
+ * @return the key to move focus to the next tab
*/
protected int getNextTabKey() {
return KeyCodes.KEY_RIGHT;
}
/**
- * @return The key code of the keyboard shortcut that closes the currently
- * selected tab in a focused tabsheet.
+ * Returns the key code of the keyboard shortcut that closes the currently
+ * focused tab (if closable) in a focused tabsheet.
+ *
+ * @return the key to close the current tab
*/
protected int getCloseTabKey() {
return KeyCodes.KEY_DELETE;
}
+ /**
+ * Scrolls the given tab into view. If the tab is hidden on the server,
+ * nothing is done.
+ *
+ * @param tab
+ * the tab to scroll to
+ */
private void scrollIntoView(Tab tab) {
if (!tab.isHiddenOnServer()) {
@@ -2061,7 +2685,13 @@ public class VTabsheet extends VTabsheetBase
updateTabScroller();
}
if (scrollerIndex >= 0 && scrollerIndex < tb.getTabCount()) {
- scrollerPositionTabId = tb.getTab(scrollerIndex).id;
+ Tab currentFirst = tb.getTab(scrollerIndex);
+ // keep the previous keyboard focus style, focus change should
+ // be handled elsewhere if needed
+ currentFirst.setStyleNames(scrollerIndex == activeTabIndex,
+ true, currentFirst.td
+ .hasClassName(Tab.TD_FOCUS_FIRST_CLASSNAME));
+ scrollerPositionTabId = currentFirst.id;
} else {
scrollerPositionTabId = null;
}
@@ -2093,6 +2723,7 @@ public class VTabsheet extends VTabsheetBase
private static final RegExp SUBPART_TAB_REGEXP = RegExp
.compile("tab\\[(\\d+)](.*)");
+ @SuppressWarnings("deprecation")
@Override
public com.google.gwt.user.client.Element getSubPartElement(
String subPart) {
@@ -2115,6 +2746,7 @@ public class VTabsheet extends VTabsheetBase
return null;
}
+ @SuppressWarnings("deprecation")
@Override
public String getSubPartName(
com.google.gwt.user.client.Element subElement) {
diff --git a/client/src/main/java/com/vaadin/client/ui/VTabsheetBase.java b/client/src/main/java/com/vaadin/client/ui/VTabsheetBase.java
index 7a18d49d3a..ef0a0b84fd 100644
--- a/client/src/main/java/com/vaadin/client/ui/VTabsheetBase.java
+++ b/client/src/main/java/com/vaadin/client/ui/VTabsheetBase.java
@@ -30,6 +30,11 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorMap;
import com.vaadin.shared.ui.tabsheet.TabState;
+/**
+ * Base class for a multi-view widget such as TabSheet or Accordion.
+ *
+ * @author Vaadin Ltd.
+ */
public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
/** For internal use only. May be removed or replaced in the future. */
@@ -52,6 +57,13 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
private boolean tabCaptionsAsHtml = false;
+ /**
+ * Constructs a multi-view widget with the given classname.
+ *
+ * @param classname
+ * the style name to set
+ */
+ @SuppressWarnings("deprecation")
public VTabsheetBase(String classname) {
setElement(DOM.createDiv());
setStyleName(classname);
@@ -64,48 +76,69 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
/**
* Clears current tabs and contents.
+ *
+ * @deprecated This method is not called by the framework code anymore.
*/
+ @Deprecated
protected abstract void clearPaintables();
/**
* Implement in extending classes. This method should render needed elements
- * and set the visibility of the tab according to the 'selected' parameter.
+ * and set the visibility of the tab according to the 'visible' parameter.
+ * This method should not update the selection, the connector should handle
+ * that separately.
+ *
+ * @param tabState
+ * shared state of a single tab
+ * @param index
+ * the index of that tab
*/
public abstract void renderTab(TabState tabState, int index);
/**
* Implement in extending classes. This method should return the number of
* tabs currently rendered.
+ *
+ * @return the number of currently rendered tabs
*/
public abstract int getTabCount();
/**
- * Implement in extending classes. This method should return the Paintable
+ * Implement in extending classes. This method should return the connector
* corresponding to the given index.
+ *
+ * @param index
+ * the index of the tab whose connector to find
+ * @return the connector of the queried tab, or {@code null} if not found
*/
public abstract ComponentConnector getTab(int index);
/**
* Implement in extending classes. This method should remove the rendered
* tab with the specified index.
+ *
+ * @param index
+ * the index of the tab to remove
*/
public abstract void removeTab(int index);
/**
- * Returns true if the width of the widget is undefined, false otherwise.
+ * Returns whether the width of the widget is undefined.
*
* @since 7.2
- * @return true if width of the widget is determined by its content
+ * @return {@code true} if width of the widget is determined by its content,
+ * {@code false} otherwise
*/
protected boolean isDynamicWidth() {
return getConnectorForWidget(this).isUndefinedWidth();
}
/**
- * Returns true if the height of the widget is undefined, false otherwise.
+ * Returns whether the height of the widget is undefined.
*
* @since 7.2
- * @return true if width of the height is determined by its content
+ * @return {@code true} if height of the widget is determined by its
+ * content, {@code false} otherwise
*/
protected boolean isDynamicHeight() {
return getConnectorForWidget(this).isUndefinedHeight();
@@ -119,6 +152,7 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
*
* @since 7.2
* @param connector
+ * the connector of this widget
*/
public void setConnector(AbstractComponentConnector connector) {
this.connector = connector;
@@ -130,7 +164,15 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
disabledTabKeys.clear();
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param key
+ * an internal key that corresponds with a tab
+ * @param disabled
+ * {@code true} if the tab should be disabled, {@code false}
+ * otherwise
+ */
public void addTabKey(String key, boolean disabled) {
tabKeys.add(key);
if (disabled) {
@@ -138,12 +180,22 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
}
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param client
+ * the current application connection instance
+ */
public void setClient(ApplicationConnection client) {
this.client = client;
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param activeTabIndex
+ * the index of the currently active tab
+ */
public void setActiveTabIndex(int activeTabIndex) {
this.activeTabIndex = activeTabIndex;
}
@@ -154,17 +206,34 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
disabled = !enabled;
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param readonly
+ * {@code true} if this widget should be read-only, {@code false}
+ * otherwise
+ */
public void setReadonly(boolean readonly) {
this.readonly = readonly;
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param widget
+ * the widget whose connector to find
+ * @return the connector
+ */
protected ComponentConnector getConnectorForWidget(Widget widget) {
return ConnectorMap.get(client).getConnector(widget);
}
- /** For internal use only. May be removed or replaced in the future. */
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param index
+ * the index of the tab to select
+ */
public abstract void selectTab(int index);
@Override
@@ -176,6 +245,8 @@ public abstract class VTabsheetBase extends ComplexPanel implements HasEnabled {
* Sets whether the caption is rendered as HTML.
* <p>
* The default is false, i.e. render tab captions as plain text
+ * <p>
+ * This value is delegated from the TabsheetState.
*
* @since 7.4
* @param tabCaptionsAsHtml
diff --git a/client/src/main/java/com/vaadin/client/ui/VTabsheetPanel.java b/client/src/main/java/com/vaadin/client/ui/VTabsheetPanel.java
index 77a777c98b..25b402b120 100644
--- a/client/src/main/java/com/vaadin/client/ui/VTabsheetPanel.java
+++ b/client/src/main/java/com/vaadin/client/ui/VTabsheetPanel.java
@@ -27,12 +27,11 @@ import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler;
/**
* A panel that displays all of its child widgets in a 'deck', where only one
- * can be visible at a time. It is used by
- * {@link com.vaadin.client.ui.VTabsheet}.
+ * can be visible at a time. It is used by {@link VTabsheet}.
*
- * This class has the same basic functionality as the GWT DeckPanel
- * {@link com.google.gwt.user.client.ui.DeckPanel}, with the exception that it
- * doesn't manipulate the child widgets' width and height attributes.
+ * This class has the same basic functionality as the GWT
+ * {@link com.google.gwt.user.client.ui.DeckPanel DeckPanel}, with the exception
+ * that it doesn't manipulate the child widgets' width and height attributes.
*/
public class VTabsheetPanel extends ComplexPanel {
@@ -43,6 +42,7 @@ public class VTabsheetPanel extends ComplexPanel {
/**
* Creates an empty tabsheet panel.
*/
+ @SuppressWarnings("deprecation")
public VTabsheetPanel() {
setElement(DOM.createDiv());
touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this);
@@ -107,7 +107,7 @@ public class VTabsheetPanel extends ComplexPanel {
visibleWidget = null;
}
if (parent != null) {
- DOM.removeChild(getElement(), parent);
+ getElement().removeChild(parent);
}
touchScrollHandler.removeElement(parent);
}
@@ -150,6 +150,19 @@ public class VTabsheetPanel extends ComplexPanel {
e.getStyle().clearVisibility();
}
+ /**
+ * Updates the size of the visible widget.
+ *
+ * @param width
+ * the width to set (in pixels), or negative if the width should
+ * be dynamic (final width might get overridden by the minimum
+ * width if that is larger)
+ * @param height
+ * the height to set (in pixels), or negative if the height
+ * should be dynamic
+ * @param minWidth
+ * the minimum width (in pixels) that can be set
+ */
public void fixVisibleTabSize(int width, int height, int minWidth) {
if (visibleWidget == null) {
return;
@@ -190,6 +203,14 @@ public class VTabsheetPanel extends ComplexPanel {
}
}
+ /**
+ * Removes the old component and sets the new component to its place.
+ *
+ * @param oldComponent
+ * the component to remove
+ * @param newComponent
+ * the component to add to the old location
+ */
public void replaceComponent(Widget oldComponent, Widget newComponent) {
boolean isVisible = (visibleWidget == oldComponent);
int widgetIndex = getWidgetIndex(oldComponent);
diff --git a/client/src/main/java/com/vaadin/client/ui/accordion/AccordionConnector.java b/client/src/main/java/com/vaadin/client/ui/accordion/AccordionConnector.java
index 3fb5390ce3..36d0209017 100644
--- a/client/src/main/java/com/vaadin/client/ui/accordion/AccordionConnector.java
+++ b/client/src/main/java/com/vaadin/client/ui/accordion/AccordionConnector.java
@@ -56,6 +56,7 @@ public class AccordionConnector extends TabsheetBaseConnector
StackItem selectedItem = widget
.getStackItem(widget.selectedItemIndex);
+ // Only the visible child widget is present in the collection.
ComponentConnector contentConnector = getChildComponents().get(0);
if (contentConnector != null) {
selectedItem.setContent(contentConnector.getWidget());
diff --git a/client/src/main/java/com/vaadin/client/ui/tabsheet/TabsheetBaseConnector.java b/client/src/main/java/com/vaadin/client/ui/tabsheet/TabsheetBaseConnector.java
index 64315d2961..f0da80b062 100644
--- a/client/src/main/java/com/vaadin/client/ui/tabsheet/TabsheetBaseConnector.java
+++ b/client/src/main/java/com/vaadin/client/ui/tabsheet/TabsheetBaseConnector.java
@@ -63,11 +63,22 @@ public abstract class TabsheetBaseConnector
// Update member references
widget.setEnabled(isEnabled());
- // Widgets in the TabSheet before update
+ // Widgets in the TabSheet before update (should be max 1)
List<Widget> oldWidgets = new ArrayList<>();
for (Iterator<Widget> iterator = widget.getWidgetIterator(); iterator
.hasNext();) {
- oldWidgets.add(iterator.next());
+ Widget child = iterator.next();
+ // filter out any current widgets (should be max 1)
+ boolean found = false;
+ for (ComponentConnector childComponent : getChildComponents()) {
+ if (childComponent.getWidget().equals(child)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ oldWidgets.add(child);
+ }
}
// Clear previous values
@@ -95,14 +106,6 @@ public abstract class TabsheetBaseConnector
widget.removeTab(index);
}
- for (int i = 0; i < widget.getTabCount(); i++) {
- ComponentConnector p = widget.getTab(i);
- // null for PlaceHolder widgets
- if (p != null) {
- oldWidgets.remove(p.getWidget());
- }
- }
-
// Detach any old tab widget, should be max 1
for (Widget oldWidget : oldWidgets) {
if (oldWidget.isAttached()) {
diff --git a/uitest/reference-screenshots/chrome/TabKeyboardNavigationTest-testFocus_ANY_Chrome__scrolled-right-to-tab-12.png b/uitest/reference-screenshots/chrome/TabKeyboardNavigationTest-testFocus_ANY_Chrome__scrolled-right-to-tab-12.png
index 72eef0f89e..f9e67af2ab 100755
--- a/uitest/reference-screenshots/chrome/TabKeyboardNavigationTest-testFocus_ANY_Chrome__scrolled-right-to-tab-12.png
+++ b/uitest/reference-screenshots/chrome/TabKeyboardNavigationTest-testFocus_ANY_Chrome__scrolled-right-to-tab-12.png
Binary files differ
diff --git a/uitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.png b/uitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.png
index 5d1e181671..8745aa7cc2 100755
--- a/uitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.png
+++ b/uitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.png
Binary files differ
diff --git a/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResize.java b/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResize.java
new file mode 100644
index 0000000000..6584cd69a5
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResize.java
@@ -0,0 +1,20 @@
+package com.vaadin.tests.components.tabsheet;
+
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TabSheet;
+import com.vaadin.ui.TabSheet.Tab;
+
+public class ScrolledTabSheetHiddenTabsResize extends ScrolledTabSheetResize {
+
+ @Override
+ protected void populate(TabSheet tabSheet) {
+ for (int i = 0; i < 40; i++) {
+ String caption = "Tab " + i;
+ Label label = new Label(caption);
+
+ Tab tab = tabSheet.addTab(label, caption);
+ tab.setClosable(true);
+ tab.setVisible(i % 2 != 0);
+ }
+ }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResize.java b/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResize.java
index c84ba5e3ef..28b37ec4e4 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResize.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResize.java
@@ -21,6 +21,15 @@ public class ScrolledTabSheetResize extends AbstractTestUI {
TabSheet tabSheet = new TabSheet();
tabSheet.setSizeFull();
+ populate(tabSheet);
+
+ addComponent(tabSheet);
+ addComponent(new Button("use reindeer", e -> {
+ setTheme("reindeer");
+ }));
+ }
+
+ protected void populate(TabSheet tabSheet) {
for (int i = 0; i < 20; i++) {
String caption = "Tab " + i;
Label label = new Label(caption);
@@ -28,11 +37,6 @@ public class ScrolledTabSheetResize extends AbstractTestUI {
Tab tab = tabSheet.addTab(label, caption);
tab.setClosable(true);
}
-
- addComponent(tabSheet);
- addComponent(new Button("use reindeer", e -> {
- setTheme("reindeer");
- }));
}
@Override
diff --git a/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResizeTest.java b/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResizeTest.java
new file mode 100644
index 0000000000..9e3b5aaf25
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetHiddenTabsResizeTest.java
@@ -0,0 +1,31 @@
+package com.vaadin.tests.components.tabsheet;
+
+import java.util.List;
+
+import org.openqa.selenium.WebElement;
+
+public class ScrolledTabSheetHiddenTabsResizeTest
+ extends ScrolledTabSheetResizeTest {
+
+ @Override
+ public void setup() throws Exception {
+ lastVisibleTabCaption = "Tab 39";
+ super.setup();
+ }
+
+ @Override
+ protected WebElement getFirstHiddenViewable(List<WebElement> tabs) {
+ // every other tab is hidden on server, return the second-to-last tab
+ // before the first one that is visible on client
+ WebElement previous = null;
+ WebElement older = null;
+ for (WebElement tab : tabs) {
+ if (hasCssClass(tab, "v-tabsheet-tabitemcell-first")) {
+ break;
+ }
+ older = previous;
+ previous = tab;
+ }
+ return older;
+ }
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResizeTest.java b/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResizeTest.java
index 4f0ae02b43..ee16707c77 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResizeTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/tabsheet/ScrolledTabSheetResizeTest.java
@@ -7,7 +7,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.junit.Test;
import org.openqa.selenium.By;
@@ -16,10 +18,15 @@ import org.openqa.selenium.WebElement;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.testbench.elements.TabSheetElement;
+import com.vaadin.testbench.elements.UIElement;
import com.vaadin.tests.tb3.MultiBrowserTest;
public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
+ protected String lastVisibleTabCaption = "Tab 19";
+
+ private WebElement pendingTab = null;
+
@Override
public void setup() throws Exception {
super.setup();
@@ -29,13 +36,14 @@ public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
@Test
public void testReindeer() throws IOException, InterruptedException {
$(ButtonElement.class).first().click();
+ Map<String, Integer> sizes = saveWidths();
StringBuilder exceptions = new StringBuilder();
boolean failed = false;
// upper limit is determined by the amount of tabs,
// lower end by limits set by Selenium version
for (int i = 1400; i >= 650; i = i - 50) {
try {
- testResize(i);
+ testResize(i, sizes);
} catch (Exception e) {
if (failed) {
exceptions.append(" --- ");
@@ -61,13 +69,14 @@ public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
@Test
public void testValo() throws IOException, InterruptedException {
+ Map<String, Integer> sizes = saveWidths();
StringBuilder exceptions = new StringBuilder();
boolean failed = false;
// 1550 would be better for the amount of tabs (wider than for
// reindeer), but IE11 can't adjust that far
for (int i = 1500; i >= 650; i = i - 50) {
try {
- testResize(i);
+ testResize(i, sizes);
} catch (Exception e) {
if (failed) {
exceptions.append(" --- ");
@@ -91,10 +100,56 @@ public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
}
}
- private void testResize(int start)
+ private Map<String, Integer> saveWidths() {
+ // save the tab widths before any scrolling
+ TabSheetElement ts = $(TabSheetElement.class).first();
+ Map<String, Integer> sizes = new HashMap<>();
+ for (WebElement tab : ts
+ .findElements(By.className("v-tabsheet-tabitemcell"))) {
+ if (hasCssClass(tab, "v-tabsheet-tabitemcell-first")) {
+ // skip the first visible for now, it has different styling and
+ // we are interested in the non-styled width
+ pendingTab = tab;
+ continue;
+ }
+ if (pendingTab != null && tab.isDisplayed()) {
+ String currentLeft = tab.getCssValue("padding-left");
+ String pendingLeft = pendingTab.getCssValue("padding-left");
+ if (currentLeft == null || "0px".equals(currentLeft)) {
+ currentLeft = tab.findElement(By.className("v-caption"))
+ .getCssValue("margin-left");
+ pendingLeft = pendingTab
+ .findElement(By.className("v-caption"))
+ .getCssValue("margin-left");
+ }
+ if (currentLeft != pendingLeft && currentLeft.endsWith("px")
+ && pendingLeft.endsWith("px")) {
+ WebElement caption = pendingTab
+ .findElement(By.className("v-captiontext"));
+ sizes.put(caption.getAttribute("innerText"),
+ pendingTab.getSize().getWidth()
+ - intValue(pendingLeft)
+ + intValue(currentLeft));
+ }
+ pendingTab = null;
+ }
+ WebElement caption = tab.findElement(By.className("v-captiontext"));
+ sizes.put(caption.getAttribute("innerText"),
+ tab.getSize().getWidth());
+ }
+ return sizes;
+ }
+
+ private Integer intValue(String pixelString) {
+ return Integer
+ .valueOf(pixelString.substring(0, pixelString.indexOf("px")));
+ }
+
+ private void testResize(int start, Map<String, Integer> sizes)
throws IOException, InterruptedException {
- testBench().resizeViewPortTo(start, 600);
+ resizeViewPortTo(start);
waitUntilLoadingIndicatorNotVisible();
+ sleep(100); // a bit more for layouting
int iterations = 0;
while (scrollRight() && iterations < 50) {
@@ -102,28 +157,35 @@ public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
++iterations;
}
- // FIXME: TabSheet definitely still has issues,
- // but it's moving to a better direction.
-
- // Sometimes the test never realises that scrolling has
- // reached the end, but this is not critical as long as
- // the other criteria is fulfilled.
- // If we decide otherwise, uncomment the following check:
- // if (iterations >= 50) {
- // fail("scrolling right never reaches the end");
- // }
-
- // This fails on some specific widths by ~15-20 pixels, likewise
- // deemed as non-critical for now so commented out.
- // assertNoExtraRoom(start);
+ if (iterations >= 50) {
+ fail("scrolling right never reaches the end");
+ }
+ assertNoExtraRoom(start, sizes);
- testBench().resizeViewPortTo(start + 150, 600);
+ resizeViewPortTo(start + 150);
waitUntilLoadingIndicatorNotVisible();
+ sleep(100); // a bit more for layouting
- assertNoExtraRoom(start + 150);
+ assertNoExtraRoom(start + 150, sizes);
}
- private void assertNoExtraRoom(int width) {
+ private void resizeViewPortTo(int width) {
+ try {
+ testBench().resizeViewPortTo(width, 600);
+ } catch (UnsupportedOperationException e) {
+ // sometimes this exception is thrown even if resize succeeded, test
+ // validity
+ waitUntilLoadingIndicatorNotVisible();
+ UIElement ui = $(UIElement.class).first();
+ int currentWidth = ui.getSize().width;
+ if (currentWidth != width) {
+ // only throw the exception if the size didn't change
+ throw e;
+ }
+ }
+ }
+
+ private void assertNoExtraRoom(int width, Map<String, Integer> sizes) {
TabSheetElement ts = $(TabSheetElement.class).first();
WebElement scroller = ts
.findElement(By.className("v-tabsheet-scroller"));
@@ -131,19 +193,35 @@ public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
.findElements(By.className("v-tabsheet-tabitemcell"));
WebElement lastTab = tabs.get(tabs.size() - 1);
- assertEquals("Tab 19",
+ assertEquals("Unexpected last visible tab,", lastVisibleTabCaption,
lastTab.findElement(By.className("v-captiontext")).getText());
+ WebElement firstHidden = getFirstHiddenViewable(tabs);
+ if (firstHidden == null) {
+ // nothing to scroll to
+ return;
+ }
+ // the sizes change during a tab's life-cycle, use the recorded size
+ // approximation for how much extra space adding this tab would need
+ // (measuring a hidden tab would definitely give too small width)
+ WebElement caption = firstHidden
+ .findElement(By.className("v-captiontext"));
+ String captionText = caption.getAttribute("innerText");
+ Integer firstHiddenWidth = sizes.get(captionText);
+ if (firstHiddenWidth == null) {
+ firstHiddenWidth = sizes.get("Tab 3");
+ }
+
int tabWidth = lastTab.getSize().width;
int tabRight = lastTab.getLocation().x + tabWidth;
- int scrollerLeft = scroller.findElement(By.tagName("button"))
- .getLocation().x;
+ assertThat("Unexpected tab width", tabRight, greaterThan(20));
- assertThat("Not scrolled to the end (width: " + width + ")",
- scrollerLeft, greaterThan(tabRight));
- // technically this should probably be just greaterThan,
+ int scrollerLeft = scroller.getLocation().x;
+ // technically these should probably be just greaterThan,
// but one pixel's difference is irrelevant for now
- assertThat("Too big gap (width: " + width + ")", tabWidth,
+ assertThat("Not scrolled to the end (width: " + width + ")",
+ scrollerLeft, greaterThanOrEqualTo(tabRight));
+ assertThat("Too big gap (width: " + width + ")", firstHiddenWidth,
greaterThanOrEqualTo(scrollerLeft - tabRight));
}
@@ -163,4 +241,18 @@ public class ScrolledTabSheetResizeTest extends MultiBrowserTest {
}
}
+ /*
+ * There is no way to differentiate between hidden-on-server and
+ * hidden-on-client here, so this method has to be overridable.
+ */
+ protected WebElement getFirstHiddenViewable(List<WebElement> tabs) {
+ WebElement previous = null;
+ for (WebElement tab : tabs) {
+ if (hasCssClass(tab, "v-tabsheet-tabitemcell-first")) {
+ break;
+ }
+ previous = tab;
+ }
+ return previous;
+ }
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/tabsheet/TabSheetFocusedTabTest.java b/uitest/src/test/java/com/vaadin/tests/components/tabsheet/TabSheetFocusedTabTest.java
index b8921bd0a7..15059fd324 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/tabsheet/TabSheetFocusedTabTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/tabsheet/TabSheetFocusedTabTest.java
@@ -24,22 +24,53 @@ public class TabSheetFocusedTabTest extends MultiBrowserTest {
getTab(1).click();
- assertTrue(isFocused(getTab(1)));
+ assertTrue("Tab 1 should have been focused but wasn't.",
+ isFocused(getTab(1)));
new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform();
- assertFalse(isFocused(getTab(1)));
- assertTrue(isFocused(getTab(3)));
+ assertFalse("Tab 1 was focused but shouldn't have been.",
+ isFocused(getTab(1)));
+ assertTrue("Tab 3 should have been focused but wasn't.",
+ isFocused(getTab(3)));
getTab(5).click();
- assertFalse(isFocused(getTab(3)));
- assertTrue(isFocused(getTab(5)));
+ assertFalse("Tab 3 was focused but shouldn't have been.",
+ isFocused(getTab(3)));
+ assertTrue("Tab 5 should have been focused but wasn't.",
+ isFocused(getTab(5)));
getTab(1).click();
- assertFalse(isFocused(getTab(5)));
- assertTrue(isFocused(getTab(1)));
+ assertFalse("Tab 5 was focused but shouldn't have been.",
+ isFocused(getTab(5)));
+ assertTrue("Tab 1 should have been focused but wasn't.",
+ isFocused(getTab(1)));
+ }
+
+ @Test
+ public void scrollingChangesFocusedTab() {
+ openTestURL();
+
+ getTab(7).click();
+
+ assertTrue("Tab 7 should have been focused but wasn't.",
+ isFocused(getTab(7)));
+
+ findElement(By.className("v-tabsheet-scrollerNext")).click();
+
+ assertFalse("Tab 7 was focused but shouldn't have been.",
+ isFocused(getTab(7)));
+ assertTrue("Tab 3 should have been focused but wasn't.",
+ isFocused(getTab(3)));
+
+ new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform();
+
+ assertFalse("Tab 3 was focused but shouldn't have been.",
+ isFocused(getTab(3)));
+ assertTrue("Tab 5 should have been focused but wasn't.",
+ isFocused(getTab(5)));
}
private WebElement getTab(int index) {
@@ -49,7 +80,6 @@ public class TabSheetFocusedTabTest extends MultiBrowserTest {
}
private boolean isFocused(WebElement tab) {
-
return tab.getAttribute("class").contains("v-tabsheet-tabitem-focus");
}