Compare commits

...

37 Commits

Author SHA1 Message Date
Cory LaViska
e23d423d29 add parts to circles; fixes #1863 2026-01-15 12:36:51 -05:00
Wendelin
c8ddc2c1c0 Fix tree-item initial RTL check (#1798)
* Fix tree-item initial RTL check

* Update packages/webawesome/src/components/tree-item/tree-item.ts

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2026-01-15 12:23:41 -05:00
Alan Chambers
64a9374aa9 wa-dialog docs - clarify prevent dialog closing documentation (#1831)
Updated the dialog documentation to match newer default behaviour (non light-dismiss) and to describe the example more accurately.
2026-01-15 12:05:40 -05:00
Cory LaViska
67785f64cb update changelog (#1952) 2026-01-15 11:48:26 -05:00
Leon Vogt
39ab883802 Set default cursor for empty <wa-tree-item> expand buttons (#1936) 2026-01-15 11:46:08 -05:00
Lindsay M
08ef969db3 Add missing layout utility descriptions (#1946) 2026-01-15 11:31:29 -05:00
Konnor Rogers
72c4fb7af7 update plop templates (#1940) 2026-01-15 11:14:06 -05:00
Cory LaViska
827d70b2f5 Fix popover and dropdown bugs (#1913)
* fix popover bug; closes #1911

* fixes #1911

* reword guard
2026-01-15 09:53:54 -05:00
Lindsay M
46aa1429a7 Apply flexbox by default with wa-align-items-* and wa-justify-content-* utils (#1943)
* add `display: flex` with 0 specificity to utils

* add changelog
2026-01-13 15:44:37 -05:00
Brian Talbot
4b68a741bd abstracting layout docs example styling (#1935) 2026-01-13 15:31:37 -05:00
Cory LaViska
bd58f7f0a1 Divider docs (#1945)
* don't you know me

* clarify and add note
2026-01-13 15:08:25 -05:00
Alex Rao
e8f366835c Update tooltip.ts (#1928)
This removes the second period in the docs, which gets reflected on the website
2026-01-13 14:52:17 -05:00
Cory LaViska
70df6fea1f fix drag bug; closes #1905 (#1908) 2026-01-13 14:12:29 -05:00
Lindsay M
d76bef5dbb Unlist unreleased utilities from the docs (#1938) 2026-01-13 11:17:36 -05:00
Lindsay M
f8f54d543a Add justify-content utility classes (#1930)
* add `justify-content` utilities

* add docs

* fix up Align Items docs

* add Justify Content to sidebar

* add changelog

* add PR to changelog

* drop specificity of `justify-content` in layout utilities
2026-01-12 16:53:00 -05:00
Lindsay M
5b0e3ed1e8 Add missing .wa-gap-4xl utility class (#1931)
* add missing `.wa-gap-4xl`

* update the docs

* update changelog

* add PR to changelog
2026-01-12 16:35:01 -05:00
Brian Talbot
09ace44c17 A Glow Up for Dialogs That Pop Up (#1927)
* adding visual divider + padding to wa-dialog footers

* adding `wa-button`-focused `.shush` to `utils.css`
2026-01-09 14:52:23 -05:00
Lindsay M
f3de45803f Ensure consistent input heights in flex/grid containers (#1915)
* apply flex rules to `form-control` parts

* prevent input heights from growing in flex containers

* change approach: ignore `form-control` wrapper for styling
2025-12-31 11:35:37 -05:00
Steve Matney
ede1f0725d Ensuring styles apply for input labels when using 'slot="label"' (#1757)
* Ensuring styles apply for input labels when using 'slot="label"'

* Using 'has-slotted' class for codebase consistency

* Fleshing out 'has-slotted' label class to relevant form elements

* Ensuring label line height uses the correct token; removing unrelated PR changes

* Using 'has-label' class instead of 'has-slotted' for labels; applying it with all label content
2025-12-30 12:13:28 -05:00
Kelsey Jackson
56b1196265 got it working, but may revert to use on repo (#1888) 2025-12-19 10:54:45 -06:00
Cory LaViska
7e5f18ea97 update changelog (#1889) 2025-12-18 14:25:22 -05:00
Brian Talbot
c748721f12 adding event tracking to site search (#1872) 2025-12-17 15:22:18 -05:00
konnorrogers
6e5a720f51 update lockfiles and prettierignore 2025-12-16 13:42:13 -05:00
konnorrogers
f739121aaf updae lockfile 2025-12-16 13:30:16 -05:00
konnorrogers
5fb3625ee5 fix script 2025-12-16 13:22:32 -05:00
konnorrogers
da206a8787 update versions 2025-12-16 13:21:46 -05:00
konnorrogers
2b69fa74a5 Bump package.json version 2025-12-16 13:21:23 -05:00
konnorrogers
d1b6628d59 update package lock 2025-12-16 11:48:56 -05:00
konnorrogers
4dca4185e7 Bump changelog 2025-12-16 11:48:29 -05:00
Konnor Rogers
1e6d468959 add changelog entry (#1877) 2025-12-16 11:46:01 -05:00
Cory LaViska
00b8150f3e ensure min/max/step are numbers; fixes #1823 (#1832)
Co-authored-by: konnorrogers <konnor5456@gmail.com>
2025-12-16 11:06:27 -05:00
Konnor Rogers
3d395653fc Bug fix: el.form = 'foo' no longer sets the el.form property (#1815)
* Bug fix: el.form = 'foo' no longer sets the el.form property

* add changelog entry

* add changelog entry

* prettier

* fix form

* skip style attr for button

* prettier
2025-12-16 10:59:40 -05:00
Konnor Rogers
b3844ef77f fixing deploy scripts, and updating root version (#1857)
* fixing deploy scripts

* update root version script

* prettier
2025-12-16 10:29:26 -05:00
Lindsay M
a50648c6e8 Update changelog with leftover items (#1874)
* add leftover logs

* remove video component log
2025-12-15 17:49:56 -05:00
Cory LaViska
b6b82fb0ac Revert "updated sidebar (#1873)" (#1875)
This reverts commit fa073e8d21.
2025-12-15 15:59:38 -05:00
Kelsey Jackson
fa073e8d21 updated sidebar (#1873)
* updated sidebar

* added video item

* fixed formatting

* update

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2025-12-15 15:01:32 -05:00
Cory LaViska
e0f6ff11ec Combobox (supporting contributions in free) (#1838)
* hold the mustard! let's make it yellower

* words

* update sidebar

* update changelog

* fix icon cropping

* add combobox support

* preserve user-selected order

* add quick pro flag

* move import to the top

* fix custom tag example
2025-12-11 13:04:42 -05:00
58 changed files with 907 additions and 276 deletions

View File

@@ -20,4 +20,5 @@ packages/**/*/src/react/index.ts
node_modules
packages/**/*/_site
packages/**/*/_bundle_
packages/webawesome/docs/assets/scripts/prism-downloaded.js

View File

@@ -1 +1,2 @@
3.0.0
3.1.0

View File

@@ -70,6 +70,7 @@
"exportparts",
"fetchpriority",
"fieldsets",
"flexbox",
"focusin",
"focusout",
"fontawesome",
@@ -105,6 +106,7 @@
"keydown",
"keyframes",
"keymaker",
"Kickstarter",
"Konnor",
"Kool",
"labelledby",
@@ -117,6 +119,7 @@
"lowercasing",
"Lucide",
"maxlength",
"mdash",
"Menlo",
"menuitemcheckbox",
"menuitemradio",
@@ -130,6 +133,7 @@
"mouseout",
"mouseup",
"multiselectable",
"nbsp",
"nextjs",
"nocheck",
"noindex",
@@ -148,6 +152,7 @@
"ParamagicDev",
"peta",
"petabit",
"pointercancel",
"Preact",
"preconnect",
"prerendered",
@@ -179,6 +184,7 @@
"shadowrootmode",
"Shortcode",
"Shortcodes",
"signup",
"sitedir",
"slotchange",
"smartquotes",
@@ -200,6 +206,8 @@
"thead",
"Themer",
"tinycolor",
"touchcancel",
"touchend",
"transitionend",
"treeitem",
"treeshaking",
@@ -233,8 +241,6 @@
"src/translations/!(en).ts",
"**/*.min.js"
],
"ignoreRegExpList": [
"(^|[^a-z])sl[a-z]*(^|[^a-z])"
],
"ignoreRegExpList": ["(^|[^a-z])sl[a-z]*(^|[^a-z])"],
"useGitignore": true
}

199
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@webawesome/monorepo",
"version": "3.0.0-alpha.13",
"version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@webawesome/monorepo",
"version": "3.0.0-alpha.13",
"version": "3.1.0",
"license": "MIT",
"workspaces": [
"packages/*"
@@ -593,6 +593,10 @@
"resolved": "packages/webawesome",
"link": true
},
"node_modules/@awesome.me/webawesome-pro": {
"resolved": "packages/webawesome-pro",
"link": true
},
"node_modules/@babel/code-frame": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
@@ -2519,10 +2523,6 @@
"resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.2.1.tgz",
"integrity": "sha512-r4C9C/5kSfMBIr0D9imvpRdCNXtUNgyYThc4YlS6K5Hchv1UyxNQ9mxwj+BTRH2i1Neits260sR3OjKMnplsFA=="
},
"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",
@@ -12139,6 +12139,16 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-typescript-paths": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-typescript-paths/-/rollup-plugin-typescript-paths-1.5.0.tgz",
"integrity": "sha512-zly2aiGNjYJNq5YUi6eyGrQnCYUQ8b5czOtHZIGriwG9U3Ba2F9hlSklafXCdsNulK/IlNmE0Kzj0h+fVV32pA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"typescript": ">=3.4"
}
},
"node_modules/run-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
@@ -14013,7 +14023,7 @@
},
"packages/webawesome": {
"name": "@awesome.me/webawesome",
"version": "3.0.0",
"version": "3.1.0",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "4.1.0",
@@ -14037,8 +14047,8 @@
}
},
"packages/webawesome-pro": {
"name": "@shoelace-style/webawesome-pro",
"version": "3.0.0",
"name": "@awesome.me/webawesome-pro",
"version": "3.1.0",
"dependencies": {
"@ctrl/tinycolor": "4.1.0",
"@floating-ui/dom": "^1.6.13",
@@ -14052,8 +14062,11 @@
},
"devDependencies": {
"@wc-toolkit/jsx-types": "^1.3.0",
"@web/dev-server-rollup": "^0.6.4",
"eleventy-plugin-git-commit-date": "^0.1.3",
"esbuild": "^0.25.11"
"esbuild": "^0.25.11",
"npm-check-updates": "^19.1.2",
"rollup-plugin-typescript-paths": "^1.5.0"
},
"engines": {
"node": ">=14.17.0"
@@ -14467,6 +14480,84 @@
"node": ">=18"
}
},
"packages/webawesome-pro/node_modules/@web/dev-server-core": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz",
"integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/koa": "^2.11.6",
"@types/ws": "^7.4.0",
"@web/parse5-utils": "^2.1.0",
"chokidar": "^4.0.1",
"clone": "^2.1.2",
"es-module-lexer": "^1.0.0",
"get-stream": "^6.0.0",
"is-stream": "^2.0.0",
"isbinaryfile": "^5.0.0",
"koa": "^2.13.0",
"koa-etag": "^4.0.0",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"lru-cache": "^8.0.4",
"mime-types": "^2.1.27",
"parse5": "^6.0.1",
"picomatch": "^2.2.2",
"ws": "^7.5.10"
},
"engines": {
"node": ">=18.0.0"
}
},
"packages/webawesome-pro/node_modules/@web/dev-server-rollup": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz",
"integrity": "sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
"@web/dev-server-core": "^0.7.2",
"nanocolors": "^0.2.1",
"parse5": "^6.0.1",
"rollup": "^4.4.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"packages/webawesome-pro/node_modules/@web/parse5-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz",
"integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/parse5": "^6.0.1",
"parse5": "^6.0.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"packages/webawesome-pro/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"packages/webawesome-pro/node_modules/esbuild": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
@@ -14509,6 +14600,16 @@
"@esbuild/win32-x64": "0.25.11"
}
},
"packages/webawesome-pro/node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16.14"
}
},
"packages/webawesome-pro/node_modules/nanoid": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
@@ -14526,6 +14627,84 @@
"node": "^18 || >=20"
}
},
"packages/webawesome-pro/node_modules/npm-check-updates": {
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.1.2.tgz",
"integrity": "sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"ncu": "build/cli.js",
"npm-check-updates": "build/cli.js"
},
"engines": {
"node": ">=20.0.0",
"npm": ">=8.12.1"
}
},
"packages/webawesome-pro/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"packages/webawesome-pro/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"packages/webawesome-pro/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/webawesome-pro/node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"packages/webawesome/node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",

View File

@@ -2,7 +2,7 @@
"name": "@webawesome/monorepo",
"private": true,
"description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.13",
"version": "3.1.0",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -85,4 +85,4 @@
"prettier --write"
]
}
}
}

View File

@@ -81,7 +81,15 @@
<li><span class="is-planned wa-split">Charts <span><a href="https://github.com/shoelace-style/webawesome/issues/1073" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/checkbox/">Checkbox</a></li>
<li><a href="/docs/components/color-picker/">Color Picker</a></li>
<li><span class="is-planned wa-split">Combobox <span><a href="https://github.com/shoelace-style/webawesome/issues/1074" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li>
<span class="wa-split">
<span>
<a href="/docs/components/combobox">Combobox</a>
<wa-icon name="flask" aria-hidden="true" class="icon-shrink"></wa-icon>
</span>
{{ proBadge() }}
</span>
</li>
<li><a href="/docs/components/comparison/">Comparison</a></li>
<li>
<a class="wa-cluster wa-gap-xs" href="/docs/components/copy-button/">
@@ -90,7 +98,7 @@
</a>
</li>
<li><span class="is-planned wa-split">Data Grid <span><a href="https://github.com/shoelace-style/webawesome/issues/1072" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><span class="is-planned wa-split">Datepicker <span><a href="https://github.com/shoelace-style/webawesome/issues/1075" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><span class="is-planned wa-split">Date Picker <span><a href="https://github.com/shoelace-style/webawesome/issues/1075" target="_blank">{{ plannedBadge("A Web Awesome Kickstarter stretch goal!") }}</a>{{ proBadge({ description: "This will require access to Web Awesome Pro" }) }}</span></span></li>
<li><a href="/docs/components/details/">Details</a></li>
<li><a href="/docs/components/dialog/">Dialog</a></li>
<li><a href="/docs/components/divider/">Divider</a></li>
@@ -188,6 +196,8 @@
</h2>
<ul>
<li><a href="/docs/utilities/align-items/">Align Items</a></li>
<!-- Pending 3.2.0 release -->
<!-- <li><a href="/docs/utilities/justify-content/">Justify Content</a></li> -->
<li><a href="/docs/utilities/gap/">Gap</a></li>
<li><a href="/docs/utilities/cluster/">Cluster</a></li>
<li><a href="/docs/utilities/flank/">Flank</a></li>

View File

@@ -8,10 +8,13 @@
<wa-badge variant="neutral">Since {{ component.since }}</wa-badge>
<wa-badge
{% if component.status == 'stable' %}variant="brand"{% endif %}
{% if component.status == 'experimental' %}variant="warning"{% endif %}
{% if component.status == 'experimental' %}variant="warning" appearance="filled"{% endif %}
>
{{ component.status }}
</wa-badge>
{% if isProComponent %}
<wa-badge class="pro">Pro</wa-badge>
{% endif %}
</div>
<p class="component-summary">
{{ component.summary | inlineMarkdown | safe }}
@@ -20,6 +23,37 @@
{# Component API #}
{% block afterContent %}
{# Importing #}
<h2>Importing</h2>
<p>
Autoloading components via <a href="/docs/#using-a-project">projects</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
</p>
{% set componentName = component.tagName | stripPrefix %}
{% set componentPath = ["components/", componentName, "/", componentName, ".js"] | join("") %}
<wa-tab-group label="How would you like to import this component?">
<wa-tab panel="cdn">CDN</wa-tab>
<wa-tab panel="npm">npm</wa-tab>
<wa-tab panel="react">React</wa-tab>
<wa-tab-panel name="cdn">
<p>
Let your project code do the work! <a href="/signup">Sign up for free</a> to use a project with your very own CDN &mdash; it's the fastest and easiest way to use Web Awesome.
</p>
</wa-tab-panel>
<wa-tab-panel name="npm">
<p>
To manually import this component from NPM, use the following code.
</p>
<pre><code class="language-js">import '@awesome.me/webawesome/dist/{{ componentPath }}';</code></pre>
</wa-tab-panel>
<wa-tab-panel name="react">
<p>
To manually import this component from React, use the following code.
</p>
<pre><code class="language-js">import {{ component.name }} from '@awesome.me/webawesome/dist/react/{{ componentName }}';</code></pre>
</wa-tab-panel>
</wa-tab-group>
{# Slots #}
{% if component.slots.length %}
<h2>Slots</h2>
@@ -270,38 +304,6 @@
</ul>
{% endif %}
{# Importing #}
<h2>Importing</h2>
<p>
Autoloading components via <a href="/docs/#using-a-project">projects</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
</p>
{% set componentName = component.tagName | stripPrefix %}
{% set componentPath = ["components/", componentName, "/", componentName, ".js"] | join("") %}
<wa-tab-group label="How would you like to import this component?">
<wa-tab panel="cdn">CDN</wa-tab>
<wa-tab panel="npm">npm</wa-tab>
<wa-tab panel="react">React</wa-tab>
<wa-tab-panel name="cdn">
<p>
Let your project code do the work! <a href="/signup">Sign up for free</a> to use a project with your very own CDN &mdash; it's the fastest and easiest way to use Web Awesome.
</p>
</wa-tab-panel>
<wa-tab-panel name="npm">
<p>
To manually import this component from NPM, use the following code.
</p>
<pre><code class="language-js">import '@awesome.me/webawesome/dist/{{ componentPath }}';</code></pre>
</wa-tab-panel>
<wa-tab-panel name="react">
<p>
To manually import this component from React, use the following code.
</p>
<pre><code class="language-js">import {{ component.name }} from '@awesome.me/webawesome/dist/react/{{ componentName }}';</code></pre>
</wa-tab-panel>
</wa-tab-group>
<wa-divider></wa-divider>
<div class="component-help">

View File

@@ -1,12 +1,24 @@
// Search data
const version = document.documentElement.getAttribute('data-version') || '';
const res = await Promise.all([import('https://cdn.jsdelivr.net/npm/lunr/+esm'), fetch(`/search.json?v=${version}`)]);
const res = await Promise.all([
import('https://cdn.jsdelivr.net/npm/lunr/+esm'),
fetch(`/search.json?v=${version}`),
import('/assets/scripts/track.js').catch(() => null),
]);
const lunr = res[0].default;
const searchData = await res[1].json();
const searchIndex = lunr.Index.load(searchData.searchIndex);
const map = searchData.map;
const searchDebounce = 200;
const queryTrackDelay = 1000;
let searchTimeout;
let queryTrackTimeout;
let lastTrackedQuery = '';
let resultSelected = false;
// Optional event tracking - works standalone if track.js isn't available
const trackModule = res[2];
const trackEvent = trackModule?.trackEvent || window.trackEvent || (() => {});
// We're using Turbo, so references to these elements aren't guaranteed to remain intact
function getElements() {
@@ -17,6 +29,24 @@ function getElements() {
};
}
function trackQuerySubmit(query, resultSelectedValue) {
if (!query || query.length === 0) return;
const { results } = getElements();
if (!results) return;
const matches = results.querySelectorAll('li').length;
const truncatedQuery = query.length > 500 ? query.substring(0, 500) : query;
trackEvent('navigation:search_query_submit', {
query: truncatedQuery,
query_length: query.length,
result_count: matches,
has_results: matches > 0,
result_selected: resultSelectedValue,
});
}
// Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element
document.addEventListener('keydown', event => {
if (
@@ -42,40 +72,98 @@ document.addEventListener('click', event => {
function show() {
const { dialog, input, results } = getElements();
if (!dialog || !input || !results) return;
const wasAlreadyOpen = dialog.open;
// Remove existing listeners before adding to prevent duplicates
input.removeEventListener('input', handleInput);
results.removeEventListener('click', handleSelection);
dialog.removeEventListener('keydown', handleKeyDown);
dialog.removeEventListener('wa-hide', handleClose);
resultSelected = false;
lastTrackedQuery = '';
input.addEventListener('input', handleInput);
results.addEventListener('click', handleSelection);
dialog.addEventListener('keydown', handleKeyDown);
dialog.addEventListener('wa-hide', handleClose);
dialog.open = true;
if (!wasAlreadyOpen) {
trackEvent('navigation:search_dialog_open');
}
}
function hide() {
function cleanup() {
const { dialog, input, results } = getElements();
if (!dialog || !input || !results) return;
clearTimeout(searchTimeout);
clearTimeout(queryTrackTimeout);
input.removeEventListener('input', handleInput);
results.removeEventListener('click', handleSelection);
dialog.removeEventListener('keydown', handleKeyDown);
dialog.removeEventListener('wa-hide', handleClose);
dialog.open = false;
// Reset state to prevent leakage between dialog sessions
resultSelected = false;
lastTrackedQuery = '';
}
function handleClose() {
const { input } = getElements();
async function handleClose() {
const { dialog, input } = getElements();
if (!dialog || !input) return;
clearTimeout(queryTrackTimeout);
queryTrackTimeout = null;
dialog.removeEventListener('wa-hide', handleClose);
if (!resultSelected) {
const query = input.value.trim();
if (query.length > 0 && query !== lastTrackedQuery) {
trackQuerySubmit(query, false);
lastTrackedQuery = query;
}
}
input.value = '';
updateResults();
try {
await updateResults();
} catch (error) {
// Silently handle errors - UI cleanup should continue
}
cleanup();
trackEvent('navigation:search_dialog_close');
}
function handleInput() {
const { input } = getElements();
if (!input) return;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
clearTimeout(queryTrackTimeout);
const query = input.value.trim();
if (query.length === 0) {
lastTrackedQuery = '';
}
searchTimeout = setTimeout(async () => {
await updateResults(query);
if (query.length > 0 && query !== lastTrackedQuery) {
queryTrackTimeout = setTimeout(() => {
const { input: currentInput, results } = getElements();
if (!currentInput || resultSelected) return;
const currentQuery = currentInput.value.trim();
if (currentQuery === query && currentQuery !== lastTrackedQuery) {
trackQuerySubmit(currentQuery, false);
lastTrackedQuery = currentQuery;
}
}, queryTrackDelay);
}
}, searchDebounce);
}
function handleKeyDown(event) {
const { input, results } = getElements();
if (!input || !results) return;
// Handle keyboard selections
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
@@ -104,7 +192,12 @@ function handleKeyDown(event) {
nextEl = items[items.length - 1];
break;
case 'Enter':
currentEl?.querySelector('a')?.click();
if (currentEl) {
const link = currentEl.querySelector('a');
if (link) {
selectResult(link, 'keyboard_enter');
}
}
break;
}
@@ -121,27 +214,62 @@ function handleKeyDown(event) {
}
}
function selectResult(link, selectionMethod) {
const { input, results } = getElements();
if (!input || !link) return;
// Clear pending query tracking timeout to prevent duplicate events
clearTimeout(queryTrackTimeout);
queryTrackTimeout = null;
resultSelected = true; // Set immediately so timeout callback (if executing) sees it
const query = input.value.trim();
if (!link.dataset.searchResultIndex) return;
const resultIndex = parseInt(link.dataset.searchResultIndex, 10);
if (isNaN(resultIndex) || resultIndex < 1) return;
const resultUrl = link.dataset.searchResultUrl || link.getAttribute('href');
if (!resultUrl) return;
lastTrackedQuery = query;
trackQuerySubmit(query, true);
trackEvent('navigation:search_result_click', {
query,
result_index: resultIndex,
result_url: resultUrl,
selection_method: selectionMethod,
});
const { dialog } = getElements();
if (dialog) {
dialog.removeEventListener('wa-hide', handleClose);
cleanup();
trackEvent('navigation:search_dialog_close');
dialog.open = false;
}
if (window.Turbo) {
Turbo.visit(resultUrl);
} else {
location.href = resultUrl;
}
}
function handleSelection(event) {
const link = event.target.closest('a');
if (link) {
event.preventDefault();
hide();
if (window.Turbo) {
Turbo.visit(link.href);
} else {
location.href = link.href;
}
selectResult(link, 'mouse_click');
}
}
// Queries the search index and updates the results
async function updateResults(query = '') {
const { dialog, input, results } = getElements();
if (!dialog || !input || !results) return;
try {
const hasQuery = query.length > 0;
const trimmedQuery = query.trim();
const hasQuery = trimmedQuery.length > 0;
let matches = [];
if (hasQuery) {
@@ -149,13 +277,13 @@ async function updateResults(query = '') {
const seenRefs = new Set();
// Start with a standard search to get the best "exact match" result
searchIndex.search(`${query}`).forEach(match => {
searchIndex.search(`${trimmedQuery}`).forEach(match => {
matches.push(match);
seenRefs.add(match.ref);
});
// Add wildcard matches if not already included
searchIndex.search(`${query}*`).forEach(match => {
searchIndex.search(`${trimmedQuery}*`).forEach(match => {
if (!seenRefs.has(match.ref)) {
matches.push(match);
seenRefs.add(match.ref);
@@ -163,11 +291,10 @@ async function updateResults(query = '') {
});
// Add fuzzy search matches last
const fuzzyTokens = query
const fuzzyTokens = trimmedQuery
.split(' ')
.map(term => `${term}~1`)
.join(' ');
searchIndex.search(fuzzyTokens).forEach(match => {
if (!seenRefs.has(match.ref)) {
matches.push(match);
@@ -180,12 +307,12 @@ async function updateResults(query = '') {
dialog.classList.toggle('has-results', hasQuery && hasResults);
dialog.classList.toggle('no-results', hasQuery && !hasResults);
input.setAttribute('aria-activedescendant', '');
results.innerHTML = '';
matches.forEach((match, index) => {
const page = map[match.ref];
if (!page || !page.url) return;
const li = document.createElement('li');
const a = document.createElement('a');
const displayTitle = page.title ?? '';
@@ -197,12 +324,10 @@ async function updateResults(query = '') {
li.setAttribute('role', 'option');
li.setAttribute('id', `search-result-item-${match.ref}`);
li.setAttribute('data-selected', index === 0 ? 'true' : 'false');
if (page.url === '/') icon = 'home';
if (page.url.startsWith('/docs/utilities/native')) icon = 'code';
if (page.url.startsWith('/docs/components')) icon = 'puzzle-piece';
if (page.url.startsWith('/docs/theme') || page.url.startsWith('/docs/restyle')) icon = 'palette';
a.href = page.url;
a.innerHTML = `
<div class="site-search-result-icon" aria-hidden="true">
@@ -218,6 +343,9 @@ async function updateResults(query = '') {
a.querySelector('.site-search-result-description').textContent = displayDescription;
a.querySelector('.site-search-result-url').textContent = displayUrl;
// Use 1-based indexing for analytics
a.dataset.searchResultIndex = (index + 1).toString();
a.dataset.searchResultUrl = page.url;
li.appendChild(a);
results.appendChild(li);
});

View File

@@ -10,6 +10,13 @@
:root {
--wa-brand-orange: #f36944;
--wa-brand-grey: #30323b;
/* layout-based example style aspects */
--layout-example-border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
--layout-example-border-radius: var(--wa-border-radius-l);
--layout-example-padding: var(--wa-space-s);
--layout-example-element-background: var(--wa-color-indigo-60);
--layout-example-element-border-radius: var(--wa-border-radius-m);
}
.wa-dark .only-light,

View File

@@ -130,6 +130,13 @@
}
}
/* dialogs */
wa-dialog:has([slot='footer']) [slot='footer'] {
border-block-start: var(--wa-border-width-s) solid var(--wa-color-surface-border);
flex-grow: 1; /* make footer contents span entire width of dialog */
padding-block-start: var(--wa-space-l);
}
/* anchor headings */
.anchor-heading a {
opacity: 0;
@@ -250,7 +257,6 @@
z-index: 0;
}
}
/* #endregion */
/* buttons with icon toggle on hover */
wa-button .icon-hover {
@@ -262,6 +268,13 @@
wa-button:hover .icon-hover {
display: inline-flex;
}
/* buttons that are "shushed" (visually muted) by default, but have their full presentation otherwise */
wa-button.shush {
&:not(:hover):not(active)::part(base) {
color: var(--wa-color-text-quiet);
}
}
/* #endregion */
/* #region resets */

View File

@@ -178,11 +178,11 @@ If you want the dialog to close when the user clicks on the overlay, add the `li
### Preventing the Dialog from Closing
By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
By default, dialogs will close when the user clicks the close button or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
To keep the dialog open in such cases, you can cancel the `wa-hide` event. When canceled, the dialog will remain open and pulse briefly to draw the user's attention to it.
You can use `event.detail.source` to determine which element triggered the request to close. This example prevents the dialog from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it.
You can use `event.detail.source` to determine which element triggered the request to close. This example prevents the dialog from closing unless a specific button is clicked.
```html {.example}
<wa-dialog label="Dialog" class="dialog-deny-close">

View File

@@ -41,7 +41,7 @@ Use the `--spacing` custom property to change the amount of space between the di
### Orientation
The default orientation for dividers is `horizontal`. Set `orientation` attribute to `vertical` to draw a vertical divider. The divider will span the full height of its container.
The default orientation for dividers is `horizontal`. Set `orientation` attribute to `vertical` to draw a vertical divider. The divider will span the full height of its [Flexbox](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Flexbox) or [CSS Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/grid) container.
```html {.example}
<div style="display: flex; align-items: center;">
@@ -53,6 +53,10 @@ The default orientation for dividers is `horizontal`. Set `orientation` attribut
</div>
```
:::info
If your container isn't Flexbox or CSS Grid, you may need to set an explicit height for the divider.
:::
### Dropdown Dividers
Use dividers in [dropdowns](/docs/components/dropdown) to visually group dropdown items.

View File

@@ -285,9 +285,10 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
const name = option.querySelector('wa-icon[slot="start"]').name;
// You can return a string, a Lit Template, or an HTMLElement here
// Important: include data-value so the tag can be removed properly!
return `
<wa-tag with-remove>
<wa-icon name="${name}" style="padding-inline-end: .5rem;"></wa-icon>
<wa-tag with-remove data-value="${option.value}">
<wa-icon name="${name}"></wa-icon>
${option.label}
</wa-tag>
`;
@@ -299,6 +300,10 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities.
:::
:::info
When using custom tags with `with-remove`, you must include the `data-value` attribute set to the option's value. This allows the select to identify which option to deselect when the tag's remove button is clicked.
:::
### Lazy loading options
Lazy loading options works similarly to native `<select>` elements. The select component handles various scenarios intelligently:

View File

@@ -13,7 +13,27 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
## Next
- Added `justify-content` CSS utilities [pr:1930]
- Added missing `.wa-gap-4xl` utility class [pr:1931]
- Added `pointercancel` and `touchcancel` event handling to draggable elements to prevent drags from getting stuck
- Added `wa-justify-content-*` utility classes [pr:1930]
- Added missing `wa-gap-4xl` utility class [pr:1931]
- Added `track` and `indicator` CSS parts to `<wa-progress-ring>` [pr:1863]
- Fixed a bug in `<wa-combobox>` that prevented the listbox from opening when options were preselected [issue:1883]
- Fixed a bug in `<wa-popup>` and `<wa-dropdown-item>` that caused an error when removing a popup while it was opening [issue:1910]
- Fixed a bug in `<wa-popup>` and `<wa-dropdown>` that caused errors when shadow DOM queries returned null [issue:1911]
- Fixed a bug in `<wa-combobox>` that prevented the listbox from opening when options were preselected [issue:1883]
- Fixed a bug in draggable elements that caused a TypeError on `touchend` events when `event.touches` was empty
- Fixed a bug in `<wa-tree-item>` that caused the cursor to show a pointer when no expand icon was present [pr:1936]
- Modified `wa-align-items-*` utility classes to apply `display: flex` by default [pr:1943]
## 3.1.0
- Added `<wa-combobox>` as an experimental pro component [issue:1074]
- Added version 2.0.0 of the [official Web Awesome Figma Design Kit](/docs/resources/figma)
- Added npm support for Web Awesome Pro
- Added `layers.css` to define cascade layer order and updated palettes, themes, native styles, and utilities to import the new rule for more fail-safe modularity [pr:1793]
- [PRO]: Fixed a few sizing bugs in `<wa-page>` and `slot="footer"` no longer will always "overflow" the container.
- Fixed a bug in `<wa-slider>` that caused some touch devices to end up with the incorrect value [issue:1703]
- Fixed a bug in `<wa-card>` that prevented some slots from being detected correctly [discuss:1450]
- Fixed a z-index bug in `<wa-scroller>` styles [issue:1724]
@@ -21,7 +41,12 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
- Fixed a bug in `<wa-tree-item>` that caused the spinner to not show when lazy loading [issue:1678]
- Fixed a bug in `<wa-dropdown>` that caused the browser to hang when cancelling the `wa-hide` event [issue:1483]
- Fixed a bug in `<wa-dropdown-item>` that prevented the icon dependency from being imported [issue:1825]
- Fixed a bug in `<wa-select>` that prevented clicks on the tag's remove button from removing options in multiple mode
- Fixed a bug in `<wa-select>` that caused tags to appear in alphabetical order instead of selection order when using `multiple`
- Improved performance of `<wa-icon>` so initial rendering occurs faster, especially with multiple icons on the page [issue:1729]
- Improved `<wa-slider>` to not throw an error when string values are passed to the `min`, `max`, and `step` properties [issue:1823]
- Fixed a bug in Web Awesome form controls that caused `<wa-input form="foo">` to set the form property to equal `"foo"` instead of returning an `HTMLFormElement` breaking platform expectations. [pr:1815]
- Fixed a bug in `<wa-button>` causing it to not copy over attributes for form submissions. [pr:1815]
- Improved performance of all components by fixing how CSS is imported and reused [issue:1812]
- Modified the default `transition` styles of `<wa-dropdown-item>` to use design tokens [pr:1693]

View File

@@ -1,35 +1,37 @@
---
title: Align Items
description: Align items utilities set the gap property of flex and grid containers, like other Web Awesome layout utilities.
description: Align items utilities align items within flex and grid containers on the cross axis.
layout: docs
tags: layoutUtilities
---
<style>
.preview-wrapper {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border: var(--layout-example-border);
border-radius: var(--wa-border-radius-m);
min-block-size: 3em;
min-inline-size: 5em;
padding: var(--wa-space-2xs);
}
.preview-block {
aspect-ratio: 1 / 1;
background-color: var(--wa-color-neutral-fill-loud);
background-color: var(--layout-example-element-background);
border-radius: var(--wa-border-radius-s);
min-block-size: 1em;
}
</style>
Web Awesome includes classes to set the `align-items` property of flex and grid containers. They can be used alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to align children in container on the container's cross axis.
Web Awesome includes classes to set the `align-items` property of flex and grid containers. Use them alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to align items in a container on the container's [cross axis](#whats-the-cross-axis).
| Class Name | `align-items` Value | Preview |
| ------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `wa-align-items-baseline` | `baseline` | <div class="wa-cluster wa-align-items-baseline preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-center` | `center` | <div class="wa-cluster wa-align-items-center preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-end` | `flex-end` | <div class="wa-cluster wa-align-items-end preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-start` | `flex-start` | <div class="wa-cluster wa-align-items-start preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-stretch` | `stretch` | <div class="wa-cluster wa-align-items-stretch preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| Class Name | `align-items` Value | Preview |
| ------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wa-align-items-baseline` | `baseline` | <div class="wa-cluster wa-gap-2xs wa-align-items-baseline preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-center` | `center` | <div class="wa-cluster wa-gap-2xs wa-align-items-center preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-end` | `flex-end` | <div class="wa-cluster wa-gap-2xs wa-align-items-end preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-start` | `flex-start` | <div class="wa-cluster wa-gap-2xs wa-align-items-start preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-align-items-stretch` | `stretch` | <div class="wa-cluster wa-gap-2xs wa-align-items-stretch preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
## What's a Cross Axis?
## What's the Cross Axis?
The cross axis runs perpendicular to a flex container's content direction. For containers where `flex-direction` is `row` and content flows in the inline direction, the cross axis runs in the block direction. For containers where `flex-direction` is `column` and content flows in the block direction, the cross axis runs in the inline direction.
The cross axis runs perpendicular to a container's content direction. For containers where `flex-direction` is `row` and content flows in the inline direction, the cross axis runs in the block direction. For containers where `flex-direction` is `column` and content flows in the block direction, the cross axis runs in the inline direction.

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-cluster']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-cluster'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-cluster">
<div></div>

View File

@@ -7,20 +7,20 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-flank']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-flank'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
When space is limited, the items wrap.
{{ description }} When space is limited, the items wrap.
```html {.example}
<div class="wa-flank">

View File

@@ -7,18 +7,20 @@ tags: layoutUtilities
<style>
[class*='wa-frame']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
padding: var(--wa-space-s);
border: var(--layout-example-border);
padding: var(--layout-example-padding);
}
[class*='wa-frame'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-frame" style="max-inline-size: 20rem;">
<div></div>

View File

@@ -6,28 +6,37 @@ tags: layoutUtilities
---
<style>
.preview-wrapper {
border: var(--layout-example-border);
border-radius: var(--wa-border-radius-m);
min-block-size: 3em;
min-inline-size: 5em;
padding: var(--wa-space-2xs);
}
.preview-block {
aspect-ratio: 1 / 1;
background-color: var(--wa-color-neutral-fill-loud);
background-color: var(--layout-example-element-background);
border-radius: var(--wa-border-radius-s);
min-block-size: 1.5em;
min-block-size: 1em;
}
</style>
Web Awesome includes classes to set the `gap` property of flex and grid containers. They can be used alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to change the space between items.
Or even by themselves — all gap properties also set `display: flex` with a specificity of 0 so that it can be trivially overridden.
Web Awesome includes classes to set the `gap` property of flex and grid containers. Use them alone to create a flex container with a gap, or use them alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to change the space between items.
Besides `wa-gap-0`, which sets `gap` to zero, each class corresponds to one of the [`--wa-space-*`](/docs/tokens/space) tokens in your theme.
| Class Name | `gap` Value | Preview |
| ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `wa-gap-0` | `0` | <div class="wa-cluster wa-gap-0"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xs` | `--wa-space-3xs` | <div class="wa-cluster wa-gap-3xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xs` | `--wa-space-2xs` | <div class="wa-cluster wa-gap-2xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xs` | `--wa-space-xs` | <div class="wa-cluster wa-gap-xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-s` | `--wa-space-s` | <div class="wa-cluster wa-gap-s"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-m` | `--wa-space-m` | <div class="wa-cluster wa-gap-m"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-l` | `--wa-space-l` | <div class="wa-cluster wa-gap-l"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xl` | `--wa-space-xl` | <div class="wa-cluster wa-gap-xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xl` | `--wa-space-2xl` | <div class="wa-cluster wa-gap-2xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xl` | `--wa-space-3xl` | <div class="wa-cluster wa-gap-3xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-0` | `0` | <div class="preview-wrapper wa-cluster wa-gap-0"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xs` | `--wa-space-3xs` | <div class="preview-wrapper wa-cluster wa-gap-3xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xs` | `--wa-space-2xs` | <div class="preview-wrapper wa-cluster wa-gap-2xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xs` | `--wa-space-xs` | <div class="preview-wrapper wa-cluster wa-gap-xs"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-s` | `--wa-space-s` | <div class="preview-wrapper wa-cluster wa-gap-s"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-m` | `--wa-space-m` | <div class="preview-wrapper wa-cluster wa-gap-m"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-l` | `--wa-space-l` | <div class="preview-wrapper wa-cluster wa-gap-l"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-xl` | `--wa-space-xl` | <div class="preview-wrapper wa-cluster wa-gap-xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-2xl` | `--wa-space-2xl` | <div class="preview-wrapper wa-cluster wa-gap-2xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-gap-3xl` | `--wa-space-3xl` | <div class="preview-wrapper wa-cluster wa-gap-3xl"><div class="preview-block"></div><div class="preview-block"></div></div> |
<!-- Pending 3.2.0 release -->
<!-- | `wa-gap-4xl` | `--wa-space-4xl` | <div class="preview-wrapper wa-cluster wa-gap-4xl"><div class="preview-block"></div><div class="preview-block"></div></div> | -->

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-grid']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-grid'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-grid">
<div></div>

View File

@@ -0,0 +1,40 @@
---
title: Justify Content
description: Justify content utilities determine how space is distributed between items in flex and grid containers.
layout: docs
tags: layoutUtilities
unpublished: true
unlisted: true
---
<style>
.preview-wrapper {
border: var(--layout-example-border);
border-radius: var(--wa-border-radius-m);
min-block-size: 3em;
min-inline-size: 5em;
padding: var(--wa-space-2xs);
}
.preview-block {
aspect-ratio: 1 / 1;
background-color: var(--layout-example-element-background);
border-radius: var(--wa-border-radius-s);
min-block-size: 1em;
}
</style>
Web Awesome includes classes to set the `justify-content` property of flex and grid containers. Use them alongside other Web Awesome layout utilities, like [cluster](/docs/utilities/cluster) and [stack](/docs/utilities/stack), to distribute space between items along the container's [main axis](#whats-the-main-axis).
| Class Name | `justify-content` Value | Preview |
| ---------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `wa-justify-content-start` | `flex-start` | <div class="wa-cluster wa-gap-2xs wa-justify-content-start preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-end` | `flex-end` | <div class="wa-cluster wa-gap-2xs wa-justify-content-end preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-center` | `center` | <div class="wa-cluster wa-gap-2xs wa-justify-content-center preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-space-around` | `space-around` | <div class="wa-cluster wa-gap-2xs wa-justify-content-space-around preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-space-between` | `space-between` | <div class="wa-cluster wa-gap-2xs wa-justify-content-space-between preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
| `wa-justify-content-space-evenly` | `space-evenly` | <div class="wa-cluster wa-gap-2xs wa-justify-content-space-evenly preview-wrapper"><div class="preview-block"></div><div class="preview-block"></div></div> |
## What's the Main Axis?
The main axis runs parallel to a container's content direction. For grid containers and flex containers where `flex-direction` is `row`, the main axis runs in the inline direction. For containers where `flex-direction` is `column`, the main axis runs in the block direction.

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-split']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-split'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-split">
<div></div>

View File

@@ -7,19 +7,21 @@ tags: layoutUtilities
<style>
:is(.wa-flank, .wa-grid, .wa-stack) > [class*='wa-stack']:has(div:empty) {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-s);
border: var(--layout-example-border);
border-radius: var(--layout-example-border-radius);
padding: var(--layout-example-padding);
}
[class*='wa-stack'] div:empty {
background-color: var(--wa-color-indigo-60);
border-radius: var(--wa-border-radius-m);
background-color: var(--layout-example-element-background);
border-radius: var(--layout-example-element-border-radius);
min-block-size: 4rem;
min-inline-size: 4rem;
}
</style>
{{ description }}
```html {.example}
<div class="wa-stack">
<div></div>

View File

@@ -4,7 +4,7 @@
"access": "public"
},
"description": "A forward-thinking library of web components.",
"version": "3.0.0",
"version": "3.1.0",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -67,7 +67,7 @@
"check-updates": "npm-check-updates --cooldown 7 --interactive --format group",
"print-version": "echo $npm_package_version",
"tag-version": "git tag -a \"v$(npm run print-version | tail -n1)\" -m \"tag v$(npm run print-version | tail -n1)\"",
"postversion": "npm run tag-version"
"postversion": "node ./scripts/update-root-version.js"
},
"engines": {
"node": ">=14.17.0"

View File

@@ -37,7 +37,7 @@ export default function (plop) {
},
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.css',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.styles.ts',
templateFile: 'templates/component/styles.hbs',
},
{

View File

@@ -2,7 +2,7 @@ import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './{{ tagWithoutPrefix tag }}.css';
import styles from './{{ tagWithoutPrefix tag }}.styles.js';
/**
* @summary Short summary of the component's intended use.
@@ -21,7 +21,7 @@ import styles from './{{ tagWithoutPrefix tag }}.css';
*/
@customElement("{{ tag }}")
export default class {{ properCase tag }} extends WebAwesomeElement {
static css = styles;
static css = [styles];
/** An example attribute. */
@property() attr = 'example';

View File

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

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env node
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const monorepoRoot = path.resolve(__dirname, '..', '..', '..');
const rootPackageJSONFile = path.join(monorepoRoot, 'package.json');
const webawesomePackageJSONFile = path.join(path.resolve(__dirname, '..'), 'package.json');
const rootPackageJSON = JSON.parse(fs.readFileSync(rootPackageJSONFile, { encoding: 'utf8' }));
const webawesomePackageJSON = JSON.parse(fs.readFileSync(webawesomePackageJSONFile, { encoding: 'utf8' }));
const currentVersion = webawesomePackageJSON.version;
rootPackageJSON.version = currentVersion;
fs.writeFileSync(rootPackageJSONFile, JSON.stringify(rootPackageJSON, null, 2));
const versionsFile = path.join(monorepoRoot, 'VERSIONS.txt');
const versions = fs.readFileSync(versionsFile, { encoding: 'utf8' }).split(/\r?\n/);
// TODO: Make this smart and understand semver and "insert" in the correct spot instead of appending.
if (!versions.includes(currentVersion)) {
fs.appendFileSync(versionsFile, webawesomePackageJSON.version);
}

View File

@@ -115,7 +115,6 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
* The "form owner" to associate the button with. If omitted, the closest containing form will be used instead. The
* value of this attribute must be an id of a form in the same document or shadow root as the button.
*/
@property({ reflect: true }) form: string | null = null;
/** Used to override the form owner's `action` attribute. */
@property({ attribute: 'formaction' }) formAction: string;
@@ -135,24 +134,27 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
private constructLightDOMButton() {
const button = document.createElement('button');
for (const attribute of this.attributes) {
if (attribute.name === 'style') {
// Skip style attributes as they *shouldn't* be necessary
continue;
}
button.setAttribute(attribute.name, attribute.value);
}
button.type = this.type;
button.style.position = 'absolute';
button.style.width = '0';
button.style.height = '0';
button.style.clipPath = 'inset(50%)';
button.style.overflow = 'hidden';
button.style.whiteSpace = 'nowrap';
button.style.position = 'absolute !important';
button.style.width = '0 !important';
button.style.height = '0 !important';
button.style.clipPath = 'inset(50%) !important';
button.style.overflow = 'hidden !important';
button.style.whiteSpace = 'nowrap !important';
if (this.name) {
button.name = this.name;
}
button.value = this.value || '';
['form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
if (this.hasAttribute(attr)) {
button.setAttribute(attr, this.getAttribute(attr)!);
}
});
return button;
}

View File

@@ -108,13 +108,6 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
@property({ type: Boolean, reflect: true, attribute: 'checked' }) defaultChecked: boolean =
this.hasAttribute('checked');
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the checkbox a required field. */
@property({ type: Boolean, reflect: true }) required = false;

View File

@@ -220,13 +220,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
*/
@property() swatches: string | string[] = '';
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the color picker a required field. */
@property({ type: Boolean, reflect: true }) required = false;
@@ -1284,7 +1277,14 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
})}
part="trigger-container form-control"
>
<div part="form-control-label" class="label" id="form-control-label">
<div
part="form-control-label"
class=${classMap({
label: true,
'has-label': hasLabel,
})}
id="form-control-label"
>
<slot name="label">${this.label}</slot>
</div>

View File

@@ -155,20 +155,21 @@ export default class WaDropdownItem extends WebAwesomeElement {
/** Opens the submenu. */
async openSubmenu() {
if (!this.hasSubmenu || !this.submenuElement) return;
const submenu = this.submenuElement;
if (!this.hasSubmenu || !submenu || !this.isConnected) return;
// Notify parent dropdown to handle positioning
this.notifyParentOfOpening();
// Use Popover API to show the submenu
this.submenuElement.showPopover();
this.submenuElement.hidden = false;
this.submenuElement.setAttribute('data-visible', '');
submenu.showPopover?.();
submenu.hidden = false;
submenu.setAttribute('data-visible', '');
this.submenuOpen = true;
this.setAttribute('aria-expanded', 'true');
// Animate the submenu
await animateWithClass(this.submenuElement, 'show');
await animateWithClass(submenu, 'show');
// Set focus to the first submenu item
setTimeout(() => {
@@ -210,16 +211,19 @@ export default class WaDropdownItem extends WebAwesomeElement {
/** Closes the submenu. */
async closeSubmenu() {
if (!this.hasSubmenu || !this.submenuElement) return;
const submenu = this.submenuElement;
if (!this.hasSubmenu || !submenu) return;
this.submenuOpen = false;
this.setAttribute('aria-expanded', 'false');
if (!this.submenuElement.hidden) {
await animateWithClass(this.submenuElement, 'hide');
this.submenuElement.hidden = true;
this.submenuElement.removeAttribute('data-visible');
this.submenuElement.hidePopover();
if (!submenu.hidden) {
await animateWithClass(submenu, 'hide');
if (submenu?.isConnected) {
submenu.hidden = true;
submenu.removeAttribute('data-visible');
submenu.hidePopover?.();
}
}
}

View File

@@ -138,9 +138,9 @@ export default class WaDropdown extends WebAwesomeElement {
/** Gets all dropdown items slotted in the menu. */
private getItems(includeDisabled = false): WaDropdownItem[] {
const items = this.defaultSlot
.assignedElements({ flatten: true })
.filter(el => el.localName === 'wa-dropdown-item') as WaDropdownItem[];
const items = (this.defaultSlot?.assignedElements({ flatten: true }) ?? []).filter(
el => el.localName === 'wa-dropdown-item',
) as WaDropdownItem[];
return includeDisabled ? items : items.filter(item => !item.disabled);
}
@@ -165,9 +165,9 @@ export default class WaDropdown extends WebAwesomeElement {
/** Syncs item sizes with the dropdown's size property. */
private syncItemSizes() {
const items = this.defaultSlot
.assignedElements({ flatten: true })
.filter(el => el.localName === 'wa-dropdown-item') as WaDropdownItem[];
const items = (this.defaultSlot?.assignedElements({ flatten: true }) ?? []).filter(
el => el.localName === 'wa-dropdown-item',
) as WaDropdownItem[];
items.forEach(item => (item.size = this.size));
}
@@ -230,7 +230,7 @@ export default class WaDropdown extends WebAwesomeElement {
/** Shows the dropdown menu. This should only be called from within updated(). */
private async showMenu() {
const anchor = this.getTrigger();
if (!anchor) return;
if (!anchor || !this.popup || !this.menu) return;
const showEvent = new WaShowEvent();
this.dispatchEvent(showEvent);
@@ -270,6 +270,8 @@ export default class WaDropdown extends WebAwesomeElement {
/** Hides the dropdown menu. This should only be called from within updated(). */
private async hideMenu() {
if (!this.popup || !this.menu) return;
const hideEvent = new WaHideEvent({ source: this });
this.dispatchEvent(hideEvent);
if (hideEvent.defaultPrevented) {
@@ -720,12 +722,12 @@ export default class WaDropdown extends WebAwesomeElement {
nativeButton.setAttribute('aria-haspopup', 'menu');
nativeButton.setAttribute('aria-expanded', this.open ? 'true' : 'false');
this.menu.setAttribute('aria-expanded', 'false');
this.menu?.setAttribute('aria-expanded', 'false');
}
render() {
// On initial render, we want to use this.open, for everything else, we sync off of this.popup.active to get animations working.
let active = this.hasUpdated ? this.popup.active : this.open;
let active = this.hasUpdated ? this.popup?.active : this.open;
return html`
<wa-popup

View File

@@ -6,7 +6,6 @@ export default css`
}
.text-field {
flex: auto;
display: flex;
align-items: stretch;
justify-content: start;

View File

@@ -48,6 +48,27 @@ describe('<wa-input>', () => {
expect(input.title).to.equal('Test');
});
it('should have label with "has-label" class if label has a slotted element', async () => {
const el = await fixture<WaInput>(html` <wa-input><span slot="label">Name</span></wa-input> `);
await el.updateComplete;
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
expect(label.classList.contains('has-label')).to.equal(true);
});
it('should have label with "has-label" class if label is provided as an attribute', async () => {
const el = await fixture<WaInput>(html` <wa-input label="Name"></wa-input> `);
await el.updateComplete;
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
expect(label.classList.contains('has-label')).to.equal(true);
});
it('should not have "has-label" class on label if no label content is provided', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
await el.updateComplete;
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
expect(label.classList.contains('has-label')).to.equal(false);
});
it('should be disabled with the disabled attribute', async () => {
const el = await fixture<WaInput>(html` <wa-input disabled></wa-input> `);
await el.updateComplete;

View File

@@ -140,13 +140,6 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
@property({ attribute: 'without-spin-buttons', type: Boolean }) withoutSpinButtons = false;
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the input a required field. */
@property({ type: Boolean, reflect: true }) required = false;
@@ -349,7 +342,15 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
(typeof this.value === 'number' || (this.value && this.value.length > 0));
return html`
<label part="form-control-label label" class="label" for="input" aria-hidden=${hasLabel ? 'false' : 'true'}>
<label
part="form-control-label label"
class=${classMap({
label: true,
'has-label': hasLabel,
})}
for="input"
aria-hidden=${hasLabel ? 'false' : 'true'}
>
<slot name="label">${this.label}</slot>
</label>

View File

@@ -5,6 +5,7 @@ import getText from '../../internal/get-text.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import type WaSelect from '../select/select.js';
import styles from './option.styles.js';
/**
@@ -109,7 +110,7 @@ export default class WaOption extends WebAwesomeElement {
this.updateDefaultLabel();
if (this.isInitialized) {
// When the label changes, tell the controller to update
// When the label changes, tell the parent <wa-select> to update
customElements.whenDefined('wa-select').then(() => {
const controller = this.closest('wa-select');
if (controller) {
@@ -117,6 +118,16 @@ export default class WaOption extends WebAwesomeElement {
controller.selectionChanged?.();
}
});
// When the label changes, tell the parent <wa-combobox> to update
customElements.whenDefined('wa-combobox').then(() => {
// We cast to <wa-select> because it shares the same API as combobox
const controller = this.closest<WaSelect>('wa-combobox');
if (controller) {
controller.handleDefaultSlotChange();
controller.selectionChanged?.();
}
});
} else {
this.isInitialized = true;
}
@@ -134,7 +145,8 @@ export default class WaOption extends WebAwesomeElement {
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.has('defaultSelected')) {
if (!this.closest('wa-select')?.hasInteracted) {
// We cast to <wa-select> because it shares the same API as combobox
if (!this.closest<WaSelect>('wa-combobox, wa-select')?.hasInteracted) {
const oldVal = this.selected;
this.selected = this.defaultSelected;
this.requestUpdate('selected', oldVal);

View File

@@ -286,11 +286,11 @@ export default class WaPopup extends WebAwesomeElement {
private start() {
// We can't start the positioner without an anchor
if (!this.anchorEl || !this.active) {
if (!this.anchorEl || !this.active || !this.isConnected) {
return;
}
this.popup.showPopover?.();
this.popup?.showPopover?.();
this.cleanup = autoUpdate(this.anchorEl, this.popup, () => {
this.reposition();
@@ -299,7 +299,7 @@ export default class WaPopup extends WebAwesomeElement {
private async stop(): Promise<void> {
return new Promise(resolve => {
this.popup.hidePopover?.();
this.popup?.hidePopover?.();
if (this.cleanup) {
this.cleanup();
@@ -317,7 +317,7 @@ export default class WaPopup extends WebAwesomeElement {
/** Forces the popup to recalculate and reposition itself. */
reposition() {
// Nothing to do if the popup is inactive or the anchor doesn't exist
if (!this.active || !this.anchorEl) {
if (!this.active || !this.anchorEl || !this.popup) {
return;
}
@@ -498,7 +498,7 @@ export default class WaPopup extends WebAwesomeElement {
}
private updateHoverBridge = () => {
if (this.hoverBridge && this.anchorEl) {
if (this.hoverBridge && this.anchorEl && this.popup) {
const anchorRect = this.anchorEl.getBoundingClientRect();
const popupRect = this.popup.getBoundingClientRect();
const isVertical = this.placement.includes('top') || this.placement.includes('bottom');

View File

@@ -15,6 +15,8 @@ import styles from './progress-ring.styles.js';
*
* @csspart base - The component's base wrapper.
* @csspart label - The progress ring label.
* @csspart track - The progress ring's track.
* @csspart indicator - The progress ring's indicator.
*
* @cssproperty --size - The diameter of the progress ring (cannot be a percentage).
* @cssproperty --track-width - The width of the track.
@@ -70,8 +72,8 @@ export default class WaProgressRing extends WebAwesomeElement {
style="--percentage: ${this.value / 100}"
>
<svg class="image">
<circle class="track"></circle>
<circle class="indicator" style="stroke-dashoffset: ${this.indicatorOffset}"></circle>
<circle part="track" class="track"></circle>
<circle part="indicator" class="indicator" style="stroke-dashoffset: ${this.indicatorOffset}"></circle>
</svg>
<slot id="label" part="label" class="label"></slot>

View File

@@ -352,7 +352,10 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
<label
part="form-control-label"
id="label"
class="label"
class=${classMap({
label: true,
'has-label': hasLabel,
})}
aria-hidden=${hasLabel ? 'false' : 'true'}
@click=${this.handleLabelClick}
>

View File

@@ -39,11 +39,6 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
/** @internal Used by radio group to force disable radios while preserving their original disabled state. */
@state() forceDisabled = false;
/**
* The string pointing to a form's id.
*/
@property({ reflect: true }) form: string | null = null;
/** The radio's value. When selected, the radio group will receive this value. */
@property({ reflect: true }) value: string;

View File

@@ -7,7 +7,7 @@ import { WaAfterHideEvent } from '../../events/after-hide.js';
import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaClearEvent } from '../../events/clear.js';
import { WaHideEvent } from '../../events/hide.js';
import type { WaRemoveEvent } from '../../events/remove.js';
import { WaRemoveEvent } from '../../events/remove.js';
import { WaShowEvent } from '../../events/show.js';
import { animateWithClass } from '../../internal/animate.js';
import { waitForEvent } from '../../internal/event.js';
@@ -99,6 +99,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
private readonly hasSlotController = new HasSlotController(this, 'hint', 'label');
private readonly localize = new LocalizeController(this);
private selectionOrder: Map<string, number> = new Map();
private typeToSelectString = '';
private typeToSelectTimeout: number;
@@ -256,13 +257,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
*/
@property({ attribute: 'with-hint', type: Boolean }) withHint = false;
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** The select's required attribute. */
@property({ type: Boolean, reflect: true }) required = false;
@@ -285,6 +279,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
?pill=${this.pill}
size=${this.size}
with-remove
data-value=${option.value}
@wa-remove=${(event: WaRemoveEvent) => this.handleTagRemove(event, option)}
>
${option.label}
</wa-tag>
@@ -520,6 +516,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
event.stopPropagation();
if (this.value !== null) {
this.selectionOrder.clear();
this.setSelectedOptions([]);
this.displayInput.focus({ preventScroll: true });
@@ -603,24 +600,20 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
if (this.disabled) return;
// Mark as interacted so selectionChanged() uses the correct filter logic
this.hasInteracted = true;
this.valueHasChanged = true;
// Use the directly provided option if available (from getTag method)
let option = directOption;
// If no direct option was provided, find the option from the event path
// If no direct option was provided, find the option from the data-value attribute
if (!option) {
const tagElement = (event.target as Element).closest('wa-tag[part~=tag]');
const tagElement = (event.target as Element).closest('wa-tag[data-value]') as HTMLElement | null;
if (tagElement) {
// Find the index of this tag among all tags
const tagsContainer = this.shadowRoot?.querySelector('[part="tags"]');
if (tagsContainer) {
const allTags = Array.from(tagsContainer.children);
const index = allTags.indexOf(tagElement as HTMLElement);
if (index >= 0 && index < this.selectedOptions.length) {
option = this.selectedOptions[index];
}
}
const value = tagElement.dataset.value;
option = this.selectedOptions.find(opt => opt.value === value);
}
}
@@ -707,7 +700,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
const options = this.getAllOptions();
// Update selected options cache
this.selectedOptions = options.filter(el => {
const newSelectedOptions = options.filter(el => {
if (!this.hasInteracted && !this.valueHasChanged) {
const defaultValue = this.defaultValue;
const defaultValues = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
@@ -717,6 +710,32 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
return el.selected;
});
// Update the selection order map
const newSelectedValues = new Set(newSelectedOptions.map(el => el.value));
// Remove deselected options from the order map
for (const value of this.selectionOrder.keys()) {
if (!newSelectedValues.has(value)) {
this.selectionOrder.delete(value);
}
}
// Add newly selected options
const maxOrder = this.selectionOrder.size > 0 ? Math.max(...this.selectionOrder.values()) : -1;
let nextOrder = maxOrder + 1;
for (const option of newSelectedOptions) {
if (!this.selectionOrder.has(option.value)) {
this.selectionOrder.set(option.value, nextOrder++);
}
}
// Sort options by selection order
this.selectedOptions = newSelectedOptions.sort((a, b) => {
const orderA = this.selectionOrder.get(a.value) ?? 0;
const orderB = this.selectionOrder.get(b.value) ?? 0;
return orderA - orderB;
});
let selectedValues = new Set(this.selectedOptions.map(el => el.value));
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
@@ -888,6 +907,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
}
formResetCallback() {
this.selectionOrder.clear();
this.value = this.defaultValue;
super.formResetCallback();
this.handleValueChange();
@@ -918,7 +938,10 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
<label
id="label"
part="form-control-label label"
class="label"
class=${classMap({
label: true,
'has-label': hasLabel,
})}
aria-hidden=${hasLabel ? 'false' : 'true'}
@click=${this.handleLabelClick}
>

View File

@@ -167,12 +167,6 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
/** The starting value from which to draw the slider's fill, which is based on its current value. */
@property({ attribute: 'indicator-offset', type: Number }) indicatorOffset: number;
/**
* The form to associate this control with. If omitted, the closest containing `<form>` will be used. The value of
* this attribute must be an ID of a form in the same document or shadow root.
*/
@property({ reflect: true }) form = null;
/** The minimum value allowed. */
@property({ type: Number }) min: number = 0;
@@ -437,8 +431,14 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
/** Clamps a number to min/max while ensuring it's a valid step interval. */
private clampAndRoundToStep(value: number) {
const stepPrecision = (String(this.step).split('.')[1] || '').replace(/0+$/g, '').length;
value = Math.round(value / this.step) * this.step;
value = clamp(value, this.min, this.max);
// Ensure we're working with numbers (in case the user passes strings to the respective properties)
const step = Number(this.step);
const min = Number(this.min);
const max = Number(this.max);
value = Math.round(value / step) * step;
value = clamp(value, min, max);
return parseFloat(value.toFixed(stepPrecision));
}
@@ -807,7 +807,7 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
id="label"
part="label"
for=${this.isRange ? 'thumb-min' : 'text-box'}
class=${classMap({ vh: !hasLabel })}
class=${classMap({ vh: !hasLabel, 'has-label': hasLabel })}
@pointerdown=${this.handleLabelPointerDown}
>
<slot name="label">${this.label}</slot>

View File

@@ -80,13 +80,6 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
@property({ type: Boolean, attribute: 'checked', reflect: true }) defaultChecked: boolean =
this.hasAttribute('checked');
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the switch a required field. */
@property({ type: Boolean, reflect: true }) required = false;

View File

@@ -107,13 +107,6 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
/** Makes the textarea readonly. */
@property({ type: Boolean, reflect: true }) readonly = false;
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
@property({ reflect: true }) form = null;
/** Makes the textarea a required field. */
@property({ type: Boolean, reflect: true }) required = false;
@@ -338,7 +331,15 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
const hasHint = this.hint ? true : !!hasHintSlot;
return html`
<label part="form-control-label label" class="label" for="input" aria-hidden=${hasLabel ? 'false' : 'true'}>
<label
part="form-control-label label"
class=${classMap({
label: true,
'has-label': hasLabel,
})}
for="input"
aria-hidden=${hasLabel ? 'false' : 'true'}
>
<slot name="label">${this.label}</slot>
</label>

View File

@@ -79,7 +79,7 @@ export default class WaTooltip extends WebAwesomeElement {
/** The amount of time to wait before showing the tooltip when the user mouses in. */
@property({ attribute: 'show-delay', type: Number }) showDelay = 150;
/** The amount of time to wait before hiding the tooltip when the user mouses out.. */
/** The amount of time to wait before hiding the tooltip when the user mouses out. */
@property({ attribute: 'hide-delay', type: Number }) hideDelay = 0;
/**

View File

@@ -85,6 +85,10 @@ export default css`
display: none;
}
.tree-item:not(.tree-item-has-expand-button):not(.tree-item-loading) .expand-button {
cursor: default;
}
.tree-item-loading .expand-icon-slot wa-icon {
display: none;
}

View File

@@ -243,7 +243,7 @@ export default class WaTreeItem extends WebAwesomeElement {
}
render() {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
const isRtl = this.localize.dir() === 'rtl';
const showExpandButton = !this.loading && (!this.isLeaf || this.lazy);
return html`

View File

@@ -28,3 +28,4 @@ export type { WaSlideChangeEvent } from './slide-change.js';
export type { WaStartEvent } from './start.js';
export type { WaTabHideEvent } from './tab-hide.js';
export type { WaTabShowEvent } from './tab-show.js';
export type { WaVideoChangeEvent } from './video-change.js';

View File

@@ -0,0 +1,26 @@
export class WaVideoChangeEvent extends Event {
readonly detail: WaVideoChangeEventDetail;
constructor(detail: WaVideoChangeEventDetail) {
super('wa-video-change', { bubbles: true, cancelable: false, composed: true });
this.detail = detail;
}
}
export interface WaVideoChangeEventDetail {
previousIndex: number;
currentIndex: number;
video: {
title?: string;
poster?: string;
duration?: string;
sources: { src: string; type: string }[];
tracks: { src: string; kind: string; srclang: string; label: string }[];
};
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-video-change': WaVideoChangeEvent;
}
}

View File

@@ -99,20 +99,24 @@ export class DraggableElement {
document.addEventListener('pointerup', this.handleDragStop);
document.addEventListener('pointermove', this.handleDragMove);
document.addEventListener('pointercancel', this.handleDragStop);
document.addEventListener('touchend', this.handleDragStop);
document.addEventListener('touchmove', this.handleDragMove);
document.addEventListener('touchcancel', this.handleDragStop);
this.options.start(clientX, clientY);
};
private handleDragStop = (event: PointerEvent | TouchEvent) => {
const clientX = 'touches' in event ? event.touches[0].clientX : (event as PointerEvent).clientX;
const clientY = 'touches' in event ? event.touches[0].clientY : (event as PointerEvent).clientY;
const clientX = 'changedTouches' in event ? event.changedTouches[0].clientX : (event as PointerEvent).clientX;
const clientY = 'changedTouches' in event ? event.changedTouches[0].clientY : (event as PointerEvent).clientY;
this.isDragging = false;
document.removeEventListener('pointerup', this.handleDragStop);
document.removeEventListener('pointermove', this.handleDragMove);
document.removeEventListener('pointercancel', this.handleDragStop);
document.removeEventListener('touchend', this.handleDragStop);
document.removeEventListener('touchmove', this.handleDragMove);
document.removeEventListener('touchcancel', this.handleDragStop);
this.options.stop(clientX, clientY);
};
@@ -141,8 +145,10 @@ export class DraggableElement {
public stop() {
document.removeEventListener('pointerup', this.handleDragStop);
document.removeEventListener('pointermove', this.handleDragMove);
document.removeEventListener('pointercancel', this.handleDragStop);
document.removeEventListener('touchend', this.handleDragStop);
document.removeEventListener('touchmove', this.handleDragMove);
document.removeEventListener('touchcancel', this.handleDragStop);
this.element.removeEventListener('pointerdown', this.handleDragStart);
if (supportsTouch) {
this.element.removeEventListener('touchstart', this.handleDragStart);

View File

@@ -23,7 +23,8 @@ export interface WebAwesomeFormControl extends WebAwesomeElement {
checked?: boolean;
defaultSelected?: boolean;
selected?: boolean;
form?: string | null;
get form(): HTMLFormElement | null;
set form(val: string);
value?: unknown;
@@ -203,6 +204,23 @@ export class WebAwesomeFormAssociatedElement
return this.internals.form;
}
/**
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
* the same document or shadow root for this to work.
*/
set form(val: string) {
if (val) {
this.setAttribute('form', val);
} else {
this.removeAttribute('form');
}
}
get form(): HTMLFormElement | null {
return this.internals.form;
}
@property({ attribute: false, state: true, type: Object })
get validity() {
return this.internals.validity;

View File

@@ -6,8 +6,14 @@ export default css`
flex-direction: column;
}
/* Treat wrapped labels, inputs, and hints as direct children of the host element */
[part~='form-control'] {
display: contents;
}
/* Label */
:is([part~='form-control-label'], [part~='label']):has(*:not(:empty)) {
:is([part~='form-control-label'], [part~='label']):has(*:not(:empty)),
:is([part~='form-control-label'], [part~='label']).has-label {
display: inline-flex;
color: var(--wa-form-control-label-color);
font-weight: var(--wa-form-control-label-font-weight);
@@ -29,7 +35,6 @@ export default css`
line-height: var(--wa-form-control-hint-line-height);
margin-block-start: 0.5em;
font-size: var(--wa-font-size-smaller);
line-height: var(--wa-form-control-label-line-height);
&:not(.has-slotted) {
display: none;

View File

@@ -5,6 +5,7 @@
@import url('utilities/scroll-lock.css');
@import url('utilities/placeholder.css');
@import url('utilities/align-items.css');
@import url('utilities/justify-content.css');
@import url('utilities/border-radius.css');
@import url('utilities/gap.css');
@import url('utilities/text.css');

View File

@@ -1,4 +1,15 @@
@layer wa-utilities {
/* Apply Flexbox with 0 specificity to ensure an align-items util produces a visible change */
:where(
.wa-align-items-start,
.wa-align-items-end,
.wa-align-items-center,
.wa-align-items-stretch,
.wa-align-items-baseline
) {
display: flex;
}
.wa-align-items-start {
align-items: flex-start;
}

View File

@@ -45,4 +45,7 @@
.wa-gap-3xl {
gap: var(--wa-space-3xl);
}
.wa-gap-4xl {
gap: var(--wa-space-4xl);
}
}

View File

@@ -0,0 +1,32 @@
@layer wa-utilities {
/* Apply Flexbox with 0 specificity to ensure a justify-content util produces a visible change */
:where(
.wa-justify-content-start,
.wa-justify-content-end,
.wa-justify-content-center,
.wa-justify-content-space-around,
.wa-justify-content-space-between,
.wa-justify-content-space-evenly
) {
display: flex;
}
.wa-justify-content-start {
justify-content: flex-start;
}
.wa-justify-content-end {
justify-content: flex-end;
}
.wa-justify-content-center {
justify-content: center;
}
.wa-justify-content-space-around {
justify-content: space-around;
}
.wa-justify-content-space-between {
justify-content: space-between;
}
.wa-justify-content-space-evenly {
justify-content: space-evenly;
}
}

View File

@@ -27,11 +27,11 @@
[class*='wa-cluster'] {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
:where([class*='wa-cluster']) {
align-items: center;
justify-content: flex-start;
}
/* #endregion */
@@ -75,7 +75,6 @@
[class*='wa-frame'] {
display: flex;
aspect-ratio: 1 / 1;
justify-content: center;
overflow: hidden;
}
@@ -98,6 +97,7 @@
:where([class*='wa-frame']) {
align-items: center;
justify-content: center;
}
/* #endregion */
@@ -118,7 +118,6 @@
[class*='wa-split'] {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
[class*='wa-split'],
@@ -141,6 +140,7 @@
:where([class*='wa-split']) {
align-items: center;
justify-content: space-between;
}
/* #endregion */
@@ -149,11 +149,11 @@
[class*='wa-stack'] {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
:where([class*='wa-stack']) {
align-items: stretch;
justify-content: flex-start;
}
/* #endregion */
}