Compare commits

...

17 Commits

Author SHA1 Message Date
Cory LaViska
7131213d61 rename var 2025-06-04 16:28:25 -04:00
Cory LaViska
a78e63c821 add data- invokers to dialog and drawer 2025-06-04 16:15:24 -04:00
Konnor Rogers
eac60a9022 Fixes for app and prettier (#1028)
* fix for app

* prettier

* fix for app

* prettier

* update prettierignore

* prettier

* prettier

* fix prism vendoring
2025-06-04 10:28:38 -04:00
Cory LaViska
f8dca5d1a8 refactor styles and simplify custom states (#1016) 2025-06-04 08:09:14 -04:00
Lindsay M
5980b5f843 Refactor form control sizing (#1005)
* visual test setup

* fix improper class placement in visual tests

* add `wa-form-control-*-font-size` and `-wa-form-control-hint-*` custom properties

* use new custom properties

* dump of component sizing improvements

* fix native color picker styles

* update menu with relative sizes

* tidy up menu, select, and tag sizing

* use relative sizing across components

* touch up custom properties

* update docs and comments

* update changelog

* revert changes to visual tests to simplify PR

* revert leftover change to visual tests

* tidy up space doc markdown

* fix default card spacing

* fix card docs

* wrap up new `--tag-max-size` for `<wa-select>`

* change default `--tag-max-size`

* prettier

* touch up

* clean up leftovers

* fix native form control margins

* set default toggle size relative to font size

* correct toggle size in docs

* Konnorrogers/lm form control sizing (#1025)

* try a second updateComplete??

* try a second updateComplete??

* more timeouts?

* try logging

* more logging
gp

* maybe now

* radio group test

* add todo note'

* add workflow dispatch for client tests

---------

Co-authored-by: Konnor Rogers <konnor5456@gmail.com>
2025-06-03 16:27:24 -04:00
Cory LaViska
9a3ffb04ba remove unused custom props; align styles with wa-slider (#1017) 2025-06-03 15:51:49 -04:00
Cory LaViska
04d37224e0 remove hint from <wa-radio>; fixes #1024 (#1026) 2025-06-03 15:31:53 -04:00
Cory LaViska
f4a63f9e22 Simplify color variants docs (#1023)
* remove

* simplify
2025-06-03 15:28:11 -04:00
Cory LaViska
3ab342ebb6 add support for name attribute (#1022) 2025-06-03 15:10:31 -04:00
Cory LaViska
48fe9389c8 Use outlined appearance for buttons in <wa-color-picker> (#1021)
* use outlined appearance for buttons

* don't override child element's size
2025-06-03 15:10:15 -04:00
Cory LaViska
6b2a081fa0 Improve rating's default a11y (#1019)
* add regular star

* support dual icons; improve default contrast

* support dual icons; improve contrast

* update docs

* Update packages/webawesome/src/components/rating/rating.css

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>

---------

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
2025-06-03 15:09:34 -04:00
Cory LaViska
afb2082c79 add resize-vertical (#1018) 2025-06-03 15:08:52 -04:00
Cory LaViska
7bdc9a2cc4 Add <wa-popover> (#1012)
* remove redundant styles from template

* rotate arrow based on placement so borders show correctly when applied

* use actual placement, not preferred

* add popover

* update changelog

* update changelog

* use <dialog> for popover

* fix arrow border in FF/Safari

* update content

* add sidebar to plop

* add popover
2025-06-02 16:13:24 -04:00
Cory LaViska
dead18d23c Fix scroll on reload (#1015)
* fix scroll on reload

* fix comment
2025-06-02 16:09:49 -04:00
Konnor Rogers
2b37c54d7c fix code blocks (#1009) 2025-06-02 10:23:47 -04:00
Cory LaViska
35b61e5cf3 Fix plop templates (#1007)
* update plop templates

* fix plop template

* don't break react
2025-06-02 09:37:30 -04:00
Konnor Rogers
cc33805d27 add convenience scripts (#1004) 2025-05-30 11:46:43 -04:00
145 changed files with 1490 additions and 1128 deletions

View File

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

View File

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

23
package-lock.json generated
View File

@@ -2488,6 +2488,10 @@
"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",
@@ -13987,6 +13991,25 @@
"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

@@ -11,7 +11,9 @@
"packages/*"
],
"scripts": {
"check-updates": "npx npm-check-updates --interactive --format group"
"check-updates": "npx npm-check-updates --interactive --format group",
"start": "cd packages/webawesome && npm run start",
"start:pro": "cd packages/webawesome-pro && npm run start"
},
"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,4 +186,3 @@ 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))];
/**
@@ -125,7 +125,7 @@ export default async function (eleventyConfig) {
eleventyConfig.addPlugin(currentLink());
// Add code examples for `<code class="example">` blocks
eleventyConfig.addPlugin(codeExamplesPlugin);
eleventyConfig.addPlugin(codeExamplesPlugin());
// Highlight code blocks with Prism
eleventyConfig.addPlugin(highlightCodePlugin());
@@ -136,6 +136,10 @@ export default async function (eleventyConfig) {
// Various text replacements
eleventyConfig.addPlugin(
replaceTextPlugin([
{
replace: /\[version\]/gs,
replaceWith: packageData.version,
},
// Replace [issue:1234] with a link to the issue on GitHub
{
replace: /\[pr:([0-9]+)\]/gs,
@@ -172,9 +176,8 @@ 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);
@@ -206,7 +209,6 @@ export default async function (eleventyConfig) {
// }
}
export const config = {
markdownTemplateEngine: 'njk',
dir: {
@@ -215,5 +217,4 @@ 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 { join, dirname, resolve } from 'path';
import { dirname, join, 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,4 +76,3 @@ components.sort((a, b) => {
});
export default components;

View File

@@ -1,43 +0,0 @@
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

@@ -137,6 +137,7 @@
</ul>
</li>
<li><a href="/docs/components/mutation-observer/">Mutation Observer</a></li>
<li><a href="/docs/components/popover/">Popover</a></li>
<li><a href="/docs/components/popup/">Popup</a></li>
<li><a href="/docs/components/progress-bar/">Progress Bar</a></li>
<li><a href="/docs/components/progress-ring/">Progress Ring</a></li>
@@ -174,6 +175,7 @@
<li><a href="/docs/components/tooltip/">Tooltip</a></li>
<li><a href="/docs/components/tree/">Tree</a></li>
<li><a href="/docs/components/tree-item/">Tree Item</a></li>
{# PLOP_NEW_COMPONENT_PLACEHOLDER #}
</ul>
</wa-details>

View File

@@ -6,3 +6,4 @@ 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

@@ -1,3 +1,19 @@
import { allDefined } from '/dist/webawesome.js';
/**
* Determines how the page was loaded. Possible return values include "reload", "navigate", "back_forward", "prerender",
* and "unknown".
*/
function getNavigationType() {
if (performance.getEntriesByType) {
const navEntries = performance.getEntriesByType('navigation');
if (navEntries.length > 0) {
return navEntries[0].type;
}
}
return 'unknown';
}
// Smooth links
document.addEventListener('click', event => {
const link = event.target.closest('a');
@@ -31,3 +47,26 @@ function updateScrollClass() {
window.addEventListener('scroll', updateScrollClass);
window.addEventListener('turbo:render', updateScrollClass);
updateScrollClass();
// Restore scroll position after components are defined
allDefined().then(() => {
const navigationType = getNavigationType();
const key = `wa-scroll-y-[${location.pathname}]`;
const scrollY = sessionStorage.getItem(key);
// Only restore when reloading, otherwise clear it
if (navigationType === 'reload' && scrollY) {
window.scrollTo(0, scrollY);
} else {
sessionStorage.removeItem(key);
}
// After restoring, keep tabs on the page's scroll position for next reload
window.addEventListener(
'scroll',
() => {
sessionStorage.setItem(key, window.scrollY);
},
{ passive: true },
);
});

View File

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

View File

@@ -1,76 +0,0 @@
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

@@ -1,89 +0,0 @@
---
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,40 +104,23 @@ Use the `appearance` attribute to change the elements visual appearance.
### Grouping Details
Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `wa-show` event.
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.
```html {.example}
<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.
<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.
</wa-details>
<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 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>
<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 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>
</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,24 +67,28 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
</script>
```
### Dismissing Dialogs
### Opening and Closing Dialogs Declaratively
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.
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.
```html {.example}
<wa-dialog label="Dialog" class="dialog-dismiss">
<wa-dialog label="Dialog" id="dialog-opening">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
<wa-button>Open Dialog</wa-button>
<wa-button data-dialog="open dialog-opening">Open Dialog</wa-button>
```
<script>
const dialog = document.querySelector('.dialog-dismiss');
const openButton = dialog.nextElementSibling;
Similarly, you can add `data-dialog="close"` to a button _inside_ of a dialog to tell it to close.
openButton.addEventListener('click', () => dialog.open = true);
</script>
```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>
```
### Custom Width

View File

@@ -65,24 +65,28 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
</script>
```
### Dismissing Drawers
### Opening and Closing Drawers Declaratively
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.
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.
```html {.example}
<wa-drawer label="Drawer" class="drawer-dismiss">
<wa-drawer label="Drawer" id="drawer-opening">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
<wa-button>Open Drawer</wa-button>
<wa-button data-drawer="open drawer-opening">Open Drawer</wa-button>
```
<script>
const drawer = document.querySelector('.drawer-dismiss');
const openButton = drawer.nextElementSibling;
Similarly, you can add `data-drawer="close"` to a button _inside_ of a drawer to tell it to close.
openButton.addEventListener('click', () => drawer.open = true);
</script>
```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>
```
### Slide in From Start

View File

@@ -0,0 +1,143 @@
---
title: Popover
layout: component
---
Popovers display interactive content when their anchor element is clicked. Unlike [tooltips](/docs/components/tooltip), popovers can contain links, buttons, and form controls. They appear without an overlay and will close when you click outside or press [[Escape]]. Only one popover can be open at a time.
```html {.example}
<wa-popover for="popover__overview">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<p>This popover contains interactive content that users can engage with directly.</p>
<wa-button variant="primary" size="small">Take Action</wa-button>
</div>
</wa-popover>
<wa-button id="popover__overview">Show popover</wa-button>
```
## Examples
### Assigning an Anchor
Use `<wa-button>` or `<button>` elements as popover anchors. Connect the popover to its anchor by setting the `for` attribute to match the anchor's `id`.
```html {.example}
<wa-button id="popover__anchor-button">Show Popover</wa-button>
<wa-popover for="popover__anchor-button">
I'm anchored to a Web Awesome button.
</wa-popover>
<br><br>
<button id="popover__anchor-native-button">Show Popover</button>
<wa-popover for="popover__anchor-native-button">
I'm anchored to a native button.
</wa-popover>
```
:::warning
Make sure the anchor element exists in the DOM before the popover connects. If it doesn't exist, the popover won't attach and you'll see a console warning.
:::
### Opening and Closing
Popovers show when you click their anchor element. You can also control them programmatically by setting the `open` property to `true` or `false`.
Use `data-popover="close"` on any button inside a popover to close it automatically.
```html {.example}
<wa-popover for="popover__opening">
<p>The button below has <code>data-popover="close"</code> so clicking it will close the popover.</p>
<wa-button data-popover="close" variant="primary">Dismiss</wa-button>
</wa-popover>
<wa-button id="popover__opening">Show popover</wa-button>
```
### Placement
Use the `placement` attribute to set where the popover appears relative to its anchor. The popover will automatically reposition if there isn't enough space in the preferred location. The default placement is `top`.
```html {.example}
<div style="display: flex; gap: 1rem; align-items: center;">
<wa-button id="popover__top">Top</wa-button>
<wa-popover for="popover__top" placement="top">I'm on the top</wa-popover>
<wa-button id="popover__bottom">Bottom</wa-button>
<wa-popover for="popover__bottom" placement="bottom">I'm on the bottom</wa-popover>
<wa-button id="popover__left">Left</wa-button>
<wa-popover for="popover__left" placement="left">I'm on the left</wa-popover>
<wa-button id="popover__right">Right</wa-button>
<wa-popover for="popover__right" placement="right">I'm on the right</wa-popover>
</div>
```
### Distance
Use the `distance` attribute to control how far the popover appears from its anchor.
```html {.example}
<div style="display: flex; gap: 1rem; align-items: center;">
<wa-button id="popover__distance-near">Near</wa-button>
<wa-popover for="popover__distance-near" distance="0">I'm very close</wa-popover>
<wa-button id="popover__distance-far">Far</wa-button>
<wa-popover for="popover__distance-far" distance="30">I'm farther away</wa-popover>
</div>
```
### Arrow Size
Use the `--arrow-size` custom property to change the size of the popover's arrow. Set it to `0` to remove the arrow entirely.
```html {.example}
<div style="display: flex; gap: 1rem; align-items: center;">
<wa-button id="popover__big-arrow">Big arrow</wa-button>
<wa-popover for="popover__big-arrow" style="--arrow-size: 8px;">I have a big arrow</wa-popover>
<wa-button id="popover__no-arrow">No arrow</wa-button>
<wa-popover for="popover__no-arrow" style="--arrow-size: 0;">I don't have an arrow</wa-popover>
</div>
```
### Setting a Maximum Width
Use the `--max-width` custom property to control the maximum width of the popover.
```html {.example}
<wa-button id="popover__max-width">Toggle me</wa-button>
<wa-popover for="popover__max-width" style="--max-width: 160px;">
Popovers will usually grow to be much wider, but this one has a custom max width that forces text to wrap.
</wa-popover>
```
### Setting Focus
Use the [`autofocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) global attribute to move focus to a specific form control when the popover opens.
```html {.example}
<wa-popover for="popover__autofocus">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<wa-textarea
autofocus
placeholder="What's on your mind?"
size="small"
resize="none"
rows="3"
></wa-textarea>
<wa-button variant="primary" size="small" data-popover="close">
Submit
</wa-button>
</div>
</wa-popover>
<wa-button id="popover__autofocus">
<wa-icon name="comment" slot="prefix"></wa-icon>
Feedback
</wa-button>
```

View File

@@ -75,14 +75,3 @@ 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.
You can also use the `getSymbol` property to render different icons based on value and/or whether the icon is currently selected.
```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 => {
rating.getSymbol = (value, isSelected) => {
const icons = ['face-angry', 'face-frown', 'face-meh', 'face-smile', 'face-laugh'];
return `<wa-icon name="${icons[value - 1]}"></wa-icon>`;
};

View File

@@ -31,13 +31,29 @@ 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,31 +8,38 @@ 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), 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}`.
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}`.
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-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` |
| 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)` |
```html {.example}
<form class="wa-block-spacing-l">
<form class="wa-stack">
<wa-input label="Input" placeholder="Placeholder"></wa-input>
<wa-select label="Select" value="option-1">
<wa-option value="option-1">Option 1</wa-option>
@@ -50,19 +57,6 @@ 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
@@ -76,7 +70,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-block-spacing-l">
<div class="wa-stack">
<wa-callout>
<wa-icon slot="icon" name="circle-info" variant="regular"></wa-icon>
This is a simple callout with an icon.
@@ -86,13 +80,6 @@ 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,21 +35,19 @@ 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.
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.
You can use `--wa-space-scale` to increase or decrease all spacing at once. By default, this multiplier is `1`.
| 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> |
The calculations for each size and the resulting pixel value (assuming a 16px root font size) are listed below.
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
| 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> |

View File

@@ -17,21 +17,32 @@ Font families are assigned specific roles &mdash; like heading or code &mdash; t
## Font Size
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.
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.
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.
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.
| Custom Property | Default Value | Preview |
| -------------------- | --------------------------------- | ---------------------------------------------------------- |
| `--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> |
| `--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)` |
## 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:
{% 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 %}
- [`<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)
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,19 +8,12 @@ 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 copy from 'recursive-copy';
import { fileURLToPath } from 'node:url';
import {
getCdnDir,
getDistDir,
getDocsDir,
getRootDir,
getSiteDir,
runScript,
} from './utils.js';
import ora from 'ora';
import copy from 'recursive-copy';
import { getCdnDir, getDistDir, getDocsDir, getRootDir, getSiteDir, runScript } from './utils.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const isDeveloping = process.argv.includes('--develop');
@@ -42,19 +35,18 @@ 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();
@@ -92,8 +84,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');
@@ -113,14 +105,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}`);
@@ -134,8 +126,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');
@@ -147,20 +139,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);
}
@@ -173,12 +165,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',
@@ -246,8 +238,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');
@@ -266,12 +258,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;
}
@@ -375,19 +367,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 {
@@ -401,8 +393,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();
@@ -427,29 +419,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();
}
};
}
})
});
}
//
@@ -468,22 +460,23 @@ 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, getSiteDir } from './utils.js';
import { getDocsDir, getEleventyConfigPath, getSiteDir } from './utils.js';
const elev = new Eleventy(getDocsDir(), getSiteDir(), {
quietMode: true,
configPath: join(getDocsDir(), '.eleventy.js'),
configPath: getEleventyConfigPath(),
});
// Cleanup
@@ -13,4 +13,3 @@ 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,4 +81,3 @@ for await (const component of components) {
// Generate the index file
fs.writeFileSync(path.join(reactDir, 'index.ts'), index.join('\n'), 'utf8');

View File

@@ -37,7 +37,7 @@ export default function (plop) {
},
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.styles.ts',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.css',
templateFile: 'templates/component/styles.hbs',
},
{
@@ -50,6 +50,12 @@ export default function (plop) {
path: '../../docs/docs/components/{{ tagWithoutPrefix tag }}.md',
templateFile: 'templates/component/docs.hbs',
},
{
type: 'modify',
path: '../../docs/_includes/sidebar.njk',
pattern: /\{# PLOP_NEW_COMPONENT_PLACEHOLDER #\}/,
template: `<li><a href="/docs/components/{{ tagWithoutPrefix tag }}">{{ tagToTitle tag }}</a></li>\n {# PLOP_NEW_COMPONENT_PLACEHOLDER #}`,
},
],
});
}

View File

@@ -1,11 +1,8 @@
import { customElement, property } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { customElement, property } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './test-element.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import styles from './{{ tagWithoutPrefix tag }}.css';
/**
* @summary Short summary of the component's intended use.
@@ -15,8 +12,6 @@ import type { CSSResultGroup } from 'lit';
*
* @dependency wa-example
*
* @event wa-event-name - Emitted as an example.
*
* @slot - The default slot.
* @slot example - An example slot.
*
@@ -26,9 +21,7 @@ import type { CSSResultGroup } from 'lit';
*/
@customElement("{{ tag }}")
export default class {{ properCase tag }} extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
private readonly localize = new LocalizeController(this);
static css = styles;
/** An example attribute. */
@property() attr = 'example';

View File

@@ -1,7 +1,3 @@
import { css } from 'lit';
export default css`
:host {
display: block;
}
`;
:host {
display: block;
}

View File

@@ -1,4 +1,3 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
describe('<{{ tag }}>', () => {

View File

@@ -11,6 +11,7 @@ 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
@@ -57,4 +58,3 @@ 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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = 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 shadowStyle = [variantStyles, appearanceStyles, styles];
static css = [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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = 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 shadowStyle = [sizeStyles, variantStyles, styles];
static css = [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' = 'medium';
@property({ reflect: true }) size: 'small' | 'medium' | 'large'; // unset by default to not override child elements
/** 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;
button.setAttribute('size', this.size);
if (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-space, var(--wa-space-m));
padding: 0 var(--wa-form-control-padding-inline);
font-family: inherit;
font-size: var(--wa-size, var(--wa-font-size-m));
font-size: inherit;
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-space);
margin-inline-end: var(--wa-form-control-padding-inline);
}
slot[name='suffix']::slotted(*),
.button:not(.visually-hidden-label) [part~='caret'] {
margin-inline-start: var(--wa-space);
margin-inline-start: var(--wa-form-control-padding-inline);
}
/*

View File

@@ -53,7 +53,7 @@ import styles from './button.css';
*/
@customElement('wa-button')
export default class WaButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [styles, variantStyles, sizeStyles, appearanceStyles];
static css = [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 shadowStyle = [variantStyles, appearanceStyles, sizeStyles, styles];
static css = [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,9 +1,5 @@
:host {
--space-s: var(--wa-space-l);
--space-m: var(--wa-space-xl);
--space-l: var(--wa-space-2xl);
--spacing: var(--wa-space);
--spacing: var(--wa-space-l);
--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)] - The amount of space around and between sections of the card. 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.
*/
@customElement('wa-card')
export default class WaCard extends WebAwesomeElement {
static shadowStyle = [sizeStyles, appearanceStyles, styles];
static css = [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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = 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,7 +11,8 @@
--border-width: var(--wa-form-control-border-width);
--box-shadow: none;
--checked-icon-color: var(--wa-color-brand-on-loud);
--toggle-size: 1lh;
--checked-icon-scale: 0.8;
--toggle-size: var(--wa-form-control-toggle-size);
color: var(--wa-form-control-value-color);
display: inline-flex;
@@ -43,7 +44,7 @@
color var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
margin-inline-end: var(--wa-space-xs);
margin-inline-end: 0.5em;
}
:host [part~='base'] {
@@ -86,6 +87,7 @@ 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.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;
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;
await clickOnElement(checkbox);
await checkbox.updateComplete;
await aTimeout(0);
expect(checkbox.hasCustomState('user-invalid')).to.be.true;
expect(checkbox.hasCustomState('user-valid')).to.be.false;
expect(checkbox.customStates.has('user-invalid')).to.be.true;
expect(checkbox.customStates.has('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.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;
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;
});
});

View File

@@ -55,7 +55,7 @@ import styles from './checkbox.css';
*/
@customElement('wa-checkbox')
export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
static shadowStyle = [formControlStyles, sizeStyles, styles];
static css = [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.toggleCustomState('checked', this.checked);
this.toggleCustomState('indeterminate', this.indeterminate);
this.customStates.set('checked', this.checked);
this.customStates.set('indeterminate', this.indeterminate);
this.updateValidity();
}
@watch('disabled')
handleDisabledChange() {
this.toggleCustomState('disabled', this.disabled);
this.customStates.set('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-border-radius-circle);
--trigger-border-radius: var(--wa-form-control-border-radius);
}
.color-picker {
@@ -301,6 +301,7 @@
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);
@@ -318,7 +319,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) * 2) var(--wa-color-surface-default);
inset 0 0 0 calc(var(--border-width) * 3) 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.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;
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;
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.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('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.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;
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;
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.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('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 shadowStyle = [visuallyHidden, sizeStyles, formControlStyles, styles];
static css = [visuallyHidden, sizeStyles, formControlStyles, styles];
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
@@ -1017,6 +1017,7 @@ 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,
@@ -1038,6 +1039,7 @@ 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 shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
@@ -55,12 +55,12 @@ export default class WaComparison extends WebAwesomeElement {
drag(this, {
onMove: x => {
this.toggleCustomState('dragging', true);
this.customStates.set('dragging', true);
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
if (isRtl) this.position = 100 - this.position;
},
onStop: () => {
this.toggleCustomState('dragging', false);
this.customStates.set('dragging', false);
},
initialEvent: event,
});

View File

@@ -17,7 +17,7 @@
border-radius: var(--wa-border-radius-m);
color: inherit;
font-size: inherit;
padding: var(--wa-space-xs);
padding: 0.5em;
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 shadowStyle = [visuallyHidden, styles];
static css = [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 shadowStyle = [appearanceStyles, styles];
static css = [appearanceStyles, styles];
private detailsObserver: MutationObserver;
private readonly localize = new LocalizeController(this);
@@ -65,6 +65,9 @@ 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;
@@ -138,6 +141,20 @@ 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) {
@@ -151,6 +168,9 @@ 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,6 +6,7 @@ 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';
@@ -54,7 +55,7 @@ import styles from './dialog.css';
*/
@customElement('wa-dialog')
export default class WaDialog extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header-actions', 'label');
@@ -264,6 +265,28 @@ 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 shadowStyle = styles;
static css = styles;
/** Sets the divider's orientation. */
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';

View File

@@ -6,6 +6,7 @@ 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';
@@ -32,9 +33,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 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
* `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
* behavior such as data loss.
*
* @csspart header - The drawer's header. This element wraps the title and header actions.
@@ -59,7 +60,7 @@ import styles from './drawer.css';
*/
@customElement('wa-drawer')
export default class WaDrawer extends WebAwesomeElement {
static shadowStyle = styles;
static css = styles;
private readonly localize = new LocalizeController(this);
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header-actions', 'label');
@@ -280,6 +281,28 @@ 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 shadowStyle = [sizeStyles, styles];
static css = [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: var(--wa-space-xs);
padding: 0.5em;
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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = styles;
@state() private svg: SVGElement | HTMLTemplateResult | null = null;

View File

@@ -33,6 +33,7 @@ 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 shadowStyle = styles;
static css = 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-size);
font-size: var(--wa-form-control-value-font-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: var(--wa-space-smaller) var(--wa-space);
padding: 0 var(--wa-form-control-padding-inline);
&:focus-within {
outline: var(--wa-focus-ring);
@@ -142,11 +142,11 @@ textarea {
}
.prefix::slotted(*) {
margin-inline-end: var(--wa-space);
margin-inline-end: var(--wa-form-control-padding-inline);
}
.suffix::slotted(*) {
margin-inline-start: var(--wa-space);
margin-inline-start: var(--wa-form-control-padding-inline);
}
/*

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.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;
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;
el.focus();
await el.updateComplete;
@@ -123,19 +123,19 @@ describe('<wa-input>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('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.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;
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;
el.focus();
await el.updateComplete;
@@ -145,20 +145,20 @@ describe('<wa-input>', () => {
el.blur();
await el.updateComplete;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('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.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;
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;
});
});
@@ -215,10 +215,10 @@ describe('<wa-input>', () => {
await input.updateComplete;
expect(input.checkValidity()).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;
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;
input.focus();
await sendKeys({ type: 'test' });
@@ -226,8 +226,8 @@ describe('<wa-input>', () => {
input.blur();
await input.updateComplete;
expect(input.hasCustomState('user-invalid')).to.be.true;
expect(input.hasCustomState('user-valid')).to.be.false;
expect(input.customStates.has('user-invalid')).to.be.true;
expect(input.customStates.has('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 shadowStyle = [sizeStyles, appearanceStyles, formControlStyles, styles];
static css = [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.toggleCustomState('blank', !this.value);
this.customStates.set('blank', !this.value);
}
}

View File

@@ -9,7 +9,7 @@
display: flex;
align-items: stretch;
font: inherit;
padding: var(--wa-space-xs) var(--wa-space-2xs);
padding: 0.5em 0.25em;
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: 0.0625rem;
--track-width: round(0.0625em, 1px);
position: absolute;
font-size: 0.8em;
font-size: var(--wa-font-size-smaller);
top: calc(50% - 0.5em);
left: 0.5rem;
left: 0.6em;
opacity: 1;
}
@@ -61,7 +61,7 @@
}
.prefix::slotted(*) {
margin-inline-end: var(--wa-space-xs);
margin-inline-end: 0.5em;
}
.suffix {
@@ -71,7 +71,7 @@
}
.suffix::slotted(*) {
margin-inline-start: var(--wa-space-xs);
margin-inline-start: 0.5em;
}
/* Safe triangle */
@@ -114,8 +114,8 @@
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875em;
width: var(--wa-space-xl);
font-size: var(--wa-font-size-smaller);
width: 2em;
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 shadowStyle = styles;
static css = 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.toggleCustomState('has-submenu', this.isSubmenu());
this.customStates.set('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.toggleCustomState('submenu-expanded', isSubmenuExpanded);
this.customStates.set('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-s);
font-size: var(--wa-font-size-smaller);
font-weight: var(--wa-font-weight-semibold);
padding: var(--wa-space-2xs) calc(var(--wa-space-2xs) + var(--wa-space-xl));
padding: 0.5em 2.25em;
-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 shadowStyle = styles;
static css = 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: var(--wa-space-xs) 0;
padding: 0.5em 0;
overflow: auto;
overscroll-behavior: none;
}
::slotted(wa-divider) {
--spacing: var(--wa-space-xs);
--spacing: 0.5em;
}

View File

@@ -25,7 +25,7 @@ export interface MenuSelectEventDetail {
*/
@customElement('wa-menu')
export default class WaMenu extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles];
static css = [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 shadowStyle = styles;
static css = styles;
private mutationObserver: MutationObserver;

View File

@@ -13,7 +13,7 @@
display: flex;
align-items: center;
font: inherit;
padding: var(--wa-space-xs) var(--wa-space-m) var(--wa-space-xs) var(--wa-space-2xs);
padding: 0.5em 1em 0.5em 0.25em;
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: 0.875em;
font-size: var(--wa-font-size-smaller);
visibility: hidden;
width: var(--wa-space-xl);
width: 2em;
}
:host(:state(selected)) .check {
@@ -68,11 +68,11 @@
}
.prefix::slotted(*) {
margin-inline-end: var(--wa-space-xs);
margin-inline-end: 0.5em;
}
.suffix::slotted(*) {
margin-inline-start: var(--wa-space-xs);
margin-inline-start: 0.5em;
}
@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 shadowStyle = styles;
static css = 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.toggleCustomState('hover', true);
this.customStates.set('hover', true);
} else if (event.type === 'mouseleave') {
this.toggleCustomState('hover', false);
this.customStates.set('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.toggleCustomState('selected', this.selected);
this.customStates.set('selected', this.selected);
}
if (changedProperties.has('value')) {
@@ -165,7 +165,7 @@ export default class WaOption extends WebAwesomeElement {
}
if (changedProperties.has('current')) {
this.toggleCustomState('current', this.current);
this.customStates.set('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 shadowStyle = [visuallyHidden, styles];
static css = [visuallyHidden, styles];
private headerResizeObserver = this.slotResizeObserver('header');
private subheaderResizeObserver = this.slotResizeObserver('subheader');

View File

@@ -0,0 +1,91 @@
:host {
--arrow-size: 0.375rem;
--max-width: 25rem;
--show-duration: 100ms;
--hide-duration: 100ms;
/* Internal calculated properties */
--arrow-diagonal-size: calc((var(--arrow-size) * sin(45deg)));
display: contents;
/** Defaults for inherited CSS properties */
font-size: var(--wa-popover-font-size);
line-height: var(--wa-popover-line-height);
text-align: start;
white-space: normal;
}
/* The native dialog element */
.dialog {
display: none;
position: fixed;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
background: transparent;
overflow: visible;
pointer-events: none;
&:focus {
outline: none;
}
&[open] {
display: block;
}
}
/* The <wa-popup> element */
.popover {
--arrow-size: inherit;
--show-duration: inherit;
--hide-duration: inherit;
pointer-events: auto;
&::part(arrow) {
background-color: var(--wa-color-surface-default);
border-top: none;
border-left: none;
border-bottom: solid var(--wa-panel-border-width) var(--wa-color-surface-border);
border-right: solid var(--wa-panel-border-width) var(--wa-color-surface-border);
box-shadow: none;
}
}
.popover[placement^='top']::part(popup) {
transform-origin: bottom;
}
.popover[placement^='bottom']::part(popup) {
transform-origin: top;
}
.popover[placement^='left']::part(popup) {
transform-origin: right;
}
.popover[placement^='right']::part(popup) {
transform-origin: left;
}
/* Body */
.body {
display: flex;
flex-direction: column;
width: max-content;
max-width: var(--max-width);
padding: var(--wa-space-l);
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);
border-style: var(--wa-panel-border-style);
box-shadow: var(--wa-shadow-s);
color: var(--wa-color-text-normal);
user-select: none;
-webkit-user-select: none;
}

View File

@@ -0,0 +1,9 @@
import { expect, fixture, html } from '@open-wc/testing';
describe('<wa-popover>', () => {
it('should render a component', async () => {
const el = await fixture(html` <wa-popover></wa-popover> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,319 @@
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';
import { WaAfterHideEvent } from '../../events/after-hide.js';
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 { waitForEvent } from '../../internal/event.js';
import { uniqueId } from '../../internal/math.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import WaPopup from '../popup/popup.js';
import styles from './popover.css';
const openPopovers = new Set<WaPopover>();
/**
* @summary Popovers display contextual content and interactive elements in a floating panel.
* @documentation https://backers.webawesome.com/docs/components/popover
* @status stable
* @since 3.0
*
* @dependency wa-popup
*
* @slot - The popover's content. Interactive elements such as buttons and links are supported.
*
* @event wa-show - Emitted when the popover begins to show. Canceling this event will stop the popover from showing.
* @event wa-after-show - Emitted after the popover has shown and all animations are complete.
* @event wa-hide - Emitted when the popover begins to hide. Canceling this event will stop the popover from hiding.
* @event wa-after-hide - Emitted after the popover has hidden and all animations are complete.
*
* @csspart dialog - The native dialog element that contains the popover content.
* @csspart body - The popover's body where its content is rendered.
* @csspart popup - The internal `<wa-popup>` element that positions the popover.
* @csspart popup__popup - The popup's exported `popup` part. Use this to target the popover's popup container.
* @csspart popup__arrow - The popup's exported `arrow` part. Use this to target the popover's arrow.
*
* @cssproperty [--arrow-size=0.375rem] - The size of the tiny arrow that points to the popover (set to zero to remove).
* @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 dependencies = { 'wa-popup': WaPopup };
@query('dialog') dialog: HTMLDialogElement;
@query('.body') body: HTMLElement;
@query('wa-popup') popup: WaPopup;
@state() anchor: null | Element = null;
/**
* The preferred placement of the popover. Note that the actual placement may vary as needed to keep the popover
* inside of the viewport.
*/
@property() placement:
| 'top'
| 'top-start'
| 'top-end'
| 'right'
| 'right-start'
| 'right-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end' = 'top';
/** Shows or hides the popover. */
@property({ type: Boolean, reflect: true }) open = false;
/** The distance in pixels from which to offset the popover away from its target. */
@property({ type: Number }) distance = 8;
/** The distance in pixels from which to offset the popover along its target. */
@property({ type: Number }) skidding = 0;
/** The ID of the popover's anchor element. This must be an interactive/focusable element such as a button. */
@property() for: string | null = null;
private eventController = new AbortController();
connectedCallback() {
super.connectedCallback();
// If the user doesn't give us an id, generate one.
if (!this.id) {
this.id = uniqueId('wa-popover-');
}
}
disconnectedCallback() {
super.disconnectedCallback();
// Cleanup events in case the popover is removed while open
document.removeEventListener('keydown', this.handleDocumentKeyDown);
this.eventController.abort();
}
firstUpdated() {
// If the popover is visible on init, update its position
if (this.open) {
this.dialog.show();
this.popup.active = true;
this.popup.reposition();
}
}
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;
};
private handleBodyClick = (event: PointerEvent) => {
const target = event.target as HTMLElement;
const button = target.closest('[data-popover="close"]');
// Watch for [data-popover="close"] clicks
if (button) {
event.stopPropagation();
this.open = false;
}
};
private handleDocumentKeyDown = (event: KeyboardEvent) => {
// Hide the popover when escape is pressed
if (event.key === 'Escape') {
event.preventDefault();
this.open = false;
if (this.anchor && typeof (this.anchor as any).focus === 'function') {
(this.anchor as any).focus();
}
}
};
private handleDocumentClick = (event: PointerEvent) => {
const target = event.target as HTMLElement;
// Ignore clicks on the anchor so it will be closed by the anchor's click handler
if (this.anchor && event.composedPath().includes(this.anchor)) {
return;
}
// Detect when clicks occur outside the popover
if (target.closest('wa-popover') !== this) {
this.open = false;
}
};
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
// Show
const waShowEvent = new WaShowEvent();
this.dispatchEvent(waShowEvent);
if (waShowEvent.defaultPrevented) {
this.open = false;
return;
}
// Close other popovers that are open
openPopovers.forEach(popover => (popover.open = false));
document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
document.addEventListener('click', this.handleDocumentClick, { signal: this.eventController.signal });
// Show the dialog non-modally
this.dialog.show();
this.popup.active = true;
openPopovers.add(this);
// Autofocus the first element with the autofocus attribute
requestAnimationFrame(() => {
const elementToFocus = this.querySelector<HTMLElement>('[autofocus]');
if (elementToFocus && typeof elementToFocus.focus === 'function') {
elementToFocus.focus();
} else {
// Fall back to setting focus on the dialog
this.dialog.focus();
}
});
await animateWithClass(this.popup.popup, 'show-with-scale');
this.popup.reposition();
this.dispatchEvent(new WaAfterShowEvent());
} else {
// Hide
const waHideEvent = new WaHideEvent();
this.dispatchEvent(waHideEvent);
if (waHideEvent.defaultPrevented) {
this.open = true;
return;
}
document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('click', this.handleDocumentClick);
openPopovers.delete(this);
await animateWithClass(this.popup.popup, 'hide-with-scale');
this.popup.active = false;
this.dialog.close();
this.dispatchEvent(new WaAfterHideEvent());
}
}
@watch('for')
handleForChange() {
const rootNode = this.getRootNode() as Document | ShadowRoot | null;
if (!rootNode) {
return;
}
const newAnchor = this.for ? rootNode.querySelector(`#${this.for}`) : null;
const oldAnchor = this.anchor;
if (newAnchor === oldAnchor) {
return;
}
const { signal } = this.eventController;
if (newAnchor) {
newAnchor.addEventListener('click', this.handleAnchorClick, { signal });
}
if (oldAnchor) {
oldAnchor.removeEventListener('click', this.handleAnchorClick);
}
this.anchor = newAnchor;
if (this.for && !newAnchor) {
console.warn(
`A popover was assigned to an element with an ID of "${this.for}" but the element could not be found.`,
this,
);
}
}
@watch(['distance', 'placement', 'skidding'])
async handleOptionsChange() {
if (this.hasUpdated) {
await this.updateComplete;
this.popup.reposition();
}
}
/** Shows the popover. */
async show() {
if (this.open) {
return undefined;
}
this.open = true;
return waitForEvent(this, 'wa-after-show');
}
/** Hides the popover. */
async hide() {
if (!this.open) {
return undefined;
}
this.open = false;
return waitForEvent(this, 'wa-after-hide');
}
render() {
return html`
<dialog part="dialog" class="dialog">
<wa-popup
part="popup"
exportparts="
popup:popup__popup,
arrow:popup__arrow
"
class=${classMap({
popover: true,
'popover-open': this.open,
})}
placement=${this.placement}
distance=${this.distance}
skidding=${this.skidding}
flip
shift
arrow
.anchor=${this.anchor}
>
<div part="body" class="body" @click=${this.handleBodyClick}>
<slot></slot>
</div>
</wa-popup>
</dialog>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'wa-popover': WaPopover;
}
}

View File

@@ -48,7 +48,19 @@
height: calc(var(--arrow-size-diagonal) * 2);
rotate: 45deg;
background: var(--arrow-color);
z-index: -1;
z-index: 3;
}
:host([data-current-placement~='left']) .arrow {
rotate: -45deg;
}
:host([data-current-placement~='right']) .arrow {
rotate: 135deg;
}
:host([data-current-placement~='bottom']) .arrow {
rotate: 225deg;
}
/* Hover bridge */

View File

@@ -68,7 +68,7 @@ const SUPPORTS_POPOVER = globalThis?.HTMLElement?.prototype.hasOwnProperty('popo
*/
@customElement('wa-popup')
export default class WaPopup extends WebAwesomeElement {
static shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = 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 shadowStyle = styles;
static css = styles;
@query('canvas') canvas: HTMLElement;

View File

@@ -26,18 +26,18 @@
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: var(--wa-space-s);
gap: 0.75em;
}
/* Horizontal */
:host([orientation='horizontal']) [part~='form-control-input'] {
flex-direction: row;
gap: var(--wa-space-m);
gap: 1em;
}
/* Help text */
[part~='hint'] {
margin-block-start: var(--wa-space-xs);
margin-block-start: 0.5em;
}
/* 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,19 +99,22 @@ describe('<wa-radio-group>', () => {
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
expect(radioGroup.checkValidity()).to.be.true;
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;
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;
await clickOnElement(secondRadio);
// TODO: Go back to clickOnElement when we can determine why CI is not cleaning up elements.
// await clickOnElement(secondRadio);
secondRadio.click();
await secondRadio.updateComplete;
await radioGroup.updateComplete;
expect(radioGroup.checkValidity()).to.be.true;
expect(radioGroup.hasCustomState('user-invalid')).to.be.false;
expect(radioGroup.hasCustomState('user-valid')).to.be.true;
expect(radioGroup.customStates.has('user-invalid')).to.be.false;
expect(radioGroup.customStates.has('user-valid')).to.be.true;
});
it('should receive the correct validation attributes ("states") when invalid', async () => {
@@ -123,19 +126,22 @@ describe('<wa-radio-group>', () => {
`);
const secondRadio = radioGroup.querySelectorAll('wa-radio')[1];
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;
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;
await clickOnElement(secondRadio);
// TODO: Go back to clickOnElement when we can determine why CI is not cleaning up elements.
// await clickOnElement(secondRadio);
secondRadio.click();
await radioGroup.updateComplete;
radioGroup.value = '';
await radioGroup.updateComplete;
expect(radioGroup.hasCustomState('user-invalid')).to.be.true;
expect(radioGroup.hasCustomState('user-valid')).to.be.false;
expect(radioGroup.customStates.has('user-invalid')).to.be.true;
expect(radioGroup.customStates.has('user-valid')).to.be.false;
});
it('should receive validation attributes ("states") even when novalidate is used on the parent form', async () => {
@@ -149,12 +155,12 @@ describe('<wa-radio-group>', () => {
`);
const radioGroup = el.querySelector<WaRadioGroup>('wa-radio-group')!;
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;
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;
});
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 shadowStyle = [sizeStyles, formControlStyles, styles];
static css = [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.75;
--toggle-size: round(1lh, 1px);
--checked-icon-scale: 0.7;
--toggle-size: var(--wa-form-control-toggle-size);
color: var(--wa-form-control-value-color);
display: inline-flex;
@@ -36,7 +36,7 @@
}
[part~='hint'] {
margin-block-start: var(--wa-space-3xs);
margin-block-start: 0.5em;
}
/* Default appearance */
@@ -63,7 +63,7 @@
color var(--wa-transition-fast);
transition-timing-function: var(--wa-transition-easing);
margin-inline-end: var(--wa-space-xs);
margin-inline-end: 0.5em;
}
.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-space);
padding: 0 var(--wa-form-control-padding-inline);
transition:
background-color var(--wa-transition-fast),
border-color var(--wa-transition-fast);

View File

@@ -1,8 +1,6 @@
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';
@@ -18,7 +16,6 @@ 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.
@@ -26,7 +23,6 @@ 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.
@@ -44,7 +40,7 @@ import styles from './radio.css';
*/
@customElement('wa-radio')
export default class WaRadio extends WebAwesomeFormAssociatedElement {
static shadowStyle = [formControlStyles, sizeStyles, styles];
static css = [formControlStyles, sizeStyles, styles];
@state() checked = false;
@@ -68,11 +64,6 @@ 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) {
@@ -95,13 +86,13 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('checked')) {
this.toggleCustomState('checked', this.checked);
this.customStates.set('checked', this.checked);
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
}
if (changedProperties.has('disabled')) {
this.toggleCustomState('disabled', this.disabled);
this.customStates.set('disabled', this.disabled);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
}
@@ -120,9 +111,6 @@ 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
@@ -135,15 +123,6 @@ 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,13 +1,7 @@
:host {
--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: var(--wa-color-neutral-on-quiet);
--symbol-color-active: var(--wa-color-yellow-70);
--symbol-size: var(--wa-size);
--symbol-spacing: var(--wa-space-3xs);
--symbol-spacing: 0.125em;
display: inline-flex;
}

View File

@@ -33,7 +33,7 @@ import styles from './rating.css';
*/
@customElement('wa-rating')
export default class WaRating extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles];
static css = [sizeStyles, styles];
private readonly localize = new LocalizeController(this);
@@ -68,8 +68,11 @@ 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) => string = () =>
'<wa-icon name="star" library="system" variant="solid"></wa-icon>';
@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>';
};
/** The component's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@@ -254,6 +257,8 @@ 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
@@ -274,7 +279,7 @@ export default class WaRating extends WebAwesomeElement {
: `inset(0 0 0 ${(displayValue - index) * 100}%)`,
})}
>
${unsafeHTML(this.getSymbol(index + 1))}
${unsafeHTML(this.getSymbol(index + 1, false))}
</div>
<div
class="partial-filled"
@@ -284,7 +289,7 @@ export default class WaRating extends WebAwesomeElement {
: `inset(0 ${100 - (displayValue - index) * 100}% 0 0)`,
})}
>
${unsafeHTML(this.getSymbol(index + 1))}
${unsafeHTML(this.getSymbol(index + 1, true))}
</div>
</span>
`;
@@ -299,7 +304,7 @@ export default class WaRating extends WebAwesomeElement {
})}
role="presentation"
>
${unsafeHTML(this.getSymbol(index + 1))}
${unsafeHTML(this.getSymbol(index + 1, isSelected))}
</span>
`;
})}

View File

@@ -17,7 +17,7 @@ import styles from './resize-observer.css';
*/
@customElement('wa-resize-observer')
export default class WaResizeObserver extends WebAwesomeElement {
static shadowStyle = styles;
static css = 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 shadowStyle = [styles];
static css = [styles];
private readonly localize = new LocalizeController(this);
private resizeObserver = new ResizeObserver(() => this.updateScroll());

View File

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

View File

@@ -331,12 +331,12 @@ describe('<wa-select>', () => {
const secondOption = el.querySelectorAll('wa-option')[1];
expect(el.checkValidity()).to.be.true;
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;
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;
await el.show();
await clickOnElement(secondOption);
@@ -345,8 +345,8 @@ describe('<wa-select>', () => {
await el.updateComplete;
expect(el.checkValidity()).to.be.true;
expect(el.hasCustomState('user-invalid')).to.be.false;
expect(el.hasCustomState('user-valid')).to.be.true;
expect(el.customStates.has('user-invalid')).to.be.false;
expect(el.customStates.has('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.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;
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;
await el.show();
await clickOnElement(secondOption);
@@ -373,8 +373,8 @@ describe('<wa-select>', () => {
el.blur();
await el.updateComplete;
expect(el.hasCustomState('user-invalid')).to.be.true;
expect(el.hasCustomState('user-valid')).to.be.false;
expect(el.customStates.has('user-invalid')).to.be.true;
expect(el.customStates.has('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.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;
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;
});
});

View File

@@ -79,12 +79,13 @@ 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 shadowStyle = [appearanceStyles, formControlStyles, sizeStyles, styles];
static css = [appearanceStyles, formControlStyles, sizeStyles, styles];
static get validators() {
const validators = isServer
@@ -739,7 +740,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
this.customStates.set('blank', !this.value);
}
}

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