Compare commits

..

12 Commits

Author SHA1 Message Date
Cory LaViska
362ea5e319 scroll on reload is no longer a crapshoot 2025-06-02 15:31:17 -04:00
Cory LaViska
28c442ba1c add popover 2025-06-02 15:19:37 -04:00
Cory LaViska
eeedca813a add sidebar to plop 2025-06-02 15:19:13 -04:00
Cory LaViska
6c210310b6 update content 2025-06-02 15:11:14 -04:00
Cory LaViska
106638b4af fix arrow border in FF/Safari 2025-06-02 15:02:15 -04:00
Cory LaViska
534966a7fc use <dialog> for popover 2025-06-02 14:57:39 -04:00
Cory LaViska
b10ec21780 update changelog 2025-06-02 14:48:04 -04:00
Cory LaViska
1ff5adfa73 update changelog 2025-06-02 14:35:30 -04:00
Cory LaViska
51677a38d9 add popover 2025-06-02 14:33:58 -04:00
Cory LaViska
698815a96b use actual placement, not preferred 2025-06-02 14:15:35 -04:00
Cory LaViska
3f62e09d51 rotate arrow based on placement so borders show correctly when applied 2025-06-02 14:13:43 -04:00
Cory LaViska
b5e7931dbc remove redundant styles from template 2025-06-02 13:51:01 -04:00
137 changed files with 1114 additions and 869 deletions

View File

@@ -4,7 +4,6 @@
name: Client Tests
on:
workflow_dispatch:
push:
branches: [next]
pull_request:

View File

@@ -1,23 +1,18 @@
# Files are relative to .prettierignore at the root of this monorepo.
# <https://github.com/prettier/prettier-vscode/issues/1252>
*.hbs
*.md
!packages/webawesome/docs/docs/patterns/**/*.md
!docs/docs/patterns/**/*.md
docs/docs/patterns/blog-news/post-list.md
**/*/.cache
.cache
.github
cspell.json
packages/**/*/dist
packages/**/*/dist-cdn
packages/**/*/docs/search.json
packages/**/*/src/components/icon/icons
packages/**/*/src/react/index.ts
**/*/package.json
**/*/package-lock.json
**/*/tsconfig.json
**/*/tsconfig.prod.json
dist
docs/search.json
src/components/icon/icons
src/react/index.ts
node_modules
packages/**/*/_site
packages/webawesome/docs/assets/scripts/prism-downloaded.js
package.json
package-lock.json
tsconfig.json
cdn
_site
docs/assets/scripts/prism-downloaded.js

23
package-lock.json generated
View File

@@ -2488,10 +2488,6 @@
"resolved": "packages/webawesome",
"link": true
},
"node_modules/@shoelace-style/webawesome-pro": {
"resolved": "packages/webawesome-pro",
"link": true
},
"node_modules/@sindresorhus/is": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
@@ -13991,25 +13987,6 @@
"engines": {
"node": ">=14.17.0"
}
},
"packages/webawesome-pro": {
"name": "@shoelace-style/webawesome-pro",
"version": "3.0.0-alpha.13",
"license": "TODO",
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
"@floating-ui/dom": "^1.6.13",
"@shoelace-style/animations": "^1.2.0",
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
},
"devDependencies": {},
"engines": {
"node": ">=14.17.0"
}
}
}
}

View File

@@ -3,9 +3,9 @@ import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
// import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
import { parse } from 'comment-parser';
import fs from 'fs';
import * as path from 'node:path';
import { pascalCase } from 'pascal-case';
import * as url from 'url';
import * as path from "node:path"
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const packageData = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
@@ -186,3 +186,4 @@ export default {
// })
],
};

View File

