Skip to content

Commit 156e423

Browse files
javier-godoypaodb
authored andcommitted
feat: add support for dynamic theme switching
1 parent 2783988 commit 156e423

5 files changed

Lines changed: 222 additions & 2 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package com.flowingcode.vaadin.addons.demo;
2+
3+
import com.vaadin.flow.component.Component;
4+
import com.vaadin.flow.component.HasElement;
5+
import com.vaadin.flow.component.page.Inline.Position;
6+
import com.vaadin.flow.server.AppShellSettings;
7+
import com.vaadin.flow.server.VaadinSession;
8+
import com.vaadin.flow.server.Version;
9+
import lombok.Getter;
10+
import lombok.RequiredArgsConstructor;
11+
12+
/**
13+
* Enumeration representing supported themes for dynamic switching.
14+
* <p>
15+
* This enum facilitates switching between themes (e.g., Lumo, Aura) at runtime.
16+
* </p>
17+
*/
18+
@RequiredArgsConstructor
19+
public enum DynamicTheme {
20+
21+
/**
22+
* The standard Lumo theme.
23+
*/
24+
LUMO("lumo/lumo.css", "hsl(214, 35%, 21%)"),
25+
26+
/**
27+
* The standard Aura theme.
28+
*/
29+
AURA("aura/aura.css", "oklch(0.2 0.01 260)"),
30+
31+
/**
32+
* A base theme without specific styling.
33+
*/
34+
BASE(null, "#000");
35+
36+
@Getter
37+
private final String href;
38+
39+
@Getter
40+
private final String bgColor;
41+
42+
43+
private static void assertFeatureSupported() {
44+
if (!isFeatureSupported()) {
45+
throw new UnsupportedOperationException("Dynamic theme switching requires Vaadin 25+");
46+
}
47+
}
48+
49+
/**
50+
* Checks if the dynamic theme feature is supported. The feature is supported in Vaadin 25.
51+
*
52+
* @return {@code true} if the feature is supported and initialized; {@code false} otherwise.
53+
*/
54+
public static boolean isFeatureSupported() {
55+
return Version.getMajorVersion() >= 25;
56+
}
57+
58+
private static void assertFeatureInitialized() {
59+
assertFeatureSupported();
60+
if (!isFeatureInitialized()) {
61+
throw new IllegalStateException("Dynamic theme switching has not been initialized");
62+
}
63+
}
64+
65+
/**
66+
* Checks if the dynamic theme feature has been initialized for the current session.
67+
*
68+
* @return {@code true} if the feature is supported and initialized; {@code false} otherwise.
69+
*/
70+
public static boolean isFeatureInitialized() {
71+
return isFeatureSupported()
72+
&& VaadinSession.getCurrent().getAttribute(DynamicTheme.class) != null;
73+
}
74+
75+
/**
76+
* Return the current dynamic theme.
77+
*
78+
* @throws UnsupportedOperationException if the runtime Vaadin version is older than 25.
79+
* @return the current dynamic theme, or {@code null} if the feature has not been initialized.
80+
*/
81+
public static DynamicTheme getCurrent() {
82+
assertFeatureSupported();
83+
return VaadinSession.getCurrent().getAttribute(DynamicTheme.class);
84+
}
85+
86+
/**
87+
* Initializes the theme settings.
88+
* <p>
89+
* This method performs a lazy initialization of the {@link DynamicTheme} within the
90+
* current {@link VaadinSession}. If no theme is present, it registers this instance
91+
* as the session default. Subsequently, it injects the corresponding CSS stylesheet
92+
* link into the {@link AppShellSettings}.
93+
* </p>
94+
*
95+
* @param settings the application shell settings to be modified
96+
* @throws UnsupportedOperationException if the runtime Vaadin version is older than 25
97+
*/
98+
public void initialize(AppShellSettings settings) {
99+
assertFeatureSupported();
100+
101+
DynamicTheme theme = getCurrent();
102+
if (theme == null) {
103+
theme = this;
104+
VaadinSession.getCurrent().setAttribute(DynamicTheme.class, theme);
105+
}
106+
107+
switch (theme) {
108+
case AURA:
109+
settings.addLink(Position.APPEND, "stylesheet", "aura/aura.css");
110+
break;
111+
case LUMO:
112+
settings.addLink(Position.APPEND, "stylesheet", "lumo/lumo.css");
113+
break;
114+
default:
115+
break;
116+
}
117+
}
118+
119+
/**
120+
* Prepares the component for dynamic theme switching by preloading stylesheets.
121+
* <p>
122+
* Adds a client-side listener to the component that detects mouseover events.
123+
* When triggered, it preloads the theme stylesheets (Lumo and Aura) to ensure
124+
* they can be applied immediately when needed.
125+
* </p>
126+
*
127+
* @param component the component to attach the listener to
128+
* @throws IllegalStateException if the dynamic theme feature has not been initialized
129+
*/
130+
public static void prepare(Component component) {
131+
assertFeatureInitialized();
132+
133+
component.addAttachListener(ev -> doPrepare(component));
134+
if (component.isAttached()) {
135+
doPrepare(component);
136+
}
137+
}
138+
139+
private static void doPrepare(Component component) {
140+
component.getElement().executeJs("""
141+
this.addEventListener('mouseover', function() {
142+
["lumo/lumo.css", "aura/aura.css"].forEach(href=> {
143+
let link = document.querySelector(`link[href="${href}"]`);
144+
if (!link) {
145+
link = document.createElement("link");
146+
link.href = href;
147+
link.as = 'style';
148+
link.rel = 'preload';
149+
document.head.prepend(link);
150+
}
151+
});
152+
}, {once:true} );
153+
""");
154+
}
155+
156+
/**
157+
* Applies this theme to the view.
158+
*
159+
* @param component a component in the view
160+
* @throws IllegalStateException if the dynamic theme feature has not been initialized
161+
*/
162+
public void apply(HasElement component) {
163+
assertFeatureInitialized();
164+
165+
VaadinSession.getCurrent().setAttribute(DynamicTheme.class, this);
166+
component.getElement().executeJs("""
167+
const applyTheme = () => {
168+
["lumo/lumo.css", "aura/aura.css"].forEach(href=> {
169+
let link = document.querySelector(`link[href='${href}']`);
170+
if (!link) return;
171+
if (href === $0) {
172+
if (link.rel === 'preload') link.rel = 'stylesheet';
173+
if (link.disabled) link.disabled = false;
174+
} else if (link.rel === 'stylesheet' && !link.disabled) {
175+
link.disabled = true;
176+
}
177+
});
178+
};
179+
180+
if (document.startViewTransition) {
181+
document.startViewTransition(applyTheme);
182+
} else {
183+
applyTheme();
184+
}
185+
""", href);
186+
}
187+
188+
}

