]> source.dussan.org Git - vaadin-framework.git/commitdiff
TabSheet focus/blur events fixed (#14304)
authorBogdan Udrescu <bogdan@vaadin.com>
Thu, 7 Aug 2014 12:31:07 +0000 (15:31 +0300)
committerBogdan Udrescu <bogdan@vaadin.com>
Thu, 14 Aug 2014 14:04:39 +0000 (17:04 +0300)
The blur and focus events should be linked with the TabSheet
component as a whole. Any click inside the TabSheet should
trigger one single focus and any leave the blur.

Change-Id: Id24a2fab12aafe6f7aa3a44635e5b9e935a1cfe1

WebContent/html-tests/ComponentFocus.html [new file with mode: 0644]
client/src/com/vaadin/client/ui/VTabsheet.java
uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigation.html [deleted file]
uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigation.java
uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigationTest.java [new file with mode: 0644]

diff --git a/WebContent/html-tests/ComponentFocus.html b/WebContent/html-tests/ComponentFocus.html
new file mode 100644 (file)
index 0000000..0a82252
--- /dev/null
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Insert title here</title>
+
+<script type="text/javascript">
+
+</script>
+
+</head>
+<body>
+<button onfocus="console.log('button focus');" onblur="console.log('button blur');">Focus me</button>
+<textarea onfocus="console.log('textarea focus');" onblur="console.log('textarea blur');">Focus me too</textarea>
+</body>
+</html>
\ No newline at end of file
index 35fdebf3532abf99dcc81010a103c0aa8f267962..a2a6889e151a8c6765429a523db8538e5455ad96 100644 (file)
@@ -1,12 +1,12 @@
 /*
  * Copyright 2000-2014 Vaadin Ltd.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -42,9 +42,12 @@ import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.HasBlurHandlers;
 import com.google.gwt.event.dom.client.HasFocusHandlers;
 import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasMouseDownHandlers;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.regexp.shared.MatchResult;
 import com.google.gwt.regexp.shared.RegExp;
@@ -74,7 +77,9 @@ import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc;
 import com.vaadin.shared.ui.tabsheet.TabsheetState;
 
 public class VTabsheet extends VTabsheetBase implements Focusable,
-        FocusHandler, BlurHandler, KeyDownHandler, SubPartAware {
+        SubPartAware,
+        // TODO: These listeners are due to be removed in 7.3
+        FocusHandler, BlurHandler, KeyDownHandler {
 
     private static class VCloseEvent {
         private Tab tab;
@@ -95,10 +100,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     /**
      * Representation of a single "tab" shown in the TabBar
-     * 
+     *
      */
     public static class Tab extends SimplePanel implements HasFocusHandlers,
-            HasBlurHandlers, HasKeyDownHandlers {
+            HasBlurHandlers, HasMouseDownHandlers, HasKeyDownHandlers {
         private static final String TD_CLASSNAME = CLASSNAME + "-tabitemcell";
         private static final String TD_FIRST_CLASSNAME = TD_CLASSNAME
                 + "-first";
@@ -152,10 +157,6 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
             Roles.getTabRole().setAriaLabelledbyProperty(getElement(),
                     Id.of(tabCaption.getElement()));
-
-            addFocusHandler(getTabsheet());
-            addBlurHandler(getTabsheet());
-            addKeyDownHandler(getTabsheet());
         }
 
         public boolean isHiddenOnServer() {
@@ -197,7 +198,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
         /**
          * Toggles the style names for the Tab
-         * 
+         *
          * @param selected
          *            true if the Tab is selected
          * @param first
@@ -281,6 +282,11 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
             return addDomHandler(handler, BlurEvent.getType());
         }
 
+        @Override
+        public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
+            return addDomHandler(handler, MouseDownEvent.getType());
+        }
+
         @Override
         public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
             return addDomHandler(handler, KeyDownEvent.getType());
@@ -420,8 +426,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     }
 
-    static class TabBar extends ComplexPanel implements ClickHandler,
-            VCloseHandler {
+    static class TabBar extends ComplexPanel implements VCloseHandler {
 
         private final Element tr = DOM.createTR();
 
@@ -443,6 +448,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
             setStyleName(spacerTd, CLASSNAME + "-spacertd");
             DOM.appendChild(tr, spacerTd);
             DOM.appendChild(spacerTd, DOM.createDiv());
+
             setElement(el);
         }
 
@@ -460,10 +466,20 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
             return DOM.asOld(tr);
         }
 
+        /**
+         * Gets the number of tabs from the tab bar.
+         *
+         * @return the number of tabs from the tab bar.
+         */
         public int getTabCount() {
             return getWidgetCount();
         }
 
+        /**
+         * Adds a tab to the tab bar.
+         *
+         * @return the added tab.
+         */
         public Tab addTab() {
             Tab t = new Tab(this);
             int tabIndex = getTabCount();
@@ -476,31 +492,18 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
                 t.setStyleNames(false, true);
             }
 
-            t.addClickHandler(this);
+            getTabsheet().selectionHandler.registerTab(t);
+
             t.setCloseHandler(this);
 
             return t;
         }
 
-        @Override
-        public void onClick(ClickEvent event) {
-            TabCaption caption = (TabCaption) event.getSource();
-            Element targetElement = event.getNativeEvent().getEventTarget()
-                    .cast();
-            // the tab should not be focused if the close button was clicked
-            if (targetElement == caption.getCloseButton()) {
-                return;
-            }
-
-            int index = getWidgetIndex(caption.getParent());
-
-            navigateTab(getTabsheet().focusedTabIndex, index);
-            getTabsheet().focusedTabIndex = index;
-            getTabsheet().focusedTab = getTab(index);
-            getTabsheet().focus();
-            getTabsheet().loadTabSheet(index);
-        }
-
+        /**
+         * Gets the tab sheet instance where the tab bar is attached to.
+         *
+         * @return the tab sheet instance where the tab bar is attached to.
+         */
         public VTabsheet getTabsheet() {
             return tabsheet;
         }
@@ -538,7 +541,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
             getTab(tabsheet.activeTabIndex).recalculateCaptionWidth();
         }
 
-        public void navigateTab(int fromIndex, int toIndex) {
+        public Tab navigateTab(int fromIndex, int toIndex) {
             Tab newNavigated = getTab(toIndex);
             if (newNavigated == null) {
                 throw new IllegalArgumentException(
@@ -553,6 +556,8 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
                 oldNavigated.setStyleNames(oldNavigated.equals(selected),
                         isFirstVisibleTab(fromIndex), false);
             }
+
+            return newNavigated;
         }
 
         public void removeTab(int i) {
@@ -580,7 +585,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
         /**
          * Returns the index of the first visible tab
-         * 
+         *
          * @return
          */
         private int getFirstVisibleTab() {
@@ -589,7 +594,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
         /**
          * Find the next visible tab. Returns -1 if none is found.
-         * 
+         *
          * @param i
          * @return
          */
@@ -608,7 +613,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
         /**
          * Find the previous visible tab. Returns -1 if none is found.
-         * 
+         *
          * @param i
          * @return
          */
@@ -663,7 +668,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
     /** For internal use only. May be removed or replaced in the future. */
     // tabbar and 'scroller' container
     public final Element tabs;
-    Tab focusedTab;
+
     /**
      * The tabindex property (position in the browser's focus cycle.) Named like
      * this to avoid confusion with activeTabIndex.
@@ -697,9 +702,6 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     private String currentStyle;
 
-    /** For internal use only. May be removed or replaced in the future. */
-    private int focusedTabIndex = 0;
-
     /**
      * @return Whether the tab could be selected or not.
      */
@@ -720,11 +722,13 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     /**
      * Load the content of a tab of the provided index.
-     * 
+     *
      * @param index
      *            of the tab to load
+     *
+     * @return true if the specified sheet gets loaded, otherwise false.
      */
-    public void loadTabSheet(int tabIndex) {
+    public boolean loadTabSheet(int tabIndex) {
         if (activeTabIndex != tabIndex && canSelectTab(tabIndex)) {
             tb.selectTab(tabIndex);
 
@@ -741,12 +745,16 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
             waitingForResponse = true;
 
             tb.getTab(tabIndex).focus(); // move keyboard focus to active tab
+
+            return true;
         }
+
+        return false;
     }
 
     /**
      * Returns the currently displayed widget in the tab panel.
-     * 
+     *
      * @since 7.2
      * @return currently displayed content widget
      */
@@ -756,7 +764,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     /**
      * Returns the client to server RPC proxy for the tabsheet.
-     * 
+     *
      * @since 7.2
      * @return RPC proxy
      */
@@ -766,10 +774,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     /**
      * For internal use only.
-     * 
+     *
      * Avoid using this method directly and use appropriate superclass methods
      * where applicable.
-     * 
+     *
      * @deprecated since 7.2 - use more specific methods instead (getRpcProxy(),
      *             getConnectorForWidget(Widget) etc.)
      * @return ApplicationConnection
@@ -799,9 +807,6 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
     public VTabsheet() {
         super(CLASSNAME);
 
-        addHandler(this, FocusEvent.getType());
-        addHandler(this, BlurEvent.getType());
-
         // Tab scrolling
         getElement().getStyle().setOverflow(Overflow.HIDDEN);
         tabs = DOM.createDiv();
@@ -812,18 +817,21 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
         Roles.getTablistRole().setAriaHiddenState(scroller, true);
 
         DOM.setElementProperty(scroller, "className", SCROLLER_CLASSNAME);
+
         scrollerPrev = DOM.createButton();
         scrollerPrev.setTabIndex(-1);
         DOM.setElementProperty(scrollerPrev, "className", SCROLLER_CLASSNAME
                 + "Prev");
         Roles.getTablistRole().setAriaHiddenState(scrollerPrev, true);
-        DOM.sinkEvents(scrollerPrev, Event.ONCLICK);
+        DOM.sinkEvents(scrollerPrev, Event.ONCLICK | Event.ONMOUSEDOWN);
+
         scrollerNext = DOM.createButton();
         scrollerNext.setTabIndex(-1);
         DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME
                 + "Next");
         Roles.getTablistRole().setAriaHiddenState(scrollerNext, true);
-        DOM.sinkEvents(scrollerNext, Event.ONCLICK);
+        DOM.sinkEvents(scrollerNext, Event.ONCLICK | Event.ONMOUSEDOWN);
+
         DOM.appendChild(getElement(), tabs);
 
         // Tabs
@@ -856,35 +864,68 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     @Override
     public void onBrowserEvent(Event event) {
+        com.google.gwt.dom.client.Element eventTarget = DOM
+                .eventGetTarget(event);
+
         if (event.getTypeInt() == Event.ONCLICK) {
+
             // Tab scrolling
-            if (isScrolledTabs() && DOM.eventGetTarget(event) == scrollerPrev) {
-                int newFirstIndex = tb.scrollLeft(scrollerIndex);
-                if (newFirstIndex != -1) {
-                    scrollerIndex = newFirstIndex;
-                    updateTabScroller();
-                }
-                event.stopPropagation();
-                return;
-            } else if (isClippedTabs()
-                    && DOM.eventGetTarget(event) == scrollerNext) {
-                int newFirstIndex = tb.scrollRight(scrollerIndex);
+            if (eventTarget == scrollerPrev || eventTarget == scrollerNext) {
+                scrollAccordingToScrollTarget(eventTarget);
 
-                if (newFirstIndex != -1) {
-                    scrollerIndex = newFirstIndex;
-                    updateTabScroller();
-                }
                 event.stopPropagation();
+            }
+
+        } else if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+
+            if (eventTarget == scrollerPrev || eventTarget == scrollerNext) {
+                // 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.
+                focusBlurManager.cancelNextBlurSchedule();
+
                 return;
             }
         }
+
         super.onBrowserEvent(event);
     }
 
+    /*
+     * Scroll the tab bar according to the last scrollTarget (the scroll button
+     * pressed).
+     */
+    private void scrollAccordingToScrollTarget(
+            com.google.gwt.dom.client.Element scrollTarget) {
+        if (scrollTarget == null) {
+            return;
+        }
+
+        int newFirstIndex = -1;
+
+        // Scroll left.
+        if (isScrolledTabs() && scrollTarget == scrollerPrev) {
+            newFirstIndex = tb.scrollLeft(scrollerIndex);
+
+            // Scroll right.
+        } else if (isClippedTabs() && scrollTarget == scrollerNext) {
+            newFirstIndex = tb.scrollRight(scrollerIndex);
+        }
+
+        if (newFirstIndex != -1) {
+            scrollerIndex = newFirstIndex;
+            updateTabScroller();
+        }
+
+        // For this to work well, make sure the method gets called only from
+        // user events.
+        selectionHandler.focusTabAtIndex(scrollerIndex);
+    }
+
     /**
      * Checks if the tab with the selected index has been scrolled out of the
      * view (on the left side).
-     * 
+     *
      * @param index
      * @return
      */
@@ -1016,7 +1057,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     /**
      * Renders the widget content for a tab sheet.
-     * 
+     *
      * @param newWidget
      */
     public void renderContent(Widget newWidget) {
@@ -1239,69 +1280,444 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
     }
 
     @Override
-    public void onBlur(BlurEvent event) {
-        getVTooltip().hideTooltip();
+    public void focus() {
+        getActiveTab().focus();
+    }
 
-        if (focusedTab != null && focusedTab == event.getSource()) {
-            focusedTab.removeAssistiveDescription();
-            focusedTab = null;
-            if (connector.hasEventListener(EventId.BLUR)) {
-                connector.getRpcProxy(FocusAndBlurServerRpc.class).blur();
-            }
-        }
+    public void blur() {
+        getActiveTab().blur();
     }
 
-    @Override
-    public void onFocus(FocusEvent event) {
-        if (focusedTab == null && event.getSource() instanceof Tab) {
-            focusedTab = (Tab) event.getSource();
+    /*
+     * Gets the active tab.
+     */
+    private Tab getActiveTab() {
+        return tb.getTab(activeTabIndex);
+    }
+
+    /*
+     * The focus and blur manager instance.
+     */
+    private FocusBlurManager focusBlurManager = new FocusBlurManager();
+
+    /*
+     * Manage the TabSheet component blur event.
+     */
+    private class FocusBlurManager implements FocusedTabProvider {
+
+        // The real tab with focus on it. If the focus goes to another element
+        // in the page this will be null.
+        private Tab focusedTab;
+
+        @Override
+        public Tab getFocusedTab() {
+            return focusedTab;
+        }
+
+        @Override
+        public void setFocusedTab(Tab focusedTab) {
+            this.focusedTab = focusedTab;
+        }
+
+        /**
+         * Process the focus event no matter where it occurs on the tab bar.
+         * We've added this method
+         *
+         * @since
+         * @param newFocusTab
+         *            the new focused tab.
+         * @see #blur(Tab)
+         */
+        public void focus(Tab newFocusTab) {
+
             if (connector.hasEventListener(EventId.FOCUS)) {
-                connector.getRpcProxy(FocusAndBlurServerRpc.class).focus();
+
+                // Send the focus event only first time when we focus on any
+                // tab. The focused tab will be reseted on the last blur.
+                if (focusedTab == null) {
+                    connector.getRpcProxy(FocusAndBlurServerRpc.class).focus();
+                }
             }
 
+            removeBlurSchedule();
+
+            setFocusedTab(newFocusTab);
+
             if (focusedTab.hasTooltip()) {
                 focusedTab.setAssistiveDescription(getVTooltip().getUniqueId());
                 getVTooltip().showAssistive(focusedTab.getTooltipInfo());
             }
+
+        }
+
+        /**
+         * Process the blur event for the current focusedTab.
+         *
+         * @param blurSource
+         *            the source of the blur.
+         *
+         * @see #focus(Tab)
+         */
+        public void blur(Tab blurSource) {
+            if (focusedTab != null && focusedTab == blurSource) {
+
+                if (connector.hasEventListener(EventId.BLUR)) {
+                    scheduleBlur(focusedTab);
+                }
+            }
+        }
+
+        /*
+         * The last blur command to be executed.
+         */
+        private BlurCommand blurCommand;
+
+        /**
+         * Schedule a new blur event for a deferred execution.
+         */
+        public void scheduleBlur(Tab blurSource) {
+
+            if (cancelNextBlurSchedule) {
+
+                // This will set the stopNextBlurCommand back to false as well.
+                removeBlurSchedule();
+
+                // Leave this though to make things clear for the developer.
+                cancelNextBlurSchedule = false;
+                return;
+            }
+
+            removeBlurSchedule();
+
+            blurCommand = new BlurCommand(blurSource, this);
+            blurCommand.scheduleDeferred();
+        }
+
+        /**
+         * Remove the last blur command from execution.
+         */
+        public void removeBlurSchedule() {
+            if (blurCommand != null) {
+                blurCommand.stopSchedule();
+                blurCommand = null;
+            }
+
+            // Maybe this is not the best place to reset this, but we really
+            // want to make sure it'll reset at any time when something
+            // interact with the blur manager. Might change it later.
+            cancelNextBlurSchedule = false;
+        }
+
+        /**
+         * Cancel the next possible scheduled execution.
+         */
+        public void cancelNextBlurSchedule() {
+
+            // Make sure there's still no other command to be executed.
+            removeBlurSchedule();
+
+            cancelNextBlurSchedule = true;
+        }
+
+        // Stupid workaround...
+        private boolean cancelNextBlurSchedule = false;
+
+    }
+
+    /*
+     * Wraps the focused tab.
+     */
+    private interface FocusedTabProvider {
+
+        /**
+         * Gets the focused Tab.
+         *
+         * @return the focused Tab.
+         */
+        Tab getFocusedTab();
+
+        /**
+         * Sets the focused Tab.
+         *
+         * @param focusedTab
+         *            the focused Tab.
+         */
+        void setFocusedTab(Tab focusedTab);
+
+    }
+
+    /*
+     * Execute the final blur command.
+     */
+    private class BlurCommand implements Command {
+
+        /*
+         * The blur source.
+         */
+        private Tab blurSource;
+
+        /*
+         * Provide the current focused Tab.
+         */
+        private FocusedTabProvider focusedTabProvider;
+
+        /**
+         * Create the blur command using the blur source.
+         *
+         * @param blurSource
+         *            the source.
+         * @param focusedTabProvider
+         *            provides the current focused tab.
+         */
+        public BlurCommand(Tab blurSource, FocusedTabProvider focusedTabProvider) {
+            this.blurSource = blurSource;
+            this.focusedTabProvider = focusedTabProvider;
+        }
+
+        /**
+         * Stop the command from being executed.
+         *
+         * @since
+         */
+        public void stopSchedule() {
+            blurSource = null;
+        }
+
+        /**
+         * Schedule the command for a deferred execution.
+         *
+         * @since
+         */
+        public void scheduleDeferred() {
+            Scheduler.get().scheduleDeferred(this);
+        }
+
+        @Override
+        public void execute() {
+
+            Tab focusedTab = focusedTabProvider.getFocusedTab();
+
+            if (blurSource == null) {
+                return;
+            }
+
+            // The focus didn't change since this blur triggered, so
+            // the new focused element is not a tab.
+            if (focusedTab == blurSource) {
+
+                // We're certain there's no focus anymore.
+                focusedTab.removeAssistiveDescription();
+                focusedTabProvider.setFocusedTab(null);
+
+                connector.getRpcProxy(FocusAndBlurServerRpc.class).blur();
+            }
+
+            // Call this to set it to null and be consistent.
+            focusBlurManager.removeBlurSchedule();
         }
     }
 
     @Override
-    public void focus() {
-        tb.getTab(activeTabIndex).focus();
+    public void onBlur(BlurEvent event) {
+        selectionHandler.onBlur(event);
     }
 
-    public void blur() {
-        tb.getTab(activeTabIndex).blur();
+    @Override
+    public void onFocus(FocusEvent event) {
+        selectionHandler.onFocus(event);
     }
 
     @Override
     public void onKeyDown(KeyDownEvent event) {
-        if (event.getSource() instanceof Tab) {
-            int keycode = event.getNativeEvent().getKeyCode();
-
-            // Scroll throw the tabs.
-            if (!event.isAnyModifierKeyDown()) {
-                if (keycode == getPreviousTabKey()) {
-                    selectPreviousTab();
-                    event.stopPropagation();
-                } else if (keycode == getNextTabKey()) {
-                    selectNextTab();
-                    event.stopPropagation();
-                } else if (keycode == getCloseTabKey()) {
-                    Tab tab = tb.getTab(activeTabIndex);
-                    if (tab.isClosable()) {
-                        tab.onClose();
-                    }
-                } else if (keycode == getSelectTabKey()) {
-                    loadTabSheet(focusedTabIndex);
+        selectionHandler.onKeyDown(event);
+    }
+
+    /*
+     * The tabs selection handler instance.
+     */
+    private final TabSelectionHandler selectionHandler = new TabSelectionHandler();
+
+    /*
+     * Handle the events for selecting the tabs.
+     */
+    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.
+        private int focusedTabIndex = 0;
+
+        /**
+         * Register the tab to the selection handler.
+         *
+         * @param tab
+         *            the tab to register.
+         */
+        public void registerTab(Tab tab) {
+
+            // TODO: change VTabsheet.this to this in 7.3
+            tab.addBlurHandler(VTabsheet.this);
+            tab.addFocusHandler(VTabsheet.this);
+            tab.addKeyDownHandler(VTabsheet.this);
+
+            tab.addClickHandler(this);
+            tab.addMouseDownHandler(this);
+        }
+
+        @Override
+        public void onBlur(final BlurEvent event) {
+
+            getVTooltip().hideTooltip();
+
+            Object blurSource = event.getSource();
+
+            if (blurSource instanceof Tab) {
+                focusBlurManager.blur((Tab) blurSource);
+            }
+        }
+
+        @Override
+        public void onFocus(FocusEvent event) {
+
+            if (event.getSource() instanceof Tab) {
+                focusBlurManager.focus((Tab) event.getSource());
+            }
+        }
+
+        @Override
+        public void onClick(ClickEvent event) {
+
+            // IE doesn't trigger focus when click, so we need to make sure
+            // the previous blur deferred command will get killed.
+            focusBlurManager.removeBlurSchedule();
+
+            TabCaption caption = (TabCaption) event.getSource();
+            Element targetElement = event.getNativeEvent().getEventTarget()
+                    .cast();
+            // the tab should not be focused if the close button was clicked
+            if (targetElement == caption.getCloseButton()) {
+                return;
+            }
+
+            int index = tb.getWidgetIndex(caption.getParent());
+
+            tb.navigateTab(focusedTabIndex, index);
 
-                    // Prevent the page from scrolling when hitting space
-                    // (select key) to select the current tab.
-                    event.preventDefault();
+            focusedTabIndex = index;
+
+            if (!loadTabSheet(index)) {
+
+                // This needs to be called at the end, as the activeTabIndex
+                // is set in the loadTabSheet.
+                focus();
+            }
+        }
+
+        @Override
+        public void onMouseDown(MouseDownEvent event) {
+
+            if (event.getSource() instanceof Tab) {
+
+                // IE doesn't trigger focus when click, so we need to make sure
+                // the
+                // next blur deferred command will get killed.
+                focusBlurManager.cancelNextBlurSchedule();
+            }
+        }
+
+        @Override
+        public void onKeyDown(KeyDownEvent event) {
+            if (event.getSource() instanceof Tab) {
+                int keycode = event.getNativeEvent().getKeyCode();
+
+                if (!event.isAnyModifierKeyDown()) {
+                    if (keycode == getPreviousTabKey()) {
+                        selectPreviousTab();
+                        event.stopPropagation();
+
+                    } else if (keycode == getNextTabKey()) {
+                        selectNextTab();
+                        event.stopPropagation();
+
+                    } else if (keycode == getCloseTabKey()) {
+                        Tab tab = tb.getTab(activeTabIndex);
+                        if (tab.isClosable()) {
+                            tab.onClose();
+                        }
+
+                    } else if (keycode == getSelectTabKey()) {
+                        loadTabSheet(focusedTabIndex);
+
+                        // Prevent the page from scrolling when hitting space
+                        // (select key) to select the current tab.
+                        event.preventDefault();
+                    }
                 }
             }
         }
+
+        /*
+         * Left arrow key selection.
+         */
+        private void selectPreviousTab() {
+            int newTabIndex = focusedTabIndex;
+            // Find the previous visible and enabled tab if any.
+            do {
+                newTabIndex--;
+            } while (newTabIndex >= 0 && !canSelectTab(newTabIndex));
+
+            if (newTabIndex >= 0) {
+                keySelectTab(newTabIndex);
+            }
+        }
+
+        /*
+         * Right arrow key selection.
+         */
+        private void selectNextTab() {
+            int newTabIndex = focusedTabIndex;
+            // Find the next visible and enabled tab if any.
+            do {
+                newTabIndex++;
+            } while (newTabIndex < getTabCount() && !canSelectTab(newTabIndex));
+
+            if (newTabIndex < getTabCount()) {
+                keySelectTab(newTabIndex);
+            }
+        }
+
+        /*
+         * Select the specified tab using left/right key.
+         */
+        private void keySelectTab(int newTabIndex) {
+            Tab tab = tb.getTab(newTabIndex);
+            if (tab == null) {
+                return;
+            }
+
+            // Focus the tab, otherwise the selected one will loose focus and
+            // TabSheet will get blurred.
+            focusTabAtIndex(newTabIndex);
+
+            tb.navigateTab(focusedTabIndex, newTabIndex);
+
+            focusedTabIndex = newTabIndex;
+        }
+
+        /**
+         * Focus the specified tab. Make sure to call this only from user
+         * events, otherwise will break things.
+         *
+         * @param tabIndex
+         *            the index of the tab to set.
+         */
+        void focusTabAtIndex(int tabIndex) {
+            Tab tabToFocus = tb.getTab(tabIndex);
+            if (tabToFocus != null) {
+                tabToFocus.focus();
+            }
+        }
+
     }
 
     /**
@@ -1341,66 +1757,32 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
         return KeyCodes.KEY_DELETE;
     }
 
-    private void selectPreviousTab() {
-        int newTabIndex = focusedTabIndex;
-        // Find the previous visible and enabled tab if any.
-        do {
-            newTabIndex--;
-        } while (newTabIndex >= 0 && !canSelectTab(newTabIndex));
-
-        if (newTabIndex >= 0) {
-            tb.navigateTab(focusedTabIndex, newTabIndex);
-            focusedTabIndex = newTabIndex;
-
-            // If this TabSheet already has focus, set the new selected tab
-            // as focused.
-            if (focusedTab != null) {
-                focusedTab = tb.getTab(focusedTabIndex);
-                focusedTab.focus();
-            }
-        }
-    }
-
-    private void selectNextTab() {
-        int newTabIndex = focusedTabIndex;
-        // Find the next visible and enabled tab if any.
-        do {
-            newTabIndex++;
-        } while (newTabIndex < getTabCount() && !canSelectTab(newTabIndex));
-
-        if (newTabIndex < getTabCount()) {
+    private void scrollIntoView(Tab tab) {
 
-            tb.navigateTab(focusedTabIndex, newTabIndex);
-            focusedTabIndex = newTabIndex;
+        if (!tab.isHiddenOnServer()) {
 
-            // If this TabSheet already has focus, set the new selected tab
-            // as focused.
-            if (focusedTab != null) {
-                focusedTab = tb.getTab(focusedTabIndex);
-                focusedTab.focus();
-            }
-        }
-    }
+            // Check for visibility first as clipped tabs to the right are
+            // always visible.
+            // On IE8 a tab with false visibility would have the bounds of the
+            // full TabBar.
+            if (!tab.isVisible()) {
+                while (!tab.isVisible()) {
+                    scrollerIndex = tb.scrollLeft(scrollerIndex);
+                }
+                updateTabScroller();
 
-    private void scrollIntoView(Tab tab) {
-        if (!tab.isHiddenOnServer()) {
-            if (isClipped(tab)) {
+            } else if (isClipped(tab)) {
                 while (isClipped(tab) && scrollerIndex != -1) {
                     scrollerIndex = tb.scrollRight(scrollerIndex);
                 }
                 updateTabScroller();
-            } else if (!tab.isVisible()) {
-                while (!tab.isVisible()) {
-                    scrollerIndex = tb.scrollLeft(scrollerIndex);
-                }
-                updateTabScroller();
             }
         }
     }
 
     /**
      * Makes tab bar visible.
-     * 
+     *
      * @since 7.2
      */
     public void showTabs() {
@@ -1411,7 +1793,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable,
 
     /**
      * Makes tab bar invisible.
-     * 
+     *
      * @since 7.2
      */
     public void hideTabs() {
diff --git a/uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigation.html b/uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigation.html
deleted file mode 100644 (file)
index 8259881..0000000
+++ /dev/null
@@ -1,241 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-<title>TabKeyboardNavigation</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
-<thead>
-<tr><td rowspan="1" colspan="3">TabKeyboardNavigation</td></tr>
-</thead><tbody>
-<tr>
-       <td>open</td>
-       <td>/run/TabKeyboardNavigation?restartApplication</td>
-       <td></td>
-</tr>
-<tr>
-       <td>mouseClick</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>9,8</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pause</td>
-       <td>1000</td>
-       <td>1000</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[1]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 1</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>space</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[1]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 2</td>
-</tr>
-<tr>
-       <td>screenCapture</td>
-       <td></td>
-       <td>tab2</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[1]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[2]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pause</td>
-       <td>1000</td>
-       <td>1000</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[3]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 2</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[2]</td>
-       <td>space</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[3]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 5</td>
-</tr>
-<tr>
-       <td>screenCapture</td>
-       <td></td>
-       <td>skip-disabled-to-tab5</td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pause</td>
-       <td>1000</td>
-       <td>1000</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[4]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 5</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>space</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[4]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 6</td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>click</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[0]/VButton[0]/domChild[0]/domChild[0]</td>
-       <td></td>
-</tr>
-<tr>
-       <td>mouseClick</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[8]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>18,10</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[8]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[9]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[10]</td>
-       <td>right</td>
-</tr>
-<tr>
-       <td>pause</td>
-       <td>1000</td>
-       <td>1000</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[8]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 9</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[10]</td>
-       <td>space</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[8]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 12</td>
-</tr>
-<tr>
-       <td>screenCapture</td>
-       <td></td>
-       <td>scrolled-right-to-tab-12</td>
-</tr>
-<tr>
-       <td>mouseClick</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[4]/domChild[0]/domChild[0]/domChild[0]</td>
-       <td>11,2</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[4]</td>
-       <td>left</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[2]</td>
-       <td>left</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[1]</td>
-       <td>left</td>
-</tr>
-<tr>
-       <td>pause</td>
-       <td>1000</td>
-       <td>1000</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[0]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 5</td>
-</tr>
-<tr>
-       <td>pressSpecialKey</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/domChild[0]/domChild[0]/domChild[0]/domChild[0]/domChild[1]</td>
-       <td>space</td>
-</tr>
-<tr>
-       <td>assertText</td>
-       <td>vaadin=runTabKeyboardNavigation::/VVerticalLayout[0]/ChildComponentContainer[1]/VVerticalLayout[0]/ChildComponentContainer[4]/VTabsheet[0]/VTabsheetPanel[0]/VVerticalLayout[0]/ChildComponentContainer[0]/VLabel[0]</td>
-       <td>Tab 1</td>
-</tr>
-<tr>
-       <td>screenCapture</td>
-       <td></td>
-       <td>scrolled-left-to-tab-1</td>
-</tr>
-
-</tbody></table>
-</body>
-</html>
index ba737f1df85063cecbe9947aa97137db7d649f1b..620f04fe6058cf7e865b782138eda89b28a87bf3 100644 (file)
@@ -6,7 +6,8 @@ import com.vaadin.event.FieldEvents.BlurEvent;
 import com.vaadin.event.FieldEvents.BlurListener;
 import com.vaadin.event.FieldEvents.FocusEvent;
 import com.vaadin.event.FieldEvents.FocusListener;
-import com.vaadin.tests.components.TestBase;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
 import com.vaadin.tests.util.Log;
 import com.vaadin.ui.Button;
 import com.vaadin.ui.Button.ClickEvent;
@@ -19,7 +20,16 @@ import com.vaadin.ui.TabSheet.Tab;
 import com.vaadin.ui.TextField;
 import com.vaadin.ui.VerticalLayout;
 
-public class TabKeyboardNavigation extends TestBase {
+/**
+ * Test if the click and key tab selection in a tabsheet generate the correct
+ * focus/blur events.
+ * 
+ * The solution was broken in ticket (#14304)
+ * 
+ * @since
+ * @author Vaadin Ltd
+ */
+public class TabKeyboardNavigation extends AbstractTestUI {
 
     int index = 1;
     ArrayList<Component> tabs = new ArrayList<Component>();
@@ -27,18 +37,18 @@ public class TabKeyboardNavigation extends TestBase {
     Log focusblur = new Log(10);
 
     @Override
-    protected void setup() {
+    protected void setup(VaadinRequest request) {
         ts.setWidth("500px");
         ts.setHeight("500px");
 
-        ts.addListener(new FocusListener() {
+        ts.addFocusListener(new FocusListener() {
             @Override
             public void focus(FocusEvent event) {
                 focusblur.log("Tabsheet focused!");
             }
         });
 
-        ts.addListener(new BlurListener() {
+        ts.addBlurListener(new BlurListener() {
             @Override
             public void blur(BlurEvent event) {
                 focusblur.log("Tabsheet blurred!");
@@ -74,7 +84,7 @@ public class TabKeyboardNavigation extends TestBase {
     }
 
     @Override
-    protected String getDescription() {
+    protected String getTestDescription() {
         return "The tab bar should be focusable and arrow keys should switch tabs. The del key should close a tab if closable.";
     }
 
@@ -83,10 +93,18 @@ public class TabKeyboardNavigation extends TestBase {
         return 5100;
     }
 
+    public final static String LABEL_ID = "sheetLabel";
+
+    public final static String labelID(int index) {
+        return LABEL_ID + index;
+    }
+
     private Tab addTab() {
         Layout content = new VerticalLayout();
         tabs.add(content);
-        content.addComponent(new Label("Tab " + index));
+        Label label = new Label("Tab " + index);
+        label.setId(labelID(index));
+        content.addComponent(label);
         content.addComponent(new TextField());
         Tab tab = ts.addTab(content, "Tab " + index, null);
         if (index == 2) {
diff --git a/uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigationTest.java b/uitest/src/com/vaadin/tests/components/tabsheet/TabKeyboardNavigationTest.java
new file mode 100644 (file)
index 0000000..65307f9
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.components.tabsheet;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+/**
+ * Add TB3 test as the TB2 one failed on keyboard events.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class TabKeyboardNavigationTest extends MultiBrowserTest {
+
+    @Test
+    public void testFocus() throws InterruptedException, IOException {
+        openTestURL();
+
+        click(1);
+        sendKeys(1, Keys.ARROW_RIGHT);
+
+        assertSheet(1);
+        sendKeys(2, Keys.SPACE);
+        assertSheet(2);
+        compareScreen("tab2");
+
+        sendKeys(2, Keys.ARROW_RIGHT);
+        sendKeys(3, Keys.ARROW_RIGHT);
+        assertSheet(2);
+
+        sendKeys(5, Keys.SPACE);
+        assertSheet(5);
+        compareScreen("skip-disabled-to-tab5");
+
+        TestBenchElement addTabButton = (TestBenchElement) getDriver()
+                .findElements(By.className("v-button")).get(0);
+
+        click(addTabButton);
+
+        click(5);
+        sendKeys(5, Keys.ARROW_RIGHT);
+        assertSheet(5);
+
+        sendKeys(6, Keys.SPACE);
+        assertSheet(6);
+
+        click(addTabButton);
+        click(addTabButton);
+        click(addTabButton);
+        click(addTabButton);
+        click(addTabButton);
+        click(addTabButton);
+
+        click(8);
+        compareScreen("click-tab-8");
+
+        sendKeys(8, Keys.ARROW_RIGHT);
+        sendKeys(9, Keys.SPACE);
+        click(9);
+        compareScreen("tab-9");
+
+        sendKeys(9, Keys.ARROW_RIGHT);
+        Thread.sleep(DELAY);
+
+        sendKeys(10, Keys.ARROW_RIGHT);
+
+        // Here PhantomJS used to fail. Or when accessing tab2. The fix was to
+        // call the elem.click(x, y) using the (x, y) position instead of the
+        // elem.click() without any arguments.
+        sendKeys(11, Keys.ARROW_RIGHT);
+
+        assertSheet(9);
+        sendKeys(12, Keys.SPACE);
+        assertSheet(12);
+        compareScreen("scrolled-right-to-tab-12");
+
+        click(5);
+
+        sendKeys(5, Keys.ARROW_LEFT);
+
+        // Here IE8 used to fail. A hidden <div> in IE8 would have the bounds of
+        // it's parent, and when trying to see in which direction to scroll
+        // (left or right) to make the key selected tab visible, the
+        // VTabSheet.scrollIntoView(Tab) used to check first whether the tab
+        // isClipped. On IE8 this will always return true for both hidden tabs
+        // on the left and clipped tabs on the right. So instead of going to
+        // left, it'll search all the way to the right.
+        sendKeys(3, Keys.ARROW_LEFT);
+        sendKeys(2, Keys.ARROW_LEFT);
+        assertSheet(5);
+
+        sendKeys(1, Keys.SPACE);
+        assertSheet(1);
+        compareScreen("scrolled-left-to-tab-1");
+    }
+
+    /*
+     * Press key on the element.
+     */
+    private void sendKeys(int tabIndex, Keys key) throws InterruptedException {
+        sendKeys(tab(tabIndex), key);
+    }
+
+    /*
+     * Press key on the element.
+     */
+    private void sendKeys(TestBenchElement element, Keys key)
+            throws InterruptedException {
+
+        element.sendKeys(key);
+        if (DELAY > 0) {
+            sleep(DELAY);
+        }
+    }
+
+    /*
+     * Click on the element.
+     */
+    private void click(int tabIndex) throws InterruptedException {
+        click(tab(tabIndex));
+    }
+
+    /*
+     * Click on the element.
+     */
+    private void click(TestBenchElement element) throws InterruptedException {
+
+        element.click(10, 10);
+        if (DELAY > 0) {
+            sleep(DELAY);
+        }
+    }
+
+    /*
+     * Delay for PhantomJS.
+     */
+    private final static int DELAY = 10;
+
+    private void assertSheet(int index) {
+        String labelCaption = "Tab " + index;
+
+        By id = By.id(TabKeyboardNavigation.labelID(index));
+        WebElement labelElement = getDriver().findElement(id);
+
+        waitForElementPresent(id);
+
+        Assert.assertEquals(labelCaption, labelCaption, labelElement.getText());
+    }
+
+    /*
+     * Provide the tab at specified index.
+     */
+    private TestBenchElement tab(int index) {
+        By by = By.className("v-tabsheet-tabitemcell");
+
+        TestBenchElement element = (TestBenchElement) getDriver().findElements(
+                by).get(index - 1);
+
+        String expected = "Tab " + index;
+        Assert.assertEquals(expected,
+                element.getText().substring(0, expected.length()));
+
+        return element;
+    }
+
+}