From 12dbc730bf025ab8886643d5e76e6356e34f7f0a Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Fri, 20 Dec 2024 12:33:16 +0200 Subject: [PATCH] feat: add virtual list item accessible name generator --- .../virtuallist/tests/VirtualListPage.java | 10 ++++- .../virtuallist/tests/VirtualListIT.java | 21 ++++++++++ .../component/virtuallist/VirtualList.java | 40 +++++++++++++++++++ .../frontend/virtualListConnector.js | 2 + .../virtuallist/tests/VirtualListTest.java | 17 ++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListPage.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListPage.java index 0234fee8c1c..5bf26333613 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListPage.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/main/java/com/vaadin/flow/component/virtuallist/tests/VirtualListPage.java @@ -107,13 +107,21 @@ private void createListWithStrings() { evt -> list.setItems("Another item 1", "Another item 2")); NativeButton setEmptyList = new NativeButton("Change list 3", evt -> list.setItems()); + NativeButton setItemAccessibleNameGenerator = new NativeButton( + "Set item accessible name generator", + evt -> list.setItemAccessibleNameGenerator( + item -> item.contains("2") ? null + : "Accessible " + item)); list.setId("list-with-strings"); setListWith3Items.setId("list-with-strings-3-items"); setListWith2Items.setId("list-with-strings-2-items"); setEmptyList.setId("list-with-strings-0-items"); + setItemAccessibleNameGenerator + .setId("list-with-strings-accessible-name"); - add(list, setListWith3Items, setListWith2Items, setEmptyList); + add(list, setListWith3Items, setListWith2Items, setEmptyList, + setItemAccessibleNameGenerator); } private void createDataProviderWithStrings() { diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListIT.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListIT.java index 7aeb1fb11cb..eeb9033b463 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListIT.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow-integration-tests/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListIT.java @@ -65,6 +65,27 @@ public void listWithStrings() { clickToSet0Items_listIsUpdated(listId, "list-with-strings-0-items"); } + @Test + public void accessibleName() { + String listId = "list-with-strings"; + + var virtualList = $(VirtualListElement.class).id(listId); + + var firstChildElement = virtualList + .findElement(By.xpath("//*[text()='Item 1']")); + + Assert.assertFalse(firstChildElement.hasAttribute("aria-label")); + + clickElementWithJs("list-with-strings-accessible-name"); + + Assert.assertEquals("Accessible Item 1", + firstChildElement.getAttribute("aria-label")); + + var secondChildElement = virtualList + .findElement(By.xpath("//*[text()='Item 2']")); + Assert.assertFalse(secondChildElement.hasAttribute("aria-label")); + } + @Test public void dataProviderWithStrings() { String listId = "dataprovider-with-strings"; diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java index 131204ebc7f..dbef4bdaf8a 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/java/com/vaadin/flow/component/virtuallist/VirtualList.java @@ -40,12 +40,14 @@ import com.vaadin.flow.data.renderer.LitRenderer; import com.vaadin.flow.data.renderer.Renderer; import com.vaadin.flow.dom.DisabledUpdateMode; +import com.vaadin.flow.function.SerializableFunction; import com.vaadin.flow.function.ValueProvider; import com.vaadin.flow.internal.JsonUtils; import com.vaadin.flow.server.Command; import com.vaadin.flow.shared.Registration; import elemental.json.Json; +import elemental.json.JsonObject; import elemental.json.JsonValue; /** @@ -119,6 +121,8 @@ public void initialize() { private Renderer renderer; + private SerializableFunction itemAccessibleNameGenerator = item -> null; + private final CompositeDataGenerator dataGenerator = new CompositeDataGenerator<>(); private final List renderingRegistrations = new ArrayList<>(); private transient T placeholderItem; @@ -134,6 +138,7 @@ public void initialize() { public VirtualList() { setRenderer((ValueProvider) String::valueOf); addAttachListener((e) -> this.setPlaceholderItem(this.placeholderItem)); + dataGenerator.addDataGenerator(this::generateItemAccessibleName); } private void initConnector() { @@ -144,6 +149,13 @@ private void initConnector() { getElement()); } + private void generateItemAccessibleName(T item, JsonObject jsonObject) { + var accessibleName = this.itemAccessibleNameGenerator.apply(item); + if (accessibleName != null) { + jsonObject.put("accessibleName", accessibleName); + } + } + @Override public void setDataProvider(DataProvider dataProvider) { Objects.requireNonNull(dataProvider, "The dataProvider cannot be null"); @@ -324,4 +336,32 @@ public void scrollToStart() { public void scrollToEnd() { scrollToIndex(Integer.MAX_VALUE); } + + /** + * A function that generates accessible names for virtual list items. The + * function gets the item as an argument and the return value should be a + * string representing that item. The result gets applied to the + * corresponding virtual list child element as an `aria-label` attribute. + * + * @param itemAccessibleNameGenerator + * the item accessible name generator to set, not {@code null} + * @throws NullPointerException + * if {@code itemAccessibleNameGenerator} is {@code null} + */ + public void setItemAccessibleNameGenerator( + SerializableFunction itemAccessibleNameGenerator) { + Objects.requireNonNull(itemAccessibleNameGenerator, + "Item accessible name generator can not be null"); + this.itemAccessibleNameGenerator = itemAccessibleNameGenerator; + getDataCommunicator().reset(); + } + + /** + * Gets the function that generates accessible names for virtual list items. + * + * @return the item accessible name generator + */ + public SerializableFunction getItemAccessibleNameGenerator() { + return itemAccessibleNameGenerator; + } } diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js index 9089923d397..2982593bc10 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/main/resources/META-INF/resources/frontend/virtualListConnector.js @@ -15,6 +15,8 @@ window.Vaadin.Flow.virtualListConnector = { list.$connector = {}; list.$connector.placeholderItem = { __placeholder: true }; + list.itemAccessibleNameGenerator = (item) => item && item.accessibleName; + const updateRequestedItem = function () { /* * TODO virtual list seems to do a small index adjustment after scrolling diff --git a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTest.java b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTest.java index 5fdf38fa9f5..eedf30a5d8e 100644 --- a/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTest.java +++ b/vaadin-virtual-list-flow-parent/vaadin-virtual-list-flow/src/test/java/com/vaadin/flow/component/virtuallist/tests/VirtualListTest.java @@ -21,6 +21,7 @@ import org.junit.rules.ExpectedException; import com.vaadin.flow.component.virtuallist.VirtualList; +import com.vaadin.flow.function.SerializableFunction; public class VirtualListTest { @@ -49,4 +50,20 @@ public void paging_setPagingEnabled_throws() { exceptionRule.expectMessage("VirtualList does not support paging"); virtualList.getDataCommunicator().setPagingEnabled(true); } + + @Test + public void setItemAccessibleNameGenerator_get() { + VirtualList virtualList = new VirtualList<>(); + SerializableFunction itemAccessibleNameGenerator = item -> "Accessible " + + item; + virtualList.setItemAccessibleNameGenerator(itemAccessibleNameGenerator); + Assert.assertEquals(itemAccessibleNameGenerator, + virtualList.getItemAccessibleNameGenerator()); + } + + @Test(expected = NullPointerException.class) + public void setItemAccessibleNameGenerator_nullThrows() { + VirtualList virtualList = new VirtualList<>(); + virtualList.setItemAccessibleNameGenerator(null); + } }