Compare commits

...

19 Commits

Author SHA1 Message Date
konnorrogers
919aa20d40 watch-removal-diff 2024-11-25 14:19:24 -05:00
Cory LaViska
3e36d13c78 fix infinite loop 2024-11-25 12:33:15 -05:00
Cory LaViska
e7999b0458 disable prettier for now 2024-11-25 12:31:55 -05:00
Cory LaViska
99a6f37a93 re move dup 2024-11-25 12:01:36 -05:00
Cory LaViska
bc361c593b Merge branch 'next' into watch-removal 2024-11-25 12:00:55 -05:00
Cory LaViska
b5ac468fac exclude dropdown/tooltip 2024-11-25 11:51:59 -05:00
Cory LaViska
c7975060b7 udpate deps 2024-11-25 11:18:45 -05:00
Cory LaViska
38288aaefb add update script 2024-11-25 11:13:20 -05:00
Cory LaViska
d01291cf9e fix spelling 2024-11-25 11:00:50 -05:00
Cory LaViska
5b9af2190a remove @watch comments 2024-11-25 10:48:55 -05:00
Cory LaViska
5a12125674 who 2024-11-25 10:48:50 -05:00
Cory LaViska
6fec10d498 Merge branch 'next' into watch-removal 2024-11-22 08:59:23 -05:00
Cory LaViska
984627241f remove @watch decorator 2024-11-22 08:58:12 -05:00
Cory LaViska
9f6c501879 fix reload 2024-11-22 08:57:56 -05:00
Cory LaViska
8573c32912 add bluesky 2024-11-21 10:26:47 -05:00
Cory LaViska
d9fb3e7353 adjust content 2024-11-21 10:26:43 -05:00
Cory LaViska
29025a92d9 reflect like all other orientations 2024-11-15 14:39:02 -05:00
Cory LaViska
e65a712f70 #216 2024-11-15 14:38:23 -05:00
Cory LaViska
0bfe35b523 change vertical => orientation="vertical"; #216 2024-11-15 14:38:13 -05:00
58 changed files with 1134 additions and 1004 deletions

View File

@@ -15,6 +15,7 @@
"autoloading",
"autoplay",
"bezier",
"bluesky",
"boxicons",
"CACHEABLE",
"callout",
@@ -83,6 +84,7 @@
"jsfiddle",
"keydown",
"keyframes",
"Konnor",
"Kool",
"labelledby",
"Laravel",
@@ -120,6 +122,7 @@
"peta",
"petabit",
"Preact",
"preconnect",
"prismjs",
"progressbar",
"radiogroup",

View File

