From 40db0585bf687c290f26336f8373d2eed59e196f Mon Sep 17 00:00:00 2001 From: Serhii Kulykov Date: Mon, 17 Jun 2024 13:52:09 +0300 Subject: [PATCH] feat: add itemClassNameGenerator to generate CSS class names (#6177) --- .../test/ComboBoxItemClassNamePage.java | 44 +++++++ .../MultiSelectComboBoxItemClassNamePage.java | 52 ++++++++ .../test/ComboBoxItemClassNameIT.java | 77 +++++++++++ .../MultiSelectComboBoxItemClassNameIT.java | 122 ++++++++++++++++++ .../flow/component/combobox/ComboBoxBase.java | 54 ++++++++ .../resources/frontend/comboBoxConnector.js | 4 + 6 files changed, 353 insertions(+) create mode 100644 vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNamePage.java create mode 100644 vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNamePage.java create mode 100644 vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNameIT.java create mode 100644 vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNameIT.java diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNamePage.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNamePage.java new file mode 100644 index 00000000000..aefe09152da --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNamePage.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2024 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.flow.component.combobox.test; + +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; + +@Route("vaadin-combo-box/item-class-name") +public class ComboBoxItemClassNamePage extends Div { + + public ComboBoxItemClassNamePage() { + ComboBox comboBox = new ComboBox<>(); + comboBox.setItems("foo", "bar", "baz"); + + NativeButton setClassNameGenerator = new NativeButton( + "Set class name generator", e -> { + comboBox.setClassNameGenerator(item -> "item-" + item); + }); + setClassNameGenerator.setId("set-generator"); + + NativeButton resetClassNameGenerator = new NativeButton( + "Reset class name generator", e -> { + comboBox.setClassNameGenerator(item -> null); + }); + resetClassNameGenerator.setId("reset-generator"); + + add(comboBox, setClassNameGenerator, resetClassNameGenerator); + } +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNamePage.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNamePage.java new file mode 100644 index 00000000000..a37ba8f888e --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/main/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNamePage.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2024 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.flow.component.combobox.test; + +import com.vaadin.flow.component.combobox.MultiSelectComboBox; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; + +@Route("vaadin-multi-select-combo-box/item-class-name") +public class MultiSelectComboBoxItemClassNamePage extends Div { + + public MultiSelectComboBoxItemClassNamePage() { + MultiSelectComboBox comboBox = new MultiSelectComboBox<>(); + comboBox.setItems("foo", "bar", "baz"); + + // Make component wider, so that we can fit multiple chips + comboBox.setWidth("300px"); + + NativeButton setClassNameGenerator = new NativeButton( + "Set class name generator", e -> { + comboBox.setClassNameGenerator(item -> "item-" + item); + }); + setClassNameGenerator.setId("set-generator"); + + NativeButton resetClassNameGenerator = new NativeButton( + "Reset class name generator", e -> { + comboBox.setClassNameGenerator(item -> null); + }); + resetClassNameGenerator.setId("reset-generator"); + + NativeButton setValue = new NativeButton("Set value", e -> { + comboBox.select("foo", "bar"); + }); + setValue.setId("set-value"); + + add(comboBox, setClassNameGenerator, resetClassNameGenerator, setValue); + } +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNameIT.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNameIT.java new file mode 100644 index 00000000000..90839425ef4 --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/ComboBoxItemClassNameIT.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2024 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.flow.component.combobox.test; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.combobox.testbench.ComboBoxElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.tests.AbstractComponentIT; +import com.vaadin.testbench.ElementQuery; +import com.vaadin.testbench.TestBenchElement; + +@TestPath("vaadin-combo-box/item-class-name") +public class ComboBoxItemClassNameIT extends AbstractComponentIT { + + private ComboBoxElement comboBox; + + @Before + public void init() { + open(); + comboBox = $(ComboBoxElement.class).first(); + } + + @Test + public void noClassesOnItemsSetInitially() { + comboBox.openPopup(); + + assertItemClassNames("", "", ""); + } + + @Test + public void setClassNameGenerator_classesGenerated() { + click("set-generator"); + comboBox.openPopup(); + + assertItemClassNames("item-foo", "item-bar", "item-baz"); + } + + @Test + public void changeClassNameGeneratorToReturnNull_classesRemoved() { + click("set-generator"); + click("reset-generator"); + comboBox.openPopup(); + + assertItemClassNames("", "", ""); + } + + private void assertItemClassNames(String... expectedClassNames) { + TestBenchElement overlay = $("vaadin-combo-box-overlay").first(); + ElementQuery items = overlay + .$("vaadin-combo-box-item"); + + for (int i = 0; i < expectedClassNames.length; i++) { + Assert.assertEquals(items.get(i).getAttribute("class"), + expectedClassNames[i]); + } + } + + private void click(String id) { + $(TestBenchElement.class).id(id).click(); + } +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNameIT.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNameIT.java new file mode 100644 index 00000000000..6fa46030fd6 --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow-integration-tests/src/test/java/com/vaadin/flow/component/combobox/test/MultiSelectComboBoxItemClassNameIT.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2024 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.flow.component.combobox.test; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.combobox.testbench.MultiSelectComboBoxElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.tests.AbstractComponentIT; +import com.vaadin.testbench.ElementQuery; +import com.vaadin.testbench.TestBenchElement; + +@TestPath("vaadin-multi-select-combo-box/item-class-name") +public class MultiSelectComboBoxItemClassNameIT extends AbstractComponentIT { + + private MultiSelectComboBoxElement comboBox; + + @Before + public void init() { + open(); + comboBox = $(MultiSelectComboBoxElement.class).first(); + } + + @Test + public void noClassesOnItemsSetInitially() { + comboBox.openPopup(); + + assertItemClassNames("", "", ""); + } + + @Test + public void setClassNameGenerator_itemClassesGenerated() { + click("set-generator"); + comboBox.openPopup(); + + assertItemClassNames("item-foo", "item-bar", "item-baz"); + } + + @Test + public void changeClassNameGeneratorToReturnNull_itemClassesRemoved() { + click("set-generator"); + click("reset-generator"); + comboBox.openPopup(); + + assertItemClassNames("", "", ""); + } + + @Test + public void setValue_noClassesOnChipsSetInitially() { + click("set-value"); + + assertChipClassNames("", ""); + } + + @Test + public void setClassNameGenerator_setValue_chipClassesGenerated() { + click("set-generator"); + click("set-value"); + + assertChipClassNames("item-foo", "item-bar"); + } + + @Test + public void setValue_setClassNameGenerator_chipClassesGenerated() { + click("set-value"); + click("set-generator"); + + assertChipClassNames("item-foo", "item-bar"); + } + + @Test + public void changeClassNameGeneratorToReturnNull_chipClassesRemoved() { + click("set-generator"); + click("set-value"); + + click("reset-generator"); + + assertChipClassNames("", ""); + } + + private void assertItemClassNames(String... expectedClassNames) { + TestBenchElement overlay = $("vaadin-multi-select-combo-box-overlay") + .first(); + ElementQuery items = overlay + .$("vaadin-multi-select-combo-box-item"); + + for (int i = 0; i < expectedClassNames.length; i++) { + Assert.assertEquals(items.get(i).getAttribute("class"), + expectedClassNames[i]); + } + } + + private void assertChipClassNames(String... expectedClassNames) { + ElementQuery chips = comboBox + .$("vaadin-multi-select-combo-box-chip"); + + for (int i = 0; i < expectedClassNames.length; i++) { + // Skip first chip as it's used for overflow items + Assert.assertEquals(chips.get(i + 1).getAttribute("class"), + expectedClassNames[i]); + } + } + + private void click(String id) { + $(TestBenchElement.class).id(id).click(); + } +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java index 11924d505b2..99cb9212725 100644 --- a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java @@ -118,6 +118,7 @@ public void remove() { } private ItemLabelGenerator itemLabelGenerator = String::valueOf; + private SerializableFunction classNameGenerator = item -> null; private final ComboBoxRenderManager renderManager; private final ComboBoxDataController dataController; private int customValueListenersCount; @@ -165,6 +166,9 @@ public Locale get() { dataController = new ComboBoxDataController<>(this, localeSupplier); dataController.getDataGenerator().addDataGenerator((item, jsonObject) -> jsonObject.put("label", generateLabel(item))); + dataController.getDataGenerator() + .addDataGenerator((item, jsonObject) -> jsonObject + .put("className", generateClassName(item))); // Configure web component to use key property from the generated // wrapper items for identification @@ -427,6 +431,56 @@ protected String generateLabel(TItem item) { return label; } + /** + * Sets the function that is used for generating CSS class names for the + * dropdown items in the ComboBox. Returning {@code null} from the generator + * results in no custom class name being set. Multiple class names can be + * returned from the generator as space-separated. + * + * @since 24.5 + * @param classNameGenerator + * the class name generator to set, not {@code null} + * @throws NullPointerException + * if {@code classNameGenerator} is {@code null} + */ + public void setClassNameGenerator( + SerializableFunction classNameGenerator) { + Objects.requireNonNull(classNameGenerator, + "Class name generator can not be null"); + this.classNameGenerator = classNameGenerator; + dataController.reset(); + } + + /** + * Gets the item class name generator that is used for generating CSS class + * names for the dropdown items in the ComboBox. + * + * @since 24.5 + * @return the item class name generator, not null + */ + public SerializableFunction getItemClassNameGenerator() { + return classNameGenerator; + } + + /** + * Generates a string class name for a data item using the current item + * class name generator + * + * @param item + * the data item + * @return string class name for the data item + */ + protected String generateClassName(TItem item) { + if (item == null) { + return ""; + } + String label = getItemClassNameGenerator().apply(item); + if (label == null) { + return ""; + } + return label; + } + /** * Sets the Renderer responsible to render the individual items in the list * of possible choices of the ComboBox. It doesn't affect how the selected diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/resources/META-INF/resources/frontend/comboBoxConnector.js b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/resources/META-INF/resources/frontend/comboBoxConnector.js index 1168c0fbeb9..b547447585f 100644 --- a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/resources/META-INF/resources/frontend/comboBoxConnector.js +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/resources/META-INF/resources/frontend/comboBoxConnector.js @@ -277,6 +277,10 @@ import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-plac 'custom-value-set', tryCatchWrapper((e) => e.preventDefault()) ); + + comboBox.itemClassNameGenerator = tryCatchWrapper(function (item) { + return item.className || ''; + }); })(comboBox) }; })();