Browse Source

Make focus circulate in modal dialog to improve accessibility (#10311)

* 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
tags/7.7.12
Adam Wagner 6 years ago
parent
commit
31de2dd8b3

+ 23
- 0
client/src/main/java/com/vaadin/client/ui/FocusUtil.java View 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();
}
}
}

+ 95
- 26
client/src/main/java/com/vaadin/client/ui/VWindow.java View 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);
}

}

+ 74
- 0
client/src/main/java/com/vaadin/client/ui/window/WindowOrderEvent.java View File

@@ -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;
}

}

+ 36
- 0
client/src/main/java/com/vaadin/client/ui/window/WindowOrderHandler.java View File

@@ -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);
}

+ 1
- 1
uitest/src/main/java/com/vaadin/tests/components/window/ModalWindowFocus.java View File

@@ -71,4 +71,4 @@ public class ModalWindowFocus extends AbstractTestUI {
return 17021;
}

}
}

+ 7
- 1
uitest/src/test/java/com/vaadin/tests/components/window/BackspaceKeyWithModalOpenedTest.java View 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();


Loading…
Cancel
Save