diff options
Diffstat (limited to 'client')
22 files changed, 959 insertions, 150 deletions
diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index 0d9c859ee8..cd1f8a206d 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -49,6 +49,7 @@ import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONNumber; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONString; import com.google.gwt.regexp.shared.MatchResult; @@ -90,6 +91,7 @@ import com.vaadin.client.ui.ui.UIConnector; import com.vaadin.client.ui.window.WindowConnector; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.JsonConstants; import com.vaadin.shared.Version; import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; import com.vaadin.shared.communication.MethodInvocation; @@ -136,10 +138,6 @@ public class ApplicationConnection { public static final String ERROR_CLASSNAME_EXT = "-error"; - public static final char VAR_BURST_SEPARATOR = '\u001d'; - - public static final char VAR_ESCAPE_CHARACTER = '\u001b'; - /** * A string that, if found in a non-JSON response to a UIDL request, will * cause the browser to refresh the page. If followed by a colon, optional @@ -270,8 +268,6 @@ public class ApplicationConnection { /** Event bus for communication events */ private EventBus eventBus = GWT.create(SimpleEventBus.class); - private int lastResponseId = -1; - /** * The communication handler methods are called at certain points during * communication with the server. This allows for making add-ons that keep @@ -490,6 +486,10 @@ public class ApplicationConnection { // initial UIDL provided in DOM, continue as if returned by request handleJSONText(jsonText, -1); + + // Tooltip can't be created earlier because the necessary fields are + // not setup to add it in the correct place in the DOM + getVTooltip().showAssistive(new TooltipInfo(" ")); } } @@ -671,7 +671,7 @@ public class ApplicationConnection { }-*/; protected void repaintAll() { - makeUidlRequest("", getRepaintAllParameters()); + makeUidlRequest(new JSONArray(), getRepaintAllParameters()); } /** @@ -702,20 +702,25 @@ public class ApplicationConnection { /** * Makes an UIDL request to the server. * - * @param requestData - * Data that is passed to the server. + * @param reqInvocations + * Data containing RPC invocations and all related information. * @param extraParams * Parameters that are added as GET parameters to the url. * Contains key=value pairs joined by & characters or is empty if * no parameters should be added. Should not start with any * special character. */ - protected void makeUidlRequest(final String requestData, + protected void makeUidlRequest(final JSONArray reqInvocations, final String extraParams) { startRequest(); - // Security: double cookie submission pattern - final String payload = getCsrfToken() + VAR_BURST_SEPARATOR - + requestData; + + JSONObject payload = new JSONObject(); + payload.put(ApplicationConstants.CSRF_TOKEN, new JSONString( + getCsrfToken())); + payload.put(ApplicationConstants.RPC_INVOCATIONS, reqInvocations); + payload.put(ApplicationConstants.SERVER_SYNC_ID, new JSONNumber( + lastSeenServerSyncId)); + VConsole.log("Making UIDL Request with params: " + payload); String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + ApplicationConstants.UIDL_PATH + '/'); @@ -739,7 +744,7 @@ public class ApplicationConnection { * @param payload * The contents of the request to send */ - protected void doUidlRequest(final String uri, final String payload) { + protected void doUidlRequest(final String uri, final JSONObject payload) { RequestCallback requestCallback = new RequestCallback() { @Override public void onError(Request request, Throwable exception) { @@ -902,14 +907,14 @@ public class ApplicationConnection { * @throws RequestException * if the request could not be sent */ - protected void doAjaxRequest(String uri, String payload, + protected void doAjaxRequest(String uri, JSONObject payload, RequestCallback requestCallback) throws RequestException { RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); // TODO enable timeout // rb.setTimeoutMillis(timeoutMillis); // TODO this should be configurable - rb.setHeader("Content-Type", "text/plain;charset=utf-8"); - rb.setRequestData(payload); + rb.setHeader("Content-Type", JsonConstants.JSON_CONTENT_TYPE); + rb.setRequestData(payload.toString()); rb.setCallback(requestCallback); final Request request = rb.send(); @@ -974,6 +979,29 @@ public class ApplicationConnection { */ private ValueMap serverTimingInfo; + /** + * Holds the last seen response id given by the server. + * <p> + * The server generates a strictly increasing id for each response to each + * request from the client. This ID is then replayed back to the server on + * each request. This helps the server in knowing in what state the client + * is, and compare it to its own state. In short, it helps with concurrent + * changes between the client and server. + * <p> + * Initial value, i.e. no responses received from the server, is + * {@link #UNDEFINED_SYNC_ID} ({@value #UNDEFINED_SYNC_ID}). This happens + * between the bootstrap HTML being loaded and the first UI being rendered; + */ + private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; + + /** + * The value of an undefined sync id. + * <p> + * This must be <code>-1</code>, because of the contract in + * {@link #getLastResponseId()} + */ + private static final int UNDEFINED_SYNC_ID = -1; + static final int MAX_CSS_WAITS = 100; protected void handleWhenCSSLoaded(final String jsonText, @@ -1254,7 +1282,13 @@ public class ApplicationConnection { * @return and id identifying the response */ public int getLastResponseId() { - return lastResponseId; + /* + * The discrepancy between field name and getter name is simply historic + * - API can't be changed, but the field was repurposed in a more + * general, yet compatible, use. "Response id" was deemed unsuitable a + * name, so it was called "server sync id" instead. + */ + return lastSeenServerSyncId; } protected void handleUIDLMessage(final Date start, final String jsonText, @@ -1281,6 +1315,17 @@ public class ApplicationConnection { VConsole.log("Handling message from server"); eventBus.fireEvent(new ResponseHandlingStartedEvent(this)); + if (json.containsKey(ApplicationConstants.SERVER_SYNC_ID)) { + int syncId = json.getInt(ApplicationConstants.SERVER_SYNC_ID); + + assert (lastSeenServerSyncId == UNDEFINED_SYNC_ID || syncId == lastSeenServerSyncId + 1) : "Newly retrieved server sync id was not exactly one larger than the previous one (new: " + + syncId + ", last seen: " + lastSeenServerSyncId + ")"; + + lastSeenServerSyncId = syncId; + } else { + VConsole.error("Server response didn't contain an id."); + } + // Handle redirect if (json.containsKey("redirect")) { String url = json.getValueMap("redirect").getString("url"); @@ -1289,8 +1334,6 @@ public class ApplicationConnection { return; } - lastResponseId++; - final MultiStepDuration handleUIDLDuration = new MultiStepDuration(); // Get security key @@ -2464,15 +2507,13 @@ public class ApplicationConnection { */ private void buildAndSendVariableBurst( LinkedHashMap<String, MethodInvocation> pendingInvocations) { - final StringBuffer req = new StringBuffer(); - while (!pendingInvocations.isEmpty()) { + JSONArray reqJson = new JSONArray(); + if (!pendingInvocations.isEmpty()) { if (ApplicationConfiguration.isDebugMode()) { Util.logVariableBurst(this, pendingInvocations.values()); } - JSONArray reqJson = new JSONArray(); - for (MethodInvocation invocation : pendingInvocations.values()) { JSONArray invocationJson = new JSONArray(); invocationJson.set(0, @@ -2511,9 +2552,6 @@ public class ApplicationConnection { reqJson.set(reqJson.size(), invocationJson); } - // escape burst separators (if any) - req.append(escapeBurstContents(reqJson.toString())); - pendingInvocations.clear(); // Keep tag string short lastInvocationTag = 0; @@ -2537,7 +2575,7 @@ public class ApplicationConnection { getConfiguration().setWidgetsetVersionSent(); } - makeUidlRequest(req.toString(), extraParams); + makeUidlRequest(reqJson, extraParams); } private boolean isJavascriptRpc(MethodInvocation invocation) { @@ -2781,35 +2819,6 @@ public class ApplicationConnection { } /** - * Encode burst separator characters in a String for transport over the - * network. This protects from separator injection attacks. - * - * @param value - * to encode - * @return encoded value - */ - protected String escapeBurstContents(String value) { - final StringBuilder result = new StringBuilder(); - for (int i = 0; i < value.length(); ++i) { - char character = value.charAt(i); - switch (character) { - case VAR_ESCAPE_CHARACTER: - // fall-through - escape character is duplicated - case VAR_BURST_SEPARATOR: - result.append(VAR_ESCAPE_CHARACTER); - // encode as letters for easier reading - result.append(((char) (character + 0x30))); - break; - default: - // the char is not a special one - add it to the result as is - result.append(character); - break; - } - } - return result.toString(); - } - - /** * Does absolutely nothing. Replaced by {@link LayoutManager}. * * @param container diff --git a/client/src/com/vaadin/client/LayoutManager.java b/client/src/com/vaadin/client/LayoutManager.java index 1ced003146..5d27527793 100644 --- a/client/src/com/vaadin/client/LayoutManager.java +++ b/client/src/com/vaadin/client/LayoutManager.java @@ -1449,10 +1449,15 @@ public class LayoutManager { /** * Informs this LayoutManager that the size of a component might have - * changed. If there is no upcoming layout phase, a new layout phase is - * scheduled. This method should be used whenever a size might have changed - * from outside of Vaadin's normal update phase, e.g. when an icon has been - * loaded or when the user resizes some part of the UI using the mouse. + * changed. This method should be used whenever the size of an individual + * component might have changed from outside of Vaadin's normal update + * phase, e.g. when an icon has been loaded or when the user resizes some + * part of the UI using the mouse. + * <p> + * To set an entire component hierarchy to be measured, use + * {@link #setNeedsMeasureRecursively(ComponentConnector)} instead. + * <p> + * If there is no upcoming layout phase, a new layout phase is scheduled. * * @param component * the component whose size might have changed. @@ -1466,6 +1471,33 @@ public class LayoutManager { } } + /** + * Informs this LayoutManager that some sizes in a component hierarchy might + * have changed. This method should be used whenever the size of any child + * component might have changed from outside of Vaadin's normal update + * phase, e.g. when a CSS class name related to sizing has been changed. + * <p> + * To set a single component to be measured, use + * {@link #setNeedsMeasure(ComponentConnector)} instead. + * <p> + * If there is no upcoming layout phase, a new layout phase is scheduled. + * + * @since 7.2 + * @param component + * the component at the root of the component hierarchy to + * measure + */ + public void setNeedsMeasureRecursively(ComponentConnector component) { + setNeedsMeasure(component); + + if (component instanceof HasComponentsConnector) { + HasComponentsConnector hasComponents = (HasComponentsConnector) component; + for (ComponentConnector child : hasComponents.getChildComponents()) { + setNeedsMeasureRecursively(child); + } + } + } + public void setEverythingNeedsMeasure() { everythingNeedsMeasure = true; } diff --git a/client/src/com/vaadin/client/VCaption.java b/client/src/com/vaadin/client/VCaption.java index d0338de4a1..d96e6ed653 100644 --- a/client/src/com/vaadin/client/VCaption.java +++ b/client/src/com/vaadin/client/VCaption.java @@ -42,6 +42,8 @@ public class VCaption extends HTML { private Icon icon; + private String iconAltText = ""; + private Element captionText; private final ApplicationConnection client; @@ -112,6 +114,8 @@ public class VCaption extends HTML { if (null != owner) { AriaHelper.bindCaption(owner.getWidget(), null); + AriaHelper.handleInputInvalid(owner.getWidget(), false); + AriaHelper.handleInputRequired(owner.getWidget(), false); } } @@ -300,6 +304,14 @@ public class VCaption extends HTML { @Deprecated public boolean updateCaptionWithoutOwner(String caption, boolean disabled, boolean hasDescription, boolean hasError, String iconURL) { + return updateCaptionWithoutOwner(caption, disabled, hasDescription, + hasError, iconURL, ""); + } + + @Deprecated + public boolean updateCaptionWithoutOwner(String caption, boolean disabled, + boolean hasDescription, boolean hasError, String iconURL, + String iconAltText) { boolean wasPlacedAfterComponent = placedAfterComponent; // Caption is placed after component unless there is some part which @@ -332,7 +344,7 @@ public class VCaption extends HTML { // Icon forces the caption to be above the component placedAfterComponent = false; - icon.setUri(iconURL); + icon.setUri(iconURL, iconAltText); } else if (icon != null) { // Remove existing diff --git a/client/src/com/vaadin/client/VErrorMessage.java b/client/src/com/vaadin/client/VErrorMessage.java index 2e42b98a05..a384b451dd 100644 --- a/client/src/com/vaadin/client/VErrorMessage.java +++ b/client/src/com/vaadin/client/VErrorMessage.java @@ -31,9 +31,6 @@ public class VErrorMessage extends FlowPanel { public VErrorMessage() { super(); setStyleName(CLASSNAME); - - // Needed for binding with WAI-ARIA attributes - getElement().setId(DOM.createUniqueId()); } /** diff --git a/client/src/com/vaadin/client/VTooltip.java b/client/src/com/vaadin/client/VTooltip.java index 6191821988..e687712b9c 100644 --- a/client/src/com/vaadin/client/VTooltip.java +++ b/client/src/com/vaadin/client/VTooltip.java @@ -15,7 +15,8 @@ */ package com.vaadin.client; -import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.LiveValue; +import com.google.gwt.aria.client.RelevantValue; import com.google.gwt.aria.client.Roles; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; @@ -36,12 +37,12 @@ import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; -import com.vaadin.client.ui.VOverlay; +import com.vaadin.client.ui.VWindowOverlay; /** * TODO open for extension */ -public class VTooltip extends VOverlay { +public class VTooltip extends VWindowOverlay { private static final String CLASSNAME = "v-tooltip"; private static final int MARGIN = 4; public static final int TOOLTIP_EVENTS = Event.ONKEYDOWN @@ -57,7 +58,6 @@ public class VTooltip extends VOverlay { private boolean justClosed = false; private String uniqueId = DOM.createUniqueId(); - private Element layoutElement; private int maxWidth; // Delays for the tooltip, configurable on the server side @@ -80,17 +80,28 @@ public class VTooltip extends VOverlay { setWidget(layout); layout.add(em); DOM.setElementProperty(description, "className", CLASSNAME + "-text"); - - layoutElement = layout.getElement(); - DOM.appendChild(layoutElement, description); + DOM.appendChild(layout.getElement(), description); setSinkShadowEvents(true); - // Used to bind the tooltip to the owner for assistive devices - layoutElement.setId(uniqueId); + // When a tooltip is shown, the content of the tooltip changes. With a + // tooltip being a live-area, this change is notified to a assistive + // device. + Roles.getTooltipRole().set(getElement()); + Roles.getTooltipRole().setAriaLiveProperty(getElement(), + LiveValue.ASSERTIVE); + Roles.getTooltipRole().setAriaRelevantProperty(getElement(), + RelevantValue.ADDITIONS); + } - description.setId(DOM.createUniqueId()); - Roles.getTooltipRole().set(layoutElement); - Roles.getTooltipRole().setAriaHiddenState(layoutElement, true); + /** + * Show the tooltip with the provided info for assistive devices. + * + * @param info + * with the content of the tooltip + */ + public void showAssistive(TooltipInfo info) { + updatePosition(null, true); + show(info); } /** @@ -229,10 +240,11 @@ public class VTooltip extends VOverlay { @Override public void hide() { - super.hide(); - Roles.getTooltipRole().setAriaHiddenState(layoutElement, true); - Roles.getTooltipRole().removeAriaDescribedbyProperty( - tooltipEventHandler.currentElement); + em.updateMessage(""); + description.setInnerHTML(""); + + updatePosition(null, true); + setPopupPosition(tooltipEventMouseX, tooltipEventMouseY); } private int tooltipEventMouseX; @@ -287,9 +299,9 @@ public class VTooltip extends VOverlay { private com.google.gwt.dom.client.Element currentElement = null; /** - * Current element focused + * Marker for handling of tooltip through focus */ - private boolean currentIsFocused; + private boolean handledByFocus; /** * Current tooltip active @@ -392,6 +404,7 @@ public class VTooltip extends VOverlay { */ @Override public void onBlur(BlurEvent be) { + handledByFocus = false; handleHideEvent(); } @@ -401,7 +414,7 @@ public class VTooltip extends VOverlay { .getEventTarget()); // We can ignore move event if it's handled by move or over already - if (currentElement == element && currentIsFocused == isFocused) { + if (currentElement == element && handledByFocus == true) { return; } @@ -409,8 +422,6 @@ public class VTooltip extends VOverlay { if (!connectorAndTooltipFound) { if (isShowing()) { handleHideEvent(); - Roles.getButtonRole() - .removeAriaDescribedbyProperty(element); } else { currentTooltipInfo = null; } @@ -419,17 +430,12 @@ public class VTooltip extends VOverlay { if (isShowing()) { replaceCurrentTooltip(); - Roles.getTooltipRole().removeAriaDescribedbyProperty( - currentElement); } else { showTooltip(); } - - Roles.getTooltipRole().setAriaDescribedbyProperty(element, - Id.of(uniqueId)); } - currentIsFocused = isFocused; + handledByFocus = isFocused; currentElement = element; } } @@ -464,9 +470,11 @@ public class VTooltip extends VOverlay { @Override public void setPopupPositionAndShow(PositionCallback callback) { - super.setPopupPositionAndShow(callback); - - Roles.getTooltipRole().setAriaHiddenState(layoutElement, false); + if (isAttached()) { + callback.setPosition(getOffsetWidth(), getOffsetHeight()); + } else { + super.setPopupPositionAndShow(callback); + } } /** diff --git a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java index 3cecb09dc1..ccb01c5a30 100644 --- a/client/src/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/client/src/com/vaadin/client/communication/AtmospherePushConnection.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.Scheduler; +import com.google.gwt.json.client.JSONObject; import com.google.gwt.user.client.Command; import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ApplicationConnection; @@ -110,7 +111,7 @@ public class AtmospherePushConnection implements PushConnection { private JavaScriptObject socket; - private ArrayList<String> messageQueue = new ArrayList<String>(); + private ArrayList<JSONObject> messageQueue = new ArrayList<JSONObject>(); private State state = State.CONNECT_PENDING; @@ -191,14 +192,8 @@ public class AtmospherePushConnection implements PushConnection { } } - /* - * (non-Javadoc) - * - * @see - * com.vaadin.client.communication.PushConenction#push(java.lang.String) - */ @Override - public void push(String message) { + public void push(JSONObject message) { switch (state) { case CONNECT_PENDING: assert isActive(); @@ -210,12 +205,13 @@ public class AtmospherePushConnection implements PushConnection { VConsole.log("Sending push message: " + message); if (transport.equals("websocket")) { - FragmentedMessage fragmented = new FragmentedMessage(message); + FragmentedMessage fragmented = new FragmentedMessage( + message.toString()); while (fragmented.hasNextFragment()) { doPush(socket, fragmented.getNextFragment()); } } else { - doPush(socket, message); + doPush(socket, message.toString()); } break; case DISCONNECT_PENDING: @@ -228,15 +224,31 @@ public class AtmospherePushConnection implements PushConnection { return config; } - protected void onOpen(AtmosphereResponse response) { - transport = response.getTransport(); + protected void onReopen(AtmosphereResponse response) { + VConsole.log("Push connection re-established using " + transport); + onConnect(response); + } + protected void onOpen(AtmosphereResponse response) { VConsole.log("Push connection established using " + transport); + onConnect(response); + } + + /** + * Called whenever a server push connection is established (or + * re-established). + * + * @param response + * + * @since 7.2 + */ + protected void onConnect(AtmosphereResponse response) { + transport = response.getTransport(); switch (state) { case CONNECT_PENDING: state = State.CONNECTED; - for (String message : messageQueue) { + for (JSONObject message : messageQueue) { push(message); } messageQueue.clear(); @@ -422,6 +434,7 @@ public class AtmospherePushConnection implements PushConnection { reconnectInterval: 5000, maxReconnectOnClose: 10000000, trackMessageLength: true, + enableProtocol: false, messageDelimiter: String.fromCharCode(@com.vaadin.shared.communication.PushConstants::MESSAGE_DELIMITER) }; }-*/; @@ -435,6 +448,9 @@ public class AtmospherePushConnection implements PushConnection { config.onOpen = $entry(function(response) { self.@com.vaadin.client.communication.AtmospherePushConnection::onOpen(*)(response); }); + config.onReopen = $entry(function(response) { + self.@com.vaadin.client.communication.AtmospherePushConnection::onReopen(*)(response); + }); config.onMessage = $entry(function(response) { self.@com.vaadin.client.communication.AtmospherePushConnection::onMessage(*)(response); }); diff --git a/client/src/com/vaadin/client/communication/PushConnection.java b/client/src/com/vaadin/client/communication/PushConnection.java index a7eba224be..ba79af9d2c 100644 --- a/client/src/com/vaadin/client/communication/PushConnection.java +++ b/client/src/com/vaadin/client/communication/PushConnection.java @@ -16,6 +16,7 @@ package com.vaadin.client.communication; +import com.google.gwt.json.client.JSONObject; import com.google.gwt.user.client.Command; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ApplicationConnection.CommunicationErrorHandler; @@ -53,14 +54,14 @@ public interface PushConnection { * replay those messages in the original order when the connection has been * established. * - * @param message - * the message to push + * @param payload + * the payload to push * @throws IllegalStateException * if this connection is not active * * @see #isActive() */ - public void push(String message); + public void push(JSONObject payload); /** * Checks whether this push connection is in a state where it can push diff --git a/client/src/com/vaadin/client/ui/Icon.java b/client/src/com/vaadin/client/ui/Icon.java index 9d5829c079..4a0e858798 100644 --- a/client/src/com/vaadin/client/ui/Icon.java +++ b/client/src/com/vaadin/client/ui/Icon.java @@ -34,11 +34,19 @@ public class Icon extends UIObject { } public Icon(ApplicationConnection client, String uidlUri) { + this(client, uidlUri, ""); + } + + public Icon(ApplicationConnection client, String uidlUri, String iconAltText) { this(client); - setUri(uidlUri); + setUri(uidlUri, iconAltText); } public void setUri(String uidlUri) { + setUri(uidlUri, ""); + } + + public void setUri(String uidlUri, String uidlAlt) { if (!uidlUri.equals(myUri)) { /* * Start sinking onload events, widgets responsibility to react. We @@ -51,6 +59,8 @@ public class Icon extends UIObject { DOM.setElementProperty(getElement(), "src", uri); myUri = uidlUri; } + + setAlternateText(uidlAlt); } /** @@ -63,5 +73,4 @@ public class Icon extends UIObject { getElement().setAttribute("alt", alternateText == null ? "" : alternateText); } - } diff --git a/client/src/com/vaadin/client/ui/VFilterSelect.java b/client/src/com/vaadin/client/ui/VFilterSelect.java index 9b14fbbb37..7efb5b8867 100644 --- a/client/src/com/vaadin/client/ui/VFilterSelect.java +++ b/client/src/com/vaadin/client/ui/VFilterSelect.java @@ -1786,6 +1786,11 @@ public class VFilterSelect extends Composite implements Field, KeyDownHandler, if (client.hasEventListeners(this, EventId.FOCUS)) { client.updateVariable(paintableId, EventId.FOCUS, "", true); } + + ComponentConnector connector = ConnectorMap.get(client).getConnector( + this); + client.getVTooltip().showAssistive( + connector.getTooltipInfo(getElement())); } /** diff --git a/client/src/com/vaadin/client/ui/VNotification.java b/client/src/com/vaadin/client/ui/VNotification.java index 7019394e3b..128de0a285 100644 --- a/client/src/com/vaadin/client/ui/VNotification.java +++ b/client/src/com/vaadin/client/ui/VNotification.java @@ -21,19 +21,25 @@ import java.util.Date; import java.util.EventObject; import java.util.Iterator; +import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; import com.vaadin.client.UIDL; import com.vaadin.client.Util; +import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.Position; +import com.vaadin.shared.ui.ui.NotificationConfigurationBean; +import com.vaadin.shared.ui.ui.NotificationConfigurationBean.Role; import com.vaadin.shared.ui.ui.UIConstants; public class VNotification extends VOverlay { @@ -46,6 +52,12 @@ public class VNotification extends VOverlay { public static final Position BOTTOM_LEFT = Position.BOTTOM_LEFT; public static final Position BOTTOM_RIGHT = Position.BOTTOM_RIGHT; + /** + * Position that is only accessible for assistive devices, invisible for + * visual users. + */ + public static final Position ASSISTIVE = Position.ASSISTIVE; + public static final int DELAY_FOREVER = -1; public static final int DELAY_NONE = 0; @@ -149,15 +161,67 @@ public class VNotification extends VOverlay { } public void show(Widget widget, Position position, String style) { - setWidget(widget); + NotificationConfigurationBean styleSetup = getUiState(style); + setWaiAriaRole(styleSetup); + + FlowPanel panel = new FlowPanel(); + if (styleSetup.hasAssistivePrefix()) { + panel.add(new Label(styleSetup.getAssistivePrefix())); + AriaHelper.setVisibleForAssistiveDevicesOnly(panel.getElement(), + true); + } + + panel.add(widget); + + if (styleSetup.hasAssistivePostfix()) { + panel.add(new Label(styleSetup.getAssistivePostfix())); + AriaHelper.setVisibleForAssistiveDevicesOnly(panel.getElement(), + true); + } + setWidget(panel); show(position, style); } public void show(String html, Position position, String style) { - setWidget(new HTML(html)); + NotificationConfigurationBean styleSetup = getUiState(style); + String assistiveDeviceOnlyStyle = AriaHelper.ASSISTIVE_DEVICE_ONLY_STYLE; + + setWaiAriaRole(styleSetup); + + String type = ""; + String usage = ""; + + if (styleSetup != null && styleSetup.hasAssistivePrefix()) { + type = "<span class='" + assistiveDeviceOnlyStyle + "'>" + + styleSetup.getAssistivePrefix() + "</span>"; + } + + if (styleSetup != null && styleSetup.hasAssistivePostfix()) { + usage = "<span class='" + assistiveDeviceOnlyStyle + "'>" + + styleSetup.getAssistivePostfix() + "</span>"; + } + + setWidget(new HTML(type + html + usage)); show(position, style); } + private NotificationConfigurationBean getUiState(String style) { + NotificationConfigurationBean styleSetup = getApplicationConnection() + .getUIConnector().getState().notificationConfiguration.setup + .get(style); + return styleSetup; + } + + private void setWaiAriaRole(NotificationConfigurationBean styleSetup) { + Roles.getAlertRole().set(getElement()); + + if (styleSetup != null && styleSetup.getAssistiveRole() != null) { + if (Role.STATUS == styleSetup.getAssistiveRole()) { + Roles.getStatusRole().set(getElement()); + } + } + } + public void show(Position position, String style) { setOpacity(getElement(), startOpacity); if (style != null) { @@ -257,6 +321,10 @@ public class VNotification extends VOverlay { DOM.setStyleAttribute(el, "top", ""); DOM.setStyleAttribute(el, "bottom", "0px"); break; + case ASSISTIVE: + DOM.setStyleAttribute(el, "top", "-2000px"); + DOM.setStyleAttribute(el, "left", "-2000px"); + break; default: case MIDDLE_CENTER: center(); @@ -377,7 +445,8 @@ public class VNotification extends VOverlay { final String parsedUri = client .translateVaadinUri(notification .getStringAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_ICON)); - html += "<img src=\"" + Util.escapeAttribute(parsedUri) + "\" />"; + html += "<img src=\"" + Util.escapeAttribute(parsedUri) + + "\" alt=\"\" />"; } if (notification .hasAttribute(UIConstants.ATTRIBUTE_NOTIFICATION_CAPTION)) { @@ -417,6 +486,8 @@ public class VNotification extends VOverlay { public static VNotification createNotification(int delayMsec, Widget owner) { final VNotification notification = GWT.create(VNotification.class); + notification.setWaiAriaRole(null); + notification.delayMsec = delayMsec; if (BrowserInfo.get().isTouchDevice()) { new Timer() { diff --git a/client/src/com/vaadin/client/ui/VOverlay.java b/client/src/com/vaadin/client/ui/VOverlay.java index ced476f9dd..206e26b960 100644 --- a/client/src/com/vaadin/client/ui/VOverlay.java +++ b/client/src/com/vaadin/client/ui/VOverlay.java @@ -172,7 +172,7 @@ public class VOverlay extends PopupPanel implements CloseHandler<PopupPanel> { * * See default theme 'shadow.css' for implementation example. */ - private static final String SHADOW_HTML = "<div class=\"top-left\"></div><div class=\"top\"></div><div class=\"top-right\"></div><div class=\"left\"></div><div class=\"center\"></div><div class=\"right\"></div><div class=\"bottom-left\"></div><div class=\"bottom\"></div><div class=\"bottom-right\"></div>"; + private static final String SHADOW_HTML = "<div aria-hidden=\"true\" class=\"top-left\"></div><div class=\"top\"></div><div class=\"top-right\"></div><div class=\"left\"></div><div class=\"center\"></div><div class=\"right\"></div><div class=\"bottom-left\"></div><div class=\"bottom\"></div><div class=\"bottom-right\"></div>"; /** * Matches {@link PopupPanel}.ANIMATION_DURATION diff --git a/client/src/com/vaadin/client/ui/VTabsheet.java b/client/src/com/vaadin/client/ui/VTabsheet.java index fe29e2ebc0..2a949c31af 100644 --- a/client/src/com/vaadin/client/ui/VTabsheet.java +++ b/client/src/com/vaadin/client/ui/VTabsheet.java @@ -19,6 +19,10 @@ package com.vaadin.client.ui; import java.util.Iterator; import java.util.List; +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.LiveValue; +import com.google.gwt.aria.client.Roles; +import com.google.gwt.aria.client.SelectedValue; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Style; @@ -55,6 +59,7 @@ import com.vaadin.client.TooltipInfo; import com.vaadin.client.UIDL; import com.vaadin.client.Util; import com.vaadin.client.VCaption; +import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.EventId; import com.vaadin.shared.ui.ComponentStateUtil; @@ -94,12 +99,18 @@ public class VTabsheet extends VTabsheetBase implements Focusable, + "-selected"; private static final String TD_SELECTED_FIRST_CLASSNAME = TD_SELECTED_CLASSNAME + "-first"; + private static final String TD_FOCUS_CLASSNAME = TD_CLASSNAME + + "-focus"; + private static final String TD_FOCUS_FIRST_CLASSNAME = TD_FOCUS_CLASSNAME + + "-first"; private static final String TD_DISABLED_CLASSNAME = TD_CLASSNAME + "-disabled"; private static final String DIV_CLASSNAME = CLASSNAME + "-tabitem"; private static final String DIV_SELECTED_CLASSNAME = DIV_CLASSNAME + "-selected"; + private static final String DIV_FOCUS_CLASSNAME = DIV_CLASSNAME + + "-focus"; private TabCaption tabCaption; Element td = getElement(); @@ -112,11 +123,17 @@ public class VTabsheet extends VTabsheetBase implements Focusable, private String styleName; + private String id; + private Tab(TabBar tabBar) { super(DOM.createTD()); this.tabBar = tabBar; setStyleName(td, TD_CLASSNAME); + Roles.getTabRole().set(getElement()); + Roles.getTabRole().setAriaSelectedState(getElement(), + SelectedValue.FALSE); + div = DOM.createDiv(); focusImpl.setTabIndex(td, -1); setStyleName(div, DIV_CLASSNAME); @@ -127,6 +144,9 @@ public class VTabsheet extends VTabsheetBase implements Focusable, .getApplicationConnection()); add(tabCaption); + Roles.getTabRole().setAriaLabelledbyProperty(getElement(), + Id.of(tabCaption.getElement())); + addFocusHandler(getTabsheet()); addBlurHandler(getTabsheet()); addKeyDownHandler(getTabsheet()); @@ -138,6 +158,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, public void setHiddenOnServer(boolean hiddenOnServer) { this.hiddenOnServer = hiddenOnServer; + Roles.getTabRole().setAriaHiddenState(getElement(), hiddenOnServer); } @Override @@ -152,6 +173,8 @@ public class VTabsheet extends VTabsheetBase implements Focusable, public void setEnabledOnServer(boolean enabled) { enabledOnServer = enabled; + Roles.getTabRole().setAriaDisabledState(getElement(), !enabled); + setStyleName(td, TD_DISABLED_CLASSNAME, !enabled); if (!enabled) { focusImpl.setTabIndex(td, -1); @@ -175,10 +198,18 @@ public class VTabsheet extends VTabsheetBase implements Focusable, * true if the Tab is the first visible Tab */ public void setStyleNames(boolean selected, boolean first) { + setStyleNames(selected, first, false); + } + + public void setStyleNames(boolean selected, boolean first, + boolean keyboardFocus) { setStyleName(td, TD_FIRST_CLASSNAME, first); setStyleName(td, TD_SELECTED_CLASSNAME, selected); setStyleName(td, TD_SELECTED_FIRST_CLASSNAME, selected && first); setStyleName(div, DIV_SELECTED_CLASSNAME, selected); + setStyleName(td, TD_FOCUS_CLASSNAME, keyboardFocus); + setStyleName(td, TD_FOCUS_FIRST_CLASSNAME, keyboardFocus && first); + setStyleName(div, DIV_FOCUS_CLASSNAME, keyboardFocus); } public void setTabulatorIndex(int tabIndex) { @@ -204,10 +235,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable, String newStyleName = tabUidl .getStringAttribute(TabsheetConstants.TAB_STYLE_NAME); // Find the nth td element - if (newStyleName != null && newStyleName.length() != 0) { + if (newStyleName != null && !newStyleName.isEmpty()) { if (!newStyleName.equals(styleName)) { // If we have a new style name - if (styleName != null && styleName.length() != 0) { + if (styleName != null && !styleName.isEmpty()) { // Remove old style name if present td.removeClassName(TD_CLASSNAME + "-" + styleName); } @@ -221,6 +252,15 @@ public class VTabsheet extends VTabsheetBase implements Focusable, td.removeClassName(TD_CLASSNAME + "-" + styleName); styleName = null; } + + String newId = tabUidl.getStringAttribute("id"); + if (newId != null && !newId.isEmpty()) { + td.setId(newId); + id = newId; + } else if (id != null) { + td.removeAttribute("id"); + id = null; + } } public void recalculateCaptionWidth() { @@ -249,6 +289,23 @@ public class VTabsheet extends VTabsheetBase implements Focusable, public void blur() { focusImpl.blur(td); } + + public boolean hasTooltip() { + return tabCaption.getTooltipInfo() != null; + } + + public TooltipInfo getTooltipInfo() { + return tabCaption.getTooltipInfo(); + } + + public void setAssistiveDescription(String descriptionId) { + Roles.getTablistRole().setAriaDescribedbyProperty(getElement(), + Id.of(descriptionId)); + } + + public void removeAssistiveDescription() { + Roles.getTablistRole().removeAriaDescribedbyProperty(getElement()); + } } public static class TabCaption extends VCaption { @@ -262,6 +319,8 @@ public class VTabsheet extends VTabsheetBase implements Focusable, super(client); this.client = client; this.tab = tab; + + AriaHelper.ensureHasId(getElement()); } public boolean updateCaption(UIDL uidl) { @@ -280,7 +339,8 @@ public class VTabsheet extends VTabsheetBase implements Focusable, uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DISABLED), uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_DESCRIPTION), uidl.hasAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ERROR_MESSAGE), - uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON)); + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON), + uidl.getStringAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON_ALT)); setClosable(uidl.hasAttribute("closable")); @@ -318,6 +378,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable, closeButton.setInnerHTML("×"); closeButton .setClassName(VTabsheet.CLASSNAME + "-caption-close"); + + Roles.getTabRole().setAriaHiddenState(closeButton, true); + Roles.getTabRole().setAriaDisabledState(closeButton, true); + getElement().appendChild(closeButton); } else if (!closable && closeButton != null) { getElement().removeChild(closeButton); @@ -364,6 +428,8 @@ public class VTabsheet extends VTabsheetBase implements Focusable, this.tabsheet = tabsheet; Element el = DOM.createTable(); + Roles.getPresentationRole().set(el); + Element tbody = DOM.createTBody(); DOM.appendChild(el, tbody); DOM.appendChild(tbody, tr); @@ -420,11 +486,9 @@ public class VTabsheet extends VTabsheetBase implements Focusable, } int index = getWidgetIndex(caption.getParent()); - // IE needs explicit focus() - if (BrowserInfo.get().isIE()) { - getTabsheet().focus(); - } - getTabsheet().onTabSelected(index); + + getTabsheet().focus(); + getTabsheet().loadTabSheet(index); } public VTabsheet getTabsheet() { @@ -442,13 +506,18 @@ public class VTabsheet extends VTabsheetBase implements Focusable, final Tab newSelected = getTab(index); final Tab oldSelected = selected; - newSelected.setStyleNames(true, isFirstVisibleTab(index)); + newSelected.setStyleNames(true, isFirstVisibleTab(index), true); newSelected.setTabulatorIndex(getTabsheet().tabulatorIndex); + Roles.getTabRole().setAriaSelectedState(newSelected.getElement(), + SelectedValue.TRUE); if (oldSelected != null && oldSelected != newSelected) { oldSelected.setStyleNames(false, isFirstVisibleTab(getWidgetIndex(oldSelected))); oldSelected.setTabulatorIndex(-1); + + Roles.getTabRole().setAriaSelectedState( + oldSelected.getElement(), SelectedValue.FALSE); } // Update the field holding the currently selected tab @@ -459,6 +528,23 @@ public class VTabsheet extends VTabsheetBase implements Focusable, getTab(tabsheet.activeTabIndex).recalculateCaptionWidth(); } + public void navigateTab(int fromIndex, int toIndex) { + Tab newNavigated = getTab(toIndex); + if (newNavigated == null) { + throw new IllegalArgumentException( + "Tab at provided index toIndex was not found"); + } + + Tab oldNavigated = getTab(fromIndex); + newNavigated.setStyleNames(newNavigated.equals(selected), + isFirstVisibleTab(toIndex), true); + + if (oldNavigated != null && fromIndex != toIndex) { + oldNavigated.setStyleNames(oldNavigated.equals(selected), + isFirstVisibleTab(fromIndex), false); + } + } + public void removeTab(int i) { Tab tab = getTab(i); if (tab == null) { @@ -591,7 +677,7 @@ public class VTabsheet extends VTabsheetBase implements Focusable, /** * @return Whether the tab could be selected or not. */ - private boolean onTabSelected(final int tabIndex) { + private boolean canSelectTab(final int tabIndex) { Tab tab = tb.getTab(tabIndex); if (client == null || disabled || waitingForResponse) { return false; @@ -599,15 +685,32 @@ public class VTabsheet extends VTabsheetBase implements Focusable, if (!tab.isEnabledOnServer() || tab.isHiddenOnServer()) { return false; } - if (activeTabIndex != tabIndex) { + + // Note that we return true when tabIndex == activeTabIndex; the active + // tab could be selected, it's just a no-op. + return true; + } + + /** + * Load the content of a tab of the provided index. + * + * @param index + * of the tab to load + */ + public void loadTabSheet(int tabIndex) { + if (activeTabIndex != tabIndex && canSelectTab(tabIndex)) { tb.selectTab(tabIndex); // If this TabSheet already has focus, set the new selected tab // as focused. if (focusedTab != null) { - focusedTab = tab; + focusedTab = tb.getTab(tabIndex); + focusedTab.focus(); } + activeTabIndex = tabIndex; + focusedTabIndex = tabIndex; + addStyleDependentName("loading"); // Hide the current contents so a loading indicator can be shown // instead @@ -619,9 +722,6 @@ public class VTabsheet extends VTabsheetBase implements Focusable, .toString(), true); waitingForResponse = true; } - // Note that we return true when tabIndex == activeTabIndex; the active - // tab could be selected, it's just a no-op. - return true; } public ApplicationConnection getApplicationConnection() { @@ -663,22 +763,30 @@ public class VTabsheet extends VTabsheetBase implements Focusable, DOM.setStyleAttribute(getElement(), "overflow", "hidden"); tabs = DOM.createDiv(); DOM.setElementProperty(tabs, "className", TABS_CLASSNAME); + Roles.getTablistRole().set(tabs); + Roles.getTablistRole().setAriaLiveProperty(tabs, LiveValue.OFF); scroller = DOM.createDiv(); + 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); scrollerNext = DOM.createButton(); + scrollerNext.setTabIndex(-1); DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME + "Next"); + Roles.getTablistRole().setAriaHiddenState(scrollerNext, true); DOM.sinkEvents(scrollerNext, Event.ONCLICK); DOM.appendChild(getElement(), tabs); // Tabs tp.setStyleName(CLASSNAME + "-tabsheetpanel"); contentNode = DOM.createDiv(); + Roles.getTabpanelRole().set(contentNode); deco = DOM.createDiv(); @@ -1083,7 +1191,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable, @Override public void onBlur(BlurEvent event) { + getApplicationConnection().getVTooltip().hideTooltip(); + if (focusedTab != null && event.getSource() instanceof Tab) { + focusedTab.removeAssistiveDescription(); focusedTab = null; if (client.hasEventListeners(this, EventId.BLUR)) { client.updateVariable(id, EventId.BLUR, "", true); @@ -1098,6 +1209,13 @@ public class VTabsheet extends VTabsheetBase implements Focusable, if (client.hasEventListeners(this, EventId.FOCUS)) { client.updateVariable(id, EventId.FOCUS, "", true); } + + if (focusedTab.hasTooltip()) { + focusedTab.setAssistiveDescription(getApplicationConnection() + .getVTooltip().getUniqueId()); + getApplicationConnection().getVTooltip().showAssistive( + focusedTab.getTooltipInfo()); + } } } @@ -1117,13 +1235,17 @@ public class VTabsheet extends VTabsheetBase implements Focusable, 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); } } } @@ -1136,6 +1258,10 @@ public class VTabsheet extends VTabsheetBase implements Focusable, return KeyCodes.KEY_LEFT; } + protected int getSelectTabKey() { + return 32; // Space key + } + /** * @return The key code of the keyboard shortcut that selects the next tab * in a focused tabsheet. @@ -1153,18 +1279,27 @@ public class VTabsheet extends VTabsheetBase implements Focusable, } private void selectPreviousTab() { - int newTabIndex = activeTabIndex; + int newTabIndex = focusedTabIndex; // Find the previous visible and enabled tab if any. do { newTabIndex--; - } while (newTabIndex >= 0 && !onTabSelected(newTabIndex)); + } while (newTabIndex >= 0 && !canSelectTab(newTabIndex)); if (newTabIndex >= 0) { - activeTabIndex = newTabIndex; + 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(); + } + if (isScrolledTabs()) { // Scroll until the new active tab is visible int newScrollerIndex = scrollerIndex; - while (tb.getTab(activeTabIndex).getAbsoluteLeft() < getAbsoluteLeft() + while (tb.getTab(focusedTabIndex).getAbsoluteLeft() < getAbsoluteLeft() && newScrollerIndex != -1) { newScrollerIndex = tb.scrollLeft(newScrollerIndex); } @@ -1175,18 +1310,28 @@ public class VTabsheet extends VTabsheetBase implements Focusable, } private void selectNextTab() { - int newTabIndex = activeTabIndex; + int newTabIndex = focusedTabIndex; // Find the next visible and enabled tab if any. do { newTabIndex++; - } while (newTabIndex < getTabCount() && !onTabSelected(newTabIndex)); + } while (newTabIndex < getTabCount() && !canSelectTab(newTabIndex)); if (newTabIndex < getTabCount()) { - activeTabIndex = newTabIndex; + + 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(); + } + if (isClippedTabs()) { // Scroll until the new active tab is completely visible int newScrollerIndex = scrollerIndex; - while (isClipped(tb.getTab(activeTabIndex)) + while (isClipped(tb.getTab(focusedTabIndex)) && newScrollerIndex != -1) { newScrollerIndex = tb.scrollRight(newScrollerIndex); } diff --git a/client/src/com/vaadin/client/ui/VTabsheetBase.java b/client/src/com/vaadin/client/ui/VTabsheetBase.java index 0923248115..bcd8811c7d 100644 --- a/client/src/com/vaadin/client/ui/VTabsheetBase.java +++ b/client/src/com/vaadin/client/ui/VTabsheetBase.java @@ -42,6 +42,8 @@ public abstract class VTabsheetBase extends ComplexPanel { /** For internal use only. May be removed or replaced in the future. */ public int activeTabIndex = 0; /** For internal use only. May be removed or replaced in the future. */ + public int focusedTabIndex = 0; + /** For internal use only. May be removed or replaced in the future. */ public boolean disabled; /** For internal use only. May be removed or replaced in the future. */ public boolean readonly; diff --git a/client/src/com/vaadin/client/ui/VTree.java b/client/src/com/vaadin/client/ui/VTree.java index 51c00ca310..1acd4bd05f 100644 --- a/client/src/com/vaadin/client/ui/VTree.java +++ b/client/src/com/vaadin/client/ui/VTree.java @@ -70,6 +70,7 @@ import com.vaadin.client.ui.dd.VDragEvent; import com.vaadin.client.ui.dd.VDropHandler; import com.vaadin.client.ui.dd.VHasDropHandler; import com.vaadin.client.ui.dd.VTransferable; +import com.vaadin.client.ui.tree.TreeConnector; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.MouseEventDetails.MouseButton; import com.vaadin.shared.ui.MultiSelectMode; @@ -162,6 +163,9 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, /** For internal use only. May be removed or replaced in the future. */ public String[] bodyActionKeys; + /** For internal use only. May be removed or replaced in the future. */ + public TreeConnector connector; + public VLazyExecutor iconLoaded = new VLazyExecutor(50, new ScheduledCommand() { @@ -1729,6 +1733,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, } } } + showTooltipForKeyboardNavigation(node); return true; } @@ -1754,6 +1759,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, } } } + showTooltipForKeyboardNavigation(node); return true; } @@ -1774,6 +1780,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, focusAndSelectNode(focusedNode.getParentNode()); } } + showTooltipForKeyboardNavigation(focusedNode); return true; } @@ -1792,6 +1799,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, focusAndSelectNode(focusedNode.getChildren().get(0)); } } + showTooltipForKeyboardNavigation(focusedNode); return true; } @@ -1820,6 +1828,7 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, selectNode(node, true); } sendSelectionToServer(); + showTooltipForKeyboardNavigation(node); return true; } @@ -1836,12 +1845,20 @@ public class VTree extends FocusElementPanel implements VHasDropHandler, selectNode(node, true); } sendSelectionToServer(); + showTooltipForKeyboardNavigation(node); return true; } return false; } + private void showTooltipForKeyboardNavigation(TreeNode node) { + if (connector != null) { + getClient().getVTooltip().showAssistive( + connector.getTooltipInfo(node.nodeCaptionSpan)); + } + } + private void focusAndSelectNode(TreeNode node) { /* * Keyboard navigation doesn't work reliably if the tree is in diff --git a/client/src/com/vaadin/client/ui/VUI.java b/client/src/com/vaadin/client/ui/VUI.java index 4817bf9304..1a84613a5d 100644 --- a/client/src/com/vaadin/client/ui/VUI.java +++ b/client/src/com/vaadin/client/ui/VUI.java @@ -18,6 +18,7 @@ package com.vaadin.client.ui; import java.util.ArrayList; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.HasScrollHandlers; @@ -43,6 +44,7 @@ import com.vaadin.client.ConnectorMap; import com.vaadin.client.Focusable; import com.vaadin.client.LayoutManager; import com.vaadin.client.Profiler; +import com.vaadin.client.Util; import com.vaadin.client.VConsole; import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler; @@ -165,6 +167,8 @@ public class VUI extends SimplePanel implements ResizeHandler, }); + private Element storedFocus; + public VUI() { super(); // Allow focusing the view by using the focus() method, the view @@ -497,4 +501,36 @@ public class VUI extends SimplePanel implements ResizeHandler, FocusUtil.setTabIndex(this, index); } + /** + * Allows to store the currently focused Element. + * + * Current use case is to store the focus when a Window is opened. Does + * currently handle only a single value. Needs to be extended for #12158 + * + * @param focusedElement + */ + public void storeFocus() { + storedFocus = Util.getFocusedElement(); + } + + /** + * Restores the previously stored focus Element. + * + * Current use case is to restore the focus when a Window is closed. Does + * currently handle only a single value. Needs to be extended for #12158 + * + * @return the lastFocusElementBeforeDialogOpened + */ + public void focusStoredElement() { + if (storedFocus != null) { + storedFocus.focus(); + + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + storedFocus.focus(); + } + }); + } + } } diff --git a/client/src/com/vaadin/client/ui/VWindow.java b/client/src/com/vaadin/client/ui/VWindow.java index ff6a15e597..03ad8d03c8 100644 --- a/client/src/com/vaadin/client/ui/VWindow.java +++ b/client/src/com/vaadin/client/ui/VWindow.java @@ -18,10 +18,16 @@ package com.vaadin.client.ui; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; +import java.util.List; +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.RelevantValue; +import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; @@ -29,34 +35,45 @@ import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; +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.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorMap; import com.vaadin.client.Focusable; import com.vaadin.client.LayoutManager; import com.vaadin.client.Util; import com.vaadin.client.debug.internal.VDebugWindow; import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.shared.Connector; import com.vaadin.shared.EventId; import com.vaadin.shared.ui.window.WindowMode; +import com.vaadin.shared.ui.window.WindowState.WindowRole; /** * "Sub window" component. * * @author Vaadin Ltd */ -public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, - ScrollHandler, KeyDownHandler, FocusHandler, BlurHandler, Focusable { +public class VWindow extends VWindowOverlay implements + ShortcutActionHandlerOwner, ScrollHandler, KeyDownHandler, + KeyUpHandler, FocusHandler, BlurHandler, Focusable { private static ArrayList<VWindow> windowOrder = new ArrayList<VWindow>(); @@ -138,6 +155,22 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, private boolean closable = true; + private Connector[] assistiveConnectors = new Connector[0]; + private String assistivePrefix; + private String assistivePostfix; + + private Element topTabStop; + private Element bottomTabStop; + + private NativePreviewHandler topEventBlocker; + private NativePreviewHandler bottomEventBlocker; + + private HandlerRegistration topBlockerRegistration; + private HandlerRegistration bottomBlockerRegistration; + + // Prevents leaving the window with the Tab key when true + private boolean doTabStop; + /** * If centered (via UIDL), the window should stay in the centered -mode * until a position is received from the server, or the user moves or @@ -172,13 +205,74 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, // Different style of shadow for windows setShadowStyle("window"); + Roles.getDialogRole().set(getElement()); + Roles.getDialogRole().setAriaRelevantProperty(getElement(), + RelevantValue.ADDITIONS); + constructDOM(); contentPanel.addScrollHandler(this); contentPanel.addKeyDownHandler(this); + contentPanel.addKeyUpHandler(this); contentPanel.addFocusHandler(this); contentPanel.addBlurHandler(this); } + @Override + protected void onAttach() { + super.onAttach(); + + /* + * Stores the element that has focus in the application UI when the + * window is opened, so it can be restored when the window closes. + * + * This is currently implemented for the case when one non-modal window + * can be open at the same time, and the focus is not changed while the + * window is open. + */ + getApplicationConnection().getUIConnector().getWidget().storeFocus(); + + /* + * When this window gets reattached, set the tabstop to the previous + * state. + */ + setTabStopEnabled(doTabStop); + } + + @Override + protected void onDetach() { + super.onDetach(); + + /* + * Restores the previously stored focused element. + * + * When the focus was changed outside the window while the window was + * open, the originally stored element is restored. + */ + getApplicationConnection().getUIConnector().getWidget() + .focusStoredElement(); + + removeTabBlockHandlers(); + } + + private void addTabBlockHandlers() { + if (topBlockerRegistration == null) { + topBlockerRegistration = Event + .addNativePreviewHandler(topEventBlocker); + bottomBlockerRegistration = Event + .addNativePreviewHandler(bottomEventBlocker); + } + } + + private void removeTabBlockHandlers() { + if (topBlockerRegistration != null) { + topBlockerRegistration.removeHandler(); + topBlockerRegistration = null; + + bottomBlockerRegistration.removeHandler(); + bottomBlockerRegistration = null; + } + } + public void bringToFront() { int curIndex = windowOrder.indexOf(this); if (curIndex + 1 < windowOrder.size()) { @@ -242,6 +336,9 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, protected void constructDOM() { setStyleName(CLASSNAME); + topTabStop = DOM.createDiv(); + DOM.setElementAttribute(topTabStop, "tabindex", "0"); + header = DOM.createDiv(); DOM.setElementProperty(header, "className", CLASSNAME + "-outerheader"); headerText = DOM.createDiv(); @@ -256,18 +353,25 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, maximizeRestoreBox = DOM.createDiv(); DOM.setElementProperty(maximizeRestoreBox, "className", CLASSNAME + "-maximizebox"); + DOM.setElementAttribute(maximizeRestoreBox, "tabindex", "0"); DOM.setElementProperty(closeBox, "className", CLASSNAME + "-closebox"); + DOM.setElementAttribute(closeBox, "tabindex", "0"); DOM.appendChild(footer, resizeBox); + bottomTabStop = DOM.createDiv(); + DOM.setElementAttribute(bottomTabStop, "tabindex", "0"); + wrapper = DOM.createDiv(); DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap"); + DOM.appendChild(wrapper, topTabStop); DOM.appendChild(wrapper, header); DOM.appendChild(wrapper, maximizeRestoreBox); DOM.appendChild(wrapper, closeBox); DOM.appendChild(header, headerText); DOM.appendChild(wrapper, contents); DOM.appendChild(wrapper, footer); + DOM.appendChild(wrapper, bottomTabStop); DOM.appendChild(super.getContainerElement(), wrapper); sinkEvents(Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.TOUCHEVENTS @@ -275,6 +379,96 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, setWidget(contentPanel); + // Make the closebox accessible for assistive devices + Roles.getButtonRole().set(closeBox); + Roles.getButtonRole().setAriaLabelProperty(closeBox, "close button"); + + // Make the maximizebox accessible for assistive devices + Roles.getButtonRole().set(maximizeRestoreBox); + Roles.getButtonRole().setAriaLabelProperty(maximizeRestoreBox, + "maximize button"); + + // Provide the title to assistive devices + AriaHelper.ensureHasId(headerText); + Roles.getDialogRole().setAriaLabelledbyProperty(getElement(), + Id.of(headerText)); + + // Handlers to Prevent tab to leave the window + 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(); + } + } + }; + + 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(); + } + } + }; + } + + /** + * Sets the message that is provided to users of assistive devices when the + * user reaches the top of the window when leaving a window with the tab key + * is prevented. + * <p> + * This message is not visible on the screen. + * + * @param topMessage + * String provided when the user navigates with Shift-Tab keys to + * the top of the window + */ + public void setTabStopTopAssistiveText(String topMessage) { + Roles.getNoteRole().setAriaLabelProperty(topTabStop, topMessage); + } + + /** + * Sets the message that is provided to users of assistive devices when the + * user reaches the bottom of the window when leaving a window with the tab + * key is prevented. + * <p> + * This message is not visible on the screen. + * + * @param bottomMessage + * String provided when the user navigates with the Tab key to + * the bottom of the window + */ + public void setTabStopBottomAssistiveText(String bottomMessage) { + Roles.getNoteRole().setAriaLabelProperty(bottomTabStop, bottomMessage); + } + + /** + * Gets the message that is provided to users of assistive devices when the + * user reaches the top of the window when leaving a window with the tab key + * is prevented. + * + * @return the top message + */ + public String getTabStopTopAssistiveText() { + return Roles.getNoteRole().getAriaLabelProperty(topTabStop); + } + + /** + * Gets the message that is provided to users of assistive devices when the + * user reaches the bottom of the window when leaving a window with the tab + * key is prevented. + * + * @return the bottom message + */ + public String getTabStopBottomAssistiveText() { + return Roles.getNoteRole().getAriaLabelProperty(bottomTabStop); } /** @@ -492,6 +686,7 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, if (isAttached()) { showModalityCurtain(); } + addTabBlockHandlers(); deferOrdering(); } else { if (modalityCurtain != null) { @@ -500,6 +695,9 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } modalityCurtain = null; } + if (!doTabStop) { + removeTabBlockHandlers(); + } } } @@ -653,11 +851,64 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, if (icon != null) { icon = client.translateVaadinUri(icon); html = "<img src=\"" + Util.escapeAttribute(icon) - + "\" class=\"v-icon\" />" + html; + + "\" class=\"v-icon\" alt=\"\" />" + html; } + + // Provide information to assistive device users that a sub window was + // opened + String prefix = "<span class='" + + AriaHelper.ASSISTIVE_DEVICE_ONLY_STYLE + "'>" + + assistivePrefix + "</span>"; + String postfix = "<span class='" + + AriaHelper.ASSISTIVE_DEVICE_ONLY_STYLE + "'>" + + assistivePostfix + "</span>"; + + html = prefix + html + postfix; DOM.setInnerHTML(headerText, html); } + /** + * Setter for the text for assistive devices the window caption is prefixed + * with. + * + * @param assistivePrefix + * the assistivePrefix to set + */ + public void setAssistivePrefix(String assistivePrefix) { + this.assistivePrefix = assistivePrefix; + } + + /** + * Getter for the text for assistive devices the window caption is prefixed + * with. + * + * @return the assistivePrefix + */ + public String getAssistivePrefix() { + return assistivePrefix; + } + + /** + * Setter for the text for assistive devices the window caption is postfixed + * with. + * + * @param assistivePostfix + * the assistivePostfix to set + */ + public void setAssistivePostfix(String assistivePostfix) { + this.assistivePostfix = assistivePostfix; + } + + /** + * Getter for the text for assistive devices the window caption is postfixed + * with. + * + * @return the assistivePostfix + */ + public String getAssistivePostfix() { + return assistivePostfix; + } + @Override protected Element getContainerElement() { // in GWT 1.5 this method is used in PopupPanel constructor @@ -1026,6 +1277,13 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, } @Override + public void onKeyUp(KeyUpEvent event) { + if (isClosable() && event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { + onCloseClick(); + } + } + + @Override public void onBlur(BlurEvent event) { if (client.hasEventListeners(this, EventId.BLUR)) { client.updateVariable(id, EventId.BLUR, "", true); @@ -1061,4 +1319,97 @@ public class VWindow extends VOverlay implements ShortcutActionHandlerOwner, - contentPanel.getElement().getOffsetWidth(); } + /** + * Allows to specify which connectors contain the description for the + * window. Text contained in the widgets of the connectors will be read by + * assistive devices when it is opened. + * <p> + * When the provided array is empty, an existing description is removed. + * + * @param connectors + * with the connectors of the widgets to use as description + */ + public void setAssistiveDescription(Connector[] connectors) { + if (connectors != null) { + assistiveConnectors = connectors; + + if (connectors.length == 0) { + Roles.getDialogRole().removeAriaDescribedbyProperty( + getElement()); + } else { + Id[] ids = new Id[connectors.length]; + for (int index = 0; index < connectors.length; index++) { + if (connectors[index] == null) { + throw new IllegalArgumentException( + "All values in parameter description need to be non-null"); + } + + Element element = ((ComponentConnector) connectors[index]) + .getWidget().getElement(); + AriaHelper.ensureHasId(element); + ids[index] = Id.of(element); + } + + Roles.getDialogRole().setAriaDescribedbyProperty(getElement(), + ids); + } + } else { + throw new IllegalArgumentException( + "Parameter description must be non-null"); + } + } + + /** + * Gets the connectors that are used as assistive description. Text + * contained in these connectors will be read by assistive devices when the + * window is opened. + * + * @return list of previously set connectors + */ + public List<Connector> getAssistiveDescription() { + return Collections.unmodifiableList(Arrays.asList(assistiveConnectors)); + } + + /** + * Sets the WAI-ARIA role the window. + * + * This role defines how an assistive device handles a window. Available + * roles are alertdialog and dialog (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a>). + * + * The default role is dialog. + * + * @param role + * WAI-ARIA role to set for the window + */ + public void setWaiAriaRole(WindowRole role) { + if ("alertdialog".equals(role)) { + Roles.getAlertdialogRole().set(getElement()); + } else { + Roles.getDialogRole().set(getElement()); + } + } + + /** + * Registers the handlers that prevent to leave the window using the + * Tab-key. + * <p> + * The value of the parameter doTabStop is stored and used for non-modal + * windows. For modal windows, the handlers are always registered, while + * preserving the stored value. + * + * @param doTabStop + * true to prevent leaving the window, false to allow leaving the + * window for non modal windows + */ + public void setTabStopEnabled(boolean doTabStop) { + this.doTabStop = doTabStop; + + if (doTabStop || vaadinModality) { + addTabBlockHandlers(); + } else { + removeTabBlockHandlers(); + } + } } diff --git a/client/src/com/vaadin/client/ui/VWindowOverlay.java b/client/src/com/vaadin/client/ui/VWindowOverlay.java new file mode 100644 index 0000000000..efc01cf63e --- /dev/null +++ b/client/src/com/vaadin/client/ui/VWindowOverlay.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2013 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; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.RootPanel; +import com.vaadin.client.ApplicationConnection; + +public class VWindowOverlay extends VOverlay { + public VWindowOverlay() { + } + + public VWindowOverlay(boolean autoHide, boolean modal, boolean showShadow) { + super(autoHide, modal, showShadow); + } + + /** + * Gets the 'overlay container' element. Tries to find the current + * {@link ApplicationConnection} using {@link #getApplicationConnection()}. + * + * @return the overlay container element for the current + * {@link ApplicationConnection} or another element if the current + * {@link ApplicationConnection} cannot be determined. + */ + @Override + public Element getOverlayContainer() { + ApplicationConnection ac = getApplicationConnection(); + if (ac == null) { + return super.getOverlayContainer(); + } else { + Element overlayContainer = getOverlayContainer(ac); + return overlayContainer; + } + } + + /** + * Gets the 'overlay container' element pertaining to the given + * {@link ApplicationConnection}. Each overlay should be created in a + * overlay container element, so that the correct theme and styles can be + * applied. + * + * @param ac + * A reference to {@link ApplicationConnection} + * @return The overlay container + */ + public static Element getOverlayContainer(ApplicationConnection ac) { + String id = ac.getConfiguration().getRootPanelId(); + id = id += "-window-overlays"; + Element container = DOM.getElementById(id); + if (container == null) { + container = DOM.createDiv(); + container.setId(id); + String styles = ac.getUIConnector().getWidget().getParent() + .getStyleName(); + container.addClassName(styles); + container.addClassName(CLASSNAME_CONTAINER); + RootPanel.get().getElement().appendChild(container); + } + + return container; + } +} diff --git a/client/src/com/vaadin/client/ui/button/ButtonConnector.java b/client/src/com/vaadin/client/ui/button/ButtonConnector.java index a6630f28b9..9a63808742 100644 --- a/client/src/com/vaadin/client/ui/button/ButtonConnector.java +++ b/client/src/com/vaadin/client/ui/button/ButtonConnector.java @@ -87,8 +87,7 @@ public class ButtonConnector extends AbstractComponentConnector implements getWidget().icon.getElement(), getWidget().captionElement); } - getWidget().icon.setUri(getIcon()); - getWidget().icon.setAlternateText(getState().iconAltText); + getWidget().icon.setUri(getIcon(), getState().iconAltText); } else { if (getWidget().icon != null) { getWidget().wrapper.removeChild(getWidget().icon diff --git a/client/src/com/vaadin/client/ui/dd/VHtml5File.java b/client/src/com/vaadin/client/ui/dd/VHtml5File.java index 4b36e7fd1b..c4ad615fbd 100644 --- a/client/src/com/vaadin/client/ui/dd/VHtml5File.java +++ b/client/src/com/vaadin/client/ui/dd/VHtml5File.java @@ -35,7 +35,13 @@ public class VHtml5File extends JavaScriptObject { return this.type; }-*/; - public native final int getSize() + /* + * Browser implementations support files >2GB dropped and report the value + * as long. Due to JSNI limitations this value needs to be sent as double + * and then cast back to a long value. + * www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html#important + */ + public native final double getSize() /*-{ return this.size ? this.size : 0; }-*/; diff --git a/client/src/com/vaadin/client/ui/tree/TreeConnector.java b/client/src/com/vaadin/client/ui/tree/TreeConnector.java index ef016c31b7..6f89137918 100644 --- a/client/src/com/vaadin/client/ui/tree/TreeConnector.java +++ b/client/src/com/vaadin/client/ui/tree/TreeConnector.java @@ -45,6 +45,11 @@ public class TreeConnector extends AbstractComponentConnector implements protected final Map<TreeNode, TooltipInfo> tooltipMap = new HashMap<TreeNode, TooltipInfo>(); @Override + protected void init() { + getWidget().connector = this; + } + + @Override public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { if (!isRealUpdate(uidl)) { return; diff --git a/client/src/com/vaadin/client/ui/ui/UIConnector.java b/client/src/com/vaadin/client/ui/ui/UIConnector.java index c6d2e1436b..45005ddd9f 100644 --- a/client/src/com/vaadin/client/ui/ui/UIConnector.java +++ b/client/src/com/vaadin/client/ui/ui/UIConnector.java @@ -101,10 +101,6 @@ public class UIConnector extends AbstractSingleComponentContainerConnector protected void init() { super.init(); registerRpc(PageClientRpc.class, new PageClientRpc() { - @Override - public void setTitle(String title) { - com.google.gwt.user.client.Window.setTitle(title); - } @Override public void reload() { @@ -661,6 +657,11 @@ public class UIConnector extends AbstractSingleComponentContainerConnector configurePolling(); } + if (stateChangeEvent.hasPropertyChanged("pageState.title")) { + com.google.gwt.user.client.Window + .setTitle(getState().pageState.title); + } + if (stateChangeEvent.hasPropertyChanged("pushConfiguration")) { getConnection().setPushEnabled( getState().pushConfiguration.mode.isEnabled()); diff --git a/client/src/com/vaadin/client/ui/window/WindowConnector.java b/client/src/com/vaadin/client/ui/window/WindowConnector.java index 4b839384a2..464ab386c1 100644 --- a/client/src/com/vaadin/client/ui/window/WindowConnector.java +++ b/client/src/com/vaadin/client/ui/window/WindowConnector.java @@ -295,8 +295,18 @@ public class WindowConnector extends AbstractSingleComponentContainerConnector if (getIcon() != null) { iconURL = getIcon(); } + + window.setAssistivePrefix(state.assistivePrefix); + window.setAssistivePostfix(state.assistivePostfix); window.setCaption(state.caption, iconURL); + window.setWaiAriaRole(getState().role); + window.setAssistiveDescription(state.contentDescription); + + window.setTabStopEnabled(getState().assistiveTabStop); + window.setTabStopTopAssistiveText(getState().assistiveTabStopTopText); + window.setTabStopBottomAssistiveText(getState().assistiveTabStopBottomText); + clickEventHandler.handleEventHandlerRegistration(); window.immediate = state.immediate; |