diff options
author | Anna Koskinen <Ansku@users.noreply.github.com> | 2021-08-13 17:05:50 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-13 17:05:50 +0300 |
commit | 54230dfa058c49bbe9e825c77cf82d9c0a170c4c (patch) | |
tree | 5cf04c471d838d521fc07b7832cd697f9b1b2a0e | |
parent | 5dd5cdca20b5ece095057c852b3a40f01bc95298 (diff) | |
download | vaadin-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.
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 Binary files differindex 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 diff --git a/uitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.png b/uitest/reference-screenshots/chrome/TabSheetFocusingTest-addAndFocusTabs_ANY_Chrome__tabsAdded.png Binary files differindex 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 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"); } |