diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java index ccee7dc5101..d328477ca02 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/AbstractUpdateImports.java @@ -33,12 +33,14 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.UnaryOperator; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -76,6 +78,7 @@ abstract class AbstractUpdateImports implements Runnable { + " tpl.innerHTML = block;\n" + " document.head.appendChild(tpl.content);\n" + "}"; private static final String IMPORT_INJECT = "import { injectGlobalCss } from 'Frontend/generated/jar-resources/theme-util.js';\n"; + private static final String IMPORT_WC_INJECT = "import { injectGlobalWebcomponentCss } from 'Frontend/generated/jar-resources/theme-util.js';\n"; private static final String CSS_IMPORT = "import $cssFromFile_%d from '%s';%n"; private static final String CSS_IMPORT_AND_MAKE_LIT_CSS = CSS_IMPORT @@ -87,6 +90,10 @@ abstract class AbstractUpdateImports implements Runnable { + "${$css_%1$d}" + CSS_POST; private static final String INJECT_CSS = CSS_IMPORT + "%ninjectGlobalCss($cssFromFile_%1$d.toString(), 'CSSImport end', document);%n"; + private static final Pattern INJECT_CSS_PATTERN = Pattern + .compile("^\\s*injectGlobalCss\\(([^,]+),.*$"); + private static final String INJECT_WC_CSS = "injectGlobalWebcomponentCss(%s);"; + private static final String THEMABLE_MIXIN_IMPORT = "import { css, unsafeCSS, registerStyles } from '@vaadin/vaadin-themable-mixin';"; private static final String REGISTER_STYLES_FOR_TEMPLATE = CSS_IMPORT_AND_MAKE_LIT_CSS + "%n" + "registerStyles('%s', $css_%1$d%s);"; @@ -211,13 +218,27 @@ protected void writeOutput(Map> outputFiles) { List filterWebComponentImports(List lines) { if (lines != null) { // Exclude Lumo global imports for exported web-component - return lines.stream() - .filter(VAADIN_LUMO_GLOBAL_IMPORT.asPredicate().negate()) - .collect(Collectors.toList()); + List copy = new ArrayList<>(lines); + copy.add(0, IMPORT_WC_INJECT); + copy.removeIf(VAADIN_LUMO_GLOBAL_IMPORT.asPredicate()); + // Add global CSS imports with a per-webcomponent registration + final ListIterator li = copy.listIterator(); + while (li.hasNext()) { + adaptCssInjectForWebComponent(li, li.next()); + } + return copy; } return lines; } + private void adaptCssInjectForWebComponent(ListIterator iterator, + String line) { + Matcher matcher = INJECT_CSS_PATTERN.matcher(line); + if (matcher.matches()) { + iterator.add(String.format(INJECT_WC_CSS, matcher.group(1))); + } + } + private void writeWebComponentImports(List lines) { if (lines != null) { try { @@ -295,7 +316,8 @@ private Map> process(Map> css, chunkLines.addAll( getModuleLines(lazyJavascript.get(chunkInfo))); } - if (lazyCss.containsKey(chunkInfo)) { + boolean hasLazyCss = lazyCss.containsKey(chunkInfo); + if (hasLazyCss) { chunkLines.add(IMPORT_INJECT); chunkLines.add(THEMABLE_MIXIN_IMPORT); chunkLines.addAll(lazyCss.get(chunkInfo)); @@ -348,6 +370,13 @@ private Map> process(Map> css, mainLines.addAll(mainCssLines); } mainLines.addAll(getModuleLines(eagerJavascript)); + + // Move all imports to the top + List copy = new ArrayList<>(mainLines); + copy.removeIf(line -> !line.startsWith("import ")); + mainLines.removeIf(line -> line.startsWith("import ")); + mainLines.addAll(0, copy); + mainLines.addAll(chunkLoader); mainLines.add("window.Vaadin = window.Vaadin || {};"); mainLines.add("window.Vaadin.Flow = window.Vaadin.Flow || {};"); diff --git a/flow-server/src/main/resources/META-INF/frontend/theme-util.js b/flow-server/src/main/resources/META-INF/frontend/theme-util.js index 5e4ed19588a..31506c1f610 100644 --- a/flow-server/src/main/resources/META-INF/frontend/theme-util.js +++ b/flow-server/src/main/resources/META-INF/frontend/theme-util.js @@ -114,6 +114,31 @@ window.Vaadin = window.Vaadin || {}; window.Vaadin.theme = window.Vaadin.theme || {}; window.Vaadin.theme.injectedGlobalCss = []; +const webcomponentGlobalCss = { + css: [], + importers: [] +}; + +export const injectGlobalWebcomponentCss = (css) => { + webcomponentGlobalCss.css.push(css); + webcomponentGlobalCss.importers.forEach(registrar => { + registrar(css); + }); +}; + +export const webcomponentGlobalCssInjector = (registrar) => { + const registeredCss = []; + const wrapper = (css) => { + const hash = getHash(css); + if (!registeredCss.includes(hash)) { + registeredCss.push(hash); + registrar(css); + } + }; + webcomponentGlobalCss.importers.push(wrapper); + webcomponentGlobalCss.css.forEach(wrapper); +}; + /** * Calculate a 32 bit FNV-1a hash * Found here: https://gist.github.com/vaiorabbit/5657561 diff --git a/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js b/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js index ceb26d5d6f6..af77f481e07 100644 --- a/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js +++ b/flow-server/src/main/resources/plugins/application-theme-plugin/theme-generator.js @@ -51,6 +51,7 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) { const styles = resolve(themeFolder, stylesCssFilename); const documentCssFile = resolve(themeFolder, documentCssFilename); const autoInjectComponents = themeProperties.autoInjectComponents ?? true; + const autoInjectGlobalCssImports = themeProperties.autoInjectGlobalCssImports ?? false; const globalFilename = 'theme-' + themeName + '.global.generated.js'; const componentsFilename = 'theme-' + themeName + '.components.generated.js'; const themeFilename = 'theme-' + themeName + '.generated.js'; @@ -77,6 +78,7 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) { } themeFileContent += `import { injectGlobalCss } from 'Frontend/generated/jar-resources/theme-util.js';\n`; + themeFileContent += `import { webcomponentGlobalCssInjector } from 'Frontend/generated/jar-resources/theme-util.js';\n`; themeFileContent += `import './${componentsFilename}';\n`; themeFileContent += `let needsReloadOnChanges = false;\n`; @@ -222,6 +224,11 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) { const removers = []; if (target !== document) { ${shadowOnlyCss.join('')} + ${autoInjectGlobalCssImports ? ` + webcomponentGlobalCssInjector((css) => { + removers.push(injectGlobalCss(css, '', target)); + }); + ` : ''} } ${parentTheme} ${globalCssCode.join('')} @@ -232,7 +239,7 @@ function writeThemeFiles(themeFolder, themeName, themeProperties, options) { } } - + `; componentsFileContent += ` ${componentCssImports.join('')} diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java index 4d9acf78a0b..5c8f204518c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/AbstractUpdateImportsTest.java @@ -443,6 +443,45 @@ public void generate_embeddedImports_doNotContainLumoGlobalThemeFiles() } + @Test + public void generate_embeddedImports_addAlsoGlobalStyles() + throws IOException { + Class[] testClasses = { FooCssImport.class, FooCssImport2.class, + UI.class, AllEagerAppConf.class }; + ClassFinder classFinder = getClassFinder(testClasses); + updater = new UpdateImports(classFinder, getScanner(classFinder), + options); + updater.run(); + + Pattern injectGlobalCssPattern = Pattern + .compile("^\\s*injectGlobalCss\\(([^,]+),.*"); + Predicate globalCssImporter = injectGlobalCssPattern + .asPredicate(); + + List globalCss = updater.getOutput() + .get(updater.generatedFlowImports).stream() + .filter(globalCssImporter).map(line -> { + Matcher matcher = injectGlobalCssPattern.matcher(line); + matcher.find(); + return matcher.group(1); + }).collect(Collectors.toList()); + + assertTrue("Import for web-components should also inject global CSS", + updater.webComponentImports.stream() + .anyMatch(globalCssImporter)); + + assertTrue( + "Should contain function to import global CSS into embedded component", + updater.webComponentImports.stream().anyMatch(line -> line + .contains("import { injectGlobalWebcomponentCss }"))); + globalCss.forEach(css -> assertTrue( + "Should register global CSS " + css + " for webcomponent", + updater.webComponentImports.stream() + .anyMatch(line -> line.contains( + "injectGlobalWebcomponentCss(" + css + ");")))); + + } + @Test public void jsModulesOrderIsPreservedAnsAfterJsModules() { updater.run(); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java index d9adeda4a5e..daac37dd925 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/UpdateImportsWithByteCodeScannerTest.java @@ -317,6 +317,7 @@ public void cssInLazyChunkWorks() throws Exception { assertOnce("import { injectGlobalCss } from", chunkLines); assertOnce("from 'Frontend/foo.css?inline';", chunkLines); assertOnce("import $cssFromFile_0 from", chunkLines); + assertOnce("injectGlobalCss($cssFromFile_0", chunkLines); // assert lines order is preserved Assert.assertEquals( diff --git a/flow-tests/test-embedding/test-embedding-application-theme/frontend/css-import-component.css b/flow-tests/test-embedding/test-embedding-application-theme/frontend/css-import-component.css new file mode 100644 index 00000000000..60394390016 --- /dev/null +++ b/flow-tests/test-embedding/test-embedding-application-theme/frontend/css-import-component.css @@ -0,0 +1,3 @@ +DIV.cssimport { + color: gold +} diff --git a/flow-tests/test-embedding/test-embedding-application-theme/frontend/themes/embedded-theme/theme.json b/flow-tests/test-embedding/test-embedding-application-theme/frontend/themes/embedded-theme/theme.json index 12898b8b6dc..3479a8c1278 100644 --- a/flow-tests/test-embedding/test-embedding-application-theme/frontend/themes/embedded-theme/theme.json +++ b/flow-tests/test-embedding/test-embedding-application-theme/frontend/themes/embedded-theme/theme.json @@ -1,4 +1,5 @@ { + "autoInjectGlobalCssImports": true, "documentCss": ["@fortawesome/fontawesome-free/css/all.css"], "assets": { "@fortawesome/fontawesome-free": { diff --git a/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/CssImportComponent.java b/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/CssImportComponent.java new file mode 100644 index 00000000000..235307a3471 --- /dev/null +++ b/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/CssImportComponent.java @@ -0,0 +1,34 @@ +/* + * 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.webcomponent; + +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.dependency.CssImport; +import com.vaadin.flow.component.html.Div; + +@Tag("css-import-component") +@CssImport("./css-import-component.css") +public class CssImportComponent extends Div { + + public CssImportComponent(String id) { + setId(id); + Div div = new Div( + "Global CssImport styles should be applied inside embedded web component, this should not be black"); + div.setClassName("cssimport"); + add(div); + } +} diff --git a/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/ThemedComponent.java b/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/ThemedComponent.java index 25c08426c90..507401aaf29 100644 --- a/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/ThemedComponent.java +++ b/flow-tests/test-embedding/test-embedding-application-theme/src/main/java/com/vaadin/flow/webcomponent/ThemedComponent.java @@ -18,7 +18,6 @@ import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; - import com.vaadin.flow.uitest.ui.dependencies.TestVersion; @NpmPackage(value = "@fortawesome/fontawesome-free", version = TestVersion.FONTAWESOME) @@ -27,6 +26,7 @@ public class ThemedComponent extends Div { public static final String TEST_TEXT_ID = "test-text"; public static final String MY_COMPONENT_ID = "field"; + public static final String CSS_IMPORT_COMPONENT_ID = "embedded-cssimport"; public static final String EMBEDDED_ID = "embedded"; public static final String HAND_ID = "sparkle-hand"; @@ -45,5 +45,6 @@ public ThemedComponent() { add(new Div()); add(new MyComponent().withId(MY_COMPONENT_ID)); + add(new CssImportComponent(CSS_IMPORT_COMPONENT_ID)); } } diff --git a/flow-tests/test-embedding/test-embedding-application-theme/src/main/webapp/index.html b/flow-tests/test-embedding/test-embedding-application-theme/src/main/webapp/index.html index a9dbc13515c..45f81992bdc 100644 --- a/flow-tests/test-embedding/test-embedding-application-theme/src/main/webapp/index.html +++ b/flow-tests/test-embedding/test-embedding-application-theme/src/main/webapp/index.html @@ -1,17 +1,20 @@ - - - - - - - -

Lumo styles should not be applied

-
Internal should not apply, this should be black
-
Document styles should apply, this should be blue
- - - - - - + + + + + +

Lumo styles should not be applied

+
+ Internal should not apply, this should be black +
+
+ CssImport styles should apply, this should not be black +
+
+ Document styles should apply, this should be blue +
+ + + + diff --git a/flow-tests/test-embedding/test-embedding-application-theme/src/test/java/com/vaadin/flow/webcomponent/ApplicationThemeComponentIT.java b/flow-tests/test-embedding/test-embedding-application-theme/src/test/java/com/vaadin/flow/webcomponent/ApplicationThemeComponentIT.java index a8b403c61cf..94c9bfb6f53 100644 --- a/flow-tests/test-embedding/test-embedding-application-theme/src/test/java/com/vaadin/flow/webcomponent/ApplicationThemeComponentIT.java +++ b/flow-tests/test-embedding/test-embedding-application-theme/src/test/java/com/vaadin/flow/webcomponent/ApplicationThemeComponentIT.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.stream.Collectors; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.testbench.DivElement; import com.vaadin.flow.component.html.testbench.H1Element; import com.vaadin.flow.component.html.testbench.SpanElement; @@ -103,6 +104,14 @@ private void validateEmbeddedComponent(TestBenchElement themedComponent, Assert.assertEquals("Color should have been applied", "rgba(0, 128, 0, 1)", handElement.getCssValue("color")); + + // Ensure @CssImport styles are applied + final WebElement cssImportElement = embeddedComponent + .$("css-import-component").first().$(DivElement.class).first(); + Assert.assertEquals( + "Color fom CSSImport annotation should have been applied", + "rgba(255, 215, 0, 1)", cssImportElement.getCssValue("color")); + } @Test @@ -223,8 +232,8 @@ public void multipleSameEmbedded_cssTargetingDocumentShouldOnlyAddElementsOneTim 2l, getCommandExecutor().executeScript( "return document.head.querySelectorAll('link[rel=stylesheet][href^=\"https://fonts.googleapis.com\"]').length")); Assert.assertEquals( - "Project contains 2 css injections to document and both should be hashed", - 2l, getCommandExecutor().executeScript( + "Project contains 3 css injections to document and all should be hashed", + 3l, getCommandExecutor().executeScript( "return window.Vaadin.theme.injectedGlobalCss.length")); } @@ -246,4 +255,22 @@ public void lumoImports_doNotLeakEmbeddingPage() { "rgba(0, 0, 0, 1)", element.getCssValue("color")); } + + @Test + public void cssImportAnnotation_applyToEmbeddingPage() { + open(); + checkLogsForErrors(); + + // Ensure embedded components are loaded before testing embedding page + validateEmbeddedComponent($("themed-component").id("first"), "first"); + validateEmbeddedComponent($("themed-component").id("second"), "second"); + + final DivElement element = $(DivElement.class) + .attribute("id", "cssimport").waitForFirst(); + Assert.assertEquals( + "CssImport styles (colors) should have been applied to elements in embedding page", + "rgba(255, 215, 0, 1)", element.getCssValue("color")); + + } + }