From 61c8896a88bc09d749f070b8267c679de8b3c0e4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?P=C3=A9ter=20T=C3=B6r=C3=B6k?= <31210544+torok-peter@users.noreply.github.com> Date: Tue, 7 Nov 2017 11:39:11 +0200 Subject: [PATCH] Make focus circulate in modal dialog to improve accessibility (#10260) --- .../java/com/vaadin/client/ui/FocusUtil.java | 23 ++++++++ .../java/com/vaadin/client/ui/VWindow.java | 56 ++++++++++--------- .../window/BackspaceKeyWithModalOpened.java | 8 ++- .../components/window/ModalWindowFocus.java | 2 + .../BackspaceKeyWithModalOpenedTest.java | 8 ++- 5 files changed, 67 insertions(+), 30 deletions(-) diff --git a/client/src/main/java/com/vaadin/client/ui/FocusUtil.java b/client/src/main/java/com/vaadin/client/ui/FocusUtil.java index 49cd9284c1..0a510f2b65 100644 --- a/client/src/main/java/com/vaadin/client/ui/FocusUtil.java +++ b/client/src/main/java/com/vaadin/client/ui/FocusUtil.java @@ -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(); + } + } } diff --git a/client/src/main/java/com/vaadin/client/ui/VWindow.java b/client/src/main/java/com/vaadin/client/ui/VWindow.java index 8b907f9a4a..76def668f9 100644 --- a/client/src/main/java/com/vaadin/client/ui/VWindow.java +++ b/client/src/main/java/com/vaadin/client/ui/VWindow.java @@ -439,37 +439,39 @@ 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) { - NativeEvent nativeEvent = event.getNativeEvent(); - if (nativeEvent.getEventTarget().cast() == topTabStop - && nativeEvent.getKeyCode() == KeyCodes.KEY_TAB - && nativeEvent.getShiftKey()) { - nativeEvent.preventDefault(); - } - if (nativeEvent.getEventTarget().cast() == topTabStop - && nativeEvent.getKeyCode() == KeyCodes.KEY_BACKSPACE) { - nativeEvent.preventDefault(); - } + topEventBlocker = 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(this.getElement()); + } + if (nativeEvent.getEventTarget().cast() == topTabStop + && nativeEvent.getKeyCode() == KeyCodes.KEY_BACKSPACE) { + nativeEvent.preventDefault(); } }; - bottomEventBlocker = new NativePreviewHandler() { - @Override - public void onPreviewNativeEvent(NativePreviewEvent event) { - NativeEvent nativeEvent = event.getNativeEvent(); - if (nativeEvent.getEventTarget().cast() == bottomTabStop - && nativeEvent.getKeyCode() == KeyCodes.KEY_TAB - && !nativeEvent.getShiftKey()) { - nativeEvent.preventDefault(); - } - if (nativeEvent.getEventTarget().cast() == bottomTabStop - && nativeEvent.getKeyCode() == KeyCodes.KEY_BACKSPACE) { - nativeEvent.preventDefault(); - } + bottomEventBlocker = 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(this.getElement()); + } + if (nativeEvent.getEventTarget().cast() == bottomTabStop + && nativeEvent.getKeyCode() == KeyCodes.KEY_BACKSPACE) { + nativeEvent.preventDefault(); } }; } diff --git a/uitest/src/main/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpened.java b/uitest/src/main/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpened.java index f25e4e9349..c53a77b223 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpened.java +++ b/uitest/src/main/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpened.java @@ -15,11 +15,13 @@ */ package com.vaadin.tests.components.window; +import com.vaadin.annotations.Theme; +import com.vaadin.annotations.Widgetset; import com.vaadin.navigator.Navigator; import com.vaadin.navigator.View; import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; import com.vaadin.server.VaadinRequest; -import com.vaadin.tests.components.AbstractReindeerTestUI; +import com.vaadin.tests.components.AbstractTestUI; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.Label; @@ -28,7 +30,9 @@ import com.vaadin.ui.TextField; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Window; -public class BackspaceKeyWithModalOpened extends AbstractReindeerTestUI { +@Theme("valo") +@Widgetset("com.vaadin.DefaultWidgetSet") +public class BackspaceKeyWithModalOpened extends AbstractTestUI { private static final String DEFAULT_VIEW_ID = ""; private static final String SECOND_VIEW_ID = "second"; diff --git a/uitest/src/main/java/com/vaadin/tests/components/window/ModalWindowFocus.java b/uitest/src/main/java/com/vaadin/tests/components/window/ModalWindowFocus.java index 85c60d3738..65e4e26215 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/window/ModalWindowFocus.java +++ b/uitest/src/main/java/com/vaadin/tests/components/window/ModalWindowFocus.java @@ -15,6 +15,7 @@ */ package com.vaadin.tests.components.window; +import com.vaadin.annotations.Widgetset; import com.vaadin.server.VaadinRequest; import com.vaadin.tests.components.AbstractReindeerTestUI; import com.vaadin.ui.Button; @@ -22,6 +23,7 @@ import com.vaadin.ui.Button.ClickEvent; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Window; +@Widgetset("com.vaadin.DefaultWidgetSet") public class ModalWindowFocus extends AbstractReindeerTestUI { @Override diff --git a/uitest/src/test/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpenedTest.java b/uitest/src/test/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpenedTest.java index ce015bab9d..b9199e0aec 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpenedTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpenedTest.java @@ -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(); -- 2.39.5