diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java b/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java new file mode 100644 index 0000000..1bffa17 --- /dev/null +++ b/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java @@ -0,0 +1,188 @@ +package com.flowingcode.vaadin.addons.demo; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasElement; +import com.vaadin.flow.component.page.Inline.Position; +import com.vaadin.flow.server.AppShellSettings; +import com.vaadin.flow.server.VaadinSession; +import com.vaadin.flow.server.Version; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Enumeration representing supported themes for dynamic switching. + *
+ * This enum facilitates switching between themes (e.g., Lumo, Aura) at runtime. + *
+ */ +@RequiredArgsConstructor +public enum DynamicTheme { + + /** + * The standard Lumo theme. + */ + LUMO("lumo/lumo.css", "hsl(214, 35%, 21%)"), + + /** + * The standard Aura theme. + */ + AURA("aura/aura.css", "oklch(0.2 0.01 260)"), + + /** + * A base theme without specific styling. + */ + BASE(null, "#000"); + + @Getter + private final String href; + + @Getter + private final String bgColor; + + + private static void assertFeatureSupported() { + if (!isFeatureSupported()) { + throw new UnsupportedOperationException("Dynamic theme switching requires Vaadin 25+"); + } + } + + /** + * Checks if the dynamic theme feature is supported. The feature is supported in Vaadin 25. + * + * @return {@code true} if the feature is supported and initialized; {@code false} otherwise. + */ + public static boolean isFeatureSupported() { + return Version.getMajorVersion() >= 25; + } + + private static void assertFeatureInitialized() { + assertFeatureSupported(); + if (!isFeatureInitialized()) { + throw new IllegalStateException("Dynamic theme switching has not been initialized"); + } + } + + /** + * Checks if the dynamic theme feature has been initialized for the current session. + * + * @return {@code true} if the feature is supported and initialized; {@code false} otherwise. + */ + public static boolean isFeatureInitialized() { + return isFeatureSupported() + && VaadinSession.getCurrent().getAttribute(DynamicTheme.class) != null; + } + + /** + * Return the current dynamic theme. + * + * @throws UnsupportedOperationException if the runtime Vaadin version is older than 25. + * @return the current dynamic theme, or {@code null} if the feature has not been initialized. + */ + public static DynamicTheme getCurrent() { + assertFeatureSupported(); + return VaadinSession.getCurrent().getAttribute(DynamicTheme.class); + } + + /** + * Initializes the theme settings. + *+ * This method performs a lazy initialization of the {@link DynamicTheme} within the + * current {@link VaadinSession}. If no theme is present, it registers this instance + * as the session default. Subsequently, it injects the corresponding CSS stylesheet + * link into the {@link AppShellSettings}. + *
+ * + * @param settings the application shell settings to be modified + * @throws UnsupportedOperationException if the runtime Vaadin version is older than 25 + */ + public void initialize(AppShellSettings settings) { + assertFeatureSupported(); + + DynamicTheme theme = getCurrent(); + if (theme == null) { + theme = this; + VaadinSession.getCurrent().setAttribute(DynamicTheme.class, theme); + } + + switch (theme) { + case AURA: + settings.addLink(Position.APPEND, "stylesheet", "aura/aura.css"); + break; + case LUMO: + settings.addLink(Position.APPEND, "stylesheet", "lumo/lumo.css"); + break; + default: + break; + } + } + + /** + * Prepares the component for dynamic theme switching by preloading stylesheets. + *+ * Adds a client-side listener to the component that detects mouseover events. + * When triggered, it preloads the theme stylesheets (Lumo and Aura) to ensure + * they can be applied immediately when needed. + *
+ * + * @param component the component to attach the listener to + * @throws IllegalStateException if the dynamic theme feature has not been initialized + */ + public static void prepare(Component component) { + assertFeatureInitialized(); + + component.addAttachListener(ev -> doPrepare(component)); + if (component.isAttached()) { + doPrepare(component); + } + } + + private static void doPrepare(Component component) { + component.getElement().executeJs(""" + this.addEventListener('mouseover', function() { + ["lumo/lumo.css", "aura/aura.css"].forEach(href=> { + let link = document.querySelector(`link[href="${href}"]`); + if (!link) { + link = document.createElement("link"); + link.href = href; + link.as = 'style'; + link.rel = 'preload'; + document.head.prepend(link); + } + }); + }, {once:true} ); + """); + } + + /** + * Applies this theme to the view. + * + * @param component a component in the view + * @throws IllegalStateException if the dynamic theme feature has not been initialized + */ + public void apply(HasElement component) { + assertFeatureInitialized(); + + VaadinSession.getCurrent().setAttribute(DynamicTheme.class, this); + component.getElement().executeJs(""" + const applyTheme = () => { + ["lumo/lumo.css", "aura/aura.css"].forEach(href=> { + let link = document.querySelector(`link[href='${href}']`); + if (!link) return; + if (href === $0) { + if (link.rel === 'preload') link.rel = 'stylesheet'; + if (link.disabled) link.disabled = false; + } else if (link.rel === 'stylesheet' && !link.disabled) { + link.disabled = true; + } + }); + }; + + if (document.startViewTransition) { + document.startViewTransition(applyTheme); + } else { + applyTheme(); + } + """, href); + } + +} diff --git a/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java b/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java index 1deeae7..a8771a3 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java +++ b/src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java @@ -28,11 +28,13 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.dependency.CssImport; import com.vaadin.flow.component.dependency.StyleSheet; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.select.Select; import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation; import com.vaadin.flow.dom.Element; import com.vaadin.flow.router.PageTitle; @@ -59,6 +61,7 @@ */ @StyleSheet("context://frontend/styles/commons-demo/shared-styles.css") @SuppressWarnings("serial") +@CssImport(value = "./styles/commons-demo/vaadin-select-overlay.css", themeFor = "vaadin-select") public class TabbedDemo extends VerticalLayout implements RouterLayout { private static final Logger logger = LoggerFactory.getLogger(TabbedDemo.class); @@ -108,6 +111,7 @@ public TabbedDemo() { boolean useDarkTheme = themeCB.getValue(); setColorScheme(this, useDarkTheme ? ColorScheme.DARK : ColorScheme.LIGHT); }); + footer = new HorizontalLayout(); footer.setWidthFull(); footer.setJustifyContentMode(JustifyContentMode.END); @@ -125,6 +129,18 @@ public TabbedDemo() { footerLeft.add(new Span(title + " " + version)); } + if (DynamicTheme.isFeatureInitialized()) { + Select