- 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.tags/8.14.0.alpha1
import com.vaadin.client.VCaption; | import com.vaadin.client.VCaption; | ||||
import com.vaadin.client.WidgetUtil; | import com.vaadin.client.WidgetUtil; | ||||
import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler; | import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler; | ||||
import com.vaadin.client.ui.VAccordion.StackItem; | |||||
import com.vaadin.shared.ComponentConstants; | import com.vaadin.shared.ComponentConstants; | ||||
import com.vaadin.shared.ui.accordion.AccordionState; | import com.vaadin.shared.ui.accordion.AccordionState; | ||||
import com.vaadin.shared.ui.tabsheet.TabState; | import com.vaadin.shared.ui.tabsheet.TabState; | ||||
import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc; | import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc; | ||||
import com.vaadin.shared.util.SharedUtil; | 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 { | public class VAccordion extends VTabsheetBase { | ||||
/** Default classname for this widget. */ | |||||
public static final String CLASSNAME = AccordionState.PRIMARY_STYLE_NAME; | public static final String CLASSNAME = AccordionState.PRIMARY_STYLE_NAME; | ||||
private Set<Widget> widgets = new HashSet<>(); | private Set<Widget> widgets = new HashSet<>(); | ||||
private int tabulatorIndex; | private int tabulatorIndex; | ||||
/** | |||||
* Constructs a widget for an Accordion. | |||||
*/ | |||||
public VAccordion() { | public VAccordion() { | ||||
super(CLASSNAME); | super(CLASSNAME); | ||||
touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); | touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); | ||||
} | } | ||||
@SuppressWarnings("deprecation") | |||||
@Override | @Override | ||||
public void renderTab(TabState tabState, int index) { | public void renderTab(TabState tabState, int index) { | ||||
StackItem item; | StackItem item; | ||||
int itemIndex; | |||||
if (getWidgetCount() <= index) { | if (getWidgetCount() <= index) { | ||||
// Create stackItem and render caption | // Create stackItem and render caption | ||||
if (getWidgetCount() == 0) { | if (getWidgetCount() == 0) { | ||||
item.addStyleDependentName("first"); | item.addStyleDependentName("first"); | ||||
} | } | ||||
itemIndex = getWidgetCount(); | |||||
add(item, getElement()); | add(item, getElement()); | ||||
} else { | } else { | ||||
item = getStackItem(index); | item = getStackItem(index); | ||||
itemIndex = index; | |||||
} | } | ||||
item.updateCaption(tabState); | item.updateCaption(tabState); | ||||
updateStyleNames(style); | 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) { | protected void updateStyleNames(String primaryStyleName) { | ||||
for (Widget w : getChildren()) { | for (Widget w : getChildren()) { | ||||
if (w instanceof StackItem) { | if (w instanceof StackItem) { | ||||
/** | /** | ||||
* For internal use only. May be renamed or removed in a future release. | * 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 | * @param tabIndex | ||||
* tabulator index for the open stack item | * tabulator index for the open stack item | ||||
} | } | ||||
} | } | ||||
/** 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) { | public void open(int itemIndex) { | ||||
StackItem item = (StackItem) getWidget(itemIndex); | StackItem item = (StackItem) getWidget(itemIndex); | ||||
boolean alreadyOpen = false; | boolean alreadyOpen = false; | ||||
} | } | ||||
/** 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) { | public void close(StackItem item) { | ||||
if (!item.isOpen()) { | if (!item.isOpen()) { | ||||
return; | return; | ||||
} | } | ||||
/** | |||||
* Handle stack item selection. | |||||
* | |||||
* @param item | |||||
* the selected stack item | |||||
*/ | |||||
public void onSelectTab(StackItem item) { | public void onSelectTab(StackItem item) { | ||||
final int index = getWidgetIndex(item); | final int index = getWidgetIndex(item); | ||||
private Widget widget; | private Widget widget; | ||||
private String id; | 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) { | public void setHeight(int height) { | ||||
if (height == -1) { | if (height == -1) { | ||||
super.setHeight(""); | super.setHeight(""); | ||||
} | } | ||||
} | } | ||||
/** | |||||
* Sets the identifier for this stack item. | |||||
* | |||||
* @param newId | |||||
* the identifier to set | |||||
*/ | |||||
public void setId(String newId) { | public void setId(String newId) { | ||||
if (!SharedUtil.equals(newId, id)) { | if (!SharedUtil.equals(newId, id)) { | ||||
if (id != null) { | if (id != null) { | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 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() { | 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() { | public void setHeightFromWidget() { | ||||
Widget widget = getChildWidget(); | Widget widget = getChildWidget(); | ||||
if (widget == null) { | if (widget == null) { | ||||
/** | /** | ||||
* Returns caption width including padding. | * 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() { | public int getCaptionWidth() { | ||||
if (caption == null) { | if (caption == null) { | ||||
return captionWidth + padding; | 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) { | public void setWidth(int width) { | ||||
if (width == -1) { | if (width == -1) { | ||||
super.setWidth(""); | super.setWidth(""); | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 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() { | public int getHeight() { | ||||
return getOffsetHeight(); | return getOffsetHeight(); | ||||
} | } | ||||
/** | |||||
* Returns the offset height of the caption node. | |||||
* | |||||
* @return the height in pixels | |||||
*/ | |||||
public int getCaptionHeight() { | public int getCaptionHeight() { | ||||
return captionNode.getOffsetHeight(); | return captionNode.getOffsetHeight(); | ||||
} | } | ||||
private Element captionNode = DOM.createDiv(); | private Element captionNode = DOM.createDiv(); | ||||
private String styleName; | private String styleName; | ||||
/** | |||||
* Constructs a stack item. The content widget should be set later when | |||||
* the stack item is opened. | |||||
*/ | |||||
@SuppressWarnings("deprecation") | |||||
public StackItem() { | public StackItem() { | ||||
setElement(DOM.createDiv()); | setElement(DOM.createDiv()); | ||||
caption = new VCaption(client); | caption = new VCaption(client); | ||||
onSelectTab(this); | 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() { | public com.google.gwt.user.client.Element getContainerElement() { | ||||
return DOM.asOld(content); | return DOM.asOld(content); | ||||
} | } | ||||
/** | |||||
* Returns the wrapped widget of this stack item. | |||||
* | |||||
* @return the widget | |||||
*/ | |||||
public Widget getChildWidget() { | public Widget getChildWidget() { | ||||
return widget; | 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) { | public void replaceWidget(Widget newWidget) { | ||||
if (widget != null) { | if (widget != null) { | ||||
widgets.remove(widget); | widgets.remove(widget); | ||||
} | } | ||||
/** | |||||
* Opens the stack item and clears any previous visibility settings. | |||||
*/ | |||||
public void open() { | public void open() { | ||||
add(widget, content); | add(widget, content); | ||||
open = true; | open = true; | ||||
getElement().setTabIndex(tabulatorIndex); | 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() { | public void hide() { | ||||
content.getStyle().setVisibility(Visibility.HIDDEN); | 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() { | public void close() { | ||||
if (widget != null) { | if (widget != null) { | ||||
remove(widget); | remove(widget); | ||||
getElement().setTabIndex(-1); | getElement().setTabIndex(-1); | ||||
} | } | ||||
/** | |||||
* Returns whether this stack item is open or not. | |||||
* | |||||
* @return {@code true} if open, {@code false} otherwise | |||||
*/ | |||||
public boolean isOpen() { | public boolean isOpen() { | ||||
return open; | return open; | ||||
} | } | ||||
onSelectTab(this); | 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) { | 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.setCaptionAsHtml(isTabCaptionsAsHtml()); | ||||
caption.updateCaptionWithoutOwner(tabState.caption, | caption.updateCaptionWithoutOwner(tabState.caption, | ||||
!tabState.enabled, hasAttribute(tabState.description), | !tabState.enabled, hasAttribute(tabState.description), | ||||
} | } | ||||
/** | /** | ||||
* 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) { | private void updateTabStyleName(String newStyleName) { | ||||
if (newStyleName != null && !newStyleName.isEmpty()) { | if (newStyleName != null && !newStyleName.isEmpty()) { | ||||
} | } | ||||
} | } | ||||
/** | |||||
* Returns the offset width of the wrapped widget. | |||||
* | |||||
* @return the offset width in pixels, or zero if no widget is set | |||||
*/ | |||||
public int getWidgetWidth() { | 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) { | public boolean contains(ComponentConnector p) { | ||||
return (getChildWidget() == p.getWidget()); | 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() { | public boolean isCaptionVisible() { | ||||
return caption.isVisible(); | return caption.isVisible(); | ||||
} | } | ||||
} | } | ||||
/** | |||||
* {@inheritDoc} | |||||
* | |||||
* @deprecated This method is not called by the framework code anymore. | |||||
*/ | |||||
@Deprecated | |||||
@Override | @Override | ||||
protected void clearPaintables() { | protected void clearPaintables() { | ||||
clear(); | clear(); | ||||
return null; | 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) { | public StackItem getStackItem(int index) { | ||||
return (StackItem) getWidget(index); | return (StackItem) getWidget(index); | ||||
} | } | ||||
/** | |||||
* Returns an iterable over all the stack items. | |||||
* | |||||
* @return the iterable | |||||
*/ | |||||
@SuppressWarnings({ "rawtypes", "unchecked" }) | |||||
public Iterable<StackItem> getStackItems() { | public Iterable<StackItem> getStackItems() { | ||||
return (Iterable) getChildren(); | 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() { | public StackItem getOpenStackItem() { | ||||
return openTab; | return openTab; | ||||
} | } |
import com.vaadin.client.ConnectorMap; | import com.vaadin.client.ConnectorMap; | ||||
import com.vaadin.shared.ui.tabsheet.TabState; | 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 { | 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. */ | ||||
private boolean tabCaptionsAsHtml = false; | 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) { | public VTabsheetBase(String classname) { | ||||
setElement(DOM.createDiv()); | setElement(DOM.createDiv()); | ||||
setStyleName(classname); | setStyleName(classname); | ||||
/** | /** | ||||
* Clears current tabs and contents. | * Clears current tabs and contents. | ||||
* | |||||
* @deprecated This method is not called by the framework code anymore. | |||||
*/ | */ | ||||
@Deprecated | |||||
protected abstract void clearPaintables(); | protected abstract void clearPaintables(); | ||||
/** | /** | ||||
* Implement in extending classes. This method should render needed elements | * 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); | public abstract void renderTab(TabState tabState, int index); | ||||
/** | /** | ||||
* Implement in extending classes. This method should return the number of | * Implement in extending classes. This method should return the number of | ||||
* tabs currently rendered. | * tabs currently rendered. | ||||
* | |||||
* @return the number of currently rendered tabs | |||||
*/ | */ | ||||
public abstract int getTabCount(); | 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. | * 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); | public abstract ComponentConnector getTab(int index); | ||||
/** | /** | ||||
* Implement in extending classes. This method should remove the rendered | * Implement in extending classes. This method should remove the rendered | ||||
* tab with the specified index. | * tab with the specified index. | ||||
* | |||||
* @param index | |||||
* the index of the tab to remove | |||||
*/ | */ | ||||
public abstract void removeTab(int index); | 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 | * @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() { | protected boolean isDynamicWidth() { | ||||
return getConnectorForWidget(this).isUndefinedWidth(); | 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 | * @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() { | protected boolean isDynamicHeight() { | ||||
return getConnectorForWidget(this).isUndefinedHeight(); | return getConnectorForWidget(this).isUndefinedHeight(); | ||||
* | * | ||||
* @since 7.2 | * @since 7.2 | ||||
* @param connector | * @param connector | ||||
* the connector of this widget | |||||
*/ | */ | ||||
public void setConnector(AbstractComponentConnector connector) { | public void setConnector(AbstractComponentConnector connector) { | ||||
this.connector = connector; | this.connector = connector; | ||||
disabledTabKeys.clear(); | 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) { | public void addTabKey(String key, boolean disabled) { | ||||
tabKeys.add(key); | tabKeys.add(key); | ||||
if (disabled) { | if (disabled) { | ||||
} | } | ||||
} | } | ||||
/** 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) { | public void setClient(ApplicationConnection client) { | ||||
this.client = 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) { | public void setActiveTabIndex(int activeTabIndex) { | ||||
this.activeTabIndex = activeTabIndex; | this.activeTabIndex = activeTabIndex; | ||||
} | } | ||||
disabled = !enabled; | 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) { | public void setReadonly(boolean readonly) { | ||||
this.readonly = 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) { | protected ComponentConnector getConnectorForWidget(Widget widget) { | ||||
return ConnectorMap.get(client).getConnector(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); | public abstract void selectTab(int index); | ||||
@Override | @Override | ||||
* Sets whether the caption is rendered as HTML. | * Sets whether the caption is rendered as HTML. | ||||
* <p> | * <p> | ||||
* The default is false, i.e. render tab captions as plain text | * The default is false, i.e. render tab captions as plain text | ||||
* <p> | |||||
* This value is delegated from the TabsheetState. | |||||
* | * | ||||
* @since 7.4 | * @since 7.4 | ||||
* @param tabCaptionsAsHtml | * @param tabCaptionsAsHtml |
/** | /** | ||||
* A panel that displays all of its child widgets in a 'deck', where only one | * 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 { | public class VTabsheetPanel extends ComplexPanel { | ||||
/** | /** | ||||
* Creates an empty tabsheet panel. | * Creates an empty tabsheet panel. | ||||
*/ | */ | ||||
@SuppressWarnings("deprecation") | |||||
public VTabsheetPanel() { | public VTabsheetPanel() { | ||||
setElement(DOM.createDiv()); | setElement(DOM.createDiv()); | ||||
touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); | touchScrollHandler = TouchScrollDelegate.enableTouchScrolling(this); | ||||
visibleWidget = null; | visibleWidget = null; | ||||
} | } | ||||
if (parent != null) { | if (parent != null) { | ||||
DOM.removeChild(getElement(), parent); | |||||
getElement().removeChild(parent); | |||||
} | } | ||||
touchScrollHandler.removeElement(parent); | touchScrollHandler.removeElement(parent); | ||||
} | } | ||||
e.getStyle().clearVisibility(); | 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) { | public void fixVisibleTabSize(int width, int height, int minWidth) { | ||||
if (visibleWidget == null) { | if (visibleWidget == null) { | ||||
return; | return; | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 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) { | public void replaceComponent(Widget oldComponent, Widget newComponent) { | ||||
boolean isVisible = (visibleWidget == oldComponent); | boolean isVisible = (visibleWidget == oldComponent); | ||||
int widgetIndex = getWidgetIndex(oldComponent); | int widgetIndex = getWidgetIndex(oldComponent); |
StackItem selectedItem = widget | StackItem selectedItem = widget | ||||
.getStackItem(widget.selectedItemIndex); | .getStackItem(widget.selectedItemIndex); | ||||
// Only the visible child widget is present in the collection. | |||||
ComponentConnector contentConnector = getChildComponents().get(0); | ComponentConnector contentConnector = getChildComponents().get(0); | ||||
if (contentConnector != null) { | if (contentConnector != null) { | ||||
selectedItem.setContent(contentConnector.getWidget()); | selectedItem.setContent(contentConnector.getWidget()); |
// Update member references | // Update member references | ||||
widget.setEnabled(isEnabled()); | widget.setEnabled(isEnabled()); | ||||
// Widgets in the TabSheet before update | |||||
// Widgets in the TabSheet before update (should be max 1) | |||||
List<Widget> oldWidgets = new ArrayList<>(); | List<Widget> oldWidgets = new ArrayList<>(); | ||||
for (Iterator<Widget> iterator = widget.getWidgetIterator(); iterator | for (Iterator<Widget> iterator = widget.getWidgetIterator(); iterator | ||||
.hasNext();) { | .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 | // Clear previous values | ||||
widget.removeTab(index); | 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 | // Detach any old tab widget, should be max 1 | ||||
for (Widget oldWidget : oldWidgets) { | for (Widget oldWidget : oldWidgets) { | ||||
if (oldWidget.isAttached()) { | if (oldWidget.isAttached()) { |
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); | |||||
} | |||||
} | |||||
} |
TabSheet tabSheet = new TabSheet(); | TabSheet tabSheet = new TabSheet(); | ||||
tabSheet.setSizeFull(); | 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++) { | for (int i = 0; i < 20; i++) { | ||||
String caption = "Tab " + i; | String caption = "Tab " + i; | ||||
Label label = new Label(caption); | Label label = new Label(caption); | ||||
Tab tab = tabSheet.addTab(label, caption); | Tab tab = tabSheet.addTab(label, caption); | ||||
tab.setClosable(true); | tab.setClosable(true); | ||||
} | } | ||||
addComponent(tabSheet); | |||||
addComponent(new Button("use reindeer", e -> { | |||||
setTheme("reindeer"); | |||||
})); | |||||
} | } | ||||
@Override | @Override |
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; | |||||
} | |||||
} |
import static org.junit.Assert.fail; | import static org.junit.Assert.fail; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.util.HashMap; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | |||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.openqa.selenium.By; | import org.openqa.selenium.By; | ||||
import com.vaadin.testbench.TestBenchElement; | import com.vaadin.testbench.TestBenchElement; | ||||
import com.vaadin.testbench.elements.ButtonElement; | import com.vaadin.testbench.elements.ButtonElement; | ||||
import com.vaadin.testbench.elements.TabSheetElement; | import com.vaadin.testbench.elements.TabSheetElement; | ||||
import com.vaadin.testbench.elements.UIElement; | |||||
import com.vaadin.tests.tb3.MultiBrowserTest; | import com.vaadin.tests.tb3.MultiBrowserTest; | ||||
public class ScrolledTabSheetResizeTest extends MultiBrowserTest { | public class ScrolledTabSheetResizeTest extends MultiBrowserTest { | ||||
protected String lastVisibleTabCaption = "Tab 19"; | |||||
private WebElement pendingTab = null; | |||||
@Override | @Override | ||||
public void setup() throws Exception { | public void setup() throws Exception { | ||||
super.setup(); | super.setup(); | ||||
@Test | @Test | ||||
public void testReindeer() throws IOException, InterruptedException { | public void testReindeer() throws IOException, InterruptedException { | ||||
$(ButtonElement.class).first().click(); | $(ButtonElement.class).first().click(); | ||||
Map<String, Integer> sizes = saveWidths(); | |||||
StringBuilder exceptions = new StringBuilder(); | StringBuilder exceptions = new StringBuilder(); | ||||
boolean failed = false; | boolean failed = false; | ||||
// upper limit is determined by the amount of tabs, | // upper limit is determined by the amount of tabs, | ||||
// lower end by limits set by Selenium version | // lower end by limits set by Selenium version | ||||
for (int i = 1400; i >= 650; i = i - 50) { | for (int i = 1400; i >= 650; i = i - 50) { | ||||
try { | try { | ||||
testResize(i); | |||||
testResize(i, sizes); | |||||
} catch (Exception e) { | } catch (Exception e) { | ||||
if (failed) { | if (failed) { | ||||
exceptions.append(" --- "); | exceptions.append(" --- "); | ||||
@Test | @Test | ||||
public void testValo() throws IOException, InterruptedException { | public void testValo() throws IOException, InterruptedException { | ||||
Map<String, Integer> sizes = saveWidths(); | |||||
StringBuilder exceptions = new StringBuilder(); | StringBuilder exceptions = new StringBuilder(); | ||||
boolean failed = false; | boolean failed = false; | ||||
// 1550 would be better for the amount of tabs (wider than for | // 1550 would be better for the amount of tabs (wider than for | ||||
// reindeer), but IE11 can't adjust that far | // reindeer), but IE11 can't adjust that far | ||||
for (int i = 1500; i >= 650; i = i - 50) { | for (int i = 1500; i >= 650; i = i - 50) { | ||||
try { | try { | ||||
testResize(i); | |||||
testResize(i, sizes); | |||||
} catch (Exception e) { | } catch (Exception e) { | ||||
if (failed) { | if (failed) { | ||||
exceptions.append(" --- "); | exceptions.append(" --- "); | ||||
} | } | ||||
} | } | ||||
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 { | throws IOException, InterruptedException { | ||||
testBench().resizeViewPortTo(start, 600); | |||||
resizeViewPortTo(start); | |||||
waitUntilLoadingIndicatorNotVisible(); | waitUntilLoadingIndicatorNotVisible(); | ||||
sleep(100); // a bit more for layouting | |||||
int iterations = 0; | int iterations = 0; | ||||
while (scrollRight() && iterations < 50) { | while (scrollRight() && iterations < 50) { | ||||
++iterations; | ++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(); | 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(); | TabSheetElement ts = $(TabSheetElement.class).first(); | ||||
WebElement scroller = ts | WebElement scroller = ts | ||||
.findElement(By.className("v-tabsheet-scroller")); | .findElement(By.className("v-tabsheet-scroller")); | ||||
.findElements(By.className("v-tabsheet-tabitemcell")); | .findElements(By.className("v-tabsheet-tabitemcell")); | ||||
WebElement lastTab = tabs.get(tabs.size() - 1); | WebElement lastTab = tabs.get(tabs.size() - 1); | ||||
assertEquals("Tab 19", | |||||
assertEquals("Unexpected last visible tab,", lastVisibleTabCaption, | |||||
lastTab.findElement(By.className("v-captiontext")).getText()); | 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 tabWidth = lastTab.getSize().width; | ||||
int tabRight = lastTab.getLocation().x + tabWidth; | 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 | // 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)); | greaterThanOrEqualTo(scrollerLeft - tabRight)); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
/* | |||||
* 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; | |||||
} | |||||
} | } |
getTab(1).click(); | 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(); | 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(); | 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(); | 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) { | private WebElement getTab(int index) { | ||||
} | } | ||||
private boolean isFocused(WebElement tab) { | private boolean isFocused(WebElement tab) { | ||||
return tab.getAttribute("class").contains("v-tabsheet-tabitem-focus"); | return tab.getAttribute("class").contains("v-tabsheet-tabitem-focus"); | ||||
} | } | ||||