From c3ed249072bfef4817b8d653ccf22415ea249cef Mon Sep 17 00:00:00 2001 From: TatuJLund Date: Sat, 23 Nov 2024 12:23:46 +0200 Subject: [PATCH] fix: Navigation menu role feat: Add accesibility data to stats feat: Add pg up/down shortcuts to form refactor: Make side panel to have role dialog so that book form can have role form fix: User form role fix: Change Delete button description to include the name for better A11y --- .../vaadin/tatu/vaadincreate/AppLayout.java | 1 + .../admin/CategoryManagementView.java | 6 +- .../tatu/vaadincreate/admin/UserForm.java | 1 + .../vaadincreate/crud/BooksPresenter.java | 4 ++ .../tatu/vaadincreate/crud/BooksView.java | 35 +++++++----- .../tatu/vaadincreate/crud/form/BookForm.java | 56 ++++++++++++++++++- .../vaadincreate/crud/form/SidePanel.java | 2 + .../tatu/vaadincreate/stats/StatsView.java | 39 +++++++++++++ 8 files changed, 125 insertions(+), 19 deletions(-) diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/AppLayout.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/AppLayout.java index 27bef2a..a258741 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/AppLayout.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/AppLayout.java @@ -92,6 +92,7 @@ public AppLayout(UI ui, AccessControl accessControl) { toggleButton.addStyleName(ValoTheme.BUTTON_SMALL); menuLayout.addComponent(toggleButton); + AttributeExtension.of(menuItems).setAttribute("role", "menu"); menuLayout.addComponent(menuItems); menuItems.addStyleName(ValoTheme.MENU_ITEMS); diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/CategoryManagementView.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/CategoryManagementView.java index 05ab037..c227b3f 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/CategoryManagementView.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/CategoryManagementView.java @@ -173,7 +173,8 @@ class CategoryForm extends Composite { deleteButton = new Button(VaadinIcons.TRASH, e -> handleConfirmDelete()); deleteButton.addStyleName(ValoTheme.BUTTON_DANGER); - deleteButton.setDescription(getTranslation(I18n.DELETE)); + deleteButton.setDescription( + getTranslation(I18n.DELETE) + ": " + category.getName()); deleteButton.setEnabled(category.getId() != null); deleteButton.setDisableOnClick(true); @@ -196,7 +197,8 @@ private void configureNameField() { nameField = new TextField(); var nameFieldExt = AttributeExtension.of(nameField); nameFieldExt.setAttribute("autocomplete", "off"); - nameFieldExt.setAttribute("aria-label", getTranslation(I18n.Category.CATEGORY)); + nameFieldExt.setAttribute("aria-label", + getTranslation(I18n.Category.CATEGORY)); nameFieldExt.removeAttribute("aria-labelledby"); nameField.setId(String.format("name-%s", category.getId())); nameField.setValueChangeMode(ValueChangeMode.LAZY); diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/UserForm.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/UserForm.java index 245c5d7..bbb1703 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/UserForm.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/admin/UserForm.java @@ -36,6 +36,7 @@ public class UserForm extends Composite implements HasI18N { public UserForm() { form.addStyleName(VaadinCreateTheme.ADMINVIEW_USERFORM); + AttributeExtension.of(form).setAttribute("role", "form"); username = new TextField(getTranslation(I18n.User.USERNAME)); username.setId("user-field"); var userNameExt = AttributeExtension.of(username); diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java index d827dfc..a9dfde4 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksPresenter.java @@ -368,6 +368,10 @@ public void rowSelected(Product product) { } } + public void selectProduct(Product product) { + view.handleSelectionChange(product); + } + /** * Saves the given product draft. * diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksView.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksView.java index 2c4347b..7826ed7 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksView.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/BooksView.java @@ -18,6 +18,7 @@ import org.vaadin.tatu.vaadincreate.i18n.I18n; import org.vaadin.tatu.vaadincreate.util.Utils; +import com.vaadin.data.HasValue.ValueChangeEvent; import com.vaadin.data.provider.ListDataProvider; import com.vaadin.icons.VaadinIcons; import com.vaadin.navigator.View; @@ -74,19 +75,7 @@ public BooksView() { grid = new BookGrid(); grid.asSingleSelect().addValueChangeListener(event -> { if (event.isUserOriginated()) { - if (form.hasChanges()) { - var dialog = createDiscardChangesConfirmDialog(); - dialog.open(); - dialog.addConfirmedListener(e -> { - presenter.unlockBook(); - form.showForm(false); - setFragmentParameter(""); - }); - dialog.addCancelListener( - e -> grid.select(form.getProduct())); - } else { - presenter.rowSelected(event.getValue()); - } + handleSelectionChange(event.getValue()); } }); grid.setVisible(false); @@ -94,7 +83,7 @@ public BooksView() { // Display fake Grid while loading data fakeGrid = new FakeGrid(); - form = new BookForm(presenter); + form = new BookForm(presenter, grid); var barAndGridLayout = new VerticalLayout(); var gridWrapper = new CssLayout(); @@ -112,6 +101,24 @@ public BooksView() { presenter.init(); } + public void handleSelectionChange(Product product) { + if (form.hasChanges()) { + var dialog = createDiscardChangesConfirmDialog(); + dialog.open(); + dialog.addConfirmedListener(e -> { + presenter.unlockBook(); + form.showForm(false); + setFragmentParameter(""); + }); + dialog.addCancelListener(e -> grid.select(form.getProduct())); + } else { + presenter.rowSelected(product); + if (product != null) { + grid.select(product); + } + } + } + // Filter the grid data based on the filter text private static boolean filterCondition(T value, String filterText) { assert filterText != null : "Filter text cannot be null"; diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/BookForm.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/BookForm.java index 2adcca3..a4704b7 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/BookForm.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/BookForm.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; import java.util.Collection; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +16,7 @@ import org.vaadin.tatu.vaadincreate.backend.data.Availability; import org.vaadin.tatu.vaadincreate.backend.data.Category; import org.vaadin.tatu.vaadincreate.backend.data.Product; +import org.vaadin.tatu.vaadincreate.crud.BookGrid; import org.vaadin.tatu.vaadincreate.crud.BooksPresenter; import org.vaadin.tatu.vaadincreate.crud.EuroConverter; import org.vaadin.tatu.vaadincreate.i18n.HasI18N; @@ -25,6 +27,7 @@ import com.vaadin.data.Binder; import com.vaadin.data.provider.ListDataProvider; import com.vaadin.event.ShortcutAction.KeyCode; +import com.vaadin.event.ShortcutListener; import com.vaadin.server.AbstractErrorMessage; import com.vaadin.server.Page; import com.vaadin.server.UserError; @@ -126,14 +129,16 @@ public class BookForm extends Composite implements HasI18N { private BooksPresenter presenter; private boolean visible; private boolean isValid; + private AttributeExtension attributes; /** * Creates a new BookForm with the given presenter. * * @param presenter * the presenter for the form. + * @param grid */ - public BookForm(BooksPresenter presenter) { + public BookForm(BooksPresenter presenter, BookGrid grid) { this.presenter = presenter; setCompositionRoot(sidePanel); buildForm(); @@ -183,6 +188,20 @@ public BookForm(BooksPresenter presenter) { cancelButton.setClickShortcut(KeyCode.ESCAPE); deleteButton.addClickListener(event -> handleDelete()); + addShortcutListener( + new ShortcutListener("Next", KeyCode.PAGE_DOWN, null) { + @Override + public void handleAction(Object sender, Object target) { + selectNextProduct(presenter, grid); + } + }); + addShortcutListener( + new ShortcutListener("Previous", KeyCode.PAGE_UP, null) { + @Override + public void handleAction(Object sender, Object target) { + selectPreviousProduct(presenter, grid); + } + }); } private void handleSave() { @@ -322,6 +341,7 @@ private void buildForm() { productName.setId("product-name"); productName.setWidthFull(); productName.setMaxLength(100); + AttributeExtension.of(productName).setAttribute("autocomplete", "off"); CharacterCountExtension.extend(productName); // Layout price and stockCount horizontally @@ -358,10 +378,12 @@ private void buildForm() { formLayout.setExpandRatio(spacer, 1); // Set ARIA attributes for the form to make it accessible - var attributes = AttributeExtension.of(formLayout); + attributes = AttributeExtension.of(formLayout); attributes.setAttribute("aria-label", getTranslation(I18n.Books.FORM_OPENED)); - attributes.setAttribute("role", "alert"); + attributes.setAttribute("role", "form"); + attributes.setAttribute("aria-keyshortcuts", "Escape PageDown PageUp"); + attributes.setAttribute("aria-live", "assertive"); sidePanel.setContent(formLayout); } @@ -440,5 +462,33 @@ public void focus() { productName.focus(); } + private static List getVisibleItems(BookGrid grid) { + return grid.getDataCommunicator().fetchItemsWithRange(0, + grid.getDataCommunicator().getDataProviderSize()); + } + + private void selectPreviousProduct(BooksPresenter presenter, + BookGrid grid) { + if (getProduct().getId() == null) { + return; + } + var items = getVisibleItems(grid); + var current = items.indexOf(getProduct()); + if (current > 0) { + presenter.selectProduct(items.get(current - 1)); + } + } + + private void selectNextProduct(BooksPresenter presenter, BookGrid grid) { + if (getProduct().getId() == null) { + return; + } + var items = getVisibleItems(grid); + var current = items.indexOf(getProduct()); + if (current < items.size() - 1) { + presenter.selectProduct(items.get(current + 1)); + } + } + private static Logger logger = LoggerFactory.getLogger(BookForm.class); } diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/SidePanel.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/SidePanel.java index 55f643e..efcc8bd 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/SidePanel.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/crud/form/SidePanel.java @@ -1,5 +1,6 @@ package org.vaadin.tatu.vaadincreate.crud.form; +import org.vaadin.tatu.vaadincreate.AttributeExtension; import org.vaadin.tatu.vaadincreate.VaadinCreateTheme; import com.vaadin.ui.Component; @@ -25,6 +26,7 @@ public SidePanel() { layout.setId("book-form"); layout.addStyleNames(VaadinCreateTheme.BOOKFORM, VaadinCreateTheme.BOOKFORM_WRAPPER); + AttributeExtension.of(layout).setAttribute("role", "dialog"); setCompositionRoot(layout); } diff --git a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/stats/StatsView.java b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/stats/StatsView.java index 98d0dbf..635b9a8 100644 --- a/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/stats/StatsView.java +++ b/vaadincreate-ui/src/main/java/org/vaadin/tatu/vaadincreate/stats/StatsView.java @@ -1,9 +1,11 @@ package org.vaadin.tatu.vaadincreate.stats; import java.util.Map; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.vaadin.tatu.vaadincreate.AttributeExtension; import org.vaadin.tatu.vaadincreate.VaadinCreateTheme; import org.vaadin.tatu.vaadincreate.auth.RolesPermitted; import org.vaadin.tatu.vaadincreate.backend.data.Availability; @@ -50,6 +52,10 @@ public class StatsView extends VerticalLayout implements View, HasI18N { private UI ui; + private AttributeExtension availabilityChartAttributes; + private AttributeExtension priceChartAttributes; + private AttributeExtension categoryChartAttributes; + public StatsView() { addStyleNames(VaadinCreateTheme.STATSVIEW, ValoTheme.SCROLLABLE); dashboard = new CssLayout(); @@ -78,6 +84,7 @@ private CssLayout createPriceChart() { var priceChartWrapper = new CssLayout(); priceChart = new Chart(ChartType.PIE); priceChart.setId("price-chart"); + priceChartAttributes = AttributeExtension.of(priceChart); priceChartWrapper.addStyleName(VaadinCreateTheme.DASHBOARD_CHART); var conf = priceChart.getConfiguration(); conf.setTitle(getTranslation(I18n.Stats.PRICES)); @@ -93,6 +100,7 @@ private CssLayout createCategoryChart() { .addStyleName(VaadinCreateTheme.DASHBOARD_CHART_WIDE); categoryChart = new Chart(ChartType.COLUMN); categoryChart.setId("category-chart"); + categoryChartAttributes = AttributeExtension.of(categoryChart); var conf = categoryChart.getConfiguration(); conf.setTitle(getTranslation(I18n.CATEGORIES)); conf.setLang(lang); @@ -105,6 +113,7 @@ private CssLayout createAvailabilityChart() { var availabilityChartWrapper = new CssLayout(); availabilityChart = new Chart(ChartType.COLUMN); availabilityChart.setId("availability-chart"); + availabilityChartAttributes = AttributeExtension.of(availabilityChart); availabilityChartWrapper .addStyleName(VaadinCreateTheme.DASHBOARD_CHART); var conf = availabilityChart.getConfiguration(); @@ -159,6 +168,14 @@ private void updatePriceChart(Map priceStats) { priceSeries.setName(getTranslation(I18n.Stats.COUNT)); var conf = priceChart.getConfiguration(); conf.setSeries(priceSeries); + + priceChartAttributes.setAttribute("role", "figure"); + priceChartAttributes.setAttribute("tabindex", "0"); + var alt = getTranslation(I18n.Stats.PRICES) + ":" + + priceSeries.getData().stream() + .map(data -> data.getName() + " " + data.getY()) + .collect(Collectors.joining(",")); + priceChartAttributes.setAttribute("aria-label", alt); } // Update the charts with the new data @@ -184,6 +201,20 @@ private void updateCategoryChart(Map categoryStats) { conf.addSeries(stockCounts); stockAxis.setTitle(getTranslation(I18n.IN_STOCK)); categoryStats.keySet().forEach(cat -> conf.getxAxis().addCategory(cat)); + + categoryChartAttributes.setAttribute("role", "figure"); + categoryChartAttributes.setAttribute("tabindex", "0"); + var alt1 = getTranslation(I18n.Stats.CATEGORIES) + " " + + getTranslation(I18n.Stats.COUNT) + ":" + + titles.getData().stream() + .map(data -> data.getName() + " " + data.getY()) + .collect(Collectors.joining(",")); + var alt2 = getTranslation(I18n.Stats.CATEGORIES) + " " + + getTranslation(I18n.IN_STOCK) + ":" + + stockCounts.getData().stream() + .map(data -> data.getName() + " " + data.getY()) + .collect(Collectors.joining(",")); + categoryChartAttributes.setAttribute("aria-label", alt1 + " " + alt2); } // Update the charts with the new data @@ -200,6 +231,14 @@ private void updateAvailabilityChart( .map(item -> item.getName()).toArray(String[]::new); var axis = conf.getxAxis(); axis.setCategories(categories); + + availabilityChartAttributes.setAttribute("role", "figure"); + availabilityChartAttributes.setAttribute("tabindex", "0"); + var alt = getTranslation(I18n.Stats.AVAILABILITIES) + ":" + + availabilitySeries.getData().stream() + .map(data -> data.getName() + " " + data.getY()) + .collect(Collectors.joining(",")); + availabilityChartAttributes.setAttribute("aria-label", alt); } private DataSeries categorySeries(Map categories,