]> source.dussan.org Git - vaadin-framework.git/commitdiff
Make focus circulate in modal dialog to improve accessibility (#10311)
authorAdam Wagner <wbadam@users.noreply.github.com>
Mon, 13 Nov 2017 08:57:01 +0000 (10:57 +0200)
committerOlli Tietäväinen <ollit@vaadin.com>
Mon, 13 Nov 2017 08:57:01 +0000 (10:57 +0200)
* Make focus circulate in modal dialog to improve accessibility (#10260)

Make focus circulate in modal dialog to improve accessibility

* Backport window order event

* Correct since tag

* Correct copyright header

client/src/main/java/com/vaadin/client/ui/FocusUtil.java
client/src/main/java/com/vaadin/client/ui/VWindow.java
client/src/main/java/com/vaadin/client/ui/window/WindowOrderEvent.java [new file with mode: 0644]
client/src/main/java/com/vaadin/client/ui/window/WindowOrderHandler.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/window/ModalWindowFocus.java
uitest/src/test/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpenedTest.java

index 27a1015d77bd02a9ea830f606e3e9ab742d01f85..7b55fa958c17d35d1ab84cd311abf4228cfdea14 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.vaadin.client.ui;
 
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.user.client.ui.Focusable;
 import com.google.gwt.user.client.ui.Widget;
 
@@ -95,4 +96,26 @@ public class FocusUtil {
 
         return focusable.getElement().getTabIndex();
     }
+
+    public static native Element[] getFocusableChildren(Element parent)
+    /*-{
+        var focusableChildren = parent.querySelectorAll('[type][tabindex]:not([tabindex="-1"]), [role=button][tabindex]:not([tabindex="-1"])');
+        return focusableChildren;
+    }-*/;
+
+    public static void focusOnFirstFocusableElement(Element parent)
+    {
+        Element[] focusableChildren = getFocusableChildren(parent);
+        if (focusableChildren.length > 0) {
+            focusableChildren[0].focus();
+        }
+    }
+
+    public static void focusOnLastFocusableElement(Element parent)
+    {
+        Element[] focusableChildren = getFocusableChildren(parent);
+        if (focusableChildren.length > 0) {
+            focusableChildren[focusableChildren.length - 1].focus();
+        }
+    }
 }
index cfdaecdb9072579d331a1df4b1cf9d04d183370c..17bfd04ad8fba585a7434006f98927bedb173fcf 100644 (file)
@@ -46,6 +46,7 @@ import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.dom.client.ScrollEvent;
 import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.event.shared.HandlerManager;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DOM;
@@ -66,6 +67,8 @@ import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
 import com.vaadin.client.ui.aria.AriaHelper;
 import com.vaadin.client.ui.window.WindowMoveEvent;
 import com.vaadin.client.ui.window.WindowMoveHandler;
+import com.vaadin.client.ui.window.WindowOrderEvent;
+import com.vaadin.client.ui.window.WindowOrderHandler;
 import com.vaadin.shared.Connector;
 import com.vaadin.shared.EventId;
 import com.vaadin.shared.ui.window.WindowMode;
@@ -79,7 +82,10 @@ import com.vaadin.shared.ui.window.WindowRole;
 public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         ScrollHandler, KeyDownHandler, FocusHandler, BlurHandler, Focusable {
 
-    private static ArrayList<VWindow> windowOrder = new ArrayList<VWindow>();
+    private static List<VWindow> windowOrder = new ArrayList<VWindow>();
+
+    private static final HandlerManager WINDOW_ORDER_HANDLER = new HandlerManager(
+            VWindow.class);
 
     private static boolean orderingDefered;
 
@@ -301,14 +307,37 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
     }
 
     public void bringToFront() {
-        int curIndex = windowOrder.indexOf(this);
+        bringToFront(true);
+    }
+
+    private void bringToFront(boolean notifyListeners) {
+        int curIndex = getWindowOrder();
         if (curIndex + 1 < windowOrder.size()) {
             windowOrder.remove(this);
             windowOrder.add(this);
             for (; curIndex < windowOrder.size(); curIndex++) {
-                windowOrder.get(curIndex).setWindowOrder(curIndex);
+                VWindow window = windowOrder.get(curIndex);
+                window.setWindowOrder(curIndex);
             }
         }
+        if (notifyListeners) {
+            fireOrderEvent();
+        }
+    }
+
+    static void fireOrderEvent() {
+        fireOrderEvent(windowOrder);
+    }
+
+    private void doFireOrderEvent() {
+        List<VWindow> list = new ArrayList<VWindow>();
+        list.add(this);
+        fireOrderEvent(list);
+    }
+
+    private static void fireOrderEvent(List<VWindow> windows) {
+        WINDOW_ORDER_HANDLER.fireEvent(
+                new WindowOrderEvent(new ArrayList<VWindow>(windows)));
     }
 
     /**
@@ -321,7 +350,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
     }
 
     private static VWindow getTopmostWindow() {
-        if (windowOrder.size() > 0) {
+        if (!windowOrder.isEmpty()) {
             return windowOrder.get(windowOrder.size() - 1);
         }
         return null;
@@ -340,13 +369,22 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         windowOrder.add(this);
         setPopupPosition(order * STACKING_OFFSET_PIXELS,
                 order * STACKING_OFFSET_PIXELS);
-
+        doFireOrderEvent();
     }
 
     private void setWindowOrder(int order) {
         setZIndex(order + Z_INDEX);
     }
 
+    /**
+     * Returns window position in list of opened and shown windows.
+     *
+     * @since 7.7.12
+     */
+    public final int getWindowOrder() {
+        return windowOrder.indexOf(this);
+    }
+
     @Override
     protected void setZIndex(int zIndex) {
         super.setZIndex(zIndex);
@@ -424,16 +462,22 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         Roles.getDialogRole().setAriaLabelledbyProperty(getElement(),
                 Id.of(headerText));
 
-        // Handlers to Prevent tab to leave the window
+        // Handlers to Prevent tab to leave the window (by circulating focus)
         // and backspace to cause browser navigation
         topEventBlocker = new NativePreviewHandler() {
             @Override
             public void onPreviewNativeEvent(NativePreviewEvent event) {
+                if (!getElement()
+                        .isOrHasChild(WidgetUtil.getFocusedElement())) {
+                    return;
+                }
                 NativeEvent nativeEvent = event.getNativeEvent();
                 if (nativeEvent.getEventTarget().cast() == topTabStop
                         && nativeEvent.getKeyCode() == KeyCodes.KEY_TAB
                         && nativeEvent.getShiftKey()) {
                     nativeEvent.preventDefault();
+                    FocusUtil.focusOnLastFocusableElement(
+                            VWindow.this.getElement());
                 }
                 if (nativeEvent.getEventTarget().cast() == topTabStop
                         && nativeEvent.getKeyCode() == KeyCodes.KEY_BACKSPACE) {
@@ -445,11 +489,17 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         bottomEventBlocker = new NativePreviewHandler() {
             @Override
             public void onPreviewNativeEvent(NativePreviewEvent event) {
+                if (!getElement()
+                        .isOrHasChild(WidgetUtil.getFocusedElement())) {
+                    return;
+                }
                 NativeEvent nativeEvent = event.getNativeEvent();
                 if (nativeEvent.getEventTarget().cast() == bottomTabStop
                         && nativeEvent.getKeyCode() == KeyCodes.KEY_TAB
                         && !nativeEvent.getShiftKey()) {
                     nativeEvent.preventDefault();
+                    FocusUtil.focusOnFirstFocusableElement(
+                            VWindow.this.getElement());
                 }
                 if (nativeEvent.getEventTarget().cast() == bottomTabStop
                         && nativeEvent.getKeyCode() == KeyCodes.KEY_BACKSPACE) {
@@ -539,27 +589,28 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
 
             @Override
             public int compare(VWindow o1, VWindow o2) {
-                /*
-                 * Order by modality, then by bringtofront sequence.
-                 */
 
+            /*
+             * Order by modality, then by bringtofront sequence.
+             */
                 if (o1.vaadinModality && !o2.vaadinModality) {
                     return 1;
-                } else if (!o1.vaadinModality && o2.vaadinModality) {
+                }
+                if (!o1.vaadinModality && o2.vaadinModality) {
                     return -1;
-                } else if (o1.bringToFrontSequence > o2.bringToFrontSequence) {
+                }
+                if (o1.bringToFrontSequence > o2.bringToFrontSequence) {
                     return 1;
-                } else if (o1.bringToFrontSequence < o2.bringToFrontSequence) {
+                }
+                if (o1.bringToFrontSequence < o2.bringToFrontSequence) {
                     return -1;
-                } else {
-                    return 0;
                 }
+                return 0;
             }
         });
-        for (int i = 0; i < array.length; i++) {
-            VWindow w = array[i];
+        for (VWindow w : array) {
             if (w.bringToFrontSequence != -1 || w.vaadinModality) {
-                w.bringToFront();
+                w.bringToFront(false);
                 w.bringToFrontSequence = -1;
             }
         }
@@ -568,9 +619,10 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
 
     private static void focusTopmostModalWindow() {
         VWindow topmost = getTopmostWindow();
-        if ((topmost != null) && (topmost.vaadinModality)) {
+        if (topmost != null && topmost.vaadinModality) {
             topmost.focus();
         }
+        fireOrderEvent();
     }
 
     @Override
@@ -697,15 +749,21 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         }
         super.hide();
 
-        int curIndex = windowOrder.indexOf(this);
+        int curIndex = getWindowOrder();
         // Remove window from windowOrder to avoid references being left
         // hanging.
         windowOrder.remove(curIndex);
         // Update the z-indices of any remaining windows
+        List<VWindow> update = new ArrayList<VWindow>(
+                windowOrder.size() - curIndex + 1);
+        update.add(this);
         while (curIndex < windowOrder.size()) {
-            windowOrder.get(curIndex).setWindowOrder(curIndex++);
+            VWindow window = windowOrder.get(curIndex);
+            window.setWindowOrder(curIndex++);
+            update.add(window);
         }
         focusTopmostModalWindow();
+        fireOrderEvent(update);
     }
 
     private void fixIE8FocusCaptureIssue() {
@@ -744,8 +802,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
     }
 
     private void showModalityCurtain() {
-        getModalityCurtain().getStyle()
-                .setZIndex(windowOrder.indexOf(this) + Z_INDEX);
+        getModalityCurtain().getStyle().setZIndex(getWindowOrder() + Z_INDEX);
 
         if (isShowing()) {
             getOverlayContainer().insertBefore(getModalityCurtain(),
@@ -1213,7 +1270,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         // Override PopupPanel which sets the width to the contents
         getElement().getStyle().setProperty("width", width);
         // Update v-has-width in case undefined window is resized
-        setStyleName("v-has-width", width != null && width.length() > 0);
+        setStyleName("v-has-width", width != null && !width.isEmpty());
     }
 
     @Override
@@ -1221,7 +1278,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         // Override PopupPanel which sets the height to the contents
         getElement().getStyle().setProperty("height", height);
         // Update v-has-height in case undefined window is resized
-        setStyleName("v-has-height", height != null && height.length() > 0);
+        setStyleName("v-has-height", height != null && !height.isEmpty());
     }
 
     private void onDragEvent(Event event) {
@@ -1518,9 +1575,22 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         return addHandler(handler, WindowMoveEvent.getType());
     }
 
+    /**
+     * Adds a Handler for window order change event.
+     *
+     * @since 7.7.12
+     *
+     * @return registration object to deregister the handler
+     */
+    public static HandlerRegistration addWindowOrderHandler(
+            WindowOrderHandler handler) {
+        return WINDOW_ORDER_HANDLER.addHandler(WindowOrderEvent.getType(),
+                handler);
+    }
+
     /**
      * Checks if a modal window is currently open.
-     * 
+     *
      * @return <code>true</code> if a modal window is open, <code>false</code>
      *         otherwise.
      */
@@ -1528,5 +1598,4 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner,
         return Document.get().getBody()
                 .hasClassName(MODAL_WINDOW_OPEN_CLASSNAME);
     }
-
 }
diff --git a/client/src/main/java/com/vaadin/client/ui/window/WindowOrderEvent.java b/client/src/main/java/com/vaadin/client/ui/window/WindowOrderEvent.java
new file mode 100644 (file)
index 0000000..016f609
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.client.ui.window;
+
+import java.util.ArrayList;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.client.ui.VWindow;
+
+/**
+ * Event for window order position updates.
+ *
+ * @since 7.7.12
+ *
+ * @author Vaadin Ltd
+ */
+public class WindowOrderEvent extends GwtEvent<WindowOrderHandler> {
+
+    private static final Type<WindowOrderHandler> TYPE = new Type<WindowOrderHandler>();
+
+    private final ArrayList<VWindow> windows;
+
+    /**
+     * Creates a new event with the given order.
+     *
+     * @param windows
+     *            The new order position for the VWindow
+     */
+    public WindowOrderEvent(ArrayList<VWindow> windows) {
+        this.windows = windows;
+    }
+
+    @Override
+    public Type<WindowOrderHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    /**
+     * Returns windows in order.
+     *
+     * @return windows in the specific order
+     */
+    public VWindow[] getWindows() {
+        return windows.toArray(new VWindow[windows.size()]);
+    }
+
+    @Override
+    protected void dispatch(WindowOrderHandler handler) {
+        handler.onWindowOrderChange(this);
+    }
+
+    /**
+     * Gets the type of the event.
+     *
+     * @return the type of the event
+     */
+    public static Type<WindowOrderHandler> getType() {
+        return TYPE;
+    }
+
+}
diff --git a/client/src/main/java/com/vaadin/client/ui/window/WindowOrderHandler.java b/client/src/main/java/com/vaadin/client/ui/window/WindowOrderHandler.java
new file mode 100644 (file)
index 0000000..cb87909
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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.client.ui.window;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link WindowOrderEvent}s.
+ *
+ * @since 7.7.12
+ *
+ * @author Vaadin Ltd
+ */
+public interface WindowOrderHandler extends EventHandler {
+
+    /**
+     * Called when the VWindow instances changed their order position.
+     *
+     * @param event
+     *            Contains windows whose position has changed
+     */
+    public void onWindowOrderChange(WindowOrderEvent event);
+}
\ No newline at end of file
index 01f27dc954a95bede44f0ab4e55110aba4bf7118..4aec13ad82a130c17214fea98339120dfb4ecf55 100644 (file)
@@ -71,4 +71,4 @@ public class ModalWindowFocus extends AbstractTestUI {
         return 17021;
     }
 
-}
\ No newline at end of file
+}
index e561d21c71ac299488d701fe318b18d170a22210..1c289c38100ae90b2ba6a491a656b74dc62ba3b6 100644 (file)
@@ -22,6 +22,7 @@ import static org.openqa.selenium.Keys.BACK_SPACE;
 import static org.openqa.selenium.Keys.TAB;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.interactions.Actions;
@@ -60,9 +61,14 @@ public class BackspaceKeyWithModalOpenedTest extends MultiBrowserTest {
     }
 
     /**
-     * Tests that backspace action in the bottom component is prevented
+     * Tests that backspace action in the bottom component is prevented.
+     *
+     * Ignored because the fix to #8855 stops the top and bottom components
+     * from functioning as focus traps. Meanwhile, navigation with Backspace
+     * is not anymore supported by reasonable browsers.
      */
     @Test
+    @Ignore
     public void testWithFocusOnBottom() throws Exception {
         TextFieldElement textField = getTextField();