diff options
author | Adam Wagner <wbadam@users.noreply.github.com> | 2018-01-31 11:26:59 +0200 |
---|---|---|
committer | Ilia Motornyi <elmot@vaadin.com> | 2018-01-31 11:26:59 +0200 |
commit | 75b98bee62185c858a875393d50c3fa5e2fe64e7 (patch) | |
tree | 7bc8f3ab6360bf4c1f6758c49faddee0ea9900fb | |
parent | 5b9d0b9175f9ce2f20d728db844b6fd03fea1461 (diff) | |
download | vaadin-framework-75b98bee62185c858a875393d50c3fa5e2fe64e7.tar.gz vaadin-framework-75b98bee62185c858a875393d50c3fa5e2fe64e7.zip |
Add ARIA label support to DateField (#10538)
Fixes #10454
8 files changed, 338 insertions, 10 deletions
diff --git a/client/src/main/java/com/vaadin/client/ui/VAbstractCalendarPanel.java b/client/src/main/java/com/vaadin/client/ui/VAbstractCalendarPanel.java index 21ea8ba4c4..8810b26e30 100644 --- a/client/src/main/java/com/vaadin/client/ui/VAbstractCalendarPanel.java +++ b/client/src/main/java/com/vaadin/client/ui/VAbstractCalendarPanel.java @@ -25,6 +25,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; +import com.google.gwt.aria.client.Id; import com.google.gwt.aria.client.Roles; import com.google.gwt.aria.client.SelectedValue; import com.google.gwt.dom.client.Element; @@ -56,6 +57,7 @@ import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.DateTimeService; import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.aria.AriaHelper; import com.vaadin.shared.util.SharedUtil; /** @@ -167,6 +169,14 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> private boolean initialRenderDone = false; + private String prevMonthAssistiveLabel; + + private String nextMonthAssistiveLabel; + + private String prevYearAssistiveLabel; + + private String nextYearAssistiveLabel; + /** * Represents a click handler for when a user selects a value by using the * mouse @@ -247,6 +257,13 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> if (curday.getDate().equals(date)) { curday.addStyleDependentName(CN_FOCUSED); focusedDay = curday; + + // Reference focused day from calendar panel + Roles.getGridRole() + .setAriaActivedescendantProperty( + getElement(), + Id.of(curday.getElement())); + return; } } @@ -490,8 +507,10 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> * * @param needsMonth * Should the month buttons be visible? + * @param needsBody + * indicates whether the calendar body is drawn */ - private void buildCalendarHeader(boolean needsMonth) { + private void buildCalendarHeader(boolean needsMonth, boolean needsBody) { getRowFormatter().addStyleName(0, parent.getStylePrimaryName() + "-calendarpanel-header"); @@ -501,16 +520,20 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> prevMonth.setHTML("‹"); prevMonth.setStyleName("v-button-prevmonth"); - prevMonth.setTabIndex(-1); - nextMonth = new VEventButton(); nextMonth.setHTML("›"); nextMonth.setStyleName("v-button-nextmonth"); - nextMonth.setTabIndex(-1); - setWidget(0, 3, nextMonth); setWidget(0, 1, prevMonth); + + Roles.getButtonRole().set(prevMonth.getElement()); + Roles.getButtonRole() + .setTabindexExtraAttribute(prevMonth.getElement(), -1); + + Roles.getButtonRole().set(nextMonth.getElement()); + Roles.getButtonRole() + .setTabindexExtraAttribute(nextMonth.getElement(), -1); } else if (prevMonth != null && !needsMonth) { // Remove month traverse buttons remove(prevMonth); @@ -525,18 +548,26 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> prevYear.setHTML("«"); prevYear.setStyleName("v-button-prevyear"); - prevYear.setTabIndex(-1); nextYear = new VEventButton(); nextYear.setHTML("»"); nextYear.setStyleName("v-button-nextyear"); - nextYear.setTabIndex(-1); setWidget(0, 0, prevYear); setWidget(0, 4, nextYear); + + Roles.getButtonRole().set(prevYear.getElement()); + Roles.getButtonRole() + .setTabindexExtraAttribute(prevYear.getElement(), -1); + + Roles.getButtonRole().set(nextYear.getElement()); + Roles.getButtonRole() + .setTabindexExtraAttribute(nextYear.getElement(), -1); } updateControlButtonRangeStyles(needsMonth); + updateAssistiveLabels(); + final String monthName = needsMonth ? getDateTimeService().getMonth(displayedMonth.getMonth()) : ""; @@ -553,6 +584,16 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> getFlexCellFormatter().setStyleName(0, 1, parent.getStylePrimaryName() + "-calendarpanel-prevmonth"); + // Set ID to be referenced from focused date or calendar panel + Element monthYearElement = getFlexCellFormatter().getElement(0, 2); + AriaHelper.ensureHasId(monthYearElement); + if (!needsBody) { + Roles.getGridRole().setAriaLabelledbyProperty(getElement(), + Id.of(monthYearElement)); + } else { + Roles.getGridRole().removeAriaLabelledbyProperty(getElement()); + } + setHTML(0, 2, "<span class=\"" + parent.getStylePrimaryName() + "-calendarpanel-month\">" + monthName + " " + year @@ -830,6 +871,17 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> Date dayDate = (Date) curr.clone(); Day day = new Day(dayDate); + // Set ID with prefix of the calendar panel's ID + day.getElement().setId(getElement().getId() + "-" + weekOfMonth + + "-" + dayOfWeek); + + // Set assistive label to read focused date and month/year + Roles.getButtonRole().set(day.getElement()); + Roles.getButtonRole() + .setAriaLabelledbyProperty(day.getElement(), + Id.of(day.getElement()), + Id.of(getFlexCellFormatter().getElement(0, 2))); + day.setStyleName(getDateField().getStylePrimaryName() + "-calendarpanel-day"); @@ -850,6 +902,11 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> focusedDay = day; if (hasFocus) { day.addStyleDependentName(CN_FOCUSED); + + // Reference focused day from calendar panel + Roles.getGridRole() + .setAriaActivedescendantProperty(getElement(), + Id.of(day.getElement())); } } if (curr.getMonth() != displayedMonth.getMonth()) { @@ -940,7 +997,7 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> final boolean needsMonth = !isYear(getResolution()); boolean needsBody = isBelowMonth(resolution); - buildCalendarHeader(needsMonth); + buildCalendarHeader(needsMonth, needsBody); clearCalendarBody(!needsBody); if (needsBody) { buildCalendarBody(); @@ -1229,7 +1286,6 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> event.getNativeEvent().getShiftKey())) { event.preventDefault(); } - } /** @@ -1345,7 +1401,7 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> renderCalendar(); return true; - } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) { + } else if (keycode == getCloseKey()) { onCancel(); // TODO fire close event @@ -2043,6 +2099,77 @@ public abstract class VAbstractCalendarPanel<R extends Enum<R>> } } + /** + * Set assistive label for the previous year element. + * + * @param label + * the label to set + * @since + */ + public void setAssistiveLabelPreviousYear(String label) { + prevYearAssistiveLabel = label; + } + + /** + * Set assistive label for the next year element. + * + * @param label + * the label to set + * @since + */ + public void setAssistiveLabelNextYear(String label) { + nextYearAssistiveLabel = label; + } + + /** + * Set assistive label for the previous month element. + * + * @param label + * the label to set + * @since + */ + public void setAssistiveLabelPreviousMonth(String label) { + prevMonthAssistiveLabel = label; + } + + /** + * Set assistive label for the next month element. + * + * @param label + * the label to set + * @since + */ + public void setAssistiveLabelNextMonth(String label) { + nextMonthAssistiveLabel = label; + } + + /** + * Updates assistive labels of the navigation elements. + * + * @since + */ + public void updateAssistiveLabels() { + if (prevMonth != null) { + Roles.getButtonRole().setAriaLabelProperty(prevMonth.getElement(), + prevMonthAssistiveLabel); + } + + if (nextMonth != null) { + Roles.getButtonRole().setAriaLabelProperty(nextMonth.getElement(), + nextMonthAssistiveLabel); + } + + if (prevYear != null) { + Roles.getButtonRole().setAriaLabelProperty(prevYear.getElement(), + prevYearAssistiveLabel); + } + + if (nextYear != null) { + Roles.getButtonRole().setAriaLabelProperty(nextYear.getElement(), + nextYearAssistiveLabel); + } + } + private static Logger getLogger() { return Logger.getLogger(VAbstractCalendarPanel.class.getName()); } diff --git a/client/src/main/java/com/vaadin/client/ui/datefield/AbstractDateFieldConnector.java b/client/src/main/java/com/vaadin/client/ui/datefield/AbstractDateFieldConnector.java index 4b52ee50f6..35d1c1c4d1 100644 --- a/client/src/main/java/com/vaadin/client/ui/datefield/AbstractDateFieldConnector.java +++ b/client/src/main/java/com/vaadin/client/ui/datefield/AbstractDateFieldConnector.java @@ -24,11 +24,15 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import com.vaadin.client.LocaleNotLoadedException; +import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractFieldConnector; +import com.vaadin.client.ui.VAbstractCalendarPanel; +import com.vaadin.client.ui.VAbstractPopupCalendar; import com.vaadin.client.ui.VDateField; import com.vaadin.shared.ui.datefield.AbstractDateFieldServerRpc; import com.vaadin.shared.ui.datefield.AbstractDateFieldState; +import com.vaadin.shared.ui.datefield.AbstractDateFieldState.AccessibleElement; public abstract class AbstractDateFieldConnector<R extends Enum<R>> extends AbstractFieldConnector { @@ -133,6 +137,36 @@ public abstract class AbstractDateFieldConnector<R extends Enum<R>> widget.setDefaultDate(getDefaultValues()); } + @OnStateChange("assistiveLabels") + private void updateAssistiveLabels() { + if (getWidget() instanceof VAbstractPopupCalendar) { + setAndUpdateAssistiveLabels( + ((VAbstractPopupCalendar) getWidget()).calendar); + } + } + + /** + * Sets assistive labels for the calendar panel's navigation elements, and + * updates these labels. + * + * @param calendar + * the calendar panel for which to set the assistive labels + * @since + */ + protected void setAndUpdateAssistiveLabels( + VAbstractCalendarPanel calendar) { + calendar.setAssistiveLabelPreviousMonth( + getState().assistiveLabels.get(AccessibleElement.PREVIOUS_MONTH)); + calendar.setAssistiveLabelNextMonth( + getState().assistiveLabels.get(AccessibleElement.NEXT_MONTH)); + calendar.setAssistiveLabelPreviousYear( + getState().assistiveLabels.get(AccessibleElement.PREVIOUS_YEAR)); + calendar.setAssistiveLabelNextYear( + getState().assistiveLabels.get(AccessibleElement.NEXT_YEAR)); + + calendar.updateAssistiveLabels(); + } + private static Logger getLogger() { return Logger.getLogger(AbstractDateFieldConnector.class.getName()); } diff --git a/client/src/main/java/com/vaadin/client/ui/datefield/AbstractInlineDateFieldConnector.java b/client/src/main/java/com/vaadin/client/ui/datefield/AbstractInlineDateFieldConnector.java index 2492272111..51852de708 100644 --- a/client/src/main/java/com/vaadin/client/ui/datefield/AbstractInlineDateFieldConnector.java +++ b/client/src/main/java/com/vaadin/client/ui/datefield/AbstractInlineDateFieldConnector.java @@ -19,6 +19,7 @@ import java.util.Date; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.UIDL; +import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.VAbstractCalendarPanel; import com.vaadin.client.ui.VAbstractDateFieldCalendar; @@ -99,6 +100,11 @@ public abstract class AbstractInlineDateFieldConnector<PANEL extends VAbstractCa widget.calendarPanel.renderCalendar(); } + @OnStateChange("assistiveLabels") + private void updateAssistiveLabels() { + setAndUpdateAssistiveLabels(getWidget().calendarPanel); + } + @Override @SuppressWarnings("unchecked") public VAbstractDateFieldCalendar<PANEL, R> getWidget() { diff --git a/server/src/main/java/com/vaadin/ui/AbstractDateField.java b/server/src/main/java/com/vaadin/ui/AbstractDateField.java index 110223c628..3998afbae6 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractDateField.java +++ b/server/src/main/java/com/vaadin/ui/AbstractDateField.java @@ -55,6 +55,7 @@ import com.vaadin.server.UserError; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.datefield.AbstractDateFieldServerRpc; import com.vaadin.shared.ui.datefield.AbstractDateFieldState; +import com.vaadin.shared.ui.datefield.AbstractDateFieldState.AccessibleElement; import com.vaadin.shared.ui.datefield.DateResolution; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; @@ -875,4 +876,31 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & } return Collections.unmodifiableMap(hashMap); } + + /** + * Sets the assistive label for a calendar navigation element. This sets the + * {@code aria-label} attribute for the element which is used by screen + * reading software. + * + * @param element + * the element for which to set the label. Not {@code null}. + * @param label + * the assistive label to set + * @since + */ + public void setAssistiveLabel(AccessibleElement element, String label) { + Objects.requireNonNull(element, "Element cannot be null"); + getState().assistiveLabels.put(element, label); + } + + /** + * Gets the assistive label of a calendar navigation element. + * + * @param element + * the element of which to get the assistive label + * @since + */ + public void getAssistiveLabel(AccessibleElement element) { + getState(false).assistiveLabels.get(element); + } } diff --git a/shared/src/main/java/com/vaadin/shared/ui/datefield/AbstractDateFieldState.java b/shared/src/main/java/com/vaadin/shared/ui/datefield/AbstractDateFieldState.java index 69bc065cff..553e82a57e 100644 --- a/shared/src/main/java/com/vaadin/shared/ui/datefield/AbstractDateFieldState.java +++ b/shared/src/main/java/com/vaadin/shared/ui/datefield/AbstractDateFieldState.java @@ -31,6 +31,18 @@ import com.vaadin.shared.annotations.NoLayout; */ public class AbstractDateFieldState extends AbstractFieldState { + /** + * Navigation elements that have assistive label. + * + * @since + */ + public enum AccessibleElement { + PREVIOUS_YEAR, + NEXT_YEAR, + PREVIOUS_MONTH, + NEXT_MONTH + } + { primaryStyleName = "v-datefield"; } @@ -114,4 +126,19 @@ public class AbstractDateFieldState extends AbstractFieldState { */ public Map<String, String> dateStyles = new HashMap<String, String>(); + /** + * Map of elements and their corresponding assistive labels. + * + * @since + */ + public Map<AccessibleElement, String> assistiveLabels = new HashMap<>(); + + // Set default accessive labels + { + assistiveLabels.put(AccessibleElement.PREVIOUS_YEAR, "Previous year"); + assistiveLabels.put(AccessibleElement.NEXT_YEAR, "Next year"); + assistiveLabels.put(AccessibleElement.PREVIOUS_MONTH, "Previous month"); + assistiveLabels.put(AccessibleElement.NEXT_MONTH, "Next month"); + } + } diff --git a/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldAria.java b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldAria.java new file mode 100644 index 0000000000..b74b1db3df --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldAria.java @@ -0,0 +1,45 @@ +package com.vaadin.tests.components.datefield; + +import java.time.LocalDate; +import java.util.Arrays; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.datefield.AbstractDateFieldState.AccessibleElement; +import com.vaadin.shared.ui.datefield.DateResolution; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.DateField; +import com.vaadin.ui.InlineDateField; + +@Widgetset("com.vaadin.DefaultWidgetSet") +public class DateFieldAria extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + DateField dateField = new DateField("Accessible DateField", + LocalDate.now()); + addComponent(dateField); + + InlineDateField inlineDateField = new InlineDateField( + "Accessible InlineDateField", LocalDate.now()); + addComponent(inlineDateField); + + ComboBox<DateResolution> resolutions = new ComboBox<>("Date resolution", + Arrays.asList(DateResolution.values())); + resolutions.setValue(DateResolution.DAY); + resolutions.addValueChangeListener(e -> { + dateField.setResolution(e.getValue()); + inlineDateField.setResolution(e.getValue()); + }); + addComponent(resolutions); + + addComponent(new Button("Change assistive labels", e -> { + dateField.setAssistiveLabel(AccessibleElement.PREVIOUS_MONTH, + "Navigate to previous month"); + inlineDateField.setAssistiveLabel(AccessibleElement.NEXT_MONTH, + "Navigate to next month"); + })); + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFields.java b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFields.java index 5e8db6f14c..86bee4af69 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFields.java +++ b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateFields.java @@ -5,12 +5,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import com.vaadin.annotations.Widgetset; import com.vaadin.shared.ui.datefield.DateResolution; import com.vaadin.tests.components.ComponentTestCase; import com.vaadin.ui.Component; import com.vaadin.ui.DateField; @SuppressWarnings("serial") +@Widgetset("com.vaadin.DefaultWidgetSet") public class DateFields extends ComponentTestCase<DateField> { private static final Locale[] LOCALES = { Locale.US, Locale.TAIWAN, diff --git a/uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldAriaTest.java b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldAriaTest.java new file mode 100644 index 0000000000..1d3cd39736 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldAriaTest.java @@ -0,0 +1,59 @@ +package com.vaadin.tests.components.datefield; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.elements.DateFieldElement; +import com.vaadin.testbench.elements.InlineDateFieldElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class DateFieldAriaTest extends SingleBrowserTest { + + @Test + public void changeAssistiveLabel() { + openTestURL(); + + DateFieldElement dateField = $(DateFieldElement.class).first(); + dateField.openPopup(); + WebElement prevMonthButton = driver + .findElement(By.className("v-datefield-popup")) + .findElement(By.className("v-button-prevmonth")); + + Assert.assertEquals("Previous month", + prevMonthButton.getAttribute("aria-label")); + + dateField.openPopup(); // This actually closes the calendar popup + + ButtonElement changeLabelsButton = $(ButtonElement.class).first(); + changeLabelsButton.click(); + + dateField.openPopup(); + prevMonthButton = driver.findElement(By.className("v-datefield-popup")) + .findElement(By.className("v-button-prevmonth")); + + Assert.assertEquals("Navigate to previous month", + prevMonthButton.getAttribute("aria-label")); + } + + @Test + public void changeAssistiveLabelInline() { + openTestURL(); + + InlineDateFieldElement dateField = $(InlineDateFieldElement.class) + .first(); + WebElement nextMonthElement = dateField + .findElement(By.className("v-button-nextmonth")); + + Assert.assertEquals("Next month", + nextMonthElement.getAttribute("aria-label")); + + ButtonElement changeLabelsButton = $(ButtonElement.class).first(); + changeLabelsButton.click(); + + Assert.assertEquals("Navigate to next month", + nextMonthElement.getAttribute("aria-label")); + } +} |