diff --git a/.github/workflows/ssr_tests.js.yml b/.github/workflows/ssr_tests.js.yml
index 1b188eaa2..a5944d604 100644
--- a/.github/workflows/ssr_tests.js.yml
+++ b/.github/workflows/ssr_tests.js.yml
@@ -1,11 +1,12 @@
-# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
-# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: SSR Tests
on:
- push:
- branches: [next]
+ # push:
+ # branches: [next]
+ workflow_dispatch:
jobs:
ssr_test:
diff --git a/docs/_includes/base.njk b/docs/_includes/base.njk
index 6cbfea2de..21bfd33a6 100644
--- a/docs/_includes/base.njk
+++ b/docs/_includes/base.njk
@@ -1,5 +1,5 @@
-
+
{% include 'head.njk' %}
diff --git a/docs/_includes/head.njk b/docs/_includes/head.njk
index 89a303d24..5a9808f7b 100644
--- a/docs/_includes/head.njk
+++ b/docs/_includes/head.njk
@@ -23,10 +23,9 @@
-
{# Web Awesome #}
-
+
{# Preset Theme #}
diff --git a/docs/assets/scripts/turbo.js b/docs/assets/scripts/turbo.js
index fb1d0ea0f..e00e470c3 100644
--- a/docs/assets/scripts/turbo.js
+++ b/docs/assets/scripts/turbo.js
@@ -1,3 +1,6 @@
+import 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
+import { preventTurboFouce } from '/dist/webawesome.js';
+
if (!window.___turboScrollPositions___) {
window.___turboScrollPositions___ = {};
}
@@ -70,3 +73,4 @@ function fixDSD(e) {
window.addEventListener('turbo:before-cache', saveScrollPosition);
window.addEventListener('turbo:before-render', restoreScrollPosition);
window.addEventListener('turbo:render', restoreScrollPosition);
+preventTurboFouce();
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
index 473e4ab1f..b142497bc 100644
--- a/docs/docs/installation.md
+++ b/docs/docs/installation.md
@@ -37,10 +37,6 @@ This snippet includes three parts:
Now you can [start using Web Awesome!](/docs/usage)
-:::info
-While convenient, autoloading may lead to a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/). The linked article describes some ways to alleviate it.
-:::
-
---
## Using Font Awesome Kit Codes
diff --git a/docs/docs/resources/changelog.md b/docs/docs/resources/changelog.md
index 633e61af2..474aa3600 100644
--- a/docs/docs/resources/changelog.md
+++ b/docs/docs/resources/changelog.md
@@ -14,6 +14,8 @@ During the alpha period, things might break! We take breaking changes very serio
## Next
+- Added the `wa-cloak` utility to prevent FOUCE
+- Added the `allDefined()` utility for awaiting component registration
- Fixed `wa-pill` class for text fields
- Fixed `pill` style for `` elements
- Fixed a bug in `` that prevented light dismiss from working when clicking immediately above the color picker dropdown
diff --git a/docs/docs/usage.md b/docs/docs/usage.md
index aea75ebc4..651ade28e 100644
--- a/docs/docs/usage.md
+++ b/docs/docs/usage.md
@@ -8,6 +8,30 @@ Web Awesome components are just regular HTML elements, or [custom elements](http
If you're new to custom elements, often referred to as "web components," this section will familiarize you with how to use them.
+## Awaiting Registration
+
+Unlike traditional frameworks, custom elements don't have a centralized initialization phase. This means you need to verify that a custom element has been properly registered before attempting to interact with its properties or methods.
+
+You can use the [`customElements.whenDefined()`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined) method to ensure a specific component is ready:
+
+```ts
+await customElements.whenDefined('wa-button');
+
+// is ready to use!
+const button = document.querySelector('wa-button');
+```
+
+When working with multiple components, checking each one individually can become tedious. For convenience, Web Awesome provides the `allDefined()` function which automatically detects and waits for all Web Awesome components in the DOM to be initialized before resolving.
+
+```ts
+import { allDefined } from '/dist/webawesome.js';
+
+// Waits for all Web Awesome components in the DOM to be registered
+await allDefined();
+
+// All Web Awesome components on the page are ready!
+```
+
## Attributes & Properties
Many components have properties that can be set using attributes. For example, buttons accept a `size` attribute that maps to the `size` property which dictates the button's size.
diff --git a/docs/docs/utilities/fouce.md b/docs/docs/utilities/fouce.md
new file mode 100644
index 000000000..e80173065
--- /dev/null
+++ b/docs/docs/utilities/fouce.md
@@ -0,0 +1,32 @@
+---
+title: Reduce FOUCE
+description: Utility to improve the loading experience by hiding non-prerendered custom elements until they are registered.
+file: styles/utilities/fouce.css
+icon: spinner
+---
+
+While convenient, autoloading can lead to a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
+The [FOUCE style utility](/docs/utilities/fouce/#opting-in) (which is automatically applied if you use the [Web Awesome utilities](/docs/utilities/)) takes care of hiding custom elements until they and their contents have been registered, up to a maximum of two seconds.
+
+In many cases, this is not enough, and you may wish to hide a broader wrapper element or even the entire page until all WA elements within it have loaded. To do that, you can add the `wa-reduce-fouce` class to any element on the page or even apply it to the whole page by placing the class on the `` element.
+
+```html
+
+ ...
+
+```
+
+As soon as all elements are registered _or_ after two seconds have elapsed, the autoloader will show the page. The two-second timeout prevents blank screens from persisting on slow networks and pages that have errors.
+
+:::details Are you using Turbo in your app?
+
+If you're using [Turbo](https://turbo.hotwired.dev/) to serve a multi-page application (MPA) as a single page application (SPA), you might notice FOUCE when navigating from page to page. This is because Turbo renders the new page's content before the autoloader has a change to register new components.
+
+The following function acts as a middleware to ensure components are registered _before_ the page shows, eliminating FOUCE for page-to-page navigation with Turbo.
+
+```js
+import { preventTurboFouce } from '/dist/webawesome.js';
+
+preventTurboFouce();
+```
+:::
diff --git a/docs/docs/utilities/fouce.njk b/docs/docs/utilities/fouce.njk
deleted file mode 100644
index 23669b9dc..000000000
--- a/docs/docs/utilities/fouce.njk
+++ /dev/null
@@ -1,131 +0,0 @@
----
-title: Reduce FOUCE
-description: Utility to improve the loading experience by hiding non-prerendered custom elements until they are registered.
-file: styles/utilities/fouce.css
-icon: spinner
----
-{% markdown %}
-No class is needed to use this utility, it will be applied automatically as long as it its CSS is included.
-
-Here is a comparison of the loading experience with and without this utility,
-with a simulated slow loading time:
-
-{% endmarkdown %}
-
-
-
- Normal loading
-
-
- Refresh
-
- With FOUCE reduction
-
-
-
-{% set sample_card %}
-
-
-
-
-
-
-
-
- Mittens
- This kitten is as cute as he is playful. Bring him home today!
- 6 weeks old
-
-
- More Info
-
-
-
-
-
-{% endset %}
-
-
-
-
-
-
-
-
-{% markdown %}
-## How does it work?
-
-The utility consists of a timeout (`2s` by default) and a fade duration (`200ms` by default).
-- If the element is _ready_ before the timeout, it will appear immediately.
-- If it takes longer than _timeout_ + _fade_, it will fade in over the fade duration.
-- If it takes somewhere between _timeout_ and _timeout_ + _fade_, you will get an interrupted fade.
-
-An element is considered ready when both of these are true:
-1. Either It has been registered or has a `did-ssr` attribute (indicating it was pre-rendered)
-2. If it’s a Web Awesome component, its contents are also ready
-
-## Customization
-
-You can use the following CSS variables to customize the behavior:
-
-| Variable | Description | Default |
-| --- | --- | --- |
-| `--wa-fouce-fade` | The transition duration for the fade effect. | `200ms` |
-| `--wa-fouce-timeout` | The timeout after which elements will appear even if not registered | `2s` |
-
-The fade duration cannot be longer than the timeout.
-This means that you can disable FOUCE reduction on an element by setting `--wa-fouce-timeout: 0s`.
-
-For example, if instead of `did-ssr` you used an `ssr` attribute to mark elements that were pre-rendered, you can do this to get them to appear immediately:
-
-```css
-[ssr] {
- --wa-fouce-timeout: 0s;
-}
-```
-
-You can also opt-out from FOUCE reduction for an element and its contents by adding the `.wa-fouce-off` class to it.
-Applying this class to the root element will disable the utility for the entire page.
-{% endmarkdown %}
diff --git a/src/styles/utilities/fouce.css b/src/styles/utilities/fouce.css
index 75e44d807..84ab08e48 100644
--- a/src/styles/utilities/fouce.css
+++ b/src/styles/utilities/fouce.css
@@ -1,20 +1,17 @@
/*
* Utility to minimize FOUCE and show custom elements only after they're registered
*/
+:not(:defined),
+:state(wa-defined):has(:not(:defined)),
+.wa-cloak:has(:not(:defined)) {
+ animation: 2s step-end wa-fouce-cloak;
+}
-@keyframes wa-fade-in {
+@keyframes wa-fouce-cloak {
from {
opacity: 0;
}
-}
-
-:not(:defined),
-:state(wa-defined):has(:not(:defined)) {
- /* The clamp() ensures that if --wa-fouce-timeout is set to 0s, the whole effect is disabled */
- --wa-fouce-animation: clamp(0s, var(--wa-fouce-fade, 200ms), var(--wa-fouce-timeout, 2s)) var(--wa-fouce-timeout, 2s)
- wa-fade-in both;
-
- &:where(:not([did-ssr], .wa-fouce-off, .wa-fouce-off *)) {
- animation: var(--wa-fouce-animation);
+ to {
+ opacity: 1;
}
}
diff --git a/src/utilities/autoloader.ts b/src/utilities/autoloader.ts
index 4ff6463ad..c40127ce5 100644
--- a/src/utilities/autoloader.ts
+++ b/src/utilities/autoloader.ts
@@ -50,6 +50,12 @@ export async function discover(root: Element | ShadowRoot) {
console.warn(imp.reason); // eslint-disable-line no-console
}
}
+
+ // Wait a cycle to allow the first Lit update to run
+ await new Promise(requestAnimationFrame);
+
+ // Dispatch an event when discovery is complete.
+ document.dispatchEvent(new CustomEvent('wa-discovery-complete'));
}
/**
@@ -69,3 +75,22 @@ function register(tagName: string): Promise {
import(path).then(() => resolve()).catch(() => reject(new Error(`Unable to autoload <${tagName}> from ${path}`)));
});
}
+
+/**
+ * Acts as a middleware for Turbo's `turbo:before-render` event to ensure components are auto-loaded before showing the
+ * next page, eliminating page-to-page FOUCE in a Turbo environment.
+ */
+export function preventTurboFouce(timeout = 2000) {
+ document.addEventListener('turbo:before-render', async (event: CustomEvent) => {
+ const newBody = event.detail.newBody;
+
+ event.preventDefault();
+
+ try {
+ // Wait until all elements are registered or two seconds, whichever comes first
+ await Promise.race([discover(newBody), new Promise(resolve => setTimeout(resolve, timeout))]);
+ } finally {
+ event.detail.resume();
+ }
+ });
+}
diff --git a/src/utilities/defined.ts b/src/utilities/defined.ts
new file mode 100644
index 000000000..277693085
--- /dev/null
+++ b/src/utilities/defined.ts
@@ -0,0 +1,64 @@
+interface AllDefinedOptions {
+ /**
+ * A callback that accepts a custom element tag name and returns `true` if the custom element should be defined before
+ * resolving or `false` to skip it. The tag name is always in lowercase.
+ */
+ match: (tagName: string) => boolean;
+
+ /**
+ * To wait for additional custom elements that may not be on the page when the function is called, provide them here.
+ */
+ additionalElements: string | string[];
+
+ /**
+ * The root in which to look for custom elements. Defaults to `document`. By design, shadow roots are not traversed,
+ * but you can call this function and set `root` to a custom element's shadow root if needed.
+ */
+ root: Document | ShadowRoot;
+}
+
+/**
+ * Waits for custom elements that are currently on the page to be registered before resolving. This is sugar for
+ * awaiting `customElements.whenDefined()` multiple times. By default, the function waits for all undefined Web Awesome
+ * elements, but you can pass a custom match function to wait for other custom elements instead.
+ *
+ * The function returns with `Promise.all()`, so any loading errors will cause it to reject. Make sure you handle errors
+ * accordingly using a try/catch block or a `.catch()`.
+ *
+ * @example
+ * // Wait for Web Awesome elements
+ * await allDefined();
+ *
+ * // Wait for all custom elements that start with `foo-` as well as the `` element
+ * await allDefined({
+ * match: tagName => tagName.startsWith('foo-'),
+ * additionalElements: ['bar-button', 'baz-dialog']
+ * });
+ */
+export async function allDefined(options?: Partial) {
+ const opts: AllDefinedOptions = {
+ match: tagName => tagName.startsWith('wa-'),
+ additionalElements: [],
+ root: document,
+ ...options,
+ };
+
+ // Ensure additional elements is an array
+ const additionalElements = Array.isArray(opts.additionalElements)
+ ? opts.additionalElements
+ : [opts.additionalElements];
+
+ // Discover undefined elements in the document
+ const undefinedElements = [...opts.root.querySelectorAll(':not(:defined)')]
+ .map(el => el.localName)
+ .filter((tag, index, arr) => arr.indexOf(tag) === index) // make it unique
+ .filter(tag => opts.match(tag)); // make sure it matches
+
+ const tagsToAwait = [...undefinedElements, ...additionalElements];
+
+ // Wait for all to be registered
+ await Promise.all(tagsToAwait.map(tag => customElements.whenDefined(tag)));
+
+ // Wait a cycle for the first update
+ await new Promise(requestAnimationFrame);
+}
diff --git a/src/webawesome.ts b/src/webawesome.ts
index ae2a4bee2..287f9d02a 100644
--- a/src/webawesome.ts
+++ b/src/webawesome.ts
@@ -1,6 +1,7 @@
export { registerIconLibrary, unregisterIconLibrary } from './components/icon/library.js';
-export { discover, startLoader, stopLoader } from './utilities/autoloader.js';
+export { discover, preventTurboFouce, startLoader, stopLoader } from './utilities/autoloader.js';
export { getBasePath, getKitCode, setBasePath, setKitCode } from './utilities/base-path.js';
+export { allDefined } from './utilities/defined.js';
export { registerTranslation } from './utilities/localize.js';
// Utilities
diff --git a/web-test-runner.config.js b/web-test-runner.config.js
index 0ce1a4570..599e2f43e 100644
--- a/web-test-runner.config.js
+++ b/web-test-runner.config.js
@@ -1,4 +1,3 @@
-import { litSsrPlugin } from '@lit-labs/testing/web-test-runner-ssr-plugin.js';
import { esbuildPlugin } from '@web/dev-server-esbuild';
import { playwrightLauncher } from '@web/test-runner-playwright';
import { readFileSync } from 'fs';
@@ -53,7 +52,6 @@ export default {
ts: true,
target: 'es2020',
}),
- litSsrPlugin(),
],
browsers: [
playwrightLauncher({ product: 'chromium', concurrency }),
@@ -78,12 +76,10 @@ export default {
${componentImports.map(str => `"${str}"`).join(',\n')}
]
- window.SSR_ONLY = ${process.env['SSR_ONLY'] === 'true'}
window.CSR_ONLY = ${process.env['CSR_ONLY'] === 'true'}