mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-13 12:39:14 +00:00
Compare commits
31 Commits
palette-re
...
alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
734417d66b | ||
|
|
2cfd651d2f | ||
|
|
3e2d1b98be | ||
|
|
40f332f37c | ||
|
|
bfda64f690 | ||
|
|
883d6df2ef | ||
|
|
b4240fd321 | ||
|
|
8755a834f6 | ||
|
|
8d905296b8 | ||
|
|
8eba1e5003 | ||
|
|
21aa85acc0 | ||
|
|
404c15b303 | ||
|
|
8a26afc334 | ||
|
|
513a1e35a9 | ||
|
|
09f668fc99 | ||
|
|
d451ba98e5 | ||
|
|
fd287edd56 | ||
|
|
8424b49646 | ||
|
|
fa24c0f70e | ||
|
|
1bba87c66d | ||
|
|
0db9ca12e3 | ||
|
|
041555fe99 | ||
|
|
b41dbd2de7 | ||
|
|
7c6f31e0c7 | ||
|
|
9e84274a93 | ||
|
|
2b3803f91e | ||
|
|
faed8da3cd | ||
|
|
17cf902f53 | ||
|
|
8214ff6b2d | ||
|
|
c9979e15f8 | ||
|
|
fcfe2bde7d |
9
.github/workflows/ssr_tests.js.yml
vendored
9
.github/workflows/ssr_tests.js.yml
vendored
@@ -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:
|
||||
|
||||
@@ -7,10 +7,10 @@ import { highlightCodePlugin } from './_utils/highlight-code.js';
|
||||
import { markdown } from './_utils/markdown.js';
|
||||
import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js';
|
||||
// import { formatCodePlugin } from './_utils/format-code.js';
|
||||
import litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||
// import litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||
import { readFile } from 'fs/promises';
|
||||
import nunjucks from 'nunjucks';
|
||||
import componentList from './_data/componentList.js';
|
||||
// import componentList from './_data/componentList.js';
|
||||
import * as filters from './_utils/filters.js';
|
||||
import { outlinePlugin } from './_utils/outline.js';
|
||||
import { replaceTextPlugin } from './_utils/replace-text.js';
|
||||
@@ -29,7 +29,6 @@ const globalData = {
|
||||
package: packageData,
|
||||
isAlpha,
|
||||
layout: 'page.njk',
|
||||
|
||||
server: {
|
||||
head: '',
|
||||
loginOrAvatar: '',
|
||||
@@ -190,30 +189,30 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy(glob);
|
||||
}
|
||||
|
||||
// SSR plugin
|
||||
// Make sure this is the last thing, we dont want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
|
||||
if (!isDev) {
|
||||
//
|
||||
// Problematic components in SSR land:
|
||||
// - animation (breaks on navigation + ssr with Turbo)
|
||||
// - mutation-observer (why SSR this?)
|
||||
// - resize-observer (why SSR this?)
|
||||
// - tooltip (why SSR this?)
|
||||
//
|
||||
const omittedModules = [];
|
||||
const componentModules = componentList
|
||||
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||
.map(component => {
|
||||
const name = component.tagName.split(/wa-/)[1];
|
||||
const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
|
||||
return path.join(componentDirectory, 'components', name, `${name}.js`);
|
||||
});
|
||||
|
||||
eleventyConfig.addPlugin(litPlugin, {
|
||||
mode: 'worker',
|
||||
componentModules,
|
||||
});
|
||||
}
|
||||
// // SSR plugin
|
||||
// // Make sure this is the last thing, we don't want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
|
||||
// if (!isDev) {
|
||||
// //
|
||||
// // Problematic components in SSR land:
|
||||
// // - animation (breaks on navigation + ssr with Turbo)
|
||||
// // - mutation-observer (why SSR this?)
|
||||
// // - resize-observer (why SSR this?)
|
||||
// // - tooltip (why SSR this?)
|
||||
// //
|
||||
// const omittedModules = [];
|
||||
// const componentModules = componentList
|
||||
// .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||
// .map(component => {
|
||||
// const name = component.tagName.split(/wa-/)[1];
|
||||
// const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
|
||||
// return path.join(componentDirectory, 'components', name, `${name}.js`);
|
||||
// });
|
||||
//
|
||||
// eleventyConfig.addPlugin(litPlugin, {
|
||||
// mode: 'worker',
|
||||
// componentModules,
|
||||
// });
|
||||
// }
|
||||
|
||||
return {
|
||||
markdownTemplateEngine: 'njk',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
|
||||
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}" class="wa-cloak">
|
||||
<head>
|
||||
{% include 'head.njk' %}
|
||||
<meta name="theme-color" content="#f36944">
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<section id="grid" class="index-grid">
|
||||
{% set groupedPages = allPages | groupPages(categories, page) %}
|
||||
{% for category, pages in groupedPages -%}
|
||||
{% if groupedPages.meta.groupCount > 1 %}
|
||||
<h2 class="index-category">
|
||||
{% if groupedPages.meta.groupCount > 1 and pages.length > 0 %}
|
||||
<h2 class="index-category" id="{{ category | slugify }}">
|
||||
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
|
||||
{% else %}
|
||||
{{ pages.meta.title }}
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
<script src="/assets/scripts/hydration-errors.js"></script>
|
||||
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
|
||||
|
||||
{# Internal components #}
|
||||
<script type="module" src="/assets/components/scoped.js"></script>
|
||||
|
||||
{# Web Awesome #}
|
||||
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
|
||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
||||
|
||||
<script type="module" src="/assets/scripts/theme-picker.js"></script>
|
||||
{# Preset Theme #}
|
||||
@@ -48,6 +50,5 @@
|
||||
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
|
||||
<link rel="stylesheet" href="/dist/styles/forms.css" />
|
||||
|
||||
|
||||
{# Used by Web Awesome App to inject other assets into the head. #}
|
||||
{% server "head" %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if not (isAlpha and page.data.noAlpha) and not page.data.unlisted -%}
|
||||
{% if page | show -%}
|
||||
<li>
|
||||
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
||||
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# Getting started #}
|
||||
<h2>Getting Started</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/installation">Installation</a></li>
|
||||
<li><a href="/docs/">Installation</a></li>
|
||||
<li><a href="/docs/usage">Usage</a></li>
|
||||
<li><a href="/docs/customizing">Customizing</a></li>
|
||||
<li><a href="/docs/form-controls">Form Controls</a></li>
|
||||
|
||||
@@ -1,31 +1,20 @@
|
||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
||||
{% set suffixes = ['-80', '', '-20'] %}
|
||||
{% set width = 20 %}
|
||||
{% set height = 12 %}
|
||||
{% set height_core = 20 %}
|
||||
{% set gap_x = 4 %}
|
||||
{% set gap_y = 4 %}
|
||||
|
||||
{% set total_width = (width + gap_x) * hues|length %}
|
||||
{% set total_height = (height + gap_y) * suffixes|length + (height_core - height) %}
|
||||
<svg viewBox="0 0 {{ total_width }} {{ total_height }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
|
||||
<style>
|
||||
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
|
||||
.palette-icon {
|
||||
height: 8ch;
|
||||
}
|
||||
</style>
|
||||
<wa-scoped class="palette-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/color/{{ paletteId }}.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
{% for hue in hues -%}
|
||||
{% set hueIndex = loop.index0 %}
|
||||
{% set y = 0 %}
|
||||
{% for suffix in suffixes -%}
|
||||
{% set swatch_height = height if suffix else height_core %}
|
||||
|
||||
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ y }}"
|
||||
width="{{ width }}" height="{{ swatch_height }}"
|
||||
fill="var(--wa-color-{{ hue }}{{ suffix }})" rx="2" />
|
||||
{% set y = y + swatch_height + gap_y %}
|
||||
{%- endfor %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
<div class="palette-icon" style="--hues: {{ hues|length }}; --suffixes: {{ suffixes|length }}">
|
||||
{% for hue in hues -%}
|
||||
{% set hueIndex = loop.index %}
|
||||
{% for suffix in suffixes -%}
|
||||
<div class="swatch"
|
||||
data-hue="{{ hue }}" data-suffix="{{ suffix }}"
|
||||
style="--color: var(--wa-color-{{ hue }}{{ suffix }}); grid-column: {{ hueIndex }}; grid-row: {{ loop.index }}"> </div>
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</template>
|
||||
</wa-scoped>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<wa-scoped class="theme-icon-host theme-color-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/color.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-color-icon wa-theme-{{ themeId }}">
|
||||
<div class="theme-icon theme-color-icon wa-theme-{{ themeId }}">
|
||||
<div class="wa-brand wa-accent">A</div>
|
||||
<div class="wa-brand wa-outlined">A</div>
|
||||
<div class="wa-brand wa-filled">A</div>
|
||||
@@ -21,4 +21,4 @@
|
||||
{# <div class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</wa-scoped>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<wa-scoped class="theme-icon-host theme-typography-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/blockquote.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<div class="theme-icon theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<h3>Title</h3>
|
||||
<p>Body text</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</wa-scoped>
|
||||
|
||||
29
docs/_includes/svgs/theme.njk
Normal file
29
docs/_includes/svgs/theme.njk
Normal file
@@ -0,0 +1,29 @@
|
||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
||||
|
||||
|
||||
<wa-scoped class="theme-icon-host theme-overall-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-icon theme-overall-icon" role="presentation" data-no-anchor data-no-outline>
|
||||
<div class="row row-1">
|
||||
<h2>Aa</h2>
|
||||
<div class="swatches">
|
||||
<div class="wa-brand"></div>
|
||||
|
||||
<div class="wa-success"></div>
|
||||
<div class="wa-warning"></div>
|
||||
<div class="wa-danger"></div>
|
||||
<div class="wa-neutral"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-2">
|
||||
<wa-input value="Input" size="small" inert></wa-input>
|
||||
<wa-button size="small" variant="brand" inert>Go</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</wa-scoped>
|
||||
@@ -178,6 +178,10 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function show(page) {
|
||||
return !(page.data.noAlpha && page.data.isAlpha) && !page.data.unlisted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
|
||||
* @param {object[]} collection
|
||||
@@ -198,7 +202,7 @@ export function groupPages(collection, options = {}, page) {
|
||||
options = { tags: options };
|
||||
}
|
||||
|
||||
let { tags, groups, titles = {}, other = 'Other' } = options;
|
||||
let { tags, groups, titles = {}, other = 'Other', filter = show } = options;
|
||||
|
||||
if (groups === undefined && Array.isArray(tags)) {
|
||||
groups = tags;
|
||||
@@ -237,6 +241,10 @@ export function groupPages(collection, options = {}, page) {
|
||||
let byUrl = {};
|
||||
let byParentUrl = {};
|
||||
|
||||
if (filter) {
|
||||
collection = collection.filter(filter);
|
||||
}
|
||||
|
||||
for (let item of collection) {
|
||||
let url = item.page.url;
|
||||
let parentUrl = item.data.parentUrl;
|
||||
@@ -313,6 +321,13 @@ export function groupPages(collection, options = {}, page) {
|
||||
|
||||
if (sortedGroups) {
|
||||
ret = sortObject(ret, sortedGroups);
|
||||
} else {
|
||||
// At least make sure other is last
|
||||
if (ret.other) {
|
||||
let otherGroup = ret.other;
|
||||
delete ret.other;
|
||||
ret.other = otherGroup;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ret, 'meta', {
|
||||
|
||||
@@ -39,7 +39,7 @@ export function outlinePlugin(options = {}) {
|
||||
}
|
||||
|
||||
// Create a clone of the heading so we can remove links and [data-no-outline] elements from the text content
|
||||
clone.querySelectorAll('a').forEach(a => a.remove());
|
||||
clone.querySelectorAll('.wa-visually-hidden, [hidden], [aria-hidden="true"]').forEach(el => el.remove());
|
||||
clone.querySelectorAll('[data-no-outline]').forEach(el => el.remove());
|
||||
|
||||
// Generate the link
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import lunr from 'lunr';
|
||||
import { parse } from 'node-html-parser';
|
||||
import * as path from 'path';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
function collapseWhitespace(string) {
|
||||
@@ -52,8 +53,9 @@ export function searchPlugin(options = {}) {
|
||||
return content;
|
||||
});
|
||||
|
||||
eleventyConfig.on('eleventy.after', ({ dir }) => {
|
||||
const outputFilename = join(dir.output, 'search.json');
|
||||
eleventyConfig.on('eleventy.after', ({ directories }) => {
|
||||
const { output } = directories;
|
||||
const outputFilename = path.resolve(join(output, 'search.json'));
|
||||
const map = [];
|
||||
const searchIndex = lunr(async function () {
|
||||
let index = 0;
|
||||
|
||||
171
docs/assets/components/scoped.js
Normal file
171
docs/assets/components/scoped.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Low-level utility to encapsulate a bit of HTML (mainly to apply certain stylesheets to it without them leaking to the rest of the page)
|
||||
* Usage: <wa-scoped><template><!-- your HTML here --></template></wa-scoped>
|
||||
*/
|
||||
import { discover } from '/dist/webawesome.js';
|
||||
|
||||
const imports = new Set();
|
||||
const fontFaceRules = new Set();
|
||||
|
||||
export default class WaScoped extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.observer = new MutationObserver(() => this.render());
|
||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.ownerDocument.documentElement.addEventListener('wa-color-scheme-change', e =>
|
||||
this.#applyDarkMode(e.detail.dark),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.observer.takeRecords();
|
||||
this.observer.disconnect();
|
||||
|
||||
this.shadowRoot.innerHTML = '';
|
||||
|
||||
// To avoid mutating this.childNodes while iterating over it
|
||||
let nodes = [];
|
||||
|
||||
for (let template of this.childNodes) {
|
||||
// Other solutions we can try if needed: <script type="text/html">, or comment nodes
|
||||
if (template instanceof HTMLTemplateElement) {
|
||||
if (template.content.childNodes.length > 0) {
|
||||
nodes.push(template.content.cloneNode(true));
|
||||
} else if (template.childNodes.length > 0) {
|
||||
// Fake template, suck its children out of the light DOM
|
||||
nodes.push(...template.childNodes);
|
||||
}
|
||||
} else {
|
||||
// Regular child, suck it out of the light DOM
|
||||
nodes.push(template);
|
||||
}
|
||||
}
|
||||
|
||||
this.shadowRoot.append(...nodes);
|
||||
|
||||
this.#fixStyles();
|
||||
this.#applyDarkMode();
|
||||
|
||||
discover(this.shadowRoot);
|
||||
|
||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
#applyDarkMode(isDark = getComputedStyle(this).colorScheme === 'dark') {
|
||||
// Hack to make dark mode work
|
||||
// NOTE If any child nodes actually have .wa-dark, this will override it
|
||||
for (let node of this.shadowRoot.children) {
|
||||
node.classList.toggle('wa-dark', isDark);
|
||||
}
|
||||
this.classList.toggle('wa-dark', isDark);
|
||||
}
|
||||
|
||||
/**
|
||||
* @font-face does not work in shadow DOM in Chrome & FF, as of March 2025 https://issues.chromium.org/issues/41085401
|
||||
* This works around this issue by traversing the shadow DOM CSS looking
|
||||
* for @font-face rules or CSS imports to known font providers and copies them to the main document
|
||||
*/
|
||||
async #fixStyles() {
|
||||
let styleElements = [...this.shadowRoot.querySelectorAll('link[rel="stylesheet"], style')];
|
||||
|
||||
let loadStates = styleElements.map(element => {
|
||||
try {
|
||||
if (element.sheet?.cssRules) {
|
||||
// Already loaded
|
||||
return Promise.resolve(element.sheet);
|
||||
}
|
||||
} catch (e) {
|
||||
// CORS
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
element.addEventListener('load', e => resolve(element.sheet));
|
||||
element.addEventListener('error', e => reject(null));
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.allSettled(loadStates);
|
||||
|
||||
let fontRules = findFontFaceRules(...this.shadowRoot.styleSheets);
|
||||
|
||||
if (!fontRules.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let doc = this.ownerDocument;
|
||||
// Why not adoptedStyleSheets? Can't have @import in those yet
|
||||
let id = `wa-scoped-hoisted-fonts`;
|
||||
let style = doc.head.querySelector('style#' + id);
|
||||
if (!style) {
|
||||
style = Object.assign(doc.createElement('style'), { id, textContent: ' ' });
|
||||
doc.head.append(style);
|
||||
}
|
||||
let sheet = style.sheet;
|
||||
|
||||
for (let rule of fontRules) {
|
||||
let cssText = rule.cssText;
|
||||
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
||||
if (fontFaceRules.has(cssText)) {
|
||||
continue;
|
||||
}
|
||||
fontFaceRules.add(cssText);
|
||||
sheet.insertRule(cssText);
|
||||
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
||||
if (imports.has(rule.href)) {
|
||||
continue;
|
||||
}
|
||||
imports.add(rule.href);
|
||||
sheet.insertRule(cssText, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static observedAttributes = [];
|
||||
}
|
||||
|
||||
customElements.define('wa-scoped', WaScoped);
|
||||
|
||||
export const WEB_FONT_HOSTS = [
|
||||
'fonts.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
'use.typekit.net',
|
||||
'fonts.adobe.com',
|
||||
'kit.fontawesome.com',
|
||||
'pro.fontawesome.com',
|
||||
'cdn.materialdesignicons.com',
|
||||
];
|
||||
|
||||
function findFontFaceRules(...stylesheets) {
|
||||
let ret = [];
|
||||
|
||||
for (let sheet of stylesheets) {
|
||||
let rules;
|
||||
try {
|
||||
rules = sheet.cssRules;
|
||||
} catch (e) {
|
||||
// CORS
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let rule of rules) {
|
||||
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
||||
ret.push(rule);
|
||||
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
||||
if (WEB_FONT_HOSTS.some(host => rule.href.includes(host))) {
|
||||
ret.push(rule);
|
||||
} else if (rule.styleSheet) {
|
||||
ret.push(...findFontFaceRules(rule.styleSheet));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ document.addEventListener('click', event => {
|
||||
const code = codeExample.querySelector('code');
|
||||
const cdnUrl = document.documentElement.dataset.cdnUrl;
|
||||
const html =
|
||||
`<script type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` +
|
||||
`<script data-fa-kit-code="b10bfbde90" type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` +
|
||||
`<link rel="stylesheet" href="${cdnUrl}styles/themes/default.css">\n` +
|
||||
`<link rel="stylesheet" href="${cdnUrl}styles/webawesome.css">\n` +
|
||||
`<link rel="stylesheet" href="${cdnUrl}styles/utilities.css">\n\n` +
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
function updateResults(input) {
|
||||
const filter = input.value.toLowerCase().trim();
|
||||
let filtered = Boolean(filter);
|
||||
@@ -18,8 +26,10 @@ function updateResults(input) {
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedUpdateResults = debounce(updateResults, 300);
|
||||
|
||||
document.documentElement.addEventListener('input', e => {
|
||||
if (e.target?.matches('#block-filter wa-input')) {
|
||||
updateResults(e.target);
|
||||
debouncedUpdateResults(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
let initialPageLoadComplete = document.readyState === 'complete';
|
||||
|
||||
if (!initialPageLoadComplete) {
|
||||
window.addEventListener('load', () => {
|
||||
initialPageLoadComplete = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for view transitions
|
||||
export function domChange(fn, { behavior = 'smooth' } = {}) {
|
||||
export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
|
||||
const canUseViewTransitions =
|
||||
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Skip transitions on initial page load
|
||||
if (!initialPageLoadComplete && ignoreInitialLoad) {
|
||||
fn(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canUseViewTransitions && behavior === 'smooth') {
|
||||
document.startViewTransition(fn);
|
||||
const transition = document.startViewTransition(() => {
|
||||
fn(true);
|
||||
// Wait a brief delay before finishing the transition to prevent jumpiness
|
||||
return new Promise(resolve => setTimeout(resolve, 200));
|
||||
});
|
||||
return transition;
|
||||
} else {
|
||||
fn(true);
|
||||
fn(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +120,7 @@ const colorScheme = new ThemeAspect({
|
||||
domChange(() => {
|
||||
let dark = this.computedValue === 'dark';
|
||||
document.documentElement.classList.toggle(`wa-dark`, dark);
|
||||
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import 'outline.css';
|
||||
@import 'search.css';
|
||||
@import 'cera_typeface.css';
|
||||
@import 'theme-icons.css';
|
||||
|
||||
:root {
|
||||
--wa-brand-orange: #f36944;
|
||||
@@ -370,10 +371,22 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
.index-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--wa-space-2xl);
|
||||
margin-block-end: var(--wa-space-3xl);
|
||||
|
||||
@media screen and (max-width: 1470px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
a {
|
||||
border-radius: var(--wa-border-radius-l);
|
||||
text-decoration: none;
|
||||
@@ -400,7 +413,6 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
&::part(header) {
|
||||
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&::part(base) {
|
||||
margin-block: 10rem;
|
||||
&::part(dialog) {
|
||||
margin-block-start: 10vh;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
&::part(body) {
|
||||
@@ -23,20 +24,20 @@
|
||||
@media screen and (max-width: 900px) {
|
||||
max-width: calc(100% - 2rem);
|
||||
|
||||
&::part(base) {
|
||||
&::part(dialog) {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
#site-search-container {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#site-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 20rem);
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
max-height: calc(100dvh - 2rem);
|
||||
}
|
||||
max-height: calc(100vh - 18rem);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
@@ -1,3 +1,61 @@
|
||||
wa-card:has(> .theme-icon-host, > [slot='header'] > .theme-icon-host) {
|
||||
&::part(header) {
|
||||
/* We want to add a background color, so any spacing needs to go on .theme-icon */
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
[slot='header'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-icon-host,
|
||||
.palette-icon-host {
|
||||
flex: 1;
|
||||
border-radius: inherit;
|
||||
|
||||
&[slot='header'],
|
||||
[slot='header']:has(&) {
|
||||
flex: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.palette-icon {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--hues, 9), 1fr);
|
||||
gap: var(--wa-space-3xs);
|
||||
min-width: 20ch;
|
||||
min-height: 9ch;
|
||||
align-content: center;
|
||||
|
||||
.swatch {
|
||||
height: 0.7em;
|
||||
background: var(--color);
|
||||
border-radius: var(--wa-border-radius-s);
|
||||
|
||||
&[data-suffix=''] {
|
||||
height: 1.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
min-width: 18ch;
|
||||
padding: var(--wa-space-xs) var(--wa-space-m);
|
||||
border-radius: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-color-icon {
|
||||
display: grid;
|
||||
gap: var(--wa-space-xs);
|
||||
@@ -25,10 +83,50 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
.theme-overall-icon {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--wa-space-xs);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 7.5rem;
|
||||
box-sizing: border-box;
|
||||
background: var(--wa-color-surface-lowered);
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.row-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
contain: inline-size;
|
||||
width: 100%;
|
||||
|
||||
wa-input {
|
||||
min-width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.swatches {
|
||||
display: flex;
|
||||
gap: var(--wa-space-3xs);
|
||||
|
||||
> div {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: var(--wa-border-radius-s);
|
||||
background: var(--wa-color-fill-loud);
|
||||
color: var(--wa-color-on-loud);
|
||||
|
||||
&.wa-brand {
|
||||
width: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ icon: card
|
||||
|
||||
<strong>Mittens</strong><br />
|
||||
This kitten is as cute as he is playful. Bring him home today!<br />
|
||||
<small>6 weeks old</small>
|
||||
<small class="wa-caption-m">6 weeks old</small>
|
||||
|
||||
<div slot="footer">
|
||||
<div slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating label="Rating"></wa-rating>
|
||||
</div>
|
||||
@@ -27,16 +27,6 @@ icon: card
|
||||
.card-overview {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.card-overview small {
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
.card-overview [slot='footer'] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -65,9 +55,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
|
||||
|
||||
```html {.example}
|
||||
<wa-card with-header class="card-header">
|
||||
<div slot="header">
|
||||
<div slot="header" class="wa-split">
|
||||
Header Title
|
||||
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
|
||||
<wa-icon-button name="gear" variant="solid" label="Settings" class="wa-size-m"></wa-icon-button>
|
||||
</div>
|
||||
|
||||
This card has a header. You can put all sorts of things in it!
|
||||
@@ -78,19 +68,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-header [slot='header'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-header wa-icon-button {
|
||||
font-size: var(--wa-font-size-m);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -103,7 +83,7 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
|
||||
<wa-card with-footer class="card-footer">
|
||||
This card has a footer. You can put all sorts of things in it!
|
||||
|
||||
<div slot="footer">
|
||||
<div slot="footer" class="wa-split">
|
||||
<wa-rating></wa-rating>
|
||||
<wa-button variant="brand">Preview</wa-button>
|
||||
</div>
|
||||
@@ -113,12 +93,6 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
|
||||
.card-footer {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-footer [slot='footer'] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -153,7 +127,7 @@ Use the `size` attribute to change a card's size.
|
||||
<wa-card with-footer size="small">
|
||||
This is a small card.
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
<footer slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</footer>
|
||||
@@ -162,7 +136,7 @@ Use the `size` attribute to change a card's size.
|
||||
<wa-card with-footer size="medium">
|
||||
This is a medium card (default).
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
<footer slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</footer>
|
||||
@@ -171,14 +145,39 @@ Use the `size` attribute to change a card's size.
|
||||
<wa-card with-footer size="large">
|
||||
This is a large card.
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
<footer slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</footer>
|
||||
</wa-card>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
<style>
|
||||
</style>
|
||||
### Appearance
|
||||
|
||||
Use the `appearance` attribute to change the card's visual appearance.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-grid">
|
||||
<wa-card>
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">Outlined (default)</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{% for appearance in ['outlined filled', 'outlined accent', 'plain', 'filled', 'accent'] -%}
|
||||
<wa-card appearance="{{ appearance }}">
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">{{ appearance | capitalize }}</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -77,6 +77,31 @@ The details component automatically adapts to right-to-left languages:
|
||||
</wa-details>
|
||||
```
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the `appearance` attribute to change the element’s visual appearance.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<wa-details summary="Outlined (default)">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled" appearance="filled">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled + Outlined" appearance="filled outlined">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Plain" appearance="plain">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Grouping Details
|
||||
|
||||
Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `wa-show` event.
|
||||
|
||||
@@ -885,4 +885,4 @@ If you don’t want to use [native styles](/docs/native/), you can include this
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="{% cdnUrl 'styles/components/page.css' %}" />
|
||||
```
|
||||
```
|
||||
@@ -5,8 +5,11 @@ import { sort } from '../_utils/filters.js';
|
||||
|
||||
export default {
|
||||
eleventyComputed: {
|
||||
// Default parent. Can be overridden by explicitly setting parent in the data.
|
||||
// parent can refer to either an ancestor page in the URL or another page in the same directory
|
||||
/**
|
||||
* Default parent slug. Can be overridden by explicitly setting parent in the data.
|
||||
* It can be either the URL slug of a page in the same directory or a parent directory.
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
parent(data) {
|
||||
let { parent, page } = data;
|
||||
|
||||
@@ -17,16 +20,28 @@ export default {
|
||||
return page.url.split('/').filter(Boolean).at(-2);
|
||||
},
|
||||
|
||||
/**
|
||||
* URL of parent page
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
parentUrl(data) {
|
||||
let { parent, page } = data;
|
||||
return getParentUrl(page.url, parent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Collection item of parent page
|
||||
* @returns {object | undefined} Parent page item
|
||||
*/
|
||||
parentItem(data) {
|
||||
let { parentUrl } = data;
|
||||
return data.collections.all.find(item => item.url === parentUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* Child pages of current page
|
||||
* @returns {object[]} Array of child pages
|
||||
*/
|
||||
children(data) {
|
||||
let { collections, page, parentOf } = data;
|
||||
|
||||
@@ -48,7 +63,17 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a parent slug against a page URL
|
||||
* @param {string} url - The URL of the page
|
||||
* @param {string} parent - The slug of the parent page
|
||||
* @returns {string} The resolved URL of the parent page
|
||||
*/
|
||||
function getParentUrl(url, parent) {
|
||||
if (!parent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let parts = url.split('/').filter(Boolean);
|
||||
let ancestorIndex = parts.findLastIndex(part => part === parent);
|
||||
let retParts = parts.slice();
|
||||
@@ -64,15 +89,18 @@ function getParentUrl(url, parent) {
|
||||
let ret = retParts.join('/');
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
// If the current page starts with a slash, make sure the parent does too
|
||||
// This is pretty much always the case with 11ty page URLs
|
||||
ret = '/' + ret;
|
||||
}
|
||||
|
||||
if (!retParts.at(-1).includes('.') && !ret.endsWith('/')) {
|
||||
if (!retParts.at(-1)?.includes('.') && !ret.endsWith('/')) {
|
||||
// If no extension, make sure to end with a slash
|
||||
ret += '/';
|
||||
}
|
||||
|
||||
if (ret === '/docs/') {
|
||||
// We don't want anyone's parent to be "Installation"!
|
||||
ret = '/';
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,11 @@ To get everything included in Web Awesome, add the following code to the `<head>
|
||||
|
||||
This snippet includes three parts:
|
||||
1. **The default theme**, a stylesheet that gives a cohesive look to Web Awesome components with both light and dark modes
|
||||
2. **Web Awesome styles**, an optional stylesheet that [styles native HTML elements](/docs/native) and includes [utility classes](/docs/utilities) you can use in your project
|
||||
2. **Web Awesome styles**, an optional stylesheet that [styles native HTML elements](/docs/native) and includes [utility classes](/docs/utilities) you can use in your project
|
||||
3. **The autoloader**, a lightweight script watches the DOM for unregistered Web Awesome elements and lazy loads them for you — even if they're added dynamically
|
||||
|
||||
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
|
||||
@@ -62,7 +58,7 @@ Font Awesome users can set their kit code to unlock Font Awesome Pro icons. You
|
||||
|
||||
## Advanced Setup
|
||||
|
||||
The autoloader is the easiest way to use Web Awesome, but different projects (or your own preferences!) may require different installation methods.
|
||||
The autoloader is the easiest way to use Web Awesome, but different projects (or your own preferences!) may require different installation methods.
|
||||
|
||||
### Installing via npm
|
||||
|
||||
@@ -126,4 +122,4 @@ Most of the magic behind assets is handled internally by Web Awesome, but if you
|
||||
// Get the path to an asset, e.g. /path/to/assets/file.ext
|
||||
const assetPath = getBasePath('file.ext');
|
||||
</script>
|
||||
```
|
||||
```
|
||||
@@ -33,7 +33,7 @@ Use the [variant utility classes](../utilities/color.md) to set the button's sem
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the [appearance utility classes](../utilities/appearance.md) to change the button's visual appearance:
|
||||
Use the [appearance utility classes](/docs/utilities/appearance) to change the button's visual appearance:
|
||||
|
||||
```html {.example}
|
||||
<div style="margin-block-end: 1rem;">
|
||||
|
||||
@@ -57,7 +57,7 @@ Use the [variant utility classes](../utilities/color.md) to set the callout's co
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the [appearance utility classes](../utilities/appearance.md) to change the callout's visual appearance (the default is `outlined filled`).
|
||||
Use the [appearance utility classes](/docs/utilities/appearance) to change the callout's visual appearance (the default is `outlined filled`).
|
||||
|
||||
```html {.example}
|
||||
<article class="wa-callout wa-brand wa-outlined wa-accent">
|
||||
|
||||
@@ -19,6 +19,35 @@ file: styles/native/details.css
|
||||
|
||||
## Examples
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the [appearance utility classes](/docs/utilities/appearance) to change the element's visual appearance:
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<details>
|
||||
<summary>Outlined (default)</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
<details class="wa-filled">
|
||||
<summary>Filled</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
<details class="wa-filled wa-outlined">
|
||||
<summary>Filled + Outlined</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
<details class="wa-plain">
|
||||
<summary>Plain</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Right-to-Left Languages
|
||||
|
||||
The details styling automatically adapts to right-to-left languages:
|
||||
|
||||
@@ -12,12 +12,37 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
|
||||
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
|
||||
:::
|
||||
|
||||
## Next
|
||||
## 3.0.0-alpha.12
|
||||
|
||||
- Fixed `wa-pill` class for text fields
|
||||
- Fixed `pill` style for `<wa-input>` elements
|
||||
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
|
||||
### Enhancements
|
||||
|
||||
- Added `appearance` to [`<wa-details>`](/docs/components/details) and [`<wa-card>`](/docs/components/card) and support for the [appearance utilities](/docs/utilities/appearance/) in the [`<details>` native styles](/docs/native/details).
|
||||
- Added an `orange` scale to all color palettes
|
||||
- Added the [`.wa-cloak` utility](/docs/utilities/fouce) to prevent FOUCE
|
||||
- Added the [`allDefined()` utility](/docs/usage/#all-defined) for awaiting component registration
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Specifying inherited CSS properties on `<wa-tooltip>` now works as expected ([thanks Dennis!](https://github.com/shoelace-style/webawesome-alpha/discussions/203))
|
||||
- Fixed a bug in `<wa-select>` that made it hard to use with VueJS, Svelte, and many other frameworks
|
||||
- Fixed a bug in `<wa-select multiple>` that sometimes resulted in empty `<div>` elements being output
|
||||
- Fixed a bug where changing a `<wa-option>` label wouldn't update the display label in `<wa-select>`
|
||||
- Added default spacing to icons slotted into `<wa-tab>`
|
||||
- Lots of fixes around pill-shaped elements:
|
||||
- Fixed the `wa-pill` class for text fields
|
||||
- Fixed `pill` style for `<wa-input>` and `<wa-radio-button>` elements
|
||||
- Fixed a bug in `<wa-radio-button>` that prevented active buttons from receiving the correct styles
|
||||
- Fixed a bug in `<wa-button>` that prevented the focus ring from showing in Safari
|
||||
- Fixed alignment of `<wa-dropdown>` inside button groups
|
||||
- Removed close watcher logic to backdrop hide animation bugs in `<wa-dialog>` and `<wa-drawer>`; this logic is already handled and we'll revisit `CloseWatcher` when browser support is better and behaviors are consistent
|
||||
- Revert `<wa-dialog>` structure and CSS to fix clipped content in dialogs (WA-A #123) and light dismiss in iOS Safari (WA-A #201)
|
||||
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
|
||||
- Fixed a bug in `<wa-progress>` that prevented Safari from animation progress changes
|
||||
- Fixed the missing indeterminate icon in [native checkbox styles](/docs/native/checkbox)
|
||||
- Fixed a bug in `<wa-radio>` where elements would stack instead of display inline
|
||||
- Docs fixes:
|
||||
- Fixed the search dialog's styles so it doesn't jump around as you search
|
||||
- Theme cards now have icons
|
||||
|
||||
## 3.0.0-alpha.11
|
||||
|
||||
|
||||
1
docs/docs/themes/themes.json
vendored
1
docs/docs/themes/themes.json
vendored
@@ -3,6 +3,7 @@
|
||||
"wide": true,
|
||||
"tags": ["themes", "theme"],
|
||||
"brand": "blue",
|
||||
"icon": "theme",
|
||||
"eleventyComputed": {
|
||||
"file": "styles/themes/{{ page.fileSlug }}.css"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,61 @@ 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.
|
||||
|
||||
### Individual components: `customElements.whenDefined()` { #when-defined}
|
||||
|
||||
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');
|
||||
|
||||
// <wa-button> is ready to use!
|
||||
const button = document.querySelector('wa-button');
|
||||
```
|
||||
|
||||
### All Web Awesome components: `allDefined()` { #all-defined }
|
||||
|
||||
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!
|
||||
```
|
||||
|
||||
#### Advanced Usage
|
||||
|
||||
By default, `allDefined()` will wait for all `wa-` prefixed custom elements within the current `document` to be registered.
|
||||
You can customize this behavior by passing in options:
|
||||
- `root` allows you to pass in a different element to search within, or a different document entirely (defaults to `document`).
|
||||
- `match` allows you to specify a custom function to determine which elements to wait for. This function should return `true` for elements you want to wait for and `false` for those you don't.
|
||||
- `additionalElements` allows you to wait for custom elements to be defined that may not be present in the DOM at the time `allDefined()` is called. This can be useful for elements that are loaded dynamically via JS.
|
||||
|
||||
Here is an example of using `match` and `root` to await registration of Web Awesome components inside an element with an id of `sidebar`, plus a `<my-component>` element if present in the DOM, and `<wa-slider>` and `<other-slider>` elements whether present in the DOM or not:
|
||||
|
||||
```js
|
||||
import { allDefined } from '/dist/webawesome.js';
|
||||
|
||||
await allDefined({
|
||||
match: tagName => tagName.startsWith('wa-') || tagName === 'my-component',
|
||||
root: document.getElementById('sidebar'),
|
||||
additionalElements: ['wa-slider', 'other-slider']
|
||||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
`additionalElements` will only wait for elements to be registered — it will not load them.
|
||||
If you're using the autoloader plus custom JS to inject HTML dynamically, **you need to make sure your JS runs _before_ the `await allDefined()` call**,
|
||||
otherwise you could run into a chicken and egg issue:
|
||||
since the autoloader will not load elements until they are present in the DOM, the promise will never resolve and your JS to inject them will not run.
|
||||
:::
|
||||
|
||||
## 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.
|
||||
@@ -88,49 +143,6 @@ For example, `<button>` and `<wa-button>` both have a `type` attribute, but the
|
||||
**Don't make assumptions about a component's API!** To prevent unexpected behaviors, please take the time to review the documentation and make sure you understand what each attribute, property, method, and event is intended to do.
|
||||
:::
|
||||
|
||||
## Waiting for Components to Load
|
||||
|
||||
Web components are registered with JavaScript, so depending on how and when you load Web Awesome, you may notice a [Flash of Undefined Custom Elements (FOUCE)](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/) when the page loads. There are a couple ways to prevent this, both of which are described in the linked article.
|
||||
|
||||
One option is to use the [`:defined`](https://developer.mozilla.org/en-US/docs/Web/CSS/:defined) CSS pseudo-class to "hide" custom elements that haven't been registered yet. You can scope it to specific tags or you can hide all undefined custom elements as shown below.
|
||||
|
||||
```css
|
||||
:not(:defined) {
|
||||
visibility: hidden;
|
||||
}
|
||||
```
|
||||
|
||||
As soon as a custom element is registered, it will immediately appear with all of its styles, effectively eliminating FOUCE. Note the use of `visibility: hidden` instead of `display: none` to reduce shifting as elements are registered. The drawback to this approach is that custom elements can potentially appear one by one instead of all at the same time.
|
||||
|
||||
Another option is to use [`customElements.whenDefined()`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined), which returns a promise that resolves when the specified element gets registered. You'll probably want to use it with [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) in case an element fails to load for some reason.
|
||||
|
||||
A clever way to use this method is to hide the `<body>` with `opacity: 0` and add a class that fades it in as soon as all your custom elements are defined.
|
||||
|
||||
```html
|
||||
<style>
|
||||
body {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
body.ready {
|
||||
opacity: 1;
|
||||
transition: 0.25s opacity;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
await Promise.allSettled([
|
||||
customElements.whenDefined('wa-button'),
|
||||
customElements.whenDefined('wa-card'),
|
||||
customElements.whenDefined('wa-rating')
|
||||
]);
|
||||
|
||||
// Button, card, and rating are registered now! Add
|
||||
// the `ready` class so the UI fades in.
|
||||
document.body.classList.add('ready');
|
||||
</script>
|
||||
```
|
||||
|
||||
## Component Rendering and Updating
|
||||
|
||||
Web Awesome components are built with [Lit](https://lit.dev/), a tiny library that makes authoring custom elements easier, more maintainable, and a lot of fun! As a Web Awesome user, here is some helpful information about rendering and updating you should probably be aware of.
|
||||
|
||||
34
docs/docs/utilities/fouce.md
Normal file
34
docs/docs/utilities/fouce.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
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
|
||||
snippet: .wa-cloak
|
||||
---
|
||||
|
||||
Often, components are shown before their logic and styles have had a chance to load, also known as a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
|
||||
The FOUCE style utility (which is automatically applied if you use our [style utilities](/docs/utilities/)) automatically takes care of hiding custom elements until **both 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-cloak` class to any element on the page or even apply it to the whole page by placing the class on the `<html>` element:
|
||||
|
||||
```html
|
||||
<html class="wa-cloak">
|
||||
...
|
||||
</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();
|
||||
```
|
||||
:::
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
<div class="wa-split wa-align-items-end">
|
||||
<strong>Normal loading</strong>
|
||||
<wa-button onclick="document.querySelectorAll('iframe').forEach(iframe => iframe.srcdoc = iframe.srcdoc)">
|
||||
<wa-icon name="refresh"></wa-icon>
|
||||
Refresh
|
||||
</wa-button>
|
||||
<strong>With FOUCE reduction</strong>
|
||||
</div>
|
||||
|
||||
|
||||
{% set sample_card %}
|
||||
<link id="theme-stylesheet" rel="stylesheet" href="/dist/styles/themes/default.css" render="blocking" fetchpriority="high">
|
||||
<link rel="stylesheet" href="/dist/styles/webawesome.css">
|
||||
<link rel="stylesheet" href="/dist/styles/forms.css">
|
||||
<script type=module>
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const loadScript = src => new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.type = "module";
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
await loadScript("/dist/components/button/button.js");
|
||||
await delay(500);
|
||||
await loadScript("/dist/components/card/card.js");
|
||||
await delay(500);
|
||||
await loadScript("/dist/components/rating/rating.js");
|
||||
</script>
|
||||
|
||||
<wa-card with-footer with-image class="card-overview">
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
|
||||
<strong>Mittens</strong><br />
|
||||
This kitten is as cute as he is playful. Bring him home today!<br />
|
||||
<small>6 weeks old</small>
|
||||
|
||||
<div slot="footer">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<style>
|
||||
.card-overview small {
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
.card-overview [slot=footer] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{% endset %}
|
||||
|
||||
|
||||
<div class="iframes">
|
||||
<iframe srcdoc='<body class="wa-fouce-off">{{ sample_card }}</body>'></iframe>
|
||||
<iframe srcdoc='{{ sample_card }}'></iframe>
|
||||
</div>
|
||||
<style>
|
||||
.iframes {
|
||||
display: flex;
|
||||
gap: var(--wa-space-m);
|
||||
margin-top: var(--wa-space-l);
|
||||
|
||||
iframe {
|
||||
flex: 1;
|
||||
height: 60ch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% 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 %}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.12",
|
||||
"homepage": "https://webawesome.com/",
|
||||
"author": "Web Awesome",
|
||||
"license": "MIT",
|
||||
@@ -52,8 +52,8 @@
|
||||
"start:alpha": "node scripts/build.js --alpha --develop",
|
||||
"publish-alpha-cdn": "./publish-alpha-cdn.sh",
|
||||
"create": "plop --plopfile scripts/plop/plopfile.js",
|
||||
"test": "web-test-runner --group default",
|
||||
"test:component": "web-test-runner -- --watch --group",
|
||||
"test": "CSR_ONLY=\"true\" web-test-runner --group default",
|
||||
"test:component": "CSR_ONLY=\"true\" web-test-runner -- --watch --group",
|
||||
"test:contrast": "cd src/styles/color && node contrast.test.js",
|
||||
"test:watch": "web-test-runner --watch --group default",
|
||||
"prettier": "prettier --check --log-level=warn .",
|
||||
|
||||
@@ -5,15 +5,16 @@
|
||||
|
||||
--spacing: var(--wa-space);
|
||||
--border-width: var(--wa-panel-border-width);
|
||||
--border-color: var(--wa-color-surface-border);
|
||||
--outlined-background-color: var(--wa-color-surface-default);
|
||||
--outlined-border-color: var(--wa-color-surface-border);
|
||||
--border-radius: var(--wa-panel-border-radius);
|
||||
|
||||
--inner-border-radius: calc(var(--border-radius) - var(--border-width));
|
||||
--inner-border-color: var(--outlined-border-color);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--wa-color-surface-default);
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--background-color, var(--wa-color-surface-default));
|
||||
border-color: var(--border-color, var(--wa-color-surface-border));
|
||||
border-radius: var(--border-radius);
|
||||
border-style: var(--wa-panel-border-style);
|
||||
box-shadow: var(--wa-shadow-s);
|
||||
@@ -21,6 +22,20 @@
|
||||
color: var(--wa-color-text-normal);
|
||||
}
|
||||
|
||||
:host(:is([appearance~='accent'], .wa-accent)) {
|
||||
color: var(--text-color, var(--wa-color-text-normal));
|
||||
}
|
||||
|
||||
:host([appearance~='filled']),
|
||||
:host(.wa-filled) {
|
||||
--inner-border-color: oklab(from var(--outlined-border-color) l a b / 65%);
|
||||
}
|
||||
|
||||
:host([appearance='plain']) {
|
||||
--inner-border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Take care of top and bottom radii */
|
||||
.image,
|
||||
:host(:not([with-image])) .header,
|
||||
@@ -46,10 +61,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Round all corners for plain appearance */
|
||||
:host([appearance='plain']) .image {
|
||||
border-radius: var(--inner-border-radius);
|
||||
|
||||
&::slotted(img) {
|
||||
border-radius: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
border-block-end-style: inherit;
|
||||
border-block-end-color: var(--border-color);
|
||||
border-block-end-color: var(--inner-border-color);
|
||||
border-block-end-width: var(--border-width);
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
}
|
||||
@@ -62,7 +86,7 @@
|
||||
.footer {
|
||||
display: block;
|
||||
border-block-start-style: inherit;
|
||||
border-block-start-color: var(--border-color);
|
||||
border-block-start-color: var(--inner-border-color);
|
||||
border-block-start-width: var(--border-width);
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import styles from './card.css';
|
||||
|
||||
@@ -22,19 +23,24 @@ import styles from './card.css';
|
||||
* @csspart footer - The container that wraps the card's footer.
|
||||
*
|
||||
* @cssproperty [--border-radius=var(--wa-panel-border-radius)] - The radius for the card's corners. Expects a single value.
|
||||
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders, including inner borders. Expects a single value.
|
||||
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders. Expects a single value.
|
||||
* @cssproperty [--inner-border-color=var(--wa-color-surface-border)] - The color of the card's inner borders, e.g. those separating headers and footers from the main content. Expects a single value.
|
||||
* @cssproperty [--border-width=var(--wa-panel-border-width)] - The width of the card's borders. Expects a single value.
|
||||
* @cssproperty [--spacing=var(--wa-space)] - The amount of space around and between sections of the card. Expects a single value.
|
||||
*/
|
||||
@customElement('wa-card')
|
||||
export default class WaCard extends WebAwesomeElement {
|
||||
static shadowStyle = [sizeStyles, styles];
|
||||
static shadowStyle = [sizeStyles, appearanceStyles, styles];
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
||||
|
||||
/** The component's size. Will be inherited by any descendants with a `size` attribute. */
|
||||
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
||||
|
||||
/** The card's visual appearance. */
|
||||
@property({ reflect: true })
|
||||
appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'outlined';
|
||||
|
||||
/** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
|
||||
@property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getTargetElement, waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import nativeStyles from '../../styles/native/details.css';
|
||||
import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../icon/icon.js';
|
||||
import styles from './details.css';
|
||||
@@ -45,7 +46,7 @@ import styles from './details.css';
|
||||
*/
|
||||
@customElement('wa-details')
|
||||
export default class WaDetails extends WebAwesomeElement {
|
||||
static shadowStyle = [nativeStyles, styles];
|
||||
static shadowStyle = [appearanceStyles, nativeStyles, styles];
|
||||
|
||||
private detailsObserver: MutationObserver;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
@@ -67,6 +68,9 @@ export default class WaDetails extends WebAwesomeElement {
|
||||
/** Disables the details so it can't be toggled. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The element's visual appearance. */
|
||||
@property({ reflect: true }) appearance: 'filled' | 'outlined' | 'plain' = 'outlined';
|
||||
|
||||
firstUpdated() {
|
||||
this.body.style.height = this.open ? 'auto' : '0';
|
||||
if (this.open) {
|
||||
|
||||
@@ -1,24 +1,89 @@
|
||||
:host {
|
||||
--background-color: var(--wa-color-surface-raised);
|
||||
--border-radius: var(--wa-panel-border-radius);
|
||||
--box-shadow: var(--wa-shadow-l);
|
||||
--width: 31rem;
|
||||
--spacing: var(--wa-space-xl);
|
||||
--show-duration: 200ms;
|
||||
--hide-duration: 200ms;
|
||||
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host(:not([open])) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
background-color: inherit;
|
||||
border-radius: inherit;
|
||||
border: inherit;
|
||||
box-shadow: inherit;
|
||||
padding: inherit;
|
||||
:host([open]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: var(--width);
|
||||
max-width: calc(100% - var(--wa-space-2xl));
|
||||
max-height: calc(100% - var(--wa-space-2xl));
|
||||
background-color: var(--background-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: none;
|
||||
box-shadow: var(--box-shadow);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
transition: inherit;
|
||||
|
||||
&.show {
|
||||
animation: show-dialog var(--show-duration) ease;
|
||||
|
||||
&::backdrop {
|
||||
animation: show-backdrop var(--show-duration, 200ms) ease;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
animation: show-dialog var(--hide-duration) ease reverse;
|
||||
|
||||
&::backdrop {
|
||||
animation: show-backdrop var(--hide-duration, 200ms) ease reverse;
|
||||
}
|
||||
}
|
||||
|
||||
&.pulse {
|
||||
animation: pulse 250ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Ensure there's enough vertical padding for phones that don't update vh when chrome appears (e.g. iPhone) */
|
||||
@media screen and (max-width: 420px) {
|
||||
.dialog {
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog--open {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: var(--spacing);
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
align-self: center;
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
font-size: var(--wa-font-size-l);
|
||||
font-weight: var(--wa-font-weight-heading);
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -28,13 +93,81 @@ dialog {
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
gap: var(--wa-space-2xs);
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: var(--spacing);
|
||||
}
|
||||
|
||||
wa-icon-button,
|
||||
::slotted(wa-icon-button) {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--wa-font-size-m);
|
||||
.header-actions wa-icon-button,
|
||||
.header-actions ::slotted(wa-icon-button) {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--wa-font-size-m);
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
padding: var(--spacing);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--wa-space-xs);
|
||||
justify-content: end;
|
||||
padding: var(--spacing);
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.footer ::slotted(wa-button:not(:first-of-type)) {
|
||||
margin-inline-start: var(--wa-spacing-xs);
|
||||
}
|
||||
|
||||
.dialog::backdrop {
|
||||
/*
|
||||
NOTE: the ::backdrop element doesn't inherit properly in Safari yet, but it will in 17.4! At that time, we can
|
||||
remove the fallback values here.
|
||||
*/
|
||||
background-color: var(--wa-color-overlay-modal, rgb(0 0 0 / 0.25));
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
scale: 1;
|
||||
}
|
||||
50% {
|
||||
scale: 1.02;
|
||||
}
|
||||
100% {
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-dialog {
|
||||
from {
|
||||
opacity: 0;
|
||||
scale: 0.8;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-backdrop {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.dialog {
|
||||
border: solid 1px white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, isServer } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { WaAfterHideEvent } from '../../events/after-hide.js';
|
||||
import { WaAfterShowEvent } from '../../events/after-show.js';
|
||||
import { WaHideEvent } from '../../events/hide.js';
|
||||
@@ -8,7 +9,6 @@ import { animateWithClass } from '../../internal/animate.js';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import dialogStyles from '../../styles/native/dialog.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../icon-button/icon-button.js';
|
||||
import styles from './dialog.css';
|
||||
@@ -35,7 +35,6 @@ import styles from './dialog.css';
|
||||
* behavior such as data loss.
|
||||
* @event wa-after-hide - Emitted after the dialog closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The inner `<dialog>` used to render this component.
|
||||
* @csspart header - The dialog's header. This element wraps the title and header actions.
|
||||
* @csspart header-actions - Optional actions to add to the header. Works best with `<wa-icon-button>`.
|
||||
* @csspart title - The dialog's title.
|
||||
@@ -44,16 +43,22 @@ import styles from './dialog.css';
|
||||
* @csspart body - The dialog's body.
|
||||
* @csspart footer - The dialog's footer.
|
||||
*
|
||||
* @cssproperty --background-color - The dialog's background color.
|
||||
* @cssproperty --border-radius - The radius of the dialog's corners.
|
||||
* @cssproperty --box-shadow - The shadow effects around the edges of the dialog.
|
||||
* @cssproperty --spacing - The amount of space around and between the dialog's content.
|
||||
* @cssproperty --width - The preferred width of the dialog. Note that the dialog will shrink to accommodate smaller screens.
|
||||
* @cssproperty [--show-duration=200ms] - The animation duration when showing the dialog.
|
||||
* @cssproperty [--hide-duration=200ms] - The animation duration when hiding the dialog.
|
||||
*/
|
||||
@customElement('wa-dialog')
|
||||
export default class WaDialog extends WebAwesomeElement {
|
||||
static shadowStyle = [dialogStyles, styles];
|
||||
static shadowStyle = styles;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('dialog') dialog: HTMLDialogElement;
|
||||
@query('.dialog') dialog: HTMLDialogElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dialog is open. You can toggle this attribute to show and hide the dialog, or you can
|
||||
@@ -76,9 +81,6 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
/** When enabled, the dialog will be closed when the user clicks outside of it. */
|
||||
@property({ attribute: 'light-dismiss', type: Boolean }) lightDismiss = false;
|
||||
|
||||
@state()
|
||||
hasOpened = this.open;
|
||||
|
||||
firstUpdated() {
|
||||
if (this.open) {
|
||||
this.addOpenListeners();
|
||||
@@ -100,12 +102,14 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
|
||||
if (waHideEvent.defaultPrevented) {
|
||||
this.open = true;
|
||||
animateWithClass(this.dialog, 'wa-dialog-pulse');
|
||||
animateWithClass(this.dialog, 'pulse');
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeOpenListeners();
|
||||
|
||||
await animateWithClass(this.dialog, 'hide');
|
||||
|
||||
this.open = false;
|
||||
this.dialog.close();
|
||||
unlockBodyScrolling(this);
|
||||
@@ -120,16 +124,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.requestClose(this.dialog);
|
||||
};
|
||||
} else {
|
||||
this.closeWatcher?.destroy();
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
@@ -161,7 +156,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
if (this.lightDismiss) {
|
||||
this.requestClose(this.dialog);
|
||||
} else {
|
||||
await animateWithClass(this.dialog, 'wa-dialog-pulse');
|
||||
await animateWithClass(this.dialog, 'pulse');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +193,6 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
this.addOpenListeners();
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.open = true;
|
||||
this.hasOpened = true;
|
||||
this.dialog.showModal();
|
||||
|
||||
lockBodyScrolling(this);
|
||||
@@ -211,20 +205,28 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
}
|
||||
});
|
||||
|
||||
await animateWithClass(this.dialog, 'show');
|
||||
|
||||
this.dispatchEvent(new WaAfterShowEvent());
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<dialog
|
||||
part="base"
|
||||
part="dialog"
|
||||
class=${classMap({
|
||||
dialog: true,
|
||||
'dialog--open': this.open,
|
||||
'dialog--with-header': this.withHeader,
|
||||
'dialog--with-footer': this.withFooter,
|
||||
})}
|
||||
@cancel=${this.handleDialogCancel}
|
||||
@click=${this.handleDialogClick}
|
||||
@pointerdown=${this.handleDialogPointerDown}
|
||||
>
|
||||
${this.withHeader
|
||||
? html`
|
||||
<header part="header">
|
||||
<header part="header" class="header">
|
||||
<h2 part="title" class="title" id="title">
|
||||
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
|
||||
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||
@@ -246,11 +248,11 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
`
|
||||
: ''}
|
||||
|
||||
<slot part="body" class="body"></slot>
|
||||
<div part="body" class="body"><slot></slot></div>
|
||||
|
||||
${this.withFooter
|
||||
? html`
|
||||
<footer part="footer">
|
||||
<footer part="footer" class="footer">
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
`
|
||||
@@ -262,7 +264,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
|
||||
// Ugly, but it fixes light dismiss in Safari: https://bugs.webkit.org/show_bug.cgi?id=267688
|
||||
if (!isServer) {
|
||||
document.body.addEventListener('pointerdown', () => {
|
||||
document.addEventListener('pointerdown', () => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export default class WaDrawer extends WebAwesomeElement {
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.drawer') drawer: HTMLDialogElement;
|
||||
|
||||
@@ -136,16 +135,7 @@ export default class WaDrawer extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.requestClose(this.drawer);
|
||||
};
|
||||
} else {
|
||||
this.closeWatcher?.destroy();
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
|
||||
@@ -50,8 +50,6 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
@query('#trigger') trigger: HTMLSlotElement;
|
||||
@query('.panel') panel: HTMLSlotElement;
|
||||
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
|
||||
* can use the `show()` and `hide()` methods and this attribute will reflect the dropdown's open state.
|
||||
@@ -148,7 +146,7 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
private handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation
|
||||
// in case any ancestors are also listening for this key.
|
||||
if (this.open && event.key === 'Escape' && !this.closeWatcher) {
|
||||
if (this.open && event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
this.hide();
|
||||
this.focusOnTrigger();
|
||||
@@ -344,16 +342,7 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
|
||||
addOpenListeners() {
|
||||
this.panel.addEventListener('wa-select', this.handlePanelSelect);
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.hide();
|
||||
this.focusOnTrigger();
|
||||
};
|
||||
} else {
|
||||
this.panel.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
this.panel.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
@@ -365,7 +354,6 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
}
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
|
||||
@@ -106,6 +106,11 @@ export default class WaOption extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private handleDefaultSlotChange() {
|
||||
// Tell the controller to update the label
|
||||
if (customElements.get('wa-select')) {
|
||||
this.closest('wa-select')?.selectionChanged();
|
||||
}
|
||||
|
||||
this.updateDefaultLabel();
|
||||
|
||||
if (this.isInitialized) {
|
||||
@@ -123,7 +128,7 @@ export default class WaOption extends WebAwesomeElement {
|
||||
|
||||
private handleHover = (event: Event) => {
|
||||
// We need this because Safari doesn't honor :hover styles while dragging
|
||||
// Testcase: https://codepen.io/leaverou/pen/VYZOOjy
|
||||
// Test case: https://codepen.io/leaverou/pen/VYZOOjy
|
||||
if (event.type === 'mouseenter') {
|
||||
this.toggleCustomState('hover', true);
|
||||
} else if (event.type === 'mouseleave') {
|
||||
|
||||
@@ -97,6 +97,7 @@ slot:not([name]) {
|
||||
:host([disable-sticky~='menu']) [part~='menu'] {
|
||||
position: static;
|
||||
overflow: unset;
|
||||
z-index: unset;
|
||||
}
|
||||
|
||||
:host([disable-sticky~='aside']) [part~='aside'],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: calc(var(--value, 0) * 1%);
|
||||
width: var(--percentage);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -26,9 +26,7 @@
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
transition: var(--wa-transition-slow, 200ms) var(--wa-transition-easing, ease);
|
||||
transition: 1s;
|
||||
/* transition-property: width, background; */
|
||||
transition: all var(--wa-transition-slow, 200ms) var(--wa-transition-easing, ease);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,9 @@ describe('<wa-progress-bar>', () => {
|
||||
expect(base.getAttribute('aria-valuenow')).to.equal('25');
|
||||
});
|
||||
|
||||
it('uses the value parameter to set the custom property on the indicator', () => {
|
||||
expect(indicator.style.getPropertyValue('--value')).to.equal('25');
|
||||
it('uses the value parameter to set the custom property on the indicator', async () => {
|
||||
await new Promise(requestAnimationFrame);
|
||||
expect(el.style.getPropertyValue('--percentage')).to.equal('25%');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { PropertyValues } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { clamp } from '../../internal/math.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import styles from './progress-bar.css';
|
||||
@@ -33,6 +35,16 @@ export default class WaProgressBar extends WebAwesomeElement {
|
||||
/** A custom label for assistive devices. */
|
||||
@property() label = '';
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has('value')) {
|
||||
// Wait a cycle before setting it so Safari animates it.
|
||||
// https://github.com/shoelace-style/webawesome/issues/356
|
||||
requestAnimationFrame(() => {
|
||||
this.style.setProperty('--percentage', `${clamp(this.value, 0, 100)}%`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -45,7 +57,7 @@ export default class WaProgressBar extends WebAwesomeElement {
|
||||
aria-valuemax="100"
|
||||
aria-valuenow=${this.indeterminate ? '0' : this.value}
|
||||
>
|
||||
<div part="indicator" class="indicator" style="--value: ${this.value}">
|
||||
<div part="indicator" class="indicator">
|
||||
${!this.indeterminate ? html` <slot part="label" class="label"></slot> ` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
/*
|
||||
* Checked buttons
|
||||
*/
|
||||
|
||||
:host([checked]) {
|
||||
--indicator-color: var(--wa-form-control-activated-color);
|
||||
--indicator-width: var(--wa-border-width-s);
|
||||
@@ -43,3 +42,41 @@
|
||||
--border-color: var(--indicator-color);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Active buttons
|
||||
*/
|
||||
button:active {
|
||||
--text-color-active: var(--wa-form-control-activated-color);
|
||||
--border-color-active: var(--wa-form-control-activated-color);
|
||||
}
|
||||
|
||||
/* Horizontal radio pill buttons */
|
||||
:host([data-wa-radio-horizontal][data-wa-radio-first]:not([data-wa-radio-last])) .wa-pill {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
:host([data-wa-radio-horizontal][data-wa-radio-inner]) .wa-pill {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:host([data-wa-radio-horizontal][data-wa-radio-last]:not([data-wa-radio-first])) .wa-pill {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
/* Vertical radio pill buttons */
|
||||
:host([data-wa-radio-vertical][data-wa-radio-first]:not([data-wa-radio-last])) .wa-pill {
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
:host([data-wa-radio-vertical][data-wa-radio-inner]) .wa-pill {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:host([data-wa-radio-vertical][data-wa-radio-last]:not([data-wa-radio-first])) .wa-pill {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import nativeStyles from '../../styles/native/button.css';
|
||||
import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import variantStyles from '../../styles/utilities/variants.css';
|
||||
import buttonStyles from '../button/button.css';
|
||||
import styles from './radio-button.css';
|
||||
|
||||
/**
|
||||
@@ -49,7 +48,7 @@ import styles from './radio-button.css';
|
||||
*/
|
||||
@customElement('wa-radio-button')
|
||||
export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
|
||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles];
|
||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
|
||||
static rectProxy = 'input';
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
|
||||
@@ -38,8 +38,6 @@ import styles from './radio-group.css';
|
||||
* @csspart form-control-input - The input's wrapper.
|
||||
* @csspart radios - The wrapper than surrounds radio items, styled as a flex container by default.
|
||||
* @csspart hint - The hint's wrapper.
|
||||
* @csspart button-group - The button group that wraps radio buttons.
|
||||
* @csspart button-group__base - The button group's `base` part.
|
||||
*/
|
||||
@customElement('wa-radio-group')
|
||||
export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
@@ -65,7 +63,6 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
|
||||
@state() private hasButtonGroup = false;
|
||||
@state() private hasRadioButtons = false;
|
||||
|
||||
/**
|
||||
@@ -150,7 +147,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
clickedRadio.checked = true;
|
||||
|
||||
const radios = this.getAllRadios();
|
||||
const hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
|
||||
const hasRadioButtons = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
|
||||
for (const radio of radios) {
|
||||
if (clickedRadio === radio) {
|
||||
continue;
|
||||
@@ -158,7 +155,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
radio.checked = false;
|
||||
|
||||
if (!hasButtonGroup) {
|
||||
if (!hasRadioButtons) {
|
||||
radio.setAttribute('tabindex', '-1');
|
||||
}
|
||||
}
|
||||
@@ -183,6 +180,15 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
// Detect the presence of radio buttons
|
||||
this.hasRadioButtons = radios.some(radio => radio.localName === 'wa-radio-button');
|
||||
|
||||
// Add data attributes to support styling
|
||||
radios.forEach((radio, index) => {
|
||||
radio.toggleAttribute('data-wa-radio-horizontal', this.orientation !== 'vertical');
|
||||
radio.toggleAttribute('data-wa-radio-vertical', this.orientation === 'vertical');
|
||||
radio.toggleAttribute('data-wa-radio-first', index === 0);
|
||||
radio.toggleAttribute('data-wa-radio-inner', index !== 0 && index !== radios.length - 1);
|
||||
radio.toggleAttribute('data-wa-radio-last', index === radios.length - 1);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
// Sync the checked state and size
|
||||
radios.map(async radio => {
|
||||
@@ -196,10 +202,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
}),
|
||||
);
|
||||
|
||||
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
|
||||
|
||||
if (radios.length > 0 && !radios.some(radio => radio.checked)) {
|
||||
if (this.hasButtonGroup) {
|
||||
if (this.hasRadioButtons) {
|
||||
const buttonRadio = radios[0].shadowRoot?.querySelector('button');
|
||||
|
||||
if (buttonRadio) {
|
||||
@@ -210,7 +214,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasButtonGroup) {
|
||||
if (this.hasRadioButtons) {
|
||||
const buttonGroup = this.shadowRoot?.querySelector('wa-button-group');
|
||||
|
||||
if (buttonGroup) {
|
||||
@@ -276,12 +280,12 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
const hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
|
||||
const hasRadioButtons = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
|
||||
|
||||
this.getAllRadios().forEach(radio => {
|
||||
radio.checked = false;
|
||||
|
||||
if (!hasButtonGroup) {
|
||||
if (!hasRadioButtons) {
|
||||
radio.setAttribute('tabindex', '-1');
|
||||
}
|
||||
});
|
||||
@@ -289,7 +293,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
this.value = radios[index].value;
|
||||
radios[index].checked = true;
|
||||
|
||||
if (!hasButtonGroup) {
|
||||
if (!hasRadioButtons) {
|
||||
radios[index].setAttribute('tabindex', '0');
|
||||
radios[index].focus();
|
||||
} else {
|
||||
@@ -351,7 +355,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
|
||||
<slot
|
||||
part="form-control-input"
|
||||
class=${classMap({
|
||||
'wa-button-group': this.hasButtonGroup,
|
||||
'wa-button-group': this.hasRadioButtons,
|
||||
'wa-button-group-vertical': this.orientation === 'vertical',
|
||||
})}
|
||||
@slotchange=${this.syncRadioElements}
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[part~='label'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
[part~='hint'] {
|
||||
grid-column: 2;
|
||||
margin-block-start: var(--wa-space-3xs);
|
||||
|
||||
@@ -22,6 +22,7 @@ import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../icon/icon.js';
|
||||
import '../option/option.js';
|
||||
import type WaOption from '../option/option.js';
|
||||
import '../popup/popup.js';
|
||||
import type WaPopup from '../popup/popup.js';
|
||||
@@ -37,6 +38,7 @@ import styles from './select.css';
|
||||
* @dependency wa-icon
|
||||
* @dependency wa-popup
|
||||
* @dependency wa-tag
|
||||
* @dependency wa-option
|
||||
*
|
||||
* @slot - The listbox options. Must be `<wa-option>` elements. You can use `<wa-divider>` to group items visually.
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
@@ -102,7 +104,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private typeToSelectString = '';
|
||||
private typeToSelectTimeout: number;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.select') popup: WaPopup;
|
||||
@query('.combobox') combobox: HTMLSlotElement;
|
||||
@@ -320,17 +321,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
if (this.getRootNode() !== document) {
|
||||
this.getRootNode().addEventListener('focusin', this.handleDocumentFocusIn);
|
||||
}
|
||||
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
if (this.open) {
|
||||
this.hide();
|
||||
this.displayInput.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
@@ -341,8 +331,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
if (this.getRootNode() !== document) {
|
||||
this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn);
|
||||
}
|
||||
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
@@ -684,9 +672,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
this.selectionChanged();
|
||||
}
|
||||
|
||||
// This method must be called whenever the selection changes. It will update the selected options cache, the current
|
||||
// value, and the display value
|
||||
private selectionChanged() {
|
||||
// @internal This method must be called whenever the selection changes. It will update the selected options cache, the
|
||||
// current value, and the display value. The option component uses it internally to update labels as they change.
|
||||
public selectionChanged() {
|
||||
const options = this.getAllOptions();
|
||||
|
||||
// Update selected options cache
|
||||
@@ -725,6 +713,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
this.updateValidity();
|
||||
});
|
||||
}
|
||||
|
||||
protected get tags() {
|
||||
return this.selectedOptions.map((option, index) => {
|
||||
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
transition: color var(--wa-transition-fast) var(--wa-transition-easing);
|
||||
|
||||
::slotted(wa-icon:first-child) {
|
||||
margin-inline-end: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
::slotted(wa-icon:last-child) {
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
}
|
||||
}
|
||||
|
||||
:host(:hover:not([disabled])) .tab {
|
||||
|
||||
@@ -44,7 +44,6 @@ export default class WaTooltip extends WebAwesomeElement {
|
||||
static dependencies = { 'wa-popup': WaPopup };
|
||||
|
||||
private hoverTimeout: number;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.body') body: HTMLElement;
|
||||
@@ -127,7 +126,6 @@ export default class WaTooltip extends WebAwesomeElement {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// Cleanup this event in case the tooltip is removed while open
|
||||
this.closeWatcher?.destroy();
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
this.eventController.abort();
|
||||
|
||||
@@ -212,15 +210,7 @@ export default class WaTooltip extends WebAwesomeElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.hide();
|
||||
};
|
||||
} else {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
|
||||
}
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
|
||||
|
||||
this.body.hidden = false;
|
||||
this.popup.active = true;
|
||||
@@ -237,7 +227,6 @@ export default class WaTooltip extends WebAwesomeElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeWatcher?.destroy();
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
|
||||
await animateWithClass(this.popup.popup, 'hide-with-scale');
|
||||
|
||||
@@ -81,10 +81,6 @@ input:is([type='button'], [type='reset'], [type='submit']),
|
||||
* States
|
||||
*/
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -103,6 +99,11 @@ input:is([type='button'], [type='reset'], [type='submit']),
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep it last so Safari doesn't stop parsing this block */
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,16 @@ input[type='checkbox']:where(:not(:host *)) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:indeterminate::after {
|
||||
background-color: currentColor;
|
||||
content: '';
|
||||
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M431 256c0 17.7-14.3 32-32 32H49c-17.7 0-32-14.3-32-32s14.3-32 32-32h350c17.7 0 32 14.3 32 32z"/></svg>')
|
||||
center no-repeat;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox']:where(:not(:host *)),
|
||||
|
||||
@@ -2,10 +2,12 @@ details:where(:not(:host *)),
|
||||
:host {
|
||||
--icon-color: var(--wa-color-text-quiet);
|
||||
--spacing: var(--wa-space-m);
|
||||
--outlined-border-color: var(--wa-color-surface-border);
|
||||
|
||||
background-color: var(--wa-color-surface-default);
|
||||
border: var(--wa-panel-border-width) var(--wa-color-surface-border) var(--wa-panel-border-style);
|
||||
background-color: var(--background-color, var(--wa-color-surface-default));
|
||||
border: var(--wa-panel-border-width) var(--border-color, var(--wa-color-surface-border)) var(--wa-panel-border-style);
|
||||
border-radius: var(--wa-panel-border-radius);
|
||||
color: var(--text-color, inherit);
|
||||
padding: var(--spacing);
|
||||
|
||||
/* Print styles */
|
||||
@@ -19,6 +21,12 @@ details:where(:not(:host *)),
|
||||
}
|
||||
}
|
||||
|
||||
details.wa-plain,
|
||||
:host(wa-details[appearance='plain']),
|
||||
:host(wa-details.wa-plain) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
details:where(:not(:host *)) summary,
|
||||
:host summary /* nesting these summary styles in the rule above breaks in Firefox and Safari */ {
|
||||
display: flex;
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
}
|
||||
|
||||
/* Horizontal */
|
||||
.wa-button-group[aria-orientation='horizontal'] {
|
||||
/* TODO - see https://github.com/shoelace-style/webawesome/issues/374 */
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.wa-button-group:not([aria-orientation='vertical']):not(.wa-button-group-vertical) {
|
||||
> :not(:first-child),
|
||||
&::slotted(:not(:first-child)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
64
src/utilities/defined.ts
Normal file
64
src/utilities/defined.ts
Normal file
@@ -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 `<bar-button>` element
|
||||
* await allDefined({
|
||||
* match: tagName => tagName.startsWith('foo-'),
|
||||
* additionalElements: ['bar-button', 'baz-dialog']
|
||||
* });
|
||||
*/
|
||||
export async function allDefined(options?: Partial<AllDefinedOptions>) {
|
||||
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);
|
||||
}
|
||||
@@ -3,3 +3,12 @@ import { startLoader } from './webawesome.js';
|
||||
export * from './webawesome.js';
|
||||
|
||||
startLoader();
|
||||
|
||||
// Remove `wa-cloak` when the autoloader finishes OR after two seconds. This prevents the entire screen from flashing
|
||||
// when unregistered components get added later on.
|
||||
Promise.race([
|
||||
new Promise(resolve => document.addEventListener('wa-discovery-complete', resolve)),
|
||||
new Promise(resolve => setTimeout(resolve, 2000)),
|
||||
]).then(() => {
|
||||
document.querySelectorAll('.wa-cloak').forEach(el => el.classList.remove('wa-cloak'));
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
</script>
|
||||
<script type="module">
|
||||
;(async () => {
|
||||
await import('@lit-labs/ssr-client/lit-element-hydrate-support.js')
|
||||
await Promise.allSettled(window.clientComponents.map(str => import(str)));
|
||||
})()
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user