@@ -11,7 +11,7 @@ import { searchPlugin } from './_utils/search.js';
import { readFile } from 'fs/promises';
import { outlinePlugin } from './_utils/outline.js';
import componentList from './_data/componentList.js';
import litPlugin from '@lit-labs/eleventy-plugin-lit';
import ssrPlugin from '@lit-labs/eleventy-plugin-lit';
import process from 'process';
@@ -98,7 +98,14 @@ export default function (eleventyConfig) {
])
);
const omittedModules = [];
const omittedModules = [
//
// TODO - dropdown and tooltip fail to open properly after hydration, they show briefly and then hide. It looks like
// the body part (of tooltip) has the hidden attribute reapplied to it.
//
'dropdown',
'tooltip'
];
// problematic components:
// animation (breaks on navigation + ssr with Turbo)
@@ -106,13 +113,13 @@ export default function (eleventyConfig) {
// resize-observer (why SSR this?)
// tooltip (why SSR this?)
const componentModules = componentList
// .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
.map(component => {
const name = component.tagName.split(/wa-/)[1];
return `./dist/components/${name}/${name}.js`;
});
eleventyConfig.addPlugin(litPlugin, {
eleventyConfig.addPlugin(ssrPlugin, {
mode: 'worker',
componentModules
});
@@ -127,10 +134,13 @@ export default function (eleventyConfig) {
);
// Production-only plugins
if (!isDeveloping) {
// Run Prettier on each file (prod only because it can be slow)
eleventyConfig.addPlugin(formatCodePlugin());
}
//
// TODO - disabled because it takes about a minute to run now
//
// if (!isDeveloping) {
// // Run Prettier on each file (prod only because it can be slow)
// eleventyConfig.addPlugin(formatCodePlugin());
// }
return {
dir: {

View File

@@ -38,16 +38,16 @@ Use the `--spacing` custom property to change the amount of space between the di
</div>
```
### Vertical
### Orientation
Add the `vertical` attribute to draw the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container.
Set the `orientation` attribute to `vertical` to render the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container.
```html {.example}
<div style="display: flex; align-items: center; height: 2rem;">
First
<wa-divider vertical></wa-divider>
<wa-divider orientation="vertical"></wa-divider>
Middle
<wa-divider vertical></wa-divider>
<wa-divider orientation="vertical"></wa-divider>
Last
</div>
```

View File

@@ -79,12 +79,12 @@ To set the initial position in pixels instead of a percentage, use the `position
</wa-split-panel>
```
### Vertical
### Orientation
Add the `vertical` attribute to render the split panel in a vertical orientation where the start and end panels are stacked. You also need to set a height when using the vertical orientation.
Set the `orientation` attribute to `vertical` to render the split panel in a vertical orientation where the start and end panels are stacked. You also need to set a height when using the vertical orientation.
```html {.example}
<wa-split-panel vertical style="height: 400px;">
<wa-split-panel orientation="vertical" style="height: 400px;">
<div
slot="start"
style="height: 100%; background: var(--wa-color-surface-lowered); display: flex; align-items: center; justify-content: center; overflow: hidden;"
@@ -247,7 +247,7 @@ Create complex layouts that can be repositioned independently by nesting split p
Start
</div>
<div slot="end">
<wa-split-panel vertical style="height: 400px;">
<wa-split-panel orientation="vertical" style="height: 400px;">
<div
slot="start"
style="height: 100%; background: var(--wa-color-surface-lowered); display: flex; align-items: center; justify-content: center; overflow: hidden"

View File

@@ -35,24 +35,19 @@ TODO Page Description
<div style="border-bottom: 1px solid var(--wa-color-surface-border);margin-bottom: 1rem; padding-bottom: 1rem;">
<img src="https://img.fortawesome.com/cfa83f3c/article-flower.jpg" alt="">
<h2 style="margin-bottom: var(--wa-space-s);">Title</h2>
<p style="margin-bottom: var(--wa-space-3xs);">Fusce lectus lorem, tincidunt non semper sit amet, laoreet vitae nunc. Morbi egestas, libero vitae elementum pretium, nibh ipsum faucibus lacus, id pretium urna ligula eu mauris. Aliquam erat volutpat. Mauris pharetra lacus rhoncus ligula bibendum, at consectetur erat auctor.</p>
<span style="font-size: small;font-weight: 600;font-style: italic;">sub-title</span>
<p style="margin-bottom: var(--wa-space-3xs);">Well, the way they make shows is, they make one show. That show's called a pilot.</p>
<span style="font-size: small;font-weight: 600;font-style: italic;">sub-title</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div>
<img src="https://img.fortawesome.com/cfa83f3c/article-flower.jpg" alt="">
<p style="margin-bottom: var(--wa-space-3xs);">Etiam et tincidunt est, sollicitudin fermentum ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Ut suscipit libero at velit fringilla, ac pretium lorem rutrum. Cras luctus blandit semper.</p>
<span style="font-size: small;font-weight: 600;font-style: italic;">sub-title</span>
<img src="https://img.fortawesome.com/cfa83f3c/article-flower.jpg" alt="">
<p style="margin-bottom: var(--wa-space-3xs);">Normally, both your asses would be dead as fucking fried chicken.</p>
<span style="font-size: small;font-weight: 600;font-style: italic;">sub-title</span>
</div>
<div>
<img src="https://img.fortawesome.com/cfa83f3c/article-flower.jpg" alt="">
<p style="margin-bottom: var(--wa-space-3xs);">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris in fringilla ante. In mattis sapien ac aliquet mattis.</p>
<span style="font-size: small;font-weight: 600;font-style: italic;">sub-title</span>
<img src="https://img.fortawesome.com/cfa83f3c/article-flower.jpg" alt="">
<p style="margin-bottom: var(--wa-space-3xs);">Besides, I've already been through too much shit this morning over this case to hand it over to your dumb ass.</p>
<span style="font-size: small;font-weight: 600;font-style: italic;">sub-title</span>
</div>
</div>
</div>
@@ -64,36 +59,36 @@ TODO Page Description
```html{.example}
<div class="news-footer">
<div class="container">
<!-- <div class="logo"> <wa-icon name="user-secret"></wa-icon> <h1 style="--wa-space-xl: 0;">Daily Snoop</h1></div> -->
<!-- <div class="logo"> <wa-icon name="user-secret"></wa-icon><h1 style="--wa-space-xl: 0;">Daily Snoop</h1></div> -->
<div class="nav">
<section>
<h4 style="--wa-space-xl: 0;">News</h4>
<ul>
<li><a href="#">U.S.</a></li>
<li><a href="#">World</a></li>
<li><a href="#">Politics</a></li>
<li><a href="#">Education</a></li>
<li><a href="#">Sports</a></li>
<li><a href="#">Business</a></li>
<li><a href="#">Tech</a></li>
<li><a href="#">Science</a></li>
</ul>
<li><a href="#">U.S.</a></li>
<li><a href="#">World</a></li>
<li><a href="#">Politics</a></li>
<li><a href="#">Education</a></li>
<li><a href="#">Sports</a></li>
<li><a href="#">Business</a></li>
<li><a href="#">Tech</a></li>
<li><a href="#">Science</a></li>
</ul>
</section>
<section>
<h4 style="--wa-space-xl: 0;">Arts</h4>
<ul>
<li><a href="#">Book Review</a></li>
<li><a href="#">Dance</a></li>
<li><a href="#">Movies</a></li>
<li><a href="#">Pop Culture</a></li>
</ul>
<li><a href="#">Book Review</a></li>
<li><a href="#">Dance</a></li>
<li><a href="#">Movies</a></li>
<li><a href="#">Pop Culture</a></li>
</ul>
</section>
<section>
<h4 style="--wa-space-xl: 0;">Subscriptions</h4>
<ul class="list">
<li><a href="#"><wa-icon fixed-width name="game-board-simple"></wa-icon> Crossword</a></li>
<li><a href="#"><wa-icon fixed-width name="paper-plane"></wa-icon> Newsletters</a></li>
<li><a href="#"><wa-icon fixed-width name="microphone-lines"></wa-icon> Podcast</a></li>
<li><a href="#"><wa-icon fixed-width name="game-board-simple"></wa-icon>Crossword</a></li>
<li><a href="#"><wa-icon fixed-width name="paper-plane"></wa-icon>Newsletters</a></li>
<li><a href="#"><wa-icon fixed-width name="microphone-lines"></wa-icon>Podcast</a></li>
</ul>
</section>
</div>
@@ -115,12 +110,11 @@ TODO Page Description
<img src="https://img.fortawesome.com/cfa83f3c/app_store.svg" alt="">
<img src="https://img.fortawesome.com/cfa83f3c/google_play.svg" alt="">
</div>
<div class="legal">&#169; 2024 All rights reserved.</div>
<div class="legal">&copy; 2024 All rights reserved.</div>
</div>
</div>
<style>
.news-footer {
.container {
max-width: 960px;
margin: auto;

View File

@@ -188,9 +188,8 @@ When authoring components, please try to follow the same structure and conventio
- `@query` decorators
- `@state` decorators
- `@property` decorators
- Lifecycle methods (`connectedCallback()`, `disconnectedCallback()`, `firstUpdated()`, etc.)
- Lifecycle methods (`connectedCallback()`, `disconnectedCallback()`, `firstUpdated()`, `updated()`, etc.)
- Private methods
- `@watch` decorators
- Public methods
- The `render()` method

284
package-lock.json generated
View File

@@ -9,23 +9,23 @@
"version": "3.0.0-alpha.4",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.0.2",
"@floating-ui/dom": "^1.5.3",
"@shoelace-style/animations": "^1.1.0",
"@ctrl/tinycolor": "^4.1.0",
"@floating-ui/dom": "^1.6.12",
"@shoelace-style/animations": "^1.2.0",
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.4",
"lit": "^3.0.0",
"lit": "^3.2.1",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0-alpha.5",
"@custom-elements-manifest/analyzer": "^0.9.4",
"@custom-elements-manifest/analyzer": "^0.10.3",
"@lit-labs/eleventy-plugin-lit": "^1.0.3",
"@lit-labs/testing": "^0.2.4",
"@lit/react": "^1.0.0",
"@lit-labs/testing": "^0.2.5",
"@lit/react": "^1.0.6",
"@open-wc/testing": "^3.2.0",
"@types/mocha": "^10.0.2",
"@types/react": "^18.2.28",
"@types/mocha": "^10.0.10",
"@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@web/dev-server-esbuild": "^0.3.6",
@@ -37,7 +37,7 @@
"change-case": "^4.1.2",
"chokidar": "^3.5.3",
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.0",
"comment-parser": "^1.4.1",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.4.0",
"custom-element-vs-code-integration": "^1.2.1",
@@ -56,31 +56,32 @@
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
"eslint-plugin-wc": "^2.0.4",
"front-matter": "^4.0.2",
"get-port": "^7.0.0",
"get-port": "^7.1.0",
"globby": "^13.2.2",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"lunr": "^2.3.9",
"markdown-it": "^14.1.0",
"markdown-it-attrs": "^4.1.6",
"markdown-it-attrs": "^4.2.0",
"markdown-it-container": "^3.0.0",
"markdown-it-ins": "^3.0.1",
"markdown-it-kbd": "^2.2.2",
"markdown-it-mark": "^3.0.1",
"marked": "^11.1.0",
"node-html-parser": "^6.1.13",
"ora": "^8.0.1",
"npm-check-updates": "^17.1.11",
"ora": "^8.1.1",
"pascal-case": "^3.1.2",
"playwright": "^1.46.1",
"plop": "^4.0.0",
"prettier": "^3.0.3",
"playwright": "^1.49.0",
"plop": "^4.0.1",
"prettier": "^3.3.3",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react": "^18.3.1",
"recursive-copy": "^2.0.14",
"sinon": "^16.1.0",
"source-map": "^0.7.4",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"user-agent-data-types": "^0.3.1",
"uuid": "^9.0.1"
},
@@ -856,17 +857,17 @@
}
},
"node_modules/@ctrl/tinycolor": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.3.tgz",
"integrity": "sha512-e9nEVehVJwkymQpkGhdSNzLT2Lr9UTTby+JePq4Z2SxBbOQjY7pLgSouAaXvfaGQVSAaY0U4eJdwfSDmCbItcw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
"integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==",
"engines": {
"node": ">=14"
}
},
"node_modules/@custom-elements-manifest/analyzer": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.9.4.tgz",
"integrity": "sha512-XPjEbfkq71oQl6tIfDy1ashdvOf42p3BtqebEaUohqTEPAyBuShGwQiB2B7xjsUwi3VfSQCnPlGwmigIDPjtWg==",
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/analyzer/-/analyzer-0.10.3.tgz",
"integrity": "sha512-e2Ax59vK9sNedmDlPqZS11L54iAlKSjOJuv5etpTy5SygLBW3GcUtocHZm8wO013L0griTPpgWB0tuV7/JXy5A==",
"dev": true,
"dependencies": {
"@custom-elements-manifest/find-dependencies": "^0.0.5",
@@ -968,6 +969,19 @@
"node": ">=8"
}
},
"node_modules/@custom-elements-manifest/analyzer/node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/@custom-elements-manifest/find-dependencies": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@custom-elements-manifest/find-dependencies/-/find-dependencies-0.0.5.tgz",
@@ -1466,26 +1480,26 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz",
"integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==",
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
"integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
"dependencies": {
"@floating-ui/utils": "^0.2.0"
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz",
"integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==",
"version": "1.6.12",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
"integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
"dependencies": {
"@floating-ui/core": "^1.5.3",
"@floating-ui/utils": "^0.2.0"
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
},
"node_modules/@github/catalyst": {
"version": "1.6.0",
@@ -1682,9 +1696,9 @@
}
},
"node_modules/@lit-labs/testing": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@lit-labs/testing/-/testing-0.2.4.tgz",
"integrity": "sha512-NasNKbELasyfA1vIcfMwM0H/2mE98uFsyf/yDWtcl9fAEsTpRRWrmPdQDrHDyim5LKnsQutCzBP3Fof83hSCIA==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@lit-labs/testing/-/testing-0.2.5.tgz",
"integrity": "sha512-VVYPhnpYhTgmZ3pWGQV8ZN/c81/aUlxSya+G94pNhlAiKUqsAwJZAkQCEZLncF8WHWg9jhas3eswxe9G3oQr1Q==",
"dev": true,
"dependencies": {
"@lit-labs/ssr": "^3.1.8",
@@ -1792,9 +1806,9 @@
}
},
"node_modules/@lit/react": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.2.tgz",
"integrity": "sha512-UJ5TQ46DPcJDIzyjbwbj6Iye0XcpCxL2yb03zcWq1BpWchpXS3Z0BPVhg7zDfZLF6JemPml8u/gt/+KwJ/23sg==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.6.tgz",
"integrity": "sha512-QIss8MPh6qUoFJmuaF4dSHts3qCsA36S3HcOLiNPShxhgYPr4XJRnCBKPipk85sR9xr6TQrOcDMfexwbNdJHYA==",
"dev": true,
"peerDependencies": {
"@types/react": "17 || 18"
@@ -2215,9 +2229,9 @@
]
},
"node_modules/@shoelace-style/animations": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.1.0.tgz",
"integrity": "sha512-Be+cahtZyI2dPKRm8EZSx3YJQ+jLvEcn3xzRP7tM4tqBnvd/eW/64Xh0iOf0t2w5P8iJKfdBbpVNE9naCaOf2g==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.2.0.tgz",
"integrity": "sha512-avvo1xxkLbv2dgtabdewBbqcJfV0e0zCwFqkPMnHFGbJbBHorRFfMAHh1NG9ymmXn0jW95ibUVH03E1NYXD6Gw==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/claviska"
@@ -2600,9 +2614,9 @@
"dev": true
},
"node_modules/@types/mocha": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz",
"integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==",
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
"integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
"dev": true
},
"node_modules/@types/node": {
@@ -2639,13 +2653,12 @@
"dev": true
},
"node_modules/@types/react": {
"version": "18.2.48",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz",
"integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==",
"version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
@@ -2655,12 +2668,6 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
@@ -8337,9 +8344,9 @@
}
},
"node_modules/get-east-asian-width": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
"integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"dev": true,
"engines": {
"node": ">=18"
@@ -8368,9 +8375,9 @@
}
},
"node_modules/get-port": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz",
"integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz",
"integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==",
"dev": true,
"engines": {
"node": ">=16"
@@ -10586,9 +10593,9 @@
}
},
"node_modules/lit": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz",
"integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.1.0",
@@ -11069,9 +11076,9 @@
}
},
"node_modules/markdown-it-attrs": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.1.6.tgz",
"integrity": "sha512-O7PDKZlN8RFMyDX13JnctQompwrrILuz2y43pW2GagcwpIIElkAdfeek+erHfxUOlXWPsjFeWmZ8ch1xtRLWpA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-4.2.0.tgz",
"integrity": "sha512-m7svtUBythvcGFFZAv9VjMEvs8UbHri2sojJ3juJumoOzv8sdkx9a7W3KxiHbXxAbvL3Xauak8TMwCnvigVPKw==",
"dev": true,
"engines": {
"node": ">=6"
@@ -11551,6 +11558,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@@ -11888,6 +11907,20 @@
"node": ">=4"
}
},
"node_modules/npm-check-updates": {
"version": "17.1.11",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.11.tgz",
"integrity": "sha512-TR2RuGIH7P3Qrb0jfdC/nT7JWqXPKjDlxuNQt3kx4oNVf1Pn5SBRB7KLypgYZhruivJthgTtfkkyK4mz342VjA==",
"dev": true,
"bin": {
"ncu": "build/cli.js",
"npm-check-updates": "build/cli.js"
},
"engines": {
"node": "^18.18.0 || >=20.0.0",
"npm": ">=8.12.1"
}
},
"node_modules/npm-run-path": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
@@ -12237,19 +12270,19 @@
}
},
"node_modules/ora": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-8.0.1.tgz",
"integrity": "sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==",
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-8.1.1.tgz",
"integrity": "sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==",
"dev": true,
"dependencies": {
"chalk": "^5.3.0",
"cli-cursor": "^4.0.0",
"cli-cursor": "^5.0.0",
"cli-spinners": "^2.9.2",
"is-interactive": "^2.0.0",
"is-unicode-supported": "^2.0.0",
"log-symbols": "^6.0.0",
"stdin-discarder": "^0.2.1",
"string-width": "^7.0.0",
"stdin-discarder": "^0.2.2",
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0"
},
"engines": {
@@ -12260,15 +12293,15 @@
}
},
"node_modules/ora/node_modules/cli-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"dependencies": {
"restore-cursor": "^4.0.0"
"restore-cursor": "^5.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -12286,46 +12319,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/ora/node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"dependencies": {
"mimic-fn": "^2.1.0"
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=6"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -12770,12 +12806,12 @@
}
},
"node_modules/playwright": {
"version": "1.46.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz",
"integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==",
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz",
"integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==",
"dev": true,
"dependencies": {
"playwright-core": "1.46.1"
"playwright-core": "1.49.0"
},
"bin": {
"playwright": "cli.js"
@@ -12788,9 +12824,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.46.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz",
"integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==",
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz",
"integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -12967,9 +13003,9 @@
}
},
"node_modules/prettier": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -13264,9 +13300,9 @@
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
@@ -14590,9 +14626,9 @@
}
},
"node_modules/string-width": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz",
"integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"dependencies": {
"emoji-regex": "^10.3.0",
@@ -14977,9 +15013,9 @@
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true
},
"node_modules/tsscmp": {
@@ -15100,9 +15136,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",

View File

@@ -56,29 +56,30 @@
"prettier:fix": "prettier --write --log-level=warn .",
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress",
"verify": "npm run prettier && npm run lint && npm run build && npm run test",
"prepublishOnly": "npm run verify"
"prepublishOnly": "npm run verify",
"check-updates": "npx npm-check-updates -i"
},
"engines": {
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^4.0.2",
"@floating-ui/dom": "^1.5.3",
"@shoelace-style/animations": "^1.1.0",
"@ctrl/tinycolor": "^4.1.0",
"@floating-ui/dom": "^1.6.12",
"@shoelace-style/animations": "^1.2.0",
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.4",
"lit": "^3.0.0",
"lit": "^3.2.1",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0-alpha.5",
"@custom-elements-manifest/analyzer": "^0.9.4",
"@custom-elements-manifest/analyzer": "^0.10.3",
"@lit-labs/eleventy-plugin-lit": "^1.0.3",
"@lit-labs/testing": "^0.2.4",
"@lit/react": "^1.0.0",
"@lit-labs/testing": "^0.2.5",
"@lit/react": "^1.0.6",
"@open-wc/testing": "^3.2.0",
"@types/mocha": "^10.0.2",
"@types/react": "^18.2.28",
"@types/mocha": "^10.0.10",
"@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@web/dev-server-esbuild": "^0.3.6",
@@ -90,7 +91,7 @@
"change-case": "^4.1.2",
"chokidar": "^3.5.3",
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.0",
"comment-parser": "^1.4.1",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.4.0",
"custom-element-vs-code-integration": "^1.2.1",
@@ -109,36 +110,37 @@
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
"eslint-plugin-wc": "^2.0.4",
"front-matter": "^4.0.2",
"get-port": "^7.0.0",
"get-port": "^7.1.0",
"globby": "^13.2.2",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"lunr": "^2.3.9",
"markdown-it": "^14.1.0",
"markdown-it-attrs": "^4.1.6",
"markdown-it-attrs": "^4.2.0",
"markdown-it-container": "^3.0.0",
"markdown-it-ins": "^3.0.1",
"markdown-it-kbd": "^2.2.2",
"markdown-it-mark": "^3.0.1",
"marked": "^11.1.0",
"node-html-parser": "^6.1.13",
"ora": "^8.0.1",
"npm-check-updates": "^17.1.11",
"ora": "^8.1.1",
"pascal-case": "^3.1.2",
"playwright": "^1.46.1",
"plop": "^4.0.0",
"prettier": "^3.0.3",
"playwright": "^1.49.0",
"plop": "^4.0.1",
"prettier": "^3.3.3",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react": "^18.3.1",
"recursive-copy": "^2.0.14",
"sinon": "^16.1.0",
"source-map": "^0.7.4",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"user-agent-data-types": "^0.3.1",
"uuid": "^9.0.1"
},
"overrides": {
"playwright": "^1.46.1"
"playwright": "^1.49.0"
},
"lint-staged": {
"*.{ts,js}": [

View File

@@ -1,7 +1,6 @@
import { customElement, property } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.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';
@@ -33,11 +32,6 @@ export default class {{ properCase tag }} extends WebAwesomeElement {
/** An example attribute. */
@property() attr = 'example';
@watch('example')
handleExampleChange() {
// do something
}
render() {
return html` <slot></slot> `;
}

View File

@@ -3,11 +3,10 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { WaErrorEvent } from '../../events/error.js';
import { WaLoadEvent } from '../../events/load.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './animated-image.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.
@@ -46,6 +45,21 @@ export default class WaAnimatedImage extends WebAwesomeElement {
/** Plays the animation. When this attribute is remove, the animation will pause. */
@property({ type: Boolean, reflect: true }) play: boolean;
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('play') && this.hasUpdated) {
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
// won't trigger another request.
if (this.play) {
this.animatedImage.src = '';
this.animatedImage.src = this.src;
}
}
if (changedProperties.has('src')) {
this.isLoaded = false;
}
}
private handleClick() {
this.play = !this.play;
}
@@ -68,21 +82,6 @@ export default class WaAnimatedImage extends WebAwesomeElement {
this.dispatchEvent(new WaErrorEvent());
}
@watch('play', { waitUntilFirstUpdate: true })
handlePlayChange() {
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
// won't trigger another request.
if (this.play) {
this.animatedImage.src = '';
this.animatedImage.src = this.src;
}
}
@watch('src')
handleSrcChange() {
this.isLoaded = false;
}
render() {
return html`
<div class="animated-image">

View File

@@ -4,11 +4,10 @@ import { html } from 'lit';
import { WaCancelEvent } from '../../events/cancel.js';
import { WaFinishEvent } from '../../events/finish.js';
import { WaStartEvent } from '../../events/start.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './animation.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. Powered by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
@@ -102,6 +101,51 @@ export default class WaAnimation extends WebAwesomeElement {
this.destroyAnimation();
}
updated(changedProperties: PropertyValues<this>) {
// Handle animation changes
if (
changedProperties.has('name') ||
changedProperties.has('delay') ||
changedProperties.has('direction') ||
changedProperties.has('duration') ||
changedProperties.has('easing') ||
changedProperties.has('endDelay') ||
changedProperties.has('fill') ||
changedProperties.has('iterations') ||
changedProperties.has('iterationStart') ||
changedProperties.has('keyframes')
) {
if (!this.hasUpdated) {
return;
}
this.createAnimation();
}
// Handle play change
if (changedProperties.has('play')) {
if (this.animation) {
if (this.play && !this.hasStarted) {
this.hasStarted = true;
this.dispatchEvent(new WaStartEvent());
}
if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
}
}
// Handle playback rate change
if (changedProperties.has('playbackRate')) {
if (this.animation) {
this.animation.playbackRate = this.playbackRate;
}
}
}
private handleAnimationFinish = () => {
this.play = false;
this.hasStarted = false;
@@ -163,52 +207,6 @@ export default class WaAnimation extends WebAwesomeElement {
}
}
@watch([
'name',
'delay',
'direction',
'duration',
'easing',
'endDelay',
'fill',
'iterations',
'iterationsStart',
'keyframes'
])
handleAnimationChange() {
if (!this.hasUpdated) {
return;
}
this.createAnimation();
}
@watch('play')
handlePlayChange() {
if (this.animation) {
if (this.play && !this.hasStarted) {
this.hasStarted = true;
this.dispatchEvent(new WaStartEvent());
}
if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
return true;
}
return false;
}
@watch('playbackRate')
handlePlaybackRateChange() {
if (this.animation) {
this.animation.playbackRate = this.playbackRate;
}
}
/** Clears all keyframe effects caused by this animation and aborts its playback. */
cancel() {
this.animation?.cancel();

View File

@@ -3,11 +3,10 @@ import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, state } from 'lit/decorators.js';
import { html } from 'lit';
import { WaErrorEvent } from '../../events/error.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './avatar.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Avatars are used to represent a person or object.
@@ -35,7 +34,7 @@ import type { CSSResultGroup } from 'lit';
export default class WaAvatar extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
@state() private hasError = false;
@state() hasError = false;
/** The image source to use for the avatar. */
@property() image = '';
@@ -52,10 +51,10 @@ export default class WaAvatar extends WebAwesomeElement {
/** The shape of the avatar. */
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
@watch('image')
handleImageChange() {
// Reset the error when a new image is provided
this.hasError = false;
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('image')) {
this.hasError = false;
}
}
private handleImageLoadError() {

View File

@@ -1,11 +1,10 @@
import { customElement, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './breadcrumb-item.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Breadcrumb Items are used inside [breadcrumbs](/docs/components/breadcrumb) to represent different links.
@@ -31,7 +30,7 @@ export default class WaBreadcrumbItem extends WebAwesomeElement {
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@state() private renderType: 'button' | 'link' | 'dropdown' = 'button';
@state() renderType: 'button' | 'link' | 'dropdown' = 'button';
/**
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
@@ -63,9 +62,10 @@ export default class WaBreadcrumbItem extends WebAwesomeElement {
this.renderType = 'button';
}
@watch('href', { waitUntilFirstUpdate: true })
hrefChanged() {
this.setRenderType();
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('href') && this.hasUpdated) {
this.setRenderType();
}
}
handleSlotChange() {

View File

@@ -8,7 +8,7 @@ export default css`
.button-group {
display: flex;
position: relative;
flex-wrap: wrap;
flex-wrap: none;
isolation: isolate;
}

View File

@@ -3,7 +3,7 @@ import { html } from 'lit';
import componentStyles from '../../styles/component.styles.js';
import styles from './button-group.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Button groups can be used to group related buttons into sections.
@@ -33,8 +33,8 @@ export default class WaButtonGroup extends WebAwesomeElement {
/** The button group's orientation. */
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';
updated(changedProps: Map<string, unknown>) {
if (changedProps.has('orientation')) {
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('orientation')) {
this.setAttribute('aria-orientation', this.orientation);
this.updateClassNames();
}

View File

@@ -9,11 +9,10 @@ import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInvalidEvent } from '../../events/invalid.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './button.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Buttons represent actions that are available to the user.
@@ -66,7 +65,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
@state() private hasFocus = false;
@state() hasFocus = false;
@state() visuallyHiddenLabel = false;
@state() invalid = false;
@property() title = ''; // make reactive to pass through
@@ -211,9 +210,10 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
return this.href ? true : false;
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
this.updateValidity();
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('disabled') && this.hasUpdated) {
this.updateValidity();
}
}
// eslint-disable-next-line

View File

@@ -12,11 +12,10 @@ import { prefersReducedMotion } from '../../internal/animate.js';
import { range } from 'lit/directives/range.js';
import { waitForEvent } from '../../internal/event.js';
import { WaSlideChangeEvent } from '../../events/slide-change.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './carousel.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup, PropertyValueMap } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
import type WaCarouselItem from '../carousel-item/carousel-item.js';
/**
@@ -83,7 +82,7 @@ export default class WaCarousel extends WebAwesomeElement {
@property({ type: Number, attribute: 'slides-per-move' }) slidesPerMove = 1;
/** Specifies the orientation in which the carousel will lay out. */
@property() orientation: 'horizontal' | 'vertical' = 'horizontal';
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';
/** When set, it is possible to scroll through the slides by dragging them with the mouse. */
@property({ type: Boolean, reflect: true, attribute: 'mouse-dragging' }) mouseDragging = false;
@@ -123,7 +122,47 @@ export default class WaCarousel extends WebAwesomeElement {
});
}
protected willUpdate(changedProperties: PropertyValueMap<WaCarousel> | Map<PropertyKey, unknown>): void {
protected updated(changedProperties: PropertyValues<this>) {
// Reinitialize when looping or slides per page change
if (changedProperties.has('loop') || changedProperties.has('slidesPerPage')) {
if (this.hasUpdated) {
this.initializeSlides();
}
}
// Handle slide changes
if (changedProperties.has('activeSlide')) {
const slides = this.getSlides();
slides.forEach((slide, i) => {
slide.classList.toggle('--is-active', i === this.activeSlide);
});
// Do not emit an event on first render
if (this.hasUpdated) {
this.dispatchEvent(
new WaSlideChangeEvent({
index: this.activeSlide,
slide: slides[this.activeSlide]
})
);
}
}
// Handle slides per move changes
if (changedProperties.has('slidesPerMove')) {
this.updateSlidesSnap();
}
// Handle autoplay changes
if (changedProperties.has('autoplay')) {
this.autoplayController.stop();
if (this.autoplay) {
this.autoplayController.start(this.autoplayInterval);
}
}
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
// Ensure the slidesPerMove is never higher than the slidesPerPage
if (changedProperties.has('slidesPerMove') || changedProperties.has('slidesPerPage')) {
this.slidesPerMove = Math.min(this.slidesPerMove, this.slidesPerPage);
@@ -355,8 +394,6 @@ export default class WaCarousel extends WebAwesomeElement {
this.requestUpdate();
};
@watch('loop', { waitUntilFirstUpdate: true })
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
initializeSlides() {
// Removes all the cloned elements from the carousel
this.getSlides({ excludeClones: false }).forEach((slide, index) => {
@@ -402,26 +439,7 @@ export default class WaCarousel extends WebAwesomeElement {
});
}
@watch('activeSlide')
handelSlideChange() {
const slides = this.getSlides();
slides.forEach((slide, i) => {
slide.classList.toggle('--is-active', i === this.activeSlide);
});
// Do not emit an event on first render
if (this.hasUpdated) {
this.dispatchEvent(
new WaSlideChangeEvent({
index: this.activeSlide,
slide: slides[this.activeSlide]
})
);
}
}
@watch('slidesPerMove')
updateSlidesSnap() {
private updateSlidesSnap() {
const slides = this.getSlides();
const slidesPerMove = this.slidesPerMove;
@@ -435,14 +453,6 @@ export default class WaCarousel extends WebAwesomeElement {
});
}
@watch('autoplay')
handleAutoplayChange() {
this.autoplayController.stop();
if (this.autoplay) {
this.autoplayController.start(this.autoplayInterval);
}
}
/**
* Move the carousel backward by `slides-per-move` slides.
*

View File

@@ -10,7 +10,6 @@ import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
@@ -79,7 +78,7 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
@query('input[type="checkbox"]') input: HTMLInputElement;
@state() private hasFocus = false;
@state() hasFocus = false;
@property() title = ''; // make reactive to pass through
@@ -129,6 +128,25 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
/** The checkbox's help text. If you need to display HTML, use the `help-text` slot instead. */
@property({ attribute: 'help-text' }) helpText = '';
updated(changedProperties: PropertyValues<this>) {
// Handle checked/indeterminate changes
if (changedProperties.has('checked') || changedProperties.has('indeterminate')) {
if (this.hasUpdated) {
this.input.checked = this.checked; // force a sync update
this.input.indeterminate = this.indeterminate; // force a sync update
this.updateValidity();
}
}
// Handle defaultChecked changes
if (changedProperties.has('defaultChecked')) {
if (!this.hasInteracted && this.checked !== this.defaultChecked) {
this.checked = this.defaultChecked;
this.handleValueOrCheckedChange();
}
}
}
private handleClick() {
this.hasInteracted = true;
this.checked = !this.checked;
@@ -150,29 +168,12 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
this.dispatchEvent(new WaFocusEvent());
}
@watch('defaultChecked')
handleDefaultCheckedChange() {
if (!this.hasInteracted && this.checked !== this.defaultChecked) {
this.checked = this.defaultChecked;
this.handleValueOrCheckedChange();
}
}
handleValueOrCheckedChange() {
this.toggleCustomState('checked', this.checked);
// These @watch() commands seem to override the base element checks for changes, so we need to setValue for the form and and updateValidity()
this.setValue(this.checked ? this.value : null, this._value);
this.updateValidity();
}
@watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true })
handleStateChange() {
this.input.checked = this.checked; // force a sync update
this.input.indeterminate = this.indeterminate; // force a sync update
this.updateValidity();
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);

View File

@@ -20,7 +20,6 @@ import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { WaInvalidEvent } from '../../events/invalid.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
@@ -142,14 +141,14 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
@query('[part~="preview"]') previewButton: HTMLButtonElement;
@query('[part~="trigger"]') trigger: HTMLButtonElement;
@state() private hasFocus = false;
@state() private isDraggingGridHandle = false;
@state() private isEmpty = true;
@state() private inputValue = '';
@state() private hue = 0;
@state() private saturation = 100;
@state() private brightness = 100;
@state() private alpha = 100;
@state() hasFocus = false;
@state() isDraggingGridHandle = false;
@state() isEmpty = true;
@state() inputValue = '';
@state() hue = 0;
@state() saturation = 100;
@state() brightness = 100;
@state() alpha = 100;
private _value: string | null = null;
@@ -183,7 +182,7 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
@property({ attribute: 'with-label', reflect: true, type: Boolean }) withLabel = false;
@property({ attribute: 'with-help-text', reflect: true, type: Boolean }) withHelpText = false;
@state() private hasEyeDropper: boolean = false;
@state() hasEyeDropper: boolean = false;
/**
* The color picker's label. This will not be displayed, but it will be announced by assistive devices. If you need to
@@ -252,6 +251,23 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
}
}
updated(changedProperties: PropertyValues<this>) {
// Handle format changes
if (changedProperties.has('format') && this.hasUpdated) {
this.syncValues();
}
// Handle opacity changes
if (changedProperties.has('opacity')) {
this.alpha = 100;
}
// Handle value changes
if (changedProperties.has('value')) {
this.handleValueChange(changedProperties.get('value') as string, this.value + '');
}
}
private handleCopy() {
this.input.select();
document.execCommand('copy');
@@ -728,16 +744,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
event.stopImmediatePropagation();
}
@watch('format', { waitUntilFirstUpdate: true })
handleFormatChange() {
this.syncValues();
}
@watch('opacity')
handleOpacityChange() {
this.alpha = 100;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
@@ -747,7 +753,6 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
}
}
@watch('value')
handleValueChange(oldValue: string | undefined, newValue: string) {
this.isEmpty = !newValue;

View File

@@ -8,11 +8,10 @@ import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
import { waitForEvent } from '../../internal/event.js';
import { WaShowEvent } from '../../events/show.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './details.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Details show a brief summary and expand to show additional content.
@@ -91,6 +90,63 @@ export default class WaDetails extends WebAwesomeElement {
this.detailsObserver.observe(this.details, { attributes: true });
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('open') && this.hasUpdated) {
if (this.open) {
this.details.open = true;
// Show
const waShow = new WaShowEvent();
this.dispatchEvent(waShow);
if (waShow.defaultPrevented) {
this.open = false;
this.details.open = false;
return;
}
const duration = parseDuration(getComputedStyle(this.body).getPropertyValue('--show-duration'));
// We can't animate to 'auto', so use the scroll height for now
animate(
this.body,
[
{ height: '0', opacity: '0' },
{ height: `${this.body.scrollHeight}px`, opacity: '1' }
],
{
duration,
easing: 'linear'
}
).then(() => {
this.body.style.height = 'auto';
this.dispatchEvent(new WaAfterShowEvent());
});
} else {
// Hide
const waHide = new WaHideEvent();
this.dispatchEvent(waHide);
if (waHide.defaultPrevented) {
this.details.open = true;
this.open = true;
return;
}
const duration = parseDuration(getComputedStyle(this.body).getPropertyValue('--hide-duration'));
// We can't animate from 'auto', so use the scroll height for now
animate(
this.body,
[
{ height: `${this.body.scrollHeight}px`, opacity: '1' },
{ height: '0', opacity: '0' }
],
{ duration, easing: 'linear' }
).then(() => {
this.body.style.height = 'auto';
this.details.open = false;
this.dispatchEvent(new WaAfterHideEvent());
});
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.detailsObserver?.disconnect();
@@ -131,62 +187,6 @@ export default class WaDetails extends WebAwesomeElement {
}
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
this.details.open = true;
// Show
const waShow = new WaShowEvent();
this.dispatchEvent(waShow);
if (waShow.defaultPrevented) {
this.open = false;
this.details.open = false;
return;
}
const duration = parseDuration(getComputedStyle(this.body).getPropertyValue('--show-duration'));
// We can't animate to 'auto', so use the scroll height for now
await animate(
this.body,
[
{ height: '0', opacity: '0' },
{ height: `${this.body.scrollHeight}px`, opacity: '1' }
],
{
duration,
easing: 'linear'
}
);
this.body.style.height = 'auto';
this.dispatchEvent(new WaAfterShowEvent());
} else {
// Hide
const waHide = new WaHideEvent();
this.dispatchEvent(waHide);
if (waHide.defaultPrevented) {
this.details.open = true;
this.open = true;
return;
}
const duration = parseDuration(getComputedStyle(this.body).getPropertyValue('--hide-duration'));
// We can't animate from 'auto', so use the scroll height for now
await animate(
this.body,
[
{ height: `${this.body.scrollHeight}px`, opacity: '1' },
{ height: '0', opacity: '0' }
],
{ duration, easing: 'linear' }
);
this.body.style.height = 'auto';
this.details.open = false;
this.dispatchEvent(new WaAfterHideEvent());
}
}
/** Shows the details. */
async show() {
if (this.open || this.disabled) {

View File

@@ -9,11 +9,10 @@ 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 { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './dialog.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Dialogs, sometimes called "modals", appear above the page and require the user's immediate attention.
@@ -92,6 +91,18 @@ export default class WaDialog extends WebAwesomeElement {
}
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('open') && this.hasUpdated) {
// Open or close the dialog
if (this.open && !this.dialog.open) {
this.show();
} else if (this.dialog.open) {
this.open = true;
this.requestClose(this.dialog);
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
@@ -181,17 +192,6 @@ export default class WaDialog extends WebAwesomeElement {
}
};
@watch('open', { waitUntilFirstUpdate: true })
handleOpenChange() {
// Open or close the dialog
if (this.open && !this.dialog.open) {
this.show();
} else if (this.dialog.open) {
this.open = true;
this.requestClose(this.dialog);
}
}
/** Shows the dialog. */
private async show() {
// Show

View File

@@ -7,13 +7,13 @@ export default css`
--spacing: var(--wa-space-m);
}
:host(:not([vertical])) {
:host(:not([orientation='vertical'])) {
display: block;
border-top: solid var(--width) var(--color);
margin: var(--spacing) 0;
}
:host([vertical]) {
:host([orientation='vertical']) {
display: inline-block;
height: 100%;
border-left: solid var(--width) var(--color);

View File

@@ -1,9 +1,8 @@
import { customElement, property } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './divider.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Dividers are used to visually separate or group elements.
@@ -20,16 +19,17 @@ export default class WaDivider extends WebAwesomeElement {
static styles: CSSResultGroup = [componentStyles, styles];
/** Draws the divider in a vertical orientation. */
@property({ type: Boolean, reflect: true }) vertical = false;
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';
connectedCallback() {
super.connectedCallback();
this.setAttribute('role', 'separator');
}
@watch('vertical')
handleVerticalChange() {
this.setAttribute('aria-orientation', this.vertical ? 'vertical' : 'horizontal');
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('orientation')) {
this.setAttribute('aria-orientation', this.orientation);
}
}
}

View File

@@ -9,11 +9,10 @@ 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 { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './drawer.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Drawers slide in from a container to expose additional options and information.
@@ -92,6 +91,12 @@ export default class WaDrawer extends WebAwesomeElement {
/** When enabled, the drawer will be closed when the user clicks outside of it. */
@property({ attribute: 'light-dismiss', type: Boolean }) lightDismiss = false;
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
this.removeOpenListeners();
}
firstUpdated() {
if (isServer) {
return;
@@ -103,10 +108,16 @@ export default class WaDrawer extends WebAwesomeElement {
}
}
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
this.removeOpenListeners();
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('open') && this.hasUpdated) {
// Open or close the dialog
if (this.open && !this.drawer.open) {
this.show();
} else if (this.drawer.open) {
this.open = true;
this.requestClose(this.drawer);
}
}
}
private async requestClose(source: Element) {
@@ -192,17 +203,6 @@ export default class WaDrawer extends WebAwesomeElement {
}
};
@watch('open', { waitUntilFirstUpdate: true })
handleOpenChange() {
// Open or close the drawer
if (this.open && !this.drawer.open) {
this.show();
} else if (this.drawer.open) {
this.open = true;
this.requestClose(this.drawer);
}
}
/** Shows the drawer. */
private async show() {
// Show

View File

@@ -9,11 +9,10 @@ import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
import { waitForEvent } from '../../internal/event.js';
import { WaShowEvent } from '../../events/show.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './dropdown.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
import type { WaSelectEvent } from '../../events/select.js';
import type WaButton from '../button/button.js';
import type WaIconButton from '../icon-button/icon-button.js';
@@ -117,20 +116,72 @@ export default class WaDropdown extends WebAwesomeElement {
}
}
firstUpdated() {
firstUpdated (changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties)
this.panel.hidden = !this.open;
const initiallyOpen = this.open
this.open = false
// If the dropdown is visible on init, update its position
if (this.open) {
this.addOpenListeners();
this.popup.active = true;
// With SSR timings, sometimes the popup animations never get a chance to end / cancel.
// This is a hacky workaround to "fix" those animation issues.
setTimeout(() => {
this.popup.popup.dispatchEvent(new Event("animationend"))
// If the dropdown is visible on init, update its position
if (initiallyOpen) {
setTimeout(() => {
this.open = true
})
}
})
}
async updated(changeProperties: PropertyValues<this>) {
// Handle open changes
if (changeProperties.has('open') && this.hasUpdated) {
if (this.disabled) {
this.open = false;
return;
}
this.updateAccessibleTrigger();
if (this.open) {
// Show
const waShowEvent = new WaShowEvent();
this.dispatchEvent(waShowEvent);
if (waShowEvent.defaultPrevented) {
this.open = false;
return;
}
this.addOpenListeners();
this.panel.hidden = false;
this.popup.active = true;
await animateWithClass(this.popup.popup, 'show-with-scale');
this.dispatchEvent(new WaAfterShowEvent());
} else {
// Hide
const waHideEvent = new WaHideEvent();
this.dispatchEvent(waHideEvent);
if (waHideEvent.defaultPrevented) {
this.open = true;
return;
}
this.removeOpenListeners();
await animateWithClass(this.popup.popup, 'hide-with-scale');
this.panel.hidden = true;
this.popup.active = false;
this.dispatchEvent(new WaAfterHideEvent());
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeOpenListeners();
this.hide();
this.open = false;
}
focusOnTrigger() {
@@ -151,7 +202,7 @@ export default class WaDropdown extends WebAwesomeElement {
// in case any ancestors are also listening for this key.
if (this.open && event.key === 'Escape' && !this.closeWatcher) {
event.stopPropagation();
this.hide();
this.open = false;
this.focusOnTrigger();
}
};
@@ -161,7 +212,7 @@ export default class WaDropdown extends WebAwesomeElement {
if (event.key === 'Escape' && this.open) {
event.stopPropagation();
this.focusOnTrigger();
this.hide();
this.open = false;
return;
}
@@ -170,7 +221,7 @@ export default class WaDropdown extends WebAwesomeElement {
// Tabbing within an open menu should close the dropdown and refocus the trigger
if (this.open && document.activeElement?.tagName.toLowerCase() === 'wa-menu-item') {
event.preventDefault();
this.hide();
this.open = false;
this.focusOnTrigger();
return;
}
@@ -189,7 +240,7 @@ export default class WaDropdown extends WebAwesomeElement {
!this.containingElement ||
activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
) {
this.hide();
this.open = false;
}
});
}
@@ -199,7 +250,7 @@ export default class WaDropdown extends WebAwesomeElement {
// Close when clicking outside of the containing element
const path = event.composedPath();
if (this.containingElement && !path.includes(this.containingElement)) {
this.hide();
this.open = false;
}
};
@@ -208,16 +259,16 @@ export default class WaDropdown extends WebAwesomeElement {
// Hide the dropdown when a menu item is selected
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'wa-menu') {
this.hide();
this.open = false;
this.focusOnTrigger();
}
};
handleTriggerClick() {
if (this.open) {
this.hide();
this.open = false;
} else {
this.show();
this.open = true;
this.focusOnTrigger();
}
}
@@ -246,7 +297,7 @@ export default class WaDropdown extends WebAwesomeElement {
// Show the menu if it's not already open
if (!this.open) {
this.show();
this.open = true;
// Wait for the dropdown to open before focusing, but not the animation
await this.updateComplete;
@@ -349,7 +400,7 @@ export default class WaDropdown extends WebAwesomeElement {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.hide();
this.open = false;
this.focusOnTrigger();
};
} else {
@@ -369,46 +420,6 @@ export default class WaDropdown extends WebAwesomeElement {
this.closeWatcher?.destroy();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.disabled) {
this.open = false;
return;
}
this.updateAccessibleTrigger();
if (this.open) {
// Show
const waShowEvent = new WaShowEvent();
this.dispatchEvent(waShowEvent);
if (waShowEvent.defaultPrevented) {
this.open = false;
return;
}
this.addOpenListeners();
this.panel.hidden = false;
this.popup.active = true;
await animateWithClass(this.popup.popup, 'show-with-scale');
this.dispatchEvent(new WaAfterShowEvent());
} else {
// Hide
const waHideEvent = new WaHideEvent();
this.dispatchEvent(waHideEvent);
if (waHideEvent.defaultPrevented) {
this.open = true;
return;
}
this.removeOpenListeners();
await animateWithClass(this.popup.popup, 'hide-with-scale');
this.panel.hidden = true;
this.popup.active = false;
this.dispatchEvent(new WaAfterHideEvent());
}
}
render() {
return html`
<wa-popup

View File

@@ -31,7 +31,7 @@ export default class WaIconButton extends WebAwesomeFormAssociatedElement {
@query('.icon-button') button: HTMLButtonElement | HTMLLinkElement;
@state() private hasFocus = false;
@state() hasFocus = false;
/** The name of the icon to draw. Available names depend on the icon library being used. */
@property({ reflect: true }) name: string | null = null;

View File

@@ -4,7 +4,6 @@ import { html } from 'lit';
import { isTemplateResult } from 'lit/directive-helpers.js';
import { WaErrorEvent } from '../../events/error.js';
import { WaLoadEvent } from '../../events/load.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './icon.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
@@ -46,7 +45,7 @@ export default class WaIcon extends WebAwesomeElement {
private initialRender = false;
@state() private svg: SVGElement | HTMLTemplateResult | null = null;
@state() svg: SVGElement | HTMLTemplateResult | null = null;
/** The name of the icon to draw. Available names depend on the icon library being used. */
@property({ reflect: true }) name?: string;
@@ -93,6 +92,43 @@ export default class WaIcon extends WebAwesomeElement {
this.setIcon();
}
updated(changedProperties: PropertyValues<this>) {
// Sometimes (like with SSR -> hydration) mutators don't get applied due to race conditions. This ensures mutators
// get re-applied.
const library = getIconLibrary(this.library);
const svg = this.shadowRoot?.querySelector('svg');
if (svg) {
library?.mutator?.(svg);
}
// Handle label changes
if (changedProperties.has('label')) {
const hasLabel = typeof this.label === 'string' && this.label.length > 0;
if (hasLabel) {
this.setAttribute('role', 'img');
this.setAttribute('aria-label', this.label);
this.removeAttribute('aria-hidden');
} else {
this.removeAttribute('role');
this.removeAttribute('aria-label');
this.setAttribute('aria-hidden', 'true');
}
}
// Handle icon changes
if (
changedProperties.has('family') ||
changedProperties.has('name') ||
changedProperties.has('library') ||
changedProperties.has('variant') ||
changedProperties.has('src')
) {
this.setIcon();
}
}
disconnectedCallback() {
super.disconnectedCallback();
unwatchIcon(this);
@@ -162,22 +198,6 @@ export default class WaIcon extends WebAwesomeElement {
}
}
@watch('label')
handleLabelChange() {
const hasLabel = typeof this.label === 'string' && this.label.length > 0;
if (hasLabel) {
this.setAttribute('role', 'img');
this.setAttribute('aria-label', this.label);
this.removeAttribute('aria-hidden');
} else {
this.removeAttribute('role');
this.removeAttribute('aria-label');
this.setAttribute('aria-hidden', 'true');
}
}
@watch(['family', 'name', 'library', 'variant', 'src'])
async setIcon() {
const { url, fromLibrary } = this.getIconSource();
const library = fromLibrary ? getIconLibrary(this.library) : undefined;
@@ -227,24 +247,15 @@ export default class WaIcon extends WebAwesomeElement {
}
}
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
// Sometimes (like with SSR -> hydration) mutators dont get applied due to race conditions. This ensures mutators get re-applied.
const library = getIconLibrary(this.library);
const svg = this.shadowRoot?.querySelector('svg');
if (svg) {
library?.mutator?.(svg);
}
}
render() {
if (this.hasUpdated) {
return this.svg;
}
// @TODO: 16x16 is generally a safe bet. Perhaps be user setable?? `size="16x16"`, size="20x16". We just want to avoid "blowouts" with SSR.
//
// TODO: 16x16 is generally a safe bet. Perhaps this should be user settable, e.g. `size="16x16"`. We just want to
// avoid SSR "blowouts."
//
return html`<svg part="svg" fill="currentColor" width="16" height="16"></svg>`;
}
}

View File

@@ -6,11 +6,10 @@ import { drag } from '../../internal/drag.js';
import { html } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { WaChangeEvent } from '../../events/change.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './image-comparer.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Compare visual differences between similar photos with a sliding panel.
@@ -47,6 +46,13 @@ export default class WaImageComparer extends WebAwesomeElement {
/** The position of the divider as a percentage. */
@property({ type: Number, reflect: true }) position = 50;
updated(changedProperties: PropertyValues<this>) {
// Handle position changes
if (changedProperties.has('position') && this.hasUpdated) {
this.dispatchEvent(new WaChangeEvent());
}
}
private handleDrag(event: PointerEvent) {
const { width } = this.base.getBoundingClientRect();
const isRtl = this.matches(':dir(rtl)');
@@ -90,11 +96,6 @@ export default class WaImageComparer extends WebAwesomeElement {
}
}
@watch('position', { waitUntilFirstUpdate: true })
handlePositionChange() {
this.dispatchEvent(new WaChangeEvent());
}
render() {
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';

View File

@@ -3,11 +3,10 @@ import { html } from 'lit';
import { requestInclude } from './request.js';
import { WaIncludeErrorEvent } from '../../events/include-error.js';
import { WaLoadEvent } from '../../events/load.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './include.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Includes give you the power to embed external HTML files into the page.
@@ -37,6 +36,36 @@ export default class WaInclude extends WebAwesomeElement {
*/
@property({ attribute: 'allow-scripts', type: Boolean }) allowScripts = false;
async updated(changedProperties: PropertyValues<this>) {
// Handle src changes
if (changedProperties.has('src')) {
try {
const src = this.src;
const file = await requestInclude(src, this.mode);
// If the src changed since the request started do nothing, otherwise we risk overwriting a subsequent response
if (src !== this.src) {
return;
}
if (!file.ok) {
this.dispatchEvent(new WaIncludeErrorEvent({ status: file.status }));
return;
}
this.innerHTML = file.html;
if (this.allowScripts) {
[...this.querySelectorAll('script')].forEach(script => this.executeScript(script));
}
this.dispatchEvent(new WaLoadEvent());
} catch {
this.dispatchEvent(new WaIncludeErrorEvent({ status: -1 }));
}
}
}
private executeScript(script: HTMLScriptElement) {
// Create a copy of the script and swap it out so the browser executes it
const newScript = document.createElement('script');
@@ -45,34 +74,6 @@ export default class WaInclude extends WebAwesomeElement {
script.parentNode!.replaceChild(newScript, script);
}
@watch('src')
async handleSrcChange() {
try {
const src = this.src;
const file = await requestInclude(src, this.mode);
// If the src changed since the request started do nothing, otherwise we risk overwriting a subsequent response
if (src !== this.src) {
return;
}
if (!file.ok) {
this.dispatchEvent(new WaIncludeErrorEvent({ status: file.status }));
return;
}
this.innerHTML = file.html;
if (this.allowScripts) {
[...this.querySelectorAll('script')].forEach(script => this.executeScript(script));
}
this.dispatchEvent(new WaLoadEvent());
} catch {
this.dispatchEvent(new WaIncludeErrorEvent({ status: -1 }));
}
}
render() {
return html`<slot></slot>`;
}

View File

@@ -12,12 +12,11 @@ import { WaChangeEvent } from '../../events/change.js';
import { WaClearEvent } from '../../events/clear.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import styles from './input.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
import type WaButton from '../button/button.js';
/**
@@ -77,7 +76,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
@query('.input__control') input: HTMLInputElement;
@state() private hasFocus = false;
@state() hasFocus = false;
@property() title = ''; // make reactive to pass through
/**
@@ -229,6 +228,16 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
*/
@property({ attribute: 'with-help-text', type: Boolean }) withHelpText = false;
updated(changedProperties: PropertyValues<this>) {
// Handle step changes
if (changedProperties.has('step') && this.hasUpdated) {
// If step changes, the value may become invalid so we need to recheck after the update. We set the new step
// imperatively so we don't have to wait for the next render to report the updated validity.
this.input.step = String(this.step);
this.updateValidity();
}
}
private handleBlur() {
this.hasFocus = false;
this.dispatchEvent(new WaBlurEvent());
@@ -314,14 +323,6 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
this.passwordVisible = !this.passwordVisible;
}
@watch('step', { waitUntilFirstUpdate: true })
handleStepChange() {
// If step changes, the value may become invalid so we need to recheck after the update. We set the new step
// imperatively so we don't have to wait for the next render to report the updated validity.
this.input.step = String(this.step);
this.updateValidity();
}
/** Sets focus on the input. */
focus(options?: FocusOptions) {
this.input.focus(options);

View File

@@ -6,7 +6,6 @@ import { customElement, property, query } from 'lit/decorators.js';
import { getTextContent } from '../../internal/slot.js';
import { html } from 'lit';
import { SubmenuController } from './submenu-controller.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './menu-item.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
@@ -82,7 +81,7 @@ export default class WaMenuItem extends WebAwesomeElement {
this.removeEventListener('mouseover', this.handleMouseOver);
}
protected firstUpdated(changedProperties: PropertyValues<this>): void {
firstUpdated(changedProperties: PropertyValues<this>): void {
// Kick it so that it renders the "submenu" properly.
if (this.isSubmenu()) {
this.requestUpdate();
@@ -91,6 +90,40 @@ export default class WaMenuItem extends WebAwesomeElement {
super.firstUpdated(changedProperties);
}
updated(changedProperties: PropertyValues<this>) {
// Handle checked change
if (changedProperties.has('checked')) {
// For proper accessibility, users have to use type="checkbox" to use the checked attribute
if (this.checked && this.type !== 'checkbox') {
this.checked = false;
return;
}
// Only checkbox types can receive the aria-checked attribute
if (this.type === 'checkbox') {
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
} else {
this.removeAttribute('aria-checked');
}
}
// Handle disabled change
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
// Handle type change
if (changedProperties.has('type')) {
if (this.type === 'checkbox') {
this.setAttribute('role', 'menuitemcheckbox');
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
} else {
this.setAttribute('role', 'menuitem');
this.removeAttribute('aria-checked');
}
}
}
private handleDefaultSlotChange() {
const textLabel = this.getTextLabel();
@@ -121,38 +154,6 @@ export default class WaMenuItem extends WebAwesomeElement {
event.stopPropagation();
};
@watch('checked')
handleCheckedChange() {
// For proper accessibility, users have to use type="checkbox" to use the checked attribute
if (this.checked && this.type !== 'checkbox') {
this.checked = false;
return;
}
// Only checkbox types can receive the aria-checked attribute
if (this.type === 'checkbox') {
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
} else {
this.removeAttribute('aria-checked');
}
}
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('type')
handleTypeChange() {
if (this.type === 'checkbox') {
this.setAttribute('role', 'menuitemcheckbox');
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
} else {
this.setAttribute('role', 'menuitem');
this.removeAttribute('aria-checked');
}
}
/** Returns a text label based on the contents of the menu item's default slot. */
getTextLabel() {
return getTextContent(this.defaultSlot);

View File

@@ -1,11 +1,10 @@
import { customElement, property } from 'lit/decorators.js';
import { html } from 'lit';
import { WaMutationEvent } from '../../events/mutation.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './mutation-observer.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary The Mutation Observer component offers a thin, declarative interface to the [`MutationObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
@@ -56,6 +55,29 @@ export default class WaMutationObserver extends WebAwesomeElement {
}
}
updated(changedProperties: PropertyValues<this>) {
// Handle disabled changes
if (changedProperties.has('disabled')) {
if (this.disabled) {
this.stopObserver();
} else {
this.startObserver();
}
}
// Handle other changes
if (
changedProperties.has('attr') ||
changedProperties.has('attrOldValue') ||
changedProperties.has('charData') ||
changedProperties.has('charDataOldValue') ||
changedProperties.has('childList')
) {
this.stopObserver();
this.startObserver();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.stopObserver();
@@ -92,25 +114,6 @@ export default class WaMutationObserver extends WebAwesomeElement {
this.mutationObserver.disconnect();
}
@watch('disabled')
handleDisabledChange() {
if (this.disabled) {
this.stopObserver();
} else {
this.startObserver();
}
}
@watch('attr', { waitUntilFirstUpdate: true })
@watch('attr-old-value', { waitUntilFirstUpdate: true })
@watch('char-data', { waitUntilFirstUpdate: true })
@watch('char-data-old-value', { waitUntilFirstUpdate: true })
@watch('childList', { waitUntilFirstUpdate: true })
handleChange() {
this.stopObserver();
this.startObserver();
}
render() {
return html` <slot></slot> `;
}

View File

@@ -3,11 +3,10 @@ import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './option.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Options define the selectable items within various form controls such as [select](/docs/components/select).
@@ -61,6 +60,33 @@ export default class WaOption extends WebAwesomeElement {
this.setAttribute('aria-selected', 'false');
}
updated(changedProperties: PropertyValues<this>) {
// Handle disabled changes\
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
// Handle selected changes\
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}
// Handle value changes\
if (changedProperties.has('value')) {
// Ensure the value is a string. This ensures the next line doesn't error and allows framework users to pass numbers
// instead of requiring them to cast the value to a string.
if (typeof this.value !== 'string') {
this.value = String(this.value);
}
if (this.value.includes(' ')) {
// eslint-disable-next-line no-console
console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this);
this.value = this.value.replace(/ /g, '_');
}
}
}
private handleDefaultSlotChange() {
// When the label changes, tell the controller to update
customElements.whenDefined('wa-select').then(() => {
@@ -79,31 +105,6 @@ export default class WaOption extends WebAwesomeElement {
this.hasHover = false;
}
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('selected')
handleSelectedChange() {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}
@watch('value')
handleValueChange() {
// Ensure the value is a string. This ensures the next line doesn't error and allows framework users to pass numbers
// instead of requiring them to cast the value to a string.
if (typeof this.value !== 'string') {
this.value = String(this.value);
}
if (this.value.includes(' ')) {
// eslint-disable-next-line no-console
console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this);
this.value = this.value.replace(/ /g, '_');
}
}
/** Returns a plain text label based on the option's content. */
getTextLabel() {
const nodes = this.childNodes;

View File

@@ -7,7 +7,7 @@ import { WaRepositionEvent } from '../../events/reposition.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './popup.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
export interface VirtualElement {
getBoundingClientRect: () => DOMRect;
@@ -222,11 +222,11 @@ export default class WaPopup extends WebAwesomeElement {
this.stop();
}
async updated(changedProps: Map<string, unknown>) {
super.updated(changedProps);
async updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
// Start or stop the positioner when active changes
if (changedProps.has('active')) {
if (changedProperties.has('active')) {
if (this.active) {
this.start();
} else {
@@ -235,7 +235,7 @@ export default class WaPopup extends WebAwesomeElement {
}
// Update the anchor when anchor changes
if (changedProps.has('anchor')) {
if (changedProperties.has('anchor')) {
this.handleAnchorChange();
}

View File

@@ -4,7 +4,7 @@ import { LocalizeController } from '../../utilities/localize.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './progress-ring.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Progress rings are used to show the progress of a determinate operation in a circular fashion.
@@ -40,15 +40,15 @@ export default class WaProgressRing extends WebAwesomeElement {
/** A custom label for assistive devices. */
@property() label = '';
updated(changedProps: Map<string, unknown>) {
super.updated(changedProps);
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
//
// This block is only required for Safari because it doesn't transition the circle when the custom properties
// change, possibly because of a mix of pixel + unit-less values in the calc() function. It seems like a Safari bug,
// but I couldn't pinpoint it so this works around the problem.
//
if (changedProps.has('value')) {
if (changedProperties.has('value')) {
const radius = parseFloat(getComputedStyle(this.indicator).getPropertyValue('r'));
const circumference = 2 * Math.PI * radius;
const offset = circumference - (this.value / 100) * circumference;

View File

@@ -1,6 +1,5 @@
import { customElement, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './qr-code.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
@@ -44,11 +43,8 @@ export default class WaQrCode extends WebAwesomeElement {
/** The level of error correction to use. [Learn more](https://www.qrcode.com/en/about/error_correction.html) */
@property({ attribute: 'error-correction' }) errorCorrection: 'L' | 'M' | 'Q' | 'H' = 'H';
/**
* Whether or not the qr-code generated.
*/
// @ts-expect-error Don't know why it marks it as unused.
@state() private generated = false;
/** Whether or not the qr-code generated. */
@state() generated = false;
firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
@@ -58,8 +54,21 @@ export default class WaQrCode extends WebAwesomeElement {
}
}
@watch(['background', 'errorCorrection', 'fill', 'radius', 'size', 'value'])
generate() {
updated(changedProperties: PropertyValues<this>) {
// Regenerate the code when these properties change
if (
changedProperties.has('background') ||
changedProperties.has('errorCorrection') ||
changedProperties.has('fill') ||
changedProperties.has('radius') ||
changedProperties.has('size') ||
changedProperties.has('value')
) {
this.generate();
}
}
private generate() {
this.style.setProperty('--size', `${this.size}px`);
if (!this.hasUpdated) {

View File

@@ -5,11 +5,10 @@ import { html } from 'lit/static-html.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './radio-button.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Radios buttons allow the user to select a single option from a group using a button-like control.
@@ -107,6 +106,13 @@ export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
this.setAttribute('role', 'presentation');
}
updated(changedProperties: PropertyValues<this>) {
// Handle disabled changes
if (changedProperties.has('disabled') && this.hasUpdated) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
}
private handleBlur() {
this.hasFocus = false;
this.dispatchEvent(new WaBlurEvent());
@@ -127,11 +133,6 @@ export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
this.dispatchEvent(new WaFocusEvent());
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
/** Sets focus on the radio button. */
focus(options?: FocusOptions) {
this.input.focus(options);

View File

@@ -8,12 +8,11 @@ import { RequiredValidator } from '../../internal/validators/required-validator.
import { uniqueId } from '../../internal/math.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaInputEvent } from '../../events/input.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import styles from './radio-group.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
import type WaRadio from '../radio/radio.js';
import type WaRadioButton from '../radio-button/radio-button.js';
@@ -65,7 +64,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@state() private hasButtonGroup = false;
@state() hasButtonGroup = false;
/**
* The radio group's label. Required for proper accessibility. If you need to display HTML, use the `label` slot
@@ -134,6 +133,13 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}
}
updated(changedProperties: PropertyValues) {
// Handle value/size changes
if (changedProperties.has('value') || changedProperties.has('size')) {
this.syncRadioElements();
}
}
private handleRadioClick = (e: Event) => {
const clickedRadio = (e.target as HTMLElement).closest<WaRadio | WaRadioButton>('wa-radio, wa-radio-button');
@@ -230,16 +236,6 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
: this.querySelector<WaRadio | WaRadioButton>(':is(wa-radio, wa-radio-button):not([disabled])') || undefined;
}
@watch('value')
handleValueChange() {
this.syncRadioElements();
}
@watch('size', { waitUntilFirstUpdate: true })
handleSizeChange() {
this.syncRadioElements();
}
formResetCallback(...args: Parameters<WebAwesomeFormAssociatedElement['formResetCallback']>) {
this.value = this.defaultValue;

View File

@@ -4,11 +4,10 @@ import { customElement, property, state } from 'lit/decorators.js';
import { html, isServer } from 'lit';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './radio.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Radios allow the user to select a single option from a group.
@@ -78,6 +77,19 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
this.setInitialAttributes();
}
updated(changedProperties: PropertyValues) {
// Handle checked change
if (changedProperties.has('checked')) {
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
}
// Handle disabled change
if (changedProperties.has('disabled') && this.hasUpdated) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
}
private handleBlur = () => {
this.hasFocus = false;
this.dispatchEvent(new WaBlurEvent());
@@ -94,12 +106,6 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('checked')
handleCheckedChange() {
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
this.tabIndex = this.checked ? 0 : -1;
}
/**
* @override
*/
@@ -107,11 +113,6 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
// We override `setValue` because we don't want to set form values from here. We want to do that in "RadioGroup" itself.
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
private handleClick = () => {
if (!this.disabled) {
this.checked = true;

View File

@@ -10,12 +10,11 @@ import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import styles from './range.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Ranges allow the user to select a single value within a given range using a slider.
@@ -65,8 +64,8 @@ export default class WaRange extends WebAwesomeFormAssociatedElement {
@query('.range__control') input: HTMLInputElement;
@query('.range__tooltip') output: HTMLOutputElement | null;
@state() private hasFocus = false;
@state() private hasTooltip = false;
@state() hasFocus = false;
@state() hasTooltip = false;
@property() title = ''; // make reactive to pass through
/** The name of the range, submitted as a name/value pair with form data. */
@@ -157,6 +156,22 @@ export default class WaRange extends WebAwesomeFormAssociatedElement {
});
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('value') && this.hasUpdated) {
// The value may have constraints, so we set the native control's value and sync it back to ensure it adhere's to
// min, max, and step properly
this.input.value = this.value.toString();
this.value = parseFloat(this.input.value);
this.updateValidity();
this.syncRange();
}
if (changedProperties.has('hasTooltip') && this.hasUpdated) {
this.syncRange();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver?.unobserve(this.input);
@@ -218,19 +233,7 @@ export default class WaRange extends WebAwesomeFormAssociatedElement {
}
}
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
// The value may have constraints, so we set the native control's value and sync it back to ensure it adhere's to
// min, max, and step properly
this.input.value = this.value.toString();
this.value = parseFloat(this.input.value);
this.updateValidity();
this.syncRange();
}
@watch('hasTooltip', { waitUntilFirstUpdate: true })
syncRange() {
private syncRange() {
const percent = Math.max(0, (this.value - this.min) / (this.max - this.min));
this.syncProgress(percent);

View File

@@ -7,11 +7,10 @@ import { styleMap } from 'lit/directives/style-map.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaHoverEvent } from '../../events/hover.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './rating.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Ratings give users a way to quickly view and provide feedback.
@@ -39,8 +38,8 @@ export default class WaRating extends WebAwesomeElement {
@query('.rating') rating: HTMLElement;
@state() private hoverValue = 0;
@state() private isHovering = false;
@state() hoverValue = 0;
@state() isHovering = false;
/** A label that describes the rating to assistive devices. */
@property() label = '';
@@ -71,6 +70,28 @@ export default class WaRating extends WebAwesomeElement {
@property() getSymbol: (value: number) => string = () =>
'<wa-icon name="star" library="system" variant="solid"></wa-icon>';
updated(changedProperties: PropertyValues<this>) {
// Handle hoverValue changes
if (changedProperties.has('hoverValue')) {
this.dispatchEvent(
new WaHoverEvent({
phase: 'move',
value: this.hoverValue
})
);
}
// Handle isHovering changes
if (changedProperties.has('isHovering')) {
this.dispatchEvent(
new WaHoverEvent({
phase: this.isHovering ? 'start' : 'end',
value: this.hoverValue
})
);
}
}
private getValueFromMousePosition(event: MouseEvent) {
return this.getValueFromXCoordinate(event.clientX);
}
@@ -183,26 +204,6 @@ export default class WaRating extends WebAwesomeElement {
return Math.ceil(numberToRound * multiplier) / multiplier;
}
@watch('hoverValue')
handleHoverValueChange() {
this.dispatchEvent(
new WaHoverEvent({
phase: 'move',
value: this.hoverValue
})
);
}
@watch('isHovering')
handleIsHoveringChange() {
this.dispatchEvent(
new WaHoverEvent({
phase: this.isHovering ? 'start' : 'end',
value: this.hoverValue
})
);
}
/** Sets focus on the rating. */
focus(options?: FocusOptions) {
this.rating.focus(options);

View File

@@ -29,8 +29,8 @@ export default class WaRelativeTime extends WebAwesomeElement {
private readonly localize = new LocalizeController(this);
private updateTimeout: number | ReturnType<typeof setTimeout>;
@state() private isoTime = '';
@state() private relativeTime = '';
@state() isoTime = '';
@state() relativeTime = '';
/**
* The date from which to calculate time from. If not set, the current date and time will be used. When passing a

View File

@@ -1,11 +1,10 @@
import { customElement, property } from 'lit/decorators.js';
import { html } from 'lit';
import { WaResizeEvent } from '../../events/resize.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './resize-observer.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary The Resize Observer component offers a thin, declarative interface to the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
@@ -40,6 +39,17 @@ export default class WaResizeObserver extends WebAwesomeElement {
}
}
updated(changedProperties: PropertyValues<this>) {
// Handle disabled changes
if (changedProperties.has('disabled') && this.hasUpdated) {
if (this.disabled) {
this.stopObserver();
} else {
this.startObserver();
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.stopObserver();
@@ -73,15 +83,6 @@ export default class WaResizeObserver extends WebAwesomeElement {
this.resizeObserver.disconnect();
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
if (this.disabled) {
this.stopObserver();
} else {
this.startObserver();
}
}
render() {
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}

View File

@@ -20,12 +20,11 @@ import { WaHideEvent } from '../../events/hide.js';
import { WaInputEvent } from '../../events/input.js';
import { waitForEvent } from '../../internal/event.js';
import { WaShowEvent } from '../../events/show.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import styles from './select.styles.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
import type { WaRemoveEvent } from '../../events/remove.js';
import type WaOption from '../option/option.js';
import type WaPopup from '../popup/popup.js';
@@ -269,6 +268,29 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.open = false;
}
firstUpdated (changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties)
// With SSR timings, sometimes the popup animations never get a chance to end / cancel.
// This is a hacky workaround to "fix" those animation issues.
setTimeout(() => {
this.popup.popup.dispatchEvent(new Event("animationend"))
})
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('disabled') && this.hasUpdated) {
// Close the listbox when the control is disabled
if (this.disabled) {
this.open = false;
this.handleOpenChange();
}
}
if (changedProperties.has('open') && this.hasUpdated) {
this.handleOpenChange();
}
}
private addOpenListeners() {
//
// Listen on the root node instead of the document in case the elements are inside a shadow root
@@ -676,17 +698,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
});
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Close the listbox when the control is disabled
if (this.disabled) {
this.open = false;
this.handleOpenChange();
}
}
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
private handleValueChange() {
const allOptions = this.getAllOptions();
const value = Array.isArray(this.value) ? this.value : [this.value];
@@ -695,7 +707,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.updateValidity();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open && !this.disabled) {
// Reset the current option

View File

@@ -40,11 +40,11 @@ export default css`
}
/* Horizontal */
:host(:not([vertical], [disabled])) .divider {
:host(:not([orientation='vertical'], [disabled])) .divider {
cursor: col-resize;
}
:host(:not([vertical])) .divider::after {
:host(:not([orientation='vertical'])) .divider::after {
display: flex;
content: '';
position: absolute;
@@ -54,15 +54,15 @@ export default css`
}
/* Vertical */
:host([vertical]) {
:host([orientation='vertical']) {
flex-direction: column;
}
:host([vertical]:not([disabled])) .divider {
:host([orientation='vertical']:not([disabled])) .divider {
cursor: row-resize;
}
:host([vertical]) .divider::after {
:host([orientation='vertical']) .divider::after {
content: '';
position: absolute;
width: 100%;

View File

@@ -185,7 +185,7 @@ describe('<wa-split-panel>', () => {
describe('panel sizing vertical', () => {
it('has two evenly sized panels by default', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
html`<wa-split-panel orientation="vertical" style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
@@ -199,7 +199,7 @@ describe('<wa-split-panel>', () => {
it('changes the sizing of the panels based on the position attribute', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel position="25" vertical style="height: 400px;">
html`<wa-split-panel position="25" orientation="vertical" style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
@@ -213,7 +213,7 @@ describe('<wa-split-panel>', () => {
it('updates the position in pixels to the correct result', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel position="25" vertical style="height: 400px;">
html`<wa-split-panel position="25" orientation="vertical" style="height: 400px;">
<div slot="start" data-testid="start-panel">Start</div>
<div slot="end" data-testid="end-panel">End</div>
</wa-split-panel>`
@@ -228,7 +228,7 @@ describe('<wa-split-panel>', () => {
it('emits the wa-reposition event on position change ', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
html`<wa-split-panel orientation="vertical" style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
@@ -241,7 +241,7 @@ describe('<wa-split-panel>', () => {
it('can be resized using the mouse ', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
html`<wa-split-panel orientation="vertical" style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
@@ -259,7 +259,7 @@ describe('<wa-split-panel>', () => {
it('cannot be resized if disabled', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel disabled vertical style="height: 400px;">
html`<wa-split-panel disabled orientation="vertical" style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`
@@ -277,7 +277,7 @@ describe('<wa-split-panel>', () => {
it('snaps to predefined positions', async () => {
const splitPanel = await fixture<WaSplitPanel>(
html`<wa-split-panel vertical style="height: 400px;">
html`<wa-split-panel orientation="vertical" style="height: 400px;">
<div slot="start">Start</div>
<div slot="end">End</div>
</wa-split-panel>`

View File

@@ -5,11 +5,10 @@ import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
import { WaRepositionEvent } from '../../events/reposition.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './split-panel.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Split panels display two adjacent panels, allowing the user to reposition them.
@@ -58,7 +57,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
@property({ attribute: 'position-in-pixels', type: Number }) positionInPixels: number;
/** Draws the split panel in a vertical orientation with the start and end panels stacked. */
@property({ type: Boolean, reflect: true }) vertical = false;
@property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'horizontal';
/** Disables resizing. Note that the position may still change as a result of resizing the host element. */
@property({ type: Boolean, reflect: true }) disabled = false;
@@ -88,6 +87,27 @@ export default class WaSplitPanel extends WebAwesomeElement {
this.cachedPositionInPixels = this.percentageToPixels(this.position);
}
updated(changedProperties: PropertyValues<this>) {
// Handle position change
if (changedProperties.has('position')) {
this.cachedPositionInPixels = this.percentageToPixels(this.position);
this.positionInPixels = this.percentageToPixels(this.position);
this.isCollapsed = false;
this.positionBeforeCollapsing = 0;
this.dispatchEvent(new WaRepositionEvent());
}
// Handle positionInPixels change
if (changedProperties.has('positionInPixels')) {
this.position = this.pixelsToPercentage(this.positionInPixels);
}
// Handle orientation change
if (changedProperties.has('orientation')) {
this.detectSize();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver?.unobserve(this);
@@ -95,7 +115,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
private detectSize() {
const { width, height } = this.getBoundingClientRect();
this.size = this.vertical ? height : width;
this.size = this.orientation === 'vertical' ? height : width;
}
private percentageToPixels(value: number) {
@@ -120,7 +140,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
drag(this, {
onMove: (x, y) => {
let newPositionInPixels = this.vertical ? y : x;
let newPositionInPixels = this.orientation === 'vertical' ? y : x;
// Flip for end panels
if (this.primary === 'end') {
@@ -140,7 +160,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
snapPoint = parseFloat(value);
}
if (isRtl && !this.vertical) {
if (isRtl && this.orientation !== 'vertical') {
snapPoint = this.size - snapPoint;
}
@@ -170,11 +190,17 @@ export default class WaSplitPanel extends WebAwesomeElement {
event.preventDefault();
if ((event.key === 'ArrowLeft' && !this.vertical) || (event.key === 'ArrowUp' && this.vertical)) {
if (
(event.key === 'ArrowLeft' && this.orientation !== 'vertical') ||
(event.key === 'ArrowUp' && this.orientation === 'vertical')
) {
newPosition -= incr;
}
if ((event.key === 'ArrowRight' && !this.vertical) || (event.key === 'ArrowDown' && this.vertical)) {
if (
(event.key === 'ArrowRight' && this.orientation !== 'vertical') ||
(event.key === 'ArrowDown' && this.orientation === 'vertical')
) {
newPosition += incr;
}
@@ -210,7 +236,7 @@ export default class WaSplitPanel extends WebAwesomeElement {
private handleResize(entries: ResizeObserverEntry[]) {
const { width, height } = entries[0].contentRect;
this.size = this.vertical ? height : width;
this.size = this.orientation === 'vertical' ? height : width;
// There's some weird logic that gets `this.cachedPositionInPixels = NaN` or `this.position === Infinity` when
// a split-panel goes from `display: none;` to showing.
@@ -226,28 +252,9 @@ export default class WaSplitPanel extends WebAwesomeElement {
}
}
@watch('position')
handlePositionChange() {
this.cachedPositionInPixels = this.percentageToPixels(this.position);
this.positionInPixels = this.percentageToPixels(this.position);
this.isCollapsed = false;
this.positionBeforeCollapsing = 0;
this.dispatchEvent(new WaRepositionEvent());
}
@watch('positionInPixels')
handlePositionInPixelsChange() {
this.position = this.pixelsToPercentage(this.positionInPixels);
}
@watch('vertical')
handleVerticalChange() {
this.detectSize();
}
render() {
const gridTemplate = this.vertical ? 'gridTemplateRows' : 'gridTemplateColumns';
const gridTemplateAlt = this.vertical ? 'gridTemplateColumns' : 'gridTemplateRows';
const gridTemplate = this.orientation === 'vertical' ? 'gridTemplateRows' : 'gridTemplateColumns';
const gridTemplateAlt = this.orientation === 'vertical' ? 'gridTemplateColumns' : 'gridTemplateRows';
const isRtl = this.hasUpdated ? this.matches(':dir(rtl)') : this.dir === 'rtl';
const primary = `
clamp(
@@ -269,13 +276,13 @@ export default class WaSplitPanel extends WebAwesomeElement {
}
if (this.primary === 'end') {
if (isRtl && !this.vertical) {
if (isRtl && this.orientation !== 'vertical') {
this.style[gridTemplate] = `${primary} var(--divider-width) ${secondary}`;
} else {
this.style[gridTemplate] = `${secondary} var(--divider-width) ${primary}`;
}
} else {
if (isRtl && !this.vertical) {
if (isRtl && this.orientation !== 'vertical') {
this.style[gridTemplate] = `${secondary} var(--divider-width) ${primary}`;
} else {
this.style[gridTemplate] = `${primary} var(--divider-width) ${secondary}`;

View File

@@ -9,7 +9,6 @@ import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
@@ -63,7 +62,7 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
@query('input[type="checkbox"]') input: HTMLInputElement;
@state() private hasFocus = false;
@state() hasFocus = false;
@property() title = ''; // make reactive to pass through
/** The name of the switch, submitted as a name/value pair with form data. */
@@ -129,6 +128,13 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
this.handleValueOrCheckedChange();
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('disabled') && this.hasUpdated) {
// Disabled form controls are always valid
this.updateValidity();
}
}
private handleBlur() {
this.hasFocus = false;
this.dispatchEvent(new WaBlurEvent());
@@ -192,12 +198,6 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
}
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Disabled form controls are always valid
this.updateValidity();
}
/** Simulates a click on the switch. */
click() {
this.input.click();

View File

@@ -8,11 +8,10 @@ import { LocalizeController } from '../../utilities/localize.js';
import { scrollIntoView } from '../../internal/scroll.js';
import { WaTabHideEvent } from '../../events/tab-hide.js';
import { WaTabShowEvent } from '../../events/tab-show.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './tab-group.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
import type WaTab from '../tab/tab.js';
import type WaTabPanel from '../tab-panel/tab-panel.js';
@@ -63,7 +62,7 @@ export default class WaTabGroup extends WebAwesomeElement {
@query('.tab-group__body') body: HTMLSlotElement;
@query('.tab-group__nav') nav: HTMLElement;
@state() private hasScrollControls = false;
@state() hasScrollControls = false;
/** Sets the active tab. */
@property({ reflect: true }) active = '';
@@ -127,6 +126,19 @@ export default class WaTabGroup extends WebAwesomeElement {
});
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('active')) {
const tab = this.tabs.find(el => el.panel === this.active);
if (tab) {
this.setActiveTab(tab, { scrollBehavior: 'smooth' });
}
}
if (changedProperties.has('noScrollControls') && this.hasUpdated) {
this.updateScrollControls();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.mutationObserver?.disconnect();
@@ -336,17 +348,7 @@ export default class WaTabGroup extends WebAwesomeElement {
this.updateComplete.then(() => this.updateScrollControls());
}
@watch('active')
updateActiveTab() {
const tab = this.tabs.find(el => el.panel === this.active);
if (tab) {
this.setActiveTab(tab, { scrollBehavior: 'smooth' });
}
}
@watch('noScrollControls', { waitUntilFirstUpdate: true })
updateScrollControls() {
private updateScrollControls() {
if (this.noScrollControls) {
this.hasScrollControls = false;
} else {

View File

@@ -1,11 +1,10 @@
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './tab-panel.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
let id = 0;
@@ -40,9 +39,10 @@ export default class WaTabPanel extends WebAwesomeElement {
this.setAttribute('role', 'tabpanel');
}
@watch('active')
handleActiveChange() {
this.setAttribute('aria-hidden', this.active ? 'false' : 'true');
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('active')) {
this.setAttribute('aria-hidden', this.active ? 'false' : 'true');
}
}
render() {

View File

@@ -1,11 +1,10 @@
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './tab.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
let id = 0;
@@ -53,19 +52,21 @@ export default class WaTab extends WebAwesomeElement {
this.setAttribute('role', 'tab');
}
@watch('active')
handleActiveChange() {
this.setAttribute('aria-selected', this.active ? 'true' : 'false');
}
updated(changedProperties: PropertyValues<this>) {
// Handle active changes
if (changedProperties.has('active')) {
this.setAttribute('aria-selected', this.active ? 'true' : 'false');
}
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
// Handle disabled changes
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
if (this.disabled && !this.active) {
this.tabIndex = -1;
} else {
this.tabIndex = 0;
if (this.disabled && !this.active) {
this.tabIndex = -1;
} else {
this.tabIndex = 0;
}
}
}

View File

@@ -9,12 +9,11 @@ import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-element.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import styles from './textarea.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Textareas collect data from the user and allow multiple lines of text.
@@ -58,7 +57,7 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
@query('.textarea__control') input: HTMLTextAreaElement;
@state() private hasFocus = false;
@state() hasFocus = false;
@property() title = ''; // make reactive to pass through
/** The name of the textarea, submitted as a name/value pair with form data. */
@@ -193,6 +192,18 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
});
}
async updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('rows') && this.hasUpdated) {
this.setTextareaHeight();
}
if (changedProperties.has('value') && this.hasUpdated) {
await this.updateComplete;
this.checkValidity();
this.setTextareaHeight();
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.input) {
@@ -232,18 +243,6 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
}
}
@watch('rows', { waitUntilFirstUpdate: true })
handleRowsChange() {
this.setTextareaHeight();
}
@watch('value', { waitUntilFirstUpdate: true })
async handleValueChange() {
await this.updateComplete;
this.checkValidity();
this.setTextareaHeight();
}
/** Sets focus on the textarea. */
focus(options?: FocusOptions) {
this.input.focus(options);

View File

@@ -8,12 +8,11 @@ import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
import { waitForEvent } from '../../internal/event.js';
import { WaShowEvent } from '../../events/show.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './tooltip.styles.js';
import WaPopup from '../popup/popup.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary Tooltips display additional information based on a specific action.
@@ -142,13 +141,57 @@ export default class WaTooltip extends WebAwesomeElement {
}
}
firstUpdated() {
firstUpdated (changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties)
this.body.hidden = !this.open;
// With SSR timings, sometimes the popup animations never get a chance to end / cancel.
// This is a hacky workaround to "fix" those animation issues.
setTimeout(() => {
this.popup.popup.dispatchEvent(new Event("animationend"))
if (this.open) {
// This makes sure the "animationend" event has finished then it will show the tooltip in the "open" state.
setTimeout(() => {
this.body.hidden = false
this.popup.active = true;
this.popup.reposition();
})
}
})
// If the tooltip is visible on init, update its position
if (this.open) {
this.popup.active = true;
this.popup.reposition();
}
async updated(changedProperties: PropertyValues<this>) {
// Handle open changes
if (changedProperties.has('open')) {
this.handleOpenChange();
}
// Handle disabled changes
if (changedProperties.has('disabled')) {
if (this.disabled && this.open) {
this.hide();
}
}
// Handle for changes
if (changedProperties.has('for')) {
this.handleForChange();
}
// Handle other changes
if (
changedProperties.has('distance') ||
changedProperties.has('hoist') ||
changedProperties.has('placement') ||
changedProperties.has('hoist')
) {
if (this.hasUpdated) {
await this.updateComplete;
this.popup.reposition();
}
}
}
@@ -202,8 +245,7 @@ export default class WaTooltip extends WebAwesomeElement {
return triggers.includes(triggerType);
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
private async handleOpenChange() {
if (this.open) {
if (this.disabled) {
return;
@@ -231,7 +273,6 @@ export default class WaTooltip extends WebAwesomeElement {
this.popup.active = true;
await animateWithClass(this.popup.popup, 'show-with-scale');
this.popup.reposition();
this.dispatchEvent(new WaAfterShowEvent());
} else {
// Hide
@@ -245,16 +286,17 @@ export default class WaTooltip extends WebAwesomeElement {
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown);
await animateWithClass(this.popup.popup, 'hide-with-scale');
if (this.hasUpdated) {
await animateWithClass(this.popup.popup, 'hide-with-scale');
}
this.popup.active = false;
this.body.hidden = true;
this.dispatchEvent(new WaAfterHideEvent());
}
}
@watch('for')
handleForChange() {
private handleForChange() {
const rootNode = this.getRootNode() as Document | ShadowRoot | null;
if (!rootNode) {
@@ -270,16 +312,16 @@ export default class WaTooltip extends WebAwesomeElement {
const { signal } = this.eventController;
// "\\b" is a space boundary, used for making sure we dont add the tooltip to aria-labelledby twice.
// "\\b" is a space boundary, used for making sure we don't add the tooltip to aria-labelledby twice.
const labelRegex = new RegExp(`\\b${this.id}\\b`);
if (newAnchor) {
/**
* We use `aria-labelledby` because it seems to have the most consistent screenreader experience.
* We use `aria-labelledby` because it seems to have the most consistent screen reader experience.
* Particularly for our "special" focusable elements like `<wa-button>`, `<wa-input>` etc.
* aria-describedby usually in some screenreaders is required to be on the actually focusable element,
* whereas with `aria-labelledby` it'll still read on first focus. The APG does and WAI-ARIA does recommend aria-describedby
* so perhaps once we have cross-root aria, we can revisit this decision.
* aria-describedby usually in some screen readers is required to be on the actually focusable element,
* whereas with `aria-labelledby` it'll still read on first focus. The APG does and WAI-ARIA does recommend
* aria-describedby so perhaps once we have cross-root aria, we can revisit this decision.
*/
const currentLabel = newAnchor.getAttribute('aria-labelledby') || '';
if (!currentLabel.match(labelRegex)) {
@@ -306,21 +348,6 @@ export default class WaTooltip extends WebAwesomeElement {
this.anchor = newAnchor;
}
@watch(['distance', 'hoist', 'placement', 'skidding'])
async handleOptionsChange() {
if (this.hasUpdated) {
await this.updateComplete;
this.popup.reposition();
}
}
@watch('disabled')
handleDisabledChange() {
if (this.disabled && this.open) {
this.hide();
}
}
/** Shows the tooltip. */
async show() {
if (this.open) {

View File

@@ -13,12 +13,11 @@ import { WaCollapseEvent } from '../../events/collapse.js';
import { WaExpandEvent } from '../../events/expand.js';
import { WaLazyChangeEvent } from '../../events/lazy-change.js';
import { WaLazyLoadEvent } from '../../events/lazy-load.js';
import { watch } from '../../internal/watch.js';
import { when } from 'lit/directives/when.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './tree-item.styles.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup, PropertyValueMap } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
/**
* @summary A tree item serves as a hierarchical node that lives inside a [tree](/docs/components/tree).
@@ -122,6 +121,55 @@ export default class WaTreeItem extends WebAwesomeElement {
this.handleExpandedChange();
}
updated(changedProperties: PropertyValues<this>) {
// Handle loading changes
if (changedProperties.has('loading') && this.hasUpdated) {
this.setAttribute('aria-busy', this.loading ? 'true' : 'false');
if (!this.loading) {
this.animateExpand();
}
}
// Handle disabled changes
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
// Handle selected changes
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}
// Handle expanded changes
if (changedProperties.has('expanded') && this.hasUpdated) {
if (!this.isLeaf) {
this.setAttribute('aria-expanded', this.expanded ? 'true' : 'false');
} else {
this.removeAttribute('aria-expanded');
}
}
// Handle expanded changes
if (changedProperties.has('expanded') && this.hasUpdated) {
if (this.expanded) {
if (this.lazy) {
this.loading = true;
this.dispatchEvent(new WaLazyLoadEvent());
} else {
this.animateExpand();
}
} else {
this.animateCollapse();
}
}
// Handle lazy changes
if (changedProperties.has('lazy') && this.hasUpdated) {
this.dispatchEvent(new WaLazyChangeEvent());
}
}
private async animateCollapse() {
this.dispatchEvent(new WaCollapseEvent());
@@ -151,7 +199,7 @@ export default class WaTreeItem extends WebAwesomeElement {
this.isLeaf = !this.lazy && this.getChildrenItems().length === 0;
}
protected willUpdate(changedProperties: PropertyValueMap<WaTreeItem> | Map<PropertyKey, unknown>) {
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('selected') && !changedProperties.has('indeterminate')) {
this.indeterminate = false;
}
@@ -179,7 +227,6 @@ export default class WaTreeItem extends WebAwesomeElement {
this.dispatchEvent(new WaAfterExpandEvent());
}
@watch('loading', { waitUntilFirstUpdate: true })
handleLoadingChange() {
this.setAttribute('aria-busy', this.loading ? 'true' : 'false');
@@ -188,17 +235,14 @@ export default class WaTreeItem extends WebAwesomeElement {
}
}
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}
@watch('selected')
handleSelectedChange() {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
}
@watch('expanded', { waitUntilFirstUpdate: true })
handleExpandedChange() {
if (!this.isLeaf) {
this.setAttribute('aria-expanded', this.expanded ? 'true' : 'false');
@@ -207,7 +251,6 @@ export default class WaTreeItem extends WebAwesomeElement {
}
}
@watch('expanded', { waitUntilFirstUpdate: true })
handleExpandAnimation() {
if (this.expanded) {
if (this.lazy) {
@@ -221,7 +264,6 @@ export default class WaTreeItem extends WebAwesomeElement {
}
}
@watch('lazy', { waitUntilFirstUpdate: true })
handleLazyChange() {
this.dispatchEvent(new WaLazyChangeEvent());
}

View File

@@ -2,12 +2,11 @@ import { clamp } from '../../internal/math.js';
import { customElement, property, query } from 'lit/decorators.js';
import { html, isServer } from 'lit';
import { WaSelectionChangeEvent } from '../../events/selection-change.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import styles from './tree.styles.js';
import WaTreeItem from '../tree-item/tree-item.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';
function syncCheckboxes(changedTreeItem: WaTreeItem, initialSync = false) {
function syncParentItem(treeItem: WaTreeItem) {
@@ -121,6 +120,12 @@ export default class WaTree extends WebAwesomeElement {
this.mutationObserver?.disconnect();
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('selection')) {
this.handleSelectionChange();
}
}
// Generates a clone of the expand icon element to use for each tree item
private getExpandButtonIcon(status: 'expand' | 'collapse') {
const slot = status === 'expand' ? this.expandedIconSlot : this.collapsedIconSlot;
@@ -353,7 +358,6 @@ export default class WaTree extends WebAwesomeElement {
items.forEach(this.initTreeItem);
}
@watch('selection')
async handleSelectionChange() {
const isSelectionMultiple = this.selection === 'multiple';
const items = this.getAllTreeItems();

View File

@@ -11,7 +11,13 @@ export async function animate(el: Element, keyframes: Keyframe[], options?: Keyf
*/
export function animateWithClass(el: Element, className: string) {
return new Promise<void>(resolve => {
if (!el) {
resolve()
return
}
el.classList.remove(className);
const controller = new AbortController();
const { signal } = controller;

View File

@@ -1,59 +0,0 @@
import type { LitElement } from 'lit';
type UpdateHandler = (prev?: unknown, next?: unknown) => void;
type NonUndefined<A> = A extends undefined ? never : A;
type UpdateHandlerFunctionKeys<T extends object> = {
[K in keyof T]-?: NonUndefined<T[K]> extends UpdateHandler ? K : never;
}[keyof T];
interface WatchOptions {
/**
* If true, will only start watching after the initial update/render
*/
waitUntilFirstUpdate?: boolean;
}
/**
* Runs when observed properties change, e.g. @property or @state, but before the component updates. To wait for an
* update to complete after a change occurs, use `await this.updateComplete` in the handler. To start watching after the
* initial update/render, use `{ waitUntilFirstUpdate: true }` or `this.hasUpdated` in the handler.
*
* Usage:
*
* @watch('propName')
* handlePropChange(oldValue, newValue) {
* ...
* }
*/
export function watch(propertyName: string | string[], options?: WatchOptions) {
const resolvedOptions: Required<WatchOptions> = {
waitUntilFirstUpdate: false,
...options
};
return <ElemClass extends LitElement>(proto: ElemClass, decoratedFnName: UpdateHandlerFunctionKeys<ElemClass>) => {
// @ts-expect-error - update is a protected property
const { update } = proto;
const watchedProperties = Array.isArray(propertyName) ? propertyName : [propertyName];
// @ts-expect-error - update is a protected property
proto.update = function (this: ElemClass, changedProps: Map<keyof ElemClass, ElemClass[keyof ElemClass]>) {
watchedProperties.forEach(property => {
const key = property as keyof ElemClass;
if (changedProps.has(key)) {
const oldValue = changedProps.get(key);
const newValue = this[key];
if (oldValue !== newValue) {
if (!resolvedOptions.waitUntilFirstUpdate || this.hasUpdated) {
(this[decoratedFnName] as unknown as UpdateHandler)(oldValue, newValue);
}
}
}
});
update.call(this, changedProps);
};
};
}

View File

@@ -201,6 +201,7 @@ export class WebAwesomeFormAssociatedElement
this.addEventListener('invalid', this.emitInvalid);
}
}
states: CustomStateSet;
connectedCallback() {
super.connectedCallback();
@@ -468,7 +469,6 @@ export class WebAwesomeFormAssociatedElement
// Custom states
addCustomState(state: string) {
try {
// @ts-expect-error CustomStateSet doesn't exist in TS yet.
(this.internals.states as Set<string>).add(state);
} catch (_) {
// Without this, test suite errors.
@@ -479,7 +479,6 @@ export class WebAwesomeFormAssociatedElement
deleteCustomState(state: string) {
try {
// @ts-expect-error CustomStateSet doesn't exist in TS yet.
(this.internals.states as Set<string>).delete(state);
} catch (_) {
// Without this, test suite errors.
@@ -506,7 +505,6 @@ export class WebAwesomeFormAssociatedElement
let bool = false;
try {
// @ts-expect-error CustomStateSet doesn't exist in TS yet.
bool = (this.internals.states as Set<string>).has(state);
} catch (_) {
// Without this, test suite errors.