Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.shared.SlotUtils;
import com.vaadin.flow.data.provider.HasListDataView;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementConstants;
import io.jmix.flowui.kit.action.Action;
import io.jmix.flowui.kit.component.loginform.EnhancedLoginForm;
Expand Down Expand Up @@ -133,11 +134,42 @@ public static Icon copyIconComponent(Icon icon) {
private static void copyAbstractIconAttributes(AbstractIcon<?> icon, AbstractIcon<?> iconCopy) {
iconCopy.setColor(icon.getColor());
iconCopy.setSize(icon.getStyle().get(ElementConstants.STYLE_WIDTH));
iconCopy.setTooltipText(icon.getTooltip().getText());
copyTooltip(icon, iconCopy);
iconCopy.setVisible(icon.isVisible());
iconCopy.addClassNames(icon.getClassNames().toArray(new String[0]));
}

/**
* Copies the tooltip content (plain text or Markdown) from the source icon to its copy.
* <p>
* Reads the tooltip directly from the source's slot instead of calling AbstractIcon#getTooltip(),
* which lazily creates and attaches a tooltip element to the source on first access. The source icon
* may be shared between UIs (e.g. an icon from menu configuration), so mutating its state node from
* several threads concurrently would cause a ConcurrentModificationException.
*
* @param icon source icon
* @param iconCopy copy of the source icon
*/
private static void copyTooltip(AbstractIcon<?> icon, AbstractIcon<?> iconCopy) {
Element tooltipElement = SlotUtils.getElementsInSlot(icon, "tooltip")
.findFirst()
.orElse(null);
if (tooltipElement == null) {
return;
}

String text = tooltipElement.getProperty("text");
if (text == null) {
return;
}

if (tooltipElement.getProperty("markdown", false)) {
iconCopy.setTooltipMarkdown(text);
} else {
iconCopy.setTooltipText(text);
}
}

/**
* Creates a copy of svg icon component.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@

package component_utils

import com.vaadin.flow.component.icon.Icon
import com.vaadin.flow.component.icon.VaadinIcon
import com.vaadin.flow.component.shared.SlotUtils
import io.jmix.flowui.kit.component.ComponentUtils
import spock.lang.Specification

import java.util.concurrent.CountDownLatch
import java.util.concurrent.CyclicBarrier
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference

class ComponentUtilsTest extends Specification {

def "Check isAutoSize()"(String size, boolean expected) {
Expand All @@ -38,4 +48,85 @@ class ComponentUtilsTest extends Specification {
"-12em" | false
"-1px" | false
}

def "copyIcon does not create a tooltip on the source icon"() {
given: "A cold icon whose tooltip has not been created yet"
Icon source = VaadinIcon.HOME.create()

when: "The icon is copied"
ComponentUtils.copyIcon(source)

then: "The source icon is left untouched, no tooltip element is attached to it"
SlotUtils.getElementsInSlot(source, "tooltip").findAny().isEmpty()
}

def "copyIcon preserves the plain text tooltip of the source icon"() {
given: "An icon with a plain text tooltip"
Icon source = VaadinIcon.HOME.create()
source.setTooltipText("My tooltip")

when: "The icon is copied"
Icon copy = (Icon) ComponentUtils.copyIcon(source)

then: "The copy has the same content and is not treated as Markdown"
def tooltip = SlotUtils.getElementsInSlot(copy, "tooltip").findFirst().orElse(null)
tooltip != null
tooltip.getProperty("text") == "My tooltip"
!tooltip.getProperty("markdown", false)
}

def "copyIcon preserves the Markdown tooltip of the source icon"() {
given: "An icon with a Markdown tooltip"
Icon source = VaadinIcon.HOME.create()
source.setTooltipMarkdown("**bold**")

when: "The icon is copied"
Icon copy = (Icon) ComponentUtils.copyIcon(source)

then: "The copy keeps the content and the Markdown flag"
def tooltip = SlotUtils.getElementsInSlot(copy, "tooltip").findFirst().orElse(null)
tooltip != null
tooltip.getProperty("text") == "**bold**"
tooltip.getProperty("markdown", false)
}

def "copyIcon does not mutate the shared source icon under concurrency"() {
given: "A thread pool that repeatedly copies a freshly created shared icon"
int threads = 16
int rounds = 500
ExecutorService executor = Executors.newFixedThreadPool(threads)
AtomicReference<Throwable> failure = new AtomicReference<>()

when: "All threads copy the same cold icon simultaneously in each round"
try {
for (int round = 0; round < rounds && failure.get() == null; round++) {
Icon shared = VaadinIcon.HOME.create()
CyclicBarrier barrier = new CyclicBarrier(threads)
CountDownLatch done = new CountDownLatch(threads)
threads.times {
executor.execute {
try {
barrier.await()
ComponentUtils.copyIcon(shared)
} catch (Throwable ex) {
failure.compareAndSet(null, ex)
} finally {
done.countDown()
}
}
}
done.await(30, TimeUnit.SECONDS)
}
} finally {
executor.shutdownNow()
}

Throwable t = failure.get()
if (t != null) {
throw t
}

then: "Copying never mutates the shared icon, so no exception is thrown"
noExceptionThrown()
}
}