@@ -1,5 +1,5 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as fs from "node:fs"
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
import { codeExamplesPlugin } from './_utils/code-examples.js';
import { copyCodePlugin } from './_utils/copy-code.js';
@@ -41,7 +41,7 @@ export default async function (eleventyConfig) {
*/
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
const docsDir = path.join(process.env.BASE_DIR || '.', 'docs');
const docsDir = path.join(process.env.BASE_DIR || ".", 'docs');
const passThrough = [...passThroughExtensions.map(ext => path.join(docsDir, '**/*.' + ext))];
/**
@@ -176,8 +176,9 @@ export default async function (eleventyConfig) {
// eleventyConfig.addPlugin(formatCodePlugin());
// }
let assetsDir = path.join(process.env.BASE_DIR || 'docs', 'assets');
fs.cpSync(assetsDir, path.join(eleventyConfig.directories.output, 'assets'), { recursive: true });
let assetsDir = path.join(process.env.BASE_DIR || "docs", "assets")
fs.cpSync(assetsDir, path.join(eleventyConfig.directories.output, "assets"), { recursive: true })
for (let glob of passThrough) {
eleventyConfig.addPassthroughCopy(glob);
@@ -209,6 +210,7 @@ export default async function (eleventyConfig) {
// }
}
export const config = {
markdownTemplateEngine: 'njk',
dir: {
@@ -217,4 +219,5 @@ export const config = {
layouts: '_layouts',
},
templateFormats: ['njk', 'md'],
};
}

View File

@@ -2,13 +2,13 @@
* @module components Fetches components from custom-elements.json and exposes them in a saner format.
*/
import { readFileSync } from 'fs';
import { dirname, join, resolve } from 'path';
import { join, dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const customElementsJSON = process.env.DIST_DIR
? join(process.env.DIST_DIR, 'custom-elements.json')
: resolve(__dirname, '../../dist/custom-elements.json');
? join(process.env.DIST_DIR, "custom-elements.json")
: resolve(__dirname, '../../dist/custom-elements.json')
const manifest = JSON.parse(readFileSync(customElementsJSON), 'utf-8');
@@ -76,3 +76,4 @@ components.sort((a, b) => {
});
export default components;

View File

@@ -0,0 +1,43 @@
import components from './components.js';
const by = {
attribute: {},
slot: {},
event: {},
method: {},
cssPart: {},
cssProperty: {},
};
function getAll(component, type) {
let prop = type + 's';
if (type === 'cssProperty') {
prop = 'cssProperties';
}
return component[prop] ?? [];
}
for (const componentName in components) {
const component = components[componentName];
for (const type of ['attribute', 'slot', 'event', 'method', 'cssPart', 'cssProperty']) {
for (const item of getAll(component, type)) {
by[type][item.name] ??= [];
by[type][item.name].push(component);
}
}
}
// Sort by descending number of components
const sortByLengthDesc = (a, b) => b[1].length - a[1].length;
for (const key in by) {
by[key] = sortObject(by[key], sortByLengthDesc);
}
export default by;
function sortObject(obj, sorter) {
return Object.fromEntries(Object.entries(obj).sort(sorter));
}

View File

@@ -6,4 +6,3 @@ Prism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0
Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
!function(){if("undefined"!=typeof Prism){var n,s,a="";Prism.plugins.customClass={add:function(s){n=s},map:function(n){s="function"==typeof n?n:function(s){return n[s]||s}},prefix:function(n){a=n||""},apply:t},Prism.hooks.add("wrap",(function(e){if(n){var u=n({content:e.content,type:e.type,language:e.language});Array.isArray(u)?e.classes.push.apply(e.classes,u):u&&e.classes.push(u)}(s||a)&&(e.classes=e.classes.map((function(n){return t(n,e.language)})))}))}function t(n,t){return a+(s?s(n,t):n)}}();

View File

@@ -48,7 +48,7 @@ window.addEventListener('scroll', updateScrollClass);
window.addEventListener('turbo:render', updateScrollClass);
updateScrollClass();
// Restore scroll position after components are defined
// Restore scroll position after components are definedAdd commentMore actions
allDefined().then(() => {
const navigationType = getNavigationType();
const key = `wa-scroll-y-[${location.pathname}]`;

View File

@@ -127,7 +127,7 @@
> input {
font: inherit;
margin-block: 0.75em;
margin-block: calc(-1 * var(--wa-space-smaller));
field-sizing: content;
}

View File

@@ -0,0 +1,76 @@
let url = new URL(location);
const pushedURL = false;
const matchers = {
default(textContent, query) {
return textContent.includes(query);
},
i(textContent, query) {
return textContent.toLowerCase().includes(query.toLowerCase());
},
regexp(textContent, query) {
query.lastIndex = 0;
return query.test(textContent);
},
};
matchers.iregexp = matchers.regexp; // i is baked into the query
function filterByName(value) {
const previousFilter = url.searchParams.get('name') || '';
url = new URL(location);
if (value) {
const isRegexp = name_search_regexp.checked;
const i = !name_search_i.checked;
const query = isRegexp ? new RegExp(value, 'gmsv' + (i ? 'i' : '')) : value;
const matcherId = (i ? 'i' : '') + (isRegexp ? 'regexp' : '');
const matcher = matchers[matcherId] ?? matchers.default;
for (const th of document.querySelectorAll('table tbody th:first-child')) {
const tr = th.parentNode;
const matches = matcher(th.textContent, query);
tr.toggleAttribute('hidden', !matches);
}
url.searchParams.set('name', value);
if (matcherId) {
url.searchParams.set('match', matcherId);
} else {
url.searchParams.delete('match');
}
} else {
for (const tr of document.querySelectorAll('table tbody tr[hidden]')) {
tr.removeAttribute('hidden');
}
url.searchParams.delete('name');
url.searchParams.delete('match');
}
if (value !== previousFilter) {
history[pushedURL ? 'replaceState' : 'pushState'](null, '', url);
}
// Update heading counts
for (const h2 of document.querySelectorAll('h2:has(+ table)')) {
const count = h2.querySelector('.count');
if (!count) continue;
const table = h2.nextElementSibling;
const visibleRows = table.querySelectorAll('tbody tr:not([hidden])').length;
count.textContent = visibleRows;
const outlineLink = document.querySelector(`#outline-standard a[href="#${h2.id}"]`);
if (outlineLink) {
// Why not just = h2.textContent? To skip the "Jump to heading" link
outlineLink.textContent = '';
outlineLink.append(...[...h2.childNodes].slice(0, 3).map(n => n.cloneNode(true)));
}
}
}
if (name_search.value) {
filterByName(name_search.value);
}
name_search_group.addEventListener('input', e => filterByName(name_search.value));

View File

@@ -0,0 +1,89 @@
---
title: Component Cheatsheet
layout: docs
unlisted: true
---
<style>
table code {
white-space: nowrap;
}
</style>
<p>
This page lists every bit of syntax used by every Web Awesome component and which components share it.
For these times when your memory is failing, or to simply explore the possibilities!
</p>
<fieldset id="name_search_group">
<legend>Filter by name</legend>
<wa-input type="search" with-clear id="name_search"></wa-input>
<wa-checkbox id="name_search_i" checked>Case sensitive</wa-checkbox>
<wa-checkbox id="name_search_regexp">Regular expression</wa-checkbox>
</fieldset>
<script>
{
let url = new URL(location);
if (url.searchParams.get("name")) {
name_search.value = url.searchParams.get("name");
}
if (url.searchParams.get("match")) {
let matcherId = url.searchParams.get("match");
let caseSensitive = !matcherId.startsWith("i");
let isRegexp = matcherId.endsWith("regexp");
customElements.whenDefined("wa-checkbox").then(async () => {
await Promise.all([
name_search_i.updateComplete,
name_search_regexp.updateComplete,
]);
name_search_i.checked = caseSensitive;
name_search_regexp.checked = isRegexp;
});
}
}
</script>
<script type="module" src="/docs/components/cheatsheet.js"></script>
{% for type, all in componentsBy -%}
{% set typeTitle = "CSS custom properties" if type == "cssProperty" else ("CSS parts" if type == "cssPart" else (type | title) + "s") %}
<h2 id="{{ typeTitle | slugify }}">
All <span class="count">{{ (all | keys).length }}</span>
{{ typeTitle }}
</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Components</th>
</tr>
</thead>
{% for name, thingComponents in all -%}
<tr>
<th><code>{{ name }}{{ "()" if type == "method" }}</code></th>
<td>
{% set componentLinks = [] %}
{% for component in thingComponents %}
{%- set link -%}
<a href="../{{ component.slug }}"><code>&lt;{{ component.tagName }}&gt;</code></a>
{%- endset -%}
{# https://giuliachiola.dev/posts/add-items-to-an-array-in-nunjucks/ #}
{% set componentLinks = (componentLinks.push(link), componentLinks) %}
{%- endfor -%}
{% if thingComponents.length > 1 %}
<details open>
<summary><strong>{{ thingComponents.length }}</strong> components</summary>
{{ componentLinks | safe }}
</details>
{% else %}
{{ componentLinks | safe }}
{% endif %}
</td>
</tr>
{%- endfor %}
</table>
{%- endfor %}

View File

@@ -49,7 +49,7 @@ Use the `expand-icon` and `collapse-icon` slots to change the expand and collaps
</style>
```
### HTML in Summary
### HTML in summary
To use HTML in the summary, use the `summary` slot.
Links and other interactive elements will still retain their behavior:
@@ -67,7 +67,7 @@ Links and other interactive elements will still retain their behavior:
</wa-details>
```
### Right-to-Left Languages
### Right-to-Left languages
The details component automatically adapts to right-to-left languages:
@@ -104,23 +104,40 @@ Use the `appearance` attribute to change the elements visual appearance.
### Grouping Details
Use the `name` attribute to create accordion-like behavior where only one details element with the same name can be open at a time. This matches the behavior of native `<details>` elements.
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.
```html {.example}
<div class="wa-stack">
<wa-details name="group-1" summary="Section 1" open>
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.
<div class="details-group-example">
<wa-details summary="First" open>
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 name="group-1" summary="Section 2">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam,
eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
<wa-details summary="Second">
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 name="group-1" summary="Section 3">
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque
corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.
<wa-details summary="Third">
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>
```
<script>
const container = document.querySelector('.details-group-example');
// Close all other details when one is shown
container.addEventListener('wa-show', event => {
if (event.target.localName === 'wa-details') {
[...container.querySelectorAll('wa-details')].map(details => (details.open = event.target === details));
}
});
</script>
<style>
.details-group-example wa-details:not(:last-of-type) {
margin-bottom: var(--wa-space-2xs);
}
</style>
```

View File

@@ -67,28 +67,24 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
</script>
```
### Opening and Closing Dialogs Declaratively
### Dismissing Dialogs
You can open and close dialogs with JavaScript by toggling the `open` attribute, but you can also do it declaratively. Add the `data-dialog="open id"` to any button on the page, where `id` is the ID of the dialog you want to open.
You can add the special `data-dialog="close"` attribute to a button inside the dialog to tell it to close without additional JavaScript. Alternatively, you can set the `open` property to `false` to close the dialog programmatically.
```html {.example}
<wa-dialog label="Dialog" id="dialog-opening">
<wa-dialog label="Dialog" class="dialog-dismiss">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
<wa-button data-dialog="open dialog-opening">Open Dialog</wa-button>
```
<wa-button>Open Dialog</wa-button>
Similarly, you can add `data-dialog="close"` to a button _inside_ of a dialog to tell it to close.
<script>
const dialog = document.querySelector('.dialog-dismiss');
const openButton = dialog.nextElementSibling;
```html {.example}
<wa-dialog label="Dialog" id="dialog-dismiss">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
<wa-button data-dialog="open dialog-dismiss">Open Dialog</wa-button>
openButton.addEventListener('click', () => dialog.open = true);
</script>
```
### Custom Width

View File

@@ -65,28 +65,24 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
</script>
```
### Opening and Closing Drawers Declaratively
### Dismissing Drawers
You can open and close drawers with JavaScript by toggling the `open` attribute, but you can also do it declaratively. Add the `data-drawer="open id"` to any button on the page, where `id` is the ID of the drawer you want to open.
You can add the special `data-drawer="close"` attribute to a button inside the drawer to tell it to close without additional JavaScript. Alternatively, you can set the `open` property to `false` to close the drawer programmatically.
```html {.example}
<wa-drawer label="Drawer" id="drawer-opening">
<wa-drawer label="Drawer" class="drawer-dismiss">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
<wa-button data-drawer="open drawer-opening">Open Drawer</wa-button>
```
<wa-button>Open Drawer</wa-button>
Similarly, you can add `data-drawer="close"` to a button _inside_ of a drawer to tell it to close.
<script>
const drawer = document.querySelector('.drawer-dismiss');
const openButton = drawer.nextElementSibling;
```html {.example}
<wa-drawer label="Drawer" id="drawer-dismiss">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
<wa-button data-drawer="open drawer-dismiss">Open Drawer</wa-button>
openButton.addEventListener('click', () => drawer.open = true);
</script>
```
### Slide in From Start

View File

@@ -75,3 +75,14 @@ Add the `size` attribute to the [Radio Group](/docs/components/radio-group) to c
</wa-radio-group>
```
### Hint
Add descriptive hint to a switch with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
```html {.example}
<wa-radio-group label="Select an option" name="a" value="1">
<wa-radio value="1" hint="What should the user know about radio 1?">Option 1</wa-radio>
<wa-radio value="2" hint="What should the user know about radio 2?">Option 2</wa-radio>
<wa-radio value="3" hint="What should the user know about radio 3?">Option 3</wa-radio>
</wa-radio-group>
```

View File

@@ -131,7 +131,7 @@ You can provide custom icons by passing a function to the `getSymbol` property.
### Value-based Icons
You can also use the `getSymbol` property to render different icons based on value and/or whether the icon is currently selected.
You can also use the `getSymbol` property to render different icons based on value.
```html {.example}
<wa-rating label="Rating" class="rating-emojis"></wa-rating>
@@ -142,7 +142,7 @@ You can also use the `getSymbol` property to render different icons based on val
await customElements.whenDefined("wa-rating")
await rating.updateComplete
rating.getSymbol = (value, isSelected) => {
rating.getSymbol = value => {
const icons = ['face-angry', 'face-frown', 'face-meh', 'face-smile', 'face-laugh'];
return `<wa-icon name="${icons[value - 1]}"></wa-icon>`;
};

View File

@@ -31,29 +31,14 @@ During the alpha period, things might break! We take breaking changes very serio
- `<wa-tab-group no-scroll-controls>` => `<wa-tab-group without-scroll-controls>`
- `<wa-tag removable>` => `<wa-tag with-remove>`
- 🚨 BREAKING: removed the `size` attribute from `<wa-card>`; please set the size of child elements on the children directly
- 🚨 BREAKING: Greatly simplified the sizing strategy across components and utilities
- Removed `--wa-size`, `--wa-size-smaller`, `--wa-size-larger`, `--wa-space`, `--wa-space-smaller`, and `--wa-space-larger`
- Added tokens for `--wa-form-control-padding-inline`, `--wa-form-control-padding-block`, and `--wa-form-control-toggle-size`
- Refactored default `--wa-font-size-*` values to use an apparent 1.125 ratio and round rendered values to the nearest whole pixel
- Added convenience tokens for `--wa-font-size-smaller` and `--wa-font-size-larger`
- Updated components to use relative `em` values for internal padding and margin wherever appropriate
- 🚨 BREAKING: removed the `hint` property and slot from `<wa-radio>`; please apply hints directly to `<wa-radio-group>` instead
- Added a new free component: `<wa-popover>` (#2 of 14 per stretch goals)
- Added a `min-block-size` to `<wa-divider orientation="vertical">` to ensure the divider is visible regardless of container height [issue:675]
- Added support for `name` in `<wa-details>` for exclusively opening one in a group
- Added `--checked-icon-scale` to `<wa-checkbox>`
- Added `--tag-max-size` to `<wa-select>` when using `multiple`
- Added support for `data-dialog="open <id>"` to `<wa-dialog>`
- Added support for `data-drawer="open <id>"` to `<wa-drawer>`
- Fixed a bug in `<wa-radio-group>` that caused radios to uncheck when assigning a numeric value [issue:924]
- Fixed `<wa-button-group>` so dividers properly show between buttons
- Fixed the tooltip position in `<wa-slider>` when using RTL
- Fixed a bug in `<wa-details>` and native `<details>` styles that made the summary hard to click [issue:684]
- Fixed a handful of bugs unify form control height across components and native elements
- Improved CSS utilities and Native Styles to use [CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) for easier end user customization (no more specificity conflicts  your CSS wins!)
- Improved native `<button>` styles to properly space icons
- Improved button appearances in `<wa-color-picker>`
- Improved `<wa-rating>` to have more accessible icons by default
- Removed the experimental `<wa-code-demo>` component
## 3.0.0-alpha.13

View File

@@ -8,38 +8,31 @@ For components that share similar qualities, Web Awesome includes custom propert
## Form Controls
Components such as [input](/docs/components/input), [select](/docs/components/select), [textarea](/docs/components/textarea), [checkbox](/docs/components/checkbox), and others share a number of styles to give your forms a cohesive appearance. Web Awesome defines custom properties for these styles using the format `--wa-form-control-{style}`.
Components such as [input](/docs/components/input), [select](/docs/components/select), [textarea](/docs/components/textarea), [checkbox](/docs/components/checkbox), etc. share a number of styles to give your forms a cohesive appearance. Web Awesome defines custom properties for these styles using the format `--wa-form-control-{style}`.
Not every form control uses all of these custom properties. For example, `<wa-radio>` defines its own height and border radius to achieve its familiar shape but shares many other styles with other components for a cohesive look and feel. Similarly, `<wa-button>` defines many of its own styles but matches the height and border width of other form controls.
| Custom Property | Default Value |
| ------------------------------------------- | ------------------------------------- |
| `--wa-form-control-background-color` | `var(--wa-color-surface-default)` |
| `--wa-form-control-border-color` | `var(--wa-color-neutral-border-loud)` |
| `--wa-form-control-border-style` | `var(--wa-border-style)` |
| `--wa-form-control-border-width` | `var(--wa-border-width-s)` |
| `--wa-form-control-border-radius` | `var(--wa-border-radius-m)` |
| `--wa-form-control-activated-color` | `var(--wa-color-brand-fill-loud)` |
| `--wa-form-control-label-color` | `var(--wa-color-neutral-border-loud)` |
| `--wa-form-control-label-font-weight` | `var(--wa-font-weight-normal)` |
| `--wa-form-control-label-line-height` | `var(--wa-line-height-normal)` |
| `--wa-form-control-value-color` | `var(--wa-color-text-normal)` |
| `--wa-form-control-value-font-weight` | `var(--wa-font-weight-body)` |
| `--wa-form-control-value-line-height` | `var(--wa-line-height-condensed)` |
| `--wa-form-control-hint-color` | `var(--wa-color-text-quiet)` |
| `--wa-form-control-hint-font-weight` | `var(--wa-font-weight-body)` |
| `--wa-form-control-hint-line-height` | `var(--wa-line-height-normal)` |
| `--wa-form-control-placeholder-color` | `var(--wa-color-gray-60)` |
| `--wa-form-control-required-content` | `'*'` |
| `--wa-form-control-required-content-color` | `inherit` |
| `--wa-form-control-required-content-offset` | `-0.1em` |
| `--wa-form-control-padding-block` | `0.75em` |
| `--wa-form-control-padding-inline` | `1em` |
| `--wa-form-control-height` | `round(calc(2 * var(--wa-form-control-padding-block) + 1em * var(--wa-form-control-value-line-height)), 1px)` |
| `--wa-form-control-toggle-size` | `round(1.25em, 1px)` |
| Custom Property | Default Value |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `--wa-form-control-background-color` | `var(--wa-color-surface-default)` |
| `--wa-form-control-border-color` | `var(--wa-color-neutral-border-loud)` |
| `--wa-form-control-border-style` | `var(--wa-border-style)` |
| `--wa-form-control-border-width` | `var(--wa-border-width-s)` |
| `--wa-form-control-border-radius` | `var(--wa-border-radius-m)` |
| `--wa-form-control-activated-color` | `var(--wa-color-brand-fill-loud)` |
| `--wa-form-control-label-color` | `var(--wa-color-neutral-border-loud)` |
| `--wa-form-control-label-font-weight` | `var(--wa-font-weight-normal)` |
| `--wa-form-control-label-line-height` | `var(--wa-line-height-normal)` |
| `--wa-form-control-value-color` | `var(--wa-color-text-normal)` |
| `--wa-form-control-value-font-weight` | `var(--wa-font-weight-body)` |
| `--wa-form-control-value-line-height` | `var(--wa-line-height-condensed)` |
| `--wa-form-control-placeholder-color` | `var(--wa-color-gray-60)` |
| `--wa-form-control-required-content` | `'*'` |
| `--wa-form-control-required-content-color` | `inherit` |
| `--wa-form-control-required-content-offset` | `-0.1em` |
```html {.example}
<form class="wa-stack">
<form class="wa-block-spacing-l">
<wa-input label="Input" placeholder="Placeholder"></wa-input>
<wa-select label="Select" value="option-1">
<wa-option value="option-1">Option 1</wa-option>
@@ -57,6 +50,19 @@ Not every form control uses all of these custom properties. For example, `<wa-ra
<wa-slider label="Range"></wa-slider>
<wa-button>Button</wa-button>
</form>
<style>
.wa-block-spacing-l > * + *, wa-radio {
display: block;
margin-block-start: var(--wa-space-l);
}
wa-radio {
margin-block-start: var(--wa-space-2xs);
}
wa-radio, wa-checkbox, wa-switch, wa-button {
width: fit-content;
}
</style>
```
## Panels
@@ -70,7 +76,7 @@ Panels consist of components with larger, contained surface areas like [callout]
| `--wa-panel-border-radius` | `var(--wa-border-radius-l)` |
```html {.example}
<div class="wa-stack">
<div class="wa-block-spacing-l">
<wa-callout>
<wa-icon slot="icon" name="circle-info" variant="regular"></wa-icon>
This is a simple callout with an icon.
@@ -80,6 +86,13 @@ Panels consist of components with larger, contained surface areas like [callout]
<code>wa-details</code>, at your service.
</wa-details>
</div>
<style>
.wa-block-spacing-l > * + * {
display: block;
margin-block-start: var(--wa-space-l);
}
</style>
```
## Tooltips

View File

@@ -35,19 +35,21 @@ description: Lock down consistent spacing Web Awesome's space properties.
Space properties are used intentionally throughout Web Awesome to create predictable rhythm and meaningful proximity. These properties use `rem` units in order to scale proportionately with the root font size.
You can use `--wa-space-scale` to increase or decrease all spacing at once. By default, this multiplier is `1`.
Each space property uses a `calc()` function with `--wa-space-scale` to scale all spacing at once. By default, this multiplier is `1`. The table below lists the result of the calculation.
The calculations for each size and the resulting pixel value (assuming a 16px root font size) are listed below.
| Custom Property | Default Value | Preview |
| ---------------- | ------------------------------- | --------------------------------------------------------------------- |
| `--wa-space-3xs` | `0.125rem` <small>(2px)</small> | <div class="spacing-example" style="width: var(--wa-space-3xs)"></div> |
| `--wa-space-2xs` | `0.25rem` <small>(4px)</small> | <div class="spacing-example" style="width: var(--wa-space-2xs)"></div> |
| `--wa-space-xs` | `0.5rem` <small>(8px)</small> | <div class="spacing-example" style="width: var(--wa-space-xs)"></div> |
| `--wa-space-s` | `0.75rem` <small>(12px)</small> | <div class="spacing-example" style="width: var(--wa-space-s)"></div> |
| `--wa-space-m` | `1rem` <small>(16px)</small> | <div class="spacing-example" style="width: var(--wa-space-m)"></div> |
| `--wa-space-l` | `1.25rem` <small>(20px)</small> | <div class="spacing-example" style="width: var(--wa-space-l)"></div> |
| `--wa-space-xl` | `1.5rem` <small>(24px)</small> | <div class="spacing-example" style="width: var(--wa-space-xl)"></div> |
| `--wa-space-2xl` | `2rem` <small>(32px)</small> | <div class="spacing-example" style="width: var(--wa-space-2xl)"></div> |
| `--wa-space-3xl` | `3rem` <small>(48px)</small> | <div class="spacing-example" style="width: var(--wa-space-3xl)"></div> |
| Custom Property | Default Value | Preview - |
| ---------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `--wa-space-3xs` | `calc(var(--wa-space-scale) * 0.125rem)` <small>(2px)</small> | <div class="spacing-example" style="width: var(--wa-space-3xs)"></div> |
| `--wa-space-2xs` | `calc(var(--wa-space-scale) * 0.25rem)` <small>(4px)</small> | <div class="spacing-example" style="width: var(--wa-space-2xs)"></div> |
| `--wa-space-xs` | `calc(var(--wa-space-scale) * 0.5rem)` <small>(8px)</small> | <div class="spacing-example" style="width: var(--wa-space-xs)"></div> |
| `--wa-space-s` | `calc(var(--wa-space-scale) * 0.75rem)` <small>(12px)</small> | <div class="spacing-example" style="width: var(--wa-space-s)"></div> |
| `--wa-space-m` | `calc(var(--wa-space-scale) * 1rem)` <small>(16px)</small> | <div class="spacing-example" style="width: var(--wa-space-m)"></div> |
| `--wa-space-l` | `calc(var(--wa-space-scale) * 1.5rem)` <small>(24px)</small> | <div class="spacing-example" style="width: var(--wa-space-l)"></div> |
| `--wa-space-xl` | `calc(var(--wa-space-scale) * 2rem)` <small>(32px)</small> | <div class="spacing-example" style="width: var(--wa-space-xl)"></div> |
| `--wa-space-2xl` | `calc(var(--wa-space-scale) * 2.5rem)` <small>(40px)</small> | <div class="spacing-example" style="width: var(--wa-space-2xl)"></div> |
| `--wa-space-3xl` | `calc(var(--wa-space-scale) * 3rem)` <small>(48px)</small> | <div class="spacing-example" style="width: var(--wa-space-3xl)"></div> |
| `--wa-space-4xl` | `calc(var(--wa-space-scale) * 4rem)` <small>(64px)</small> | <div class="spacing-example" style="width: var(--wa-space-4xl)"></div> |
When using space properties, it may be helpful to consider three distinct groups:
- Small-scale space (`3xs`, `2xs`, and `xs`) can be used for gaps between cooperating elements, such as a dropdown button and its menu, and padding within small components, such as badges and tooltips
- Normal space (`s`, `m`, and `l`) can be used for gaps between related elements with distinct touch targets and padding within typical interface elements, such as buttons and inputs
- Large-scale space (`xl`, `2xl`, and `3xl`) can be used for gaps between unrelated elements and padding within larger components, such as cards and dialogs

View File

@@ -17,32 +17,21 @@ Font families are assigned specific roles &mdash; like heading or code &mdash; t
## Font Size
Font sizes use a ratio of 1.125 to scale sizes proportionally. Starting with the medium (`m`) font size, smaller sizes (`s` through `2xs`) are 1.125x smaller as the sizes decrease, and larger sizes (`l` through `4xl`) are _twice_ 1.125x larger as sizes increase — here, the ratio is doubled to maximize impact between sizes.
Font sizes use the Major Second type scale, rounded to the nearest whole pixel assuming a 16px root font size. To maximize variation in larger font sizes, every other step on the scale is skipped.
Each value uses `rem` units and is rounded to the nearest whole pixel when rendered with [`round()`](https://developer.mozilla.org/en-US/docs/Web/CSS/round).
You can use `--wa-font-size-scale` to increase or decrease all font sizes at once. By default, this multiplier is `1`.
The calculations for each size and the resulting pixel value (assuming a 16px root font size) are listed below.
Each font size uses a `calc()` function with `--wa-font-size-scale` to scale all font sizes at once. By default, this multiplier is `1`. The table below lists the result of the calculation.
| Custom Property | Default Value | Preview |
| -------------------- | --------------------------------- | ---------------------------------------------------------- |
| `--wa-font-size-2xs` | `round(calc(var(--wa-font-size-xs) / 1.125), 1px)` <small>(11px)</small> | <div style="font-size: var(--wa-font-size-2xs)">AaBb</div> |
| `--wa-font-size-xs` | `round(calc(var(--wa-font-size-s) / 1.125), 1px)` <small>(12px)</small> | <div style="font-size: var(--wa-font-size-xs)">AaBb</div> |
| `--wa-font-size-s` | `round(calc(var(--wa-font-size-m) / 1.125), 1px)` <small>(14px)</small> | <div style="font-size: var(--wa-font-size-s)">AaBb</div> |
| `--wa-font-size-m` | `calc(1rem * var(--wa-font-size-scale))` <small>(16px)</small> | <div style="font-size: var(--wa-font-size-m)">AaBb</div> |
| `--wa-font-size-l` | `round(calc(var(--wa-font-size-m) * 1.125 * 1.125), 1px)` <small>(20px)</small> | <div style="font-size: var(--wa-font-size-l)">AaBb</div> |
| `--wa-font-size-xl` | `round(calc(var(--wa-font-size-l) * 1.125 * 1.125), 1px)` <small>(25px)</small> | <div style="font-size: var(--wa-font-size-xl)">AaBb</div> |
| `--wa-font-size-2xl` | `round(calc(var(--wa-font-size-xl) * 1.125 * 1.125), 1px)` <small>(32px)</small> | <div style="font-size: var(--wa-font-size-2xl)">AaBb</div> |
| `--wa-font-size-3xl` | `round(calc(var(--wa-font-size-2xl) * 1.125 * 1.125), 1px)` <small>(41px)</small> | <div style="font-size: var(--wa-font-size-3xl)">AaBb</div> |
| `--wa-font-size-4xl` | `round(calc(var(--wa-font-size-3xl) * 1.125 * 1.125)` <small>(52px)</small> | <div style="font-size: var(--wa-font-size-4xl)">AaBb</div> |
You can also use these two custom properties make any font size proportionally smaller or larger to its parent.
| Custom Property | Default Value |
| ------------------------ | --------------------------------------- |
| `--wa-font-size-smaller` | `round(calc(1em / 1.125), 1px)` |
| `--wa-font-size-larger` | `round(calc(1em * 1.125 * 1.125), 1px)` |
| `--wa-font-size-2xs` | `0.6875rem` <small>(11px)</small> | <div style="font-size: var(--wa-font-size-2xs)">AaBb</div> |
| `--wa-font-size-xs` | `0.75rem` <small>(12px)</small> | <div style="font-size: var(--wa-font-size-xs)">AaBb</div> |
| `--wa-font-size-s` | `0.875rem` <small>(14px)</small> | <div style="font-size: var(--wa-font-size-s)">AaBb</div> |
| `--wa-font-size-m` | `1rem` <small>(16px)</small> | <div style="font-size: var(--wa-font-size-m)">AaBb</div> |
| `--wa-font-size-l` | `1.25rem` <small>(20px)</small> | <div style="font-size: var(--wa-font-size-l)">AaBb</div> |
| `--wa-font-size-xl` | `1.625rem` <small>(26px)</small> | <div style="font-size: var(--wa-font-size-xl)">AaBb</div> |
| `--wa-font-size-2xl` | `2rem` <small>(32px)</small> | <div style="font-size: var(--wa-font-size-2xl)">AaBb</div> |
| `--wa-font-size-3xl` | `2.5625rem` <small>(41px)</small> | <div style="font-size: var(--wa-font-size-3xl)">AaBb</div> |
| `--wa-font-size-4xl` | `3.25rem` <small>(52px)</small> | <div style="font-size: var(--wa-font-size-4xl)">AaBb</div> |
## Font Weight

View File

@@ -7,11 +7,11 @@ file: styles/utilities/variants.css
Some Web Awesome components, like `<wa-button>`, allow you to change the color by using a `variant` attribute:
- [`<wa-badge>`](/docs/components/badge)
- [`<wa-button>`](/docs/components/button)
- [`<wa-button-group>`](/docs/components/button-group)
- [`<wa-callout>`](/docs/components/callout)
- [`<wa-tag>`](/docs/components/tag)
{% for component in componentsBy.attribute.variant %}
{% if component.fileSlug != "icon" or component.fileSlug != "icon-button" -%}
- <a href="../{{ component.url }}"><code>&lt;{{ component.tagName }}&gt;</code></a>
{%- endif %}
{%- endfor %}
You can create the same effect on any element by using the color variant utility classes:

View File

@@ -54,7 +54,7 @@
"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 --ignore-path=\"../../.prettierignore\" .",
"prettier": "prettier --check --log-level=warn --ignore-path=\"../../.prettierignore\".",
"prettier:fix": "prettier --write --log-level=warn --ignore-path=\"../../.prettierignore\" .",
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress --config=\"../../cspell.json\"",
"verify": "npm run prettier && npm run build && npm run test",

View File

@@ -8,12 +8,19 @@ import { replace } from 'esbuild-plugin-replace';
import { mkdir, readFile } from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { globby } from 'globby';
import ora from 'ora';
import { dirname, join, relative } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import ora from 'ora';
import copy from 'recursive-copy';
import { getCdnDir, getDistDir, getDocsDir, getRootDir, getSiteDir, runScript } from './utils.js';
import { fileURLToPath } from 'node:url';
import {
getCdnDir,
getDistDir,
getDocsDir,
getRootDir,
getSiteDir,
runScript,
} from './utils.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const isDeveloping = process.argv.includes('--develop');
@@ -35,18 +42,19 @@ let buildContexts = {
/**
* @param {BuildOptions} [options={}]
*/
export async function build(options = {}) {
export async function build (options = {}) {
if (!options.watchedSrcDirectories) {
options.watchedSrcDirectories = ['src'];
options.watchedSrcDirectories = ['src']
}
if (!options.watchedDocsDirectories) {
options.watchedDocsDirectories = [getDocsDir()];
options.watchedDocsDirectories = [getDocsDir()]
}
/**
* Runs the full build.
*/
* Runs the full build.
*/
async function buildAll() {
const start = Date.now();
@@ -84,8 +92,8 @@ export async function build(options = {}) {
}
/**
* Analyzes components and generates the custom elements manifest file.
*/
* Analyzes components and generates the custom elements manifest file.
*/
function generateManifest() {
spinner.start('Generating CEM');
@@ -105,14 +113,14 @@ export async function build(options = {}) {
}
/**
* Generates React wrappers for all components.
*/
* Generates React wrappers for all components.
*/
function generateReactWrappers() {
spinner.start('Generating React wrappers');
try {
// need to run make-react from this directories.
execSync(`node ${join(__dirname, 'make-react.js')} --outdir "${getCdnDir()}"`, { stdio: 'inherit' });
execSync(`node ${join(__dirname, "make-react.js")} --outdir "${getCdnDir()}"`, { stdio: 'inherit' });
} catch (error) {
console.error(`\n\n${error.message}`);
@@ -126,8 +134,8 @@ export async function build(options = {}) {
}
/**
* Copies theme stylesheets to the dist.
*/
* Copies theme stylesheets to the dist.
*/
async function generateStyles() {
spinner.start('Copying stylesheets');
@@ -139,20 +147,20 @@ export async function build(options = {}) {
}
/**
* Runs TypeScript to generate types.
*/
* Runs TypeScript to generate types.
*/
async function generateTypes() {
spinner.start('Running the TypeScript compiler');
const cwd = process.cwd();
const cwd = process.cwd()
try {
if (process.env.ROOT_DIR) {
process.chdir(process.env.ROOT_DIR);
process.chdir(process.env.ROOT_DIR)
}
execSync(`tsc --project ./tsconfig.prod.json --outdir "${getCdnDir()}"`);
process.chdir(cwd);
process.chdir(cwd)
} catch (error) {
process.chdir(cwd);
process.chdir(cwd)
if (!isDeveloping) {
process.exit(1);
}
@@ -165,12 +173,12 @@ export async function build(options = {}) {
}
/**
* Runs esbuild to generate the final dist.
*/
* Runs esbuild to generate the final dist.
*/
async function generateBundle() {
spinner.start('Bundling with esbuild');
const rootDir = process.env.ROOT_DIR || '.';
const rootDir = process.env.ROOT_DIR || "."
// Bundled config
const config = {
format: 'esm',
@@ -238,8 +246,8 @@ export async function build(options = {}) {
}
/**
* Incrementally rebuilds the source files. Must be called only after `generateBundle()` has been called.
*/
* Incrementally rebuilds the source files. Must be called only after `generateBundle()` has been called.
*/
async function regenerateBundle() {
try {
spinner.start('Re-bundling with esbuild');
@@ -258,12 +266,12 @@ export async function build(options = {}) {
}
/**
* Generates the documentation site.
*/
* Generates the documentation site.
*/
async function generateDocs() {
/**
* Used by the webawesome-app to skip doc generation since it will do its own.
*/
* Used by the webawesome-app to skip doc generation since it will do its own.
*/
if (process.env.SKIP_ELEVENTY === 'true') {
return;
}
@@ -367,19 +375,19 @@ export async function build(options = {}) {
// TODO: Should probably listen for all of these instead of just "change"
const watchEvents = [
'change',
"change",
// "unlink",
// "add"
];
]
// Rebuild and reload when source files change
options.watchedSrcDirectories.forEach(dir => {
const watcher = bs.watch(join(dir, '**', '!(*.test).*'));
options.watchedSrcDirectories.forEach((dir) => {
const watcher = bs.watch(join(dir, "**", "!(*.test).*"))
watchEvents.forEach(evt => {
watcher.on(evt, handleWatchEvent(evt));
});
function handleWatchEvent(evt) {
return async filename => {
watchEvents.forEach((evt) => {
watcher.on(evt, handleWatchEvent(evt))
})
function handleWatchEvent (evt) {
return async (filename) => {
spinner.info(`File modified ${chalk.gray(`(${relative(getRootDir(), filename)})`)}`);
try {
@@ -393,8 +401,8 @@ export async function build(options = {}) {
return;
}
if (typeof options.onWatchEvent === 'function') {
await options.onWatchEvent(evt, filename);
if (typeof options.onWatchEvent === "function") {
await options.onWatchEvent(evt, filename)
}
await regenerateBundle();
@@ -419,29 +427,29 @@ export async function build(options = {}) {
process.exit(1);
}
}
};
}
}
});
})
// Rebuild the docs and reload when the docs change
options.watchedDocsDirectories.forEach(dir => {
const watcher = bs.watch(join(dir, '**', '*.*'));
options.watchedDocsDirectories.forEach((dir) => {
const watcher = bs.watch(join(dir, "**", "*.*"))
watchEvents.forEach(evt => {
watcher.on(evt, handleWatchEvent(evt));
});
watchEvents.forEach((evt) => {
watcher.on(evt, handleWatchEvent(evt))
})
function handleWatchEvent(evt) {
return async filename => {
function handleWatchEvent (evt) {
return async (filename) => {
spinner.info(`File modified ${chalk.gray(`(${relative(getRootDir(), filename)})`)}`);
if (typeof options.onWatchEvent === 'function') {
await options.onWatchEvent(evt, filename);
if (typeof options.onWatchEvent === "function") {
await options.onWatchEvent(evt, filename)
}
await generateDocs();
reload();
};
}
}
});
})
}
//
@@ -460,23 +468,22 @@ export async function build(options = {}) {
process.on('SIGINT', terminate);
process.on('SIGTERM', terminate);
}
}
// https://exploringjs.com/nodejs-shell-scripting/ch_nodejs-path.html#detecting-if-module-is-main
// Detects if this was called via node scripts/build.js
function isRunAsMain() {
if (import.meta.url.startsWith('file:')) {
// (A)
function isRunAsMain () {
if (import.meta.url.startsWith('file:')) { // (A)
const modulePath = fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
// (B)
return true;
if (process.argv[1] === modulePath) { // (B)
return true
}
}
return false;
return false
}
if (isRunAsMain()) {
await build();
await build()
}

View File

@@ -1,11 +1,11 @@
import Eleventy from '@11ty/eleventy';
import { deleteAsync } from 'del';
import { join } from 'path';
import { getDocsDir, getEleventyConfigPath, getSiteDir } from './utils.js';
import { getDocsDir, getSiteDir } from './utils.js';
const elev = new Eleventy(getDocsDir(), getSiteDir(), {
quietMode: true,
configPath: getEleventyConfigPath(),
configPath: join(getDocsDir(), '.eleventy.js'),
});
// Cleanup
@@ -13,3 +13,4 @@ await deleteAsync(getSiteDir());
// Write it
await elev.write();

View File

@@ -8,8 +8,8 @@ import { getAllComponents } from './shared.js';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const reactDir = path.join(process.env.ROOT_DIR || '.', 'src', 'react');
const srcDir = process.env.ROOT_DIR ? path.join(process.env.ROOT_DIR, 'src') : '.';
const reactDir = path.join(process.env.ROOT_DIR || ".", 'src', 'react');
const srcDir = process.env.ROOT_DIR ? path.join(process.env.ROOT_DIR, "src") : "."
// Clear build directory
deleteSync(reactDir);
@@ -25,7 +25,7 @@ for await (const component of components) {
const tagWithoutPrefix = component.tagName.replace(/^wa-/, '');
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
const importPath = path.relative(srcDir, component.path);
const importPath = path.relative(srcDir, component.path)
// We only want to wrap wa- prefixed events, because the others are native
const eventsToWrap = component.events?.filter(event => event.name.startsWith('wa-')) || [];
@@ -81,3 +81,4 @@ for await (const component of components) {
// Generate the index file
fs.writeFileSync(path.join(reactDir, 'index.ts'), index.join('\n'), 'utf8');

View File

@@ -21,7 +21,7 @@ import styles from './{{ tagWithoutPrefix tag }}.css';
*/
@customElement("{{ tag }}")
export default class {{ properCase tag }} extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
/** An example attribute. */
@property() attr = 'example';

View File

@@ -11,7 +11,6 @@ export const getDistDir = () => process.env.DIST_DIR || join(getRootDir(), 'dist
export const getCdnDir = () => process.env.CDN_DIR || join(getRootDir(), 'dist-cdn');
export const getDocsDir = () => process.env.DOCS_DIR || join(getRootDir(), 'docs');
export const getSiteDir = () => process.env.SITE_DIR || join(getRootDir(), '_site');
export const getEleventyConfigPath = () => process.env.ELEVENTY_CONFIG_PATH || join(getDocsDir(), '.eleventy.js');
/**
* Runs a script and returns a promise that resolves with the content of stdout when the script exits or rejects with
@@ -58,3 +57,4 @@ export function runScript(scriptPath, args = [], options = {}) {
});
});
}

View File

@@ -28,7 +28,7 @@ import styles from './animated-image.css';
*/
@customElement('wa-animated-image')
export default class WaAnimatedImage extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
@query('.animated') animatedImage: HTMLImageElement;

View File

@@ -23,7 +23,7 @@ import { animations } from './animations.js';
*/
@customElement('wa-animation')
export default class WaAnimation extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private animation?: Animation;
private hasStarted = false;

View File

@@ -29,7 +29,7 @@ import styles from './avatar.css';
*/
@customElement('wa-avatar')
export default class WaAvatar extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
@state() private hasError = false;

View File

@@ -21,7 +21,7 @@ import styles from './badge.css';
*/
@customElement('wa-badge')
export default class WaBadge extends WebAwesomeElement {
static css = [variantStyles, appearanceStyles, styles];
static shadowStyle = [variantStyles, appearanceStyles, styles];
/** The badge's theme variant. Defaults to `brand` if not within another element with a variant. */
@property({ reflect: true }) variant: 'brand' | 'neutral' | 'success' | 'warning' | 'danger' = 'brand';

View File

@@ -24,7 +24,7 @@ import styles from './breadcrumb-item.css';
*/
@customElement('wa-breadcrumb-item')
export default class WaBreadcrumbItem extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;

View File

@@ -21,7 +21,7 @@ import styles from './breadcrumb.css';
*/
@customElement('wa-breadcrumb')
export default class WaBreadcrumb extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
private separatorDir = this.localize.dir();

View File

@@ -20,7 +20,7 @@ import styles from './button-group.css';
*/
@customElement('wa-button-group')
export default class WaButtonGroup extends WebAwesomeElement {
static css = [sizeStyles, variantStyles, styles];
static shadowStyle = [sizeStyles, variantStyles, styles];
@query('slot') defaultSlot: HTMLSlotElement;
@@ -37,7 +37,7 @@ export default class WaButtonGroup extends WebAwesomeElement {
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';
/** The component's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large'; // unset by default to not override child elements
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
/** The button group's theme variant. Defaults to `neutral` if not within another element with a variant. */
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral';
@@ -85,7 +85,7 @@ export default class WaButtonGroup extends WebAwesomeElement {
if (button) {
if ((button as WaButton).appearance === 'outlined') this.hasOutlined = true;
if (this.size) button.setAttribute('size', this.size);
button.setAttribute('size', this.size);
button.classList.add('wa-button-group__button');
button.classList.toggle('wa-button-group__horizontal', this.orientation === 'horizontal');
button.classList.toggle('wa-button-group__vertical', this.orientation === 'vertical');

View File

@@ -36,9 +36,9 @@
transition-duration: var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
cursor: pointer;
padding: 0 var(--wa-form-control-padding-inline);
padding: 0 var(--wa-space, var(--wa-space-m));
font-family: inherit;
font-size: inherit;
font-size: var(--wa-size, var(--wa-font-size-m));
font-weight: var(--wa-font-weight-action);
line-height: calc(var(--wa-form-control-height) - var(--border-width) * 2);
height: var(--wa-form-control-height);
@@ -182,12 +182,12 @@ button ::slotted(wa-badge) {
*/
slot[name='prefix']::slotted(*) {
margin-inline-end: var(--wa-form-control-padding-inline);
margin-inline-end: var(--wa-space);
}
slot[name='suffix']::slotted(*),
.button:not(.visually-hidden-label) [part~='caret'] {
margin-inline-start: var(--wa-form-control-padding-inline);
margin-inline-start: var(--wa-space);
}
/*

View File

@@ -53,7 +53,7 @@ import styles from './button.css';
*/
@customElement('wa-button')
export default class WaButton extends WebAwesomeFormAssociatedElement {
static css = [styles, variantStyles, sizeStyles, appearanceStyles];
static shadowStyle = [styles, variantStyles, sizeStyles, appearanceStyles];
static get validators() {
return [...super.validators, MirrorValidator()];

View File

@@ -24,7 +24,7 @@ import styles from './callout.css';
*/
@customElement('wa-callout')
export default class WaCallout extends WebAwesomeElement {
static css = [variantStyles, appearanceStyles, sizeStyles, styles];
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, styles];
/** The callout's theme variant. Defaults to `brand` if not within another element with a variant. */
@property({ reflect: true }) variant: 'brand' | 'neutral' | 'success' | 'warning' | 'danger' | 'brand' = 'brand';

View File

@@ -1,5 +1,9 @@
:host {
--spacing: var(--wa-space-l);
--space-s: var(--wa-space-l);
--space-m: var(--wa-space-xl);
--space-l: var(--wa-space-2xl);
--spacing: var(--wa-space);
--border-width: var(--wa-panel-border-width);
--outlined-background-color: var(--wa-color-surface-default);
--outlined-border-color: var(--wa-color-surface-border);

View File

@@ -26,11 +26,11 @@ import styles from './card.css';
* @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-l)] - The amount of space around and between sections of the card. 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 css = [sizeStyles, appearanceStyles, styles];
static shadowStyle = [sizeStyles, appearanceStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'media');

View File

@@ -16,7 +16,7 @@ import styles from './carousel-item.css';
*/
@customElement('wa-carousel-item')
export default class WaCarouselItem extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
connectedCallback() {
super.connectedCallback();

View File

@@ -52,7 +52,7 @@ import styles from './carousel.css';
*/
@customElement('wa-carousel')
export default class WaCarousel extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
/** When set, allows the user to navigate the carousel in the same direction indefinitely. */
@property({ type: Boolean, reflect: true }) loop = false;

View File

@@ -11,8 +11,7 @@
--border-width: var(--wa-form-control-border-width);
--box-shadow: none;
--checked-icon-color: var(--wa-color-brand-on-loud);
--checked-icon-scale: 0.8;
--toggle-size: var(--wa-form-control-toggle-size);
--toggle-size: 1lh;
color: var(--wa-form-control-value-color);
display: inline-flex;
@@ -44,7 +43,7 @@
color var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
margin-inline-end: 0.5em;
margin-inline-end: var(--wa-space-xs);
}
:host [part~='base'] {
@@ -87,7 +86,6 @@ input {
[part~='icon'] {
display: flex;
scale: var(--checked-icon-scale);
/* Without this, Safari renders the icon slightly to the left */
&::part(svg) {

View File

@@ -199,17 +199,17 @@ describe('<wa-checkbox>', () => {
expect(checkbox.checkValidity()).to.be.false;
expect(checkbox.checkValidity()).to.be.false;
expect(checkbox.customStates.has('invalid')).to.be.true;
expect(checkbox.customStates.has('valid')).to.be.false;
expect(checkbox.customStates.has('user-invalid')).to.be.true;
expect(checkbox.customStates.has('user-valid')).to.be.false;
expect(checkbox.hasCustomState('invalid')).to.be.true;
expect(checkbox.hasCustomState('valid')).to.be.false;
expect(checkbox.hasCustomState('user-invalid')).to.be.true;
expect(checkbox.hasCustomState('user-valid')).to.be.false;
await clickOnElement(checkbox);
await checkbox.updateComplete;
await aTimeout(0);
expect(checkbox.customStates.has('user-invalid')).to.be.true;
expect(checkbox.customStates.has('user-valid')).to.be.false;
expect(checkbox.hasCustomState('user-invalid')).to.be.true;
expect(checkbox.hasCustomState('user-valid')).to.be.false;
});
it('should be invalid when required and unchecked', async () => {
@@ -244,12 +244,12 @@ describe('<wa-checkbox>', () => {
`);
const checkbox = el.querySelector<WaCheckbox>('wa-checkbox')!;
expect(checkbox.customStates.has('required')).to.be.true;
expect(checkbox.customStates.has('optional')).to.be.false;
expect(checkbox.customStates.has('invalid')).to.be.true;
expect(checkbox.customStates.has('valid')).to.be.false;
expect(checkbox.customStates.has('user-invalid')).to.be.false;
expect(checkbox.customStates.has('user-valid')).to.be.false;
expect(checkbox.hasCustomState('required')).to.be.true;
expect(checkbox.hasCustomState('optional')).to.be.false;
expect(checkbox.hasCustomState('invalid')).to.be.true;
expect(checkbox.hasCustomState('valid')).to.be.false;
expect(checkbox.hasCustomState('user-invalid')).to.be.false;
expect(checkbox.hasCustomState('user-valid')).to.be.false;
});
});

View File

@@ -55,7 +55,7 @@ import styles from './checkbox.css';
*/
@customElement('wa-checkbox')
export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
static css = [formControlStyles, sizeStyles, styles];
static shadowStyle = [formControlStyles, sizeStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
@@ -156,14 +156,14 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
this.input.indeterminate = this.indeterminate; // force a sync update
}
this.customStates.set('checked', this.checked);
this.customStates.set('indeterminate', this.indeterminate);
this.toggleCustomState('checked', this.checked);
this.toggleCustomState('indeterminate', this.indeterminate);
this.updateValidity();
}
@watch('disabled')
handleDisabledChange() {
this.customStates.set('disabled', this.disabled);
this.toggleCustomState('disabled', this.disabled);
}
protected willUpdate(changedProperties: PropertyValues<this>): void {

View File

@@ -14,7 +14,7 @@
--slider-handle-size: calc(var(--slider-height) + 0.25rem);
--swatch-border-radius: var(--wa-border-radius-m);
--swatch-size: 1.5rem;
--trigger-border-radius: var(--wa-form-control-border-radius);
--trigger-border-radius: var(--wa-border-radius-circle);
}
.color-picker {
@@ -301,7 +301,6 @@
background-color: transparent;
border: none;
cursor: pointer;
font-size: inherit;
forced-color-adjust: none;
width: var(--wa-form-control-height);
height: var(--wa-form-control-height);
@@ -319,7 +318,7 @@
background-color: currentColor;
box-shadow:
inset 0 0 0 var(--border-width) var(--wa-form-control-border-color),
inset 0 0 0 calc(var(--border-width) * 3) var(--wa-color-surface-default);
inset 0 0 0 calc(var(--border-width) * 2) var(--wa-color-surface-default);
}
.trigger-empty:before {

View File

@@ -501,12 +501,12 @@ describe('<wa-color-picker>', () => {
const grid = el.shadowRoot!.querySelector('[part~="grid"]')!;
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('required')).to.be.true;
expect(el.hasCustomState('optional')).to.be.false;
expect(el.hasCustomState('invalid')).to.be.false;
expect(el.hasCustomState('valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.false;
await clickOnElement(trigger);
await aTimeout(500);
@@ -514,8 +514,8 @@ describe('<wa-color-picker>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -523,12 +523,12 @@ describe('<wa-color-picker>', () => {
const trigger = el.shadowRoot!.querySelector('[part~="trigger"]')!;
const grid = el.shadowRoot!.querySelector('[part~="grid"]')!;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('required')).to.be.true;
expect(el.hasCustomState('optional')).to.be.false;
expect(el.hasCustomState('invalid')).to.be.true;
expect(el.hasCustomState('valid')).to.be.false;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.false;
await clickOnElement(trigger);
await aTimeout(500);
@@ -536,8 +536,8 @@ describe('<wa-color-picker>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
});
});
});

View File

@@ -102,7 +102,7 @@ declare const EyeDropper: EyeDropperConstructor;
*/
@customElement('wa-color-picker')
export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
static css = [visuallyHidden, sizeStyles, formControlStyles, styles];
static shadowStyle = [visuallyHidden, sizeStyles, formControlStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
@@ -1017,7 +1017,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
<wa-button
part="format-button"
size="small"
appearance="outlined"
aria-label=${this.localize.term('toggleColorFormat')}
exportparts="
base:format-button__base,
@@ -1039,7 +1038,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
<wa-button
part="eye-dropper-button"
size="small"
appearance="outlined"
exportparts="
base:eye-dropper-button__base,
prefix:eye-dropper-button__prefix,

View File

@@ -38,7 +38,7 @@ import styles from './comparison.css';
*/
@customElement('wa-comparison')
export default class WaComparison extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
@@ -55,12 +55,12 @@ export default class WaComparison extends WebAwesomeElement {
drag(this, {
onMove: x => {
this.customStates.set('dragging', true);
this.toggleCustomState('dragging', true);
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
if (isRtl) this.position = 100 - this.position;
},
onStop: () => {
this.customStates.set('dragging', false);
this.toggleCustomState('dragging', false);
},
initialEvent: event,
});

View File

@@ -17,7 +17,7 @@
border-radius: var(--wa-border-radius-m);
color: inherit;
font-size: inherit;
padding: 0.5em;
padding: var(--wa-space-xs);
cursor: pointer;
transition: color var(--wa-transition-fast) var(--wa-transition-easing);
}

View File

@@ -44,7 +44,7 @@ import styles from './copy-button.css';
*/
@customElement('wa-copy-button')
export default class WaCopyButton extends WebAwesomeElement {
static css = [visuallyHidden, styles];
static shadowStyle = [visuallyHidden, styles];
private readonly localize = new LocalizeController(this);

View File

@@ -46,7 +46,7 @@ import styles from './details.css';
*/
@customElement('wa-details')
export default class WaDetails extends WebAwesomeElement {
static css = [appearanceStyles, styles];
static shadowStyle = [appearanceStyles, styles];
private detailsObserver: MutationObserver;
private readonly localize = new LocalizeController(this);
@@ -65,9 +65,6 @@ export default class WaDetails extends WebAwesomeElement {
/** The summary to show in the header. If you need to display HTML, use the `summary` slot instead. */
@property() summary: string;
/** Groups related details elements. When one opens, others with the same name will close. */
@property() name: string;
/** Disables the details so it can't be toggled. */
@property({ type: Boolean, reflect: true }) disabled = false;
@@ -141,20 +138,6 @@ export default class WaDetails extends WebAwesomeElement {
}
}
/** Closes other <wa-details> elements in the same document when they have the same name. */
private closeOthersWithSameName() {
if (!this.name) return;
const root = this.getRootNode() as Document | ShadowRoot;
const otherDetails = root.querySelectorAll(`wa-details[name="${this.name}"]`) as NodeListOf<WaDetails>;
otherDetails.forEach(detail => {
if (detail !== this && detail.open) {
detail.open = false;
}
});
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
@@ -168,9 +151,6 @@ export default class WaDetails extends WebAwesomeElement {
return;
}
// Close other details with the same name
this.closeOthersWithSameName();
const duration = parseDuration(getComputedStyle(this.body).getPropertyValue('--show-duration'));
// We can't animate to 'auto', so use the scroll height for now
await animate(

View File

@@ -6,7 +6,6 @@ import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
import { WaShowEvent } from '../../events/show.js';
import { animateWithClass } from '../../internal/animate.js';
import { parseSpaceDelimitedTokens } from '../../internal/parse.js';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js';
import { HasSlotController } from '../../internal/slot.js';
import { watch } from '../../internal/watch.js';
@@ -55,7 +54,7 @@ import styles from './dialog.css';
*/
@customElement('wa-dialog')
export default class WaDialog extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header-actions', 'label');
@@ -265,28 +264,6 @@ export default class WaDialog extends WebAwesomeElement {
}
}
//
// Watch for data-dialog="open *" clicks
//
document.addEventListener('click', (event: MouseEvent) => {
const dialogAttrEl = (event.target as Element).closest('[data-dialog]');
if (dialogAttrEl instanceof Element) {
const [command, id] = parseSpaceDelimitedTokens(dialogAttrEl.getAttribute('data-dialog') || '');
if (command === 'open' && id?.length) {
const doc = dialogAttrEl.getRootNode() as Document | ShadowRoot;
const dialog = doc.getElementById(id) as WaDialog;
if (dialog?.localName === 'wa-dialog') {
dialog.open = true;
} else {
console.warn(`A dialog with an ID of "${id}" could not be found in this document.`);
}
}
}
});
// Ugly, but it fixes light dismiss in Safari: https://bugs.webkit.org/show_bug.cgi?id=267688
if (!isServer) {
document.addEventListener('pointerdown', () => {

View File

@@ -15,7 +15,7 @@ import styles from './divider.css';
*/
@customElement('wa-divider')
export default class WaDivider extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
/** Sets the divider's orientation. */
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';

View File

@@ -6,7 +6,6 @@ import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
import { WaShowEvent } from '../../events/show.js';
import { animateWithClass } from '../../internal/animate.js';
import { parseSpaceDelimitedTokens } from '../../internal/parse.js';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js';
import { HasSlotController } from '../../internal/slot.js';
import { watch } from '../../internal/watch.js';
@@ -33,9 +32,9 @@ import styles from './drawer.css';
* @event wa-hide - Emitted when the drawer closes.
* @event wa-after-hide - Emitted after the drawer closes and all animations are complete.
* @event {{ source: Element }} wa-hide - Emitted when the drawer is requesting to close. Calling
* `event.preventDefault()` will prevent the drawer from closing. You can inspect `event.detail.source` to see which
* element caused the drawer to close. If the source is the drawer element itself, the user has pressed [[Escape]] or
* the drawer has been closed programmatically. Avoid using this unless closing the drawer will result in destructive
* `event.preventDefault()` will prevent the dialog from closing. You can inspect `event.detail.source` to see which
* element caused the dialog to close. If the source is the dialog element itself, the user has pressed [[Escape]] or
* the dialog has been closed programmatically. Avoid using this unless closing the dialog will result in destructive
* behavior such as data loss.
*
* @csspart header - The drawer's header. This element wraps the title and header actions.
@@ -60,7 +59,7 @@ import styles from './drawer.css';
*/
@customElement('wa-drawer')
export default class WaDrawer extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header-actions', 'label');
@@ -281,28 +280,6 @@ export default class WaDrawer extends WebAwesomeElement {
}
}
//
// Watch for data-drawer="open *" clicks
//
document.addEventListener('click', (event: MouseEvent) => {
const drawerAttrEl = (event.target as Element).closest('[data-drawer]');
if (drawerAttrEl instanceof Element) {
const [command, id] = parseSpaceDelimitedTokens(drawerAttrEl.getAttribute('data-drawer') || '');
if (command === 'open' && id?.length) {
const doc = drawerAttrEl.getRootNode() as Document | ShadowRoot;
const drawer = doc.getElementById(id) as WaDrawer;
if (drawer?.localName === 'wa-drawer') {
drawer.open = true;
} else {
console.warn(`A drawer with an ID of "${id}" could not be found in this document.`);
}
}
}
});
if (!isServer) {
// Ugly, but it fixes light dismiss in Safari: https://bugs.webkit.org/show_bug.cgi?id=267688
document.body.addEventListener('pointerdown', () => {

View File

@@ -44,7 +44,7 @@ import styles from './dropdown.css';
*/
@customElement('wa-dropdown')
export default class WaDropdown extends WebAwesomeElement {
static css = [sizeStyles, styles];
static shadowStyle = [sizeStyles, styles];
@query('.dropdown') popup: WaPopup;
@query('#trigger') trigger: HTMLSlotElement;

View File

@@ -17,7 +17,7 @@
border-radius: var(--wa-border-radius-m);
font-size: inherit;
color: inherit;
padding: 0.5em;
padding: var(--wa-space-xs);
cursor: pointer;
transition: color var(--wa-transition-fast) var(--wa-transition-easing);
-webkit-appearance: none;

View File

@@ -26,7 +26,7 @@ import styles from './icon-button.css';
*/
@customElement('wa-icon-button')
export default class WaIconButton extends WebAwesomeFormAssociatedElement {
static css = styles;
static shadowStyle = styles;
@query('.icon-button') button: HTMLButtonElement | HTMLLinkElement;

View File

@@ -41,7 +41,7 @@ interface IconSource {
*/
@customElement('wa-icon')
export default class WaIcon extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
@state() private svg: SVGElement | HTMLTemplateResult | null = null;

View File

@@ -33,7 +33,6 @@ export const icons: { [key: string]: { [key: string]: string } } = {
copy: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M384 336H192c-8.8 0-16-7.2-16-16V64c0-8.8 7.2-16 16-16l140.1 0L400 115.9V320c0 8.8-7.2 16-16 16zM192 384H384c35.3 0 64-28.7 64-64V115.9c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1H192c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H256c35.3 0 64-28.7 64-64V416H272v32c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V192c0-8.8 7.2-16 16-16H96V128H64z"/></svg>`,
eye: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256c13.6 30 40.2 72.5 78.6 108.3C169.2 402.4 222.8 432 288 432s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80zM95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6zM288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80c-.7 0-1.3 0-2 0c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2c0 .7 0 1.3 0 2c0 44.2 35.8 80 80 80zm0-208a128 128 0 1 1 0 256 128 128 0 1 1 0-256z"/></svg>`,
'eye-slash': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm51.3 163.3l-41.9-33C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5zm-88-69.3L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8z"/></svg>`,
star: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L433.6 328.4l26.2 155.6c1.5 9-2.2 18.1-9.7 23.5s-17.3 6-25.3 1.7l-137-73.2L151 509.1c-8.1 4.3-17.9 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l26.2-155.6L31.1 218.2c-6.5-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L266.3 13.5C270.4 5.2 278.7 0 287.9 0zm0 79L235.4 187.2c-3.5 7.1-10.2 12.1-18.1 13.3L99 217.9 184.9 303c5.5 5.5 8.1 13.3 6.8 21L171.4 443.7l105.2-56.2c7.1-3.8 15.6-3.8 22.6 0l105.2 56.2L384.2 324.1c-1.3-7.7 1.2-15.5 6.8-21l85.9-85.1L358.6 200.5c-7.8-1.2-14.6-6.1-18.1-13.3L287.9 79z"/></svg>',
},
};

View File

@@ -18,7 +18,7 @@ import { requestInclude } from './request.js';
*/
@customElement('wa-include')
export default class WaInclude extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
/**
* The location of the HTML file to include. Be sure you trust the content you are including as it will be executed as

View File

@@ -22,7 +22,7 @@
border-width: var(--border-width);
cursor: text;
color: var(--wa-form-control-value-color);
font-size: var(--wa-form-control-value-font-size);
font-size: var(--wa-size);
font-family: inherit;
font-weight: var(--wa-form-control-value-font-weight);
line-height: var(--wa-form-control-value-line-height);
@@ -35,7 +35,7 @@
transition-timing-function: var(--wa-transition-easing);
background-color: var(--background-color, var(--wa-form-control-background-color));
box-shadow: var(--box-shadow);
padding: 0 var(--wa-form-control-padding-inline);
padding: var(--wa-space-smaller) var(--wa-space);
&:focus-within {
outline: var(--wa-focus-ring);
@@ -142,11 +142,11 @@ textarea {
}
.prefix::slotted(*) {
margin-inline-end: var(--wa-form-control-padding-inline);
margin-inline-end: var(--wa-space);
}
.suffix::slotted(*) {
margin-inline-start: var(--wa-form-control-padding-inline);
margin-inline-start: var(--wa-space);
}
/*

View File

@@ -108,12 +108,12 @@ describe('<wa-input>', () => {
const el = await fixture<WaInput>(html` <wa-input required value="a"></wa-input> `);
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('required')).to.be.true;
expect(el.hasCustomState('optional')).to.be.false;
expect(el.hasCustomState('invalid')).to.be.false;
expect(el.hasCustomState('valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.false;
el.focus();
await el.updateComplete;
@@ -123,19 +123,19 @@ describe('<wa-input>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
const el = await fixture<WaInput>(html` <wa-input required></wa-input> `);
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('required')).to.be.true;
expect(el.hasCustomState('optional')).to.be.false;
expect(el.hasCustomState('invalid')).to.be.true;
expect(el.hasCustomState('valid')).to.be.false;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.false;
el.focus();
await el.updateComplete;
@@ -145,20 +145,20 @@ describe('<wa-input>', () => {
el.blur();
await el.updateComplete;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-input required></wa-input></form> `);
const input = el.querySelector<WaInput>('wa-input')!;
expect(input.customStates.has('required')).to.be.true;
expect(input.customStates.has('optional')).to.be.false;
expect(input.customStates.has('invalid')).to.be.true;
expect(input.customStates.has('valid')).to.be.false;
expect(input.customStates.has('user-invalid')).to.be.false;
expect(input.customStates.has('user-valid')).to.be.false;
expect(input.hasCustomState('required')).to.be.true;
expect(input.hasCustomState('optional')).to.be.false;
expect(input.hasCustomState('invalid')).to.be.true;
expect(input.hasCustomState('valid')).to.be.false;
expect(input.hasCustomState('user-invalid')).to.be.false;
expect(input.hasCustomState('user-valid')).to.be.false;
});
});
@@ -215,10 +215,10 @@ describe('<wa-input>', () => {
await input.updateComplete;
expect(input.checkValidity()).to.be.false;
expect(input.customStates.has('invalid')).to.be.true;
expect(input.customStates.has('valid')).to.be.false;
expect(input.customStates.has('user-invalid')).to.be.false;
expect(input.customStates.has('user-valid')).to.be.false;
expect(input.hasCustomState('invalid')).to.be.true;
expect(input.hasCustomState('valid')).to.be.false;
expect(input.hasCustomState('user-invalid')).to.be.false;
expect(input.hasCustomState('user-valid')).to.be.false;
input.focus();
await sendKeys({ type: 'test' });
@@ -226,8 +226,8 @@ describe('<wa-input>', () => {
input.blur();
await input.updateComplete;
expect(input.customStates.has('user-invalid')).to.be.true;
expect(input.customStates.has('user-valid')).to.be.false;
expect(input.hasCustomState('user-invalid')).to.be.true;
expect(input.hasCustomState('user-valid')).to.be.false;
});
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {

View File

@@ -57,7 +57,7 @@ import styles from './input.css';
*/
@customElement('wa-input')
export default class WaInput extends WebAwesomeFormAssociatedElement {
static css = [sizeStyles, appearanceStyles, formControlStyles, styles];
static shadowStyle = [sizeStyles, appearanceStyles, formControlStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
@@ -300,7 +300,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.customStates.set('blank', !this.value);
this.toggleCustomState('blank', !this.value);
}
}

View File

@@ -9,7 +9,7 @@
display: flex;
align-items: stretch;
font: inherit;
padding: 0.5em 0.25em;
padding: var(--wa-space-xs) var(--wa-space-2xs);
line-height: var(--wa-line-height-condensed);
transition: fill var(--wa-transition-normal) var(--wa-transition-easing);
user-select: none;
@@ -39,11 +39,11 @@
:host([loading]) wa-spinner {
--indicator-color: currentColor;
--track-width: round(0.0625em, 1px);
--track-width: 0.0625rem;
position: absolute;
font-size: var(--wa-font-size-smaller);
font-size: 0.8em;
top: calc(50% - 0.5em);
left: 0.6em;
left: 0.5rem;
opacity: 1;
}
@@ -61,7 +61,7 @@
}
.prefix::slotted(*) {
margin-inline-end: 0.5em;
margin-inline-end: var(--wa-space-xs);
}
.suffix {
@@ -71,7 +71,7 @@
}
.suffix::slotted(*) {
margin-inline-start: 0.5em;
margin-inline-start: var(--wa-space-xs);
}
/* Safe triangle */
@@ -114,8 +114,8 @@
display: flex;
align-items: center;
justify-content: center;
font-size: var(--wa-font-size-smaller);
width: 2em;
font-size: 0.875em;
width: var(--wa-space-xl);
visibility: hidden;
}

View File

@@ -43,7 +43,7 @@ import { SubmenuController } from './submenu-controller.js';
*/
@customElement('wa-menu-item')
export default class WaMenuItem extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
@@ -133,7 +133,7 @@ export default class WaMenuItem extends WebAwesomeElement {
this.dispatchEvent(new Event('slotchange', { bubbles: true, composed: false, cancelable: false }));
}
this.customStates.set('has-submenu', this.isSubmenu());
this.toggleCustomState('has-submenu', this.isSubmenu());
}
private handleHostClick = (event: MouseEvent) => {
@@ -201,7 +201,7 @@ export default class WaMenuItem extends WebAwesomeElement {
render() {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isSubmenuExpanded = this.submenuController.isExpanded();
this.customStates.set('submenu-expanded', isSubmenuExpanded);
this.toggleCustomState('submenu-expanded', isSubmenuExpanded);
this.internals.ariaHasPopup = this.isSubmenu() + '';
this.internals.ariaExpanded = isSubmenuExpanded + '';

View File

@@ -1,9 +1,9 @@
:host {
display: block;
color: var(--wa-color-text-quiet);
font-size: var(--wa-font-size-smaller);
font-size: var(--wa-font-size-s);
font-weight: var(--wa-font-weight-semibold);
padding: 0.5em 2.25em;
padding: var(--wa-space-2xs) calc(var(--wa-space-2xs) + var(--wa-space-xl));
-webkit-user-select: none;
user-select: none;
}

View File

@@ -13,7 +13,7 @@ import styles from './menu-label.css';
*/
@customElement('wa-menu-label')
export default class WaMenuLabel extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
render() {
return html`<slot></slot>`;

View File

@@ -5,11 +5,11 @@
background-color: var(--wa-color-surface-raised);
border: var(--wa-border-style) var(--wa-border-width-s) var(--wa-color-surface-border);
border-radius: var(--wa-border-radius-m);
padding: 0.5em 0;
padding: var(--wa-space-xs) 0;
overflow: auto;
overscroll-behavior: none;
}
::slotted(wa-divider) {
--spacing: 0.5em;
--spacing: var(--wa-space-xs);
}

View File

@@ -25,7 +25,7 @@ export interface MenuSelectEventDetail {
*/
@customElement('wa-menu')
export default class WaMenu extends WebAwesomeElement {
static css = [sizeStyles, styles];
static shadowStyle = [sizeStyles, styles];
/** The component's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';

View File

@@ -17,7 +17,7 @@ import styles from './mutation-observer.css';
*/
@customElement('wa-mutation-observer')
export default class WaMutationObserver extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private mutationObserver: MutationObserver;

View File

@@ -13,7 +13,7 @@
display: flex;
align-items: center;
font: inherit;
padding: 0.5em 1em 0.5em 0.25em;
padding: var(--wa-space-xs) var(--wa-space-m) var(--wa-space-xs) var(--wa-space-2xs);
line-height: var(--wa-line-height-condensed);
transition: fill var(--wa-transition-normal) var(--wa-transition-easing);
cursor: pointer;
@@ -51,9 +51,9 @@
display: flex;
align-items: center;
justify-content: center;
font-size: var(--wa-font-size-smaller);
font-size: 0.875em;
visibility: hidden;
width: 2em;
width: var(--wa-space-xl);
}
:host(:state(selected)) .check {
@@ -68,11 +68,11 @@
}
.prefix::slotted(*) {
margin-inline-end: 0.5em;
margin-inline-end: var(--wa-space-xs);
}
.suffix::slotted(*) {
margin-inline-start: 0.5em;
margin-inline-start: var(--wa-space-xs);
}
@media (forced-colors: active) {

View File

@@ -35,7 +35,7 @@ import styles from './option.css';
*/
@customElement('wa-option')
export default class WaOption extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
// @ts-expect-error - Controller is currently unused
private readonly localize = new LocalizeController(this);
@@ -130,9 +130,9 @@ export default class WaOption extends WebAwesomeElement {
// We need this because Safari doesn't honor :hover styles while dragging
// Test case: https://codepen.io/leaverou/pen/VYZOOjy
if (event.type === 'mouseenter') {
this.customStates.set('hover', true);
this.toggleCustomState('hover', true);
} else if (event.type === 'mouseleave') {
this.customStates.set('hover', false);
this.toggleCustomState('hover', false);
}
};
@@ -145,7 +145,7 @@ export default class WaOption extends WebAwesomeElement {
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
this.customStates.set('selected', this.selected);
this.toggleCustomState('selected', this.selected);
}
if (changedProperties.has('value')) {
@@ -165,7 +165,7 @@ export default class WaOption extends WebAwesomeElement {
}
if (changedProperties.has('current')) {
this.customStates.set('current', this.current);
this.toggleCustomState('current', this.current);
}
}

View File

@@ -128,7 +128,7 @@ function toLength(px: number | string): string {
*/
@customElement('wa-page')
export default class WaPage extends WebAwesomeElement {
static css = [visuallyHidden, styles];
static shadowStyle = [visuallyHidden, styles];
private headerResizeObserver = this.slotResizeObserver('header');
private subheaderResizeObserver = this.slotResizeObserver('subheader');

View File

@@ -79,7 +79,7 @@
flex-direction: column;
width: max-content;
max-width: var(--max-width);
padding: var(--wa-space-l);
padding: var(--wa-space);
background-color: var(--wa-color-surface-default);
border: var(--wa-panel-border-width) solid var(--wa-color-surface-border);
border-radius: var(--wa-panel-border-radius);

View File

@@ -1,4 +1,3 @@
import type { PropertyValues } from 'lit';
import { html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -41,12 +40,10 @@ const openPopovers = new Set<WaPopover>();
* @cssproperty [--max-width=25rem] - The maximum width of the popover's body content.
* @cssproperty [--show-duration=100ms] - The speed of the show animation.
* @cssproperty [--hide-duration=100ms] - The speed of the hide animation.
*
* @cssstate open - Applied when the popover is open.
*/
@customElement('wa-popover')
export default class WaPopover extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
static dependencies = { 'wa-popup': WaPopup };
@query('dialog') dialog: HTMLDialogElement;
@@ -113,12 +110,6 @@ export default class WaPopover extends WebAwesomeElement {
}
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('open')) {
this.customStates.set('open', this.open);
}
}
private handleAnchorClick = () => {
// Clicks on the anchor should toggle the popover
this.open = !this.open;

View File

@@ -68,7 +68,7 @@ const SUPPORTS_POPOVER = globalThis?.HTMLElement?.prototype.hasOwnProperty('popo
*/
@customElement('wa-popup')
export default class WaPopup extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private anchorEl: Element | VirtualElement | null;
private cleanup: ReturnType<typeof autoUpdate> | undefined;

View File

@@ -24,7 +24,7 @@ import styles from './progress-bar.css';
*/
@customElement('wa-progress-bar')
export default class WaProgressBar extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
/** The current progress as a percentage, 0 to 100. */

View File

@@ -25,7 +25,7 @@ import styles from './progress-ring.css';
*/
@customElement('wa-progress-ring')
export default class WaProgressRing extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);

View File

@@ -18,7 +18,7 @@ let QrCreator: _QrCreator.default;
*/
@customElement('wa-qr-code')
export default class WaQrCode extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
@query('canvas') canvas: HTMLElement;

View File

@@ -26,18 +26,18 @@
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 0.75em;
gap: var(--wa-space-s);
}
/* Horizontal */
:host([orientation='horizontal']) [part~='form-control-input'] {
flex-direction: row;
gap: 1em;
gap: var(--wa-space-m);
}
/* Help text */
[part~='hint'] {
margin-block-start: 0.5em;
margin-block-start: var(--wa-space-xs);
}
/* Radios have the "button" appearance */

View File

@@ -2,7 +2,7 @@ import { aTimeout, expect, oneEvent } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import { html } from 'lit';
import sinon from 'sinon';
// import { clickOnElement } from '../../internal/test.js';
import { clickOnElement } from '../../internal/test.js';
import { fixtures } from '../../internal/test/fixture.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
import type WaRadio from '../radio/radio.js';
@@ -99,22 +99,19 @@ describe('<wa-radio-group>', () => {
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
expect(radioGroup.checkValidity()).to.be.true;
expect(radioGroup.customStates.has('required')).to.be.true;
expect(radioGroup.customStates.has('optional')).to.be.false;
expect(radioGroup.customStates.has('invalid')).to.be.false;
expect(radioGroup.customStates.has('valid')).to.be.true;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
expect(radioGroup.hasCustomState('required')).to.be.true;
expect(radioGroup.hasCustomState('optional')).to.be.false;
expect(radioGroup.hasCustomState('invalid')).to.be.false;
expect(radioGroup.hasCustomState('valid')).to.be.true;
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
// TODO: Go back to clickOnElement when we can determine why CI is not cleaning up elements.
// await clickOnElement(secondRadio);
secondRadio.click();
await clickOnElement(secondRadio);
await secondRadio.updateComplete;
await radioGroup.updateComplete;
expect(radioGroup.checkValidity()).to.be.true;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.true;
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
expect(radioGroup.hasCustomState('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -126,22 +123,19 @@ describe('<wa-radio-group>', () => {
`);
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
expect(radioGroup.customStates.has('required')).to.be.true;
expect(radioGroup.customStates.has('optional')).to.be.false;
expect(radioGroup.customStates.has('invalid')).to.be.true;
expect(radioGroup.customStates.has('valid')).to.be.false;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
expect(radioGroup.hasCustomState('required')).to.be.true;
expect(radioGroup.hasCustomState('optional')).to.be.false;
expect(radioGroup.hasCustomState('invalid')).to.be.true;
expect(radioGroup.hasCustomState('valid')).to.be.false;
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
// TODO: Go back to clickOnElement when we can determine why CI is not cleaning up elements.
// await clickOnElement(secondRadio);
secondRadio.click();
await radioGroup.updateComplete;
await clickOnElement(secondRadio);
radioGroup.value = '';
await radioGroup.updateComplete;
expect(radioGroup.customStates.has('user-invalid')).to.be.true;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
expect(radioGroup.hasCustomState('user-invalid')).to.be.true;
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -155,12 +149,12 @@ describe('<wa-radio-group>', () => {
`);
const radioGroup = el.querySelector<WaRadioGroup>('wa-radio-group')!;
expect(radioGroup.customStates.has('required')).to.be.true;
expect(radioGroup.customStates.has('optional')).to.be.false;
expect(radioGroup.customStates.has('invalid')).to.be.true;
expect(radioGroup.customStates.has('valid')).to.be.false;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
expect(radioGroup.hasCustomState('required')).to.be.true;
expect(radioGroup.hasCustomState('optional')).to.be.false;
expect(radioGroup.hasCustomState('invalid')).to.be.true;
expect(radioGroup.hasCustomState('valid')).to.be.false;
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
});
it('should show a constraint validation error when setCustomValidity() is called', async () => {

View File

@@ -37,7 +37,7 @@ import styles from './radio-group.css';
*/
@customElement('wa-radio-group')
export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
static css = [sizeStyles, formControlStyles, styles];
static shadowStyle = [sizeStyles, formControlStyles, styles];
static get validators() {
const validators = isServer

View File

@@ -7,8 +7,8 @@
--border-width: var(--wa-form-control-border-width);
--box-shadow: none;
--checked-icon-color: var(--wa-form-control-activated-color);
--checked-icon-scale: 0.7;
--toggle-size: var(--wa-form-control-toggle-size);
--checked-icon-scale: 0.75;
--toggle-size: round(1lh, 1px);
color: var(--wa-form-control-value-color);
display: inline-flex;
@@ -36,7 +36,7 @@
}
[part~='hint'] {
margin-block-start: 0.5em;
margin-block-start: var(--wa-space-3xs);
}
/* Default appearance */
@@ -63,7 +63,7 @@
color var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
margin-inline-end: 0.5em;
margin-inline-end: var(--wa-space-xs);
}
.checked-icon {
@@ -101,7 +101,7 @@
background-color: var(--wa-color-surface-default);
border: var(--border-width) var(--border-style) var(--wa-form-control-border-color);
border-radius: var(--wa-border-radius-m);
padding: 0 var(--wa-form-control-padding-inline);
padding: 0 var(--wa-space);
transition:
background-color var(--wa-transition-fast),
border-color var(--wa-transition-fast);

View File

@@ -1,6 +1,8 @@
import type { PropertyValues } from 'lit';
import { html, isServer } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { HasSlotController } from '../../internal/slot.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import formControlStyles from '../../styles/component/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
@@ -16,6 +18,7 @@ import styles from './radio.css';
* @dependency wa-icon
*
* @slot - The radio's label.
* @slot hint - Text that describes how to use the checkbox. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the control loses focus.
* @event focus - Emitted when the control gains focus.
@@ -23,6 +26,7 @@ import styles from './radio.css';
* @csspart control - The circular container that wraps the radio's checked state.
* @csspart checked-icon - The checked icon.
* @csspart label - The container that wraps the radio's label.
* @csspart hint - The hint's wrapper.
*
* @cssproperty --background-color - The radio's background color.
* @cssproperty --background-color-checked - The radio's background color when checked.
@@ -40,7 +44,7 @@ import styles from './radio.css';
*/
@customElement('wa-radio')
export default class WaRadio extends WebAwesomeFormAssociatedElement {
static css = [formControlStyles, sizeStyles, styles];
static shadowStyle = [formControlStyles, sizeStyles, styles];
@state() checked = false;
@@ -64,6 +68,11 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
/** Disables the radio. */
@property({ type: Boolean }) disabled = false;
/** The radio's hint. If you need to display HTML, use the `hint` slot instead. */
@property() hint = '';
private readonly hasSlotController = new HasSlotController(this, 'hint');
constructor() {
super();
if (!isServer) {
@@ -86,13 +95,13 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('checked')) {
this.customStates.set('checked', this.checked);
this.toggleCustomState('checked', this.checked);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
}
if (changedProperties.has('disabled')) {
this.customStates.set('disabled', this.disabled);
this.toggleCustomState('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
}
@@ -111,6 +120,9 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
};
render() {
const hasHintSlot = isServer ? true : this.hasSlotController.test('hint');
const hasHint = this.hint ? true : !!hasHintSlot;
return html`
<span part="control" class="control">
${this.checked
@@ -123,6 +135,15 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
</span>
<slot part="label" class="label"></slot>
<slot
name="hint"
aria-hidden=${hasHint ? 'false' : 'true'}
class="${classMap({ 'has-slotted': hasHint })}"
id="hint"
part="hint"
>${this.hint}</slot
>
`;
}
}

View File

@@ -1,7 +1,13 @@
:host {
--symbol-color: var(--wa-color-neutral-on-quiet);
--size-xs: var(--wa-space-s);
--size-s: var(--wa-space-m);
--size-m: var(--wa-space-l);
--size-l: var(--wa-space-xl);
--symbol-color: var(--wa-color-neutral-fill-normal);
--symbol-color-active: var(--wa-color-yellow-70);
--symbol-spacing: 0.125em;
--symbol-size: var(--wa-size);
--symbol-spacing: var(--wa-space-3xs);
display: inline-flex;
}

View File

@@ -33,7 +33,7 @@ import styles from './rating.css';
*/
@customElement('wa-rating')
export default class WaRating extends WebAwesomeElement {
static css = [sizeStyles, styles];
static shadowStyle = [sizeStyles, styles];
private readonly localize = new LocalizeController(this);
@@ -68,11 +68,8 @@ export default class WaRating extends WebAwesomeElement {
* The function should return a string containing trusted HTML of the symbol to render at the specified value. Works
* well with `<wa-icon>` elements.
*/
@property() getSymbol: (value: number, isSelected: boolean) => string = (_value, isSelected) => {
return isSelected
? '<wa-icon name="star" library="system" variant="solid"></wa-icon>'
: '<wa-icon name="star" library="system" variant="regular"></wa-icon>';
};
@property() getSymbol: (value: number) => string = () =>
'<wa-icon name="star" library="system" variant="solid"></wa-icon>';
/** The component's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@@ -257,8 +254,6 @@ export default class WaRating extends WebAwesomeElement {
>
<span class="symbols">
${counter.map(index => {
const isSelected = displayValue >= index + 1;
if (displayValue > index && displayValue < index + 1) {
// Users can click the current value to clear the rating. When this happens, we set this.isHovering to
// false to prevent the hover state from confusing them as they move the mouse out of the control. This
@@ -279,7 +274,7 @@ export default class WaRating extends WebAwesomeElement {
: `inset(0 0 0 ${(displayValue - index) * 100}%)`,
})}
>
${unsafeHTML(this.getSymbol(index + 1, false))}
${unsafeHTML(this.getSymbol(index + 1))}
</div>
<div
class="partial-filled"
@@ -289,7 +284,7 @@ export default class WaRating extends WebAwesomeElement {
: `inset(0 ${100 - (displayValue - index) * 100}% 0 0)`,
})}
>
${unsafeHTML(this.getSymbol(index + 1, true))}
${unsafeHTML(this.getSymbol(index + 1))}
</div>
</span>
`;
@@ -304,7 +299,7 @@ export default class WaRating extends WebAwesomeElement {
})}
role="presentation"
>
${unsafeHTML(this.getSymbol(index + 1, isSelected))}
${unsafeHTML(this.getSymbol(index + 1))}
</span>
`;
})}

View File

@@ -17,7 +17,7 @@ import styles from './resize-observer.css';
*/
@customElement('wa-resize-observer')
export default class WaResizeObserver extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private resizeObserver: ResizeObserver;
private observedElements: HTMLElement[] = [];

View File

@@ -20,7 +20,7 @@ import styles from './scroller.css';
*/
@customElement('wa-scroller')
export default class WaScroller extends WebAwesomeElement {
static css = [styles];
static shadowStyle = [styles];
private readonly localize = new LocalizeController(this);
private resizeObserver = new ResizeObserver(() => this.updateScroll());

View File

@@ -6,7 +6,32 @@ label:has(select),
--outlined-text-color: var(--wa-form-control-value-color);
--border-width: var(--wa-form-control-border-width);
--box-shadow: initial;
--tag-max-size: 10ch;
}
:host [part~='combobox'] {
background-color: var(--background-color, var(--wa-form-control-background-color));
border-color: var(--border-color, var(--wa-form-control-border-color));
border-radius: var(--wa-form-control-border-radius);
border-style: var(--wa-form-control-border-style);
border-width: var(--border-width);
box-shadow: var(--box-shadow);
color: var(--wa-form-control-value-color);
cursor: pointer;
font-family: inherit;
font-size: var(--wa-size);
font-weight: var(--wa-form-control-value-font-weight);
line-height: var(--wa-form-control-value-line-height);
min-width: 0;
overflow: hidden;
padding: var(--wa-space-smaller) var(--wa-space);
position: relative;
vertical-align: middle;
width: 100%;
transition:
background-color var(--wa-transition-normal),
border var(--wa-transition-normal),
outline var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
}
/* Add ellipses to multi select options */
@@ -15,7 +40,7 @@ label:has(select),
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: var(--tag-max-size);
max-width: 7ch;
}
:host .disabled [part~='combobox'] {
@@ -61,31 +86,21 @@ label:has(select),
min-height: var(--wa-form-control-height);
background-color: var(--background-color, var(--wa-form-control-background-color));
border-color: var(--border-color, var(--wa-form-control-border-color));
border-radius: var(--wa-form-control-border-radius);
border-style: var(--wa-form-control-border-style);
border-width: var(--border-width);
box-shadow: var(--box-shadow);
color: var(--wa-form-control-value-color);
cursor: pointer;
font-family: inherit;
font-weight: var(--wa-form-control-value-font-weight);
line-height: var(--wa-form-control-value-line-height);
overflow: hidden;
padding: 0 var(--wa-form-control-padding-inline);
position: relative;
vertical-align: middle;
width: 100%;
transition:
background-color var(--wa-transition-normal),
border var(--wa-transition-normal),
outline var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
:host([size='small']) & {
&:not(.placeholder-visible *) {
padding-block: 2px;
}
}
:host([multiple]) .select:not(.placeholder-visible) & {
:host([size='large']) & {
&:not(.placeholder-visible *) {
padding-block: 4px;
}
}
:host([multiple]) .select:not(.placeholder-visible) {
padding-inline-start: 0;
padding-block: calc(var(--wa-form-control-height) * 0.1 - var(--wa-form-control-border-width));
padding-block: 3px;
}
/* Pills */
@@ -145,8 +160,16 @@ label:has(select),
flex: 1;
align-items: center;
flex-wrap: wrap;
margin-inline-start: 0.25em;
gap: 0.25em;
margin-inline-start: var(--wa-space-2xs);
gap: 3px;
:host([size='small']) & {
gap: 2px;
}
:host([size='large']) & {
gap: 4px;
}
&::slotted(wa-tag) {
cursor: pointer !important;
@@ -169,15 +192,15 @@ label:has(select),
}
.suffix::slotted(*) {
margin-inline-start: var(--wa-form-control-padding-inline);
margin-inline-start: var(--wa-space-s);
}
.prefix::slotted(*) {
margin-inline-end: var(--wa-form-control-padding-inline);
margin-inline-end: var(--wa-space);
}
:host([multiple]) .prefix::slotted(*) {
margin-inline: var(--wa-form-control-padding-inline);
margin-inline: var(--wa-space);
}
/* Clear button */
@@ -192,7 +215,7 @@ label:has(select),
padding: 0;
transition: color var(--wa-transition-normal);
cursor: pointer;
margin-inline-start: var(--wa-form-control-padding-inline);
margin-inline-start: var(--wa-space);
&:focus {
outline: none;
@@ -215,7 +238,7 @@ label:has(select),
color: var(--wa-color-neutral-on-quiet);
transition: rotate var(--wa-transition-slow) ease;
rotate: 0deg;
margin-inline-start: var(--wa-form-control-padding-inline);
margin-inline-start: var(--wa-space-s);
.open & {
rotate: -180deg;
@@ -233,7 +256,7 @@ label:has(select),
border-radius: var(--wa-border-radius-m);
border-style: var(--wa-border-style);
border-width: var(--border-width);
padding-block: 0.5em;
padding-block: var(--wa-space-xs);
padding-inline: 0;
overflow: auto;
overscroll-behavior: none;
@@ -243,15 +266,15 @@ label:has(select),
max-height: var(--auto-size-available-height);
&::slotted(wa-divider) {
--spacing: 0.5em;
--spacing: var(--wa-space-xs);
}
}
slot:not([name])::slotted(small) {
display: block;
font-size: var(--wa-font-size-smaller);
font-size: var(--wa-font-size-s);
font-weight: var(--wa-font-weight-semibold);
color: var(--wa-color-text-quiet);
padding-block: 0.5em;
padding-inline: 2.25em;
padding-block: var(--wa-space-xs);
padding-inline: var(--wa-space-xl);
}

View File

@@ -331,12 +331,12 @@ describe('<wa-select>', () => {
const secondOption = el.querySelectorAll('wa-option')[1];
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.false;
expect(el.customStates.has('valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('required')).to.be.true;
expect(el.hasCustomState('optional')).to.be.false;
expect(el.hasCustomState('invalid')).to.be.false;
expect(el.hasCustomState('valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.false;
await el.show();
await clickOnElement(secondOption);
@@ -345,8 +345,8 @@ describe('<wa-select>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -359,12 +359,12 @@ describe('<wa-select>', () => {
`);
const secondOption = el.querySelectorAll('wa-option')[1];
expect(el.customStates.has('required')).to.be.true;
expect(el.customStates.has('optional')).to.be.false;
expect(el.customStates.has('invalid')).to.be.true;
expect(el.customStates.has('valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('required')).to.be.true;
expect(el.hasCustomState('optional')).to.be.false;
expect(el.hasCustomState('invalid')).to.be.true;
expect(el.hasCustomState('valid')).to.be.false;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.false;
await el.show();
await clickOnElement(secondOption);
@@ -373,8 +373,8 @@ describe('<wa-select>', () => {
el.blur();
await el.updateComplete;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('user-valid')).to.be.false;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -389,12 +389,12 @@ describe('<wa-select>', () => {
`);
const select = el.querySelector<WaSelect>('wa-select')!;
expect(select.customStates.has('required')).to.be.true;
expect(select.customStates.has('optional')).to.be.false;
expect(select.customStates.has('invalid')).to.be.true;
expect(select.customStates.has('valid')).to.be.false;
expect(select.customStates.has('user-invalid')).to.be.false;
expect(select.customStates.has('user-valid')).to.be.false;
expect(select.hasCustomState('required')).to.be.true;
expect(select.hasCustomState('optional')).to.be.false;
expect(select.hasCustomState('invalid')).to.be.true;
expect(select.hasCustomState('valid')).to.be.false;
expect(select.hasCustomState('user-invalid')).to.be.false;
expect(select.hasCustomState('user-valid')).to.be.false;
});
});

View File

@@ -79,13 +79,12 @@ import styles from './select.css';
* @cssproperty --border-color - The border color of the select's combobox.
* @cssproperty --border-width - The width of the select's borders, including the listbox.
* @cssproperty --box-shadow - The shadow effects around the edges of the select's combobox.
* @cssproperty [--tag-max-size=10ch] - When using `multiple`, the max size of tags before their content is truncated.
*
* @cssstate blank - The select is empty.
*/
@customElement('wa-select')
export default class WaSelect extends WebAwesomeFormAssociatedElement {
static css = [appearanceStyles, formControlStyles, sizeStyles, styles];
static shadowStyle = [appearanceStyles, formControlStyles, sizeStyles, styles];
static get validators() {
const validators = isServer
@@ -740,7 +739,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.customStates.set('blank', !this.value);
this.toggleCustomState('blank', !this.value);
}
}

View File

@@ -17,7 +17,7 @@ import styles from './skeleton.css';
*/
@customElement('wa-skeleton')
export default class WaSkeleton extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
/** Determines which effect the skeleton will use. */
@property({ reflect: true }) effect: 'pulse' | 'sheen' | 'none' = 'none';

View File

@@ -2,7 +2,7 @@
--thumb-color: var(--wa-form-control-activated-color);
--thumb-gap: calc(var(--thumb-size) * 0.125);
--thumb-shadow: initial;
--thumb-size: calc(1em * var(--wa-form-control-value-line-height));
--thumb-size: calc(1rem * var(--wa-form-control-value-line-height));
--track-color-active: var(--wa-color-neutral-fill-normal);
--track-color-inactive: var(--wa-color-neutral-fill-normal);
@@ -22,7 +22,6 @@ input[type='range'] {
border-radius: calc(var(--track-height) / 2);
width: 100%;
height: var(--track-height);
font-size: inherit;
line-height: var(--wa-form-control-height);
vertical-align: middle;
margin: 0;
@@ -153,7 +152,7 @@ input[type='range']:focus {
line-height: var(--wa-tooltip-line-height);
color: var(--wa-tooltip-content-color);
opacity: 0;
padding: 0.25em 0.5em;
padding: var(--wa-space-2xs) var(--wa-space-xs);
transition: var(--wa-transition-normal) opacity;
pointer-events: none;

View File

@@ -133,18 +133,18 @@ describe('<wa-slider>', () => {
await slider.updateComplete;
expect(slider.checkValidity()).to.be.false;
expect(slider.customStates.has('invalid')).to.be.true;
expect(slider.customStates.has('valid')).to.be.false;
expect(slider.customStates.has('user-invalid')).to.be.false;
expect(slider.customStates.has('user-valid')).to.be.false;
expect(slider.hasCustomState('invalid')).to.be.true;
expect(slider.hasCustomState('valid')).to.be.false;
expect(slider.hasCustomState('user-invalid')).to.be.false;
expect(slider.hasCustomState('user-valid')).to.be.false;
await clickOnElement(slider);
await slider.updateComplete;
slider.blur();
await slider.updateComplete;
expect(slider.customStates.has('user-invalid')).to.be.true;
expect(slider.customStates.has('user-valid')).to.be.false;
expect(slider.hasCustomState('user-invalid')).to.be.true;
expect(slider.hasCustomState('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -154,10 +154,10 @@ describe('<wa-slider>', () => {
slider.setCustomValidity('Invalid value');
await slider.updateComplete;
expect(slider.customStates.has('invalid')).to.be.true;
expect(slider.customStates.has('valid')).to.be.false;
expect(slider.customStates.has('user-invalid')).to.be.false;
expect(slider.customStates.has('user-valid')).to.be.false;
expect(slider.hasCustomState('invalid')).to.be.true;
expect(slider.hasCustomState('valid')).to.be.false;
expect(slider.hasCustomState('user-invalid')).to.be.false;
expect(slider.hasCustomState('user-valid')).to.be.false;
});
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {

View File

@@ -45,7 +45,7 @@ import styles from './slider.css';
*/
@customElement('wa-slider')
export default class WaSlider extends WebAwesomeFormAssociatedElement {
static css = [formControlStyles, styles];
static shadowStyle = [formControlStyles, styles];
static get validators() {
return [...super.validators, MirrorValidator()];

View File

@@ -19,7 +19,7 @@ import styles from './spinner.css';
*/
@customElement('wa-spinner')
export default class WaSpinner extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);

View File

@@ -35,7 +35,7 @@ import styles from './split-panel.css';
*/
@customElement('wa-split-panel')
export default class WaSplitPanel extends WebAwesomeElement {
static css = styles;
static shadowStyle = styles;
private cachedPositionInPixels: number;
private isCollapsed = false;

View File

@@ -6,7 +6,7 @@
--border-style: var(--wa-form-control-border-style);
--border-width: var(--wa-form-control-border-width);
--box-shadow: initial;
--height: var(--wa-form-control-toggle-size);
--height: calc(1em * var(--wa-form-control-value-line-height));
--thumb-color: var(--wa-form-control-border-color);
--thumb-color-checked: var(--wa-form-control-background-color);
--thumb-shadow: initial;
@@ -14,7 +14,6 @@
--width: calc(var(--height) * 1.75);
display: inline-flex;
line-height: var(--wa-form-control-value-line-height);
}
label {

View File

@@ -230,12 +230,12 @@ describe('<wa-switch>', () => {
const el = await fixture<HTMLFormElement>(html` <form novalidate><wa-switch required></wa-switch></form> `);
const waSwitch = el.querySelector<WaSwitch>('wa-switch')!;
expect(waSwitch.customStates.has('required')).to.be.true;
expect(waSwitch.customStates.has('optional')).to.be.false;
expect(waSwitch.customStates.has('invalid')).to.be.true;
expect(waSwitch.customStates.has('valid')).to.be.false;
expect(waSwitch.customStates.has('user-invalid')).to.be.false;
expect(waSwitch.customStates.has('user-valid')).to.be.false;
expect(waSwitch.hasCustomState('required')).to.be.true;
expect(waSwitch.hasCustomState('optional')).to.be.false;
expect(waSwitch.hasCustomState('invalid')).to.be.true;
expect(waSwitch.hasCustomState('valid')).to.be.false;
expect(waSwitch.hasCustomState('user-invalid')).to.be.false;
expect(waSwitch.hasCustomState('user-valid')).to.be.false;
});
});

Some files were not shown because too many files have changed in this diff Show More