/* * 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; import java.util.Date; 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.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.VConsole; import com.vaadin.client.ui.VCalendarPanel.FocusOutListener; import com.vaadin.client.ui.VCalendarPanel.SubmitListener; import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.ui.datefield.PopupDateFieldState; import com.vaadin.shared.ui.datefield.Resolution; /** * Represents a date selection component with a text field and a popup date * selector. * * Note: To change the keyboard assignments used in the popup dialog you * should extend com.vaadin.client.ui.VCalendarPanel and then pass * set it by calling the setCalendarPanel(VCalendarPanel panel) * method. * */ public class VPopupCalendar extends VTextualDate implements Field, ClickHandler, CloseHandler, SubPartAware { /** For internal use only. May be removed or replaced in the future. */ public final Button calendarToggle = new Button(); /** For internal use only. May be removed or replaced in the future. */ public VCalendarPanel calendar; /** For internal use only. May be removed or replaced in the future. */ public final VOverlay popup; /** For internal use only. May be removed or replaced in the future. */ public boolean parsable = true; private boolean open = false; private boolean textFieldEnabled = true; private String captionId; private Label selectedDate; private Element descriptionForAssisitveDevicesElement; public VPopupCalendar() { super(); calendarToggle.setText(""); calendarToggle.addClickHandler(this); // -2 instead of -1 to avoid FocusWidget.onAttach to reset it calendarToggle.getElement().setTabIndex(-2); Roles.getButtonRole().set(calendarToggle.getElement()); Roles.getButtonRole().setAriaHiddenState(calendarToggle.getElement(), true); add(calendarToggle); // Description of the usage of the widget for assisitve device users descriptionForAssisitveDevicesElement = DOM.createDiv(); descriptionForAssisitveDevicesElement .setInnerText(PopupDateFieldState.DESCRIPTION_FOR_ASSISTIVE_DEVICES); AriaHelper.ensureHasId(descriptionForAssisitveDevicesElement); Roles.getTextboxRole().setAriaDescribedbyProperty(text.getElement(), Id.of(descriptionForAssisitveDevicesElement)); AriaHelper.setVisibleForAssistiveDevicesOnly( descriptionForAssisitveDevicesElement, true); calendar = GWT.create(VCalendarPanel.class); calendar.setParentField(this); calendar.setFocusOutListener(new FocusOutListener() { @Override public boolean onFocusOut(DomEvent event) { event.preventDefault(); closeCalendarPanel(); return true; } }); // FIXME: Problem is, that the element with the provided id does not // exist yet in html. This is the same problem as with the context menu. // Apply here the same fix (#11795) Roles.getTextboxRole().setAriaControlsProperty(text.getElement(), Id.of(calendar.getElement())); Roles.getButtonRole().setAriaControlsProperty( calendarToggle.getElement(), Id.of(calendar.getElement())); calendar.setSubmitListener(new SubmitListener() { @Override public void onSubmit() { // Update internal value and send valuechange event if immediate updateValue(calendar.getDate()); // Update text field (a must when not immediate). buildDate(true); closeCalendarPanel(); } @Override public void onCancel() { closeCalendarPanel(); } }); popup = new VOverlay(true, false, true); popup.setOwner(this); FlowPanel wrapper = new FlowPanel(); selectedDate = new Label(); selectedDate.setStyleName(getStylePrimaryName() + "-selecteddate"); AriaHelper.setVisibleForAssistiveDevicesOnly(selectedDate.getElement(), true); Roles.getTextboxRole().setAriaLiveProperty(selectedDate.getElement(), LiveValue.ASSERTIVE); Roles.getTextboxRole().setAriaAtomicProperty(selectedDate.getElement(), true); wrapper.add(selectedDate); wrapper.add(calendar); popup.setWidget(wrapper); popup.addCloseHandler(this); DOM.setElementProperty(calendar.getElement(), "id", "PID_VAADIN_POPUPCAL"); sinkEvents(Event.ONKEYDOWN); updateStyleNames(); } @Override protected void onAttach() { super.onAttach(); DOM.appendChild(RootPanel.get().getElement(), descriptionForAssisitveDevicesElement); } @Override protected void onDetach() { super.onDetach(); descriptionForAssisitveDevicesElement.removeFromParent(); } @SuppressWarnings("deprecation") public void updateValue(Date newDate) { Date currentDate = getCurrentDate(); if (currentDate == null || newDate.getTime() != currentDate.getTime()) { setCurrentDate((Date) newDate.clone()); getClient().updateVariable(getId(), "year", newDate.getYear() + 1900, false); if (getCurrentResolution().getCalendarField() > Resolution.YEAR .getCalendarField()) { getClient().updateVariable(getId(), "month", newDate.getMonth() + 1, false); if (getCurrentResolution().getCalendarField() > Resolution.MONTH .getCalendarField()) { getClient().updateVariable(getId(), "day", newDate.getDate(), false); if (getCurrentResolution().getCalendarField() > Resolution.DAY .getCalendarField()) { getClient().updateVariable(getId(), "hour", newDate.getHours(), false); if (getCurrentResolution().getCalendarField() > Resolution.HOUR .getCalendarField()) { getClient().updateVariable(getId(), "min", newDate.getMinutes(), false); if (getCurrentResolution().getCalendarField() > Resolution.MINUTE .getCalendarField()) { getClient().updateVariable(getId(), "sec", newDate.getSeconds(), false); } } } } } } } /** * Checks whether the text field is enabled. * * @see VPopupCalendar#setTextFieldEnabled(boolean) * @return The current state of the text field. */ public boolean isTextFieldEnabled() { return textFieldEnabled; } /** * Sets the state of the text field of this component. By default the text * field is enabled. Disabling it causes only the button for date selection * to be active, thus preventing the user from entering invalid dates. See * {@link http://dev.vaadin.com/ticket/6790}. * * @param state */ public void setTextFieldEnabled(boolean textFieldEnabled) { this.textFieldEnabled = textFieldEnabled; text.setEnabled(textFieldEnabled); if (textFieldEnabled) { calendarToggle.setTabIndex(-1); Roles.getButtonRole().setAriaHiddenState( calendarToggle.getElement(), true); } else { calendarToggle.setTabIndex(0); Roles.getButtonRole().setAriaHiddenState( calendarToggle.getElement(), false); } handleAriaAttributes(); } @Override public void bindAriaCaption( com.google.gwt.user.client.Element captionElement) { if (captionElement == null) { captionId = null; } else { captionId = captionElement.getId(); } if (isTextFieldEnabled()) { super.bindAriaCaption(captionElement); } else { AriaHelper.bindCaption(calendarToggle, captionElement); } handleAriaAttributes(); } private void handleAriaAttributes() { Widget removeFromWidget; Widget setForWidget; if (isTextFieldEnabled()) { setForWidget = text; removeFromWidget = calendarToggle; } else { setForWidget = calendarToggle; removeFromWidget = text; } Roles.getFormRole().removeAriaLabelledbyProperty( removeFromWidget.getElement()); if (captionId == null) { Roles.getFormRole().removeAriaLabelledbyProperty( setForWidget.getElement()); } else { Roles.getFormRole().setAriaLabelledbyProperty( setForWidget.getElement(), Id.of(captionId)); } } /* * (non-Javadoc) * * @see * com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String) */ @Override public void setStyleName(String style) { super.setStyleName(style); updateStyleNames(); } @Override public void setStylePrimaryName(String style) { removeStyleName(getStylePrimaryName() + "-popupcalendar"); super.setStylePrimaryName(style); updateStyleNames(); } @Override protected void updateStyleNames() { super.updateStyleNames(); if (getStylePrimaryName() != null && calendarToggle != null) { addStyleName(getStylePrimaryName() + "-popupcalendar"); calendarToggle.setStyleName(getStylePrimaryName() + "-button"); popup.setStyleName(getStylePrimaryName() + "-popup"); calendar.setStyleName(getStylePrimaryName() + "-calendarpanel"); } } /** * Opens the calendar panel popup */ public void openCalendarPanel() { if (!open && !readonly && isEnabled()) { open = true; if (getCurrentDate() != null) { calendar.setDate((Date) getCurrentDate().clone()); } else { calendar.setDate(new Date()); } // clear previous values popup.setWidth(""); popup.setHeight(""); popup.setPopupPositionAndShow(new PositionCallback() { @Override public void setPosition(int offsetWidth, int offsetHeight) { final int w = offsetWidth; final int h = offsetHeight; final int browserWindowWidth = Window.getClientWidth() + Window.getScrollLeft(); final int browserWindowHeight = Window.getClientHeight() + Window.getScrollTop(); int t = calendarToggle.getAbsoluteTop(); int l = calendarToggle.getAbsoluteLeft(); // Add a little extra space to the right to avoid // problems with IE7 scrollbars and to make it look // nicer. int extraSpace = 30; boolean overflowRight = false; if (l + +w + extraSpace > browserWindowWidth) { overflowRight = true; // Part of the popup is outside the browser window // (to the right) l = browserWindowWidth - w - extraSpace; } if (t + h + calendarToggle.getOffsetHeight() + 30 > browserWindowHeight) { // Part of the popup is outside the browser window // (below) t = browserWindowHeight - h - calendarToggle.getOffsetHeight() - 30; if (!overflowRight) { // Show to the right of the popup button unless we // are in the lower right corner of the screen l += calendarToggle.getOffsetWidth(); } } popup.setPopupPosition(l, t + calendarToggle.getOffsetHeight() + 2); /* * We have to wait a while before focusing since the popup * needs to be opened before we can focus */ Timer focusTimer = new Timer() { @Override public void run() { setFocus(true); } }; focusTimer.schedule(100); } }); } else { VConsole.error("Cannot reopen popup, it is already open!"); } } /* * (non-Javadoc) * * @see * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event * .dom.client.ClickEvent) */ @Override public void onClick(ClickEvent event) { if (event.getSource() == calendarToggle && isEnabled()) { openCalendarPanel(); } } /* * (non-Javadoc) * * @see * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt * .event.logical.shared.CloseEvent) */ @Override public void onClose(CloseEvent event) { if (event.getSource() == popup) { buildDate(); if (!BrowserInfo.get().isTouchDevice()) { /* * Move focus to textbox, unless on touch device (avoids opening * virtual keyboard). */ focus(); } // TODO resolve what the "Sigh." is all about and document it here // Sigh. Timer t = new Timer() { @Override public void run() { open = false; } }; t.schedule(100); } } /** * Sets focus to Calendar panel. * * @param focus */ public void setFocus(boolean focus) { calendar.setFocus(focus); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); calendarToggle.setEnabled(enabled); Roles.getButtonRole().setAriaDisabledState(calendarToggle.getElement(), !enabled); } /** * Sets the content of a special field for assistive devices, so that they * can recognize the change and inform the user (reading out in case of * screen reader) * * @param selectedDate * Date that is currently selected */ public void setFocusedDate(Date selectedDate) { this.selectedDate.setText(DateTimeFormat.getFormat("dd, MMMM, yyyy") .format(selectedDate)); } /** * For internal use only. May be removed or replaced in the future. * * @see com.vaadin.client.ui.VTextualDate#buildDate() */ @Override public void buildDate() { // Save previous value String previousValue = getText(); super.buildDate(); // Restore previous value if the input could not be parsed if (!parsable) { setText(previousValue); } // superclass sets the text field independently when building date text.setEnabled(isEnabled() && isTextFieldEnabled()); } /** * Update the text field contents from the date. See {@link #buildDate()}. * * @param forceValid * true to force the text field to be updated, false to only * update if the parsable flag is true. */ protected void buildDate(boolean forceValid) { if (forceValid) { parsable = true; } buildDate(); } /* * (non-Javadoc) * * @see com.vaadin.client.ui.VDateField#onBrowserEvent(com.google * .gwt.user.client.Event) */ @Override public void onBrowserEvent(com.google.gwt.user.client.Event event) { super.onBrowserEvent(event); if (DOM.eventGetType(event) == Event.ONKEYDOWN && event.getKeyCode() == getOpenCalenderPanelKey()) { openCalendarPanel(); event.preventDefault(); } } /** * Get the key code that opens the calendar panel. By default it is the down * key but you can override this to be whatever you like * * @return */ protected int getOpenCalenderPanelKey() { return KeyCodes.KEY_DOWN; } /** * Closes the open popup panel */ public void closeCalendarPanel() { if (open) { popup.hide(true); } } private final String CALENDAR_TOGGLE_ID = "popupButton"; @Override public com.google.gwt.user.client.Element getSubPartElement(String subPart) { if (subPart.equals(CALENDAR_TOGGLE_ID)) { return calendarToggle.getElement(); } return super.getSubPartElement(subPart); } @Override public String getSubPartName(com.google.gwt.user.client.Element subElement) { if (calendarToggle.getElement().isOrHasChild(subElement)) { return CALENDAR_TOGGLE_ID; } return super.getSubPartName(subElement); } /** * Set a description that explains the usage of the Widget for users of * assistive devices. * * @param descriptionForAssistiveDevices * String with the description */ public void setDescriptionForAssistiveDevices( String descriptionForAssistiveDevices) { descriptionForAssisitveDevicesElement .setInnerText(descriptionForAssistiveDevices); } /** * Get the description that explains the usage of the Widget for users of * assistive devices. * * @return String with the description */ public String getDescriptionForAssistiveDevices() { return descriptionForAssisitveDevicesElement.getInnerText(); } /** * Sets the start range for this component. The start range is inclusive, * and it depends on the current resolution, what is considered inside the * range. * * @param startDate * - the allowed range's start date */ public void setRangeStart(Date rangeStart) { calendar.setRangeStart(rangeStart); } /** * Sets the end range for this component. The end range is inclusive, and it * depends on the current resolution, what is considered inside the range. * * @param endDate * - the allowed range's end date */ public void setRangeEnd(Date rangeEnd) { calendar.setRangeEnd(rangeEnd); } }