src/main/java/com/flowingcode/vaadin/addons/demo/TabbedDemo.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
import com.vaadin.flow.component.button.Button;
2929
import com.vaadin.flow.component.button.ButtonVariant;
3030
import com.vaadin.flow.component.checkbox.Checkbox;
31+
import com.vaadin.flow.component.dependency.CssImport;
3132
import com.vaadin.flow.component.dependency.StyleSheet;
3233
import com.vaadin.flow.component.html.Div;
3334
import com.vaadin.flow.component.html.Span;
3435
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
3536
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
37+
import com.vaadin.flow.component.select.Select;
3638
import com.vaadin.flow.component.splitlayout.SplitLayout.Orientation;
3739
import com.vaadin.flow.dom.Element;
3840
import com.vaadin.flow.router.PageTitle;
@@ -59,6 +61,7 @@
5961
*/
6062
@StyleSheet("context://frontend/styles/commons-demo/shared-styles.css")
6163
@SuppressWarnings("serial")
64+
@CssImport(value = "./styles/commons-demo/vaadin-select-overlay.css", themeFor = "vaadin-select")
6265
public class TabbedDemo extends VerticalLayout implements RouterLayout {
6366

6467
private static final Logger logger = LoggerFactory.getLogger(TabbedDemo.class);
@@ -108,6 +111,7 @@ public TabbedDemo() {
108111
boolean useDarkTheme = themeCB.getValue();
109112
setColorScheme(this, useDarkTheme ? ColorScheme.DARK : ColorScheme.LIGHT);
110113
});
114+
111115
footer = new HorizontalLayout();
112116
footer.setWidthFull();
113117
footer.setJustifyContentMode(JustifyContentMode.END);
@@ -125,6 +129,18 @@ public TabbedDemo() {
125129
footerLeft.add(new Span(title + " " + version));
126130
}
127131

132+
if (DynamicTheme.isFeatureInitialized()) {
133+
Select<DynamicTheme> themeSelect = new Select<>();
134+
themeSelect.addThemeName("demo-footer-theme-select");
135+
themeSelect.addThemeName("small");
136+
DynamicTheme.prepare(themeSelect);
137+
themeSelect.setItems(DynamicTheme.values());
138+
themeSelect.setValue(DynamicTheme.getCurrent());
139+
themeSelect.setWidth("85px");
140+
themeSelect.addValueChangeListener(ev -> ev.getValue().apply(this));
141+
footer.add(themeSelect);
142+
}
143+
128144
this.add(tabs);
129145
this.add(new Div());
130146
this.add(footer);

src/main/resources/META-INF/resources/frontend/styles/commons-demo/shared-styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ code-highlighter code {
3535
bottom: 0;
3636
left: 0;
3737
background: var(--lumo-base-color);
38+
align-items: center;
39+
}
40+
41+
.demo-footer vaadin-select-value-button {
42+
mask-image:none;
43+
padding:0;
3844
}
3945

4046
.helper-button {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** Needed in order to prevent vaadin-select from remaining 'closing' when switching from Lumo to Aura/Base*/
2+
:host([theme~='demo-footer-theme-select']) vaadin-select-overlay {
3+
animation-name: none;
4+
}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package com.flowingcode.vaadin.addons.demo;
22

33
import com.vaadin.flow.component.page.AppShellConfigurator;
4-
import com.vaadin.flow.theme.Theme;
4+
import com.vaadin.flow.server.AppShellSettings;
55

6-
@Theme
76
public class AppShellConfiguratorImpl implements AppShellConfigurator {
87

8+
@Override
9+
public void configurePage(AppShellSettings settings) {
10+
if (DynamicTheme.isFeatureSupported()) {
11+
DynamicTheme.LUMO.initialize(settings);
12+
}
13+
}
14+
915
}

0 commit comments

Comments
 (0)