diff --git a/core/src/main/java/org/linkki/core/uicreation/ComponentAnnotationReader.java b/core/src/main/java/org/linkki/core/uicreation/ComponentAnnotationReader.java index b7b870f7f..c174fbd39 100644 --- a/core/src/main/java/org/linkki/core/uicreation/ComponentAnnotationReader.java +++ b/core/src/main/java/org/linkki/core/uicreation/ComponentAnnotationReader.java @@ -133,6 +133,11 @@ public static Optional findComponentDefinition(Annota .map(annotation -> getComponentDefinition(annotation, annotatedElement)); } + public static List getComponentDefinitionAnnotations(AnnotatedElement annotatedElement) { + return LINKKI_COMPONENT_ANNOTATION + .findAnnotatedAnnotationsOn(annotatedElement).toList(); + } + /** * Finds the annotation of the {@link AnnotatedElement} that defines a * {@link LinkkiComponentDefinition}. diff --git a/core/src/main/java/org/linkki/core/uicreation/UiCreator.java b/core/src/main/java/org/linkki/core/uicreation/UiCreator.java index a3db7f6d3..b282ef508 100644 --- a/core/src/main/java/org/linkki/core/uicreation/UiCreator.java +++ b/core/src/main/java/org/linkki/core/uicreation/UiCreator.java @@ -309,7 +309,7 @@ private static W createComponent( return componentWrapper; } - private static String getComponentId(BoundProperty boundProperty, Object pmo) { + public static String getComponentId(BoundProperty boundProperty, Object pmo) { return boundProperty.getPmoProperty().isEmpty() ? Sections.getSectionId(pmo) : boundProperty.getPmoProperty(); diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/HelperTextAspectDefinition.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/HelperTextAspectDefinition.java new file mode 100644 index 000000000..23574f5bd --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/HelperTextAspectDefinition.java @@ -0,0 +1,120 @@ +package org.linkki.core.ui.aspects; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.aspect.Aspect; +import org.linkki.core.binding.descriptor.aspect.base.ModelToUiAspectDefinition; +import org.linkki.core.binding.wrapper.ComponentWrapper; +import org.linkki.core.ui.aspects.annotation.BindHelperText; +import org.linkki.core.vaadin.component.base.LinkkiText; + +import com.vaadin.flow.component.HasHelper; +import com.vaadin.flow.component.HasTheme; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.combobox.ComboBoxVariant; +import com.vaadin.flow.component.combobox.MultiSelectComboBox; +import com.vaadin.flow.component.combobox.MultiSelectComboBoxVariant; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.component.textfield.TextAreaVariant; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.component.textfield.TextFieldVariant; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public class HelperTextAspectDefinition extends ModelToUiAspectDefinition { + + private static final String NAME = "helperText"; + + private final BindHelperText annotation; + + public HelperTextAspectDefinition(BindHelperText annotation) { + this.annotation = annotation; + } + + private void setThemeVariant(@NonNull HasTheme component) { + if (ComboBox.class.isAssignableFrom(component.getClass())) { + ((ComboBox)component).addThemeVariants(ComboBoxVariant.LUMO_HELPER_ABOVE_FIELD); + } else if (MultiSelectComboBox.class.isAssignableFrom(component.getClass())) { + ((MultiSelectComboBox)component).addThemeVariants(MultiSelectComboBoxVariant.LUMO_HELPER_ABOVE_FIELD); + } else if (TextArea.class.isAssignableFrom(component.getClass())) { + ((TextArea)component).addThemeVariants(TextAreaVariant.LUMO_HELPER_ABOVE_FIELD); + } else if (TextField.class.isAssignableFrom(component.getClass())) { + ((TextField)component).addThemeVariants(TextFieldVariant.LUMO_HELPER_ABOVE_FIELD); + } else { + component.setThemeName("helper-above-field", true); + } + } + + @Override + public Consumer createComponentValueSetter(ComponentWrapper componentWrapper) { + final var helper = extractHelper(componentWrapper); + + return getStringConsumer(helper); + } + + @NonNull + private HasHelper extractHelper(ComponentWrapper componentWrapper) { + if (!(componentWrapper.getComponent() instanceof HasHelper)) { + throw new IllegalArgumentException("Component " + componentWrapper.getComponent().getClass().getSimpleName() + // + " does not implement HasHelper"); + } + + return (HasHelper)componentWrapper.getComponent(); + } + + @NonNull + private Consumer getStringConsumer(HasHelper hasHelper) { + return text -> { + if (annotation.placeAboveElement() && hasHelper instanceof HasTheme) { + setThemeVariant((HasTheme)hasHelper); + } + + if (annotation.htmlContent()) { + final var component = Optional.of(hasHelper) + .map(HasHelper::getHelperComponent) + .filter(LinkkiText.class::isInstance) + .map(LinkkiText.class::cast) + .orElseGet(() -> createHelperComponent(hasHelper)); + + setIcon(component); + setInnerHtml(text, component); + } else { + hasHelper.setHelperText(text); + } + }; + } + + private void setIcon(LinkkiText component) { + if (annotation.showIcon()) { + component.setIcon(annotation.icon()); + component.setIconPosition(annotation.iconPosition()); + } else { + component.setIcon(null); + } + } + + private void setInnerHtml(String text, LinkkiText element) { + element.setText(text, true); + } + + private LinkkiText createHelperComponent(HasHelper hasHelper) { + final var component = new LinkkiText(); + + hasHelper.setHelperComponent(component); + + return component; + } + + @Override + public Aspect createAspect() { + return switch (annotation.helperTextType()) { + case AUTO -> annotation.value() == null || StringUtils.isEmpty(annotation.value()) ? + Aspect.of(NAME) : + Aspect.of(NAME, annotation.value()); + case STATIC -> Aspect.of(NAME, annotation.value()); + case DYNAMIC -> Aspect.of(NAME); + }; + } +} \ No newline at end of file diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/ToggletipAspectDefinition.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/ToggletipAspectDefinition.java new file mode 100644 index 000000000..2992a4910 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/ToggletipAspectDefinition.java @@ -0,0 +1,112 @@ +package org.linkki.core.ui.aspects; + + + +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.PREFIX; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.SUFFIX; + +import java.util.Collections; +import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.aspect.Aspect; +import org.linkki.core.binding.descriptor.aspect.base.ModelToUiAspectDefinition; +import org.linkki.core.binding.wrapper.ComponentWrapper; +import org.linkki.core.ui.aspects.annotation.BindToggletip; +import org.linkki.core.ui.wrapper.VaadinComponentWrapper; +import org.linkki.core.vaadin.component.ComponentFactory; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.shared.HasPrefix; +import com.vaadin.flow.component.shared.HasSuffix; +import com.vaadin.flow.component.shared.HasTooltip; +import com.vaadin.flow.component.shared.Tooltip; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public class ToggletipAspectDefinition extends ModelToUiAspectDefinition { + + private static final String NAME = "toggletip"; + private final BindToggletip annotation; + private Tooltip tooltip; + private Button button; + + public ToggletipAspectDefinition(BindToggletip annotation) { + this.annotation = annotation; + } + + public static Tooltip extractTooltip(ComponentWrapper componentWrapper) { + if (!(componentWrapper.getComponent() instanceof HasTooltip component)) { + throw new IllegalArgumentException("Component " + componentWrapper.getComponent().getClass().getSimpleName() + // + " does not implement HasTooltip"); + } + + return component.getTooltip(); + } + + @Override + public Consumer createComponentValueSetter(ComponentWrapper componentWrapper) { + return getStringConsumer(componentWrapper); + } + + private void initializeButton(ComponentWrapper componentWrapper) { + if (button == null) { + button = createButton(componentWrapper); + tooltip = extractTooltip(componentWrapper); + + if (tooltip != null) { + tooltip// + .withPosition(annotation.tooltipPosition()) + .withManual(true); + + button.addClickListener(event -> tooltip.setOpened(!tooltip.isOpened())); + } + } + } + + private Button createButton(ComponentWrapper componentWrapper) { + if (componentWrapper instanceof VaadinComponentWrapper) { + final var component = ((VaadinComponentWrapper)componentWrapper).getComponent(); + if ((component instanceof HasPrefix && annotation.toggletipPosition() == PREFIX) || (component instanceof HasSuffix + && annotation.toggletipPosition() == SUFFIX)) { + final var newButton = ComponentFactory.newButton(annotation.icon() + .create(), Collections.emptyList()); + if (annotation.toggletipPosition() == PREFIX) { + ((HasPrefix)component).setPrefixComponent(newButton); + } else if (annotation.toggletipPosition() == SUFFIX) { + ((HasSuffix)component).setSuffixComponent(newButton); + } + + return newButton; + } else { + // + throw new IllegalArgumentException("Component %s does not implement %s or %s".formatted(component.getClass() + .getSimpleName(), HasPrefix.class.getSimpleName(), HasSuffix.class.getSimpleName())); + } + + } else { + throw new IllegalArgumentException("ComponentWrapper " + componentWrapper.getClass().getSimpleName() + // + " is not a VaadinComponentWrapper"); + } + } + + @NonNull + private Consumer getStringConsumer(ComponentWrapper componentWrapper) { + return text -> { + initializeButton(componentWrapper); + tooltip.setText(text); + }; + } + + @Override + public Aspect createAspect() { + return switch (annotation.toggletipType()) { + case AUTO -> annotation.value() == null || StringUtils.isEmpty(annotation.value()) ? + Aspect.of(NAME) : + Aspect.of(NAME, annotation.value()); + case STATIC -> Aspect.of(NAME, annotation.value()); + case DYNAMIC -> Aspect.of(NAME); + }; + } + +} \ No newline at end of file diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/TooltipAspectDefinition.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/TooltipAspectDefinition.java new file mode 100644 index 000000000..dbf705917 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/TooltipAspectDefinition.java @@ -0,0 +1,68 @@ +package org.linkki.core.ui.aspects; + +import static org.linkki.core.ui.aspects.ToggletipAspectDefinition.extractTooltip; +import static org.linkki.core.ui.aspects.annotation.BindTooltip.DEFAULT_DELAY; + +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.aspect.Aspect; +import org.linkki.core.binding.descriptor.aspect.base.ModelToUiAspectDefinition; +import org.linkki.core.binding.wrapper.ComponentWrapper; +import org.linkki.core.ui.aspects.annotation.BindTooltip; +import org.linkki.util.Consumers; + +import com.vaadin.flow.component.shared.Tooltip; + +public class TooltipAspectDefinition extends ModelToUiAspectDefinition { + + public static final String NAME = "tooltip"; + + private final BindTooltip annotation; + + public TooltipAspectDefinition(BindTooltip annotation) { + this.annotation = annotation; + } + + @SuppressWarnings("checkstyle:WhitespaceAround") + @Override + public Consumer createComponentValueSetter(ComponentWrapper componentWrapper) { + final var tooltip = extractTooltip(componentWrapper); + if (tooltip == null) { + return Consumers.nopConsumer(); + } + + return getStringConsumer(tooltip); + } + + @edu.umd.cs.findbugs.annotations.NonNull + private Consumer getStringConsumer(Tooltip tooltip) { + return label -> { + tooltip// + .withText(label) + .withPosition(annotation.position()); + + setIfPresent(tooltip::setFocusDelay, annotation.focusDelay()); + setIfPresent(tooltip::setHideDelay, annotation.hideDelay()); + setIfPresent(tooltip::setHoverDelay, annotation.hoverDelay()); + }; + } + + private void setIfPresent(IntConsumer consumer, int value) { + if (value != DEFAULT_DELAY) { + consumer.accept(value); + } + } + + @Override + public Aspect createAspect() { + return switch (annotation.tooltipType()) { + case AUTO -> annotation.value() == null || StringUtils.isEmpty(annotation.value()) ? + Aspect.of(NAME) : + Aspect.of(NAME, annotation.value()); + case STATIC -> Aspect.of(NAME, annotation.value()); + case DYNAMIC -> Aspect.of(NAME); + }; + } +} \ No newline at end of file diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindHelperText.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindHelperText.java new file mode 100644 index 000000000..72200113f --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindHelperText.java @@ -0,0 +1,67 @@ +/******************************************************* + * Copyright (c) Faktor Zehn GmbH - www.faktorzehn.de + * + * All Rights Reserved - Alle Rechte vorbehalten. + *******************************************************/ + +package org.linkki.core.ui.aspects.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.aspect.LinkkiAspectDefinition; +import org.linkki.core.binding.descriptor.aspect.annotation.AspectDefinitionCreator; +import org.linkki.core.binding.descriptor.aspect.annotation.LinkkiAspect; +import org.linkki.core.ui.aspects.HelperTextAspectDefinition; +import org.linkki.core.ui.aspects.types.HelperTextType; +import org.linkki.core.ui.aspects.types.IconPosition; +import org.linkki.core.util.HtmlSanitizer; + +import com.vaadin.flow.component.HasHelper; +import com.vaadin.flow.component.icon.VaadinIcon; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Setzt einen Helper Text an der Component. Diese muss {@link HasHelper} implementieren. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.METHOD }) +@LinkkiAspect(BindHelperText.BindHelperTextAspectDefinition.class) +public @interface BindHelperText { + + String value() default StringUtils.EMPTY; + + /** Defines how the tooltip text should be retrieved */ + HelperTextType helperTextType() default HelperTextType.AUTO; + + /** + * When set to {@code true}, the label's content will be displayed as HTML, otherwise as plain text. + * The HTML content is automatically {@link HtmlSanitizer#sanitizeText(String) sanitized}.
+ * Note that user-supplied strings have to be {@link HtmlSanitizer#escapeText(String) + * escaped} when including them in the HTML content. Otherwise, they will also be interpreted as + * HTML. + *

+ * HTML content is not compatible with some annotations that manipulate the resulting component, + * like {@link BindIcon}. + */ + boolean htmlContent() default false; + + boolean placeAboveElement() default false; + + boolean showIcon() default false; + VaadinIcon icon() default VaadinIcon.INFO_CIRCLE_O; + IconPosition iconPosition() default IconPosition.LEFT; + + class BindHelperTextAspectDefinition implements AspectDefinitionCreator { + + @Override + public LinkkiAspectDefinition create(@NonNull BindHelperText annotation) { + return new HelperTextAspectDefinition(annotation); + } + + } +} diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindToggletip.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindToggletip.java new file mode 100644 index 000000000..dd4bbb3f7 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindToggletip.java @@ -0,0 +1,64 @@ +/******************************************************* + * Copyright (c) Faktor Zehn GmbH - www.faktorzehn.de + * + * All Rights Reserved - Alle Rechte vorbehalten. + *******************************************************/ + +package org.linkki.core.ui.aspects.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.aspect.LinkkiAspectDefinition; +import org.linkki.core.binding.descriptor.aspect.annotation.AspectDefinitionCreator; +import org.linkki.core.binding.descriptor.aspect.annotation.LinkkiAspect; +import org.linkki.core.defaults.ui.aspects.annotations.BindTooltip; +import org.linkki.core.ui.aspects.ToggletipAspectDefinition; +import org.linkki.core.ui.aspects.types.ToggletipType; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.shared.HasTooltip; +import com.vaadin.flow.component.shared.Tooltip; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Setzt einen Toggletip für die {@linkplain Component Komponente}. Die Komponente muss {@link HasTooltip} implementieren. + * Im Gegensatz zu {@link BindTooltip}, wird die neue Vaadin 23 Tooltip API verwendet, statt dem Setzen des Attributes + * title. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.METHOD }) +@LinkkiAspect(BindToggletip.BindToggletipAspectDefinitionCreator.class) +public @interface BindToggletip { + + String value() default StringUtils.EMPTY; + + /** Defines how the tooltip text should be retrieved */ + ToggletipType toggletipType() default ToggletipType.AUTO; + + ToogletipPosition toggletipPosition() default ToogletipPosition.SUFFIX; + + Tooltip.TooltipPosition tooltipPosition() default Tooltip.TooltipPosition.TOP; + + VaadinIcon icon() default VaadinIcon.QUESTION_CIRCLE_O; + + enum ToogletipPosition { + PREFIX, + SUFFIX + } + + class BindToggletipAspectDefinitionCreator implements AspectDefinitionCreator { + + @Override + public LinkkiAspectDefinition create(@NonNull BindToggletip annotation) { + return new ToggletipAspectDefinition(annotation); + } + + } + +} diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindTooltip.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindTooltip.java new file mode 100644 index 000000000..0ea17ae39 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/BindTooltip.java @@ -0,0 +1,60 @@ +/******************************************************* + * Copyright (c) Faktor Zehn GmbH - www.faktorzehn.de + * + * All Rights Reserved - Alle Rechte vorbehalten. + *******************************************************/ + +package org.linkki.core.ui.aspects.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.aspect.LinkkiAspectDefinition; +import org.linkki.core.binding.descriptor.aspect.annotation.AspectDefinitionCreator; +import org.linkki.core.binding.descriptor.aspect.annotation.LinkkiAspect; +import org.linkki.core.ui.aspects.TooltipAspectDefinition; +import org.linkki.core.ui.aspects.types.TooltipType; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.shared.HasTooltip; +import com.vaadin.flow.component.shared.Tooltip; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Setzt einen Tooltip für die {@linkplain Component Komponente}. Die Komponente muss {@link HasTooltip} implementieren. + * Im Gegensatz zu {@link org.linkki.core.defaults.ui.aspects.annotations.BindTooltip}, wird die neue Vaadin 23 Tooltip API verwendet, statt dem Setzen des Attributes + * title. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.FIELD, ElementType.METHOD }) +@LinkkiAspect(BindTooltip.BindTooltipAspectDefinitionCreator.class) +public @interface BindTooltip { + + int DEFAULT_DELAY = -1; + + String value() default StringUtils.EMPTY; + + /** Defines how the tooltip text should be retrieved */ + TooltipType tooltipType() default TooltipType.AUTO; + + int focusDelay() default DEFAULT_DELAY; + int hideDelay() default DEFAULT_DELAY; + int hoverDelay() default DEFAULT_DELAY; + + Tooltip.TooltipPosition position() default Tooltip.TooltipPosition.TOP; + + + class BindTooltipAspectDefinitionCreator implements AspectDefinitionCreator { + + @Override + public LinkkiAspectDefinition create(@NonNull BindTooltip annotation) { + return new TooltipAspectDefinition(annotation); + } + + } + +} diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/UINestedToggleTip.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/UINestedToggleTip.java new file mode 100644 index 000000000..a09bce98a --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/annotation/UINestedToggleTip.java @@ -0,0 +1,263 @@ +/* + * Copyright Faktor Zehn GmbH. + * + * 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 org.linkki.core.ui.aspects.annotation; + +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.SUFFIX; +import static org.linkki.core.uicreation.UiCreator.getComponentId; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.binding.descriptor.BindingDescriptor; +import org.linkki.core.binding.descriptor.aspect.Aspect; +import org.linkki.core.binding.descriptor.aspect.LinkkiAspectDefinition; +import org.linkki.core.binding.descriptor.aspect.annotation.AspectDefinitionCreator; +import org.linkki.core.binding.descriptor.aspect.annotation.LinkkiAspect; +import org.linkki.core.binding.descriptor.aspect.base.CompositeAspectDefinition; +import org.linkki.core.binding.descriptor.aspect.base.ModelToUiAspectDefinition; +import org.linkki.core.binding.descriptor.property.annotation.BoundPropertyCreator.SimpleMemberNameBoundPropertyCreator; +import org.linkki.core.binding.descriptor.property.annotation.LinkkiBoundProperty; +import org.linkki.core.binding.uicreation.LinkkiComponent; +import org.linkki.core.binding.uicreation.LinkkiComponentDefinition; +import org.linkki.core.binding.wrapper.ComponentWrapper; +import org.linkki.core.binding.wrapper.WrapperType; +import org.linkki.core.ui.aspects.LabelAspectDefinition; +import org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition; +import org.linkki.core.ui.aspects.types.ToggletipType; +import org.linkki.core.ui.wrapper.NoLabelComponentWrapper; +import org.linkki.core.uicreation.ComponentAnnotationReader; +import org.linkki.core.uicreation.ComponentDefinitionCreator; +import org.linkki.core.uicreation.LinkkiPositioned; +import org.linkki.core.uicreation.layout.LayoutDefinitionCreator; +import org.linkki.core.uicreation.layout.LinkkiLayout; +import org.linkki.core.uicreation.layout.LinkkiLayoutDefinition; +import org.linkki.core.vaadin.component.ComponentFactory; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasSize; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.shared.HasTooltip; +import com.vaadin.flow.component.shared.Tooltip; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Embeds another PMO in the current layout. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@LinkkiLayout(UINestedToggleTip.NestedToggleTipLayoutDefinitionCreator.class) +@LinkkiComponent(UINestedToggleTip.NestedToggleTipComponentDefinitionCreator.class) +@LinkkiPositioned +@LinkkiAspect(UINestedToggleTip.NestedToggletipAspectDefinitionCreator.class) +@LinkkiBoundProperty(SimpleMemberNameBoundPropertyCreator.class) +public @interface UINestedToggleTip { + + /** + * Mandatory attribute that defines the order in which UI-Elements are displayed + */ + @LinkkiPositioned.Position int position(); + + String value() default StringUtils.EMPTY; + + /** + * Defines how the tooltip text should be retrieved + */ + ToggletipType toggletipType() default ToggletipType.AUTO; + + ToogletipPosition toggletipPosition() default SUFFIX; + + Tooltip.TooltipPosition tooltipPosition() default Tooltip.TooltipPosition.TOP; + + VaadinIcon icon() default VaadinIcon.QUESTION_CIRCLE_O; + + String label() default ""; + + class NestedToggleTipComponentDefinitionCreator implements ComponentDefinitionCreator { + @Override + public LinkkiComponentDefinition create(@NonNull UINestedToggleTip annotation, + @NonNull AnnotatedElement annotatedElement) { + return pmo -> new HorizontalLayout(); + } + } + + class NestedToggleTipLayoutDefinitionCreator implements LayoutDefinitionCreator { + + @Override + public LinkkiLayoutDefinition create(@NonNull UINestedToggleTip annotation, @NonNull AnnotatedElement annotatedElement) { + return (parentComponent, pmo, bindingContext) -> { + final var wrapper = (HorizontalLayout)parentComponent; + final var method = (Method)annotatedElement; + + final var componentDefAnnotation = ComponentAnnotationReader.getComponentDefinitionAnnotations(method) + .stream() + .filter(c -> !c.annotationType() + .isAssignableFrom(UINestedToggleTip.class)) + .reduce((a, b) -> { + throw new IllegalStateException("Multiple component definition annotations found: " + a + ", " + b); + }) + .orElseThrow(() -> new IllegalStateException("No component definition annotation found")); + + final Function creatorFunction = (Component c) -> new NoLabelComponentWrapper( + c, WrapperType.FIELD); + + final var component = (Component)ComponentAnnotationReader.getComponentDefinition(componentDefAnnotation, method) + .createComponent(pmo); + + var bindingDescriptor = BindingDescriptor.forMethod(method); + final var componentWrapper = creatorFunction.apply(component); + componentWrapper.setId(getComponentId(bindingDescriptor.getBoundProperty(), pmo)); + + bindingContext.bind(pmo, bindingDescriptor, componentWrapper); + if (componentWrapper.getComponent() instanceof HasSize hasSize) { + hasSize.setWidth("100%"); + } + final var button = ComponentFactory.newButton(annotation.icon() + .create(), Collections.emptyList()); + + switch (annotation.toggletipPosition()) { + case PREFIX -> { + wrapper.add(button); + wrapper.add(componentWrapper.getComponent()); + } + case SUFFIX -> { + wrapper.add(componentWrapper.getComponent()); + wrapper.add(button); + } + } + + bindingContext.updateUi(); + }; + } + } + + class NestedToggletipAspectDefinitionCreator implements AspectDefinitionCreator { + + @Override + public LinkkiAspectDefinition create(@NonNull UINestedToggleTip annotation) { + return new CompositeAspectDefinition(new ToggletipAspectDefinition(annotation), + new LabelAspectDefinition(annotation.label())); + } + + } + + class ToggletipAspectDefinition extends ModelToUiAspectDefinition { + + private static final String NAME = "toggletip"; + private final UINestedToggleTip annotation; + private Tooltip tooltip; + private Button button; + + public ToggletipAspectDefinition(UINestedToggleTip annotation) { + this.annotation = annotation; + } + + public Tooltip extractTooltip(ComponentWrapper componentWrapper) { + if (componentWrapper.getComponent() instanceof HorizontalLayout layout) { + final var component = getComponent(layout); + if (component instanceof HasTooltip hasTooltip) { + return hasTooltip.getTooltip(); + } else { + throw new IllegalArgumentException("Component " + component.getClass() + .getSimpleName() + // + " does not implement HasTooltip"); + } + } + throw new IllegalArgumentException("Component " + componentWrapper.getComponent() + .getClass() + .getSimpleName() + // + " does not implement HasTooltip"); + } + + private Component getComponent(HorizontalLayout parentLayout) { + return switch (annotation.toggletipPosition()) { + case PREFIX -> parentLayout.getComponentAt(1); + case SUFFIX -> parentLayout.getComponentAt(0); + }; + } + + private Component getToggletipComponent(HorizontalLayout parentLayout) { + return switch (annotation.toggletipPosition()) { + case PREFIX -> parentLayout.getComponentAt(0); + case SUFFIX -> parentLayout.getComponentAt(1); + }; + } + + @Override + public Consumer createComponentValueSetter(@NonNull ComponentWrapper componentWrapper) { + return text -> { + initializeTooltip(componentWrapper); + Optional.ofNullable(tooltip) + .ifPresent(t -> t.setText(text)); + }; + } + + private void initializeTooltip(ComponentWrapper componentWrapper) { + if (componentWrapper.getComponent() instanceof HorizontalLayout layout && layout.getComponentCount() == 0 + || !(componentWrapper.getComponent() instanceof HorizontalLayout)) { + return; + } + + if (tooltip == null && button == null) { + button = extractToggletipButton(componentWrapper); + tooltip = extractTooltip(componentWrapper); + tooltip.withPosition(annotation.tooltipPosition()) + .withManual(true); + + button.addClickListener(event -> tooltip.setOpened(!tooltip.isOpened())); + } + } + + private Button extractToggletipButton(ComponentWrapper componentWrapper) { + if (componentWrapper.getComponent() instanceof HorizontalLayout layout) { + final var component = getToggletipComponent(layout); + if (component instanceof Button buttonComponent) { + return buttonComponent; + } else { + throw new IllegalArgumentException("Component " + component.getClass() + .getSimpleName() + // + " does not implement Button"); + } + } + throw new IllegalArgumentException("Component " + componentWrapper.getComponent() + .getClass() + .getSimpleName() + // + " is not the wrapper horizontal layout"); + } + + @Override + public Aspect createAspect() { + return switch (annotation.toggletipType()) { + case AUTO -> annotation.value() == null || StringUtils.isEmpty(annotation.value()) ? + Aspect.of(NAME) : + Aspect.of(NAME, annotation.value()); + case STATIC -> Aspect.of(NAME, annotation.value()); + case DYNAMIC -> Aspect.of(NAME); + }; + } + } + +} diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/HelperTextType.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/HelperTextType.java new file mode 100644 index 000000000..488a606a6 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/HelperTextType.java @@ -0,0 +1,5 @@ +package org.linkki.core.ui.aspects.types; + +public enum HelperTextType { + AUTO, STATIC, DYNAMIC +} diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/ToggletipType.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/ToggletipType.java new file mode 100644 index 000000000..e138d4e47 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/ToggletipType.java @@ -0,0 +1,5 @@ +package org.linkki.core.ui.aspects.types; + +public enum ToggletipType { + AUTO, STATIC, DYNAMIC; +} diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/TooltipType.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/TooltipType.java new file mode 100644 index 000000000..a9cbb23e6 --- /dev/null +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/aspects/types/TooltipType.java @@ -0,0 +1,38 @@ +/* + * Copyright Faktor Zehn GmbH. + * + * 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 org.linkki.core.ui.aspects.types; + +/** + * Defines the type of the tooltip binding. + */ +public enum TooltipType { + + /** + * The tooltip is static and given by the value attribute. + */ + STATIC, + + /** + * Tooltip is bound to the property using the method {@code getTooltip()}. + */ + DYNAMIC, + + /** + * Linkki decides whether the tooltip is {@link #DYNAMIC} or {@link #STATIC}. In case the value + * is the empty string it calls a method named {@code getTooltip()}. Otherwise the + * specified value is used as tooltip. + */ + AUTO; +} \ No newline at end of file diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/ui/uiframework/VaadinComponentWrapperFactory.java b/vaadin-flow/core/src/main/java/org/linkki/core/ui/uiframework/VaadinComponentWrapperFactory.java index eb0e23898..71aa154ec 100644 --- a/vaadin-flow/core/src/main/java/org/linkki/core/ui/uiframework/VaadinComponentWrapperFactory.java +++ b/vaadin-flow/core/src/main/java/org/linkki/core/ui/uiframework/VaadinComponentWrapperFactory.java @@ -14,10 +14,10 @@ package org.linkki.core.ui.uiframework; -import org.linkki.core.binding.wrapper.ComponentWrapper; import org.linkki.core.binding.wrapper.ComponentWrapperFactory; import org.linkki.core.binding.wrapper.WrapperType; import org.linkki.core.ui.wrapper.NoLabelComponentWrapper; +import org.linkki.core.ui.wrapper.VaadinComponentWrapper; import com.vaadin.flow.component.Component; @@ -46,7 +46,7 @@ public boolean isUiComponent(Class clazz) { } @Override - public ComponentWrapper createComponentWrapper(Object component) { + public VaadinComponentWrapper createComponentWrapper(Object component) { return new NoLabelComponentWrapper((Component)component, WrapperType.FIELD); } diff --git a/vaadin-flow/core/src/main/java/org/linkki/core/vaadin/component/base/LinkkiText.java b/vaadin-flow/core/src/main/java/org/linkki/core/vaadin/component/base/LinkkiText.java index 03476ee39..f489a7070 100644 --- a/vaadin-flow/core/src/main/java/org/linkki/core/vaadin/component/base/LinkkiText.java +++ b/vaadin-flow/core/src/main/java/org/linkki/core/vaadin/component/base/LinkkiText.java @@ -31,6 +31,7 @@ import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.shared.HasPrefix; import com.vaadin.flow.component.shared.HasSuffix; +import com.vaadin.flow.component.shared.HasTooltip; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -41,7 +42,7 @@ */ @Tag("linkki-text") @JsModule("./src/linkki-text.ts") -public class LinkkiText extends Component implements HasIcon, HasPrefix, HasSuffix, HasText { +public class LinkkiText extends Component implements HasIcon, HasPrefix, HasSuffix, HasText, HasTooltip { public static final String CLASS_NAME = "linkki-text"; diff --git a/vaadin-flow/core/src/main/resources/META-INF/resources/frontend/src/linkki-text.ts b/vaadin-flow/core/src/main/resources/META-INF/resources/frontend/src/linkki-text.ts index b70ef19cc..99a896b57 100644 --- a/vaadin-flow/core/src/main/resources/META-INF/resources/frontend/src/linkki-text.ts +++ b/vaadin-flow/core/src/main/resources/META-INF/resources/frontend/src/linkki-text.ts @@ -1,4 +1,4 @@ -import {LitElement, css, html} from 'lit'; +import {css, html, LitElement} from 'lit'; import {property} from 'lit/decorators.js'; class LinkkiText extends LitElement { @@ -134,6 +134,7 @@ class LinkkiText extends LitElement { + `; } diff --git a/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindHelperTextIntegrationTest.java b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindHelperTextIntegrationTest.java new file mode 100644 index 000000000..d9381fe90 --- /dev/null +++ b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindHelperTextIntegrationTest.java @@ -0,0 +1,84 @@ +package org.linkki.core.ui.aspects; + +import static com.github.mvysny.kaributesting.v10.LocatorJ._get; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.linkki.core.binding.BindingContext; +import org.linkki.core.ui.aspects.annotation.BindTooltip; +import org.linkki.core.ui.creation.VaadinUiCreator; +import org.linkki.core.ui.element.annotation.UITextField; +import org.linkki.core.ui.layout.annotation.UISection; + +import com.github.mvysny.kaributesting.v10.MockVaadin; +import com.github.mvysny.kaributesting.v10.Routes; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.shared.Tooltip; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; + +class BindHelperTextIntegrationTest { + + public static final String TOOLTIP_LABEL = "Tooltip"; + public static final int FOCUS_DELAY = 10; + public static final int HIDE_DELAY = 100; + public static final int HOVER_DELAY = 1000; + private static Routes routes; + + @BeforeAll + static void beforeAll() { + routes = new Routes(); + routes.getRoutes().add(MainView.class); + } + + @BeforeEach + void setUp() { + MockVaadin.setup(routes); + } + + @AfterEach + void tearDown() { + MockVaadin.tearDown(); + } + + @Test + void testTooltip() { + final var ui = UI.getCurrent().getChildren().findFirst().orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(TextField.class, spec -> spec.withValue(TOOLTIP_LABEL)); + + assertThat(component.getTooltip()).isNotNull().satisfies(tooltip -> { + assertThat(tooltip.getText()).isEqualTo(TOOLTIP_LABEL); + assertThat(tooltip.getPosition()).isEqualTo(Tooltip.TooltipPosition.BOTTOM); + assertThat(tooltip.getFocusDelay()).isEqualTo(FOCUS_DELAY); + assertThat(tooltip.getHideDelay()).isEqualTo(HIDE_DELAY); + assertThat(tooltip.getHoverDelay()).isEqualTo(HOVER_DELAY); + }); + } + + @UISection + public static class TestPmo { + + @UITextField(position = 10, label = TOOLTIP_LABEL) + @BindTooltip(value = TOOLTIP_LABEL, position = Tooltip.TooltipPosition.BOTTOM, focusDelay = FOCUS_DELAY, hideDelay = HIDE_DELAY, hoverDelay = HOVER_DELAY) + public String getTooltip() { + return TOOLTIP_LABEL; + } + } + + @Route(value = "") + public static class MainView extends VerticalLayout { + public MainView() { + this.setSizeFull(); + final var pmo = new TestPmo(); + final var component = VaadinUiCreator.createComponent(pmo, new BindingContext()); + add(component); + } + } + +} \ No newline at end of file diff --git a/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindNestedToggletipIntegrationTest.java b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindNestedToggletipIntegrationTest.java new file mode 100644 index 000000000..8b5bb48ee --- /dev/null +++ b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindNestedToggletipIntegrationTest.java @@ -0,0 +1,152 @@ +package org.linkki.core.ui.aspects; + +import static com.github.mvysny.kaributesting.v10.LocatorJ._click; +import static com.github.mvysny.kaributesting.v10.LocatorJ._get; +import static org.assertj.core.api.Assertions.assertThat; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.PREFIX; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.SUFFIX; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.linkki.core.binding.BindingContext; +import org.linkki.core.ui.aspects.annotation.UINestedToggleTip; +import org.linkki.core.ui.creation.VaadinUiCreator; +import org.linkki.core.ui.element.annotation.UILabel; +import org.linkki.core.ui.layout.annotation.UISection; +import org.linkki.core.vaadin.component.base.LinkkiText; + +import com.github.mvysny.kaributesting.v10.MockVaadin; +import com.github.mvysny.kaributesting.v10.Routes; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.shared.Tooltip; +import com.vaadin.flow.router.Route; + +class BindNestedToggletipIntegrationTest { + + public static final String LABEL_TEXT = "Toggletip"; + public static final String TOGGLETIP_LABEL = "Toggletip"; + private static final String LABEL_TEXT_PREFIX = "ToggletipPrefix"; + private static Routes routes; + + @BeforeAll + static void beforeAll() { + routes = new Routes(); + routes.getRoutes() + .add(MainView.class); + } + + @BeforeEach + void setUp() { + MockVaadin.setup(routes); + } + + @AfterEach + void tearDown() { + MockVaadin.tearDown(); + } + + @Test + void testToggletip() { + final var ui = UI.getCurrent() + .getChildren() + .findFirst() + .orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(LinkkiText.class, spec -> spec.withText(LABEL_TEXT)); + final var layoutComponent = component.getParent(); + + assertThat(layoutComponent).hasValueSatisfying(l -> { + assertThat(l).isInstanceOf(HorizontalLayout.class); + final var children = l.getChildren() + .toList(); + assertThat(children).hasSize(2) + .hasExactlyElementsOfTypes(LinkkiText.class, Button.class); + }); + assertThat(component.getPrefixComponent()).isNull(); + assertThat(component.getSuffixComponent()).isNull(); + + final var toggletip = (Button)layoutComponent.get() + .getChildren() + .toList() + .get(1); + assertThat(component.getTooltip() + .isOpened()).isFalse(); + + _click(toggletip); + + assertThat(component.getTooltip()).isNotNull() + .satisfies(tooltip -> { + assertThat(tooltip.isOpened()).isTrue(); + assertThat(tooltip.getText()).isEqualTo(TOGGLETIP_LABEL); + assertThat(tooltip.getPosition()).isEqualTo(Tooltip.TooltipPosition.TOP); + }); + + _click(toggletip); + + assertThat(component.getTooltip() + .isOpened()).isFalse(); + } + + @Test + void testToggletipPrefix() { + final var ui = UI.getCurrent() + .getChildren() + .findFirst() + .orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(LinkkiText.class, spec -> spec.withText(LABEL_TEXT_PREFIX)); + final var layout = component.getParent(); + + assertThat(layout).hasValueSatisfying(l -> { + assertThat(l).isInstanceOf(HorizontalLayout.class); + final var children = l.getChildren() + .toList(); + assertThat(children).hasSize(2) + .hasExactlyElementsOfTypes(Button.class, LinkkiText.class); + }); + assertThat(component.getPrefixComponent()).isNull(); + assertThat(component.getSuffixComponent()).isNull(); + } + + @UISection + public static class TestPmo { + + @UILabel(position = 0) + @UINestedToggleTip(position = 0, value = TOGGLETIP_LABEL, toggletipPosition = SUFFIX) + public String getLabel() { + return LABEL_TEXT; + } + + public Class getLabelComponentType() { + return UINestedToggleTip.class; + } + + @UILabel(position = 1) + @UINestedToggleTip(position = 1, value = TOGGLETIP_LABEL, toggletipPosition = PREFIX) + public String getLabelPrefix() { + return LABEL_TEXT_PREFIX; + } + + public Class getLabelPrefixComponentType() { + return UINestedToggleTip.class; + } + } + + @Route(value = "") + public static class MainView extends VerticalLayout { + public MainView() { + this.setSizeFull(); + final var pmo = new TestPmo(); + final var component = VaadinUiCreator.createComponent(pmo, new BindingContext()); + add(component); + } + } + +} \ No newline at end of file diff --git a/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindNewTooltipIntegrationTest.java b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindNewTooltipIntegrationTest.java new file mode 100644 index 000000000..6c069a0d5 --- /dev/null +++ b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindNewTooltipIntegrationTest.java @@ -0,0 +1,106 @@ +package org.linkki.core.ui.aspects; + +import static com.github.mvysny.kaributesting.v10.LocatorJ._get; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.linkki.core.binding.BindingContext; +import org.linkki.core.ui.aspects.annotation.BindHelperText; +import org.linkki.core.ui.creation.VaadinUiCreator; +import org.linkki.core.ui.element.annotation.UITextField; +import org.linkki.core.ui.layout.annotation.UISection; +import org.linkki.core.vaadin.component.base.LinkkiText; + +import com.github.mvysny.kaributesting.v10.MockVaadin; +import com.github.mvysny.kaributesting.v10.Routes; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; + +class BindNewTooltipIntegrationTest { + + public static final String HELPER_TEXT_LABEL_HTML = "HelperTextHtml"; + public static final String HELPER_TEXT_LABEL = "HelperText"; + public static final String HELPER_TEXT_CONTENT = "HelperText"; + private static Routes routes; + + @BeforeAll + static void beforeAll() { + routes = new Routes(); + routes.getRoutes().add(MainView.class); + } + + @BeforeEach + void setUp() { + MockVaadin.setup(routes); + } + + @AfterEach + void tearDown() { + MockVaadin.tearDown(); + } + + @Test + void testHelperText_Html() { + final var ui = UI.getCurrent().getChildren().findFirst().orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(TextField.class, spec -> spec.withValue(HELPER_TEXT_LABEL_HTML)); + + assertThat(component.getThemeNames()).containsAnyElementsOf(List.of("helper-above-field")); + + assertThat(component.getHelperComponent()).isNotNull().isInstanceOf(LinkkiText.class).satisfies(helper -> { + final var text = (LinkkiText)helper; + + assertThat(text.getText()).isEqualTo(HELPER_TEXT_CONTENT); + assertThat(text.getIcon()).isEqualTo(VaadinIcon.INFO_CIRCLE_O); + }); + } + + @Test + void testHelperText_PlainText() { + final var ui = UI.getCurrent().getChildren().findFirst().orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(TextField.class, spec -> spec.withValue(HELPER_TEXT_LABEL)); + + assertThat(component.getThemeNames()).doesNotContain("helper-above-field"); + + assertThat(component.getHelperComponent()).isNull(); + assertThat(component.getHelperText()).isEqualTo(HELPER_TEXT_CONTENT); + } + + @UISection + public static class TestPmo { + + @UITextField(position = 20, label = HELPER_TEXT_LABEL_HTML) + @BindHelperText(value = HELPER_TEXT_CONTENT, showIcon = true, placeAboveElement = true, htmlContent = true) + public String getHelperTextHtml() { + return HELPER_TEXT_LABEL_HTML; + } + + @UITextField(position = 30, label = HELPER_TEXT_LABEL) + @BindHelperText(value = HELPER_TEXT_CONTENT) + public String getHelperText() { + return HELPER_TEXT_LABEL; + } + } + + @Route(value = "") + public static class MainView extends VerticalLayout { + public MainView() { + this.setSizeFull(); + final var pmo = new TestPmo(); + final var component = VaadinUiCreator.createComponent(pmo, new BindingContext()); + add(component); + } + } + +} \ No newline at end of file diff --git a/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindToggletipIntegrationTest.java b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindToggletipIntegrationTest.java new file mode 100644 index 000000000..cd4e8e648 --- /dev/null +++ b/vaadin-flow/core/src/test/java/org/linkki/core/ui/aspects/BindToggletipIntegrationTest.java @@ -0,0 +1,111 @@ +package org.linkki.core.ui.aspects; + +import static com.github.mvysny.kaributesting.v10.LocatorJ._click; +import static com.github.mvysny.kaributesting.v10.LocatorJ._get; +import static org.assertj.core.api.Assertions.assertThat; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.PREFIX; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.SUFFIX; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.linkki.core.binding.BindingContext; +import org.linkki.core.ui.aspects.annotation.BindToggletip; +import org.linkki.core.ui.creation.VaadinUiCreator; +import org.linkki.core.ui.element.annotation.UITextField; +import org.linkki.core.ui.layout.annotation.UISection; + +import com.github.mvysny.kaributesting.v10.MockVaadin; +import com.github.mvysny.kaributesting.v10.Routes; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.shared.Tooltip; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.Route; + +class BindToggletipIntegrationTest { + + public static final String LABEL_TEXT = "Toggletip"; + public static final String TOGGLETIP_LABEL = "Toggletip"; + private static final String LABEL_TEXT_PREFIX = "ToggletipPrefix"; + private static Routes routes; + + @BeforeAll + static void beforeAll() { + routes = new Routes(); + routes.getRoutes().add(MainView.class); + } + + @BeforeEach + void setUp() { + MockVaadin.setup(routes); + } + + @AfterEach + void tearDown() { + MockVaadin.tearDown(); + } + + @Test + void testToggletip() { + final var ui = UI.getCurrent().getChildren().findFirst().orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(TextField.class, spec -> spec.withValue(LABEL_TEXT)); + assertThat(component.getSuffixComponent()).isNotNull(); + + assertThat(component.getSuffixComponent()).isInstanceOf(Button.class); + final var toggletip = (Button)component.getSuffixComponent(); + assertThat(component.getTooltip().isOpened()).isFalse(); + + _click(toggletip); + + assertThat(component.getTooltip()).isNotNull().satisfies(tooltip -> { + assertThat(tooltip.isOpened()).isTrue(); + assertThat(tooltip.getText()).isEqualTo(TOGGLETIP_LABEL); + assertThat(tooltip.getPosition()).isEqualTo(Tooltip.TooltipPosition.TOP); + }); + + _click(toggletip); + + assertThat(component.getTooltip().isOpened()).isFalse(); + } + + @Test + void testToggletipPrefix() { + final var ui = UI.getCurrent().getChildren().findFirst().orElseThrow(); + assertThat(ui).isInstanceOf(MainView.class); + + final var component = _get(TextField.class, spec -> spec.withValue(LABEL_TEXT_PREFIX)); + assertThat(component.getPrefixComponent()).isNotNull(); + } + + @UISection + public static class TestPmo { + + @UITextField(position = 0, label = LABEL_TEXT) + @BindToggletip(value = TOGGLETIP_LABEL, toggletipPosition = SUFFIX) + public String getLabel() { + return LABEL_TEXT; + } + + @UITextField(position = 1, label = LABEL_TEXT) + @BindToggletip(value = TOGGLETIP_LABEL, toggletipPosition = PREFIX) + public String getLabelPrefix() { + return LABEL_TEXT_PREFIX; + } + } + + @Route(value = "") + public static class MainView extends VerticalLayout { + public MainView() { + this.setSizeFull(); + final var pmo = new TestPmo(); + final var component = VaadinUiCreator.createComponent(pmo, new BindingContext()); + add(component); + } + } + +} \ No newline at end of file diff --git a/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/TestScenarioView.java b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/TestScenarioView.java index 03f1c1eaf..dd06e9d51 100644 --- a/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/TestScenarioView.java +++ b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/TestScenarioView.java @@ -54,6 +54,7 @@ import org.linkki.samples.playground.ts.aspects.BindCaptionWithSectionHeaderButtonPmo; import org.linkki.samples.playground.ts.aspects.BindCaptionWithoutButtonPmo; import org.linkki.samples.playground.ts.aspects.BindComboBoxItemStylePmo; +import org.linkki.samples.playground.ts.aspects.BindHelperTextPmo; import org.linkki.samples.playground.ts.aspects.BindIconPmo; import org.linkki.samples.playground.ts.aspects.BindLabelPmo; import org.linkki.samples.playground.ts.aspects.BindPlaceholderPmo; @@ -61,10 +62,12 @@ import org.linkki.samples.playground.ts.aspects.BindSlotPmo; import org.linkki.samples.playground.ts.aspects.BindStyleNamesPmo; import org.linkki.samples.playground.ts.aspects.BindSuffixPmo; +import org.linkki.samples.playground.ts.aspects.BindToggletipPmo; import org.linkki.samples.playground.ts.aspects.BindTooltipPmo; import org.linkki.samples.playground.ts.aspects.BindVariantNamesTables.BindVariantNamesTablePmoNoBorder; import org.linkki.samples.playground.ts.aspects.BindVariantNamesTables.BindVariantNamesTablePmoWithoutVariant; import org.linkki.samples.playground.ts.aspects.BindVisiblePmo; +import org.linkki.samples.playground.ts.aspects.NewBindTooltipPmo; import org.linkki.samples.playground.ts.components.ButtonPmo; import org.linkki.samples.playground.ts.components.CheckboxesPmo; import org.linkki.samples.playground.ts.components.ComboBoxCaptionRefreshPmo; @@ -195,6 +198,7 @@ public class TestScenarioView extends Div implements HasUrlParameter { public static final String TC016 = "TC016"; public static final String TC017 = "TC017"; public static final String TC018 = "TC018"; + public static final String TC019 = "TC019"; static final String ROUTE = "playground"; @@ -312,6 +316,9 @@ public TestScenarioView() { new BindVariantNamesTablePmoWithoutVariant()) .testCase(TC015, new BindLabelPmo()) .testCase(TC016, new BindSlotPmo(new BindSlotPmo.RightSlotPmo())) + .testCase(TC017, new NewBindTooltipPmo()) + .testCase(TC018, new BindToggletipPmo()) + .testCase(TC019, new BindHelperTextPmo()) .createTabSheet(), TestScenario.id(TS009) .testCase(TC001, new TextNotificationPmo()) diff --git a/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/BindHelperTextPmo.java b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/BindHelperTextPmo.java new file mode 100644 index 000000000..e37ce46f2 --- /dev/null +++ b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/BindHelperTextPmo.java @@ -0,0 +1,47 @@ +/* + * Copyright Faktor Zehn GmbH. + * + * 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 org.linkki.samples.playground.ts.aspects; + +import org.linkki.core.ui.aspects.annotation.BindHelperText; +import org.linkki.core.ui.aspects.types.HelperTextType; +import org.linkki.core.ui.element.annotation.UITextField; +import org.linkki.core.ui.layout.annotation.UISection; + +@UISection +public class BindHelperTextPmo { + + private String helperTextText = "This field has a helper text that changes dynamically with the content of this textfield"; + + @UITextField(position = 20, label = "Helper Text") + @BindHelperText(helperTextType = HelperTextType.DYNAMIC) + public String getHelperText() { + return helperTextText; + } + + public void setHelperText(String tooltipText) { + this.helperTextText = tooltipText; + } + + public String getHelperTextHelperText() { + return helperTextText; + } + + @UITextField(position = 21, label = "Static Helper Text") + @BindHelperText(helperTextType = HelperTextType.STATIC, value = "A nice static helper text", htmlContent = + true, placeAboveElement = true, showIcon = true) + public String getHelperTextStaticText() { + return "This field has a static helper text that cannot be changed with html content and is shown above the field"; + } +} \ No newline at end of file diff --git a/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/BindToggletipPmo.java b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/BindToggletipPmo.java new file mode 100644 index 000000000..26abba15e --- /dev/null +++ b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/BindToggletipPmo.java @@ -0,0 +1,86 @@ +/* + * Copyright Faktor Zehn GmbH. + * + * 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 org.linkki.samples.playground.ts.aspects; + +import static com.vaadin.flow.component.shared.Tooltip.TooltipPosition.BOTTOM; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.PREFIX; +import static org.linkki.core.ui.aspects.annotation.BindToggletip.ToogletipPosition.SUFFIX; + +import org.apache.commons.lang3.StringUtils; +import org.linkki.core.defaults.ui.aspects.types.AvailableValuesType; +import org.linkki.core.ui.aspects.annotation.BindToggletip; +import org.linkki.core.ui.aspects.annotation.UINestedToggleTip; +import org.linkki.core.ui.element.annotation.UIComboBox; +import org.linkki.core.ui.element.annotation.UILabel; +import org.linkki.core.ui.element.annotation.UITextField; +import org.linkki.core.ui.layout.annotation.UISection; + +import com.vaadin.flow.component.icon.VaadinIcon; + +@UISection +public class BindToggletipPmo { + + private String toggletipText = "This field has a toggletip at suffix position that changes dynamically with the content of this textfield"; + private String nestedToggletip = "This field has a nested toggletip"; + + @UITextField(position = 20, label = "Toggletip") + @BindToggletip(toggletipPosition = SUFFIX) + public String getToggletipText() { + return toggletipText; + } + + public void setToggletipText(String toggletipText) { + this.toggletipText = toggletipText; + } + + public String getToggletipTextToggletip() { + return toggletipText; + } + + @UIComboBox(position = 21, label = "Static Toggletip", content = AvailableValuesType.NO_VALUES) + @BindToggletip(toggletipPosition = PREFIX, value = "A nice static toggletip", tooltipPosition = BOTTOM, icon = VaadinIcon.ABACUS) + public String getToggletipStaticText() { + return "This field has a static toggletip that cannot be changed with tooltip position bottom and a different icon"; + } + + @UITextField(position = 22) + @UINestedToggleTip(position = 22, label = "Nested Toggletip Suffix") + public String getNestedToggletipSuffix() { + return nestedToggletip; + } + + public void setNestedToggletipSuffix(String nestedToggletip) { + this.nestedToggletip = nestedToggletip; + } + + public Class getNestedToggletipSuffixComponentType() { + return UINestedToggleTip.class; + } + + public String getNestedToggletipSuffixToggletip() { + return "Toggletip: " + StringUtils.defaultIfBlank(nestedToggletip, "No content"); + } + + @UILabel(position = 23) + @UINestedToggleTip(position = 23, label = "Nested Toggletip Prefix", + toggletipPosition = PREFIX, icon = VaadinIcon.EXCLAMATION_CIRCLE_O, value = "Toggletip") + public String getNestedToggletipPrefix() { + return nestedToggletip; + } + + public Class getNestedToggletipPrefixComponentType() { + return UINestedToggleTip.class; + } +} \ No newline at end of file diff --git a/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/NewBindTooltipPmo.java b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/NewBindTooltipPmo.java new file mode 100644 index 000000000..ed6ff6158 --- /dev/null +++ b/vaadin-flow/samples/test-playground/src/main/java/org/linkki/samples/playground/ts/aspects/NewBindTooltipPmo.java @@ -0,0 +1,70 @@ +/* + * Copyright Faktor Zehn GmbH. + * + * 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 org.linkki.samples.playground.ts.aspects; + +import static com.vaadin.flow.component.shared.Tooltip.TooltipPosition.BOTTOM; + +import org.linkki.core.ui.aspects.annotation.BindTooltip; +import org.linkki.core.ui.aspects.types.TooltipType; +import org.linkki.core.ui.element.annotation.UITextField; +import org.linkki.core.ui.layout.annotation.UISection; + +@UISection +public class NewBindTooltipPmo { + + private String tooltipText = "This field has a tooltip that changes dynamically with the content of this textfield"; + private String tooltipHtmlText = + "

This is a nice
text with some
new lines


and some html
"; + + @UITextField(position = 20, label = "Tooltip") + @BindTooltip(tooltipType = TooltipType.DYNAMIC) + public String getTooltipText() { + return tooltipText; + } + + public void setTooltipText(String tooltipText) { + this.tooltipText = tooltipText; + } + + public String getTooltipTextTooltip() { + return tooltipText; + } + + @UITextField(position = 21, label = "Html Tooltip") + @BindTooltip(tooltipType = TooltipType.DYNAMIC) + public String getTooltipHtmlText() { + return tooltipHtmlText; + } + + public void setTooltipHtmlText(String tooltipText) { + this.tooltipHtmlText = tooltipText; + } + + public String getTooltipHtmlTextTooltip() { + return tooltipHtmlText; + } + + @UITextField(position = 22, label = "Static Tooltip") + @BindTooltip(tooltipType = TooltipType.STATIC, value = "A nice static tooltip", position = BOTTOM) + public String getTooltipStaticText() { + return "This field has a static tooltip that cannot be changed with position bottom"; + } + + @UITextField(position = 23, label = "Static Tooltip with Delays") + @BindTooltip(tooltipType = TooltipType.STATIC, value = "A nice static tooltip", hoverDelay = 1000, focusDelay = 2000, hideDelay = 3000, position = BOTTOM) + public String getTooltipStaticTextDelays() { + return "This field has a static tooltip with hover delay = 1000ms, focus delay = 2000ms and hide delay = 3000ms"; + } +} \ No newline at end of file diff --git a/vaadin-flow/samples/test-playground/src/main/resources/org/linkki/samples/playground/testcatalog.properties b/vaadin-flow/samples/test-playground/src/main/resources/org/linkki/samples/playground/testcatalog.properties index dc57f1a70..4a0d96f57 100644 --- a/vaadin-flow/samples/test-playground/src/main/resources/org/linkki/samples/playground/testcatalog.properties +++ b/vaadin-flow/samples/test-playground/src/main/resources/org/linkki/samples/playground/testcatalog.properties @@ -197,6 +197,15 @@ TS008.TC015.items=The labels below 'Dynamic labels' should update dynamically.\n TS008.TC016=@BindSlot TS008.TC016.description=Tests @BindSlot - the displayed layout is defined by a custom, reusable TypeScript file. TS008.TC016.items=One button is displayed on the left side.\n Two buttons are display right-aligned with a padding to the right. +TS008.TC017=@BindTooltip +TS008.TC017.description=Tests the BindTooltip annotation +TS008.TC017.items=The tooltip should be shown as title attribute and dynamically replaced with the content of the textfield.\n Static tooltips cannot be changed \n The position of a tooltip can be changed (Default is top) \n The focus, hide and hover delays can be changed +TS008.TC018=@BindToggletip +TS008.TC018.description=Tests the BindToggletip annotation +TS008.TC018.items=The content of the tooltip should be dynamically replaced with the content of the textfield. \n Static toggletips cannot be changed \n The position of the tooltip can be changed \n The position of the toggletip can be either Prefix or Suffix, but only if the component where it is applied on, supports it. +TS008.TC019=@BindHelperText +TS008.TC019.description=Tests the BindHelperText annotation +TS008.TC019.items=The helper text should dynamically replaced with the content of the textfield.\n HTML content can also be rendered \n Static helper texts cannot be changed \n You can optionally show an icon either on the left or right but only with html mode\n The helper text can also be placed above the item TS009=Notifications TS009.TC001=Text Notifications TS009.TC001.description=Notifications containing a title and text.