Compare commits
366 Commits
autoload
...
applied-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e09db9d40 | ||
|
|
07ca5a45ae | ||
|
|
88a8173178 | ||
|
|
5a8c6912dc | ||
|
|
a7c786987d | ||
|
|
1179e48955 | ||
|
|
07f0884462 | ||
|
|
ef3575358e | ||
|
|
5e2762cbc6 | ||
|
|
7e165fa8bd | ||
|
|
33706e0f27 | ||
|
|
e262ed14b0 | ||
|
|
545eb467fc | ||
|
|
2848ab68ef | ||
|
|
fc1aa42c26 | ||
|
|
8baa32d8c9 | ||
|
|
a519077112 | ||
|
|
5219188690 | ||
|
|
267b9eba20 | ||
|
|
9c343ef3fd | ||
|
|
e82b076981 | ||
|
|
97bd88f904 | ||
|
|
21d8cdbb5c | ||
|
|
a5f8c51904 | ||
|
|
94ad43e130 | ||
|
|
1f04cd2a50 | ||
|
|
5a55c240ee | ||
|
|
e02b36873e | ||
|
|
f89ef95d65 | ||
|
|
a65db66005 | ||
|
|
a6e19d0710 | ||
|
|
df02aeef89 | ||
|
|
2d03f60c70 | ||
|
|
e45b44ad03 | ||
|
|
015429e05d | ||
|
|
5628381449 | ||
|
|
0bf3cf2535 | ||
|
|
d4aa9ff99e | ||
|
|
0229c315bb | ||
|
|
67d4458e69 | ||
|
|
67bfbed308 | ||
|
|
2c053b6fd3 | ||
|
|
6156e38a34 | ||
|
|
631df0293c | ||
|
|
b86a6a54ab | ||
|
|
201b32f3fb | ||
|
|
ebed8daee6 | ||
|
|
55be0a557f | ||
|
|
b4c45b480b | ||
|
|
8b9df9871a | ||
|
|
af7682aaca | ||
|
|
883cb161ec | ||
|
|
a2fbe121c3 | ||
|
|
ab770c566e | ||
|
|
1867603225 | ||
|
|
cf195da424 | ||
|
|
0cb6aa5d12 | ||
|
|
7e4d4c3c98 | ||
|
|
b5ef3191b7 | ||
|
|
f30481e229 | ||
|
|
ae010c333b | ||
|
|
43d1f9ee7a | ||
|
|
ec17e8736d | ||
|
|
44b27e791e | ||
|
|
02385027db | ||
|
|
b311072d9b | ||
|
|
87ac077b0a | ||
|
|
87837df35c | ||
|
|
5d72bbd162 | ||
|
|
a4fc1c5b44 | ||
|
|
539eaded73 | ||
|
|
93b2e78092 | ||
|
|
402a00dcd3 | ||
|
|
b63368d5f6 | ||
|
|
74c6d3ee36 | ||
|
|
621aa4362b | ||
|
|
c8919ad11f | ||
|
|
fad76dd1a2 | ||
|
|
b2f6499b87 | ||
|
|
9520e850dd | ||
|
|
4ee5271a83 | ||
|
|
d8de7bcc51 | ||
|
|
7ee31be6d6 | ||
|
|
9cb5ba7ac1 | ||
|
|
c380368b61 | ||
|
|
e298f7e5f4 | ||
|
|
c743561c25 | ||
|
|
e73e32fb71 | ||
|
|
b09a48bec4 | ||
|
|
aeef986cf5 | ||
|
|
6f08f50639 | ||
|
|
8fc5f598d0 | ||
|
|
1383ea3fe8 | ||
|
|
f8c37e0d14 | ||
|
|
cf543ef335 | ||
|
|
a3450a7d83 | ||
|
|
e80b2c9fb9 | ||
|
|
8d617fb98c | ||
|
|
a6e225e47c | ||
|
|
e21943f4fb | ||
|
|
c36df5ecc1 | ||
|
|
458def7830 | ||
|
|
b5d800f07a | ||
|
|
6551a6330b | ||
|
|
cb5f670909 | ||
|
|
5b6c1632bd | ||
|
|
bf15f2fb8a | ||
|
|
31ef2f7929 | ||
|
|
8aab94f184 | ||
|
|
b7acb27c98 | ||
|
|
dcbbc55f28 | ||
|
|
81dfcc2eae | ||
|
|
16f3e256b0 | ||
|
|
75b2da9eab | ||
|
|
9736f053d9 | ||
|
|
d0b710c26d | ||
|
|
5b83d4d1b0 | ||
|
|
89f0f4a02c | ||
|
|
a067ccb9e0 | ||
|
|
1ccea42cca | ||
|
|
0f90dd0f54 | ||
|
|
262cbc9a22 | ||
|
|
3a61d20d93 | ||
|
|
95f4f87eb8 | ||
|
|
5b3cc0d492 | ||
|
|
0de39a8163 | ||
|
|
879fd7a224 | ||
|
|
50af138424 | ||
|
|
9d592f4e08 | ||
|
|
5016d27af7 | ||
|
|
7218a19357 | ||
|
|
33d2d4368f | ||
|
|
cca40ca710 | ||
|
|
c6281859fd | ||
|
|
956271880d | ||
|
|
201ff4efc5 | ||
|
|
f954233bda | ||
|
|
8267968b76 | ||
|
|
1e243e4257 | ||
|
|
0b6c3a46cf | ||
|
|
a2e58b7696 | ||
|
|
119d299657 | ||
|
|
e8634e4178 | ||
|
|
2e2a683d11 | ||
|
|
414197acc9 | ||
|
|
8fd01e1eda | ||
|
|
e1ca7d1f59 | ||
|
|
f84d6939bd | ||
|
|
82446e2114 | ||
|
|
a4f0ae9088 | ||
|
|
fe3906f766 | ||
|
|
c9e644f3fc | ||
|
|
8ffbd02db7 | ||
|
|
e88d57d17d | ||
|
|
5f4de6d9f5 | ||
|
|
2cce87deeb | ||
|
|
630b5b19a0 | ||
|
|
2ce1451a9f | ||
|
|
2d1badba96 | ||
|
|
1b5db078a7 | ||
|
|
91095bd63a | ||
|
|
d9703a64fd | ||
|
|
4c22e72390 | ||
|
|
d05b8fca20 | ||
|
|
afca2ad2e0 | ||
|
|
b2aa854d98 | ||
|
|
287fff7cf1 | ||
|
|
136ecae4a6 | ||
|
|
cac772d5e6 | ||
|
|
e1dedcb1b5 | ||
|
|
c4901eca68 | ||
|
|
a001c2d12b | ||
|
|
c4c622eabd | ||
|
|
1ae018bedd | ||
|
|
24929e27c1 | ||
|
|
33a8d92aec | ||
|
|
32d21fa560 | ||
|
|
347d8b7f79 | ||
|
|
8f9c15913b | ||
|
|
60d7f688eb | ||
|
|
15f914914c | ||
|
|
2914475821 | ||
|
|
8e831aa3e7 | ||
|
|
985d4585c4 | ||
|
|
854db13bd7 | ||
|
|
4ddf80459a | ||
|
|
89fc2ff643 | ||
|
|
d7145f1f84 | ||
|
|
441a957432 | ||
|
|
67cbb85682 | ||
|
|
0005d16a06 | ||
|
|
ca5ab03cd4 | ||
|
|
c9e30022df | ||
|
|
c167bdd80f | ||
|
|
b9f62bb1bc | ||
|
|
a01b2cf8a2 | ||
|
|
f4b2623c8f | ||
|
|
af8426579e | ||
|
|
6b9ba9becf | ||
|
|
c6cc7b6983 | ||
|
|
0e869ec18d | ||
|
|
1b347874ef | ||
|
|
ff5b1e8573 | ||
|
|
73ad76a2fa | ||
|
|
aadcb486a9 | ||
|
|
4c854d64a7 | ||
|
|
c2e02d34ad | ||
|
|
8c8977549c | ||
|
|
24ef154d42 | ||
|
|
b5a3045bae | ||
|
|
7404e496cb | ||
|
|
5ba2c7eeec | ||
|
|
514a7f3d51 | ||
|
|
15474b83b1 | ||
|
|
a5f1bc6c82 | ||
|
|
834d44e0e4 | ||
|
|
c070149ae6 | ||
|
|
b0b6ea943e | ||
|
|
65b72217ea | ||
|
|
c4c2e8e3a9 | ||
|
|
47018d61cd | ||
|
|
d18db9adfa | ||
|
|
41913c8c58 | ||
|
|
68b982a744 | ||
|
|
a582302a79 | ||
|
|
bd3b2c93ee | ||
|
|
4704d63791 | ||
|
|
415a1477bb | ||
|
|
f363d5e187 | ||
|
|
efb0ee9c48 | ||
|
|
96daee5e1a | ||
|
|
d236206cce | ||
|
|
1ef8e1cf73 | ||
|
|
dc63f858b0 | ||
|
|
b8a3952153 | ||
|
|
4b2a62f660 | ||
|
|
08c074e44b | ||
|
|
d1953b0215 | ||
|
|
b268d7dd8e | ||
|
|
90e56e2f07 | ||
|
|
45ddaa4d38 | ||
|
|
0de54b163a | ||
|
|
620b86cddb | ||
|
|
32d0ac4147 | ||
|
|
a16733eb93 | ||
|
|
b67eef484c | ||
|
|
5166964659 | ||
|
|
e72c2df6d2 | ||
|
|
cdafb3870c | ||
|
|
20abf21791 | ||
|
|
482d155af4 | ||
|
|
ece23c727e | ||
|
|
b7726cd514 | ||
|
|
c31fe1ed35 | ||
|
|
9e034810d9 | ||
|
|
217aabe55a | ||
|
|
1dea2f384e | ||
|
|
c24edec6b9 | ||
|
|
add09ca5b8 | ||
|
|
57bd3632e8 | ||
|
|
6f44b6ffa6 | ||
|
|
ca8ba2d16b | ||
|
|
c37e4ba6b5 | ||
|
|
82cc778a0f | ||
|
|
5085ef831b | ||
|
|
4721bd117b | ||
|
|
57b13d848a | ||
|
|
e01a43f01e | ||
|
|
ec92351acf | ||
|
|
8c44eb75d5 | ||
|
|
aa48566aef | ||
|
|
7cbb26cbdb | ||
|
|
b8b6175a64 | ||
|
|
de5ad1b1b9 | ||
|
|
0866785495 | ||
|
|
947b3a9ec4 | ||
|
|
6db27ca51f | ||
|
|
f966ba97d7 | ||
|
|
1af711bc89 | ||
|
|
c71da4a075 | ||
|
|
d609fa87b4 | ||
|
|
dd16b0f65f | ||
|
|
6144e5eff4 | ||
|
|
c4db99f5a3 | ||
|
|
21431b0e56 | ||
|
|
136e6bc915 | ||
|
|
05aeb78b01 | ||
|
|
22010a1f9b | ||
|
|
e0fd6b210e | ||
|
|
be1c38f0e5 | ||
|
|
b1bdedd3a3 | ||
|
|
429b47963b | ||
|
|
466c8a0883 | ||
|
|
3a212030c0 | ||
|
|
7f1ac48f5f | ||
|
|
9c3e344ae0 | ||
|
|
db147e77b0 | ||
|
|
0384b03528 | ||
|
|
fc06de7b11 | ||
|
|
5ada0a7093 | ||
|
|
f136b8eb12 | ||
|
|
56a160464f | ||
|
|
3dc9430932 | ||
|
|
afa69b4f9c | ||
|
|
102f46d185 | ||
|
|
9c5e184d82 | ||
|
|
385b5451c8 | ||
|
|
0e7487257b | ||
|
|
eab0e3219f | ||
|
|
cf89c901a2 | ||
|
|
902b08cc0f | ||
|
|
caf9a09efa | ||
|
|
65734dc993 | ||
|
|
0f02fffc3a | ||
|
|
931ecad8c5 | ||
|
|
c137f83df6 | ||
|
|
d3a0a38dce | ||
|
|
b76af1aa21 | ||
|
|
5cf6a37ee2 | ||
|
|
63194abf93 | ||
|
|
e196b0915a | ||
|
|
d2369d1de8 | ||
|
|
a9bbcc5556 | ||
|
|
8d9430e7a2 | ||
|
|
0411754949 | ||
|
|
91ffaa1a2d | ||
|
|
ae9972a91a | ||
|
|
478fa6f2bb | ||
|
|
6a52a04591 | ||
|
|
a8f87e0d5e | ||
|
|
cbc96fdf5c | ||
|
|
b4d24dd9af | ||
|
|
4b66cc2acb | ||
|
|
3766d5ce27 | ||
|
|
4b7d686754 | ||
|
|
b948a07a4d | ||
|
|
6d3505aefa | ||
|
|
b22650ff51 | ||
|
|
23a7f65b49 | ||
|
|
f4fba8eab4 | ||
|
|
1734bf54a7 | ||
|
|
88efec7815 | ||
|
|
e335189bb8 | ||
|
|
d03ca4ab95 | ||
|
|
257407758f | ||
|
|
2443c046aa | ||
|
|
d710eb3947 | ||
|
|
7b2f6f230d | ||
|
|
07cb6070cc | ||
|
|
bd7dc2a7be | ||
|
|
db931c12be | ||
|
|
765b311a08 | ||
|
|
ce198d9c0b | ||
|
|
8f5893931b | ||
|
|
221be48589 | ||
|
|
234ff2619d | ||
|
|
b37be46ba3 | ||
|
|
6e2ea508db | ||
|
|
0e6e2abd28 | ||
|
|
db1bdfbf65 | ||
|
|
7bf0f647b3 | ||
|
|
df25f8617b | ||
|
|
ad2099a27f | ||
|
|
708127f96d | ||
|
|
9deb51e95a | ||
|
|
67852ea657 |
@@ -92,10 +92,17 @@ module.exports = {
|
||||
'@typescript-eslint/member-delimiter-style': 'warn',
|
||||
'@typescript-eslint/method-signature-style': 'warn',
|
||||
'@typescript-eslint/no-extraneous-class': 'error',
|
||||
'@typescript-eslint/no-parameter-properties': 'error',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
||||
'@typescript-eslint/parameter-properties': 'error',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.cjs'],
|
||||
env: {
|
||||
node: true
|
||||
}
|
||||
},
|
||||
{
|
||||
extends: ['plugin:chai-expect/recommended', 'plugin:chai-friendly/recommended'],
|
||||
files: ['*.test.ts'],
|
||||
@@ -179,6 +186,17 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
ignorePackages: true,
|
||||
pattern: {
|
||||
js: 'always',
|
||||
ts: 'never'
|
||||
}
|
||||
}
|
||||
],
|
||||
'import/no-duplicates': 'warn',
|
||||
'sort-imports-es6-autofix/sort-imports-es6': [
|
||||
2,
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -3,8 +3,7 @@ name: Bug Report
|
||||
about: Create a bug report to help us fix a demonstrable problem with code in the library.
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: claviska
|
||||
|
||||
assignees:
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -3,8 +3,6 @@ name: Feature Request
|
||||
about: Suggest an idea for this project.
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: claviska
|
||||
|
||||
---
|
||||
|
||||
### What issue are you having?
|
||||
|
||||
4
.github/SECURITY.md
vendored
@@ -1,7 +1,7 @@
|
||||
# Reporting Security Issues
|
||||
|
||||
We take security issues in Shoelace very seriously and appreciate your efforts to disclose your findings responsibly.
|
||||
We take security issues in Web Awesome very seriously and appreciate your efforts to disclose your findings responsibly.
|
||||
|
||||
To report a security issue, email [cory@abeautifulsite.net](mailto:cory@abeautifulsite.net) and include "SHOELACE SECURITY" in the subject line.
|
||||
To report a security issue, email [cory@fontawesome.com](mailto:cory@abeautifulsite.net) and include "WEB AWESOME SECURITY" in the subject line.
|
||||
|
||||
We'll respond as soon as possible and keep you updated throughout the process.
|
||||
|
||||
7
.gitignore
vendored
@@ -1,7 +1,8 @@
|
||||
.DS_Store
|
||||
_site
|
||||
.cache
|
||||
docs/dist
|
||||
docs/search.json
|
||||
.DS_Store
|
||||
dist
|
||||
docs/assets/images/sprite.svg
|
||||
node_modules
|
||||
src/react
|
||||
cdn
|
||||
|
||||
@@ -7,5 +7,8 @@ docs/search.json
|
||||
src/components/icon/icons
|
||||
src/react/index.ts
|
||||
node_modules
|
||||
package.json
|
||||
package-lock.json
|
||||
tsconfig.json
|
||||
cdn
|
||||
_site
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -3,5 +3,6 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"debug.enableStatusBarColor": false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Contributing to Shoelace
|
||||
# Contributing to Web Awesome
|
||||
|
||||
Before contributing, please review the contributions guidelines at:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020 A Beautiful Site, LLC
|
||||
Copyright (c) 2023 Fonticons, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
40
README.md
@@ -1,4 +1,4 @@
|
||||
# Shoelace
|
||||
# Web Awesome
|
||||
|
||||
A forward-thinking library of web components.
|
||||
|
||||
@@ -9,7 +9,7 @@ A forward-thinking library of web components.
|
||||
- Built with accessibility in mind ♿️
|
||||
- Open source 😸
|
||||
|
||||
Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
|
||||
Built by the folks behind [Font Awesome](https://fontawesome.com/).
|
||||
|
||||
---
|
||||
|
||||
@@ -21,15 +21,15 @@ Twitter: [@shoelace_style](https://twitter.com/shoelace_style)
|
||||
|
||||
---
|
||||
|
||||
## Shoemakers 🥾
|
||||
## Developers ✨
|
||||
|
||||
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14.17 to build and run the project locally.
|
||||
Developers can use this documentation to learn how to build Web Awesome from source. You will need Node >= 14.17 to build and run the project locally.
|
||||
|
||||
**You don't need to do any of this to use Shoelace!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Shoelace.
|
||||
**You don't need to do any of this to use Web Awesome!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Web Awesome.
|
||||
|
||||
If that's not what you're trying to do, the [documentation website](https://shoelace.style) is where you want to be.
|
||||
|
||||
### What are you using to build Shoelace?
|
||||
### What are you using to build Web Awesome?
|
||||
|
||||
Components are built with [LitElement](https://lit-element.polymer-project.org/), a custom elements base class that provides an intuitive API and reactive data binding. The build is a custom script with bundling powered by [esbuild](https://esbuild.github.io/).
|
||||
|
||||
@@ -38,8 +38,8 @@ Components are built with [LitElement](https://lit-element.polymer-project.org/)
|
||||
Start by [forking the repo](https://github.com/shoelace-style/shoelace/fork) on GitHub, then clone it locally and install dependencies.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_GITHUB_USERNAME/shoelace
|
||||
cd shoelace
|
||||
git clone https://github.com/YOUR_GITHUB_USERNAME/webawesome
|
||||
cd webawesome
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -51,9 +51,7 @@ Once you've cloned the repo, run the following command.
|
||||
npm start
|
||||
```
|
||||
|
||||
This will spin up the Shoelace dev server. After the initial build, a browser will open automatically. There is currently no hot module reloading (HMR), as browser's don't provide a way to reregister custom elements, but most changes to the source will reload the browser automatically.
|
||||
|
||||
The documentation is powered by Docsify, which uses raw markdown files to generate pages. As such, no static files are built for the docs.
|
||||
This will spin up the dev server. After the initial build, a browser will open automatically. There is currently no hot module reloading (HMR), as browser's don't provide a way to reregister custom elements, but most changes to the source will reload the browser automatically.
|
||||
|
||||
### Building
|
||||
|
||||
@@ -65,30 +63,18 @@ npm run build
|
||||
|
||||
### Creating New Components
|
||||
|
||||
To scaffold a new component, run the following command, replacing `sl-tag-name` with the desired tag name.
|
||||
To scaffold a new component, run the following command, replacing `wa-tag-name` with the desired tag name.
|
||||
|
||||
```bash
|
||||
npm run create sl-tag-name
|
||||
npm run create wa-tag-name
|
||||
```
|
||||
|
||||
This will generate a source file, a stylesheet, and a docs page for you. When you start the dev server, you'll find the new component in the "Components" section of the sidebar.
|
||||
|
||||
### Contributing
|
||||
|
||||
Shoelace is an open source project and contributions are encouraged! If you're interesting in contributing, please review the [contribution guidelines](CONTRIBUTING.md) first.
|
||||
Web Awesome is an open source project and contributions are encouraged! If you're interesting in contributing, please review the [contribution guidelines](CONTRIBUTING.md) first.
|
||||
|
||||
## License
|
||||
|
||||
Shoelace is designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska). It’s available under the terms of the MIT license.
|
||||
|
||||
Designing, developing, and supporting this library requires a lot of time, effort, and skill. I’d like to keep it open source so everyone can use it, but that doesn’t provide me with any income.
|
||||
|
||||
**Therefore, if you’re using my software to make a profit,** I respectfully ask that you help [fund its development](https://github.com/sponsors/claviska) by becoming a sponsor. There are multiple tiers to choose from with benefits at every level, including prioritized support, bug fixes, feature requests, and advertising.
|
||||
|
||||
👇 Your support is very much appreciated! 👇
|
||||
|
||||
- [Become a sponsor](https://github.com/sponsors/claviska)
|
||||
- [Star on GitHub](https://github.com/shoelace-style/shoelace/stargazers)
|
||||
- [Follow on Twitter](https://twitter.com/shoelace_style)
|
||||
|
||||
Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾
|
||||
Web Awesome is available under the terms of the MIT license.
|
||||
|
||||
23
cspell.json
@@ -15,8 +15,10 @@
|
||||
"autoplay",
|
||||
"bezier",
|
||||
"boxicons",
|
||||
"CACHEABLE",
|
||||
"callout",
|
||||
"callouts",
|
||||
"cdndir",
|
||||
"chatbubble",
|
||||
"checkmark",
|
||||
"claviska",
|
||||
@@ -26,6 +28,8 @@
|
||||
"colocated",
|
||||
"colour",
|
||||
"combobox",
|
||||
"Commonmark",
|
||||
"compat",
|
||||
"Composability",
|
||||
"Consolas",
|
||||
"contenteditable",
|
||||
@@ -39,13 +43,16 @@
|
||||
"datetime",
|
||||
"describedby",
|
||||
"Docsify",
|
||||
"dogfood",
|
||||
"dropdowns",
|
||||
"easings",
|
||||
"endraw",
|
||||
"enterkeyhint",
|
||||
"eqeqeq",
|
||||
"erroneou",
|
||||
"errormessage",
|
||||
"esbuild",
|
||||
"exportmaps",
|
||||
"exportparts",
|
||||
"fieldsets",
|
||||
"formaction",
|
||||
@@ -79,9 +86,11 @@
|
||||
"labelledby",
|
||||
"Laravel",
|
||||
"LaViska",
|
||||
"linkify",
|
||||
"listbox",
|
||||
"listitem",
|
||||
"litelement",
|
||||
"longform",
|
||||
"lowercasing",
|
||||
"Lucide",
|
||||
"maxlength",
|
||||
@@ -92,6 +101,7 @@
|
||||
"minlength",
|
||||
"monospace",
|
||||
"mousedown",
|
||||
"mousemove",
|
||||
"mouseup",
|
||||
"multiselectable",
|
||||
"nextjs",
|
||||
@@ -99,10 +109,16 @@
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"novalidate",
|
||||
"npmdir",
|
||||
"Numberish",
|
||||
"oklab",
|
||||
"oklch",
|
||||
"outdir",
|
||||
"ParamagicDev",
|
||||
"peta",
|
||||
"petabit",
|
||||
"Preact",
|
||||
"prismjs",
|
||||
"progressbar",
|
||||
"radiogroup",
|
||||
"Railsbyte",
|
||||
@@ -110,6 +126,8 @@
|
||||
"reregister",
|
||||
"resizer",
|
||||
"resizers",
|
||||
"retargeted",
|
||||
"RETRYABLE",
|
||||
"rgba",
|
||||
"roadmap",
|
||||
"Roboto",
|
||||
@@ -122,7 +140,9 @@
|
||||
"scroller",
|
||||
"Segoe",
|
||||
"semibold",
|
||||
"sitedir",
|
||||
"slotchange",
|
||||
"smartquotes",
|
||||
"spacebar",
|
||||
"stylesheet",
|
||||
"Tabbable",
|
||||
@@ -138,16 +158,19 @@
|
||||
"tinycolor",
|
||||
"transitionend",
|
||||
"treeitem",
|
||||
"treeshaking",
|
||||
"Triaging",
|
||||
"turbolinks",
|
||||
"typeof",
|
||||
"unbundles",
|
||||
"unbundling",
|
||||
"unicons",
|
||||
"unsanitized",
|
||||
"unsupportive",
|
||||
"valpha",
|
||||
"valuenow",
|
||||
"valuetext",
|
||||
"webawesome",
|
||||
"WEBP",
|
||||
"Webpacker",
|
||||
"wordmark"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { generateCustomData } from 'cem-plugin-vs-code-custom-data-generator';
|
||||
import * as path from 'path';
|
||||
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
|
||||
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
|
||||
import { parse } from 'comment-parser';
|
||||
import { pascalCase } from 'pascal-case';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
@@ -26,20 +28,47 @@ function replace(string, terms) {
|
||||
}
|
||||
|
||||
export default {
|
||||
globs: ['src/components/**/*.ts'],
|
||||
globs: ['src/components/**/*.component.ts'],
|
||||
exclude: ['**/*.styles.ts', '**/*.test.ts'],
|
||||
plugins: [
|
||||
// Append package data
|
||||
{
|
||||
name: 'shoelace-package-data',
|
||||
name: 'wa-package-data',
|
||||
packageLinkPhase({ customElementsManifest }) {
|
||||
customElementsManifest.package = { name, description, version, author, homepage, license };
|
||||
}
|
||||
},
|
||||
// Infer tag names because we no longer use @customElement decorators.
|
||||
{
|
||||
name: 'wa-infer-tag-names',
|
||||
analyzePhase({ ts, node, moduleDoc }) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration: {
|
||||
const className = node.name.getText();
|
||||
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
|
||||
|
||||
const importPath = moduleDoc.path;
|
||||
|
||||
// This is kind of a best guess at components. "thing.component.ts"
|
||||
if (!importPath.endsWith('.component.ts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagNameWithoutPrefix = path.basename(importPath, '.component.ts');
|
||||
const tagName = 'wa-' + tagNameWithoutPrefix;
|
||||
|
||||
classDoc.tagNameWithoutPrefix = tagNameWithoutPrefix;
|
||||
classDoc.tagName = tagName;
|
||||
|
||||
// This used to be set to true by @customElement
|
||||
classDoc.customElement = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Parse custom jsDoc tags
|
||||
{
|
||||
name: 'shoelace-custom-tags',
|
||||
name: 'wa-custom-tags',
|
||||
analyzePhase({ ts, node, moduleDoc }) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration: {
|
||||
@@ -58,6 +87,9 @@ export default {
|
||||
});
|
||||
});
|
||||
|
||||
// This is what allows us to map JSDOC comments to ReactWrappers.
|
||||
classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n');
|
||||
|
||||
const parsed = parse(`${customComments}\n */`);
|
||||
parsed[0].tags?.forEach(t => {
|
||||
switch (t.tag) {
|
||||
@@ -106,7 +138,7 @@ export default {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'shoelace-react-event-names',
|
||||
name: 'wa-react-event-names',
|
||||
analyzePhase({ ts, node, moduleDoc }) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration: {
|
||||
@@ -116,6 +148,7 @@ export default {
|
||||
if (classDoc?.events) {
|
||||
classDoc.events.forEach(event => {
|
||||
event.reactName = `on${pascalCase(event.name)}`;
|
||||
event.eventName = `${pascalCase(event.name)}Event`;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -123,7 +156,7 @@ export default {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'shoelace-translate-module-paths',
|
||||
name: 'wa-translate-module-paths',
|
||||
packageLinkPhase({ customElementsManifest }) {
|
||||
customElementsManifest?.modules?.forEach(mod => {
|
||||
//
|
||||
@@ -137,7 +170,7 @@ export default {
|
||||
//
|
||||
const terms = [
|
||||
{ from: /^src\//, to: '' }, // Strip the src/ prefix
|
||||
{ from: /\.(t|j)sx?$/, to: '.js' } // Convert .ts to .js
|
||||
{ from: /\.component.(t|j)sx?$/, to: '.js' } // Convert .ts to .js
|
||||
];
|
||||
|
||||
mod.path = replace(mod.path, terms);
|
||||
@@ -159,9 +192,25 @@ export default {
|
||||
}
|
||||
},
|
||||
// Generate custom VS Code data
|
||||
generateCustomData({
|
||||
customElementVsCodePlugin({
|
||||
outdir,
|
||||
cssFileName: null
|
||||
cssFileName: null,
|
||||
referencesTemplate: (_, tag) => [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: `https://shoelace.style/components/${tag.replace('wa-', '')}`
|
||||
}
|
||||
]
|
||||
}),
|
||||
customElementJetBrainsPlugin({
|
||||
outdir: './dist',
|
||||
excludeCss: true,
|
||||
referencesTemplate: (_, tag) => {
|
||||
return {
|
||||
name: 'Documentation',
|
||||
url: `https://shoelace.style/components/${tag.replace('wa-', '')}`
|
||||
};
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Not Found
|
||||
|
||||
<img class="not-found-image" src="/assets/images/undraw-not-found.svg" alt="Cute monsters hiding behind a tree">
|
||||
|
||||
Sorry, I couldn't find that page. Have you tried pressing <kbd>/</kbd> to search?
|
||||
349
docs/_includes/component.njk
Normal file
@@ -0,0 +1,349 @@
|
||||
{% extends "default.njk" %}
|
||||
|
||||
{# Find the component based on the `tag` front matter #}
|
||||
{% set component = getComponent('wa-' + page.fileSlug) %}
|
||||
|
||||
{% block content %}
|
||||
{# Determine the badge variant #}
|
||||
{% if component.status == 'stable' %}
|
||||
{% set badgeVariant = 'brand' %}
|
||||
{% elseif component.status == 'experimental' %}
|
||||
{% set badgeVariant = 'warning' %}
|
||||
{% elseif component.status == 'planned' %}
|
||||
{% set badgeVariant = 'neutral' %}
|
||||
{% elseif component.status == 'deprecated' %}
|
||||
{% set badgeVariant = 'danger' %}
|
||||
{% else %}
|
||||
{% set badgeVariant = 'neutral' %}
|
||||
{% endif %}
|
||||
|
||||
{# Header #}
|
||||
<header class="component-header">
|
||||
<h1>{{ component.name | classNameToComponentName }}</h1>
|
||||
|
||||
<div class="component-header__tag">
|
||||
<code><{{ component.tagName }}> | {{ component.name }}</code>
|
||||
</div>
|
||||
|
||||
<div class="component-header__info">
|
||||
<wa-badge variant="neutral" pill>
|
||||
Since {{component.since or '?' }}
|
||||
</wa-badge>
|
||||
<wa-badge variant="{{ badgeVariant }}" pill style="text-transform: capitalize;">
|
||||
{{ component.status }}
|
||||
</wa-badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="component-summary">
|
||||
{% if component.summary %}
|
||||
{{ component.summary | markdownInline | safe }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{# Markdown content #}
|
||||
{{ content | safe }}
|
||||
|
||||
{# Importing #}
|
||||
<h2>Importing</h2>
|
||||
<p>
|
||||
If you're using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use
|
||||
any of the following snippets to <a href="/getting-started/installation#cherry-picking">cherry pick</a> this component.
|
||||
</p>
|
||||
|
||||
<wa-tab-group>
|
||||
<wa-tab slot="nav" panel="script">Script</wa-tab>
|
||||
<wa-tab slot="nav" panel="import">Import</wa-tab>
|
||||
<wa-tab slot="nav" panel="bundler">Bundler</wa-tab>
|
||||
<wa-tab slot="nav" panel="react">React</wa-tab>
|
||||
|
||||
<wa-tab-panel name="script">
|
||||
<p>
|
||||
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
|
||||
using a script tag:
|
||||
</p>
|
||||
<pre><code class="language-html"><script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}"></script></code></pre>
|
||||
</wa-tab-panel>
|
||||
|
||||
<wa-tab-panel name="import">
|
||||
<p>
|
||||
To import this component from <a href="https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace">the CDN</a>
|
||||
using a JavaScript import:
|
||||
</p>
|
||||
<pre><code class="language-js">import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}';</code></pre>
|
||||
</wa-tab-panel>
|
||||
|
||||
<wa-tab-panel name="bundler">
|
||||
<p>
|
||||
To import this component using <a href="{{ rootUrl('/getting-started/installation#bundling') }}">a bundler</a>:
|
||||
</p>
|
||||
<pre><code class="language-js">import '@shoelace-style/shoelace/{{ meta.npmdir }}/{{ component.path }}';</code></pre>
|
||||
</wa-tab-panel>
|
||||
|
||||
<wa-tab-panel name="react">
|
||||
<p>
|
||||
To import this component as a <a href="/frameworks/react">React component</a>:
|
||||
</p>
|
||||
<pre><code class="language-js">import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';</code></pre>
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
|
||||
{# Slots #}
|
||||
{% if component.slots.length %}
|
||||
<h2>Slots</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slot in component.slots %}
|
||||
<tr>
|
||||
<td class="nowrap">
|
||||
{% if slot.name %}
|
||||
<code>{{ slot.name }}</code>
|
||||
{% else %}
|
||||
(default)
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ slot.description | markdownInline | safe }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#slots') }}">using slots</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Properties #}
|
||||
{% if component.properties.length %}
|
||||
<h2>Properties</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
<th class="table-reflects">Reflects</th>
|
||||
<th class="table-type">Type</th>
|
||||
<th class="table-default">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for prop in component.properties %}
|
||||
<tr>
|
||||
<td>
|
||||
<code class="nowrap">{{ prop.name }}</code>
|
||||
{% if prop.attribute | length > 0 %}
|
||||
{% if prop.attribute != prop.name %}
|
||||
<br>
|
||||
<wa-tooltip content="This attribute is different from its property">
|
||||
<small>
|
||||
<code class="nowrap">
|
||||
{{ prop.attribute }}
|
||||
</code>
|
||||
</small>
|
||||
</wa-tooltip>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ prop.description | markdownInline | safe }}
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
{% if prop.reflects %}
|
||||
<wa-icon label="yes" name="check-lg"></wa-icon>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if prop.type.text %}
|
||||
<code>{{ prop.type.text | markdownInline | safe }}</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if prop.default %}
|
||||
<code>{{ prop.default | markdownInline | safe }}</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td class="nowrap"><code>updateComplete</code></td>
|
||||
<td>
|
||||
A read-only promise that resolves when the component has
|
||||
<a href="/getting-started/usage?#component-rendering-and-updating">finished updating</a>.
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Events #}
|
||||
{% if component.events.length %}
|
||||
<h2>Events</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name" data-flavor="html">Name</th>
|
||||
<th class="table-name" data-flavor="react">React Event</th>
|
||||
<th class="table-description">Description</th>
|
||||
<th class="table-event-detail">Event Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in component.events %}
|
||||
<tr>
|
||||
<td data-flavor="html"><code class="nowrap">{{ event.name }}</code></td>
|
||||
<td data-flavor="react"><code class="nowrap">{{ event.reactName }}</code></td>
|
||||
<td>{{ event.description | markdownInline | safe }}</td>
|
||||
<td>
|
||||
{% if event.type.text %}
|
||||
<code>{{ event.type.text }}</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#events') }}">events</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Methods #}
|
||||
{% if component.methods.length %}
|
||||
<h2>Methods</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
<th class="table-arguments">Arguments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for method in component.methods %}
|
||||
<tr>
|
||||
<td class="nowrap"><code>{{ method.name }}()</code></td>
|
||||
<td>{{ method.description | markdownInline | safe }}</td>
|
||||
<td>
|
||||
{% if method.parameters.length %}
|
||||
<code>
|
||||
{% for param in method.parameters %}
|
||||
{{ param.name }}: {{ param.type.text }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#methods') }}">methods</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Custom Properties #}
|
||||
{% if component.cssProperties.length %}
|
||||
<h2>Custom Properties</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
<th class="table-default">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cssProperty in component.cssProperties %}
|
||||
<tr>
|
||||
<td class="nowrap"><code>{{ cssProperty.name }}</code></td>
|
||||
<td>{{ cssProperty.description | markdownInline | safe }}</td>
|
||||
<td>{{ cssProperty.default }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#custom-properties') }}">customizing CSS custom properties</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# CSS Parts #}
|
||||
{% if component.cssParts.length %}
|
||||
<h2>Parts</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cssPart in component.cssParts %}
|
||||
<tr>
|
||||
<td class="nowrap"><code>{{ cssPart.name }}</code></td>
|
||||
<td>{{ cssPart.description | markdownInline | safe }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Animations #}
|
||||
{% if component.animations.length %}
|
||||
<h2>Animations</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="table-name">Name</th>
|
||||
<th class="table-description">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for animation in component.animations %}
|
||||
<tr>
|
||||
<td class="nowrap"><code>{{ animation.name }}</code></td>
|
||||
<td>{{ animation.description | markdownInline | safe }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
|
||||
{% endif %}
|
||||
|
||||
{# Dependencies #}
|
||||
{% if component.dependencies.length %}
|
||||
<h2>Dependencies</h2>
|
||||
|
||||
<p>This component automatically imports the following dependencies.</p>
|
||||
|
||||
<ul>
|
||||
{% for dependency in component.dependencies %}
|
||||
<li><code><{{ dependency }}></code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
128
docs/_includes/default.njk
Normal file
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="en"
|
||||
data-layout="{{ layout }}"
|
||||
data-wa-version="{{ meta.version }}"
|
||||
>
|
||||
<head>
|
||||
{# Metadata #}
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="{{ meta.description }}" />
|
||||
<title>{{ meta.title }}</title>
|
||||
|
||||
{# Opt out of Turbo caching #}
|
||||
<meta name="turbo-cache-control">
|
||||
|
||||
{# Stylesheets #}
|
||||
<link rel="stylesheet" href="{{ assetUrl('styles/docs.css') }}" />
|
||||
<link rel="stylesheet" href="{{ assetUrl('styles/code-previews.css') }}" />
|
||||
<link rel="stylesheet" href="{{ assetUrl('styles/search.css') }}" />
|
||||
|
||||
{# Favicons #}
|
||||
<link rel="icon" href="{{ assetUrl('images/logo.svg') }}" type="image/x-icon" />
|
||||
|
||||
{# Twitter Cards #}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:creator" content="shoelace_style" />
|
||||
<meta name="twitter:image" content="{{ assetUrl(meta.image, true) }}" />
|
||||
|
||||
{# OpenGraph #}
|
||||
<meta property="og:url" content="{{ rootUrl(page.url, true) }}" />
|
||||
<meta property="og:title" content="{{ meta.title }}" />
|
||||
<meta property="og:description" content="{{ meta.description }}" />
|
||||
<meta property="og:image" content="{{ assetUrl(meta.image, true) }}" />
|
||||
|
||||
{# Web Awesome #}
|
||||
<link rel="stylesheet" href="/dist/themes/applied.css" />
|
||||
<link rel="stylesheet" href="/dist/themes/default.css" />
|
||||
<script type="module" src="/dist/autoloader.js"></script>
|
||||
|
||||
{# Set the initial theme and menu states here to prevent flashing #}
|
||||
<script>
|
||||
(() => {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const theme = localStorage.getItem('theme') || 'auto';
|
||||
document.documentElement.classList.toggle('wa-theme-default-dark', theme === 'dark' || (theme === 'auto' && prefersDark));
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# Web Fonts #}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500;1,600&family=Noto+Sans+Mono&display=swap" rel="stylesheet">
|
||||
|
||||
{# Turbo + Scroll positioning #}
|
||||
<script src="{{ assetUrl('scripts/turbo.js') }}" type="module"></script>
|
||||
<script src="{{ assetUrl('scripts/docs.js') }}" defer></script>
|
||||
<script src="{{ assetUrl('scripts/code-previews.js') }}" defer></script>
|
||||
<script src="{{ assetUrl('scripts/lunr.js') }}" defer></script>
|
||||
<script src="{{ assetUrl('scripts/search.js') }}" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a id="skip-to-main" class="wa-visually-hidden" href="#main-content" data-smooth-link="false">
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
{# Menu toggle #}
|
||||
<button id="menu-toggle" type="button" aria-label="Menu">
|
||||
<svg width="148" height="148" viewBox="0 0 148 148" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="currentColor" stroke-width="18" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||
<path d="M9.5 125.5h129M9.5 74.5h129M9.5 23.5h129"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<aside id="sidebar" data-preserve-scroll>
|
||||
<header>
|
||||
<a href="/">
|
||||
{% include 'workmark.njk' %}
|
||||
</a>
|
||||
<div class="sidebar-version">
|
||||
{{ meta.version }}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="sidebar-buttons">
|
||||
<wa-button outline size="small" class="repo-button repo-button--github" href="https://github.com/shoelace-style/shoelace" target="_blank">
|
||||
<wa-icon slot="prefix" name="github"></wa-icon> Code
|
||||
</wa-button>
|
||||
<wa-button outline size="small" class="repo-button repo-button--star" href="https://github.com/shoelace-style/shoelace/stargazers" target="_blank">
|
||||
<wa-icon slot="prefix" name="star-fill"></wa-icon> Star
|
||||
</wa-button>
|
||||
<wa-button outline size="small" class="repo-button repo-button--twitter" href="https://twitter.com/shoelace_style" target="_blank">
|
||||
<wa-icon slot="prefix" name="twitter"></wa-icon> Follow
|
||||
</wa-button>
|
||||
</div>
|
||||
|
||||
<button class="search-box" type="button" title="Press / to search" aria-label="Search" data-plugin="search">
|
||||
<wa-icon name="search"></wa-icon>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
|
||||
<nav>
|
||||
{% include 'sidebar.njk' %}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{# Content #}
|
||||
<main>
|
||||
<a id="main-content"></a>
|
||||
<article id="content" class="content{% if toc %} content--with-toc{% endif %}">
|
||||
{% if toc %}
|
||||
<div class="content__toc">
|
||||
<ul>
|
||||
<li class="top"><a href="#">{{ meta.title }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="content__body">
|
||||
{% block content %}
|
||||
{{ content | safe }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
83
docs/_includes/sidebar.njk
Normal file
@@ -0,0 +1,83 @@
|
||||
<ul>
|
||||
<li>
|
||||
<h2>Experimental</h2>
|
||||
<ul>
|
||||
<li><a href="/experimental/style-guide">Style Guide</a></li>
|
||||
<li><a href="/experimental/themer">Themer</a></li>
|
||||
<li style="margin-top: .5rem;"><wa-switch id="theme-toggle">Dark mode</wa-switch></li>
|
||||
<script type="module">
|
||||
// Temporary dark toggle
|
||||
const toggle = document.getElementById('theme-toggle');
|
||||
toggle.checked = document.documentElement.classList.contains('wa-theme-default-dark');
|
||||
|
||||
toggle.addEventListener('wa-change', () => {
|
||||
document.documentElement.classList.toggle('wa-theme-default-dark');
|
||||
localStorage.setItem('theme', toggle.checked ? 'dark' : 'light');
|
||||
});
|
||||
</script>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2>Getting Started</h2>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/getting-started/installation">Installation</a></li>
|
||||
<li><a href="/getting-started/usage">Usage</a></li>
|
||||
<li><a href="/getting-started/themes">Themes</a></li>
|
||||
<li><a href="/getting-started/customizing">Customizing</a></li>
|
||||
<li><a href="/getting-started/form-controls">Form Controls</a></li>
|
||||
<li><a href="/getting-started/localization">Localization</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2>Frameworks</h2>
|
||||
<ul>
|
||||
<li><a href="/frameworks/react">React</a></li>
|
||||
<li><a href="/frameworks/vue">Vue</a></li>
|
||||
<li><a href="/frameworks/angular">Angular</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li><a href="/resources/community">Community</a></li>
|
||||
<li><a href="https://github.com/shoelace-style/shoelace/discussions">Help & Support</a></li>
|
||||
<li><a href="/resources/accessibility">Accessibility</a></li>
|
||||
<li><a href="/resources/contributing">Contributing</a></li>
|
||||
<li><a href="/resources/changelog">Changelog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2>Components</h2>
|
||||
<ul>
|
||||
{% for component in meta.components %}
|
||||
<li>
|
||||
<a href="/components/{{ component.tagName | removeWaPrefix }}">
|
||||
{{ component.name | classNameToComponentName }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2>Design Tokens</h2>
|
||||
<ul>
|
||||
<li><a href="/tokens/typography">Typography</a></li>
|
||||
<li><a href="/tokens/color">Color</a></li>
|
||||
<li><a href="/tokens/spacing">Spacing</a></li>
|
||||
<li><a href="/tokens/borders">Borders</a></li>
|
||||
<li><a href="/tokens/shadows">Shadows</a></li>
|
||||
<li><a href="/tokens/transition">Transition</a></li>
|
||||
<li><a href="/tokens/z-index">Z-index</a></li>
|
||||
<li><a href="/tokens/more">More Tokens</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2>Tutorials</h2>
|
||||
<ul>
|
||||
<li><a href="/tutorials/integrating-with-laravel">Integrating with Laravel</a></li>
|
||||
<li><a href="/tutorials/integrating-with-nextjs">Integrating with NextJS</a></li>
|
||||
<li><a href="/tutorials/integrating-with-rails">Integrating with Rails</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
7
docs/_includes/workmark.njk
Normal file
|
After Width: | Height: | Size: 21 KiB |
103
docs/_sidebar.md
@@ -1,103 +0,0 @@
|
||||
- Getting Started
|
||||
|
||||
- [Overview](/)
|
||||
- [Installation](/getting-started/installation)
|
||||
- [Usage](/getting-started/usage)
|
||||
- [Themes](/getting-started/themes)
|
||||
- [Customizing](/getting-started/customizing)
|
||||
- [Form Controls](/getting-started/form-controls)
|
||||
- [Localization](/getting-started/localization)
|
||||
|
||||
- Frameworks
|
||||
|
||||
- [React](/frameworks/react)
|
||||
- [Vue](/frameworks/vue)
|
||||
- [Angular](/frameworks/angular)
|
||||
|
||||
- Resources
|
||||
|
||||
- [Community](/resources/community)
|
||||
- [Accessibility](/resources/accessibility)
|
||||
- [Contributing](/resources/contributing)
|
||||
- [Changelog](/resources/changelog)
|
||||
|
||||
- Components
|
||||
|
||||
- [Alert](/components/alert)
|
||||
- [Avatar](/components/avatar)
|
||||
- [Badge](/components/badge)
|
||||
- [Breadcrumb](/components/breadcrumb)
|
||||
- [Breadcrumb Item](/components/breadcrumb-item)
|
||||
- [Button](/components/button)
|
||||
- [Button Group](/components/button-group)
|
||||
- [Card](/components/card)
|
||||
- [Carousel](/components/carousel)
|
||||
- [Carousel Item](/components/carousel-item)
|
||||
- [Checkbox](/components/checkbox)
|
||||
- [Color Picker](/components/color-picker)
|
||||
- [Details](/components/details)
|
||||
- [Dialog](/components/dialog)
|
||||
- [Divider](/components/divider)
|
||||
- [Drawer](/components/drawer)
|
||||
- [Dropdown](/components/dropdown)
|
||||
- [Icon](/components/icon)
|
||||
- [Icon Button](/components/icon-button)
|
||||
- [Image Comparer](/components/image-comparer)
|
||||
- [Input](/components/input)
|
||||
- [Menu](/components/menu)
|
||||
- [Menu Item](/components/menu-item)
|
||||
- [Menu Label](/components/menu-label)
|
||||
- [Option](/components/option)
|
||||
- [Progress Bar](/components/progress-bar)
|
||||
- [Progress Ring](/components/progress-ring)
|
||||
- [QR Code](/components/qr-code)
|
||||
- [Radio](/components/radio)
|
||||
- [Radio Button](/components/radio-button)
|
||||
- [Radio Group](/components/radio-group)
|
||||
- [Range](/components/range)
|
||||
- [Rating](/components/rating)
|
||||
- [Select](/components/select)
|
||||
- [Skeleton](/components/skeleton)
|
||||
- [Spinner](/components/spinner)
|
||||
- [Split Panel](/components/split-panel)
|
||||
- [Switch](/components/switch)
|
||||
- [Tab Group](/components/tab-group)
|
||||
- [Tab](/components/tab)
|
||||
- [Tab Panel](/components/tab-panel)
|
||||
- [Tag](/components/tag)
|
||||
- [Textarea](/components/textarea)
|
||||
- [Tooltip](/components/tooltip)
|
||||
- [Tree](/components/tree)
|
||||
- [Tree Item](/components/tree-item)
|
||||
<!--plop:component-->
|
||||
|
||||
- Utilities
|
||||
|
||||
- [Animated Image](/components/animated-image)
|
||||
- [Animation](/components/animation)
|
||||
- [Format Bytes](/components/format-bytes)
|
||||
- [Format Date](/components/format-date)
|
||||
- [Format Number](/components/format-number)
|
||||
- [Include](/components/include)
|
||||
- [Mutation Observer](/components/mutation-observer)
|
||||
- [Popup](/components/popup)
|
||||
- [Relative Time](/components/relative-time)
|
||||
- [Resize Observer](/components/resize-observer)
|
||||
- [Visually Hidden](/components/visually-hidden)
|
||||
|
||||
- Design Tokens
|
||||
|
||||
- [Typography](/tokens/typography)
|
||||
- [Color](/tokens/color)
|
||||
- [Spacing](/tokens/spacing)
|
||||
- [Elevation](/tokens/elevation)
|
||||
- [Border Radius](/tokens/border-radius)
|
||||
- [Transition](/tokens/transition)
|
||||
- [Z-index](/tokens/z-index)
|
||||
- [More](/tokens/more)
|
||||
|
||||
- Tutorials
|
||||
|
||||
- [Integrating with Laravel](/tutorials/integrating-with-laravel)
|
||||
- [Integrating with NextJS](/tutorials/integrating-with-nextjs)
|
||||
- [Integrating with Rails](/tutorials/integrating-with-rails)
|
||||
35
docs/_utilities/active-links.cjs
Normal file
@@ -0,0 +1,35 @@
|
||||
function normalizePathname(pathname) {
|
||||
// Remove /index.html
|
||||
if (pathname.endsWith('/index.html')) {
|
||||
pathname = pathname.replace(/\/index\.html/, '');
|
||||
}
|
||||
|
||||
// Remove trailing slashes
|
||||
return pathname.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a class name to links that are currently active.
|
||||
*/
|
||||
module.exports = function (doc, options) {
|
||||
options = {
|
||||
className: 'active-link', // the class to add to active links
|
||||
pathname: undefined, // the current pathname to compare
|
||||
within: 'body', // element containing the target links
|
||||
...options
|
||||
};
|
||||
|
||||
const within = doc.querySelector(options.within);
|
||||
|
||||
if (!within) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
within.querySelectorAll('a').forEach(link => {
|
||||
if (normalizePathname(options.pathname) === normalizePathname(link.pathname)) {
|
||||
link.classList.add(options.className);
|
||||
}
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
64
docs/_utilities/anchor-headings.cjs
Normal file
@@ -0,0 +1,64 @@
|
||||
const { createSlug } = require('./strings.cjs');
|
||||
|
||||
/**
|
||||
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
|
||||
* The same document will be returned with the appropriate DOM manipulations.
|
||||
*/
|
||||
module.exports = function (doc, options) {
|
||||
options = {
|
||||
levels: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], // the headings to convert
|
||||
className: 'anchor-heading', // the class name to add
|
||||
within: 'body', // the element containing the target headings
|
||||
...options
|
||||
};
|
||||
|
||||
const within = doc.querySelector(options.within);
|
||||
|
||||
if (!within) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
within.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
|
||||
const hasAnchor = heading.querySelector('a');
|
||||
const anchor = doc.createElement('a');
|
||||
let id = heading.textContent ?? '';
|
||||
let suffix = 0;
|
||||
|
||||
// Skip heading levels we don't care about
|
||||
if (!options.levels?.includes(heading.tagName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert dots to underscores
|
||||
id = id.replace(/\./g, '_');
|
||||
|
||||
// Turn it into a slug
|
||||
id = createSlug(id);
|
||||
|
||||
// Make sure it starts with a letter
|
||||
if (!/^[a-z]/i.test(id)) {
|
||||
id = `id_${id}`;
|
||||
}
|
||||
|
||||
// Make sure the id is unique
|
||||
const originalId = id;
|
||||
while (doc.getElementById(id) !== null) {
|
||||
id = `${originalId}-${++suffix}`;
|
||||
}
|
||||
|
||||
if (hasAnchor || !id) return;
|
||||
|
||||
heading.setAttribute('id', id);
|
||||
anchor.setAttribute('href', `#${encodeURIComponent(id)}`);
|
||||
anchor.setAttribute('aria-label', `Direct link to "${heading.textContent}"`);
|
||||
|
||||
if (options.className) {
|
||||
heading.classList.add(options.className);
|
||||
}
|
||||
|
||||
// Append the anchor
|
||||
heading.append(anchor);
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
71
docs/_utilities/cem.cjs
Normal file
@@ -0,0 +1,71 @@
|
||||
const customElementsManifest = require('../../dist/custom-elements.json');
|
||||
|
||||
//
|
||||
// Export it here so we can import it elsewhere and use the same version
|
||||
//
|
||||
module.exports.customElementsManifest = customElementsManifest;
|
||||
|
||||
//
|
||||
// Gets all components from custom-elements.json and returns them in a more documentation-friendly format.
|
||||
//
|
||||
module.exports.getAllComponents = function () {
|
||||
const allComponents = [];
|
||||
|
||||
customElementsManifest.modules?.forEach(module => {
|
||||
module.declarations?.forEach(declaration => {
|
||||
if (declaration.customElement) {
|
||||
// Generate the dist path based on the src path and attach it to the component
|
||||
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
|
||||
|
||||
// Remove members that are private or don't have a description
|
||||
const members = declaration.members?.filter(member => member.description && member.privacy !== 'private');
|
||||
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
|
||||
const properties = members?.filter(prop => {
|
||||
// Look for a corresponding attribute
|
||||
const attribute = declaration.attributes?.find(attr => attr.fieldName === prop.name);
|
||||
if (attribute) {
|
||||
prop.attribute = attribute.name || attribute.fieldName;
|
||||
}
|
||||
|
||||
return prop.kind === 'field' && prop.privacy !== 'private';
|
||||
});
|
||||
allComponents.push({
|
||||
...declaration,
|
||||
methods,
|
||||
properties
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Build dependency graphs
|
||||
allComponents.forEach(component => {
|
||||
const dependencies = [];
|
||||
|
||||
// Recursively fetch sub-dependencies
|
||||
function getDependencies(tag) {
|
||||
const cmp = allComponents.find(c => c.tagName === tag);
|
||||
if (!cmp || !Array.isArray(component.dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cmp.dependencies?.forEach(dependentTag => {
|
||||
if (!dependencies.includes(dependentTag)) {
|
||||
dependencies.push(dependentTag);
|
||||
}
|
||||
getDependencies(dependentTag);
|
||||
});
|
||||
}
|
||||
|
||||
getDependencies(component.tagName);
|
||||
|
||||
component.dependencies = dependencies.sort();
|
||||
});
|
||||
|
||||
// Sort by name
|
||||
return allComponents.sort((a, b) => {
|
||||
if (a.name < b.name) return -1;
|
||||
if (a.name > b.name) return 1;
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
138
docs/_utilities/code-previews.cjs
Normal file
@@ -0,0 +1,138 @@
|
||||
let count = 1;
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns code fields with the :preview suffix into interactive code previews.
|
||||
*/
|
||||
module.exports = function (doc, options) {
|
||||
options = {
|
||||
within: 'body', // the element containing the code fields to convert
|
||||
...options
|
||||
};
|
||||
|
||||
const within = doc.querySelector(options.within);
|
||||
if (!within) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
within.querySelectorAll('[class*=":preview"]').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
if (!pre) {
|
||||
return;
|
||||
}
|
||||
const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null;
|
||||
const reactCode = adjacentPre?.querySelector('code[class$="react"]');
|
||||
const sourceGroupId = `code-preview-source-group-${count}`;
|
||||
const isExpanded = code.getAttribute('class').includes(':expanded');
|
||||
const noCodePen = code.getAttribute('class').includes(':no-codepen');
|
||||
|
||||
count++;
|
||||
|
||||
const htmlButton = `
|
||||
<button type="button"
|
||||
title="Show HTML code"
|
||||
class="code-preview__button code-preview__button--html"
|
||||
>
|
||||
HTML
|
||||
</button>
|
||||
`;
|
||||
|
||||
const reactButton = `
|
||||
<button type="button" title="Show React code" class="code-preview__button code-preview__button--react">
|
||||
React
|
||||
</button>
|
||||
`;
|
||||
|
||||
const codePenButton = `
|
||||
<button type="button" class="code-preview__button code-preview__button--codepen" title="Edit on CodePen">
|
||||
<svg
|
||||
width="138"
|
||||
height="26"
|
||||
viewBox="0 0 138 26"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const codePreview = `
|
||||
<div class="code-preview ${isExpanded ? 'code-preview--expanded' : ''}">
|
||||
<div class="code-preview__preview">
|
||||
${code.textContent}
|
||||
<div class="code-preview__resizer">
|
||||
<wa-icon name="grip-vertical"></wa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-preview__source-group" id="${sourceGroupId}">
|
||||
<div class="code-preview__source code-preview__source--html" ${reactCode ? 'data-flavor="html"' : ''}>
|
||||
<pre><code class="language-html">${escapeHtml(code.textContent)}</code></pre>
|
||||
</div>
|
||||
|
||||
${
|
||||
reactCode
|
||||
? `
|
||||
<div class="code-preview__source code-preview__source--react" data-flavor="react">
|
||||
<pre><code class="language-jsx">${escapeHtml(reactCode.textContent)}</code></pre>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="code-preview__buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="code-preview__button code-preview__toggle"
|
||||
aria-expanded="${isExpanded ? 'true' : 'false'}"
|
||||
aria-controls="${sourceGroupId}"
|
||||
>
|
||||
Source
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
${reactCode ? ` ${htmlButton} ${reactButton} ` : ''}
|
||||
|
||||
${noCodePen ? '' : codePenButton}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
pre.insertAdjacentHTML('afterend', codePreview);
|
||||
pre.remove();
|
||||
|
||||
if (adjacentPre) {
|
||||
adjacentPre.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Wrap code preview scripts in anonymous functions so they don't run in the global scope
|
||||
doc.querySelectorAll('.code-preview__preview script').forEach(script => {
|
||||
if (script.type === 'module') {
|
||||
// Modules are already scoped
|
||||
script.textContent = script.innerHTML;
|
||||
} else {
|
||||
// Wrap non-modules in an anonymous function so they don't run in the global scope
|
||||
script.textContent = `(() => { ${script.innerHTML} })();`;
|
||||
}
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
23
docs/_utilities/copy-code-buttons.cjs
Normal file
@@ -0,0 +1,23 @@
|
||||
let codeBlockId = 0;
|
||||
|
||||
/**
|
||||
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
|
||||
* document will be returned with the appropriate DOM manipulations.
|
||||
*/
|
||||
module.exports = function (doc) {
|
||||
doc.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
const button = doc.createElement('wa-copy-button');
|
||||
|
||||
if (!code.id) {
|
||||
code.id = `code-block-${++codeBlockId}`;
|
||||
}
|
||||
|
||||
button.classList.add('copy-code-button');
|
||||
button.setAttribute('from', code.id);
|
||||
|
||||
pre.append(button);
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
41
docs/_utilities/external-links.cjs
Normal file
@@ -0,0 +1,41 @@
|
||||
const { isExternalLink } = require('./strings.cjs');
|
||||
|
||||
/**
|
||||
* Transforms external links to make them safer and optionally add a target. The provided doc should be a document
|
||||
* object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations.
|
||||
*/
|
||||
module.exports = function (doc, options) {
|
||||
options = {
|
||||
className: 'external-link', // the class name to add to links
|
||||
noopener: true, // sets rel="noopener"
|
||||
noreferrer: true, // sets rel="noreferrer"
|
||||
ignore: () => false, // callback function to filter links that should be ignored
|
||||
within: 'body', // element that contains the target links
|
||||
target: '', // sets the target attribute
|
||||
...options
|
||||
};
|
||||
|
||||
const within = doc.querySelector(options.within);
|
||||
|
||||
if (within) {
|
||||
within.querySelectorAll('a').forEach(link => {
|
||||
if (isExternalLink(link) && !options.ignore(link)) {
|
||||
link.classList.add(options.className);
|
||||
|
||||
const rel = [];
|
||||
if (options.noopener) rel.push('noopener');
|
||||
if (options.noreferrer) rel.push('noreferrer');
|
||||
|
||||
if (rel.length) {
|
||||
link.setAttribute('rel', rel.join(' '));
|
||||
}
|
||||
|
||||
if (options.target) {
|
||||
link.setAttribute('target', options.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return doc;
|
||||
};
|
||||
63
docs/_utilities/highlight-code.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
const Prism = require('prismjs');
|
||||
const PrismLoader = require('prismjs/components/index.js');
|
||||
|
||||
PrismLoader('diff');
|
||||
PrismLoader.silent = true;
|
||||
|
||||
/** Highlights a code string. */
|
||||
function highlight(code, language) {
|
||||
const alias = language.replace(/^diff-/, '');
|
||||
const isDiff = /^diff-/i.test(language);
|
||||
|
||||
// Auto-load the target language
|
||||
if (!Prism.languages[alias]) {
|
||||
PrismLoader(alias);
|
||||
|
||||
if (!Prism.languages[alias]) {
|
||||
throw new Error(`Unsupported language for code highlighting: "${language}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Register diff-* languages to use the diff grammar
|
||||
if (isDiff) {
|
||||
Prism.languages[language] = Prism.languages.diff;
|
||||
}
|
||||
|
||||
return Prism.highlight(code, Prism.languages[language], language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights all code fields that have a language parameter. If the language has a colon in its name, the first chunk
|
||||
* will be the language used and additional chunks will be applied as classes to the `<pre>`. For example, a code field
|
||||
* tagged with "html:preview" will be rendered as `<pre class="language-html preview">`.
|
||||
*
|
||||
* The provided doc should be a document object provided by JSDOM. The same document will be returned with the
|
||||
* appropriate DOM manipulations.
|
||||
*/
|
||||
module.exports = function (doc) {
|
||||
doc.querySelectorAll('pre > code[class]').forEach(code => {
|
||||
// Look for class="language-*" and split colons into separate classes
|
||||
code.classList.forEach(className => {
|
||||
if (className.startsWith('language-')) {
|
||||
//
|
||||
// We use certain suffixes to indicate code previews, expanded states, etc. The class might look something like
|
||||
// this:
|
||||
//
|
||||
// class="language-html:preview:expanded"
|
||||
//
|
||||
// The language will always come first, so we need to drop the "language-" prefix and everything after the first
|
||||
// color to get the highlighter language.
|
||||
//
|
||||
const language = className.replace(/^language-/, '').split(':')[0];
|
||||
|
||||
try {
|
||||
code.innerHTML = highlight(code.textContent ?? '', language);
|
||||
} catch (err) {
|
||||
// Language not found, skip it
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
75
docs/_utilities/markdown.cjs
Normal file
@@ -0,0 +1,75 @@
|
||||
const MarkdownIt = require('markdown-it');
|
||||
const markdownItContainer = require('markdown-it-container');
|
||||
const markdownItIns = require('markdown-it-ins');
|
||||
const markdownItKbd = require('markdown-it-kbd');
|
||||
const markdownItMark = require('markdown-it-mark');
|
||||
const markdownItReplaceIt = require('markdown-it-replace-it');
|
||||
|
||||
const markdown = MarkdownIt({
|
||||
html: true,
|
||||
xhtmlOut: false,
|
||||
breaks: false,
|
||||
langPrefix: 'language-',
|
||||
linkify: false,
|
||||
typographer: false
|
||||
});
|
||||
|
||||
// Third-party plugins
|
||||
markdown.use(markdownItContainer);
|
||||
markdown.use(markdownItIns);
|
||||
markdown.use(markdownItKbd);
|
||||
markdown.use(markdownItMark);
|
||||
markdown.use(markdownItReplaceIt);
|
||||
|
||||
// Callouts
|
||||
['tip', 'warning', 'danger'].forEach(type => {
|
||||
const variant = type === 'tip' ? 'brand' : type;
|
||||
let icon = 'info-circle';
|
||||
if (type === 'warning') icon = 'exclamation-circle';
|
||||
if (type === 'danger') icon = 'exclamation-triangle';
|
||||
|
||||
markdown.use(markdownItContainer, type, {
|
||||
render: function (tokens, idx) {
|
||||
if (tokens[idx].nesting === 1) {
|
||||
return `
|
||||
<wa-alert class="callout" variant="${variant}" open>
|
||||
<wa-icon slot="icon" name="${icon}"></wa-icon>
|
||||
`;
|
||||
}
|
||||
return '</wa-alert>\n';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Asides
|
||||
markdown.use(markdownItContainer, 'aside', {
|
||||
render: function (tokens, idx) {
|
||||
if (tokens[idx].nesting === 1) {
|
||||
return `<aside>`;
|
||||
}
|
||||
return '</aside>\n';
|
||||
}
|
||||
});
|
||||
|
||||
// Details
|
||||
markdown.use(markdownItContainer, 'details', {
|
||||
validate: params => params.trim().match(/^details\s+(.*)$/),
|
||||
render: (tokens, idx) => {
|
||||
const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
|
||||
if (tokens[idx].nesting === 1) {
|
||||
return `<details>\n<summary><span>${markdown.utils.escapeHtml(m[1])}</span></summary>\n`;
|
||||
}
|
||||
return '</details>\n';
|
||||
}
|
||||
});
|
||||
|
||||
// Replace [#1234] with a link to GitHub issues
|
||||
markdownItReplaceIt.replacements.push({
|
||||
name: 'github-issues',
|
||||
re: /\[#([0-9]+)\]/gs,
|
||||
sub: '<a href="https://github.com/shoelace-style/shoelace/issues/$1">#$1</a>',
|
||||
html: true,
|
||||
default: true
|
||||
});
|
||||
|
||||
module.exports = markdown;
|
||||
26
docs/_utilities/prettier.cjs
Normal file
@@ -0,0 +1,26 @@
|
||||
const { format } = require('prettier');
|
||||
|
||||
/** Formats markup using prettier. */
|
||||
module.exports = function (content, options) {
|
||||
options = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSpacing: true,
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
insertPragma: false,
|
||||
bracketSameLine: false,
|
||||
jsxSingleQuote: false,
|
||||
parser: 'html',
|
||||
printWidth: 120,
|
||||
proseWrap: 'preserve',
|
||||
quoteProps: 'as-needed',
|
||||
requirePragma: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'none',
|
||||
useTabs: false,
|
||||
...options
|
||||
};
|
||||
|
||||
return format(content, options);
|
||||
};
|
||||
24
docs/_utilities/replacer.cjs
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @typedef {object} Replacement
|
||||
* @property {string | RegExp} pattern
|
||||
* @property {string} replacement
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Array<Replacement>} Replacements
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Document} content
|
||||
* @param {Replacements} replacements
|
||||
*/
|
||||
module.exports = function (content, replacements) {
|
||||
/** This seems trivial, but by assigning to a string first, THEN using innerHTML after iterating over every replacement, we reduce the calculations of JSDOM. At the time of writing benchmarks show a reduction from 9seconds to 3 seconds by doing so. */
|
||||
let html = content.body.innerHTML;
|
||||
|
||||
replacements.forEach(replacement => {
|
||||
html = html.replaceAll(replacement.pattern, replacement.replacement);
|
||||
});
|
||||
|
||||
content.body.innerHTML = html;
|
||||
};
|
||||
21
docs/_utilities/scrolling-tables.cjs
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM.
|
||||
* The same document will be returned with the appropriate DOM manipulations.
|
||||
*/
|
||||
module.exports = function (doc, options) {
|
||||
const tables = [...doc.querySelectorAll('table')];
|
||||
|
||||
options = {
|
||||
className: 'table-scroll', // the class name to add to the table's container
|
||||
...options
|
||||
};
|
||||
|
||||
tables.forEach(table => {
|
||||
const div = doc.createElement('div');
|
||||
div.classList.add(options.className);
|
||||
table.insertAdjacentElement('beforebegin', div);
|
||||
div.append(table);
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
16
docs/_utilities/strings.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
const slugify = require('slugify');
|
||||
|
||||
/** Creates a slug from an arbitrary string of text. */
|
||||
module.exports.createSlug = function (text) {
|
||||
return slugify(String(text), {
|
||||
remove: /[^\w|\s]/g,
|
||||
lower: true
|
||||
});
|
||||
};
|
||||
|
||||
/** Determines whether or not a link is external. */
|
||||
module.exports.isExternalLink = function (link) {
|
||||
// We use the "internal" hostname when initializing JSDOM so we know that those are local links
|
||||
if (!link.hostname || link.hostname === 'internal') return false;
|
||||
return true;
|
||||
};
|
||||
42
docs/_utilities/table-of-contents.cjs
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Generates an in-page table of contents based on headings.
|
||||
*/
|
||||
module.exports = function (doc, options) {
|
||||
options = {
|
||||
levels: ['h2'], // headings to include (they must have an id)
|
||||
container: 'nav', // the container to append links to
|
||||
listItem: true, // if true, links will be wrapped in <li>
|
||||
within: 'body', // the element containing the headings to summarize
|
||||
...options
|
||||
};
|
||||
|
||||
const container = doc.querySelector(options.container);
|
||||
const within = doc.querySelector(options.within);
|
||||
const headingSelector = options.levels.map(h => `${h}[id]`).join(', ');
|
||||
|
||||
if (!container || !within) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
within.querySelectorAll(headingSelector).forEach(heading => {
|
||||
const listItem = doc.createElement('li');
|
||||
const link = doc.createElement('a');
|
||||
const level = heading.tagName.slice(1);
|
||||
|
||||
link.href = `#${heading.id}`;
|
||||
link.textContent = heading.textContent;
|
||||
|
||||
if (options.listItem) {
|
||||
// List item + link
|
||||
listItem.setAttribute('data-level', level);
|
||||
listItem.append(link);
|
||||
container.append(listItem);
|
||||
} else {
|
||||
// Link only
|
||||
link.setAttribute('data-level', level);
|
||||
container.append(link);
|
||||
}
|
||||
});
|
||||
|
||||
return doc;
|
||||
};
|
||||
23
docs/_utilities/typography.cjs
Normal file
@@ -0,0 +1,23 @@
|
||||
const smartquotes = require('smartquotes');
|
||||
|
||||
smartquotes.replacements.push([/---/g, '\u2014']); // em dash
|
||||
smartquotes.replacements.push([/--/g, '\u2013']); // en dash
|
||||
smartquotes.replacements.push([/\.\.\./g, '\u2026']); // ellipsis
|
||||
smartquotes.replacements.push([/\(c\)/gi, '\u00A9']); // copyright
|
||||
smartquotes.replacements.push([/\(r\)/gi, '\u00AE']); // registered trademark
|
||||
smartquotes.replacements.push([/\?!/g, '\u2048']); // ?!
|
||||
smartquotes.replacements.push([/!!/g, '\u203C']); // !!
|
||||
smartquotes.replacements.push([/\?\?/g, '\u2047']); // ??
|
||||
smartquotes.replacements.push([/([0-9]\s?)-(\s?[0-9])/g, '$1\u2013$2']); // number ranges use en dash
|
||||
|
||||
/**
|
||||
* Improves typography by adding smart quotes and similar corrections within the specified element(s).
|
||||
*
|
||||
* The provided doc should be a document object provided by JSDOM. The same document will be returned with the
|
||||
* appropriate DOM manipulations.
|
||||
*/
|
||||
module.exports = function (doc, selector = 'body') {
|
||||
const elements = [...doc.querySelectorAll(selector)];
|
||||
elements.forEach(el => smartquotes.element(el));
|
||||
return doc;
|
||||
};
|
||||
|
Before Width: | Height: | Size: 1.0 MiB |
11
docs/assets/images/awesome.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="733" xmlns="http://www.w3.org/2000/svg" height="733">
|
||||
<circle cy="366.5" cx="366.5" r="366.5"/>
|
||||
<circle cy="366.5" cx="366.5" r="336.5" fill="#fede58"/>
|
||||
<path d="M325 665c-121-21-194-115-212-233v-8l-25-1-1-18h481c6 13 10 27 13 41 13 94-38 146-114 193-45 23-93 29-142 26z"/>
|
||||
<path d="M372 647c52-6 98-28 138-62 28-25 46-56 51-87 4-20 1-57-5-70l-423-1c-2 56 39 118 74 157 31 34 72 54 116 63 11 2 38 2 49 0z" fill="#871945"/>
|
||||
<path d="M76 342c-13-26-13-57-9-85 6-27 18-52 35-68 21-20 50-23 77-18 15 4 28 12 39 23 18 17 30 40 36 67 4 20 4 41 0 60l-6 21z"/>
|
||||
<path d="M234 323c5-6 6-40 2-58-3-16-4-16-10-10-14 14-38 14-52 0-15-18-12-41 6-55 3-3 5-5 5-6-1-4-22-8-34-7-42 4-57.6 40-66.2 77-3 17-1 53 4 59H234z" fill="#fff"/>
|
||||
<path d="M378 343c-2-3-6-20-7-29-5-28-1-57 11-83 15-30 41-52 72-60 29-7 57 0 82 15 26 17 45 49 50 82 2 12 2 33 0 45-1 10-5 26-8 30z"/>
|
||||
<path d="M565 324c4-5 5-34 4-50-2-14-6-24-8-24-1 0-3 2-6 5-17 17-47 13-58-9-7-16-4-31 8-43 4-4 7-8 7-9 0 0-4-2-8-3-51-17-105 20-115 80-3 15 0 43 3 53z" fill="#fff"/>
|
||||
<path d="M504 590s-46 40-105 53c-66 15-114-7-114-7s14-76 93-95c76-18 126 49 126 49z" fill="#f9bedd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -1,6 +1,6 @@
|
||||
<svg viewBox="0 0 127 141" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill-rule="nonzero" fill="#0ea5e9">
|
||||
<g fill-rule="nonzero" fill="#0070f2">
|
||||
<path d="M102.375,90.85 C102.979,90.557 103.57,90.215 104.15,89.826 L106.425,88.501 C106.848,88.19 107.64,87.573 108.8,86.65 L108.95,86.501 C109.883,85.567 110.916,85.117 112.05,85.15 C112.55,85.15 113.133,85.284 113.8,85.55 L122.3,78 C122.533,77.8 122.767,77.633 123,77.5 L123.05,77.5 C123.95,76.967 124.7,77 125.3,77.6 C126.367,78.166 126.45,79.133 125.55,80.5 L116.65,88.399 C116.717,88.533 116.75,88.666 116.75,88.799 C116.883,89.399 116.833,89.999 116.6,90.599 C116.4,90.999 116.117,91.382 115.75,91.749 C115.379,92.145 114.996,92.528 114.6,92.898 L109.45,96.648 C108.99,96.97 108.523,97.27 108.05,97.548 C107.46,97.906 106.86,98.232 106.25,98.523 C105.985,98.644 105.718,98.76 105.45,98.874 C103.841,99.559 102.174,100.017 100.45,100.249 C99.65,106.982 97.35,113.183 93.55,118.849 C88.75,125.915 82.183,131.299 73.85,134.999 C65.55,138.699 56.567,140.549 46.9,140.549 C33.567,140.415 22.75,137.415 14.45,131.549 C4.817,124.75 0,115.7 0,104.399 L0,102.849 C0.333,95.149 2.6,87.849 6.8,80.95 C10.933,74.116 16.983,69.5 24.95,67.1 C29.05,65.9 33.166,65.3 37.3,65.3 C41.567,65.3 45.967,66.083 50.5,67.65 C55.033,69.216 60.15,71.916 65.85,75.75 L80.7,85.7 C84.833,88.399 88.6,90.233 92,91.2 C91.3,84.3 88.8,78.399 84.5,73.5 C80.2,68.533 74.717,64.9 68.05,62.6 L61.65,60.4 C55.783,58.333 51.417,56.3 48.55,54.3 C40.817,49.067 36.517,41.883 35.65,32.75 L35.5,30.05 C35.5,21.25 39.067,13.883 46.2,7.95 C52.567,2.65 60.133,0 68.9,0 C75.5,0 81.417,1.9 86.65,5.7 C91.917,9.533 94.9,14.967 95.6,22 L95.75,24.75 C95.75,29.85 94.433,34.216 91.8,37.85 C89.1,41.483 86.717,43.3 84.65,43.3 C84.21,43.269 83.802,43.21 83.425,43.125 L74.1,51.9 C72.6,52.733 71.583,52.583 71.05,51.45 C70.517,50.85 70.567,50.1 71.2,49.2 L71.25,49.15 C71.383,48.95 71.567,48.733 71.8,48.5 L80.475,40.275 C80.376,39.872 80.318,39.431 80.3,38.95 C80.3,37.817 80.75,36.867 81.65,36.1 C85.45,32.7 87.35,28.784 87.35,24.35 C87.35,19.95 85.683,16.25 82.35,13.25 C79.017,10.25 74.467,8.716 68.7,8.65 C61.5,8.65 55.583,10.817 50.95,15.15 C46.317,19.483 44,24.683 44,30.75 C44,35.65 45.883,40.066 49.65,44 C53.383,47.9 59.15,50.966 66.95,53.2 C77.883,56.367 86.233,61.7 92,69.2 C97.133,75.833 100,83.283 100.6,91.55 C101.199,91.365 101.791,91.132 102.375,90.85 Z M71.95,49.05 C71.95,49.35 72.117,49.5 72.45,49.5 C72.483,49.5 75.117,47.066 80.35,42.2 C80.35,41.533 78.95,42.45 76.15,44.95 C73.35,47.483 71.95,48.85 71.95,49.05 Z M74.15,50.8 C74.15,50.533 74.017,50.4 73.75,50.4 C73.45,50.4 73.183,50.55 72.95,50.85 C72.416,50.817 72.033,50.884 71.8,51.05 C71.7,51.117 71.65,51.183 71.65,51.25 C71.65,51.45 71.783,51.6 72.05,51.7 L72.95,51.7 C73.75,51.4 74.15,51.1 74.15,50.8 Z M80.35,45.35 C80.35,44.583 79.9,44.583 79,45.35 C78.567,45.75 78.017,46.317 77.35,47.05 C77.117,47.217 76.633,47.667 75.9,48.4 C75.133,49.2 74.75,49.683 74.75,49.85 L74.8,50.2 C75,50.267 75.133,50.3 75.2,50.3 C75.233,50.3 76.1,49.5 77.8,47.9 C79.5,46.3 80.35,45.45 80.35,45.35 Z M124.2,78.3 L115.8,85.7 C116.3,85.967 116.667,86.349 116.9,86.849 L125.2,79.45 C125.266,79.116 125.217,78.849 125.05,78.649 C124.883,78.517 124.6,78.399 124.2,78.3 Z M123.75,78.05 L123.55,77.85 L116.15,83.85 L116.6,84.4 L123.75,78.05 Z M91.85,99.899 C89.65,99.333 87.617,98.649 85.75,97.849 C81.55,96.149 76.333,93.183 70.1,88.95 L59.85,82 C55.517,79.233 51.567,77.166 48,75.8 C44.4,74.467 40.867,73.8 37.4,73.8 L35.9,73.8 C27.067,74.267 20.2,77.583 15.3,83.75 C10.734,89.45 8.45,96.184 8.45,103.95 C8.45,112.683 11.95,119.516 18.95,124.45 C25.916,129.416 35.467,131.899 47.6,131.899 C57.133,131.899 65.166,130.2 71.7,126.799 C78.2,123.399 83.25,118.899 86.85,113.299 C89.55,109.033 91.217,104.566 91.85,99.899 Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
32
docs/assets/images/undraw-taken.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg width="673" height="739" viewBox="0 0 673 739" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill-rule="nonzero" fill="none">
|
||||
<path d="M467 149.805c-46.62-7.44-99.71-11.41-155-11.41-50.6 0-99.35 3.32-142.98 9.58.01-.67.02-1.34.05-2.01C171.475 65.082 237.996.903 318.914 1.398c80.917.494 146.649 65.48 148.066 146.387.01.68.02 1.35.02 2.02Z" fill="#0BA5E9"/>
|
||||
<path d="M337.55 1.342a149.047 149.047 0 0 0-168.93 143.229c-.031.67-.04 1.34-.05 2.01 12.961-1.86 26.384-3.454 40.164-4.784 3.478-71.745 57.64-130.8 128.816-140.455Z" opacity=".1" fill="#FFF"/>
|
||||
<path d="M532.18 161.625a600.121 600.121 0 0 0-65.2-13.84 943.364 943.364 0 0 0-108.74-10.45 1133.608 1133.608 0 0 0-83.01-.34 973.29 973.29 0 0 0-106.16 8.97 624.292 624.292 0 0 0-77.25 15.66C32.61 177.995 0 199.935 0 223.395s32.61 45.4 91.82 61.77c41.64 11.52 92.98 19.37 148.92 22.97 23.09 1.5 46.96 2.26 71.26 2.26 24.38 0 48.33-.77 71.49-2.27 50.91-3.29 98.01-10.1 137.43-20 .21-.06.41-.11.62-.16 2.66-.66 5.28-1.35 7.87-2.04.93-.26 1.85-.51 2.77-.76a.978.978 0 0 1 .16-.05c.88-.24 1.75-.49 2.62-.73 1.74-.5 3.46-.99 5.15-1.5.08-.02.15-.04.22-.06 1.47-.44 2.91-.88 4.34-1.32 1.17-.37 2.33-.73 3.48-1.1.84-.27 1.67-.54 2.49-.81.6-.2 1.19-.39 1.77-.59.79-.26 1.58-.53 2.36-.8.33-.11.66-.22.98-.34.75-.25 1.48-.51 2.21-.77.79-.28 1.58-.57 2.36-.85.65-.23 1.3-.47 1.94-.71.54-.21 1.07-.41 1.61-.61 1.47-.55 2.91-1.12 4.33-1.68.71-.29 1.42-.57 2.12-.86.69-.28 1.39-.57 2.07-.86 1.12-.47 2.22-.94 3.3-1.41.52-.24 1.05-.47 1.56-.69.39-.18.77-.35 1.16-.53.28-.12.56-.25.83-.38 1.01-.46 2.01-.93 2.99-1.4 3.76-1.8 7.27-3.64 10.53-5.52 20.45-11.71 31.24-24.7 31.24-38.2 0-23.46-32.61-45.4-91.82-61.77Zm-.54 121.62c-41.69 11.53-93.17 19.38-149.26 22.95-22.81 1.45-46.39 2.2-70.38 2.2-23.91 0-47.41-.74-70.15-2.19-56.18-3.56-107.74-11.41-149.49-22.96C34.09 267.125 2 245.875 2 223.395c0-1.986.252-3.965.74-5.89 5.1-20.28 36.47-39.26 89.62-53.96a623.806 623.806 0 0 1 76.66-15.57 976.027 976.027 0 0 1 106.8-9c11.92-.39 23.98-.583 36.18-.58 15.41 0 30.65.31 45.63.91a941.367 941.367 0 0 1 109.37 10.5 598.858 598.858 0 0 1 64.64 13.74c53.14 14.7 84.5 33.67 89.61 53.94.496 1.93.75 3.916.75 5.91 0 22.48-32.09 43.73-90.36 59.85Z" fill="#3F3D56"/>
|
||||
<path d="M623.43 224.305c0 13.36-11.01 26-30.67 37.29-3.27 1.88-6.79 3.72-10.53 5.52-.98.47-1.98.94-2.99 1.4-.27.13-.55.26-.83.38-.39.18-.77.35-1.16.53-.51.22-1.04.45-1.56.69-1.08.47-2.18.94-3.3 1.41-.68.29-1.38.58-2.07.86-.7.29-1.41.57-2.12.86-1.42.56-2.86 1.13-4.33 1.68-.54.2-1.07.4-1.61.61-.64.24-1.29.48-1.94.71-.78.28-1.57.57-2.36.85-.73.26-1.46.52-2.21.77-.32.12-.65.23-.98.34-.78.27-1.57.54-2.36.8-.58.2-1.17.39-1.77.59-.82.27-1.65.54-2.49.81-1.15.37-2.31.73-3.48 1.1-1.43.44-2.87.88-4.34 1.32-.07.02-.14.04-.22.06-1.69.51-3.41 1-5.15 1.5-.87.24-1.74.49-2.62.73a.978.978 0 0 0-.16.05c-.92.25-1.84.5-2.77.76-2.58.68-5.21 1.37-7.87 2.04-.21.05-.41.1-.62.16-38.35 9.58-85.4 16.56-137.47 19.93-22.81 1.47-46.59 2.25-71.02 2.25-24.65 0-48.63-.79-71.62-2.29-137.24-8.95-239.38-43.03-239.38-83.71.01-2.475.388-4.936 1.12-7.3.06.17.12.33.19.5 14.27 37.48 115.54 67.77 246.94 75.16 20.13 1.14 40.98 1.73 62.32 1.73 21.43 0 42.36-.6 62.57-1.74 131.29-7.42 232.46-37.72 246.68-75.17.24-.6.45-1.2.63-1.8a25.304 25.304 0 0 1 1.55 8.62ZM91.67 213.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217Z" fill="#3F3D56"/>
|
||||
<path d="M162.67 260.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217ZM531.67 213.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217ZM460.67 260.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217ZM311.67 282.54c-16.643 0-34.331-3.58-34.331-10.216 0-6.636 17.688-10.217 34.33-10.217 16.643 0 34.331 3.58 34.331 10.217 0 6.636-17.688 10.217-34.33 10.217Zm0-18.433c-19.054 0-32.331 4.33-32.331 8.217 0 3.886 13.277 8.217 32.33 8.217 19.053 0 32.331-4.33 32.331-8.217 0-3.886-13.278-8.217-32.33-8.217Z" fill="#3F3D56"/>
|
||||
<circle fill="#2F2E41" cx="336.978" cy="450.705" r="42.012"/>
|
||||
<path fill="#2F2E41" d="m300.555 488.547 20.447-10.24 5.715 11.413-20.447 10.24z"/>
|
||||
<ellipse fill="#2F2E41" transform="rotate(-56.601 300.086 492.946)" cx="300.086" cy="492.946" rx="3.989" ry="10.636"/>
|
||||
<path fill="#2F2E41" d="m347.239 489.72 5.715-11.412 20.447 10.24-5.715 11.412z"/>
|
||||
<ellipse fill="#2F2E41" transform="rotate(-33.399 373.87 492.946)" cx="373.87" cy="492.946" rx="10.636" ry="3.989"/>
|
||||
<circle fill="#FFF" cx="334.037" cy="440.429" r="14.359"/>
|
||||
<ellipse fill="#3F3D56" transform="rotate(-45 334.135 434.282)" cx="334.135" cy="434.282" rx="4.766" ry="4.8"/>
|
||||
<path d="M370.12 405c.632-15.553-12.773-28.727-29.941-29.425-17.168-.697-31.597 11.346-32.229 26.9-.632 15.553 11.302 19.087 28.47 19.785 17.167.697 33.068-1.706 33.7-17.26Z" fill="#0BA5E9"/>
|
||||
<ellipse fill="#2F2E41" transform="rotate(-40.645 380.654 456.766)" cx="380.654" cy="456.766" rx="6.594" ry="21.006"/>
|
||||
<ellipse fill="#2F2E41" transform="rotate(-49.355 293.42 456.766)" cx="293.419" cy="456.766" rx="21.006" ry="6.594"/>
|
||||
<path d="M348.517 467.262a9.572 9.572 0 1 1-18.836 3.428l-.003-.018c-.942-5.202 3.08-7.043 8.282-7.985 5.203-.942 9.615-.628 10.557 4.575Z" fill="#FFF"/>
|
||||
<path d="M266 495.395a2 2 0 0 1-2-2v-118a2 2 0 0 1 4 0v118a2 2 0 0 1-2 2ZM236 601.395a2 2 0 0 1-2-2v-86a2 2 0 0 1 4 0v86a2 2 0 0 1-2 2ZM313 530.395a2 2 0 0 1-2-2v-118a2 2 0 0 1 4 0v118a2 2 0 0 1-2 2ZM284 615.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM325 369.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM225 390.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM399 395.395a2 2 0 0 1-2-2v-48a2 2 0 0 1 4 0v48a2 2 0 0 1-2 2ZM395 545.395a2 2 0 0 1-2-2v-58a2 2 0 0 1 4 0v58a2 2 0 0 1-2 2ZM355 596.395a2 2 0 0 1-2-2v-86a2 2 0 0 1 4 0v86a2 2 0 0 1-2 2ZM363 449.395a2 2 0 0 1-2-2v-118a2 2 0 0 1 4 0v118a2 2 0 0 1-2 2Z" fill="#CCC"/>
|
||||
<ellipse fill="#2F2E41" transform="rotate(-39.938 594.37 683.981)" cx="594.369" cy="683.981" rx="6.76" ry="21.534"/>
|
||||
<circle fill="#2F2E41" transform="rotate(-71.565 548.562 676.503)" cx="548.562" cy="676.503" r="43.067"/>
|
||||
<path fill="#2F2E41" d="M553.707 710.303h13.084v23.442h-13.084zM527.54 710.303h13.084v23.442H527.54z"/>
|
||||
<ellipse fill="#2F2E41" cx="555.888" cy="734.017" rx="10.903" ry="4.089"/>
|
||||
<ellipse fill="#2F2E41" cx="529.72" cy="733.472" rx="10.903" ry="4.089"/>
|
||||
<path d="M535.04 622.366c3.845-15.487 20.82-24.6 37.914-20.356 17.094 4.245 27.834 20.24 23.989 35.727-3.846 15.487-16.604 15.537-33.698 11.293-17.094-4.245-32.051-11.177-28.206-26.664Z" fill="#0BA5E9"/>
|
||||
<ellipse fill="#2F2E41" transform="rotate(-64.626 500.054 656.52)" cx="500.054" cy="656.52" rx="6.76" ry="21.534"/>
|
||||
<circle fill="#FFF" cx="542.124" cy="667.416" r="14.359"/>
|
||||
<circle fill="#3F3D56" cx="536.222" cy="662.269" r="4.786"/>
|
||||
<circle fill="#FFF" cx="542" cy="697.395" r="6"/>
|
||||
<path d="M671.531 738.395h-236a1 1 0 1 1 0-2h236a1 1 0 0 1 0 2Z" fill="#3F3D56"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -1,225 +0,0 @@
|
||||
.code-block {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: var(--sl-color-neutral-50);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.code-block__preview {
|
||||
position: relative;
|
||||
border: solid 1px var(--sl-color-neutral-200);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
background-color: var(--sl-color-neutral-0);
|
||||
min-width: 20rem;
|
||||
max-width: 100%;
|
||||
padding: 1.5rem 3.25rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Block the preview while dragging to prevent iframes from intercepting drag events */
|
||||
.code-block__preview--dragging:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.code-block__resizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1.75rem;
|
||||
font-size: 20px;
|
||||
color: var(--sl-color-neutral-600);
|
||||
background-color: var(--sl-color-neutral-0);
|
||||
border-left: solid 1px var(--sl-color-neutral-200);
|
||||
border-top-right-radius: 3px;
|
||||
cursor: ew-resize;
|
||||
transition: 250ms background-color;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.code-block__preview {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.code-block__resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block__source {
|
||||
border: solid 1px var(--sl-color-neutral-200);
|
||||
border-bottom: none;
|
||||
border-radius: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.code-block--expanded .code-block__source {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.code-block__source pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-block__buttons {
|
||||
position: relative;
|
||||
border: solid 1px var(--sl-color-neutral-200);
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.code-block__button {
|
||||
flex: 0 0 auto;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: var(--sl-color-neutral-0);
|
||||
font: inherit;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: var(--sl-color-neutral-600);
|
||||
padding: 0 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-block__button:not(:last-of-type) {
|
||||
border-right: solid 1px var(--sl-color-neutral-200);
|
||||
}
|
||||
|
||||
.code-block__button--html,
|
||||
.code-block__button--react {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.code-block__button--selected {
|
||||
font-weight: 700;
|
||||
color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
.code-block__button--codepen {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.code-block__button:first-of-type {
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.code-block__button:last-of-type {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.code-block__button:hover,
|
||||
.code-block__button:active {
|
||||
box-shadow: 0 0 0 1px var(--sl-color-primary-400);
|
||||
border-right-color: transparent;
|
||||
background-color: var(--sl-color-primary-50);
|
||||
color: var(--sl-color-primary-600);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.code-block__button:focus-visible {
|
||||
outline: none;
|
||||
outline: var(--sl-focus-ring);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.code-block__toggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: var(--sl-color-neutral-600);
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.code-block__toggle svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.code-block--expanded .code-block__toggle svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Copy button styles */
|
||||
.markdown-section .docsify-copy-code-button {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background-color: var(--sl-color-neutral-500);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-x-small);
|
||||
font-weight: var(--sl-font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
padding: 8px;
|
||||
user-select: none;
|
||||
transition: 0.1s all;
|
||||
}
|
||||
|
||||
.markdown-section .docsify-copy-code-button.copied {
|
||||
animation: pulse 0.75s;
|
||||
--pulse-color: var(--sl-color-neutral-600);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 var(--pulse-color);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 0.5rem transparent;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-section .docsify-copy-code-button .label {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.markdown-section .docsify-copy-code-button .success,
|
||||
.markdown-section .docsify-copy-code-button .error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-section .docsify-copy-code-button:focus-visible {
|
||||
outline: var(--sl-focus-ring);
|
||||
outline-offset: var(--sl-focus-ring-offset);
|
||||
}
|
||||
|
||||
.markdown-section .docsify-copy-code-button:active {
|
||||
background-color: var(--sl-color-neutral-600);
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
/* We can apply data-flavor="html|react" to any element on the page to toggle it when the user's flavor changes */
|
||||
body.flavor-html [data-flavor]:not([data-flavor='html']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.flavor-react [data-flavor]:not([data-flavor='react']) {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
/* global Prism */
|
||||
|
||||
(() => {
|
||||
const reactVersion = '17.0.2';
|
||||
let flavor = getFlavor();
|
||||
let count = 1;
|
||||
|
||||
// Sync flavor UI on page load
|
||||
setFlavor(getFlavor());
|
||||
|
||||
if (!window.$docsify) {
|
||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||
}
|
||||
|
||||
function convertModuleLinks(html) {
|
||||
const version = sessionStorage.getItem('sl-version');
|
||||
|
||||
html = html
|
||||
.replace(/@shoelace-style\/shoelace/g, `https://cdn.skypack.dev/@shoelace-style/shoelace@${version}`)
|
||||
.replace(/from 'react'/g, `from 'https://cdn.skypack.dev/react@${reactVersion}'`)
|
||||
.replace(/from "react"/g, `from "https://cdn.skypack.dev/react@${reactVersion}"`);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function getAdjacentExample(name, pre) {
|
||||
let currentPre = pre.nextElementSibling;
|
||||
|
||||
while (currentPre?.tagName.toLowerCase() === 'pre') {
|
||||
if (currentPre?.getAttribute('data-lang').split(' ').includes(name)) {
|
||||
return currentPre;
|
||||
}
|
||||
|
||||
currentPre = currentPre.nextElementSibling;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function runScript(script) {
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
if (script.type === 'module') {
|
||||
newScript.type = 'module';
|
||||
newScript.textContent = script.innerHTML;
|
||||
} else {
|
||||
newScript.appendChild(document.createTextNode(`(() => { ${script.innerHTML} })();`));
|
||||
}
|
||||
|
||||
script.parentNode.replaceChild(newScript, script);
|
||||
}
|
||||
|
||||
function getFlavor() {
|
||||
return localStorage.getItem('flavor') || 'html';
|
||||
}
|
||||
|
||||
function setFlavor(newFlavor) {
|
||||
flavor = ['html', 'react'].includes(newFlavor) ? newFlavor : 'html';
|
||||
localStorage.setItem('flavor', flavor);
|
||||
|
||||
// Set the flavor class on the body
|
||||
document.body.classList.toggle('flavor-html', flavor === 'html');
|
||||
document.body.classList.toggle('flavor-react', flavor === 'react');
|
||||
}
|
||||
|
||||
window.$docsify.plugins.push(hook => {
|
||||
// Convert code blocks to previews
|
||||
hook.afterEach((html, next) => {
|
||||
const domParser = new DOMParser();
|
||||
const doc = domParser.parseFromString(html, 'text/html');
|
||||
|
||||
const htmlButton = `
|
||||
<button
|
||||
type="button"
|
||||
title="Show HTML code"
|
||||
class="code-block__button code-block__button--html ${flavor === 'html' ? 'code-block__button--selected' : ''}"
|
||||
>
|
||||
HTML
|
||||
</button>
|
||||
`;
|
||||
|
||||
const reactButton = `
|
||||
<button
|
||||
type="button"
|
||||
title="Show React code"
|
||||
class="code-block__button code-block__button--react ${
|
||||
flavor === 'react' ? 'code-block__button--selected' : ''
|
||||
}"
|
||||
>
|
||||
React
|
||||
</button>
|
||||
`;
|
||||
|
||||
const codePenButton = `
|
||||
<button type="button" class="code-block__button code-block__button--codepen" title="Edit on CodePen">
|
||||
<svg
|
||||
width="138"
|
||||
height="26"
|
||||
viewBox="0 0 138 26"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
[...doc.querySelectorAll('code[class^="lang-"]')].forEach(code => {
|
||||
if (code.classList.contains('preview')) {
|
||||
const isExpanded = code.classList.contains('expanded');
|
||||
const pre = code.closest('pre');
|
||||
const sourceGroupId = `code-block-source-group-${count}`;
|
||||
const toggleId = `code-block-toggle-${count}`;
|
||||
const reactPre = getAdjacentExample('react', pre);
|
||||
const hasReact = reactPre !== null;
|
||||
|
||||
pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, ''));
|
||||
pre.setAttribute('aria-labelledby', toggleId);
|
||||
|
||||
const codeBlock = `
|
||||
<div class="code-block ${isExpanded ? 'code-block--expanded' : ''}">
|
||||
<div class="code-block__preview">
|
||||
${code.textContent}
|
||||
<div class="code-block__resizer">
|
||||
<sl-icon name="grip-vertical"></sl-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-block__source-group" id="${sourceGroupId}">
|
||||
<div class="code-block__source code-block__source--html" ${hasReact ? 'data-flavor="html"' : ''}>
|
||||
${pre.outerHTML}
|
||||
</div>
|
||||
|
||||
${
|
||||
hasReact
|
||||
? `
|
||||
<div class="code-block__source code-block__source--react" data-flavor="react">
|
||||
${reactPre.outerHTML}
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="code-block__buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="code-block__button code-block__toggle"
|
||||
aria-expanded="${isExpanded ? 'true' : 'false'}"
|
||||
aria-controls="${sourceGroupId}"
|
||||
>
|
||||
Source
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
${hasReact ? ` ${htmlButton} ${reactButton} ` : ''}
|
||||
|
||||
${!code.classList.contains('no-codepen') ? codePenButton : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body);
|
||||
reactPre?.remove();
|
||||
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
// Force the highlighter to run again so JSX fields get highlighted properly
|
||||
requestAnimationFrame(() => Prism.highlightAll());
|
||||
|
||||
next(doc.body.innerHTML);
|
||||
});
|
||||
|
||||
// After the page is done loading, force scripts in previews to execute
|
||||
hook.doneEach(() => {
|
||||
[...document.querySelectorAll('.code-block__preview script')].map(script => runScript(script));
|
||||
});
|
||||
|
||||
// Horizontal resizing
|
||||
hook.doneEach(() => {
|
||||
[...document.querySelectorAll('.code-block__preview')].forEach(preview => {
|
||||
const resizer = preview.querySelector('.code-block__resizer');
|
||||
let startX;
|
||||
let startWidth;
|
||||
|
||||
function dragStart(event) {
|
||||
startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
|
||||
startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
|
||||
preview.classList.add('code-block__preview--dragging');
|
||||
event.preventDefault();
|
||||
document.documentElement.addEventListener('mousemove', dragMove);
|
||||
document.documentElement.addEventListener('touchmove', dragMove);
|
||||
document.documentElement.addEventListener('mouseup', dragStop);
|
||||
document.documentElement.addEventListener('touchend', dragStop);
|
||||
}
|
||||
|
||||
function dragMove(event) {
|
||||
setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX);
|
||||
}
|
||||
|
||||
function dragStop() {
|
||||
preview.classList.remove('code-block__preview--dragging');
|
||||
document.documentElement.removeEventListener('mousemove', dragMove);
|
||||
document.documentElement.removeEventListener('touchmove', dragMove);
|
||||
document.documentElement.removeEventListener('mouseup', dragStop);
|
||||
document.documentElement.removeEventListener('touchend', dragStop);
|
||||
}
|
||||
|
||||
function setWidth(width) {
|
||||
preview.style.width = `${width}px`;
|
||||
}
|
||||
|
||||
resizer.addEventListener('mousedown', dragStart);
|
||||
resizer.addEventListener('touchstart', dragStart, { passive: true });
|
||||
}, false);
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle source mode
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('.code-block__button');
|
||||
const codeBlock = button?.closest('.code-block');
|
||||
|
||||
if (button?.classList.contains('code-block__button--html')) {
|
||||
// Show HTML
|
||||
setFlavor('html');
|
||||
toggleSource(codeBlock, true);
|
||||
} else if (button?.classList.contains('code-block__button--react')) {
|
||||
// Show React
|
||||
setFlavor('react');
|
||||
toggleSource(codeBlock, true);
|
||||
} else if (button?.classList.contains('code-block__toggle')) {
|
||||
// Toggle source
|
||||
toggleSource(codeBlock);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update flavor buttons
|
||||
[...document.querySelectorAll('.code-block')].forEach(cb => {
|
||||
cb.querySelector('.code-block__button--html')?.classList.toggle(
|
||||
'code-block__button--selected',
|
||||
flavor === 'html'
|
||||
);
|
||||
cb.querySelector('.code-block__button--react')?.classList.toggle(
|
||||
'code-block__button--selected',
|
||||
flavor === 'react'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function toggleSource(codeBlock, force) {
|
||||
const toggle = codeBlock.querySelector('.code-block__toggle');
|
||||
|
||||
if (toggle) {
|
||||
codeBlock.classList.toggle('code-block--expanded', force === undefined ? undefined : force);
|
||||
event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-block--expanded'));
|
||||
}
|
||||
}
|
||||
|
||||
// Show pulse when copying
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('.docsify-copy-code-button');
|
||||
if (button) {
|
||||
button.classList.remove('copied');
|
||||
requestAnimationFrame(() => {
|
||||
button.addEventListener('animationend', () => button.classList.remove('copied'), { once: true });
|
||||
button.classList.add('copied');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Open in CodePen
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('button');
|
||||
const version = sessionStorage.getItem('sl-version');
|
||||
|
||||
if (button?.classList.contains('code-block__button--codepen')) {
|
||||
const codeBlock = button.closest('.code-block');
|
||||
const htmlExample = codeBlock.querySelector('.code-block__source--html > pre > code')?.textContent;
|
||||
const reactExample = codeBlock.querySelector('.code-block__source--react > pre > code')?.textContent;
|
||||
const isReact = flavor === 'react' && typeof reactExample === 'string';
|
||||
const theme = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDark = theme === 'dark' || (theme === 'auto' && prefersDark);
|
||||
const editors = isReact ? '0010' : '1000';
|
||||
let htmlTemplate = '';
|
||||
let jsTemplate = '';
|
||||
let cssTemplate = '';
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.action = 'https://codepen.io/pen/define';
|
||||
form.method = 'POST';
|
||||
form.target = '_blank';
|
||||
|
||||
// HTML templates
|
||||
if (!isReact) {
|
||||
htmlTemplate =
|
||||
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
|
||||
`\n${htmlExample}`;
|
||||
jsTemplate = '';
|
||||
}
|
||||
|
||||
// React templates
|
||||
if (isReact) {
|
||||
htmlTemplate = '<div id="root"></div>';
|
||||
jsTemplate =
|
||||
`import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` +
|
||||
`import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` +
|
||||
`import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/utilities/base-path';\n` +
|
||||
`\n` +
|
||||
`// Set the base path for Shoelace assets\n` +
|
||||
`setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/')\n` +
|
||||
`\n${convertModuleLinks(reactExample)}\n` +
|
||||
`\n` +
|
||||
`ReactDOM.render(<App />, document.getElementById('root'));`;
|
||||
}
|
||||
|
||||
// CSS templates
|
||||
cssTemplate =
|
||||
`@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/themes/${
|
||||
isDark ? 'dark' : 'light'
|
||||
}.css';\n` +
|
||||
'\n' +
|
||||
'body {\n' +
|
||||
' font: 16px sans-serif;\n' +
|
||||
' background-color: var(--sl-color-neutral-0);\n' +
|
||||
' color: var(--sl-color-neutral-900);\n' +
|
||||
' padding: 1rem;\n' +
|
||||
'}';
|
||||
|
||||
// Docs: https://blog.codepen.io/documentation/prefill/
|
||||
const data = {
|
||||
title: '',
|
||||
description: '',
|
||||
tags: ['shoelace', 'web components'],
|
||||
editors,
|
||||
head: `<meta name="viewport" content="width=device-width">`,
|
||||
html_classes: `sl-theme-${isDark ? 'dark' : 'light'}`,
|
||||
css_external: ``,
|
||||
js_external: ``,
|
||||
js_module: true,
|
||||
js_pre_processor: isReact ? 'babel' : 'none',
|
||||
html: htmlTemplate,
|
||||
css: cssTemplate,
|
||||
js: jsTemplate
|
||||
};
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'data';
|
||||
input.value = JSON.stringify(data);
|
||||
form.append(input);
|
||||
|
||||
document.body.append(form);
|
||||
form.submit();
|
||||
form.remove();
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -1,594 +0,0 @@
|
||||
(() => {
|
||||
const isDev = location.hostname === 'localhost';
|
||||
const isNext = location.hostname === 'next.shoelace.style';
|
||||
const customElements = fetch('/dist/custom-elements.json')
|
||||
.then(res => res.json())
|
||||
.catch(err => console.error(err));
|
||||
|
||||
function createPropsTable(props) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Reflects</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${props
|
||||
.map(prop => {
|
||||
const hasAttribute = !!prop.attribute;
|
||||
const isAttributeDifferent = prop.attribute !== prop.name;
|
||||
let attributeInfo = '';
|
||||
|
||||
if (!hasAttribute) {
|
||||
attributeInfo = `<br><small>(property only)</small>`;
|
||||
} else if (isAttributeDifferent) {
|
||||
attributeInfo = `
|
||||
<br>
|
||||
<sl-tooltip content="This attribute is different from its property">
|
||||
<small>
|
||||
<code class="nowrap">
|
||||
${escapeHtml(prop.attribute)}
|
||||
</code>
|
||||
</small>
|
||||
</sl-tooltip>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<code class="nowrap">${escapeHtml(prop.name)}</code>
|
||||
${attributeInfo}
|
||||
</td>
|
||||
<td>
|
||||
${escapeHtml(prop.description)}
|
||||
</td>
|
||||
<td style="text-align: center;">${
|
||||
prop.reflects ? '<sl-icon label="yes" name="check-lg"></sl-icon>' : ''
|
||||
}</td>
|
||||
<td>${prop.type?.text ? `<code>${escapeHtml(prop.type?.text || '')}</code>` : '-'}</td>
|
||||
<td>${prop.default ? `<code>${escapeHtml(prop.default)}</code>` : '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
|
||||
<tr>
|
||||
<td class="nowrap"><code>updateComplete</code></td>
|
||||
<td>
|
||||
A read-only promise that resolves when the component has
|
||||
<a href="/getting-started/usage?id=component-rendering-and-updating">finished updating</a>.
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createEventsTable(events) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-flavor="html">Name</th>
|
||||
<th data-flavor="react">React Event</th>
|
||||
<th>Description</th>
|
||||
<th>Event Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${events
|
||||
.map(
|
||||
event => `
|
||||
<tr>
|
||||
<td data-flavor="html"><code class="nowrap">${escapeHtml(event.name)}</code></td>
|
||||
<td data-flavor="react"><code class="nowrap">${escapeHtml(event.reactName)}</code></td>
|
||||
<td>${escapeHtml(event.description)}</td>
|
||||
<td>${event.type?.text ? `<code>${escapeHtml(event.type?.text)}` : '-'}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createMethodsTable(methods) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Arguments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${methods
|
||||
.map(
|
||||
method => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(method.name)}()</code></td>
|
||||
<td>${escapeHtml(method.description)}</td>
|
||||
<td>
|
||||
${
|
||||
method.parameters?.length
|
||||
? `
|
||||
<code>${escapeHtml(
|
||||
method.parameters.map(param => `${param.name}: ${param.type?.text || ''}`).join(', ')
|
||||
)}</code>
|
||||
`
|
||||
: '-'
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createSlotsTable(slots) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${slots
|
||||
.map(
|
||||
slot => `
|
||||
<tr>
|
||||
<td class="nowrap">${slot.name ? `<code>${escapeHtml(slot.name)}</code>` : '(default)'}</td>
|
||||
<td>${escapeHtml(slot.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createCustomPropertiesTable(styles) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${styles
|
||||
.map(
|
||||
style => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(style.name)}</code></td>
|
||||
<td>${escapeHtml(style.description)}</td>
|
||||
<td>${style.default ? `<code>${escapeHtml(style.default)}</code>` : ''}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createPartsTable(parts) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${parts
|
||||
.map(
|
||||
part => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(part.name)}</code></td>
|
||||
<td>${escapeHtml(part.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createAnimationsTable(animations) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('metadata-table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${animations
|
||||
.map(
|
||||
animation => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(animation.name)}</code></td>
|
||||
<td>${escapeHtml(animation.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createDependenciesList(targetComponent, allComponents) {
|
||||
const ul = document.createElement('ul');
|
||||
const dependencies = [];
|
||||
|
||||
// Recursively fetch sub-dependencies
|
||||
function getDependencies(tag) {
|
||||
const component = allComponents.find(c => c.tagName === tag);
|
||||
if (!component || !Array.isArray(component.dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.dependencies?.forEach(dependentTag => {
|
||||
if (!dependencies.includes(dependentTag)) {
|
||||
dependencies.push(dependentTag);
|
||||
}
|
||||
getDependencies(dependentTag);
|
||||
});
|
||||
}
|
||||
|
||||
getDependencies(targetComponent);
|
||||
dependencies.sort().forEach(tag => {
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<code><${tag}></code>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
return ul.outerHTML;
|
||||
}
|
||||
|
||||
function escapeHtml(html) {
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
return html
|
||||
.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" rel="noopener noreferrer" target="_blank">$1</a>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
function getAllComponents(metadata) {
|
||||
const allComponents = [];
|
||||
metadata.modules?.forEach(module => {
|
||||
module.declarations?.forEach(declaration => {
|
||||
if (declaration.customElement) {
|
||||
// Generate the dist path based on the src path and attach it to the component
|
||||
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
|
||||
|
||||
allComponents.push(declaration);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return allComponents;
|
||||
}
|
||||
|
||||
function getComponent(metadata, tagName) {
|
||||
return getAllComponents(metadata).find(component => component.tagName === tagName);
|
||||
}
|
||||
|
||||
if (!window.$docsify) {
|
||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||
}
|
||||
|
||||
window.$docsify.plugins.push(hook => {
|
||||
hook.mounted(async () => {
|
||||
const metadata = await customElements;
|
||||
const target = document.querySelector('.app-name');
|
||||
|
||||
// Add version
|
||||
const version = document.createElement('div');
|
||||
version.classList.add('sidebar-version');
|
||||
version.textContent = isDev ? 'Development' : isNext ? 'Next' : metadata.package.version;
|
||||
target.appendChild(version);
|
||||
|
||||
// Store version for reuse
|
||||
sessionStorage.setItem('sl-version', metadata.package.version);
|
||||
|
||||
// Add repo buttons
|
||||
const buttons = document.createElement('div');
|
||||
buttons.classList.add('sidebar-buttons');
|
||||
buttons.innerHTML = `
|
||||
<sl-button size="small" class="repo-button repo-button--sponsor" href="https://github.com/sponsors/claviska" target="_blank">
|
||||
<sl-icon slot="prefix" name="heart"></sl-icon> Sponsor
|
||||
</sl-button>
|
||||
<sl-button size="small" class="repo-button repo-button--github" href="https://github.com/shoelace-style/shoelace/stargazers" target="_blank">
|
||||
<sl-icon slot="prefix" name="github"></sl-icon> Star
|
||||
</sl-button>
|
||||
<sl-button size="small" class="repo-button repo-button--twitter" href="https://twitter.com/shoelace_style" target="_blank">
|
||||
<sl-icon slot="prefix" name="twitter"></sl-icon> Follow
|
||||
</sl-button>
|
||||
`;
|
||||
target.appendChild(buttons);
|
||||
});
|
||||
|
||||
hook.beforeEach(async (content, next) => {
|
||||
const metadata = await customElements;
|
||||
|
||||
// Replace %VERSION% placeholders
|
||||
content = content.replace(/%VERSION%/g, metadata.package.version);
|
||||
|
||||
// Handle [component-header] tags
|
||||
content = content.replace(/\[component-header:([a-z-]+)\]/g, (match, tag) => {
|
||||
const component = getComponent(metadata, tag);
|
||||
let result = '';
|
||||
|
||||
if (!component) {
|
||||
console.error(`Component not found in metadata: ${tag}`);
|
||||
return next(content);
|
||||
}
|
||||
|
||||
let badgeType = 'neutral';
|
||||
if (component.status === 'stable') {
|
||||
badgeType = 'primary';
|
||||
}
|
||||
if (component.status === 'experimental') {
|
||||
badgeType = 'warning';
|
||||
}
|
||||
if (component.status === 'planned') {
|
||||
badgeType = 'neutral';
|
||||
}
|
||||
if (component.status === 'deprecated') {
|
||||
badgeType = 'danger';
|
||||
}
|
||||
|
||||
result += `
|
||||
<div class="component-header">
|
||||
<div class="component-header__tag">
|
||||
<code><${component.tagName}> | ${component.title ?? component.name}</code>
|
||||
</div>
|
||||
|
||||
<div class="component-header__info">
|
||||
<sl-badge variant="neutral" pill>
|
||||
Since ${component.since || '?'}
|
||||
</sl-badge>
|
||||
|
||||
<sl-badge variant="${badgeType}" pill style="text-transform: capitalize;">
|
||||
${component.status}
|
||||
</sl-badge>
|
||||
</div>
|
||||
|
||||
<div class="component-header__summary">
|
||||
${component.summary ? `<p>${marked(component.summary)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return result.replace(/^ +| +$/gm, '');
|
||||
});
|
||||
|
||||
// Handle [component-metadata] tags
|
||||
content = content.replace(/\[component-metadata:([a-z-]+)\]/g, (match, tag) => {
|
||||
const component = getComponent(metadata, tag);
|
||||
let result = '';
|
||||
|
||||
if (!component) {
|
||||
console.error(`Component not found in metadata: ${tag}`);
|
||||
return next(content);
|
||||
}
|
||||
|
||||
// Remove members that are private or don't have a description
|
||||
const members = component.members?.filter(member => member.description && member.privacy !== 'private');
|
||||
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
|
||||
const props = members?.filter(prop => {
|
||||
// Look for a corresponding attribute
|
||||
const attribute = component.attributes?.find(attr => attr.fieldName === prop.name);
|
||||
if (attribute) {
|
||||
prop.attribute = attribute.name || attribute.fieldName;
|
||||
}
|
||||
|
||||
return prop.kind === 'field' && prop.privacy !== 'private';
|
||||
});
|
||||
|
||||
if (component.path) {
|
||||
/* prettier-ignore */
|
||||
result += `
|
||||
## Importing
|
||||
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="script">Script</sl-tab>
|
||||
<sl-tab slot="nav" panel="import">Import</sl-tab>
|
||||
<sl-tab slot="nav" panel="bundler">Bundler</sl-tab>
|
||||
<sl-tab slot="nav" panel="react">React</sl-tab>
|
||||
|
||||
<sl-tab-panel name="script">\n
|
||||
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace) using a script tag:
|
||||
|
||||
\`\`\`html
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/dist/${component.path}"></script>
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
|
||||
<sl-tab-panel name="import">\n
|
||||
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace) using a JavaScript import:
|
||||
|
||||
\`\`\`js
|
||||
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/dist/${component.path}';
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
|
||||
<sl-tab-panel name="bundler">\n
|
||||
To import this component using [a bundler](/getting-started/installation#bundling):
|
||||
\`\`\`js
|
||||
import '@shoelace-style/shoelace/dist/${component.path}';
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
|
||||
<sl-tab-panel name="react">\n
|
||||
To import this component as a [React component](/frameworks/react):
|
||||
\`\`\`js
|
||||
import { ${component.name} } from '@shoelace-style/shoelace/dist/react';
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
|
||||
<div class="sponsor-callout">
|
||||
<p>
|
||||
Shoelace is designed, developed, and maintained by <a href="https://twitter.com/claviska" target="_blank">Cory LaViska</a>.
|
||||
Please sponsor my open source work on GitHub. Your support will keep this project alive and growing!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<sl-button class="repo-button repo-button--sponsor" href="https://github.com/sponsors/claviska" target="_blank">
|
||||
<sl-icon slot="prefix" name="heart"></sl-icon> Sponsor <span class="sponsor-callout__secondary-label">Development</span>
|
||||
</sl-button>
|
||||
|
||||
<sl-button class="repo-button repo-button--github" href="https://github.com/shoelace-style/shoelace/stargazers" target="_blank">
|
||||
<sl-icon slot="prefix" name="github"></sl-icon> Star <span class="sponsor-callout__secondary-label">on GitHub</span>
|
||||
</sl-button>
|
||||
|
||||
<sl-button class="repo-button repo-button--twitter" href="https://twitter.com/shoelace_style" target="_blank">
|
||||
<sl-icon slot="prefix" name="twitter"></sl-icon> Follow <span class="sponsor-callout__secondary-label">on Twitter</span>
|
||||
</sl-button>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.slots?.length) {
|
||||
result += `
|
||||
## Slots
|
||||
${createSlotsTable(component.slots)}
|
||||
|
||||
_Learn more about [using slots](/getting-started/usage#slots)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (props?.length) {
|
||||
result += `
|
||||
## Attributes & Properties
|
||||
${createPropsTable(props)}
|
||||
|
||||
_Learn more about [attributes and properties](/getting-started/usage#properties)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.events?.length) {
|
||||
result += `
|
||||
## Events
|
||||
${createEventsTable(component.events)}
|
||||
|
||||
_Learn more about [listening to events](/getting-started/usage#events)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (methods?.length) {
|
||||
result += `
|
||||
## Methods
|
||||
|
||||
${createMethodsTable(methods)}
|
||||
|
||||
_Learn more about [calling methods](/getting-started/usage#methods)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.cssProperties?.length) {
|
||||
result += `
|
||||
## CSS Custom Properties
|
||||
${createCustomPropertiesTable(component.cssProperties)}
|
||||
|
||||
_Learn more about [customizing CSS Custom Properties](/getting-started/customizing#custom-properties)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.cssParts?.length) {
|
||||
result += `
|
||||
## CSS Parts
|
||||
${createPartsTable(component.cssParts)}
|
||||
|
||||
_Learn more about [customizing CSS Parts](/getting-started/customizing#component-parts)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.animations?.length) {
|
||||
result += `
|
||||
## Animations
|
||||
${createAnimationsTable(component.animations)}
|
||||
|
||||
_Learn more about [customizing animations](/getting-started/customizing#animations)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.dependencies?.length) {
|
||||
result += `
|
||||
## Dependencies
|
||||
|
||||
This component automatically imports the following dependencies.
|
||||
|
||||
${createDependenciesList(component.tagName, getAllComponents(metadata))}
|
||||
`;
|
||||
}
|
||||
|
||||
// Strip whitespace so markdown doesn't process things as code blocks
|
||||
return result.replace(/^ +| +$/gm, '');
|
||||
});
|
||||
|
||||
next(content);
|
||||
});
|
||||
|
||||
// Wrap tables so we can scroll them horizontally when needed
|
||||
hook.doneEach(() => {
|
||||
const content = document.querySelector('.content');
|
||||
const tables = [...content.querySelectorAll('table')];
|
||||
|
||||
tables.forEach(table => {
|
||||
table.outerHTML = `
|
||||
<div class="table-wrapper">
|
||||
${table.outerHTML}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -1,24 +0,0 @@
|
||||
(() => {
|
||||
if (!window.$docsify) {
|
||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||
}
|
||||
|
||||
//
|
||||
// Docsify generates pages dynamically and asynchronously, so when a reload happens, the scroll position can't be
|
||||
// be restored immediately. This plugin waits until Docsify loads the page and then restores it.
|
||||
//
|
||||
window.$docsify.plugins.push(hook => {
|
||||
hook.ready(() => {
|
||||
// Restore
|
||||
const scrollTop = sessionStorage.getItem('bs-scroll');
|
||||
if (scrollTop) {
|
||||
document.documentElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
// Remember
|
||||
document.addEventListener('scroll', () => {
|
||||
sessionStorage.setItem('bs-scroll', document.documentElement.scrollTop);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
1311
docs/assets/plugins/search/lunr.min.js
vendored
@@ -1,192 +0,0 @@
|
||||
body.site-search-visible {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar .search-box {
|
||||
margin: 1.25rem 26px;
|
||||
}
|
||||
|
||||
.sidebar .search-box kbd {
|
||||
margin-top: 2px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
/* Site search */
|
||||
.site-search {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.site-search[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-search__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--sl-overlay-background-color);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.site-search__panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 460px;
|
||||
max-height: calc(100vh - 20rem);
|
||||
background-color: var(--sl-panel-background-color);
|
||||
border-radius: var(--sl-border-radius-large);
|
||||
box-shadow: var(--sl-shadow-x-large);
|
||||
margin: 10rem auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.site-search__panel {
|
||||
max-width: 100%;
|
||||
max-height: calc(92vh - 120px); /* allow iOS browser chrome */
|
||||
margin: 4vh var(--sl-spacing-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.site-search__input::part(base) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--sl-border-radius-large);
|
||||
}
|
||||
|
||||
.site-search__input:focus-within::part(base) {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.site-search__input {
|
||||
--sl-input-height-large: 4rem;
|
||||
}
|
||||
|
||||
.site-search__body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.site-search--has-results .site-search__body {
|
||||
border-top: solid 1px var(--sl-color-neutral-200);
|
||||
}
|
||||
|
||||
.site-search__results {
|
||||
display: none;
|
||||
line-height: var(--sl-line-height-dense);
|
||||
list-style: none;
|
||||
padding: var(--sl-spacing-x-small) 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.site-search--has-results .site-search__results {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.site-search__results a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--sl-spacing-x-small) var(--sl-spacing-large);
|
||||
}
|
||||
|
||||
.site-search__results li a:hover,
|
||||
.site-search__results li a:hover small {
|
||||
background-color: var(--sl-color-neutral-100);
|
||||
}
|
||||
|
||||
.site-search__results li[aria-selected='true'] a,
|
||||
.site-search__results li[aria-selected='true'] a small,
|
||||
.site-search__results li[aria-selected='true'] a sl-icon {
|
||||
outline: none;
|
||||
color: var(--sl-color-neutral-0);
|
||||
background-color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
.sl-theme-dark .site-search__results li[aria-selected='true'] a,
|
||||
.sl-theme-dark .site-search__results li[aria-selected='true'] a small,
|
||||
.sl-theme-dark .site-search__results li[aria-selected='true'] a sl-icon {
|
||||
background-color: var(--sl-color-primary-400);
|
||||
color: var(--sl-color-neutral-1000);
|
||||
}
|
||||
|
||||
.site-search__results h3 {
|
||||
font-weight: var(--sl-font-weight-semibold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.site-search__results small {
|
||||
display: block;
|
||||
color: var(--sl-color-neutral-600);
|
||||
}
|
||||
|
||||
.site-search__result {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.site-search__result a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.site-search__result-icon {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
color: var(--sl-color-neutral-400);
|
||||
font-size: var(--sl-font-size-x-large);
|
||||
}
|
||||
|
||||
.site-search__result-description {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.site-search__empty {
|
||||
display: none;
|
||||
border-top: solid 1px var(--sl-color-neutral-200);
|
||||
text-align: center;
|
||||
padding: var(--sl-spacing-x-large);
|
||||
}
|
||||
|
||||
.site-search--no-results .site-search__empty {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.site-search__footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--sl-spacing-large);
|
||||
border-top: solid 1px var(--sl-color-neutral-200);
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
padding: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.site-search__footer small {
|
||||
color: var(--sl-color-neutral-700);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.site-search__footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forced colors mode */
|
||||
@media (forced-colors: active) {
|
||||
.site-search__panel {
|
||||
border: solid 1px var(--sl-color-neutral-0);
|
||||
}
|
||||
|
||||
.site-search__results li[aria-selected='true'] a {
|
||||
outline: dashed 1px SelectedItem;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
/* global lunr */
|
||||
(() => {
|
||||
if (!window.$docsify) {
|
||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||
}
|
||||
|
||||
window.$docsify.plugins.push(hook => {
|
||||
// Append the search box to the sidebar
|
||||
hook.mounted(() => {
|
||||
const appName = document.querySelector('.sidebar .app-name');
|
||||
const searchBox = document.createElement('div');
|
||||
searchBox.classList.add('search-box');
|
||||
searchBox.innerHTML = `
|
||||
<sl-input type="search" placeholder="Search" pill>
|
||||
<sl-icon slot="prefix" name="search"></sl-icon>
|
||||
<kbd slot="suffix" title="Press / to search">/</kbd>
|
||||
</sl-input>
|
||||
`;
|
||||
const searchBoxInput = searchBox.querySelector('sl-input');
|
||||
|
||||
appName.insertAdjacentElement('afterend', searchBox);
|
||||
|
||||
// Show the search panel when the search is clicked
|
||||
searchBoxInput.addEventListener('mousedown', event => {
|
||||
event.preventDefault();
|
||||
show();
|
||||
});
|
||||
|
||||
// Show the search panel when a key is pressed
|
||||
searchBoxInput.addEventListener('keydown', event => {
|
||||
if (event.key === 'Tab') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the character that was typed through to the search input
|
||||
if (event.key.length === 1) {
|
||||
event.preventDefault();
|
||||
input.value = event.key;
|
||||
show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Append the search panel to the body
|
||||
const siteSearch = document.createElement('div');
|
||||
siteSearch.classList.add('site-search');
|
||||
siteSearch.hidden = true;
|
||||
siteSearch.innerHTML = `
|
||||
<div class="site-search__overlay"></div>
|
||||
<div
|
||||
id="site-search-panel"
|
||||
class="site-search__panel"
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-owns="site-search-results"
|
||||
aria-activedescendant=""
|
||||
>
|
||||
<header class="site-search__header">
|
||||
<sl-input
|
||||
class="site-search__input"
|
||||
type="search"
|
||||
placeholder="Search this site"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="site-search-results"
|
||||
size="large"
|
||||
clearable
|
||||
>
|
||||
<sl-icon slot="prefix" name="search"></sl-icon>
|
||||
</sl-input>
|
||||
</header>
|
||||
<div class="site-search__body">
|
||||
<ul
|
||||
id="site-search-results"
|
||||
class="site-search__results"
|
||||
role="listbox"
|
||||
aria-labelledby="site-search-panel"
|
||||
>
|
||||
</ul>
|
||||
<div class="site-search__empty">No results found.</div>
|
||||
</div>
|
||||
<footer class="site-search__footer">
|
||||
<small><kbd>↑</kbd> <kbd>↓</kbd> navigate</small>
|
||||
<small><kbd>↲</kbd> select</small>
|
||||
<small><kbd>esc</kbd> close</small>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
document.body.append(siteSearch);
|
||||
|
||||
const overlay = siteSearch.querySelector('.site-search__overlay');
|
||||
const panel = siteSearch.querySelector('.site-search__panel');
|
||||
const input = siteSearch.querySelector('.site-search__input');
|
||||
const results = siteSearch.querySelector('.site-search__results');
|
||||
const animationDuration = 150;
|
||||
const searchDebounce = 200;
|
||||
let isShowing = false;
|
||||
let searchTimeout;
|
||||
let searchIndex;
|
||||
let map;
|
||||
|
||||
// Load search data
|
||||
fetch('../../../search.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
searchIndex = lunr.Index.load(data.searchIndex);
|
||||
map = data.map;
|
||||
});
|
||||
|
||||
async function show() {
|
||||
isShowing = true;
|
||||
document.body.classList.add('site-search-visible');
|
||||
siteSearch.hidden = false;
|
||||
requestAnimationFrame(() => input.focus());
|
||||
updateResults();
|
||||
|
||||
await Promise.all([
|
||||
panel.animate(
|
||||
[
|
||||
{ opacity: 0, transform: 'scale(.9)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
{ duration: animationDuration }
|
||||
).finished,
|
||||
overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished
|
||||
]);
|
||||
|
||||
document.addEventListener('mousedown', handleDocumentMouseDown);
|
||||
document.addEventListener('keydown', handleDocumentKeyDown);
|
||||
document.addEventListener('focusin', handleDocumentFocusIn);
|
||||
}
|
||||
|
||||
async function hide() {
|
||||
isShowing = false;
|
||||
document.body.classList.remove('site-search-visible');
|
||||
|
||||
await Promise.all([
|
||||
panel.animate(
|
||||
[
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(.9)' }
|
||||
],
|
||||
{ duration: animationDuration }
|
||||
).finished,
|
||||
overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished
|
||||
]);
|
||||
|
||||
siteSearch.hidden = true;
|
||||
input.value = '';
|
||||
updateResults();
|
||||
|
||||
document.removeEventListener('mousedown', handleDocumentMouseDown);
|
||||
document.removeEventListener('keydown', handleDocumentKeyDown);
|
||||
document.removeEventListener('focusin', handleDocumentFocusIn);
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
// Debounce search queries
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
|
||||
}
|
||||
|
||||
function handleDocumentFocusIn(event) {
|
||||
// Close when focus leaves the panel
|
||||
if (event.target.closest('.site-search__panel') !== panel) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDocumentMouseDown(event) {
|
||||
// Close when clicking outside of the panel
|
||||
if (event.target.closest('.site-search__overlay') === overlay) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDocumentKeyDown(event) {
|
||||
// Close when pressing escape
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle keyboard selections
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentEl = results.querySelector('[aria-selected="true"]');
|
||||
const items = [...results.querySelectorAll('li')];
|
||||
const index = items.indexOf(currentEl);
|
||||
let nextEl;
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
nextEl = items[Math.max(0, index - 1)];
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
nextEl = items[Math.min(items.length - 1, index + 1)];
|
||||
break;
|
||||
case 'Home':
|
||||
nextEl = items[0];
|
||||
break;
|
||||
case 'End':
|
||||
nextEl = items[items.length - 1];
|
||||
break;
|
||||
case 'Enter':
|
||||
currentEl?.querySelector('a')?.click();
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the selected item
|
||||
items.forEach(item => {
|
||||
if (item === nextEl) {
|
||||
const a = nextEl.querySelector('a');
|
||||
panel.setAttribute('aria-activedescendant', a.id);
|
||||
|
||||
item.setAttribute('aria-selected', 'true');
|
||||
nextEl.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateResults(query = '') {
|
||||
try {
|
||||
await searchIndex;
|
||||
|
||||
const hasQuery = query.length > 0;
|
||||
const searchTokens = query
|
||||
.split(' ')
|
||||
.map((term, index, arr) => `${term}${index === arr.length - 1 ? `* ${term}~1` : '~1'}`)
|
||||
.join(' ');
|
||||
const matches = hasQuery ? searchIndex.search(`${query} ${searchTokens}`) : [];
|
||||
const hasResults = hasQuery && matches.length > 0;
|
||||
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
|
||||
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
|
||||
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
|
||||
|
||||
results.innerHTML = '';
|
||||
|
||||
matches.forEach((match, index) => {
|
||||
const page = map[match.ref];
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
let icon = 'file-text';
|
||||
|
||||
a.setAttribute('role', 'option');
|
||||
a.setAttribute('id', `search-result-item-${match.ref}`);
|
||||
|
||||
if (page.url.includes('getting-started/')) {
|
||||
icon = 'lightbulb';
|
||||
}
|
||||
if (page.url.includes('resources/')) {
|
||||
icon = 'book';
|
||||
}
|
||||
if (page.url.includes('components/')) {
|
||||
icon = 'puzzle';
|
||||
}
|
||||
if (page.url.includes('tokens/')) {
|
||||
icon = 'palette2';
|
||||
}
|
||||
if (page.url.includes('utilities/')) {
|
||||
icon = 'wrench';
|
||||
}
|
||||
if (page.url.includes('tutorials/')) {
|
||||
icon = 'joystick';
|
||||
}
|
||||
|
||||
a.href = window.$docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
|
||||
a.innerHTML = `
|
||||
<div class="site-search__result-icon">
|
||||
<sl-icon name="${icon}" aria-hidden="true"></sl-icon>
|
||||
</div>
|
||||
<div class="site-search__result__details">
|
||||
<h3>${page.title}</h3>
|
||||
<small>${page.url}</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
li.classList.add('site-search__result');
|
||||
li.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
|
||||
li.appendChild(a);
|
||||
results.appendChild(li);
|
||||
});
|
||||
} catch {
|
||||
// Ignore query errors as the user types
|
||||
}
|
||||
}
|
||||
|
||||
// Show the search panel slash is pressed outside of a form element
|
||||
document.addEventListener('keydown', event => {
|
||||
const isSlash = event.key === '/';
|
||||
const isCtrlK = (event.metaKey || event.ctrlKey) && event.key === 'k';
|
||||
|
||||
if (
|
||||
!isShowing &&
|
||||
(isSlash || isCtrlK) &&
|
||||
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
|
||||
) {
|
||||
event.preventDefault();
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('sl-input', handleInput);
|
||||
|
||||
// Close when a result is selected
|
||||
results.addEventListener('click', event => {
|
||||
if (event.target.closest('a')) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -1,29 +0,0 @@
|
||||
.theme-picker {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.theme-picker:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-picker sl-menu-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theme-picker sl-menu-label kbd {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.theme-picker {
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-picker sl-menu-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
(() => {
|
||||
if (!window.$docsify) {
|
||||
throw new Error('Docsify must be loaded before installing this plugin.');
|
||||
}
|
||||
|
||||
window.$docsify.plugins.push(hook => {
|
||||
hook.mounted(() => {
|
||||
function getTheme() {
|
||||
return localStorage.getItem('theme') || 'auto';
|
||||
}
|
||||
|
||||
function isDark() {
|
||||
if (theme === 'auto') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
return theme === 'dark';
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
const noTransitions = Object.assign(document.createElement('style'), {
|
||||
textContent: '* { transition: none !important; }'
|
||||
});
|
||||
|
||||
theme = newTheme;
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
// Update the UI
|
||||
[...menu.querySelectorAll('sl-menu-item')].map(item => (item.checked = item.getAttribute('value') === theme));
|
||||
menuIcon.name = isDark() ? 'moon' : 'sun';
|
||||
|
||||
// Toggle the dark mode class without transitions
|
||||
document.body.appendChild(noTransitions);
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.classList.toggle('sl-theme-dark', isDark());
|
||||
requestAnimationFrame(() => document.body.removeChild(noTransitions));
|
||||
});
|
||||
}
|
||||
|
||||
let theme = getTheme();
|
||||
|
||||
// Generate the theme picker dropdown
|
||||
const dropdown = document.createElement('sl-dropdown');
|
||||
dropdown.classList.add('theme-picker');
|
||||
dropdown.innerHTML = `
|
||||
<sl-button size="small" pill slot="trigger" caret>
|
||||
<sl-icon name="sun" label="Select Theme"></sl-icon>
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-label>Toggle <kbd>\\</kbd></sl-menu-label>
|
||||
<sl-menu-item type="checkbox" value="light">Light</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="dark">Dark</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item type="checkbox" value="auto">Auto</sl-menu-item>
|
||||
</sl-menu>
|
||||
`;
|
||||
document.querySelector('.sidebar-toggle').insertAdjacentElement('afterend', dropdown);
|
||||
|
||||
// Listen for selections
|
||||
const menu = dropdown.querySelector('sl-menu');
|
||||
const menuIcon = dropdown.querySelector('sl-icon');
|
||||
menu.addEventListener('sl-select', event => setTheme(event.detail.item.value));
|
||||
|
||||
// Update the theme when the preference changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setTheme(theme));
|
||||
|
||||
// Toggle themes when pressing backslash
|
||||
document.addEventListener('keydown', event => {
|
||||
if (
|
||||
event.key === '\\' &&
|
||||
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
setTheme(isDark() ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
// Set the initial theme and sync the UI
|
||||
setTheme(theme);
|
||||
});
|
||||
});
|
||||
})();
|
||||
243
docs/assets/scripts/code-previews.js
Normal file
@@ -0,0 +1,243 @@
|
||||
(() => {
|
||||
function convertModuleLinks(html) {
|
||||
html = html
|
||||
.replace(/@shoelace-style\/shoelace/g, `https://esm.sh/@shoelace-style/shoelace@${waVersion}`)
|
||||
.replace(/from 'react'/g, `from 'https://esm.sh/react@${reactVersion}'`)
|
||||
.replace(/from "react"/g, `from "https://esm.sh/react@${reactVersion}"`);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function getAdjacentExample(name, pre) {
|
||||
let currentPre = pre.nextElementSibling;
|
||||
|
||||
while (currentPre?.tagName.toLowerCase() === 'pre') {
|
||||
if (currentPre?.getAttribute('data-lang').split(' ').includes(name)) {
|
||||
return currentPre;
|
||||
}
|
||||
|
||||
currentPre = currentPre.nextElementSibling;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function runScript(script) {
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
if (script.type === 'module') {
|
||||
newScript.type = 'module';
|
||||
newScript.textContent = script.innerHTML;
|
||||
} else {
|
||||
newScript.appendChild(document.createTextNode(`(() => { ${script.innerHTML} })();`));
|
||||
}
|
||||
|
||||
script.parentNode.replaceChild(newScript, script);
|
||||
}
|
||||
|
||||
function getFlavor() {
|
||||
return sessionStorage.getItem('flavor') || 'html';
|
||||
}
|
||||
|
||||
function setFlavor(newFlavor) {
|
||||
flavor = ['html', 'react'].includes(newFlavor) ? newFlavor : 'html';
|
||||
sessionStorage.setItem('flavor', flavor);
|
||||
|
||||
// Set the flavor class on the body
|
||||
document.documentElement.classList.toggle('flavor-html', flavor === 'html');
|
||||
document.documentElement.classList.toggle('flavor-react', flavor === 'react');
|
||||
}
|
||||
|
||||
function syncFlavor() {
|
||||
setFlavor(getFlavor());
|
||||
|
||||
document.querySelectorAll('.code-preview__button--html').forEach(preview => {
|
||||
if (flavor === 'html') {
|
||||
preview.classList.add('code-preview__button--selected');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.code-preview__button--react').forEach(preview => {
|
||||
if (flavor === 'react') {
|
||||
preview.classList.add('code-preview__button--selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const waVersion = document.documentElement.getAttribute('data-wa-version');
|
||||
const reactVersion = '18.2.0';
|
||||
const cdndir = 'cdn';
|
||||
const npmdir = 'dist';
|
||||
let flavor = getFlavor();
|
||||
let count = 1;
|
||||
|
||||
// We need the version to open
|
||||
if (!waVersion) {
|
||||
throw new Error('The data-wa-version attribute is missing from <html>.');
|
||||
}
|
||||
|
||||
// Sync flavor UI on page load
|
||||
syncFlavor();
|
||||
|
||||
//
|
||||
// Resizing previews
|
||||
//
|
||||
document.addEventListener('mousedown', handleResizerDrag);
|
||||
document.addEventListener('touchstart', handleResizerDrag, { passive: true });
|
||||
|
||||
function handleResizerDrag(event) {
|
||||
const resizer = event.target.closest('.code-preview__resizer');
|
||||
const preview = event.target.closest('.code-preview__preview');
|
||||
|
||||
if (!resizer || !preview) return;
|
||||
|
||||
let startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
|
||||
let startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
|
||||
|
||||
event.preventDefault();
|
||||
preview.classList.add('code-preview__preview--dragging');
|
||||
document.documentElement.addEventListener('mousemove', dragMove);
|
||||
document.documentElement.addEventListener('touchmove', dragMove);
|
||||
document.documentElement.addEventListener('mouseup', dragStop);
|
||||
document.documentElement.addEventListener('touchend', dragStop);
|
||||
|
||||
function dragMove(event) {
|
||||
const width = startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX;
|
||||
preview.style.width = `${width}px`;
|
||||
}
|
||||
|
||||
function dragStop() {
|
||||
preview.classList.remove('code-preview__preview--dragging');
|
||||
document.documentElement.removeEventListener('mousemove', dragMove);
|
||||
document.documentElement.removeEventListener('touchmove', dragMove);
|
||||
document.documentElement.removeEventListener('mouseup', dragStop);
|
||||
document.documentElement.removeEventListener('touchend', dragStop);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Toggle source mode
|
||||
//
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('.code-preview__button');
|
||||
const codeBlock = button?.closest('.code-preview');
|
||||
|
||||
if (button?.classList.contains('code-preview__button--html')) {
|
||||
// Show HTML
|
||||
setFlavor('html');
|
||||
toggleSource(codeBlock, true);
|
||||
} else if (button?.classList.contains('code-preview__button--react')) {
|
||||
// Show React
|
||||
setFlavor('react');
|
||||
toggleSource(codeBlock, true);
|
||||
} else if (button?.classList.contains('code-preview__toggle')) {
|
||||
// Toggle source
|
||||
toggleSource(codeBlock);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update flavor buttons
|
||||
[...document.querySelectorAll('.code-preview')].forEach(cb => {
|
||||
cb.querySelector('.code-preview__button--html')?.classList.toggle(
|
||||
'code-preview__button--selected',
|
||||
flavor === 'html'
|
||||
);
|
||||
cb.querySelector('.code-preview__button--react')?.classList.toggle(
|
||||
'code-preview__button--selected',
|
||||
flavor === 'react'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function toggleSource(codeBlock, force) {
|
||||
codeBlock.classList.toggle('code-preview--expanded', force);
|
||||
event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-preview--expanded'));
|
||||
}
|
||||
|
||||
//
|
||||
// Open in CodePen
|
||||
//
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('button');
|
||||
|
||||
if (button?.classList.contains('code-preview__button--codepen')) {
|
||||
const codeBlock = button.closest('.code-preview');
|
||||
const htmlExample = codeBlock.querySelector('.code-preview__source--html > pre > code')?.textContent;
|
||||
const reactExample = codeBlock.querySelector('.code-preview__source--react > pre > code')?.textContent;
|
||||
const isReact = flavor === 'react' && typeof reactExample === 'string';
|
||||
const editors = isReact ? '0010' : '1000';
|
||||
let htmlTemplate = '';
|
||||
let jsTemplate = '';
|
||||
let cssTemplate = '';
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.action = 'https://codepen.io/pen/define';
|
||||
form.method = 'POST';
|
||||
form.target = '_blank';
|
||||
|
||||
// HTML templates
|
||||
if (!isReact) {
|
||||
htmlTemplate =
|
||||
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${waVersion}/${cdndir}/autoloader.js"></script>\n` +
|
||||
`\n${htmlExample}`;
|
||||
jsTemplate = '';
|
||||
}
|
||||
|
||||
// React templates
|
||||
if (isReact) {
|
||||
htmlTemplate = '<div id="root"></div>';
|
||||
jsTemplate =
|
||||
`import React from 'https://esm.sh/react@${reactVersion}';\n` +
|
||||
`import ReactDOM from 'https://esm.sh/react-dom@${reactVersion}';\n` +
|
||||
`import { setBasePath } from 'https://esm.sh/@shoelace-style/shoelace@${waVersion}/${cdndir}/utilities/base-path';\n` +
|
||||
`\n` +
|
||||
`// Set the base path for Web Awesome assets\n` +
|
||||
`setBasePath('https://esm.sh/@shoelace-style/shoelace@${waVersion}/${npmdir}/')\n` +
|
||||
`\n${convertModuleLinks(reactExample)}\n` +
|
||||
`\n` +
|
||||
`ReactDOM.render(<App />, document.getElementById('root'));`;
|
||||
}
|
||||
|
||||
// CSS templates
|
||||
cssTemplate =
|
||||
`@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${waVersion}/${cdndir}/themes/default.css';\n` +
|
||||
'\n' +
|
||||
'body {\n' +
|
||||
' font: var(--wa-font-size-root) sans-serif;\n' +
|
||||
' background-color: var(--wa-color-surface-default);\n' +
|
||||
' color: var(--wa-color-text-normal);\n' +
|
||||
' padding: var(--wa-space-m);\n' +
|
||||
'}';
|
||||
|
||||
// Docs: https://blog.codepen.io/documentation/prefill/
|
||||
const data = {
|
||||
title: '',
|
||||
description: '',
|
||||
tags: ['web awesome', 'web components'],
|
||||
editors,
|
||||
head: `<meta name="viewport" content="width=device-width">`,
|
||||
css_external: ``,
|
||||
js_external: ``,
|
||||
js_module: true,
|
||||
js_pre_processor: isReact ? 'babel' : 'none',
|
||||
html: htmlTemplate,
|
||||
css: cssTemplate,
|
||||
js: jsTemplate
|
||||
};
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'data';
|
||||
input.value = JSON.stringify(data);
|
||||
form.append(input);
|
||||
|
||||
document.documentElement.append(form);
|
||||
form.submit();
|
||||
form.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the initial flavor
|
||||
window.addEventListener('turbo:load', syncFlavor);
|
||||
})();
|
||||
206
docs/assets/scripts/docs.js
Normal file
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// Sidebar
|
||||
//
|
||||
// When the sidebar is hidden, we apply the inert attribute to prevent focus from reaching it. Due to the many states
|
||||
// the sidebar can have (e.g. static, hidden, expanded), we test for visibility by checking to see if it's placed
|
||||
// offscreen or not. Then, on resize/transition we make sure to update the attribute accordingly.
|
||||
//
|
||||
(() => {
|
||||
function getSidebar() {
|
||||
return document.getElementById('sidebar');
|
||||
}
|
||||
|
||||
function isSidebarOpen() {
|
||||
return document.documentElement.classList.contains('sidebar-open');
|
||||
}
|
||||
|
||||
function isSidebarVisible() {
|
||||
return getSidebar().getBoundingClientRect().x >= 0;
|
||||
}
|
||||
|
||||
function toggleSidebar(force) {
|
||||
const isOpen = typeof force === 'boolean' ? force : !isSidebarOpen();
|
||||
return document.documentElement.classList.toggle('sidebar-open', isOpen);
|
||||
}
|
||||
|
||||
function updateInert() {
|
||||
getSidebar().inert = !isSidebarVisible();
|
||||
}
|
||||
|
||||
// Toggle the menu
|
||||
document.addEventListener('click', event => {
|
||||
const menuToggle = event.target.closest('#menu-toggle');
|
||||
if (!menuToggle) return;
|
||||
toggleSidebar();
|
||||
});
|
||||
|
||||
// Update the sidebar's inert state when the window resizes and when the sidebar transitions
|
||||
window.addEventListener('resize', () => toggleSidebar(false));
|
||||
|
||||
document.addEventListener('transitionend', event => {
|
||||
const sidebar = event.target.closest('#sidebar');
|
||||
if (!sidebar) return;
|
||||
updateInert();
|
||||
});
|
||||
|
||||
// Close when a menu item is selected on mobile
|
||||
document.addEventListener('click', event => {
|
||||
const sidebar = event.target.closest('#sidebar');
|
||||
const link = event.target.closest('a');
|
||||
if (!sidebar || !link) return;
|
||||
|
||||
if (isSidebarOpen()) {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when open and escape is pressed
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' && isSidebarOpen()) {
|
||||
event.stopImmediatePropagation();
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when clicking outside of the sidebar
|
||||
document.addEventListener('mousedown', event => {
|
||||
if (isSidebarOpen() & !event.target?.closest('#sidebar, #menu-toggle')) {
|
||||
event.stopImmediatePropagation();
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
updateInert();
|
||||
})();
|
||||
|
||||
//
|
||||
// Open details when printing
|
||||
//
|
||||
(() => {
|
||||
const detailsOpenOnPrint = new Set();
|
||||
|
||||
window.addEventListener('beforeprint', () => {
|
||||
detailsOpenOnPrint.clear();
|
||||
document.querySelectorAll('details').forEach(details => {
|
||||
if (details.open) {
|
||||
detailsOpenOnPrint.add(details);
|
||||
}
|
||||
details.open = true;
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('afterprint', () => {
|
||||
document.querySelectorAll('details').forEach(details => {
|
||||
details.open = detailsOpenOnPrint.has(details);
|
||||
});
|
||||
detailsOpenOnPrint.clear();
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
// Smooth links
|
||||
//
|
||||
(() => {
|
||||
document.addEventListener('click', event => {
|
||||
const link = event.target.closest('a');
|
||||
const id = (link?.hash ?? '').substr(1);
|
||||
const isFragment = link?.hasAttribute('href') && link?.getAttribute('href').startsWith('#');
|
||||
|
||||
if (!link || !isFragment || link.getAttribute('data-smooth-link') === 'false') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll to the top
|
||||
if (link.hash === '') {
|
||||
event.preventDefault();
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
history.pushState(undefined, undefined, location.pathname);
|
||||
}
|
||||
|
||||
// Scroll to an id
|
||||
if (id) {
|
||||
const target = document.getElementById(id);
|
||||
|
||||
if (target) {
|
||||
event.preventDefault();
|
||||
window.scroll({ top: target.offsetTop, behavior: 'smooth' });
|
||||
history.pushState(undefined, undefined, `#${id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
//
|
||||
// Table of Contents scrollspy
|
||||
//
|
||||
(() => {
|
||||
// This will be stale if its not a function.
|
||||
const getLinks = () => [...document.querySelectorAll('.content__toc a')];
|
||||
const linkTargets = new WeakMap();
|
||||
const visibleTargets = new WeakSet();
|
||||
const observer = new IntersectionObserver(handleIntersect, { rootMargin: '0px 0px' });
|
||||
let debounce;
|
||||
|
||||
function handleIntersect(entries) {
|
||||
entries.forEach(entry => {
|
||||
// Remember which targets are visible
|
||||
if (entry.isIntersecting) {
|
||||
visibleTargets.add(entry.target);
|
||||
} else {
|
||||
visibleTargets.delete(entry.target);
|
||||
}
|
||||
});
|
||||
|
||||
updateActiveLinks();
|
||||
}
|
||||
|
||||
function updateActiveLinks() {
|
||||
const links = getLinks();
|
||||
// Find the first visible target and activate the respective link
|
||||
links.find(link => {
|
||||
const target = linkTargets.get(link);
|
||||
|
||||
if (target && visibleTargets.has(target)) {
|
||||
links.forEach(el => el.classList.toggle('active', el === link));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Observe link targets
|
||||
function observeLinks() {
|
||||
getLinks().forEach(link => {
|
||||
const hash = link.hash.slice(1);
|
||||
const target = hash ? document.querySelector(`.content__body #${hash}`) : null;
|
||||
|
||||
if (target) {
|
||||
linkTargets.set(link, target);
|
||||
observer.observe(target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observeLinks();
|
||||
|
||||
document.addEventListener('turbo:load', updateActiveLinks);
|
||||
document.addEventListener('turbo:load', observeLinks);
|
||||
})();
|
||||
|
||||
//
|
||||
// Show custom versions in the sidebar
|
||||
//
|
||||
(() => {
|
||||
function updateVersion() {
|
||||
const el = document.querySelector('.sidebar-version');
|
||||
if (!el) return;
|
||||
|
||||
if (location.hostname === 'next.shoelace.style') el.textContent = 'Next';
|
||||
if (location.hostname === 'localhost') el.textContent = 'Development';
|
||||
}
|
||||
|
||||
updateVersion();
|
||||
|
||||
document.addEventListener('turbo:load', updateVersion);
|
||||
})();
|
||||
384
docs/assets/scripts/search.js
Normal file
@@ -0,0 +1,384 @@
|
||||
(() => {
|
||||
// Append the search dialog to the body
|
||||
const siteSearch = document.createElement('div');
|
||||
const scrollbarWidth = Math.abs(window.innerWidth - document.documentElement.clientWidth);
|
||||
|
||||
siteSearch.classList.add('search');
|
||||
siteSearch.innerHTML = `
|
||||
<div class="search__overlay"></div>
|
||||
<dialog id="search-dialog" class="search__dialog">
|
||||
<div class="search__content">
|
||||
<div class="search__header">
|
||||
<div id="search-combobox" class="search__input-wrapper">
|
||||
<wa-icon name="search"></wa-icon>
|
||||
<input
|
||||
id="search-input"
|
||||
class="search__input"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
enterkeyhint="go"
|
||||
spellcheck="false"
|
||||
maxlength="100"
|
||||
role="combobox"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="true"
|
||||
aria-controls="search-listbox"
|
||||
aria-haspopup="listbox"
|
||||
aria-activedescendant
|
||||
>
|
||||
<button type="button" class="search__clear-button" aria-label="Clear entry" tabindex="-1" hidden>
|
||||
<wa-icon name="x-circle-fill"></wa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search__body">
|
||||
<ul
|
||||
id="search-listbox"
|
||||
class="search__results"
|
||||
role="listbox"
|
||||
aria-label="Search results"
|
||||
></ul>
|
||||
<div class="search__empty">No matching pages</div>
|
||||
</div>
|
||||
<footer class="search__footer">
|
||||
<small><kbd>↑</kbd> <kbd>↓</kbd> Navigate</small>
|
||||
<small><kbd>↲</kbd> Select</small>
|
||||
<small><kbd>Esc</kbd> Close</small>
|
||||
</footer>
|
||||
</div>
|
||||
</dialog>
|
||||
`;
|
||||
|
||||
const overlay = siteSearch.querySelector('.search__overlay');
|
||||
const dialog = siteSearch.querySelector('.search__dialog');
|
||||
const input = siteSearch.querySelector('.search__input');
|
||||
const clearButton = siteSearch.querySelector('.search__clear-button');
|
||||
const results = siteSearch.querySelector('.search__results');
|
||||
const version = document.documentElement.getAttribute('data-wa-version');
|
||||
const key = `search_${version}`;
|
||||
const searchDebounce = 50;
|
||||
const animationDuration = 150;
|
||||
let isShowing = false;
|
||||
let searchTimeout;
|
||||
let searchIndex;
|
||||
let map;
|
||||
|
||||
const loadSearchIndex = new Promise(resolve => {
|
||||
const cache = localStorage.getItem(key);
|
||||
const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame;
|
||||
|
||||
// Cleanup older search indices (everything before this version)
|
||||
try {
|
||||
const items = { ...localStorage };
|
||||
|
||||
Object.keys(items).forEach(k => {
|
||||
if (key > k) {
|
||||
localStorage.removeItem(k);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
// Look for a cached index
|
||||
try {
|
||||
if (cache) {
|
||||
const data = JSON.parse(cache);
|
||||
|
||||
searchIndex = window.lunr.Index.load(data.searchIndex);
|
||||
map = data.map;
|
||||
|
||||
return resolve();
|
||||
}
|
||||
} catch {
|
||||
/* do nothing */
|
||||
}
|
||||
|
||||
// Wait until idle to fetch the index
|
||||
wait(() => {
|
||||
fetch('/assets/search.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!window.lunr) {
|
||||
console.error('The Lunr search client has not yet been loaded.');
|
||||
}
|
||||
|
||||
searchIndex = window.lunr.Index.load(data.searchIndex);
|
||||
map = data.map;
|
||||
|
||||
// Cache the search index for this version
|
||||
if (version) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
} catch (err) {
|
||||
console.warn(`Unable to cache the search index: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function show() {
|
||||
isShowing = true;
|
||||
document.body.append(siteSearch);
|
||||
document.body.classList.add('search-visible');
|
||||
document.body.style.setProperty('--docs-search-scroll-lock-size', `${scrollbarWidth}px`);
|
||||
clearButton.hidden = true;
|
||||
requestAnimationFrame(() => input.focus());
|
||||
updateResults();
|
||||
|
||||
dialog.showModal();
|
||||
|
||||
await Promise.all([
|
||||
dialog.animate(
|
||||
[
|
||||
{ opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' },
|
||||
{ opacity: 1, transform: 'scale(1)', transformOrigin: 'top' }
|
||||
],
|
||||
{ duration: animationDuration }
|
||||
).finished,
|
||||
overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished
|
||||
]);
|
||||
|
||||
dialog.addEventListener('mousedown', handleMouseDown);
|
||||
dialog.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
async function hide() {
|
||||
isShowing = false;
|
||||
|
||||
await Promise.all([
|
||||
dialog.animate(
|
||||
[
|
||||
{ opacity: 1, transform: 'scale(1)', transformOrigin: 'top' },
|
||||
{ opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' }
|
||||
],
|
||||
{ duration: animationDuration }
|
||||
).finished,
|
||||
overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished
|
||||
]);
|
||||
|
||||
dialog.close();
|
||||
|
||||
input.blur(); // otherwise Safari will scroll to the bottom of the page on close
|
||||
input.value = '';
|
||||
document.body.classList.remove('search-visible');
|
||||
document.body.style.removeProperty('--docs-search-scroll-lock-size');
|
||||
siteSearch.remove();
|
||||
updateResults();
|
||||
|
||||
dialog.removeEventListener('mousedown', handleMouseDown);
|
||||
dialog.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
clearButton.hidden = input.value === '';
|
||||
|
||||
// Debounce search queries
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
clearButton.hidden = true;
|
||||
input.value = '';
|
||||
input.focus();
|
||||
updateResults();
|
||||
}
|
||||
|
||||
function handleMouseDown(event) {
|
||||
if (!event.target.closest('.search__content')) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
// Close when pressing escape
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault(); // prevent <dialog> from closing immediately so it can animate
|
||||
event.stopImmediatePropagation();
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle keyboard selections
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentEl = results.querySelector('[data-selected="true"]');
|
||||
const items = [...results.querySelectorAll('li')];
|
||||
const index = items.indexOf(currentEl);
|
||||
let nextEl;
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
nextEl = items[Math.max(0, index - 1)];
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
nextEl = items[Math.min(items.length - 1, index + 1)];
|
||||
break;
|
||||
case 'Home':
|
||||
nextEl = items[0];
|
||||
break;
|
||||
case 'End':
|
||||
nextEl = items[items.length - 1];
|
||||
break;
|
||||
case 'Enter':
|
||||
currentEl?.querySelector('a')?.click();
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the selected item
|
||||
items.forEach(item => {
|
||||
if (item === nextEl) {
|
||||
input.setAttribute('aria-activedescendant', item.id);
|
||||
item.setAttribute('data-selected', 'true');
|
||||
nextEl.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.setAttribute('data-selected', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateResults(query = '') {
|
||||
try {
|
||||
await loadSearchIndex;
|
||||
|
||||
const hasQuery = query.length > 0;
|
||||
const searchTerms = query
|
||||
.split(' ')
|
||||
.map((term, index, arr) => {
|
||||
// Search API: https://lunrjs.com/guides/searching.html
|
||||
if (index === arr.length - 1) {
|
||||
// The last term is not mandatory and 1x fuzzy. We also duplicate it with a wildcard to match partial words
|
||||
// as the user types.
|
||||
return `${term}~1 ${term}*`;
|
||||
} else {
|
||||
// All other terms are mandatory and 1x fuzzy
|
||||
return `+${term}~1`;
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
const matches = hasQuery ? searchIndex.search(searchTerms) : [];
|
||||
const hasResults = hasQuery && matches.length > 0;
|
||||
|
||||
siteSearch.classList.toggle('search--has-results', hasQuery && hasResults);
|
||||
siteSearch.classList.toggle('search--no-results', hasQuery && !hasResults);
|
||||
|
||||
input.setAttribute('aria-activedescendant', '');
|
||||
results.innerHTML = '';
|
||||
|
||||
matches.forEach((match, index) => {
|
||||
const page = map[match.ref];
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
const displayTitle = page.title ?? '';
|
||||
const displayDescription = page.description ?? '';
|
||||
const displayUrl = page.url.replace(/^\//, '').replace(/\/$/, '');
|
||||
let icon = 'file-text';
|
||||
|
||||
a.setAttribute('role', 'option');
|
||||
a.setAttribute('id', `search-result-item-${match.ref}`);
|
||||
|
||||
if (page.url.includes('getting-started/')) {
|
||||
icon = 'lightbulb';
|
||||
}
|
||||
if (page.url.includes('resources/')) {
|
||||
icon = 'book';
|
||||
}
|
||||
if (page.url.includes('components/')) {
|
||||
icon = 'puzzle';
|
||||
}
|
||||
if (page.url.includes('tokens/')) {
|
||||
icon = 'palette2';
|
||||
}
|
||||
if (page.url.includes('utilities/')) {
|
||||
icon = 'wrench';
|
||||
}
|
||||
if (page.url.includes('tutorials/')) {
|
||||
icon = 'joystick';
|
||||
}
|
||||
|
||||
li.classList.add('search__result');
|
||||
li.setAttribute('role', 'option');
|
||||
li.setAttribute('id', `search-result-item-${match.ref}`);
|
||||
li.setAttribute('data-selected', index === 0 ? 'true' : 'false');
|
||||
|
||||
a.href = page.url;
|
||||
a.innerHTML = `
|
||||
<div class="search__result-icon" aria-hidden="true">
|
||||
<wa-icon name="${icon}"></wa-icon>
|
||||
</div>
|
||||
<div class="search__result__details">
|
||||
<div class="search__result-title"></div>
|
||||
<div class="search__result-description"></div>
|
||||
<div class="search__result-url"></div>
|
||||
</div>
|
||||
`;
|
||||
a.querySelector('.search__result-title').textContent = displayTitle;
|
||||
a.querySelector('.search__result-description').textContent = displayDescription;
|
||||
a.querySelector('.search__result-url').textContent = displayUrl;
|
||||
|
||||
li.appendChild(a);
|
||||
results.appendChild(li);
|
||||
});
|
||||
} catch {
|
||||
// Ignore query errors as the user types
|
||||
}
|
||||
}
|
||||
|
||||
// Show the search dialog when clicking on data-plugin="search"
|
||||
document.addEventListener('click', event => {
|
||||
const searchButton = event.target.closest('[data-plugin="search"]');
|
||||
if (searchButton) {
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
// Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element
|
||||
document.addEventListener('keydown', event => {
|
||||
if (
|
||||
!isShowing &&
|
||||
(event.key === '/' || (event.key === 'k' && (event.metaKey || event.ctrlKey))) &&
|
||||
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
|
||||
) {
|
||||
event.preventDefault();
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
// Purge cache when we press CMD+CTRL+R
|
||||
document.addEventListener('keydown', event => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'r') {
|
||||
localStorage.clear();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('input', handleInput);
|
||||
clearButton.addEventListener('click', handleClear);
|
||||
|
||||
// Close when a result is selected
|
||||
results.addEventListener('click', event => {
|
||||
if (event.target.closest('a')) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
|
||||
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
|
||||
// get trapped.
|
||||
window.addEventListener('turbo:render', () => {
|
||||
document.body.classList.remove('search-visible');
|
||||
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
|
||||
});
|
||||
})();
|
||||
29
docs/assets/scripts/turbo.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm';
|
||||
|
||||
(() => {
|
||||
if (!window.scrollPositions) {
|
||||
window.scrollPositions = {};
|
||||
}
|
||||
|
||||
function preserveScroll() {
|
||||
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
|
||||
scrollPositions[element.id] = element.scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
function restoreScroll(event) {
|
||||
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
|
||||
element.scrollTop = scrollPositions[element.id];
|
||||
});
|
||||
|
||||
if (event.detail && event.detail.newBody) {
|
||||
event.detail.newBody.querySelectorAll('[data-preserve-scroll').forEach(element => {
|
||||
element.scrollTop = scrollPositions[element.id];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('turbo:before-cache', preserveScroll);
|
||||
window.addEventListener('turbo:before-render', restoreScroll);
|
||||
window.addEventListener('turbo:render', restoreScroll);
|
||||
})();
|
||||
173
docs/assets/styles/code-previews.css
Normal file
@@ -0,0 +1,173 @@
|
||||
/* Interactive code blocks */
|
||||
.code-preview {
|
||||
position: relative;
|
||||
border-radius: var(--wa-corners-1x);
|
||||
background-color: var(--wa-color-surface-lowered);
|
||||
margin-bottom: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.code-preview__preview {
|
||||
position: relative;
|
||||
border: var(--wa-border-style) var(--wa-border-width-thin) var(--wa-color-surface-outline);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: var(--wa-corners-1x);
|
||||
border-top-right-radius: var(--wa-corners-1x);
|
||||
background-color: var(--wa-color-surface-default);
|
||||
min-width: 20rem;
|
||||
max-width: 100%;
|
||||
padding: var(--wa-space-xl) var(--wa-space-3xl) var(--wa-space-xl) var(--wa-space-xl);
|
||||
}
|
||||
|
||||
/* Block the preview while dragging to prevent iframes from intercepting drag events */
|
||||
.code-preview__preview--dragging:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.code-preview__resizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1.75rem;
|
||||
font-size: 20px;
|
||||
color: var(--wa-color-text-quiet);
|
||||
background-color: var(--wa-color-surface-default);
|
||||
border-left: var(--wa-border-style) var(--wa-border-width-thin) var(--wa-color-surface-outline);
|
||||
border-top-right-radius: var(--wa-corners-1x);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.code-preview__preview {
|
||||
padding-right: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
.code-preview__resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.code-preview__source {
|
||||
border: var(--wa-border-style) var(--wa-border-width-thin) var(--wa-color-surface-outline);
|
||||
border-bottom: none;
|
||||
border-radius: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.code-preview--expanded .code-preview__source {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.code-preview__source pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-preview__buttons {
|
||||
position: relative;
|
||||
border: var(--wa-border-style) var(--wa-border-width-thin) var(--wa-color-surface-outline);
|
||||
border-bottom-left-radius: var(--wa-corners-1x);
|
||||
border-bottom-right-radius: var(--wa-corners-1x);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.code-preview__button {
|
||||
flex: 0 0 auto;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: var(--wa-color-surface-default);
|
||||
font: inherit;
|
||||
font-size: var(--wa-font-size-xs);
|
||||
font-weight: var(--wa-font-weight-normal);
|
||||
text-transform: uppercase;
|
||||
color: var(--wa-color-text-quiet);
|
||||
padding: 0 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-preview__button:not(:last-of-type) {
|
||||
border-right: var(--wa-border-style) var(--wa-border-width-thin) var(--wa-color-surface-outline);
|
||||
}
|
||||
|
||||
.code-preview__button--html,
|
||||
.code-preview__button--react {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.code-preview__button--selected {
|
||||
font-weight: var(--wa-font-weight-heavy);
|
||||
color: var(--wa-color-brand-text-on-surface);
|
||||
}
|
||||
|
||||
.code-preview__button--codepen {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.code-preview__button:first-of-type {
|
||||
border-bottom-left-radius: var(--wa-corners-1x);
|
||||
}
|
||||
|
||||
.code-preview__button:last-of-type {
|
||||
border-bottom-right-radius: var(--wa-corners-1x);
|
||||
}
|
||||
|
||||
.code-preview__button:hover,
|
||||
.code-preview__button:active {
|
||||
box-shadow: 0 0 0 var(--wa-border-width-thin) var(--wa-color-brand-outline-muted);
|
||||
border-right-color: transparent;
|
||||
background-color: var(--wa-color-brand-fill-muted);
|
||||
color: var(--wa-color-brand-text-on-surface);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.code-preview__button:focus-visible {
|
||||
outline: none;
|
||||
outline: var(--wa-focus-ring);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.code-preview__toggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
color: var(--wa-color-text-quiet);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-preview__toggle svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.code-preview--expanded .code-preview__toggle svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* We can apply data-flavor="html|react" to any element on the page to toggle it when the flavor changes */
|
||||
.flavor-html [data-flavor]:not([data-flavor='html']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flavor-react [data-flavor]:not([data-flavor='react']) {
|
||||
display: none;
|
||||
}
|
||||
344
docs/assets/styles/search.css
Normal file
@@ -0,0 +1,344 @@
|
||||
/* Search plugin */
|
||||
:root {
|
||||
--docs-search-box-background: var(--wa-form-controls-background);
|
||||
--docs-search-box-border-width: var(--wa-form-controls-border-width);
|
||||
--docs-search-box-border-color: var(--wa-form-controls-border-color-resting);
|
||||
--docs-search-box-color: var(--wa-form-controls-placeholder-color);
|
||||
|
||||
--docs-search-dialog-background: var(--wa-color-surface-raised);
|
||||
--docs-search-border-width: var(--wa-border-width-thin);
|
||||
--docs-search-border-color: var(--wa-color-surface-outline);
|
||||
--docs-search-text-color: var(--wa-color-text-normal);
|
||||
--docs-search-text-color-muted: var(--wa-color-text-quiet);
|
||||
--docs-search-font-weight-normal: var(--wa-font-weight-normal);
|
||||
--docs-search-font-weight-semibold: var(--wa-font-weight-medium);
|
||||
--docs-search-border-radius: calc(2 * var(--wa-corners-1x));
|
||||
|
||||
--docs-search-accent-color: var(--wa-color-brand-text-on-surface);
|
||||
--docs-search-icon-color: var(--wa-color-neutral-fill-vivid);
|
||||
--docs-search-icon-color-active: color-mix(in lch, var(--wa-color-neutral-fill-vivid), 8% black);
|
||||
--docs-search-shadow: var(--wa-shadow-level-3);
|
||||
--docs-search-result-background-hover: var(--wa-color-neutral-fill-muted-alt);
|
||||
--docs-search-result-color-hover: var(--wa-color-neutral-text-on-muted);
|
||||
--docs-search-result-background-active: var(--wa-color-brand-fill-vivid);
|
||||
--docs-search-result-color-active: var(--wa-color-brand-text-on-vivid);
|
||||
--docs-search-focus-ring: var(--wa-focus-ring);
|
||||
--docs-search-overlay-background: rgb(0 0 0 / 0.33);
|
||||
}
|
||||
|
||||
body.search-visible {
|
||||
padding-right: var(--docs-search-scroll-lock-size) !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Search box */
|
||||
.search-box {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
background: var(--docs-search-box-background);
|
||||
border: solid var(--docs-search-box-border-width) var(--docs-search-box-border-color);
|
||||
font: inherit;
|
||||
color: var(--docs-search-box-color);
|
||||
padding: 0.75rem 1rem;
|
||||
margin: var(--wa-space-l) 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-box span {
|
||||
flex: 1 1 auto;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
text-align: left;
|
||||
line-height: 1;
|
||||
margin: 0 0.75rem;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box:focus-visible {
|
||||
outline: var(--docs-search-focus-ring);
|
||||
}
|
||||
|
||||
/* Site search */
|
||||
.search {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.search[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--docs-search-overlay-background);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.search__dialog {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search__dialog:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search__dialog::backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Fixes an iOS Safari 16.4 bug that draws the parent element's border radius incorrectly when showing/hiding results */
|
||||
.search__header {
|
||||
background-color: var(--docs-search-dialog-background);
|
||||
border-radius: var(--docs-search-border-radius);
|
||||
}
|
||||
|
||||
.search--has-results .search__header {
|
||||
border-top-left-radius: var(--docs-search-border-radius);
|
||||
border-top-right-radius: var(--docs-search-border-radius);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.search__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: calc(100vh - 20rem);
|
||||
background-color: var(--docs-search-dialog-background);
|
||||
border-radius: var(--docs-search-border-radius);
|
||||
box-shadow: var(--docs-search-shadow);
|
||||
padding: 0;
|
||||
margin: 10rem auto;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.search__content {
|
||||
max-width: calc(100% - 2rem);
|
||||
max-height: calc(90svh);
|
||||
margin: 4vh 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search__input-wrapper wa-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
flex: 0 0 auto;
|
||||
color: var(--docs-search-icon-color);
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
.search__clear-button {
|
||||
display: flex;
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search__clear-button[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search__clear-button:active wa-icon {
|
||||
color: var(--docs-search-icon-color-active);
|
||||
}
|
||||
|
||||
.search__input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
font: inherit;
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--docs-search-font-weight-normal);
|
||||
color: var(--docs-search-text-color);
|
||||
background: transparent;
|
||||
padding: 1rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search__input::placeholder {
|
||||
color: var(--docs-search-text-color-muted);
|
||||
}
|
||||
|
||||
.search__input::-webkit-search-decoration,
|
||||
.search__input::-webkit-search-cancel-button,
|
||||
.search__input::-webkit-search-results-button,
|
||||
.search__input::-webkit-search-results-decoration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search__input:focus,
|
||||
.search__input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search__body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.search--has-results .search__body {
|
||||
border-top: solid var(--docs-search-border-width) var(--docs-search-border-color);
|
||||
}
|
||||
|
||||
.search__results {
|
||||
display: none;
|
||||
line-height: 1.2;
|
||||
list-style: none;
|
||||
padding: 0.5rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search--has-results .search__results {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search__results a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.search__results a:focus-visible {
|
||||
outline: var(--docs-search-focus-ring);
|
||||
}
|
||||
|
||||
.search__results li a:hover,
|
||||
.search__results li a:hover small {
|
||||
background-color: var(--docs-search-result-background-hover);
|
||||
color: var(--docs-search-result-color-hover);
|
||||
}
|
||||
|
||||
.search__results li[data-selected='true'] a,
|
||||
.search__results li[data-selected='true'] a * {
|
||||
outline: none;
|
||||
background-color: var(--docs-search-result-background-active);
|
||||
color: var(--docs-search-result-color-active);
|
||||
}
|
||||
|
||||
.search__results h3 {
|
||||
font-weight: var(--docs-search-font-weight-semibold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search__results small {
|
||||
display: block;
|
||||
color: var(--docs-search-text-color-muted);
|
||||
}
|
||||
|
||||
.search__result {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search__result a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search__result-icon {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
color: var(--docs-search-text-color-muted);
|
||||
}
|
||||
|
||||
.search__result-icon wa-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.search__result__details {
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
.search__result-title,
|
||||
.search__result-description,
|
||||
.search__result-url {
|
||||
max-width: 400px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search__result-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: var(--docs-search-font-weight-semibold);
|
||||
color: var(--docs-search-accent-color);
|
||||
}
|
||||
|
||||
.search__result-description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--docs-search-text-color);
|
||||
}
|
||||
|
||||
.search__result-url {
|
||||
font-size: 0.875rem;
|
||||
color: var(--docs-search-text-color-muted);
|
||||
}
|
||||
|
||||
.search__empty {
|
||||
display: none;
|
||||
border-top: solid var(--docs-search-border-width) var(--docs-search-border-color);
|
||||
text-align: center;
|
||||
color: var(--docs-search-text-color-muted);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.search--no-results .search__empty {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search__footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
border-top: solid var(--docs-search-border-width) var(--docs-search-border-color);
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.search__footer small {
|
||||
color: var(--docs-search-text-color-muted);
|
||||
}
|
||||
|
||||
.search__footer small kbd:last-of-type {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.search__footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
# Badge
|
||||
|
||||
[component-header:sl-badge]
|
||||
|
||||
```html preview
|
||||
<sl-badge>Badge</sl-badge>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlBadge>Badge</SlBadge>;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Variants
|
||||
|
||||
Set the `variant` attribute to change the badge's variant.
|
||||
|
||||
```html preview
|
||||
<sl-badge variant="primary">Primary</sl-badge>
|
||||
<sl-badge variant="success">Success</sl-badge>
|
||||
<sl-badge variant="neutral">Neutral</sl-badge>
|
||||
<sl-badge variant="warning">Warning</sl-badge>
|
||||
<sl-badge variant="danger">Danger</sl-badge>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlBadge variant="primary">Primary</SlBadge>
|
||||
<SlBadge variant="success">Success</SlBadge>
|
||||
<SlBadge variant="neutral">Neutral</SlBadge>
|
||||
<SlBadge variant="warning">Warning</SlBadge>
|
||||
<SlBadge variant="danger">Danger</SlBadge>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill Badges
|
||||
|
||||
Use the `pill` attribute to give badges rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-badge variant="primary" pill>Primary</sl-badge>
|
||||
<sl-badge variant="success" pill>Success</sl-badge>
|
||||
<sl-badge variant="neutral" pill>Neutral</sl-badge>
|
||||
<sl-badge variant="warning" pill>Warning</sl-badge>
|
||||
<sl-badge variant="danger" pill>Danger</sl-badge>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlBadge variant="primary" pill>
|
||||
Primary
|
||||
</SlBadge>
|
||||
<SlBadge variant="success" pill>
|
||||
Success
|
||||
</SlBadge>
|
||||
<SlBadge variant="neutral" pill>
|
||||
Neutral
|
||||
</SlBadge>
|
||||
<SlBadge variant="warning" pill>
|
||||
Warning
|
||||
</SlBadge>
|
||||
<SlBadge variant="danger" pill>
|
||||
Danger
|
||||
</SlBadge>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pulsating Badges
|
||||
|
||||
Use the `pulse` attribute to draw attention to the badge with a subtle animation.
|
||||
|
||||
```html preview
|
||||
<div class="badge-pulse">
|
||||
<sl-badge variant="primary" pill pulse>1</sl-badge>
|
||||
<sl-badge variant="success" pill pulse>1</sl-badge>
|
||||
<sl-badge variant="neutral" pill pulse>1</sl-badge>
|
||||
<sl-badge variant="warning" pill pulse>1</sl-badge>
|
||||
<sl-badge variant="danger" pill pulse>1</sl-badge>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.badge-pulse sl-badge:not(:last-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.badge-pulse sl-badge:not(:last-of-type) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div className="badge-pulse">
|
||||
<SlBadge variant="primary" pill pulse>
|
||||
1
|
||||
</SlBadge>
|
||||
<SlBadge variant="success" pill pulse>
|
||||
1
|
||||
</SlBadge>
|
||||
<SlBadge variant="neutral" pill pulse>
|
||||
1
|
||||
</SlBadge>
|
||||
<SlBadge variant="warning" pill pulse>
|
||||
1
|
||||
</SlBadge>
|
||||
<SlBadge variant="danger" pill pulse>
|
||||
1
|
||||
</SlBadge>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### With Buttons
|
||||
|
||||
One of the most common use cases for badges is attaching them to buttons. To make this easier, badges will be automatically positioned at the top-right when they're a child of a button.
|
||||
|
||||
```html preview
|
||||
<sl-button>
|
||||
Requests
|
||||
<sl-badge pill>30</sl-badge>
|
||||
</sl-button>
|
||||
|
||||
<sl-button style="margin-inline-start: 1rem;">
|
||||
Warnings
|
||||
<sl-badge variant="warning" pill>8</sl-badge>
|
||||
</sl-button>
|
||||
|
||||
<sl-button style="margin-inline-start: 1rem;">
|
||||
Errors
|
||||
<sl-badge variant="danger" pill>6</sl-badge>
|
||||
</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge, SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton>
|
||||
Requests
|
||||
<SlBadge pill>30</SlBadge>
|
||||
</SlButton>
|
||||
|
||||
<SlButton style={{ marginInlineStart: '1rem' }}>
|
||||
Warnings
|
||||
<SlBadge variant="warning" pill>
|
||||
8
|
||||
</SlBadge>
|
||||
</SlButton>
|
||||
|
||||
<SlButton style={{ marginInlineStart: '1rem' }}>
|
||||
Errors
|
||||
<SlBadge variant="danger" pill>
|
||||
6
|
||||
</SlBadge>
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### With Menu Items
|
||||
|
||||
When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 240px;">
|
||||
<sl-menu-label>Messages</sl-menu-label>
|
||||
<sl-menu-item>Comments <sl-badge slot="suffix" variant="neutral" pill>4</sl-badge></sl-menu-item>
|
||||
<sl-menu-item>Replies <sl-badge slot="suffix" variant="neutral" pill>12</sl-badge></sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge, SlButton, SlMenu, SlMenuItem, SlMenuLabel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu
|
||||
style={{
|
||||
maxWidth: '240px',
|
||||
border: 'solid 1px var(--sl-panel-border-color)',
|
||||
borderRadius: 'var(--sl-border-radius-medium)'
|
||||
}}
|
||||
>
|
||||
<SlMenuLabel>Messages</SlMenuLabel>
|
||||
<SlMenuItem>
|
||||
Comments
|
||||
<SlBadge slot="suffix" variant="neutral" pill>
|
||||
4
|
||||
</SlBadge>
|
||||
</SlMenuItem>
|
||||
<SlMenuItem>
|
||||
Replies
|
||||
<SlBadge slot="suffix" variant="neutral" pill>
|
||||
12
|
||||
</SlBadge>
|
||||
</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-badge]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Breadcrumb Item
|
||||
|
||||
[component-header:sl-breadcrumb-item]
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>
|
||||
<sl-icon slot="prefix" name="house"></sl-icon>
|
||||
Home
|
||||
</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Clothing</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Shirts</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlBreadcrumb>
|
||||
<SlBreadcrumbItem>
|
||||
<SlIcon slot="prefix" name="house"></SlIcon>
|
||||
Home
|
||||
</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Clothing</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Shirts</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
?> Additional demonstrations can be found in the [breadcrumb examples](/components/breadcrumb).
|
||||
|
||||
[component-metadata:sl-breadcrumb-item]
|
||||
@@ -1,250 +0,0 @@
|
||||
# Breadcrumb
|
||||
|
||||
[component-header:sl-breadcrumb]
|
||||
|
||||
Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation.
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>Catalog</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Clothing</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Women's</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Shirts & Tops</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlBreadcrumb>
|
||||
<SlBreadcrumbItem>Catalog</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Clothing</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Women's</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Shirts & Tops</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Breadcrumb Links
|
||||
|
||||
By default, breadcrumb items are rendered as buttons so you can use them to navigate single-page applications. In this case, you'll need to add event listeners to handle clicks.
|
||||
|
||||
For websites, you'll probably want to use links instead. You can make any breadcrumb item a link by applying an `href` attribute to it. Now, when the user activates it, they'll be taken to the corresponding page — no event listeners required.
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item href="https://example.com/home">Homepage</sl-breadcrumb-item>
|
||||
|
||||
<sl-breadcrumb-item href="https://example.com/home/services">Our Services</sl-breadcrumb-item>
|
||||
|
||||
<sl-breadcrumb-item href="https://example.com/home/services/digital">Digital Media</sl-breadcrumb-item>
|
||||
|
||||
<sl-breadcrumb-item href="https://example.com/home/services/digital/web-design">Web Design</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlBreadcrumb>
|
||||
<SlBreadcrumbItem href="https://example.com/home">Homepage</SlBreadcrumbItem>
|
||||
|
||||
<SlBreadcrumbItem href="https://example.com/home/services">Our Services</SlBreadcrumbItem>
|
||||
|
||||
<SlBreadcrumbItem href="https://example.com/home/services/digital">Digital Media</SlBreadcrumbItem>
|
||||
|
||||
<SlBreadcrumbItem href="https://example.com/home/services/digital/web-design">Web Design</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Separators
|
||||
|
||||
Use the `separator` slot to change the separator that goes between breadcrumb items. Icons work well, but you can also use text or an image.
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-icon name="dot" slot="separator"></sl-icon>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-breadcrumb>
|
||||
<sl-icon name="arrow-right" slot="separator"></sl-icon>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-breadcrumb>
|
||||
<span slot="separator">/</span>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
|
||||
import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlBreadcrumb>
|
||||
<sl-icon name="dot" slot="separator" />
|
||||
<SlBreadcrumbItem>First</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Second</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Third</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
|
||||
<br />
|
||||
|
||||
<SlBreadcrumb>
|
||||
<sl-icon name="arrow-right" slot="separator" />
|
||||
<SlBreadcrumbItem>First</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Second</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Third</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
|
||||
<br />
|
||||
|
||||
<SlBreadcrumb>
|
||||
<span slot="separator">/</span>
|
||||
<SlBreadcrumbItem>First</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Second</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Third</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefixes
|
||||
|
||||
Use the `prefix` slot to add content before any breadcrumb item.
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>
|
||||
<sl-icon slot="prefix" name="house"></sl-icon>
|
||||
Home
|
||||
</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Articles</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Traveling</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlBreadcrumb>
|
||||
<SlBreadcrumbItem>
|
||||
<SlIcon slot="prefix" name="house" />
|
||||
Home
|
||||
</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Articles</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Traveling</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
### Suffixes
|
||||
|
||||
Use the `suffix` slot to add content after any breadcrumb item.
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>Documents</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Policies</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>
|
||||
Security
|
||||
<sl-icon slot="suffix" name="shield-lock"></sl-icon>
|
||||
</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlBreadcrumb>
|
||||
<SlBreadcrumbItem>Documents</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Policies</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>
|
||||
Security
|
||||
<SlIcon slot="suffix" name="shield-lock"></SlIcon>
|
||||
</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
### With Dropdowns
|
||||
|
||||
Dropdown menus can be placed in a prefix or suffix slot to provide additional options.
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>Homepage</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Our Services</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Digital Media</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>
|
||||
Web Design
|
||||
<sl-dropdown slot="suffix">
|
||||
<sl-button slot="trigger" size="small" circle>
|
||||
<sl-icon label="More options" name="three-dots"></sl-icon>
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item type="checkbox" checked>Web Design</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Web Development</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Marketing</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlBreadcrumb,
|
||||
SlBreadcrumbItem,
|
||||
SlButton,
|
||||
SlDropdown,
|
||||
SlIcon,
|
||||
SlMenu,
|
||||
SlMenuItem
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlBreadcrumb>
|
||||
<SlBreadcrumbItem>Homepage</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Our Services</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>Digital Media</SlBreadcrumbItem>
|
||||
<SlBreadcrumbItem>
|
||||
Web Design
|
||||
<SlDropdown slot="suffix">
|
||||
<SlButton slot="trigger" size="small" circle>
|
||||
<SlIcon label="More options" name="three-dots"></SlIcon>
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Web Design
|
||||
</SlMenuItem>
|
||||
<SlMenuItem type="checkbox">Web Development</SlMenuItem>
|
||||
<SlMenuItem type="checkbox">Marketing</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
</SlBreadcrumbItem>
|
||||
</SlBreadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-breadcrumb]
|
||||
@@ -1,493 +0,0 @@
|
||||
# Button Group
|
||||
|
||||
[component-header:sl-button-group]
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button>Left</sl-button>
|
||||
<sl-button>Center</sl-button>
|
||||
<sl-button>Right</sl-button>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton>Left</SlButton>
|
||||
<SlButton>Center</SlButton>
|
||||
<SlButton>Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Button Sizes
|
||||
|
||||
All button sizes are supported, but avoid mixing sizes within the same button group.
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="small">Left</sl-button>
|
||||
<sl-button size="small">Center</sl-button>
|
||||
<sl-button size="small">Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="medium">Left</sl-button>
|
||||
<sl-button size="medium">Center</sl-button>
|
||||
<sl-button size="medium">Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="large">Left</sl-button>
|
||||
<sl-button size="large">Center</sl-button>
|
||||
<sl-button size="large">Right</sl-button>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="small">Left</SlButton>
|
||||
<SlButton size="small">Center</SlButton>
|
||||
<SlButton size="small">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="medium">Left</SlButton>
|
||||
<SlButton size="medium">Center</SlButton>
|
||||
<SlButton size="medium">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="large">Left</SlButton>
|
||||
<SlButton size="large">Center</SlButton>
|
||||
<SlButton size="large">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Theme Buttons
|
||||
|
||||
Theme buttons are supported through the button's `variant` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="primary">Left</sl-button>
|
||||
<sl-button variant="primary">Center</sl-button>
|
||||
<sl-button variant="primary">Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="success">Left</sl-button>
|
||||
<sl-button variant="success">Center</sl-button>
|
||||
<sl-button variant="success">Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="neutral">Left</sl-button>
|
||||
<sl-button variant="neutral">Center</sl-button>
|
||||
<sl-button variant="neutral">Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="warning">Left</sl-button>
|
||||
<sl-button variant="warning">Center</sl-button>
|
||||
<sl-button variant="warning">Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="danger">Left</sl-button>
|
||||
<sl-button variant="danger">Center</sl-button>
|
||||
<sl-button variant="danger">Right</sl-button>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="primary">Left</SlButton>
|
||||
<SlButton variant="primary">Center</SlButton>
|
||||
<SlButton variant="primary">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="success">Left</SlButton>
|
||||
<SlButton variant="success">Center</SlButton>
|
||||
<SlButton variant="success">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="neutral">Left</SlButton>
|
||||
<SlButton variant="neutral">Center</SlButton>
|
||||
<SlButton variant="neutral">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="warning">Left</SlButton>
|
||||
<SlButton variant="warning">Center</SlButton>
|
||||
<SlButton variant="warning">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="danger">Left</SlButton>
|
||||
<SlButton variant="danger">Center</SlButton>
|
||||
<SlButton variant="danger">Right</SlButton>
|
||||
</SlButtonGroup>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill Buttons
|
||||
|
||||
Pill buttons are supported through the button's `pill` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="small" pill>Left</sl-button>
|
||||
<sl-button size="small" pill>Center</sl-button>
|
||||
<sl-button size="small" pill>Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="medium" pill>Left</sl-button>
|
||||
<sl-button size="medium" pill>Center</sl-button>
|
||||
<sl-button size="medium" pill>Right</sl-button>
|
||||
</sl-button-group>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="large" pill>Left</sl-button>
|
||||
<sl-button size="large" pill>Center</sl-button>
|
||||
<sl-button size="large" pill>Right</sl-button>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="small" pill>
|
||||
Left
|
||||
</SlButton>
|
||||
<SlButton size="small" pill>
|
||||
Center
|
||||
</SlButton>
|
||||
<SlButton size="small" pill>
|
||||
Right
|
||||
</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="medium" pill>
|
||||
Left
|
||||
</SlButton>
|
||||
<SlButton size="medium" pill>
|
||||
Center
|
||||
</SlButton>
|
||||
<SlButton size="medium" pill>
|
||||
Right
|
||||
</SlButton>
|
||||
</SlButtonGroup>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="large" pill>
|
||||
Left
|
||||
</SlButton>
|
||||
<SlButton size="large" pill>
|
||||
Center
|
||||
</SlButton>
|
||||
<SlButton size="large" pill>
|
||||
Right
|
||||
</SlButton>
|
||||
</SlButtonGroup>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Dropdowns in Button Groups
|
||||
|
||||
Dropdowns can be placed inside button groups as long as the trigger is an `<sl-button>` element.
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Example Button Group">
|
||||
<sl-button>Button</sl-button>
|
||||
<sl-button>Button</sl-button>
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Dropdown</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlButtonGroup label="Example Button Group">
|
||||
<SlButton>Button</SlButton>
|
||||
<SlButton>Button</SlButton>
|
||||
<SlDropdown>
|
||||
<SlButton slot="trigger" caret>
|
||||
Dropdown
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Item 1</SlMenuItem>
|
||||
<SlMenuItem>Item 2</SlMenuItem>
|
||||
<SlMenuItem>Item 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
</SlButtonGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Split Buttons
|
||||
|
||||
Create a split button using a button and a dropdown. Use a [visually hidden](/components/visually-hidden) label to ensure the dropdown is accessible to users with assistive devices.
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Example Button Group">
|
||||
<sl-button variant="primary">Save</sl-button>
|
||||
<sl-dropdown placement="bottom-end">
|
||||
<sl-button slot="trigger" variant="primary" caret>
|
||||
<sl-visually-hidden>More options</sl-visually-hidden>
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Save</sl-menu-item>
|
||||
<sl-menu-item>Save as…</sl-menu-item>
|
||||
<sl-menu-item>Save all</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlButtonGroup label="Example Button Group">
|
||||
<SlButton variant="primary">Save</SlButton>
|
||||
<SlDropdown placement="bottom-end">
|
||||
<SlButton slot="trigger" variant="primary" caret></SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Save</SlMenuItem>
|
||||
<SlMenuItem>Save as…</SlMenuItem>
|
||||
<SlMenuItem>Save all</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
</SlButtonGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Tooltips in Button Groups
|
||||
|
||||
Buttons can be wrapped in tooltips to provide more detail when the user interacts with them.
|
||||
|
||||
```html preview
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-tooltip content="I'm on the left">
|
||||
<sl-button>Left</sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="I'm in the middle">
|
||||
<sl-button>Center</sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="I'm on the right">
|
||||
<sl-button>Right</sl-button>
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlTooltip content="I'm on the left">
|
||||
<SlButton>Left</SlButton>
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="I'm in the middle">
|
||||
<SlButton>Center</SlButton>
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="I'm on the right">
|
||||
<SlButton>Right</SlButton>
|
||||
</SlTooltip>
|
||||
</SlButtonGroup>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Toolbar Example
|
||||
|
||||
Create interactive toolbars with button groups.
|
||||
|
||||
```html preview
|
||||
<div class="button-group-toolbar">
|
||||
<sl-button-group label="History">
|
||||
<sl-tooltip content="Undo">
|
||||
<sl-button><sl-icon name="arrow-counterclockwise" label="Undo"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip content="Redo">
|
||||
<sl-button><sl-icon name="arrow-clockwise" label="Redo"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
|
||||
<sl-button-group label="Formatting">
|
||||
<sl-tooltip content="Bold">
|
||||
<sl-button><sl-icon name="type-bold" label="Bold"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip content="Italic">
|
||||
<sl-button><sl-icon name="type-italic" label="Italic"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip content="Underline">
|
||||
<sl-button><sl-icon name="type-underline" label="Underline"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-tooltip content="Align Left">
|
||||
<sl-button><sl-icon name="justify-left" label="Align Left"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip content="Align Center">
|
||||
<sl-button><sl-icon name="justify" label="Align Center"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
<sl-tooltip content="Align Right">
|
||||
<sl-button><sl-icon name="justify-right" label="Align Right"></sl-icon></sl-button>
|
||||
</sl-tooltip>
|
||||
</sl-button-group>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button-group-toolbar sl-button-group:not(:last-of-type) {
|
||||
margin-right: var(--sl-spacing-x-small);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlButtonGroup, SlIcon, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.button-group-toolbar sl-button-group:not(:last-of-type) {
|
||||
margin-right: var(--sl-spacing-x-small);
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div className="button-group-toolbar">
|
||||
<SlButtonGroup label="History">
|
||||
<SlTooltip content="Undo">
|
||||
<SlButton>
|
||||
<SlIcon name="arrow-counterclockwise"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
<SlTooltip content="Redo">
|
||||
<SlButton>
|
||||
<SlIcon name="arrow-clockwise"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
</SlButtonGroup>
|
||||
|
||||
<SlButtonGroup label="Formatting">
|
||||
<SlTooltip content="Bold">
|
||||
<SlButton>
|
||||
<SlIcon name="type-bold"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
<SlTooltip content="Italic">
|
||||
<SlButton>
|
||||
<SlIcon name="type-italic"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
<SlTooltip content="Underline">
|
||||
<SlButton>
|
||||
<SlIcon name="type-underline"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
</SlButtonGroup>
|
||||
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlTooltip content="Align Left">
|
||||
<SlButton>
|
||||
<SlIcon name="justify-left"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
<SlTooltip content="Align Center">
|
||||
<SlButton>
|
||||
<SlIcon name="justify"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
<SlTooltip content="Align Right">
|
||||
<SlButton>
|
||||
<SlIcon name="justify-right"></SlIcon>
|
||||
</SlButton>
|
||||
</SlTooltip>
|
||||
</SlButtonGroup>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-button-group]
|
||||
@@ -1,536 +0,0 @@
|
||||
# Button
|
||||
|
||||
[component-header:sl-button]
|
||||
|
||||
```html preview
|
||||
<sl-button>Button</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlButton>Button</SlButton>;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Variants
|
||||
|
||||
Use the `variant` attribute to set the button's variant.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default">Default</sl-button>
|
||||
<sl-button variant="primary">Primary</sl-button>
|
||||
<sl-button variant="success">Success</sl-button>
|
||||
<sl-button variant="neutral">Neutral</sl-button>
|
||||
<sl-button variant="warning">Warning</sl-button>
|
||||
<sl-button variant="danger">Danger</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default">Default</SlButton>
|
||||
<SlButton variant="primary">Primary</SlButton>
|
||||
<SlButton variant="success">Success</SlButton>
|
||||
<SlButton variant="neutral">Neutral</SlButton>
|
||||
<SlButton variant="warning">Warning</SlButton>
|
||||
<SlButton variant="danger">Danger</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a button's size.
|
||||
|
||||
```html preview
|
||||
<sl-button size="small">Small</sl-button>
|
||||
<sl-button size="medium">Medium</sl-button>
|
||||
<sl-button size="large">Large</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton size="small">Small</SlButton>
|
||||
<SlButton size="medium">Medium</SlButton>
|
||||
<SlButton size="large">Large</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Outline Buttons
|
||||
|
||||
Use the `outline` attribute to draw outlined buttons with transparent backgrounds.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" outline>Default</sl-button>
|
||||
<sl-button variant="primary" outline>Primary</sl-button>
|
||||
<sl-button variant="success" outline>Success</sl-button>
|
||||
<sl-button variant="neutral" outline>Neutral</sl-button>
|
||||
<sl-button variant="warning" outline>Warning</sl-button>
|
||||
<sl-button variant="danger" outline>Danger</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default" outline>
|
||||
Default
|
||||
</SlButton>
|
||||
<SlButton variant="primary" outline>
|
||||
Primary
|
||||
</SlButton>
|
||||
<SlButton variant="success" outline>
|
||||
Success
|
||||
</SlButton>
|
||||
<SlButton variant="neutral" outline>
|
||||
Neutral
|
||||
</SlButton>
|
||||
<SlButton variant="warning" outline>
|
||||
Warning
|
||||
</SlButton>
|
||||
<SlButton variant="danger" outline>
|
||||
Danger
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill Buttons
|
||||
|
||||
Use the `pill` attribute to give buttons rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-button size="small" pill>Small</sl-button>
|
||||
<sl-button size="medium" pill>Medium</sl-button>
|
||||
<sl-button size="large" pill>Large</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton size="small" pill>
|
||||
Small
|
||||
</SlButton>
|
||||
<SlButton size="medium" pill>
|
||||
Medium
|
||||
</SlButton>
|
||||
<SlButton size="large" pill>
|
||||
Large
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Circle Buttons
|
||||
|
||||
Use the `circle` attribute to create circular icon buttons. When this attribute is set, the button expects a single `<sl-icon>` in the default slot.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" size="small" circle>
|
||||
<sl-icon name="gear" label="Settings"></sl-icon>
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default" size="medium" circle>
|
||||
<sl-icon name="gear" label="Settings"></sl-icon>
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default" size="large" circle>
|
||||
<sl-icon name="gear" label="Settings"></sl-icon>
|
||||
</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default" size="small" circle>
|
||||
<SlIcon name="gear" />
|
||||
</SlButton>
|
||||
<SlButton variant="default" size="medium" circle>
|
||||
<SlIcon name="gear" />
|
||||
</SlButton>
|
||||
<SlButton variant="default" size="large" circle>
|
||||
<SlIcon name="gear" />
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Text Buttons
|
||||
|
||||
Use the `text` variant to create text buttons that share the same size as regular buttons but don't have backgrounds or borders.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="text" size="small">Text</sl-button>
|
||||
<sl-button variant="text" size="medium">Text</sl-button>
|
||||
<sl-button variant="text" size="large">Text</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="text" size="small">
|
||||
Text
|
||||
</SlButton>
|
||||
<SlButton variant="text" size="medium">
|
||||
Text
|
||||
</SlButton>
|
||||
<SlButton variant="text" size="large">
|
||||
Text
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Link Buttons
|
||||
|
||||
It's often helpful to have a button that works like a link. This is possible by setting the `href` attribute, which will make the component render an `<a>` under the hood. This gives you all the default link behavior the browser provides (e.g. <kbd>CMD/CTRL/SHIFT + CLICK</kbd>) and exposes the `target` and `download` attributes.
|
||||
|
||||
```html preview
|
||||
<sl-button href="https://example.com/">Link</sl-button>
|
||||
<sl-button href="https://example.com/" target="_blank">New Window</sl-button>
|
||||
<sl-button href="/assets/images/wordmark.svg" download="shoelace.svg">Download</sl-button>
|
||||
<sl-button href="https://example.com/" disabled>Disabled</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton href="https://example.com/">Link</SlButton>
|
||||
<SlButton href="https://example.com/" target="_blank">
|
||||
New Window
|
||||
</SlButton>
|
||||
<SlButton href="/assets/images/wordmark.svg" download="shoelace.svg">
|
||||
Download
|
||||
</SlButton>
|
||||
<SlButton href="https://example.com/" disabled>
|
||||
Disabled
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
?> When a `target` is set, the link will receive `rel="noreferrer noopener"` for [security reasons](https://mathiasbynens.github.io/rel-noopener/).
|
||||
|
||||
### Setting a Custom Width
|
||||
|
||||
As expected, buttons can be given a custom width by setting the `width` attribute. This is useful for making buttons span the full width of their container on smaller screens.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" size="small" style="width: 100%; margin-bottom: 1rem;">Small</sl-button>
|
||||
<sl-button variant="default" size="medium" style="width: 100%; margin-bottom: 1rem;">Medium</sl-button>
|
||||
<sl-button variant="default" size="large" style="width: 100%;">Large</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default" size="small" style={{ width: '100%', marginBottom: '1rem' }}>
|
||||
Small
|
||||
</SlButton>
|
||||
<SlButton variant="default" size="medium" style={{ width: '100%', marginBottom: '1rem' }}>
|
||||
Medium
|
||||
</SlButton>
|
||||
<SlButton variant="default" size="large" style={{ width: '100%' }}>
|
||||
Large
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix and Suffix Icons
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" size="small">
|
||||
<sl-icon slot="prefix" name="gear"></sl-icon>
|
||||
Settings
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default" size="small">
|
||||
<sl-icon slot="suffix" name="arrow-counterclockwise"></sl-icon>
|
||||
Refresh
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default" size="small">
|
||||
<sl-icon slot="prefix" name="link-45deg"></sl-icon>
|
||||
<sl-icon slot="suffix" name="box-arrow-up-right"></sl-icon>
|
||||
Open
|
||||
</sl-button>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button variant="default">
|
||||
<sl-icon slot="prefix" name="gear"></sl-icon>
|
||||
Settings
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default">
|
||||
<sl-icon slot="suffix" name="arrow-counterclockwise"></sl-icon>
|
||||
Refresh
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default">
|
||||
<sl-icon slot="prefix" name="link-45deg"></sl-icon>
|
||||
<sl-icon slot="suffix" name="box-arrow-up-right"></sl-icon>
|
||||
Open
|
||||
</sl-button>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button variant="default" size="large">
|
||||
<sl-icon slot="prefix" name="gear"></sl-icon>
|
||||
Settings
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default" size="large">
|
||||
<sl-icon slot="suffix" name="arrow-counterclockwise"></sl-icon>
|
||||
Refresh
|
||||
</sl-button>
|
||||
|
||||
<sl-button variant="default" size="large">
|
||||
<sl-icon slot="prefix" name="link-45deg"></sl-icon>
|
||||
<sl-icon slot="suffix" name="box-arrow-up-right"></sl-icon>
|
||||
Open
|
||||
</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default" size="small">
|
||||
<SlIcon slot="prefix" name="gear"></SlIcon>
|
||||
Settings
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="default" size="small">
|
||||
<SlIcon slot="suffix" name="arrow-counterclockwise"></SlIcon>
|
||||
Refresh
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="default" size="small">
|
||||
<SlIcon slot="prefix" name="link-45deg"></SlIcon>
|
||||
<SlIcon slot="suffix" name="box-arrow-up-right"></SlIcon>
|
||||
Open
|
||||
</SlButton>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButton variant="default">
|
||||
<SlIcon slot="prefix" name="gear"></SlIcon>
|
||||
Settings
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="default">
|
||||
<SlIcon slot="suffix" name="arrow-counterclockwise"></SlIcon>
|
||||
Refresh
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="default">
|
||||
<SlIcon slot="prefix" name="link-45deg"></SlIcon>
|
||||
<SlIcon slot="suffix" name="box-arrow-up-right"></SlIcon>
|
||||
Open
|
||||
</SlButton>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButton variant="default" size="large">
|
||||
<SlIcon slot="prefix" name="gear"></SlIcon>
|
||||
Settings
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="default" size="large">
|
||||
<SlIcon slot="suffix" name="arrow-counterclockwise"></SlIcon>
|
||||
Refresh
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="default" size="large">
|
||||
<SlIcon slot="prefix" name="link-45deg"></SlIcon>
|
||||
<SlIcon slot="suffix" name="box-arrow-up-right"></SlIcon>
|
||||
Open
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Caret
|
||||
|
||||
Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover.
|
||||
|
||||
```html preview
|
||||
<sl-button size="small" caret>Small</sl-button>
|
||||
<sl-button size="medium" caret>Medium</sl-button>
|
||||
<sl-button size="large" caret>Large</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton size="small" caret>
|
||||
Small
|
||||
</SlButton>
|
||||
<SlButton size="medium" caret>
|
||||
Medium
|
||||
</SlButton>
|
||||
<SlButton size="large" caret>
|
||||
Large
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. Clicks will be suppressed until the loading state is removed.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" loading>Default</sl-button>
|
||||
<sl-button variant="primary" loading>Primary</sl-button>
|
||||
<sl-button variant="success" loading>Success</sl-button>
|
||||
<sl-button variant="neutral" loading>Neutral</sl-button>
|
||||
<sl-button variant="warning" loading>Warning</sl-button>
|
||||
<sl-button variant="danger" loading>Danger</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default" loading>
|
||||
Default
|
||||
</SlButton>
|
||||
<SlButton variant="primary" loading>
|
||||
Primary
|
||||
</SlButton>
|
||||
<SlButton variant="success" loading>
|
||||
Success
|
||||
</SlButton>
|
||||
<SlButton variant="neutral" loading>
|
||||
Neutral
|
||||
</SlButton>
|
||||
<SlButton variant="warning" loading>
|
||||
Warning
|
||||
</SlButton>
|
||||
<SlButton variant="danger" loading>
|
||||
Danger
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a button. Clicks will be suppressed until the disabled state is removed.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" disabled>Default</sl-button>
|
||||
<sl-button variant="primary" disabled>Primary</sl-button>
|
||||
<sl-button variant="success" disabled>Success</sl-button>
|
||||
<sl-button variant="neutral" disabled>Neutral</sl-button>
|
||||
<sl-button variant="warning" disabled>Warning</sl-button>
|
||||
<sl-button variant="danger" disabled>Danger</sl-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButton variant="default" disabled>
|
||||
Default
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="primary" disabled>
|
||||
Primary
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="success" disabled>
|
||||
Success
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="neutral" disabled>
|
||||
Neutral
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="warning" disabled>
|
||||
Warning
|
||||
</SlButton>
|
||||
|
||||
<SlButton variant="danger" disabled>
|
||||
Danger
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Styling Buttons
|
||||
|
||||
This example demonstrates how to style buttons using a custom class. This is the recommended approach if you need to add additional variations. To customize an existing variation, modify the selector to target the button's `variant` attribute instead of a class (e.g. `sl-button[variant="primary"]`).
|
||||
|
||||
```html preview
|
||||
<sl-button class="pink">Pink Button</sl-button>
|
||||
|
||||
<style>
|
||||
sl-button.pink::part(base) {
|
||||
/* Set design tokens for height and border width */
|
||||
--sl-input-height-medium: 48px;
|
||||
--sl-input-border-width: 4px;
|
||||
|
||||
border-radius: 0;
|
||||
background-color: #ff1493;
|
||||
border-top-color: #ff7ac1;
|
||||
border-left-color: #ff7ac1;
|
||||
border-bottom-color: #ad005c;
|
||||
border-right-color: #ad005c;
|
||||
color: white;
|
||||
font-size: 1.125rem;
|
||||
box-shadow: 0 2px 10px #0002;
|
||||
transition: var(--sl-transition-medium) transform ease, var(--sl-transition-medium) border ease;
|
||||
}
|
||||
|
||||
sl-button.pink::part(base):hover {
|
||||
transform: scale(1.05) rotate(-1deg);
|
||||
}
|
||||
|
||||
sl-button.pink::part(base):active {
|
||||
border-top-color: #ad005c;
|
||||
border-right-color: #ff7ac1;
|
||||
border-bottom-color: #ff7ac1;
|
||||
border-left-color: #ad005c;
|
||||
transform: scale(1.05) rotate(-1deg) translateY(2px);
|
||||
}
|
||||
|
||||
sl-button.pink::part(base):focus-visible {
|
||||
outline: dashed 2px deeppink;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-button]
|
||||
@@ -1,156 +0,0 @@
|
||||
# Checkbox
|
||||
|
||||
[component-header:sl-checkbox]
|
||||
|
||||
```html preview
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlCheckbox>Checkbox</SlCheckbox>;
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Checked
|
||||
|
||||
Use the `checked` attribute to activate the checkbox.
|
||||
|
||||
```html preview
|
||||
<sl-checkbox checked>Checked</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlCheckbox checked>Checked</SlCheckbox>;
|
||||
```
|
||||
|
||||
### Indeterminate
|
||||
|
||||
Use the `indeterminate` attribute to make the checkbox indeterminate.
|
||||
|
||||
```html preview
|
||||
<sl-checkbox indeterminate>Indeterminate</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlCheckbox indeterminate>Indeterminate</SlCheckbox>;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable the checkbox.
|
||||
|
||||
```html preview
|
||||
<sl-checkbox disabled>Disabled</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlCheckbox disabled>Disabled</SlCheckbox>;
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
Use the `size` attribute to change a checkbox's size.
|
||||
|
||||
```html preview
|
||||
<sl-checkbox size="small">Small</sl-checkbox>
|
||||
<br />
|
||||
<sl-checkbox size="medium">Medium</sl-checkbox>
|
||||
<br />
|
||||
<sl-checkbox size="large">Large</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlCheckbox size="small">Small</SlCheckbox>
|
||||
<br />
|
||||
<SlCheckbox size="medium">Medium</SlCheckbox>
|
||||
<br />
|
||||
<SlCheckbox size="large">Large</SlCheckbox>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Validity
|
||||
|
||||
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
|
||||
|
||||
```html preview
|
||||
<form class="custom-validity">
|
||||
<sl-checkbox>Check me</sl-checkbox>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary" style="margin-top: 1rem;">Submit</sl-button>
|
||||
</form>
|
||||
<script>
|
||||
const form = document.querySelector('.custom-validity');
|
||||
const checkbox = form.querySelector('sl-checkbox');
|
||||
const errorMessage = `Don't forget to check me!`;
|
||||
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-checkbox').then(async () => {
|
||||
await checkbox.updateComplete;
|
||||
checkbox.setCustomValidity(errorMessage);
|
||||
});
|
||||
|
||||
// Update validity on change
|
||||
checkbox.addEventListener('sl-change', () => {
|
||||
checkbox.setCustomValidity(checkbox.checked ? '' : errorMessage);
|
||||
});
|
||||
|
||||
// Handle submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { SlButton, SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const checkbox = useRef(null);
|
||||
const errorMessage = `Don't forget to check me!`;
|
||||
|
||||
function handleChange() {
|
||||
checkbox.current.setCustomValidity(checkbox.current.checked ? '' : errorMessage);
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkbox.current.setCustomValidity(errorMessage);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit}>
|
||||
<SlCheckbox ref={checkbox} onSlChange={handleChange}>
|
||||
Check me
|
||||
</SlCheckbox>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary" style={{ marginTop: '1rem' }}>
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-checkbox]
|
||||
@@ -1,137 +0,0 @@
|
||||
# Color Picker
|
||||
|
||||
[component-header:sl-color-picker]
|
||||
|
||||
```html preview
|
||||
<sl-color-picker label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlColorPicker label="Select a color" />;
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Initial Value
|
||||
|
||||
Use the `value` attribute to set an initial value for the color picker.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker value="#4a90e2" label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlColorPicker value="#4a90e2" label="Select a color" />;
|
||||
```
|
||||
|
||||
### Opacity
|
||||
|
||||
Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA, or HSVA based on `format`.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker value="#f5a623ff" opacity label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlColorPicker opacity label="Select a color" />;
|
||||
```
|
||||
|
||||
### Formats
|
||||
|
||||
Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl`, and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option.
|
||||
|
||||
To prevent users from toggling the format themselves, add the `no-format-toggle` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker format="hex" value="#4a90e2" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker format="rgb" value="rgb(80, 227, 194)" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker format="hsl" value="hsl(290, 87%, 47%)" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker format="hsv" value="hsv(55, 89%, 97%)" label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlColorPicker format="hex" value="#4a90e2" />
|
||||
<SlColorPicker format="rgb" value="rgb(80, 227, 194)" />
|
||||
<SlColorPicker format="hsl" value="hsl(290, 87%, 47%)" />
|
||||
<SlColorPicker format="hsv" value="hsv(55, 89%, 97%)" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Swatches
|
||||
|
||||
Use the `swatches` attribute to add convenient presets to the color picker. Any format the color picker can parse is acceptable (including CSS color names), but each value must be separated by a semicolon (`;`). Alternatively, you can pass an array of color values to this property using JavaScript.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker
|
||||
label="Select a color"
|
||||
swatches="
|
||||
#d0021b; #f5a623; #f8e71c; #8b572a; #7ed321; #417505; #bd10e0; #9013fe;
|
||||
#4a90e2; #50e3c2; #b8e986; #000; #444; #888; #ccc; #fff;
|
||||
"
|
||||
></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlColorPicker
|
||||
label="Select a color"
|
||||
swatches="
|
||||
#d0021b; #f5a623; #f8e71c; #8b572a; #7ed321; #417505; #bd10e0; #9013fe;
|
||||
#4a90e2; #50e3c2; #b8e986; #000; #444; #888; #ccc; #fff;
|
||||
"
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change the color picker's trigger size.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker size="small" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker size="medium" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker size="large" label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlColorPicker size="small" label="Select a color" />
|
||||
<SlColorPicker size="medium" label="Select a color" />
|
||||
<SlColorPicker size="large" label="Select a color" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Inline
|
||||
|
||||
The color picker can be rendered inline instead of in a dropdown using the `inline` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker inline label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlColorPicker inline label="Select a color" />;
|
||||
```
|
||||
|
||||
[component-metadata:sl-color-picker]
|
||||
@@ -1,316 +0,0 @@
|
||||
<!-- cspell:dictionaries lorem-ipsum -->
|
||||
|
||||
# Dialog
|
||||
|
||||
[component-header:sl-dialog]
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-overview">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-overview');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## UX Tips
|
||||
|
||||
- Use a dialog when you immediately require the user's attention, e.g. confirming a destructive action.
|
||||
- Always provide an obvious way for the user to dismiss the dialog.
|
||||
- Don't nest dialogs. It almost always leads to a poor experience for the user.
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Width
|
||||
|
||||
Use the `--width` custom property to set the dialog's width.
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-width" style="--width: 50vw;">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-width');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} style={{ '--width': '50vw' }} onSlAfterHide={() => setOpen(false)}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Scrolling
|
||||
|
||||
By design, a dialog's height will never exceed that of the viewport. As such, dialogs will not scroll with the page ensuring the header and footer are always accessible to the user.
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-scrolling">
|
||||
<div style="height: 150vh; border: dashed 2px var(--sl-color-neutral-200); padding: 0 1rem;">
|
||||
<p>Scroll down and give it a try! 👇</p>
|
||||
</div>
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-scrolling');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<div
|
||||
style={{
|
||||
height: '150vh',
|
||||
border: 'dashed 2px var(--sl-color-neutral-200)',
|
||||
padding: '0 1rem'
|
||||
}}
|
||||
>
|
||||
<p>Scroll down and give it a try! 👇</p>
|
||||
</div>
|
||||
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Header Actions
|
||||
|
||||
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed.
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-header-actions">
|
||||
<sl-icon-button class="new-window" slot="header-actions" name="box-arrow-up-right"></sl-icon-button>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-header-actions');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
const newWindowButton = dialog.querySelector('.new-window');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
newWindowButton.addEventListener('click', () => window.open(location.href));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog, SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlIconButton
|
||||
class="new-window"
|
||||
slot="header-actions"
|
||||
name="box-arrow-up-right"
|
||||
onClick={() => window.open(location.href)}
|
||||
/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Preventing the Dialog from Closing
|
||||
|
||||
By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the <kbd>Escape</kbd> key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
|
||||
|
||||
To keep the dialog open in such cases, you can cancel the `sl-request-close` event. When canceled, the dialog will remain open and pulse briefly to draw the user's attention to it.
|
||||
|
||||
You can use `event.detail.source` to determine what triggered the request to close. This example prevents the dialog from closing when the overlay is clicked, but allows the close button or <kbd>Escape</kbd> to dismiss it.
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-deny-close">
|
||||
This dialog will not close when you click on the overlay.
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-deny-close');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
|
||||
// Prevent the dialog from closing when the user clicks on the overlay
|
||||
dialog.addEventListener('sl-request-close', event => {
|
||||
if (event.detail.source === 'overlay') {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Prevent the dialog from closing when the user clicks on the overlay
|
||||
function handleRequestClose(event) {
|
||||
if (event.detail.source === 'overlay') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlRequestClose={handleRequestClose} onSlAfterHide={() => setOpen(false)}>
|
||||
This dialog will not close when you click on the overlay.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Customizing Initial Focus
|
||||
|
||||
By default, the dialog's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the dialog. If you want a different element to have focus, add the `autofocus` attribute to it as shown below.
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-focus">
|
||||
<sl-input autofocus placeholder="I will have focus when the dialog is opened"></sl-input>
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-focus');
|
||||
const input = dialog.querySelector('sl-input');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlInput autofocus placeholder="I will have focus when the dialog is opened" />
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
?> You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler.
|
||||
|
||||
[component-metadata:sl-dialog]
|
||||
@@ -1,135 +0,0 @@
|
||||
# Divider
|
||||
|
||||
[component-header:sl-divider]
|
||||
|
||||
```html preview
|
||||
<sl-divider></sl-divider>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlDivider />;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Width
|
||||
|
||||
Use the `--width` custom property to change the width of the divider.
|
||||
|
||||
```html preview
|
||||
<sl-divider style="--width: 4px;"></sl-divider>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlDivider style={{ '--width': '4px' }} />;
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
Use the `--color` custom property to change the color of the divider.
|
||||
|
||||
```html preview
|
||||
<sl-divider style="--color: tomato;"></sl-divider>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlDivider style={{ '--color': 'tomato' }} />;
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements.
|
||||
|
||||
```html preview
|
||||
<div style="text-align: center;">
|
||||
Above
|
||||
<sl-divider style="--spacing: 2rem;"></sl-divider>
|
||||
Below
|
||||
</div>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
Above
|
||||
<SlDivider style={{ '--spacing': '2rem' }} />
|
||||
Below
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Vertical
|
||||
|
||||
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.
|
||||
|
||||
```html preview
|
||||
<div style="display: flex; align-items: center; height: 2rem;">
|
||||
First
|
||||
<sl-divider vertical></sl-divider>
|
||||
Middle
|
||||
<sl-divider vertical></sl-divider>
|
||||
Last
|
||||
</div>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '2rem'
|
||||
}}
|
||||
>
|
||||
First
|
||||
<SlDivider vertical />
|
||||
Middle
|
||||
<SlDivider vertical />
|
||||
Last
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Menu Dividers
|
||||
|
||||
Use dividers in [menus](/components/menu) to visually group menu items.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item value="1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="4">Option 4</sl-menu-item>
|
||||
<sl-menu-item value="5">Option 5</sl-menu-item>
|
||||
<sl-menu-item value="6">Option 6</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem value="1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="3">Option 3</SlMenuItem>
|
||||
<sl-divider />
|
||||
<SlMenuItem value="4">Option 4</SlMenuItem>
|
||||
<SlMenuItem value="5">Option 5</SlMenuItem>
|
||||
<SlMenuItem value="6">Option 6</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-divider]
|
||||
@@ -1,364 +0,0 @@
|
||||
# Dropdown
|
||||
|
||||
[component-header:sl-dropdown]
|
||||
|
||||
Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it.
|
||||
|
||||
Dropdowns are designed to work well with [menus](/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker) and [select](/components/select)). The API gives you complete control over showing, hiding, and positioning the panel.
|
||||
|
||||
```html preview
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Dropdown</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Dropdown Item 1</sl-menu-item>
|
||||
<sl-menu-item>Dropdown Item 2</sl-menu-item>
|
||||
<sl-menu-item>Dropdown Item 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item type="checkbox" checked>Checkbox</sl-menu-item>
|
||||
<sl-menu-item disabled>Disabled</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>
|
||||
Prefix
|
||||
<sl-icon slot="prefix" name="gift"></sl-icon>
|
||||
</sl-menu-item>
|
||||
<sl-menu-item>
|
||||
Suffix Icon
|
||||
<sl-icon slot="suffix" name="heart"></sl-icon>
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDivider, SlDropdown, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlDropdown>
|
||||
<SlButton slot="trigger" caret>
|
||||
Dropdown
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Dropdown Item 1</SlMenuItem>
|
||||
<SlMenuItem>Dropdown Item 2</SlMenuItem>
|
||||
<SlMenuItem>Dropdown Item 3</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Checkbox
|
||||
</SlMenuItem>
|
||||
<SlMenuItem disabled>Disabled</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>
|
||||
Prefix
|
||||
<SlIcon slot="prefix" name="gift" />
|
||||
</SlMenuItem>
|
||||
<SlMenuItem>
|
||||
Suffix Icon
|
||||
<SlIcon slot="suffix" name="heart" />
|
||||
</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the [`sl-select`](/components/menu#events) event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleSelect(event) {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlDropdown>
|
||||
<SlButton slot="trigger" caret>
|
||||
Edit
|
||||
</SlButton>
|
||||
<SlMenu onSlSelect={handleSelect}>
|
||||
<SlMenuItem value="cut">Cut</SlMenuItem>
|
||||
<SlMenuItem value="copy">Copy</SlMenuItem>
|
||||
<SlMenuItem value="paste">Paste</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection-alt">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection-alt');
|
||||
const cut = container.querySelector('sl-menu-item[value="cut"]');
|
||||
const copy = container.querySelector('sl-menu-item[value="copy"]');
|
||||
const paste = container.querySelector('sl-menu-item[value="paste"]');
|
||||
|
||||
cut.addEventListener('click', () => console.log('cut'));
|
||||
copy.addEventListener('click', () => console.log('copy'));
|
||||
paste.addEventListener('click', () => console.log('paste'));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleCut() {
|
||||
console.log('cut');
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
console.log('copy');
|
||||
}
|
||||
|
||||
function handlePaste() {
|
||||
console.log('paste');
|
||||
}
|
||||
|
||||
return (
|
||||
<SlDropdown>
|
||||
<SlButton slot="trigger" caret>
|
||||
Edit
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem onClick={handleCut}>Cut</SlMenuItem>
|
||||
<SlMenuItem onClick={handleCopy}>Copy</SlMenuItem>
|
||||
<SlMenuItem onClick={handlePaste}>Paste</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
|
||||
|
||||
```html preview
|
||||
<sl-dropdown placement="top-start">
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Cut</sl-menu-item>
|
||||
<sl-menu-item>Copy</sl-menu-item>
|
||||
<sl-menu-item>Paste</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>Find</sl-menu-item>
|
||||
<sl-menu-item>Replace</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDivider, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlDropdown placement="top-start">
|
||||
<SlButton slot="trigger" caret>
|
||||
Edit
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Cut</SlMenuItem>
|
||||
<SlMenuItem>Copy</SlMenuItem>
|
||||
<SlMenuItem>Paste</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>Find</SlMenuItem>
|
||||
<SlMenuItem>Replace</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
### Distance
|
||||
|
||||
The distance from the panel to the trigger can be customized using the `distance` attribute. This value is specified in pixels.
|
||||
|
||||
```html preview
|
||||
<sl-dropdown distance="30">
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Cut</sl-menu-item>
|
||||
<sl-menu-item>Copy</sl-menu-item>
|
||||
<sl-menu-item>Paste</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>Find</sl-menu-item>
|
||||
<sl-menu-item>Replace</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDivider, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlDropdown distance={30}>
|
||||
<SlButton slot="trigger" caret>
|
||||
Edit
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Cut</SlMenuItem>
|
||||
<SlMenuItem>Copy</SlMenuItem>
|
||||
<SlMenuItem>Paste</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>Find</SlMenuItem>
|
||||
<SlMenuItem>Replace</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
### Skidding
|
||||
|
||||
The offset of the panel along the trigger can be customized using the `skidding` attribute. This value is specified in pixels.
|
||||
|
||||
```html preview
|
||||
<sl-dropdown skidding="30">
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Cut</sl-menu-item>
|
||||
<sl-menu-item>Copy</sl-menu-item>
|
||||
<sl-menu-item>Paste</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>Find</sl-menu-item>
|
||||
<sl-menu-item>Replace</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDivider, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlDropdown skidding={30}>
|
||||
<SlButton slot="trigger" caret>
|
||||
Edit
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Cut</SlMenuItem>
|
||||
<SlMenuItem>Copy</SlMenuItem>
|
||||
<SlMenuItem>Paste</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>Find</SlMenuItem>
|
||||
<SlMenuItem>Replace</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
### Hoisting
|
||||
|
||||
Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its containing block, which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-hoist">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>No Hoist</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
|
||||
<sl-dropdown hoist>
|
||||
<sl-button slot="trigger" caret>Hoist</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropdown-hoist {
|
||||
position: relative;
|
||||
border: solid 2px var(--sl-panel-border-color);
|
||||
padding: var(--sl-spacing-medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlDivider, SlDropdown, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.dropdown-hoist {
|
||||
border: solid 2px var(--sl-panel-border-color);
|
||||
padding: var(--sl-spacing-medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div className="dropdown-hoist">
|
||||
<SlDropdown>
|
||||
<SlButton slot="trigger" caret>
|
||||
No Hoist
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Item 1</SlMenuItem>
|
||||
<SlMenuItem>Item 2</SlMenuItem>
|
||||
<SlMenuItem>Item 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
|
||||
<SlDropdown hoist>
|
||||
<SlButton slot="trigger" caret>
|
||||
Hoist
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem>Item 1</SlMenuItem>
|
||||
<SlMenuItem>Item 2</SlMenuItem>
|
||||
<SlMenuItem>Item 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-dropdown]
|
||||
@@ -1,127 +0,0 @@
|
||||
# Format Bytes
|
||||
|
||||
[component-header:sl-format-bytes]
|
||||
|
||||
```html preview
|
||||
<div class="format-bytes-overview">
|
||||
The file is <sl-format-bytes value="1000"></sl-format-bytes> in size. <br /><br />
|
||||
<sl-input type="number" value="1000" label="Number to Format" style="max-width: 180px;"></sl-input>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.format-bytes-overview');
|
||||
const formatter = container.querySelector('sl-format-bytes');
|
||||
const input = container.querySelector('sl-input');
|
||||
|
||||
input.addEventListener('sl-input', () => (formatter.value = input.value || 0));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlFormatBytes, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState(1000);
|
||||
|
||||
return (
|
||||
<>
|
||||
The file is <SlFormatBytes value={value} /> in size.
|
||||
<br />
|
||||
<br />
|
||||
<SlInput
|
||||
type="number"
|
||||
value={value}
|
||||
label="Number to Format"
|
||||
style={{ maxWidth: '180px' }}
|
||||
onSlInput={event => setValue(event.target.value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Formatting Bytes
|
||||
|
||||
Set the `value` attribute to a number to get the value in bytes.
|
||||
|
||||
```html preview
|
||||
<sl-format-bytes value="12"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200000"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200000000"></sl-format-bytes>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlFormatBytes value="12" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200000" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200000000" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Formatting Bits
|
||||
|
||||
To get the value in bits, set the `unit` attribute to `bit`.
|
||||
|
||||
```html preview
|
||||
<sl-format-bytes value="12" unit="bit"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200" unit="bit"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200000" unit="bit"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200000000" unit="bit"></sl-format-bytes>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlFormatBytes value="12" unit="bit" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200" unit="bit" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200000" unit="bit" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200000000" unit="bit" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Localization
|
||||
|
||||
Use the `lang` attribute to set the number formatting locale.
|
||||
|
||||
```html preview
|
||||
<sl-format-bytes value="12" lang="de"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200" lang="de"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200000" lang="de"></sl-format-bytes><br />
|
||||
<sl-format-bytes value="1200000000" lang="de"></sl-format-bytes>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlFormatBytes value="12" lang="de" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200" lang="de" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200000" lang="de" />
|
||||
<br />
|
||||
<SlFormatBytes value="1200000000" lang="de" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-format-bytes]
|
||||
@@ -1,124 +0,0 @@
|
||||
# Format Date
|
||||
|
||||
[component-header:sl-format-date]
|
||||
|
||||
Localization is handled by the browser's [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). No language packs are required.
|
||||
|
||||
```html preview
|
||||
<!-- Shoelace 2 release date 🎉 -->
|
||||
<sl-format-date date="2020-07-15T09:17:00-04:00"></sl-format-date>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlFormatDate date="2020-07-15T09:17:00-04:00" />;
|
||||
```
|
||||
|
||||
The `date` attribute determines the date/time to use when formatting. It must be a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object set via JavaScript. If omitted, the current date/time will be assumed.
|
||||
|
||||
?> When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, always use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure the date will be parsed properly by all clients.
|
||||
|
||||
## Examples
|
||||
|
||||
### Date & Time Formatting
|
||||
|
||||
Formatting options are based on those found in the [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). When formatting options are provided, the date/time will be formatted according to those values. When no formatting options are provided, a localized, numeric date will be displayed instead.
|
||||
|
||||
```html preview
|
||||
<!-- Human-readable date -->
|
||||
<sl-format-date month="long" day="numeric" year="numeric"></sl-format-date><br />
|
||||
|
||||
<!-- Time -->
|
||||
<sl-format-date hour="numeric" minute="numeric"></sl-format-date><br />
|
||||
|
||||
<!-- Weekday -->
|
||||
<sl-format-date weekday="long"></sl-format-date><br />
|
||||
|
||||
<!-- Month -->
|
||||
<sl-format-date month="long"></sl-format-date><br />
|
||||
|
||||
<!-- Year -->
|
||||
<sl-format-date year="numeric"></sl-format-date><br />
|
||||
|
||||
<!-- No formatting options -->
|
||||
<sl-format-date></sl-format-date>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
{/* Human-readable date */}
|
||||
<SlFormatDate month="long" day="numeric" year="numeric" />
|
||||
<br />
|
||||
|
||||
{/* Time */}
|
||||
<SlFormatDate hour="numeric" minute="numeric" />
|
||||
<br />
|
||||
|
||||
{/* Weekday */}
|
||||
<SlFormatDate weekday="long" />
|
||||
<br />
|
||||
|
||||
{/* Month */}
|
||||
<SlFormatDate month="long" />
|
||||
<br />
|
||||
|
||||
{/* Year */}
|
||||
<SlFormatDate year="numeric" />
|
||||
<br />
|
||||
|
||||
{/* No formatting options */}
|
||||
<SlFormatDate />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Hour Formatting
|
||||
|
||||
By default, the browser will determine whether to use 12-hour or 24-hour time. To force one or the other, set the `hour-format` attribute to `12` or `24`.
|
||||
|
||||
```html preview
|
||||
<sl-format-date hour="numeric" minute="numeric" hour-format="12"></sl-format-date><br />
|
||||
<sl-format-date hour="numeric" minute="numeric" hour-format="24"></sl-format-date>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlFormatDate hour="numeric" minute="numeric" hour-format="12" />
|
||||
<br />
|
||||
<SlFormatDate hour="numeric" minute="numeric" hour-format="24" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Localization
|
||||
|
||||
Use the `lang` attribute to set the date/time formatting locale.
|
||||
|
||||
```html preview
|
||||
English: <sl-format-date lang="en"></sl-format-date><br />
|
||||
French: <sl-format-date lang="fr"></sl-format-date><br />
|
||||
Russian: <sl-format-date lang="ru"></sl-format-date>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
English: <SlFormatDate lang="en" />
|
||||
<br />
|
||||
French: <SlFormatDate lang="fr" />
|
||||
<br />
|
||||
Russian: <SlFormatDate lang="ru" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-format-date]
|
||||
@@ -1,133 +0,0 @@
|
||||
# Format Number
|
||||
|
||||
[component-header:sl-format-number]
|
||||
|
||||
Localization is handled by the browser's [`Intl.NumberFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required.
|
||||
|
||||
```html preview
|
||||
<div class="format-number-overview">
|
||||
<sl-format-number value="1000"></sl-format-number>
|
||||
<br /><br />
|
||||
<sl-input type="number" value="1000" label="Number to Format" style="max-width: 180px;"></sl-input>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.format-number-overview');
|
||||
const formatter = container.querySelector('sl-format-number');
|
||||
const input = container.querySelector('sl-input');
|
||||
|
||||
input.addEventListener('sl-input', () => (formatter.value = input.value || 0));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlFormatNumber, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState(1000);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlFormatNumber value={value} />
|
||||
<br />
|
||||
<br />
|
||||
<SlInput
|
||||
type="number"
|
||||
value={value}
|
||||
label="Number to Format"
|
||||
style={{ maxWidth: '180px' }}
|
||||
onSlInput={event => setValue(event.target.value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Percentages
|
||||
|
||||
To get the value as a percent, set the `type` attribute to `percent`.
|
||||
|
||||
```html preview
|
||||
<sl-format-number type="percent" value="0"></sl-format-number><br />
|
||||
<sl-format-number type="percent" value="0.25"></sl-format-number><br />
|
||||
<sl-format-number type="percent" value="0.50"></sl-format-number><br />
|
||||
<sl-format-number type="percent" value="0.75"></sl-format-number><br />
|
||||
<sl-format-number type="percent" value="1"></sl-format-number>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatNumber } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlFormatNumber type="percent" value={0} />
|
||||
<br />
|
||||
<SlFormatNumber type="percent" value={0.25} />
|
||||
<br />
|
||||
<SlFormatNumber type="percent" value={0.5} />
|
||||
<br />
|
||||
<SlFormatNumber type="percent" value={0.75} />
|
||||
<br />
|
||||
<SlFormatNumber type="percent" value={1} />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Localization
|
||||
|
||||
Use the `lang` attribute to set the number formatting locale.
|
||||
|
||||
```html preview
|
||||
English: <sl-format-number value="2000" lang="en" minimum-fraction-digits="2"></sl-format-number><br />
|
||||
German: <sl-format-number value="2000" lang="de" minimum-fraction-digits="2"></sl-format-number><br />
|
||||
Russian: <sl-format-number value="2000" lang="ru" minimum-fraction-digits="2"></sl-format-number>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatNumber } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
English: <SlFormatNumber value="2000" lang="en" minimum-fraction-digits="2" />
|
||||
<br />
|
||||
German: <SlFormatNumber value="2000" lang="de" minimum-fraction-digits="2" />
|
||||
<br />
|
||||
Russian: <SlFormatNumber value="2000" lang="ru" minimum-fraction-digits="2" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Currency
|
||||
|
||||
To format a number as a monetary value, set the `type` attribute to `currency` and set the `currency` attribute to the desired ISO 4217 currency code. You should also specify `lang` to ensure the the number is formatted correctly for the target locale.
|
||||
|
||||
```html preview
|
||||
<sl-format-number type="currency" currency="USD" value="2000" lang="en-US"></sl-format-number><br />
|
||||
<sl-format-number type="currency" currency="GBP" value="2000" lang="en-GB"></sl-format-number><br />
|
||||
<sl-format-number type="currency" currency="EUR" value="2000" lang="de"></sl-format-number><br />
|
||||
<sl-format-number type="currency" currency="RUB" value="2000" lang="ru"></sl-format-number><br />
|
||||
<sl-format-number type="currency" currency="CNY" value="2000" lang="zh-cn"></sl-format-number>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlFormatNumber } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlFormatNumber type="currency" currency="USD" value="2000" lang="en-US" />
|
||||
<br />
|
||||
<SlFormatNumber type="currency" currency="GBP" value="2000" lang="en-GB" />
|
||||
<br />
|
||||
<SlFormatNumber type="currency" currency="EUR" value="2000" lang="de" />
|
||||
<br />
|
||||
<SlFormatNumber type="currency" currency="RUB" value="2000" lang="ru" />
|
||||
<br />
|
||||
<SlFormatNumber type="currency" currency="CNY" value="2000" lang="zh-cn" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-format-number]
|
||||
@@ -1,147 +0,0 @@
|
||||
# Icon Button
|
||||
|
||||
[component-header:sl-icon-button]
|
||||
|
||||
For a full list of icons that come bundled with Shoelace, refer to the [icon component](/components/icon).
|
||||
|
||||
```html preview
|
||||
<sl-icon-button name="gear" label="Settings"></sl-icon-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlIconButton name="gear" label="Settings" />;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
Icon buttons inherit their parent element's `font-size`.
|
||||
|
||||
```html preview
|
||||
<sl-icon-button name="pencil" label="Edit" style="font-size: 1.5rem;"></sl-icon-button>
|
||||
<sl-icon-button name="pencil" label="Edit" style="font-size: 2rem;"></sl-icon-button>
|
||||
<sl-icon-button name="pencil" label="Edit" style="font-size: 2.5rem;"></sl-icon-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlIconButton name="pencil" label="Edit" style={{ fontSize: '1.5rem' }} />
|
||||
<SlIconButton name="pencil" label="Edit" style={{ fontSize: '2rem' }} />
|
||||
<SlIconButton name="pencil" label="Edit" style={{ fontSize: '2.5rem' }} />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
Icon buttons are designed to have a uniform appearance, so their color is not inherited. However, you can still customize them by styling the `base` part.
|
||||
|
||||
```html preview
|
||||
<div class="icon-button-color">
|
||||
<sl-icon-button name="type-bold" label="Bold"></sl-icon-button>
|
||||
<sl-icon-button name="type-italic" label="Italic"></sl-icon-button>
|
||||
<sl-icon-button name="type-underline" label="Underline"></sl-icon-button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.icon-button-color sl-icon-button::part(base) {
|
||||
color: #b00091;
|
||||
}
|
||||
|
||||
.icon-button-color sl-icon-button::part(base):hover,
|
||||
.icon-button-color sl-icon-button::part(base):focus {
|
||||
color: #c913aa;
|
||||
}
|
||||
|
||||
.icon-button-color sl-icon-button::part(base):active {
|
||||
color: #960077;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.icon-button-color sl-icon-button::part(base) {
|
||||
color: #b00091;
|
||||
}
|
||||
|
||||
.icon-button-color sl-icon-button::part(base):hover,
|
||||
.icon-button-color sl-icon-button::part(base):focus {
|
||||
color: #c913aa;
|
||||
}
|
||||
|
||||
.icon-button-color sl-icon-button::part(base):active {
|
||||
color: #960077;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div className="icon-button-color">
|
||||
<SlIconButton name="type-bold" label="Bold" />
|
||||
<SlIconButton name="type-italic" label="Italic" />
|
||||
<SlIconButton name="type-underline" label="Underline" />
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Link Buttons
|
||||
|
||||
Use the `href` attribute to convert the button to a link.
|
||||
|
||||
```html preview
|
||||
<sl-icon-button name="gear" label="Settings" href="https://example.com" target="_blank"></sl-icon-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlIconButton name="gear" label="Settings" href="https://example.com" target="_blank" />;
|
||||
```
|
||||
|
||||
### Icon Button with Tooltip
|
||||
|
||||
Wrap a tooltip around an icon button to provide contextual information to the user.
|
||||
|
||||
```html preview
|
||||
<sl-tooltip content="Settings">
|
||||
<sl-icon-button name="gear" label="Settings"></sl-icon-button>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIconButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTooltip content="Settings">
|
||||
<SlIconButton name="gear" label="Settings" />
|
||||
</SlTooltip>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable the icon button.
|
||||
|
||||
```html preview
|
||||
<sl-icon-button name="gear" label="Settings" disabled></sl-icon-button>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlIconButton name="gear" label="Settings" disabled />;
|
||||
```
|
||||
|
||||
[component-metadata:sl-icon-button]
|
||||
@@ -1,47 +0,0 @@
|
||||
# Include
|
||||
|
||||
[component-header:sl-include]
|
||||
|
||||
Included files are asynchronously requested using `window.fetch()`. Requests are cached, so the same file can be included multiple times, but only one request will be made.
|
||||
|
||||
The included content will be inserted into the `<sl-include>` element's default slot so it can be easily accessed and styled through the light DOM.
|
||||
|
||||
```html preview
|
||||
<sl-include src="https://shoelace.style/assets/examples/include.html"></sl-include>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInclude } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInclude src="https://shoelace.style/assets/examples/include.html" />;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Listening for Events
|
||||
|
||||
When an include file loads successfully, the `sl-load` event will be emitted. You can listen for this event to add custom loading logic to your includes.
|
||||
|
||||
If the request fails, the `sl-error` event will be emitted. In this case, `event.detail.status` will contain the resulting HTTP status code of the request, e.g. 404 (not found).
|
||||
|
||||
```html
|
||||
<sl-include src="https://shoelace.style/assets/examples/include.html"></sl-include>
|
||||
|
||||
<script>
|
||||
const include = document.querySelector('sl-include');
|
||||
|
||||
include.addEventListener('sl-load', event => {
|
||||
if (event.eventPhase === Event.AT_TARGET) {
|
||||
console.log('Success');
|
||||
}
|
||||
});
|
||||
|
||||
include.addEventListener('sl-error', event => {
|
||||
if (event.eventPhase === Event.AT_TARGET) {
|
||||
console.log('Error', event.detail.status);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[component-metadata:sl-include]
|
||||
@@ -1,275 +0,0 @@
|
||||
# Input
|
||||
|
||||
[component-header:sl-input]
|
||||
|
||||
```html preview
|
||||
<sl-input></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput />;
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the input an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-input label="What is your name?"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput label="What is your name?" />;
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to an input with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-input label="Nickname" help-text="What would you like people to call you?"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput label="Nickname" help-text="What would you like people to call you?" />;
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
|
||||
Use the `placeholder` attribute to add a placeholder.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Type something"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput placeholder="Type something" />;
|
||||
```
|
||||
|
||||
### Clearable
|
||||
|
||||
Add the `clearable` attribute to add a clear button when the input has content.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Clearable" clearable></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput placeholder="Clearable" clearable />;
|
||||
```
|
||||
|
||||
### Toggle Password
|
||||
|
||||
Add the `password-toggle` attribute to add a toggle button that will show the password when activated.
|
||||
|
||||
```html preview
|
||||
<sl-input type="password" placeholder="Password Toggle" password-toggle></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput type="password" placeholder="Password Toggle" size="medium" password-toggle />;
|
||||
```
|
||||
|
||||
### Filled Inputs
|
||||
|
||||
Add the `filled` attribute to draw a filled input.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Type something" filled></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput placeholder="Type something" filled />;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an input.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Disabled" disabled></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput placeholder="Disabled" disabled />;
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change an input's size.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Small" size="small"></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Medium" size="medium"></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Large" size="large"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput placeholder="Small" size="small" />
|
||||
<br />
|
||||
<SlInput placeholder="Medium" size="medium" />
|
||||
<br />
|
||||
<SlInput placeholder="Large" size="large" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give inputs rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Small" size="small" pill></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Medium" size="medium" pill></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Large" size="large" pill></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput placeholder="Small" size="small" pill />
|
||||
<br />
|
||||
<SlInput placeholder="Medium" size="medium" pill />
|
||||
<br />
|
||||
<SlInput placeholder="Large" size="large" pill />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Input Types
|
||||
|
||||
The `type` attribute controls the type of input the browser renders.
|
||||
|
||||
```html preview
|
||||
<sl-input type="email" placeholder="Email"></sl-input>
|
||||
<br />
|
||||
<sl-input type="number" placeholder="Number"></sl-input>
|
||||
<br />
|
||||
<sl-input type="date" placeholder="Date"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput type="email" placeholder="Email" />
|
||||
<br />
|
||||
<SlInput type="number" placeholder="Number" />
|
||||
<br />
|
||||
<SlInput type="date" placeholder="Date" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix & Suffix Icons
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Small" size="small">
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-icon name="chat" slot="suffix"></sl-icon>
|
||||
</sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Medium" size="medium">
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-icon name="chat" slot="suffix"></sl-icon>
|
||||
</sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Large" size="large">
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-icon name="chat" slot="suffix"></sl-icon>
|
||||
</sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput placeholder="Small" size="small">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlIcon name="chat" slot="suffix"></SlIcon>
|
||||
</SlInput>
|
||||
<br />
|
||||
<SlInput placeholder="Medium" size="medium">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlIcon name="chat" slot="suffix"></SlIcon>
|
||||
</SlInput>
|
||||
<br />
|
||||
<SlInput placeholder="Large" size="large">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlIcon name="chat" slot="suffix"></SlIcon>
|
||||
</SlInput>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Customizing Label Position
|
||||
|
||||
Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the label to the left of the control, but the possible orientations are nearly endless. The same technique works for inputs, textareas, radio groups, and similar form controls.
|
||||
|
||||
```html preview
|
||||
<sl-input class="label-on-left" label="Name" help-text="Enter your name""></sl-input>
|
||||
<sl-input class="label-on-left" label="Email" type="email" help-text="Enter your email"></sl-input>
|
||||
<sl-textarea class="label-on-left" label="Bio" help-text="Tell us something about yourself"></sl-textarea>
|
||||
|
||||
<style>
|
||||
.label-on-left {
|
||||
--label-width: 3.75rem;
|
||||
--gap-width: 1rem;
|
||||
}
|
||||
|
||||
.label-on-left + .label-on-left {
|
||||
margin-top: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.label-on-left::part(form-control) {
|
||||
display: grid;
|
||||
grid: auto / var(--label-width) 1fr;
|
||||
gap: var(--sl-spacing-3x-small) var(--gap-width);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-on-left::part(form-control-label) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.label-on-left::part(form-control-help-text) {
|
||||
grid-column-start: 2;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-input]
|
||||
@@ -1,214 +0,0 @@
|
||||
# Menu Item
|
||||
|
||||
[component-header:sl-menu-item]
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item>Option 1</sl-menu-item>
|
||||
<sl-menu-item>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item type="checkbox" checked>Checkbox</sl-menu-item>
|
||||
<sl-menu-item disabled>Disabled</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>
|
||||
Prefix Icon
|
||||
<sl-icon slot="prefix" name="gift"></sl-icon>
|
||||
</sl-menu-item>
|
||||
<sl-menu-item>
|
||||
Suffix Icon
|
||||
<sl-icon slot="suffix" name="heart"></sl-icon>
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>Option 1</SlMenuItem>
|
||||
<SlMenuItem>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Checkbox
|
||||
</SlMenuItem>
|
||||
<SlMenuItem disabled>Disabled</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>
|
||||
Prefix Icon
|
||||
<SlIcon slot="prefix" name="gift" />
|
||||
</SlMenuItem>
|
||||
<SlMenuItem>
|
||||
Suffix Icon
|
||||
<SlIcon slot="suffix" name="heart" />
|
||||
</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Disabled
|
||||
|
||||
Add the `disabled` attribute to disable the menu item so it cannot be selected.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item>Option 1</sl-menu-item>
|
||||
<sl-menu-item disabled>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>Option 1</SlMenuItem>
|
||||
<SlMenuItem disabled>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="house"></sl-icon>
|
||||
Home
|
||||
</sl-menu-item>
|
||||
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="envelope"></sl-icon>
|
||||
Messages
|
||||
<sl-badge slot="suffix" variant="primary" pill>12</sl-badge>
|
||||
</sl-menu-item>
|
||||
|
||||
<sl-divider></sl-divider>
|
||||
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="gear"></sl-icon>
|
||||
Settings
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlBadge, SlDivider, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>
|
||||
<SlIcon slot="prefix" name="house" />
|
||||
Home
|
||||
</SlMenuItem>
|
||||
|
||||
<SlMenuItem>
|
||||
<SlIcon slot="prefix" name="envelope" />
|
||||
Messages
|
||||
<SlBadge slot="suffix" variant="primary" pill>
|
||||
12
|
||||
</SlBadge>
|
||||
</SlMenuItem>
|
||||
|
||||
<SlDivider />
|
||||
|
||||
<SlMenuItem>
|
||||
<SlIcon slot="prefix" name="gear" />
|
||||
Settings
|
||||
</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
### Checkbox Menu Items
|
||||
|
||||
Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state.
|
||||
|
||||
Checkbox menu items are visually indistinguishable from regular menu items. Their ability to be toggled is primarily inferred from context, much like you'd find in the menu of a native app.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item type="checkbox">Autosave</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" checked>Check Spelling</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Word Wrap</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem type="checkbox">Autosave</SlMenuItem>
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Check Spelling
|
||||
</SlMenuItem>
|
||||
<SlMenuItem type="checkbox">Word Wrap</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
### Value & Selection
|
||||
|
||||
The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `sl-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more.
|
||||
|
||||
```html preview
|
||||
<sl-menu class="menu-value" style="max-width: 200px;">
|
||||
<sl-menu-item value="opt-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="opt-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="opt-3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item type="checkbox" value="opt-4">Checkbox 4</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="opt-5">Checkbox 5</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="opt-6">Checkbox 6</sl-menu-item>
|
||||
</sl-menu>
|
||||
|
||||
<script>
|
||||
const menu = document.querySelector('.menu-value');
|
||||
|
||||
menu.addEventListener('sl-select', event => {
|
||||
const item = event.detail.item;
|
||||
|
||||
// Log value
|
||||
if (item.type === 'checkbox') {
|
||||
console.log(`Selected value: ${item.value} (${item.checked ? 'checked' : 'unchecked'})`);
|
||||
} else {
|
||||
console.log(`Selected value: ${item.value}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleSelect(event) {
|
||||
const item = event.detail.item;
|
||||
|
||||
// Toggle checked state
|
||||
item.checked = !item.checked;
|
||||
|
||||
// Log value
|
||||
console.log(`Selected value: ${item.value}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlMenu style={{ maxWidth: '200px' }} onSlSelect={handleSelect}>
|
||||
<SlMenuItem value="opt-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="opt-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="opt-3">Option 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-menu-item]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Menu Label
|
||||
|
||||
[component-header:sl-menu-label]
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-label>Fruits</sl-menu-label>
|
||||
<sl-menu-item value="apple">Apple</sl-menu-item>
|
||||
<sl-menu-item value="banana">Banana</sl-menu-item>
|
||||
<sl-menu-item value="orange">Orange</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-label>Vegetables</sl-menu-label>
|
||||
<sl-menu-item value="broccoli">Broccoli</sl-menu-item>
|
||||
<sl-menu-item value="carrot">Carrot</sl-menu-item>
|
||||
<sl-menu-item value="zucchini">Zucchini</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenu, SlMenuLabel, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuLabel>Fruits</SlMenuLabel>
|
||||
<SlMenuItem value="apple">Apple</SlMenuItem>
|
||||
<SlMenuItem value="banana">Banana</SlMenuItem>
|
||||
<SlMenuItem value="orange">Orange</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuLabel>Vegetables</SlMenuLabel>
|
||||
<SlMenuItem value="broccoli">Broccoli</SlMenuItem>
|
||||
<SlMenuItem value="carrot">Carrot</SlMenuItem>
|
||||
<SlMenuItem value="zucchini">Zucchini</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-menu-label]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Menu
|
||||
|
||||
[component-header:sl-menu]
|
||||
|
||||
You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem value="undo">Undo</SlMenuItem>
|
||||
<SlMenuItem value="redo">Redo</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem value="cut">Cut</SlMenuItem>
|
||||
<SlMenuItem value="copy">Copy</SlMenuItem>
|
||||
<SlMenuItem value="paste">Paste</SlMenuItem>
|
||||
<SlMenuItem value="delete">Delete</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
?> Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `<nav>` and `<a>` elements instead.
|
||||
|
||||
[component-metadata:sl-menu]
|
||||
@@ -1,79 +0,0 @@
|
||||
# Option
|
||||
|
||||
[component-header:sl-option]
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an option and prevent it from being selected.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2" disabled>Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2" disabled>
|
||||
Option 2
|
||||
</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add icons to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">
|
||||
<sl-icon slot="prefix" name="envelope"></sl-icon>
|
||||
Email
|
||||
<sl-icon slot="suffix" name="patch-check"></sl-icon>
|
||||
</sl-option>
|
||||
|
||||
<sl-option value="option-2">
|
||||
<sl-icon slot="prefix" name="telephone"></sl-icon>
|
||||
Phone
|
||||
<sl-icon slot="suffix" name="patch-check"></sl-icon>
|
||||
</sl-option>
|
||||
|
||||
<sl-option value="option-3">
|
||||
<sl-icon slot="prefix" name="chat-dots"></sl-icon>
|
||||
Chat
|
||||
<sl-icon slot="suffix" name="patch-check"></sl-icon>
|
||||
</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
[component-metadata:sl-option]
|
||||
@@ -1,122 +0,0 @@
|
||||
# Progress Bar
|
||||
|
||||
[component-header:sl-progress-bar]
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50"></sl-progress-bar>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressBar value={50} />;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to label the progress bar and tell assistive devices how to announce it.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50" label="Upload progress"></sl-progress-bar>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressBar value="50" label="Upload progress" />;
|
||||
```
|
||||
|
||||
### Custom Height
|
||||
|
||||
Use the `--height` custom property to set the progress bar's height.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50" style="--height: 6px;"></sl-progress-bar>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressBar value={50} style={{ '--height': '6px' }} />;
|
||||
```
|
||||
|
||||
### Showing Values
|
||||
|
||||
Use the default slot to show a value.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50" class="progress-bar-values">50%</sl-progress-bar>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-button circle><sl-icon name="dash" label="Decrease"></sl-icon></sl-button>
|
||||
<sl-button circle><sl-icon name="plus" label="Increase"></sl-icon></sl-button>
|
||||
|
||||
<script>
|
||||
const progressBar = document.querySelector('.progress-bar-values');
|
||||
const subtractButton = progressBar.nextElementSibling.nextElementSibling;
|
||||
const addButton = subtractButton.nextElementSibling;
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
const value = Math.min(100, progressBar.value + 10);
|
||||
progressBar.value = value;
|
||||
progressBar.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
subtractButton.addEventListener('click', () => {
|
||||
const value = Math.max(0, progressBar.value - 10);
|
||||
progressBar.value = value;
|
||||
progressBar.textContent = `${value}%`;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlIcon, SlProgressBar } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState(50);
|
||||
|
||||
function adjustValue(amount) {
|
||||
let newValue = value + amount;
|
||||
if (newValue < 0) newValue = 0;
|
||||
if (newValue > 100) newValue = 100;
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlProgressBar value={value}>{value}%</SlProgressBar>
|
||||
|
||||
<br />
|
||||
|
||||
<SlButton circle onClick={() => adjustValue(-10)}>
|
||||
<SlIcon name="dash" label="Decrease" />
|
||||
</SlButton>
|
||||
|
||||
<SlButton circle onClick={() => adjustValue(10)}>
|
||||
<SlIcon name="plus" label="Increase" />
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Indeterminate
|
||||
|
||||
The `indeterminate` attribute can be used to inform the user that the operation is pending, but its status cannot currently be determined. In this state, `value` is ignored and the label, if present, will not be shown.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar indeterminate></sl-progress-bar>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressBar indeterminate />;
|
||||
```
|
||||
|
||||
[component-metadata:sl-progress-bar]
|
||||
@@ -1,152 +0,0 @@
|
||||
# Progress Ring
|
||||
|
||||
[component-header:sl-progress-ring]
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="25"></sl-progress-ring>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressRing value="25" />;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Size
|
||||
|
||||
Use the `--size` custom property to set the diameter of the progress ring.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" style="--size: 200px;"></sl-progress-ring>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressRing value="50" style={{ '--size': '200px' }} />;
|
||||
```
|
||||
|
||||
### Track and Indicator Width
|
||||
|
||||
Use the `--track-width` and `--indicator-width` custom properties to set the width of the progress ring's track and indicator.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" style="--track-width: 6px; --indicator-width: 12px;"></sl-progress-ring>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressRing value="50" style={{ '--track-width': '6px', '--indicator-width': '12px' }} />;
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
To change the color, use the `--track-color` and `--indicator-color` custom properties.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring
|
||||
value="50"
|
||||
style="
|
||||
--track-color: pink;
|
||||
--indicator-color: deeppink;
|
||||
"
|
||||
></sl-progress-ring>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlProgressRing
|
||||
value="50"
|
||||
style={{
|
||||
'--track-color': 'pink',
|
||||
'--indicator-color': 'deeppink'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to label the progress ring and tell assistive devices how to announce it.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" label="Upload progress"></sl-progress-ring>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlProgressRing value="50" label="Upload progress" />;
|
||||
```
|
||||
|
||||
### Showing Values
|
||||
|
||||
Use the default slot to show a label inside the progress ring.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" class="progress-ring-values" style="margin-bottom: .5rem;">50%</sl-progress-ring>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-button circle><sl-icon name="dash" label="Decrease"></sl-icon></sl-button>
|
||||
<sl-button circle><sl-icon name="plus" label="Increase"></sl-icon></sl-button>
|
||||
|
||||
<script>
|
||||
const progressRing = document.querySelector('.progress-ring-values');
|
||||
const subtractButton = progressRing.nextElementSibling.nextElementSibling;
|
||||
const addButton = subtractButton.nextElementSibling;
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
const value = Math.min(100, progressRing.value + 10);
|
||||
progressRing.value = value;
|
||||
progressRing.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
subtractButton.addEventListener('click', () => {
|
||||
const value = Math.max(0, progressRing.value - 10);
|
||||
progressRing.value = value;
|
||||
progressRing.textContent = `${value}%`;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlIcon, SlProgressRing } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState(50);
|
||||
|
||||
function adjustValue(amount) {
|
||||
let newValue = value + amount;
|
||||
if (newValue < 0) newValue = 0;
|
||||
if (newValue > 100) newValue = 100;
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlProgressRing value={value} style={{ marginBottom: '.5rem' }}>
|
||||
{value}%
|
||||
</SlProgressRing>
|
||||
|
||||
<br />
|
||||
|
||||
<SlButton circle onClick={() => adjustValue(-10)}>
|
||||
<SlIcon name="dash" label="Decrease" />
|
||||
</SlButton>
|
||||
|
||||
<SlButton circle onClick={() => adjustValue(10)}>
|
||||
<SlIcon name="plus" label="Increase" />
|
||||
</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-progress-ring]
|
||||
@@ -1,161 +0,0 @@
|
||||
# QR Code
|
||||
|
||||
[component-header:sl-qr-code]
|
||||
|
||||
QR codes are useful for providing small pieces of information to users who can quickly scan them with a smartphone. Most smartphones have built-in QR code scanners, so simply pointing the camera at a QR code will decode it and allow the user to visit a website, dial a phone number, read a message, etc.
|
||||
|
||||
```html preview
|
||||
<div class="qr-overview">
|
||||
<sl-qr-code value="https://shoelace.style/" label="Scan this code to visit Shoelace on the web!"></sl-qr-code>
|
||||
<br />
|
||||
|
||||
<sl-input maxlength="255" clearable label="Value"></sl-input>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.qr-overview');
|
||||
const qrCode = container.querySelector('sl-qr-code');
|
||||
const input = container.querySelector('sl-input');
|
||||
|
||||
customElements.whenDefined('sl-qr-code').then(() => {
|
||||
input.value = qrCode.value;
|
||||
input.addEventListener('sl-input', () => (qrCode.value = input.value));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.qr-overview {
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.qr-overview sl-input {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlQrCode, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.qr-overview {
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.qr-overview sl-input {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState('https://shoelace.style/');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="qr-overview">
|
||||
<SlQrCode value={value} label="Scan this code to visit Shoelace on the web!" />
|
||||
<br />
|
||||
|
||||
<SlInput maxlength="255" clearable onInput={event => setValue(event.target.value)} />
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Colors
|
||||
|
||||
Use the `fill` and `background` attributes to modify the QR code's colors. You should always ensure good contrast for optimal compatibility with QR code scanners.
|
||||
|
||||
```html preview
|
||||
<sl-qr-code value="https://shoelace.style/" fill="deeppink" background="white"></sl-qr-code>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlQrCode value="https://shoelace.style/" fill="deeppink" background="white" />;
|
||||
```
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` attribute to change the size of the QR code.
|
||||
|
||||
```html preview
|
||||
<sl-qr-code value="https://shoelace.style/" size="64"></sl-qr-code>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlQrCode value="https://shoelace.style/" size="64" />;
|
||||
```
|
||||
|
||||
### Radius
|
||||
|
||||
Create a rounded effect with the `radius` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-qr-code value="https://shoelace.style/" radius="0.5"></sl-qr-code>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlQrCode value="https://shoelace.style/" radius="0.5" />;
|
||||
```
|
||||
|
||||
### Error Correction
|
||||
|
||||
QR codes can be rendered with various levels of [error correction](https://www.qrcode.com/en/about/error_correction.html) that can be set using the `error-correction` attribute. This example generates four codes with the same value using different error correction levels.
|
||||
|
||||
```html preview
|
||||
<div class="qr-error-correction">
|
||||
<sl-qr-code value="https://shoelace.style/" error-correction="L"></sl-qr-code>
|
||||
<sl-qr-code value="https://shoelace.style/" error-correction="M"></sl-qr-code>
|
||||
<sl-qr-code value="https://shoelace.style/" error-correction="Q"></sl-qr-code>
|
||||
<sl-qr-code value="https://shoelace.style/" error-correction="H"></sl-qr-code>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.qr-error-correction {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.qr-error-correction {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="qr-error-correction">
|
||||
<SlQrCode value="https://shoelace.style/" error-correction="L" />
|
||||
<SlQrCode value="https://shoelace.style/" error-correction="M" />
|
||||
<SlQrCode value="https://shoelace.style/" error-correction="Q" />
|
||||
<SlQrCode value="https://shoelace.style/" error-correction="H" />
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-qr-code]
|
||||
@@ -1,295 +0,0 @@
|
||||
# Radio Button
|
||||
|
||||
[component-header:sl-radio-button]
|
||||
|
||||
Radio buttons are designed to be used with [radio groups](/components/radio-group). When a radio button has focus, the arrow keys can be used to change the selected option just like standard radio controls.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Checked States
|
||||
|
||||
To set the initial value and checked state, use the `value` attribute on the containing radio group.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a radio button.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2" disabled>Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2" disabled>
|
||||
Option 2
|
||||
</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a radio button's size.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button size="small" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button size="small" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="small" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button size="medium" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button size="medium" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="medium" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button size="large" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button size="large" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button size="large" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton size="small" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton size="small" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="small" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton size="medium" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton size="medium" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="medium" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton size="large" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton size="large" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton size="large" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill Buttons
|
||||
|
||||
Use the `pill` attribute to give radio buttons rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button pill size="small" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button pill size="small" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill size="small" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button pill size="medium" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button pill size="medium" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill size="medium" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button pill size="large" value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button pill size="large" value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill size="large" value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton pill size="small" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton pill size="small" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill size="small" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton pill size="medium" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton pill size="medium" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill size="medium" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
|
||||
<br />
|
||||
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton pill size="large" value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton pill size="large" value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill size="large" value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix and Suffix Icons
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio-button value="1">
|
||||
<sl-icon slot="prefix" name="archive"></sl-icon>
|
||||
Option 1
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button value="2">
|
||||
<sl-icon slot="suffix" name="bag"></sl-icon>
|
||||
Option 2
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button value="3">
|
||||
<sl-icon slot="prefix" name="gift"></sl-icon>
|
||||
<sl-icon slot="suffix" name="cart"></sl-icon>
|
||||
Option 3
|
||||
</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton value="1">
|
||||
<SlIcon slot="prefix" name="archive" />
|
||||
Option 1
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton value="2">
|
||||
<SlIcon slot="suffix" name="bag" />
|
||||
Option 2
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton value="3">
|
||||
<SlIcon slot="prefix" name="gift" />
|
||||
<SlIcon slot="suffix" name="cart" />
|
||||
Option 3
|
||||
</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Buttons with Icons
|
||||
|
||||
You can omit button labels and use icons instead. Make sure to set a `label` attribute on each icon so screen readers will announce each option correctly.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="neutral">
|
||||
<sl-radio-button value="angry">
|
||||
<sl-icon name="emoji-angry" label="Angry"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button value="sad">
|
||||
<sl-icon name="emoji-frown" label="Sad"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button value="neutral">
|
||||
<sl-icon name="emoji-neutral" label="Neutral"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button value="happy">
|
||||
<sl-icon name="emoji-smile" label="Happy"></sl-icon>
|
||||
</sl-radio-button>
|
||||
|
||||
<sl-radio-button value="laughing">
|
||||
<sl-icon name="emoji-laughing" label="Laughing"></sl-icon>
|
||||
</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="neutral">
|
||||
<SlRadioButton value="angry">
|
||||
<SlIcon name="emoji-angry" label="Angry" />
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton value="sad">
|
||||
<SlIcon name="emoji-frown" label="Sad" />
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton value="neutral">
|
||||
<SlIcon name="emoji-neutral" label="Neutral" />
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton value="happy">
|
||||
<SlIcon name="emoji-smile" label="Happy" />
|
||||
</SlRadioButton>
|
||||
|
||||
<SlRadioButton value="laughing">
|
||||
<SlIcon name="emoji-laughing" label="Laughing" />
|
||||
</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio-button]
|
||||
@@ -1,232 +0,0 @@
|
||||
# Radio Group
|
||||
|
||||
[component-header:sl-radio-group]
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2">Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2">Option 2</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a radio group with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" help-text="Choose the most appropriate option." name="a" value="1">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2">Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" help-text="Choose the most appropriate option." name="a" value="1">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2">Option 2</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Radio Buttons
|
||||
|
||||
[Radio buttons](/components/radio-button) offer an alternate way to display radio controls. In this case, an internal [button group](/components/button-group) is used to group the buttons into a single, cohesive control.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" help-text="Select an option that makes you proud." name="a" value="1">
|
||||
<sl-radio-button value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadioButton value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabling Options
|
||||
|
||||
Radios and radio buttons can be disabled by adding the `disabled` attribute to the respective options inside the radio group.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2" disabled>Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2" disabled>
|
||||
Option 2
|
||||
</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Setting the `required` attribute to make selecting an option mandatory. If a value has not been selected, it will prevent the form from submitting and display an error message.
|
||||
|
||||
```html preview
|
||||
<form class="validation">
|
||||
<sl-radio-group label="Select an option" name="a" required>
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2">Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.validation');
|
||||
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => {
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit}>
|
||||
<SlRadioGroup label="Select an option" name="a" required onSlChange={handleChange}>
|
||||
<SlRadio value="1">
|
||||
Option 1
|
||||
</SlRadio>
|
||||
<SlRadiovalue="2">
|
||||
Option 2
|
||||
</SlRadio>
|
||||
<SlRadio value="3">
|
||||
Option 3
|
||||
</SlRadio>
|
||||
</SlRadioGroup>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Validity
|
||||
|
||||
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
|
||||
|
||||
```html preview
|
||||
<form class="custom-validity">
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio value="1">Not me</sl-radio>
|
||||
<sl-radio value="2">Me neither</sl-radio>
|
||||
<sl-radio value="3">Choose me</sl-radio>
|
||||
</sl-radio-group>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.custom-validity');
|
||||
const radioGroup = form.querySelector('sl-radio-group');
|
||||
const errorMessage = 'You must choose the last option';
|
||||
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-radio').then(() => {
|
||||
radioGroup.setCustomValidity(errorMessage);
|
||||
});
|
||||
|
||||
// Update validity when a selection is made
|
||||
form.addEventListener('sl-change', () => {
|
||||
const isValid = radioGroup.value === '3';
|
||||
radioGroup.setCustomValidity(isValid ? '' : errorMessage);
|
||||
});
|
||||
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => {
|
||||
const radioGroup = useRef(null);
|
||||
const errorMessage = 'You must choose this option';
|
||||
|
||||
function handleChange() {
|
||||
radioGroup.current.setCustomValidity(radioGroup.current.value === '3' ? '' : errorMessage);
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
radio.current.setCustomValidity(errorMessage);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form class="custom-validity" onSubmit={handleSubmit}>
|
||||
<SlRadioGroup ref={radioGroup} label="Select an option" name="a" value="1" onSlChange={handleChange}>
|
||||
<SlRadio value="1">Not me</SlRadio>
|
||||
<SlRadio value="2">Me neither</SlRadio>
|
||||
<SlRadio value="3">Choose me</SlRadio>
|
||||
</SlRadioGroup>
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio-group]
|
||||
@@ -1,105 +0,0 @@
|
||||
# Radio
|
||||
|
||||
[component-header:sl-radio]
|
||||
|
||||
Radios are designed to be used with [radio groups](/components/radio-group).
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2">Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2">Option 2</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Initial Value
|
||||
|
||||
To set the initial value and checked state, use the `value` attribute on the containing radio group.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="3">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2">Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="3">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2">Option 2</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a radio.
|
||||
|
||||
```html preview
|
||||
<sl-radio-group label="Select an option" name="a" value="1">
|
||||
<sl-radio value="1">Option 1</sl-radio>
|
||||
<sl-radio value="2" disabled>Option 2</sl-radio>
|
||||
<sl-radio value="3">Option 3</sl-radio>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRadioGroup label="Select an option" name="a" value="1">
|
||||
<SlRadio value="1">Option 1</SlRadio>
|
||||
<SlRadio value="2" disabled>
|
||||
Option 2
|
||||
</SlRadio>
|
||||
<SlRadio value="3">Option 3</SlRadio>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
Use the `size` attribute to change a radio's size.
|
||||
|
||||
```html preview
|
||||
<sl-radio size="small">Small</sl-radio>
|
||||
<sl-radio size="medium">Medium</sl-radio>
|
||||
<sl-radio size="large">Large</sl-radio>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlRadio size="small">Small</SlRadio>
|
||||
<br />
|
||||
<SlRadio size="medium">Medium</SlRadio>
|
||||
<br />
|
||||
<SlRadio size="large">Large</SlRadio>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio]
|
||||
@@ -1,180 +0,0 @@
|
||||
# Range
|
||||
|
||||
[component-header:sl-range]
|
||||
|
||||
```html preview
|
||||
<sl-range></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange />;
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the range an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-range label="Volume" min="0" max="100"></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange label="Volume" min={0} max={100} />;
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a range with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-range label="Volume" help-text="Controls the volume of the current song." min="0" max="100"></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange label="Volume" help-text="Controls the volume of the current song." min={0} max={100} />;
|
||||
```
|
||||
|
||||
### Min, Max, and Step
|
||||
|
||||
Use the `min` and `max` attributes to set the range's minimum and maximum values, respectively. The `step` attribute determines the value's interval when increasing and decreasing.
|
||||
|
||||
```html preview
|
||||
<sl-range min="0" max="10" step="1"></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange min={0} max={10} step={1} />;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a slider.
|
||||
|
||||
```html preview
|
||||
<sl-range disabled></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange disabled />;
|
||||
```
|
||||
|
||||
### Tooltip Placement
|
||||
|
||||
By default, the tooltip is shown on top. Set `tooltip` to `bottom` to show it below the slider.
|
||||
|
||||
```html preview
|
||||
<sl-range tooltip="bottom"></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange tooltip="bottom" />;
|
||||
```
|
||||
|
||||
### Disable the Tooltip
|
||||
|
||||
To disable the tooltip, set `tooltip` to `none`.
|
||||
|
||||
```html preview
|
||||
<sl-range tooltip="none"></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange tooltip="none" />;
|
||||
```
|
||||
|
||||
### Custom Track Colors
|
||||
|
||||
You can customize the active and inactive portions of the track using the `--track-color-active` and `--track-color-inactive` custom properties.
|
||||
|
||||
```html preview
|
||||
<sl-range
|
||||
style="
|
||||
--track-color-active: var(--sl-color-primary-600);
|
||||
--track-color-inactive: var(--sl-color-primary-100);
|
||||
"
|
||||
></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRange
|
||||
style={{
|
||||
'--track-color-active': 'var(--sl-color-primary-600)',
|
||||
'--track-color-inactive': 'var(--sl-color-primary-200)'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Track Offset
|
||||
|
||||
You can customize the initial offset of the active track using the `--track-active-offset` custom property.
|
||||
|
||||
```html preview
|
||||
<sl-range
|
||||
min="-100"
|
||||
max="100"
|
||||
style="
|
||||
--track-color-active: var(--sl-color-primary-600);
|
||||
--track-color-inactive: var(--sl-color-primary-100);
|
||||
--track-active-offset: 50%;
|
||||
"
|
||||
></sl-range>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlRange
|
||||
min={-100}
|
||||
max={100}
|
||||
style={{
|
||||
'--track-color-active': 'var(--sl-color-primary-600)',
|
||||
'--track-color-inactive': 'var(--sl-color-primary-200)',
|
||||
'--track-active-offset': '50%'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Tooltip Formatter
|
||||
|
||||
You can change the tooltip's content by setting the `tooltipFormatter` property to a function that accepts the range's value as an argument.
|
||||
|
||||
```html preview
|
||||
<sl-range min="0" max="100" step="1" class="range-with-custom-formatter"></sl-range>
|
||||
|
||||
<script>
|
||||
const range = document.querySelector('.range-with-custom-formatter');
|
||||
range.tooltipFormatter = value => `Total - ${value}%`;
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRange } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRange min={0} max={100} step={1} tooltipFormatter={value => `Total - ${value}%`} />;
|
||||
```
|
||||
|
||||
[component-metadata:sl-range]
|
||||
@@ -1,103 +0,0 @@
|
||||
# Relative Time
|
||||
|
||||
[component-header:sl-relative-time]
|
||||
|
||||
Localization is handled by the browser's [`Intl.RelativeTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat). No language packs are required.
|
||||
|
||||
```html preview
|
||||
<!-- Shoelace 2 release date 🎉 -->
|
||||
<sl-relative-time date="2020-07-15T09:17:00-04:00"></sl-relative-time>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlRelativeTime date="2020-07-15T09:17:00-04:00" />;
|
||||
```
|
||||
|
||||
The `date` attribute determines when the date/time is calculated from. It must be a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object set via JavaScript.
|
||||
|
||||
?> When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, always use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure the date will be parsed properly by all clients.
|
||||
|
||||
## Examples
|
||||
|
||||
### Keeping Time in Sync
|
||||
|
||||
Use the `sync` attribute to update the displayed value automatically as time passes.
|
||||
|
||||
```html preview
|
||||
<div class="relative-time-sync">
|
||||
<sl-relative-time sync></sl-relative-time>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.relative-time-sync');
|
||||
const relativeTime = container.querySelector('sl-relative-time');
|
||||
|
||||
relativeTime.date = new Date(new Date().getTime() - 60000);
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const date = new Date(new Date().getTime() - 60000);
|
||||
|
||||
const App = () => <SlRelativeTime date={date} sync />;
|
||||
```
|
||||
|
||||
### Formatting Styles
|
||||
|
||||
You can change how the time is displayed using the `format` attribute. Note that some locales may display the same values for `narrow` and `short` formats.
|
||||
|
||||
```html preview
|
||||
<sl-relative-time date="2020-07-15T09:17:00-04:00" format="narrow"></sl-relative-time><br />
|
||||
<sl-relative-time date="2020-07-15T09:17:00-04:00" format="short"></sl-relative-time><br />
|
||||
<sl-relative-time date="2020-07-15T09:17:00-04:00" format="long"></sl-relative-time>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlRelativeTime date="2020-07-15T09:17:00-04:00" format="narrow" />
|
||||
<br />
|
||||
<SlRelativeTime date="2020-07-15T09:17:00-04:00" format="short" />
|
||||
<br />
|
||||
<SlRelativeTime date="2020-07-15T09:17:00-04:00" format="long" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Localization
|
||||
|
||||
Use the `lang` attribute to set the desired locale.
|
||||
|
||||
```html preview
|
||||
English: <sl-relative-time date="2020-07-15T09:17:00-04:00" lang="en-US"></sl-relative-time><br />
|
||||
Chinese: <sl-relative-time date="2020-07-15T09:17:00-04:00" lang="zh-CN"></sl-relative-time><br />
|
||||
German: <sl-relative-time date="2020-07-15T09:17:00-04:00" lang="de"></sl-relative-time><br />
|
||||
Greek: <sl-relative-time date="2020-07-15T09:17:00-04:00" lang="el"></sl-relative-time><br />
|
||||
Russian: <sl-relative-time date="2020-07-15T09:17:00-04:00" lang="ru"></sl-relative-time>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
English: <SlRelativeTime date="2020-07-15T09:17:00-04:00" lang="en-US" />
|
||||
<br />
|
||||
Chinese: <SlRelativeTime date="2020-07-15T09:17:00-04:00" lang="zh-CN" />
|
||||
<br />
|
||||
German: <SlRelativeTime date="2020-07-15T09:17:00-04:00" lang="de" />
|
||||
<br />
|
||||
Greek: <SlRelativeTime date="2020-07-15T09:17:00-04:00" lang="el" />
|
||||
<br />
|
||||
Russian: <SlRelativeTime date="2020-07-15T09:17:00-04:00" lang="ru" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-relative-time]
|
||||
@@ -1,434 +0,0 @@
|
||||
# Select
|
||||
|
||||
[component-header:sl-select]
|
||||
|
||||
```html preview
|
||||
<sl-select>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
<sl-option value="option-5">Option 5</sl-option>
|
||||
<sl-option value="option-6">Option 6</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the select an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Select one">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a select with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Experience" help-text="Please tell us your skill level.">
|
||||
<sl-option value="1">Novice</sl-option>
|
||||
<sl-option value="2">Intermediate</sl-option>
|
||||
<sl-option value="3">Advanced</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Experience" help-text="Please tell us your skill level.">
|
||||
<SlOption value="1">Novice</SlOption>
|
||||
<SlOption value="2">Intermediate</SlOption>
|
||||
<SlOption value="3">Advanced</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
|
||||
Use the `placeholder` attribute to add a placeholder.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Select one">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Clearable
|
||||
|
||||
Use the `clearable` attribute to make the control clearable. The clear button only appears when an option is selected.
|
||||
|
||||
```html preview
|
||||
<sl-select clearable value="option-1">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Clearable" clearable>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Filled Selects
|
||||
|
||||
Add the `filled` attribute to draw a filled select.
|
||||
|
||||
```html preview
|
||||
<sl-select filled>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect filled>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give selects rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-select pill>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect pill>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a select.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Disabled" disabled>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Disabled" disabled>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Multiple
|
||||
|
||||
To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `clearable` when this option is enabled. To set multiple values at once, set `value` to a space-delimited list of values.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
<sl-option value="option-5">Option 5</sl-option>
|
||||
<sl-option value="option-6">Option 6</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
?> Note that multi-select options may wrap, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
|
||||
|
||||
### Setting Initial Values
|
||||
|
||||
Use the `value` attribute to set the initial selection. When using `multiple`, use space-delimited values to select more than one option.
|
||||
|
||||
```html preview
|
||||
<sl-select value="option-1 option-2" multiple clearable>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect value="option-1 option-2" multiple clearable>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Grouping Options
|
||||
|
||||
Use `<sl-divider>` to group listbox items visually. You can also use `<small>` to provide labels, but they won't be announced by most assistive devices.
|
||||
|
||||
```html preview
|
||||
<sl-select>
|
||||
<small>Section 1</small>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-divider></sl-divider>
|
||||
<small>Section 2</small>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
<sl-option value="option-5">Option 5</sl-option>
|
||||
<sl-option value="option-6">Option 6</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a select's size. Note that size does not apply to listbox options.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Small" size="small">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-select placeholder="Medium" size="medium">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-select placeholder="Large" size="large">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSelect placeholder="Small" size="small">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlSelect placeholder="Medium" size="medium">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlSelect placeholder="Large" size="large">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the select's listbox can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. Valid placements are `top` and `bottom`.
|
||||
|
||||
```html preview
|
||||
<sl-select placement="top">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlOption,
|
||||
SlSelect
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placement="top">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix Icons
|
||||
|
||||
Use the `prefix` slot to prepend an icon to the control.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Small" size="small" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-select placeholder="Medium" size="medium" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-select placeholder="Large" size="large" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSelect placeholder="Small" size="small">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlSelect placeholder="Medium" size="medium">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlSelect placeholder="Large" size="large">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-select]
|
||||
@@ -1,82 +0,0 @@
|
||||
# Spinner
|
||||
|
||||
[component-header:sl-spinner]
|
||||
|
||||
```html preview
|
||||
<sl-spinner></sl-spinner>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlSpinner />;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Size
|
||||
|
||||
Spinners are sized based on the current font size. To change their size, set the `font-size` property on the spinner itself or on a parent element as shown below.
|
||||
|
||||
```html preview
|
||||
<sl-spinner></sl-spinner>
|
||||
<sl-spinner style="font-size: 2rem;"></sl-spinner>
|
||||
<sl-spinner style="font-size: 3rem;"></sl-spinner>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSpinner />
|
||||
<SlSpinner style={{ fontSize: '2rem' }} />
|
||||
<SlSpinner style={{ fontSize: '3rem' }} />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Track Width
|
||||
|
||||
The width of the spinner's track can be changed by setting the `--track-width` custom property.
|
||||
|
||||
```html preview
|
||||
<sl-spinner style="font-size: 50px; --track-width: 10px;"></sl-spinner>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSpinner
|
||||
style={{
|
||||
fontSize: '3rem',
|
||||
'--track-width': '6px'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
The spinner's colors can be changed by setting the `--indicator-color` and `--track-color` custom properties.
|
||||
|
||||
```html preview
|
||||
<sl-spinner style="font-size: 3rem; --indicator-color: deeppink; --track-color: pink;"></sl-spinner>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSpinner
|
||||
style={{
|
||||
fontSize: '3rem',
|
||||
'--indicator-color': 'deeppink',
|
||||
'--track-color': 'pink'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-spinner]
|
||||
@@ -1,95 +0,0 @@
|
||||
# Switch
|
||||
|
||||
[component-header:sl-switch]
|
||||
|
||||
```html preview
|
||||
<sl-switch>Switch</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlSwitch>Switch</SlSwitch>;
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Checked
|
||||
|
||||
Use the `checked` attribute to activate the switch.
|
||||
|
||||
```html preview
|
||||
<sl-switch checked>Checked</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlSwitch checked>Checked</SlSwitch>;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable the switch.
|
||||
|
||||
```html preview
|
||||
<sl-switch disabled>Disabled</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlSwitch disabled>Disabled</SlSwitch>;
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
Use the `size` attribute to change a switch's size.
|
||||
|
||||
```html preview
|
||||
<sl-switch size="small">Small</sl-switch>
|
||||
<br />
|
||||
<sl-switch size="medium">Medium</sl-switch>
|
||||
<br />
|
||||
<sl-switch size="large">Large</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSwitch size="small">Small</SlSwitch>
|
||||
<br />
|
||||
<SlSwitch size="medium">Medium</SlSwitch>
|
||||
<br />
|
||||
<SlSwitch size="large">Large</SlSwitch>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Styles
|
||||
|
||||
Use the available custom properties to change how the switch is styled.
|
||||
|
||||
```html preview
|
||||
<sl-switch style="--width: 80px; --height: 40px; --thumb-size: 36px;">Really big</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSwitch
|
||||
style={{
|
||||
'--width': '80px',
|
||||
'--height': '32px',
|
||||
'--thumb-size': '26px'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-switch]
|
||||
@@ -1,443 +0,0 @@
|
||||
# Tab Group
|
||||
|
||||
[component-header:sl-tab-group]
|
||||
|
||||
Tab groups make use of [tabs](/components/tab) and [tab panels](/components/tab-panel). Each tab must be slotted into the `nav` slot and its `panel` must refer to a tab panel of the same name.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup>
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="custom">
|
||||
Custom
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="advanced">
|
||||
Advanced
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="disabled" disabled>
|
||||
Disabled
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="custom">This is the custom tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="advanced">This is the advanced tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="disabled">This is a disabled tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Tabs on Bottom
|
||||
|
||||
Tabs can be shown on the bottom by setting `placement` to `bottom`.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group placement="bottom">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup placement="bottom">
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="custom">
|
||||
Custom
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="advanced">
|
||||
Advanced
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="disabled" disabled>
|
||||
Disabled
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="custom">This is the custom tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="advanced">This is the advanced tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="disabled">This is a disabled tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Tabs on Start
|
||||
|
||||
Tabs can be shown on the starting side by setting `placement` to `start`.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group placement="start">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup placement="start">
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="custom">
|
||||
Custom
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="advanced">
|
||||
Advanced
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="disabled" disabled>
|
||||
Disabled
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="custom">This is the custom tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="advanced">This is the advanced tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="disabled">This is a disabled tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Tabs on End
|
||||
|
||||
Tabs can be shown on the ending side by setting `placement` to `end`.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group placement="end">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup placement="end">
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="custom">
|
||||
Custom
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="advanced">
|
||||
Advanced
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="disabled" disabled>
|
||||
Disabled
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="custom">This is the custom tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="advanced">This is the advanced tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="disabled">This is a disabled tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Closable Tabs
|
||||
|
||||
Add the `closable` attribute to a tab to show a close button. This example shows how you can dynamically remove tabs from the DOM when the close button is activated.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group class="tabs-closable">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="closable-1" closable>Closable 1</sl-tab>
|
||||
<sl-tab slot="nav" panel="closable-2" closable>Closable 2</sl-tab>
|
||||
<sl-tab slot="nav" panel="closable-3" closable>Closable 3</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="closable-1">This is the first closable tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="closable-2">This is the second closable tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="closable-3">This is the third closable tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
|
||||
<script>
|
||||
const tabGroup = document.querySelector('.tabs-closable');
|
||||
|
||||
tabGroup.addEventListener('sl-close', async event => {
|
||||
const tab = event.target;
|
||||
const panel = tabGroup.querySelector(`sl-tab-panel[name="${tab.panel}"]`);
|
||||
|
||||
// Show the previous tab if the tab is currently active
|
||||
if (tab.active) {
|
||||
tabGroup.show(tab.previousElementSibling.panel);
|
||||
}
|
||||
|
||||
// Remove the tab + panel
|
||||
tab.remove();
|
||||
panel.remove();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleClose(event) {
|
||||
//
|
||||
// This is a crude example that removes the tab and its panel from the DOM.
|
||||
// There are better ways to manage tab creation/removal in React, but that
|
||||
// would significantly complicate the example.
|
||||
//
|
||||
const tab = event.target;
|
||||
const tabGroup = tab.closest('sl-tab-group');
|
||||
const tabPanel = tabGroup.querySelector(`[aria-labelledby="${tab.id}"]`);
|
||||
|
||||
tab.remove();
|
||||
tabPanel.remove();
|
||||
}
|
||||
|
||||
return (
|
||||
<SlTabGroup className="tabs-closable" onSlClose={handleClose}>
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="closable-1" closable onSlClose={handleClose}>
|
||||
Closable 1
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="closable-2" closable onSlClose={handleClose}>
|
||||
Closable 2
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="closable-3" closable onSlClose={handleClose}>
|
||||
Closable 3
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="closable-1">This is the first closable tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="closable-2">This is the second closable tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="closable-3">This is the third closable tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Scrolling Tabs
|
||||
|
||||
When there are more tabs than horizontal space allows, the nav will be scrollable.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="tab-1">Tab 1</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-2">Tab 2</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-3">Tab 3</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-4">Tab 4</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-5">Tab 5</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-6">Tab 6</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-7">Tab 7</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-8">Tab 8</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-9">Tab 9</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-10">Tab 10</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-11">Tab 11</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-12">Tab 12</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-13">Tab 13</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-14">Tab 14</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-15">Tab 15</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-16">Tab 16</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-17">Tab 17</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-18">Tab 18</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-19">Tab 19</sl-tab>
|
||||
<sl-tab slot="nav" panel="tab-20">Tab 20</sl-tab>
|
||||
|
||||
<sl-tab-panel name="tab-1">Tab panel 1</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-2">Tab panel 2</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-3">Tab panel 3</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-4">Tab panel 4</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-5">Tab panel 5</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-6">Tab panel 6</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-7">Tab panel 7</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-8">Tab panel 8</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-9">Tab panel 9</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-10">Tab panel 10</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-11">Tab panel 11</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-12">Tab panel 12</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-13">Tab panel 13</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-14">Tab panel 14</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-15">Tab panel 15</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-16">Tab panel 16</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-17">Tab panel 17</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-18">Tab panel 18</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-19">Tab panel 19</sl-tab-panel>
|
||||
<sl-tab-panel name="tab-20">Tab panel 20</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup>
|
||||
<SlTab slot="nav" panel="tab-1">
|
||||
Tab 1
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-2">
|
||||
Tab 2
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-3">
|
||||
Tab 3
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-4">
|
||||
Tab 4
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-5">
|
||||
Tab 5
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-6">
|
||||
Tab 6
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-7">
|
||||
Tab 7
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-8">
|
||||
Tab 8
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-9">
|
||||
Tab 9
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-10">
|
||||
Tab 10
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-11">
|
||||
Tab 11
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-12">
|
||||
Tab 12
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-13">
|
||||
Tab 13
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-14">
|
||||
Tab 14
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-15">
|
||||
Tab 15
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-16">
|
||||
Tab 16
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-17">
|
||||
Tab 17
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-18">
|
||||
Tab 18
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-19">
|
||||
Tab 19
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="tab-20">
|
||||
Tab 20
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="tab-1">Tab panel 1</SlTabPanel>
|
||||
<SlTabPanel name="tab-2">Tab panel 2</SlTabPanel>
|
||||
<SlTabPanel name="tab-3">Tab panel 3</SlTabPanel>
|
||||
<SlTabPanel name="tab-4">Tab panel 4</SlTabPanel>
|
||||
<SlTabPanel name="tab-5">Tab panel 5</SlTabPanel>
|
||||
<SlTabPanel name="tab-6">Tab panel 6</SlTabPanel>
|
||||
<SlTabPanel name="tab-7">Tab panel 7</SlTabPanel>
|
||||
<SlTabPanel name="tab-8">Tab panel 8</SlTabPanel>
|
||||
<SlTabPanel name="tab-9">Tab panel 9</SlTabPanel>
|
||||
<SlTabPanel name="tab-10">Tab panel 10</SlTabPanel>
|
||||
<SlTabPanel name="tab-11">Tab panel 11</SlTabPanel>
|
||||
<SlTabPanel name="tab-12">Tab panel 12</SlTabPanel>
|
||||
<SlTabPanel name="tab-13">Tab panel 13</SlTabPanel>
|
||||
<SlTabPanel name="tab-14">Tab panel 14</SlTabPanel>
|
||||
<SlTabPanel name="tab-15">Tab panel 15</SlTabPanel>
|
||||
<SlTabPanel name="tab-16">Tab panel 16</SlTabPanel>
|
||||
<SlTabPanel name="tab-17">Tab panel 17</SlTabPanel>
|
||||
<SlTabPanel name="tab-18">Tab panel 18</SlTabPanel>
|
||||
<SlTabPanel name="tab-19">Tab panel 19</SlTabPanel>
|
||||
<SlTabPanel name="tab-20">Tab panel 20</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
### Manual Activation
|
||||
|
||||
When focused, keyboard users can press <kbd>Left</kbd> or <kbd>Right</kbd> to select the desired tab. By default, the corresponding tab panel will be shown immediately (automatic activation). You can change this behavior by setting `activation="manual"` which will require the user to press <kbd>Space</kbd> or <kbd>Enter</kbd> before showing the tab panel (manual activation).
|
||||
|
||||
```html preview
|
||||
<sl-tab-group activation="manual">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup activation="manual">
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="custom">
|
||||
Custom
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="advanced">
|
||||
Advanced
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="disabled" disabled>
|
||||
Disabled
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="custom">This is the custom tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="advanced">This is the advanced tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="disabled">This is a disabled tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-tab-group]
|
||||
@@ -1,47 +0,0 @@
|
||||
# Tab Panel
|
||||
|
||||
[component-header:sl-tab-panel]
|
||||
|
||||
```html preview
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
<sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>
|
||||
|
||||
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
|
||||
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTabGroup>
|
||||
<SlTab slot="nav" panel="general">
|
||||
General
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="custom">
|
||||
Custom
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="advanced">
|
||||
Advanced
|
||||
</SlTab>
|
||||
<SlTab slot="nav" panel="disabled" disabled>
|
||||
Disabled
|
||||
</SlTab>
|
||||
|
||||
<SlTabPanel name="general">This is the general tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="custom">This is the custom tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="advanced">This is the advanced tab panel.</SlTabPanel>
|
||||
<SlTabPanel name="disabled">This is a disabled tab panel.</SlTabPanel>
|
||||
</SlTabGroup>
|
||||
);
|
||||
```
|
||||
|
||||
?> Additional demonstrations can be found in the [tab group examples](/components/tab-group).
|
||||
|
||||
[component-metadata:sl-tab-panel]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Tab
|
||||
|
||||
[component-header:sl-tab]
|
||||
|
||||
```html preview
|
||||
<sl-tab>Tab</sl-tab>
|
||||
<sl-tab active>Active</sl-tab>
|
||||
<sl-tab closable>Closable</sl-tab>
|
||||
<sl-tab disabled>Disabled</sl-tab>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTab } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlTab>Tab</SlTab>
|
||||
<SlTab active>Active</SlTab>
|
||||
<SlTab closable>Closable</SlTab>
|
||||
<SlTab disabled>Disabled</SlTab>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
?> Additional demonstrations can be found in the [tab group examples](/components/tab-group).
|
||||
|
||||
[component-metadata:sl-tab]
|
||||
@@ -1,145 +0,0 @@
|
||||
# Tag
|
||||
|
||||
[component-header:sl-tag]
|
||||
|
||||
```html preview
|
||||
<sl-tag variant="primary">Primary</sl-tag>
|
||||
<sl-tag variant="success">Success</sl-tag>
|
||||
<sl-tag variant="neutral">Neutral</sl-tag>
|
||||
<sl-tag variant="warning">Warning</sl-tag>
|
||||
<sl-tag variant="danger">Danger</sl-tag>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTag } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlTag variant="primary">Primary</SlTag>
|
||||
<SlTag variant="success">Success</SlTag>
|
||||
<SlTag variant="neutral">Neutral</SlTag>
|
||||
<SlTag variant="warning">Warning</SlTag>
|
||||
<SlTag variant="danger">Danger</SlTag>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a tab's size.
|
||||
|
||||
```html preview
|
||||
<sl-tag size="small">Small</sl-tag>
|
||||
<sl-tag size="medium">Medium</sl-tag>
|
||||
<sl-tag size="large">Large</sl-tag>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTag } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlTag size="small">Small</SlTag>
|
||||
<SlTag size="medium">Medium</SlTag>
|
||||
<SlTag size="large">Large</SlTag>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give tabs rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-tag size="small" pill>Small</sl-tag>
|
||||
<sl-tag size="medium" pill>Medium</sl-tag>
|
||||
<sl-tag size="large" pill>Large</sl-tag>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTag } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlTag size="small" pill>
|
||||
Small
|
||||
</SlTag>
|
||||
<SlTag size="medium" pill>
|
||||
Medium
|
||||
</SlTag>
|
||||
<SlTag size="large" pill>
|
||||
Large
|
||||
</SlTag>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Removable
|
||||
|
||||
Use the `removable` attribute to add a remove button to the tag.
|
||||
|
||||
```html preview
|
||||
<div class="tags-removable">
|
||||
<sl-tag size="small" removable>Small</sl-tag>
|
||||
<sl-tag size="medium" removable>Medium</sl-tag>
|
||||
<sl-tag size="large" removable>Large</sl-tag>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const div = document.querySelector('.tags-removable');
|
||||
|
||||
div.addEventListener('sl-remove', event => {
|
||||
const tag = event.target;
|
||||
tag.style.opacity = '0';
|
||||
setTimeout(() => (tag.style.opacity = '1'), 2000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tags-removable sl-tag {
|
||||
transition: var(--sl-transition-medium) opacity;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTag } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.tags-removable sl-tag {
|
||||
transition: var(--sl-transition-medium) opacity;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
function handleRemove(event) {
|
||||
const tag = event.target;
|
||||
tag.style.opacity = '0';
|
||||
setTimeout(() => (tag.style.opacity = '1'), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tags-removable">
|
||||
<SlTag size="small" removable onSlRemove={handleRemove}>
|
||||
Small
|
||||
</SlTag>
|
||||
|
||||
<SlTag size="medium" removable onSlRemove={handleRemove}>
|
||||
Medium
|
||||
</SlTag>
|
||||
|
||||
<SlTag size="large" removable onSlRemove={handleRemove}>
|
||||
Large
|
||||
</SlTag>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-tag]
|
||||
@@ -1,157 +0,0 @@
|
||||
# Textarea
|
||||
|
||||
[component-header:sl-textarea]
|
||||
|
||||
```html preview
|
||||
<sl-textarea></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea />;
|
||||
```
|
||||
|
||||
?> This component works with standard `<form>` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation.
|
||||
|
||||
## Examples
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to give the textarea an accessible label. For labels that contain HTML, use the `label` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-textarea label="Comments"></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea label="Comments" />;
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a textarea with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html preview
|
||||
<sl-textarea label="Feedback" help-text="Please tell us what you think."> </sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea label="Feedback" help-text="Please tell us what you think." />;
|
||||
```
|
||||
|
||||
### Rows
|
||||
|
||||
Use the `rows` attribute to change the number of text rows that get shown.
|
||||
|
||||
```html preview
|
||||
<sl-textarea rows="2"></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea rows={2} />;
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
|
||||
Use the `placeholder` attribute to add a placeholder.
|
||||
|
||||
```html preview
|
||||
<sl-textarea placeholder="Type something"></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea placeholder="Type something" />;
|
||||
```
|
||||
|
||||
### Filled Textareas
|
||||
|
||||
Add the `filled` attribute to draw a filled textarea.
|
||||
|
||||
```html preview
|
||||
<sl-textarea placeholder="Type something" filled></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea placeholder="Type something" filled />;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable a textarea.
|
||||
|
||||
```html preview
|
||||
<sl-textarea placeholder="Textarea" disabled></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea placeholder="Textarea" disabled />;
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a textarea's size.
|
||||
|
||||
```html preview
|
||||
<sl-textarea placeholder="Small" size="small"></sl-textarea>
|
||||
<br />
|
||||
<sl-textarea placeholder="Medium" size="medium"></sl-textarea>
|
||||
<br />
|
||||
<sl-textarea placeholder="Large" size="large"></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlTextarea placeholder="Small" size="small"></SlTextarea>
|
||||
<br />
|
||||
<SlTextarea placeholder="Medium" size="medium"></SlTextarea>
|
||||
<br />
|
||||
<SlTextarea placeholder="Large" size="large"></SlTextarea>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Prevent Resizing
|
||||
|
||||
By default, textareas can be resized vertically by the user. To prevent resizing, set the `resize` attribute to `none`.
|
||||
|
||||
```html preview
|
||||
<sl-textarea resize="none"></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea resize="none" />;
|
||||
```
|
||||
|
||||
### Expand with Content
|
||||
|
||||
Textareas will automatically resize to expand to fit their content when `resize` is set to `auto`.
|
||||
|
||||
```html preview
|
||||
<sl-textarea resize="auto"></sl-textarea>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlTextarea resize="auto" />;
|
||||
```
|
||||
|
||||
[component-metadata:sl-textarea]
|
||||
@@ -1,418 +0,0 @@
|
||||
# Tooltip
|
||||
|
||||
[component-header:sl-tooltip]
|
||||
|
||||
A tooltip's target is its _first child element_, so you should only wrap one element inside of the tooltip. If you need the tooltip to show up for multiple elements, nest them inside a container first.
|
||||
|
||||
Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout.
|
||||
|
||||
```html preview
|
||||
<sl-tooltip content="This is a tooltip">
|
||||
<sl-button>Hover Me</sl-button>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTooltip content="This is a tooltip">
|
||||
<SlButton>Hover Me</SlButton>
|
||||
</SlTooltip>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Placement
|
||||
|
||||
Use the `placement` attribute to set the preferred placement of the tooltip.
|
||||
|
||||
```html preview
|
||||
<div class="tooltip-placement-example">
|
||||
<div class="tooltip-placement-example-row">
|
||||
<sl-tooltip content="top-start" placement="top-start">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="top" placement="top">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="top-end" placement="top-end">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="tooltip-placement-example-row">
|
||||
<sl-tooltip content="left-start" placement="left-start">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="right-start" placement="right-start">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="tooltip-placement-example-row">
|
||||
<sl-tooltip content="left" placement="left">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="right" placement="right">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="tooltip-placement-example-row">
|
||||
<sl-tooltip content="left-end" placement="left-end">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="right-end" placement="right-end">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="tooltip-placement-example-row">
|
||||
<sl-tooltip content="bottom-start" placement="bottom-start">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="bottom" placement="bottom">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="bottom-end" placement="bottom-end">
|
||||
<sl-button></sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tooltip-placement-example {
|
||||
width: 250px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.tooltip-placement-example-row:after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.tooltip-placement-example sl-button {
|
||||
float: left;
|
||||
width: 2.5rem;
|
||||
margin-right: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.tooltip-placement-example-row:nth-child(1) sl-tooltip:first-child sl-button,
|
||||
.tooltip-placement-example-row:nth-child(5) sl-tooltip:first-child sl-button {
|
||||
margin-left: calc(40px + 0.25rem);
|
||||
}
|
||||
|
||||
.tooltip-placement-example-row:nth-child(2) sl-tooltip:nth-child(2) sl-button,
|
||||
.tooltip-placement-example-row:nth-child(3) sl-tooltip:nth-child(2) sl-button,
|
||||
.tooltip-placement-example-row:nth-child(4) sl-tooltip:nth-child(2) sl-button {
|
||||
margin-left: calc((40px * 3) + (0.25rem * 3));
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.tooltip-placement-example {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.tooltip-placement-example-row:after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.tooltip-placement-example sl-button {
|
||||
float: left;
|
||||
width: 2.5rem;
|
||||
margin-right: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.tooltip-placement-example-row:nth-child(1) sl-tooltip:first-child sl-button,
|
||||
.tooltip-placement-example-row:nth-child(5) sl-tooltip:first-child sl-button {
|
||||
margin-left: calc(40px + 0.25rem);
|
||||
}
|
||||
|
||||
.tooltip-placement-example-row:nth-child(2) sl-tooltip:nth-child(2) sl-button,
|
||||
.tooltip-placement-example-row:nth-child(3) sl-tooltip:nth-child(2) sl-button,
|
||||
.tooltip-placement-example-row:nth-child(4) sl-tooltip:nth-child(2) sl-button {
|
||||
margin-left: calc((40px * 3) + (0.25rem * 3));
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div className="tooltip-placement-example">
|
||||
<div className="tooltip-placement-example-row">
|
||||
<SlTooltip content="top-start" placement="top-start">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="top" placement="top">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="top-end" placement="top-end">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
</div>
|
||||
|
||||
<div className="tooltip-placement-example-row">
|
||||
<SlTooltip content="left-start" placement="left-start">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="right-start" placement="right-start">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
</div>
|
||||
|
||||
<div className="tooltip-placement-example-row">
|
||||
<SlTooltip content="left" placement="left">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="right" placement="right">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
</div>
|
||||
|
||||
<div className="tooltip-placement-example-row">
|
||||
<SlTooltip content="left-end" placement="left-end">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="right-end" placement="right-end">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
</div>
|
||||
|
||||
<div className="tooltip-placement-example-row">
|
||||
<SlTooltip content="bottom-start" placement="bottom-start">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="bottom" placement="bottom">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="bottom-end" placement="bottom-end">
|
||||
<SlButton />
|
||||
</SlTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Click Trigger
|
||||
|
||||
Set the `trigger` attribute to `click` to toggle the tooltip on click instead of hover.
|
||||
|
||||
```html preview
|
||||
<sl-tooltip content="Click again to dismiss" trigger="click">
|
||||
<sl-button>Click to Toggle</sl-button>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTooltip content="Click again to dismiss" trigger="click">
|
||||
<SlButton>Click to Toggle</SlButton>
|
||||
</SlTooltip>
|
||||
);
|
||||
```
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
Tooltips can be controller programmatically by setting the `trigger` attribute to `manual`. Use the `open` attribute to control when the tooltip is shown.
|
||||
|
||||
```html preview
|
||||
<sl-button style="margin-right: 4rem;">Toggle Manually</sl-button>
|
||||
|
||||
<sl-tooltip content="This is an avatar" trigger="manual" class="manual-tooltip">
|
||||
<sl-avatar label="User"></sl-avatar>
|
||||
</sl-tooltip>
|
||||
|
||||
<script>
|
||||
const tooltip = document.querySelector('.manual-tooltip');
|
||||
const toggle = tooltip.previousElementSibling;
|
||||
|
||||
toggle.addEventListener('click', () => (tooltip.open = !tooltip.open));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlAvatar, SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlButton style={{ marginRight: '4rem' }} onClick={() => setOpen(!open)}>
|
||||
Toggle Manually
|
||||
</SlButton>
|
||||
|
||||
<SlTooltip open={open} content="This is an avatar" trigger="manual">
|
||||
<SlAvatar />
|
||||
</SlTooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Removing Arrows
|
||||
|
||||
You can control the size of tooltip arrows by overriding the `--sl-tooltip-arrow-size` design token. To remove them, set the value to `0` as shown below.
|
||||
|
||||
```html preview
|
||||
<sl-tooltip content="This is a tooltip" style="--sl-tooltip-arrow-size: 0;">
|
||||
<sl-button>No Arrow</sl-button>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<div style={{ '--sl-tooltip-arrow-size': '0' }}>
|
||||
<SlTooltip content="This is a tooltip">
|
||||
<SlButton>Above</SlButton>
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="This is a tooltip" placement="bottom">
|
||||
<SlButton>Below</SlButton>
|
||||
</SlTooltip>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
To override it globally, set it in a root block in your stylesheet after the Shoelace stylesheet is loaded.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--sl-tooltip-arrow-size: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### HTML in Tooltips
|
||||
|
||||
Use the `content` slot to create tooltips with HTML content. Tooltips are designed only for text and presentational elements. Avoid placing interactive content, such as buttons, links, and form controls, in a tooltip.
|
||||
|
||||
```html preview
|
||||
<sl-tooltip>
|
||||
<div slot="content">I'm not <strong>just</strong> a tooltip, I'm a <em>tooltip</em> with HTML!</div>
|
||||
|
||||
<sl-button>Hover me</sl-button>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTooltip>
|
||||
<div slot="content">
|
||||
I'm not <strong>just</strong> a tooltip, I'm a <em>tooltip</em> with HTML!
|
||||
</div>
|
||||
|
||||
<SlButton>Hover Me</SlButton>
|
||||
</SlTooltip>
|
||||
);
|
||||
```
|
||||
|
||||
### Setting a Maximum Width
|
||||
|
||||
Use the `--max-width` custom property to change the width the tooltip can grow to before wrapping occurs.
|
||||
|
||||
```html preview
|
||||
<sl-tooltip style="--max-width: 80px;" content="This tooltip will wrap after only 80 pixels.">
|
||||
<sl-button>Hover me</sl-button>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTooltip style={{ '--max-width': '80px' }} content="This tooltip will wrap after only 80 pixels.">
|
||||
<SlButton>Hover Me</SlButton>
|
||||
</SlTooltip>
|
||||
);
|
||||
```
|
||||
|
||||
### Hoisting
|
||||
|
||||
Tooltips will be clipped if they're inside a container that has `overflow: auto|hidden|scroll`. The `hoist` attribute forces the tooltip to use a fixed positioning strategy, allowing it to break out of the container. In this case, the tooltip will be positioned relative to its containing block, which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
|
||||
|
||||
```html preview
|
||||
<div class="tooltip-hoist">
|
||||
<sl-tooltip content="This is a tooltip">
|
||||
<sl-button>No Hoist</sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="This is a tooltip" hoist>
|
||||
<sl-button>Hoist</sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tooltip-hoist {
|
||||
position: relative;
|
||||
border: solid 2px var(--sl-panel-border-color);
|
||||
overflow: hidden;
|
||||
padding: var(--sl-spacing-medium);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.tooltip-hoist {
|
||||
border: solid 2px var(--sl-panel-border-color);
|
||||
overflow: hidden;
|
||||
padding: var(--sl-spacing-medium);
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div class="tooltip-hoist">
|
||||
<SlTooltip content="This is a tooltip">
|
||||
<SlButton>No Hoist</SlButton>
|
||||
</SlTooltip>
|
||||
|
||||
<SlTooltip content="This is a tooltip" hoist>
|
||||
<SlButton>Hoist</SlButton>
|
||||
</SlTooltip>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-tooltip]
|
||||
@@ -1,163 +0,0 @@
|
||||
# Tree Item
|
||||
|
||||
[component-header:sl-tree-item]
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>Item A</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem>
|
||||
Item 1
|
||||
<SlTreeItem>Item A</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Nested tree items
|
||||
|
||||
A tree item can contain other tree items. This allows the node to be expanded or collapsed by the user.
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem>
|
||||
Item 1
|
||||
<SlTreeItem>
|
||||
Item A
|
||||
<SlTreeItem>Item Z</SlTreeItem>
|
||||
<SlTreeItem>Item Y</SlTreeItem>
|
||||
<SlTreeItem>Item X</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### Selected
|
||||
|
||||
Use the `selected` attribute to select a tree item initially.
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree-item selected>
|
||||
Item 1
|
||||
<sl-tree-item>Item A</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem selected>
|
||||
Item 1
|
||||
<SlTreeItem>Item A</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### Expanded
|
||||
|
||||
Use the `expanded` attribute to expand a tree item initially.
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree-item expanded>
|
||||
Item 1
|
||||
<sl-tree-item expanded>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem expanded>
|
||||
Item 1
|
||||
<SlTreeItem expanded>
|
||||
Item A
|
||||
<SlTreeItem>Item Z</SlTreeItem>
|
||||
<SlTreeItem>Item Y</SlTreeItem>
|
||||
<SlTreeItem>Item X</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-tree-item]
|
||||
@@ -1,464 +0,0 @@
|
||||
# Tree
|
||||
|
||||
[component-header:sl-tree]
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree-item>
|
||||
Deciduous
|
||||
<sl-tree-item>Birch</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
Maple
|
||||
<sl-tree-item>Field maple</sl-tree-item>
|
||||
<sl-tree-item>Red maple</sl-tree-item>
|
||||
<sl-tree-item>Sugar maple</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Oak</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Coniferous
|
||||
<sl-tree-item>Cedar</sl-tree-item>
|
||||
<sl-tree-item>Pine</sl-tree-item>
|
||||
<sl-tree-item>Spruce</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Non-trees
|
||||
<sl-tree-item>Bamboo</sl-tree-item>
|
||||
<sl-tree-item>Cactus</sl-tree-item>
|
||||
<sl-tree-item>Fern</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlTreeItem>
|
||||
Deciduous
|
||||
<SlTreeItem>Birch</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
Maple
|
||||
<SlTreeItem>Field maple</SlTreeItem>
|
||||
<SlTreeItem>Red maple</SlTreeItem>
|
||||
<SlTreeItem>Sugar maple</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Oak</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Coniferous
|
||||
<SlTreeItem>Cedar</SlTreeItem>
|
||||
<SlTreeItem>Pine</SlTreeItem>
|
||||
<SlTreeItem>Spruce</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Non-trees
|
||||
<SlTreeItem>Bamboo</SlTreeItem>
|
||||
<SlTreeItem>Cactus</SlTreeItem>
|
||||
<SlTreeItem>Fern</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Selection Modes
|
||||
|
||||
The `selection` attribute lets you change the selection behavior of the tree.
|
||||
|
||||
- Use `single` to allow the selection of a single item (default).
|
||||
- Use `multiple` to allow the selection of multiple items.
|
||||
- Use `leaf` to only allow leaf nodes to be selected.
|
||||
|
||||
```html preview
|
||||
<sl-select id="selection-mode" value="single" label="Selection">
|
||||
<sl-option value="single">Single</sl-option>
|
||||
<sl-option value="multiple">Multiple</sl-option>
|
||||
<sl-option value="leaf">Leaf</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-tree class="tree-selectable">
|
||||
<sl-tree-item>
|
||||
Item 1
|
||||
<sl-tree-item>
|
||||
Item A
|
||||
<sl-tree-item>Item Z</sl-tree-item>
|
||||
<sl-tree-item>Item Y</sl-tree-item>
|
||||
<sl-tree-item>Item X</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item B</sl-tree-item>
|
||||
<sl-tree-item>Item C</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Item 2</sl-tree-item>
|
||||
<sl-tree-item>Item 3</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<script>
|
||||
const selectionMode = document.querySelector('#selection-mode');
|
||||
const tree = document.querySelector('.tree-selectable');
|
||||
|
||||
selectionMode.addEventListener('sl-change', () => {
|
||||
tree.querySelectorAll('sl-tree-item').forEach(item => (item.selected = false));
|
||||
tree.selection = selectionMode.value;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [selection, setSelection] = useState('single');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlSelect label="Selection" value={selection} onSlChange={event => setSelection(event.target.value)}>
|
||||
<SlMenuItem value="single">single</SlMenuItem>
|
||||
<SlMenuItem value="multiple">multiple</SlMenuItem>
|
||||
<SlMenuItem value="leaf">leaf</SlMenuItem>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlTree selection={selection}>
|
||||
<SlTreeItem>
|
||||
Item 1
|
||||
<SlTreeItem>
|
||||
Item A
|
||||
<SlTreeItem>Item Z</SlTreeItem>
|
||||
<SlTreeItem>Item Y</SlTreeItem>
|
||||
<SlTreeItem>Item X</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item B</SlTreeItem>
|
||||
<SlTreeItem>Item C</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Item 2</SlTreeItem>
|
||||
<SlTreeItem>Item 3</SlTreeItem>
|
||||
</SlTree>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Showing Indent Guides
|
||||
|
||||
Indent guides can be drawn by setting `--indent-guide-width`. You can also change the color, offset, and style, using `--indent-guide-color`, `--indent-guide-style`, and `--indent-guide-offset`, respectively.
|
||||
|
||||
```html preview
|
||||
<sl-tree class="tree-with-lines">
|
||||
<sl-tree-item expanded>
|
||||
Deciduous
|
||||
<sl-tree-item>Birch</sl-tree-item>
|
||||
<sl-tree-item expanded>
|
||||
Maple
|
||||
<sl-tree-item>Field maple</sl-tree-item>
|
||||
<sl-tree-item>Red maple</sl-tree-item>
|
||||
<sl-tree-item>Sugar maple</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Oak</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Coniferous
|
||||
<sl-tree-item>Cedar</sl-tree-item>
|
||||
<sl-tree-item>Pine</sl-tree-item>
|
||||
<sl-tree-item>Spruce</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Non-trees
|
||||
<sl-tree-item>Bamboo</sl-tree-item>
|
||||
<sl-tree-item>Cactus</sl-tree-item>
|
||||
<sl-tree-item>Fern</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<style>
|
||||
.tree-with-lines {
|
||||
--indent-guide-width: 1px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree class="tree-with-lines" style={{ '--indent-guide-width': '1px' }}>
|
||||
<SlTreeItem expanded>
|
||||
Deciduous
|
||||
<SlTreeItem>Birch</SlTreeItem>
|
||||
<SlTreeItem expanded>
|
||||
Maple
|
||||
<SlTreeItem>Field maple</SlTreeItem>
|
||||
<SlTreeItem>Red maple</SlTreeItem>
|
||||
<SlTreeItem>Sugar maple</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Oak</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Coniferous
|
||||
<SlTreeItem>Cedar</SlTreeItem>
|
||||
<SlTreeItem>Pine</SlTreeItem>
|
||||
<SlTreeItem>Spruce</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Non-trees
|
||||
<SlTreeItem>Bamboo</SlTreeItem>
|
||||
<SlTreeItem>Cactus</SlTreeItem>
|
||||
<SlTreeItem>Fern</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Use the `lazy` attribute on a tree item to indicate that the content is not yet present and will be loaded later. When the user tries to expand the node, the `loading` state is set to `true` and the `sl-lazy-load` event will be emitted to allow you to load data asynchronously. The item will remain in a loading state until its content is changed.
|
||||
|
||||
If you want to disable this behavior after the first load, simply remove the `lazy` attribute and, on the next expand, the existing content will be shown instead.
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree-item lazy>Available Trees</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<script type="module">
|
||||
const lazyItem = document.querySelector('sl-tree-item[lazy]');
|
||||
|
||||
lazyItem.addEventListener('sl-lazy-load', () => {
|
||||
// Simulate asynchronous loading
|
||||
setTimeout(() => {
|
||||
const subItems = ['Birch', 'Cedar', 'Maple', 'Pine'];
|
||||
|
||||
for (const item of subItems) {
|
||||
const treeItem = document.createElement('sl-tree-item');
|
||||
treeItem.innerText = item;
|
||||
lazyItem.append(treeItem);
|
||||
}
|
||||
|
||||
// Disable lazy mode once the content has been loaded
|
||||
lazyItem.lazy = false;
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [childItems, setChildItems] = useState([]);
|
||||
const [lazy, setLazy] = useState(true);
|
||||
|
||||
const handleLazyLoad = () => {
|
||||
// Simulate asynchronous loading
|
||||
setTimeout(() => {
|
||||
setChildItems(['Birch', 'Cedar', 'Maple', 'Pine']);
|
||||
|
||||
// Disable lazy mode once the content has been loaded
|
||||
setLazy(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<SlTree>
|
||||
<SlTreeItem lazy={lazy} onSlLazyLoad={handleLazyLoad}>
|
||||
Available Trees
|
||||
{childItems.map(item => (
|
||||
<SlTreeItem>{item}</SlTreeItem>
|
||||
))}
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Customizing the Expand and Collapse Icons
|
||||
|
||||
Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. To disable the animation, override the `rotate` property on the `expand-button` part as shown below.
|
||||
|
||||
```html preview
|
||||
<sl-tree class="custom-icons">
|
||||
<sl-icon name="plus-square" slot="expand-icon"></sl-icon>
|
||||
<sl-icon name="dash-square" slot="collapse-icon"></sl-icon>
|
||||
|
||||
<sl-tree-item>
|
||||
Deciduous
|
||||
<sl-tree-item>Birch</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
Maple
|
||||
<sl-tree-item>Field maple</sl-tree-item>
|
||||
<sl-tree-item>Red maple</sl-tree-item>
|
||||
<sl-tree-item>Sugar maple</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>Oak</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Coniferous
|
||||
<sl-tree-item>Cedar</sl-tree-item>
|
||||
<sl-tree-item>Pine</sl-tree-item>
|
||||
<sl-tree-item>Spruce</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
Non-trees
|
||||
<sl-tree-item>Bamboo</sl-tree-item>
|
||||
<sl-tree-item>Cactus</sl-tree-item>
|
||||
<sl-tree-item>Fern</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<style>
|
||||
.custom-icons sl-tree-item::part(expand-button) {
|
||||
/* Disable the expand/collapse animation */
|
||||
rotate: none;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```jsx react
|
||||
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTree>
|
||||
<SlIcon name="plus-square" slot="expand-icon"></SlIcon>
|
||||
<SlIcon name="dash-square" slot="collapse-icon"></SlIcon>
|
||||
|
||||
<SlTreeItem>
|
||||
Deciduous
|
||||
<SlTreeItem>Birch</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
Maple
|
||||
<SlTreeItem>Field maple</SlTreeItem>
|
||||
<SlTreeItem>Red maple</SlTreeItem>
|
||||
<SlTreeItem>Sugar maple</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>Oak</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Coniferous
|
||||
<SlTreeItem>Cedar</SlTreeItem>
|
||||
<SlTreeItem>Pine</SlTreeItem>
|
||||
<SlTreeItem>Spruce</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
|
||||
<SlTreeItem>
|
||||
Non-trees
|
||||
<SlTreeItem>Bamboo</SlTreeItem>
|
||||
<SlTreeItem>Cactus</SlTreeItem>
|
||||
<SlTreeItem>Fern</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
```
|
||||
|
||||
### With Icons
|
||||
|
||||
Decorative icons can be used before labels to provide hints for each node.
|
||||
|
||||
```html preview
|
||||
<sl-tree class="tree-with-icons">
|
||||
<sl-tree-item expanded>
|
||||
<sl-icon name="folder"></sl-icon>
|
||||
Documents
|
||||
|
||||
<sl-tree-item>
|
||||
<sl-icon name="folder"> </sl-icon>
|
||||
Photos
|
||||
<sl-tree-item>
|
||||
<sl-icon name="image"></sl-icon>
|
||||
birds.jpg
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="image"></sl-icon>
|
||||
kitten.jpg
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="image"></sl-icon>
|
||||
puppy.jpg
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
<sl-tree-item>
|
||||
<sl-icon name="folder"></sl-icon>
|
||||
Writing
|
||||
<sl-tree-item>
|
||||
<sl-icon name="file"></sl-icon>
|
||||
draft.txt
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="file-pdf"></sl-icon>
|
||||
final.pdf
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="file-bar-graph"></sl-icon>
|
||||
sales.xls
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<SlTree class="tree-with-icons">
|
||||
<SlTreeItem expanded>
|
||||
<SlIcon name="folder" />
|
||||
Root
|
||||
<SlTreeItem>
|
||||
<SlIcon name="folder" />
|
||||
Folder 1<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 1 - 1
|
||||
</SlTreeItem>
|
||||
<SlTreeItem disabled>
|
||||
<SlIcon name="files" />
|
||||
File 1 - 2
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 1 - 3
|
||||
</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
Folder 2<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 2 - 1
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 2 - 2
|
||||
</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
<SlTreeItem>
|
||||
<SlIcon name="files" />
|
||||
File 1
|
||||
</SlTreeItem>
|
||||
</SlTreeItem>
|
||||
</SlTree>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
[component-metadata:sl-tree]
|
||||
240
docs/eleventy.config.cjs
Normal file
@@ -0,0 +1,240 @@
|
||||
/* eslint-disable no-invalid-this */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const lunr = require('lunr');
|
||||
const { capitalCase } = require('change-case');
|
||||
const { JSDOM } = require('jsdom');
|
||||
const { customElementsManifest, getAllComponents } = require('./_utilities/cem.cjs');
|
||||
const webAwesomeFlavoredMarkdown = require('./_utilities/markdown.cjs');
|
||||
const activeLinks = require('./_utilities/active-links.cjs');
|
||||
const anchorHeadings = require('./_utilities/anchor-headings.cjs');
|
||||
const codePreviews = require('./_utilities/code-previews.cjs');
|
||||
const copyCodeButtons = require('./_utilities/copy-code-buttons.cjs');
|
||||
const externalLinks = require('./_utilities/external-links.cjs');
|
||||
const highlightCodeBlocks = require('./_utilities/highlight-code.cjs');
|
||||
const tableOfContents = require('./_utilities/table-of-contents.cjs');
|
||||
const prettier = require('./_utilities/prettier.cjs');
|
||||
const scrollingTables = require('./_utilities/scrolling-tables.cjs');
|
||||
const typography = require('./_utilities/typography.cjs');
|
||||
const replacer = require('./_utilities/replacer.cjs');
|
||||
|
||||
const assetsDir = 'assets';
|
||||
const cdndir = 'cdn';
|
||||
const npmdir = 'dist';
|
||||
const allComponents = getAllComponents();
|
||||
let hasBuiltSearchIndex = false;
|
||||
|
||||
module.exports = function (eleventyConfig) {
|
||||
//
|
||||
// Global data
|
||||
//
|
||||
eleventyConfig.addGlobalData('baseUrl', 'https://shoelace.style/'); // the production URL
|
||||
eleventyConfig.addGlobalData('layout', 'default'); // make 'default' the default layout
|
||||
eleventyConfig.addGlobalData('toc', true); // enable the table of contents
|
||||
eleventyConfig.addGlobalData('meta', {
|
||||
title: 'Web Awesome',
|
||||
description: 'A forward-thinking library of web components.',
|
||||
image: 'images/og-image.png',
|
||||
version: customElementsManifest.package.version,
|
||||
components: allComponents,
|
||||
cdndir,
|
||||
npmdir
|
||||
});
|
||||
|
||||
//
|
||||
// Layout aliases
|
||||
//
|
||||
eleventyConfig.addLayoutAlias('default', 'default.njk');
|
||||
|
||||
//
|
||||
// Copy assets
|
||||
//
|
||||
eleventyConfig.addPassthroughCopy(assetsDir);
|
||||
eleventyConfig.setServerPassthroughCopyBehavior('passthrough'); // emulates passthrough copy during --serve
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
// Generates a URL relative to the site's root
|
||||
eleventyConfig.addNunjucksGlobal('rootUrl', (value = '', absolute = false) => {
|
||||
value = path.join('/', value);
|
||||
return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value;
|
||||
});
|
||||
|
||||
// Generates a URL relative to the site's asset directory
|
||||
eleventyConfig.addNunjucksGlobal('assetUrl', (value = '', absolute = false) => {
|
||||
value = path.join(`/${assetsDir}`, value);
|
||||
return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value;
|
||||
});
|
||||
|
||||
// Fetches a specific component's metadata
|
||||
eleventyConfig.addNunjucksGlobal('getComponent', tagName => {
|
||||
const component = allComponents.find(c => c.tagName === tagName);
|
||||
if (!component) {
|
||||
throw new Error(
|
||||
`Unable to find a component called "${tagName}". Make sure the file name is the same as the component's tag ` +
|
||||
`name (minus the wa- prefix).`
|
||||
);
|
||||
}
|
||||
return component;
|
||||
});
|
||||
|
||||
//
|
||||
// Custom markdown syntaxes
|
||||
//
|
||||
eleventyConfig.setLibrary('md', webAwesomeFlavoredMarkdown);
|
||||
|
||||
//
|
||||
// Filters
|
||||
//
|
||||
eleventyConfig.addFilter('markdown', content => {
|
||||
return webAwesomeFlavoredMarkdown.render(content);
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('markdownInline', content => {
|
||||
return webAwesomeFlavoredMarkdown.renderInline(content);
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('classNameToComponentName', className => {
|
||||
let name = capitalCase(className.replace(/^Wa/, ''));
|
||||
if (name === 'Qr Code') name = 'QR Code'; // manual override
|
||||
return name;
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('removeWaPrefix', tagName => {
|
||||
return tagName.replace(/^wa-/, '');
|
||||
});
|
||||
|
||||
//
|
||||
// Transforms
|
||||
//
|
||||
eleventyConfig.addTransform('html-transform', function (content) {
|
||||
// Parse the template and get a Document object
|
||||
const doc = new JSDOM(content, {
|
||||
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
|
||||
// identify which ones are internal and which ones are external.
|
||||
url: `https://internal/`
|
||||
}).window.document;
|
||||
|
||||
// DOM transforms
|
||||
activeLinks(doc, { pathname: this.page.url });
|
||||
anchorHeadings(doc, {
|
||||
within: '#content .content__body',
|
||||
levels: ['h2', 'h3', 'h4', 'h5']
|
||||
});
|
||||
tableOfContents(doc, {
|
||||
levels: ['h2', 'h3'],
|
||||
container: '#content .content__toc > ul',
|
||||
within: '#content .content__body'
|
||||
});
|
||||
codePreviews(doc);
|
||||
externalLinks(doc, { target: '_blank' });
|
||||
highlightCodeBlocks(doc);
|
||||
scrollingTables(doc);
|
||||
copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks
|
||||
typography(doc, '#content');
|
||||
|
||||
replacer(doc, [
|
||||
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
|
||||
{ pattern: '%CDNDIR%', replacement: cdndir },
|
||||
{ pattern: '%NPMDIR%', replacement: npmdir }
|
||||
]);
|
||||
|
||||
// Serialize the Document object to an HTML string and prepend the doctype
|
||||
content = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
||||
|
||||
// String transforms
|
||||
content = prettier(content);
|
||||
|
||||
return content;
|
||||
});
|
||||
|
||||
//
|
||||
// Build a search index
|
||||
//
|
||||
eleventyConfig.on('eleventy.after', ({ results }) => {
|
||||
// We only want to build the search index on the first run so all pages get indexed.
|
||||
if (hasBuiltSearchIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const map = {};
|
||||
const searchIndexFilename = path.join(eleventyConfig.dir.output, assetsDir, 'search.json');
|
||||
const lunrInput = path.resolve('../node_modules/lunr/lunr.min.js');
|
||||
const lunrOutput = path.join(eleventyConfig.dir.output, assetsDir, 'scripts/lunr.js');
|
||||
const searchIndex = lunr(function () {
|
||||
// The search index uses these field names extensively, so shortening them can save some serious bytes. The
|
||||
// initial index file went from 468 KB => 401 KB by using single-character names!
|
||||
this.ref('id'); // id
|
||||
this.field('t', { boost: 50 }); // title
|
||||
this.field('h', { boost: 25 }); // headings
|
||||
this.field('c'); // content
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const url = path
|
||||
.join('/', path.relative(eleventyConfig.dir.output, result.outputPath))
|
||||
.replace(/\\/g, '/') // convert backslashes to forward slashes
|
||||
.replace(/\/index.html$/, '/'); // convert trailing /index.html to /
|
||||
const doc = new JSDOM(result.content, {
|
||||
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
|
||||
// identify which ones are internal and which ones are external.
|
||||
url: `https://internal/`
|
||||
}).window.document;
|
||||
const content = doc.querySelector('#content');
|
||||
|
||||
// Get title and headings
|
||||
const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim();
|
||||
const headings = [...content.querySelectorAll('h1, h2, h3, h4')]
|
||||
.map(heading => heading.textContent)
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Remove code blocks and whitespace from content
|
||||
[...content.querySelectorAll('code[class|=language]')].forEach(code => code.remove());
|
||||
const textContent = content.textContent.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Update the index and map
|
||||
this.add({ id: index, t: title, h: headings, c: textContent });
|
||||
map[index] = { title, url };
|
||||
});
|
||||
});
|
||||
|
||||
// Copy the Lunr search client and write the index
|
||||
fs.mkdirSync(path.dirname(lunrOutput), { recursive: true });
|
||||
fs.copyFileSync(lunrInput, lunrOutput);
|
||||
fs.writeFileSync(searchIndexFilename, JSON.stringify({ searchIndex, map }), 'utf-8');
|
||||
|
||||
hasBuiltSearchIndex = true;
|
||||
});
|
||||
|
||||
//
|
||||
// Send a signal to stdout that let's the build know we've reached this point
|
||||
//
|
||||
eleventyConfig.on('eleventy.after', () => {
|
||||
console.log('[eleventy.after]');
|
||||
});
|
||||
|
||||
//
|
||||
// Dev server options (see https://www.11ty.dev/docs/dev-server/#options)
|
||||
//
|
||||
eleventyConfig.setServerOptions({
|
||||
domDiff: false, // disable dom diffing so custom elements don't break on reload,
|
||||
port: 4000, // if port 4000 is taken, 11ty will use the next one available
|
||||
watch: ['cdn/**/*'] // additional files to watch that will trigger server updates (array of paths or globs)
|
||||
});
|
||||
|
||||
//
|
||||
// 11ty config
|
||||
//
|
||||
return {
|
||||
dir: {
|
||||
input: 'pages',
|
||||
output: '../_site',
|
||||
includes: '../_includes' // resolved relative to the input dir
|
||||
},
|
||||
markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files
|
||||
templateEngineOverride: ['njk'] // just Nunjucks and then markdown
|
||||
};
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
# Angular
|
||||
|
||||
Angular [plays nice](https://custom-elements-everywhere.com/#angular) with custom elements, so you can use Shoelace in your Angular apps with ease.
|
||||
|
||||
## Installation
|
||||
|
||||
To add Shoelace to your Angular app, install the package from npm.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
|
||||
|
||||
```jsx
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
|
||||
|
||||
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/');
|
||||
```
|
||||
|
||||
?> If you'd rather not use the CDN for assets, you can create a build task that copies `node_modules/@shoelace-style/shoelace/dist/assets` into a public folder in your app. Then you can point the base path to that folder instead.
|
||||
|
||||
## Configuration
|
||||
|
||||
Then make sure to apply the custom elements schema as shown below.
|
||||
|
||||
```js
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
Now you can start using Shoelace components in your app!
|
||||
|
||||
?> Are you using Shoelace with Angular? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/angular.md)
|
||||
@@ -1,142 +0,0 @@
|
||||
# React
|
||||
|
||||
Shoelace offers a React version of every component to provide an idiomatic experience for React users. You can easily toggle between HTML and React examples throughout the documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
To add Shoelace to your React app, install the package from npm.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
|
||||
|
||||
```jsx
|
||||
// App.jsx
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
|
||||
|
||||
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/');
|
||||
```
|
||||
|
||||
?> If you'd rather not use the CDN for assets, you can create a [build task](https://webpack.js.org/plugins/copy-webpack-plugin/) that copies `node_modules/@shoelace-style/shoelace/dist/assets` into your app's `public` directory. Then you can point the base path to that folder instead.
|
||||
|
||||
Now you can start using components!
|
||||
|
||||
## Usage
|
||||
|
||||
### Importing Components
|
||||
|
||||
Every Shoelace component is available to import as a React component. Note that we're importing the `<SlButton>` _React component_ instead of the `<sl-button>` _custom element_ in the example below.
|
||||
|
||||
```jsx
|
||||
import { SlButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const MyComponent = () => <SlButton variant="primary">Click me</SlButton>;
|
||||
|
||||
export default MyComponent;
|
||||
```
|
||||
|
||||
You can find a copy + paste import for each component in the "importing" section of its documentation.
|
||||
|
||||
### Event Handling
|
||||
|
||||
Many Shoelace components emit [custom events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). For example, the [input component](/components/input) emits the `sl-input` event when it receives input. In React, you can listen for the event using `onSlInput`.
|
||||
|
||||
Here's how you can bind the input's value to a state variable.
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
function MyComponent() {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return <SlInput value={value} onSlInput={event => setValue(event.target.value)} />;
|
||||
}
|
||||
|
||||
export default MyComponent;
|
||||
```
|
||||
|
||||
If you're using TypeScript, it's important to note that `event.target` will be a reference to the underlying custom element. You can use `(event.target as any).value` as a quick fix, or you can strongly type the event target as shown below.
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
import type SlInputElement from '@shoelace-style/shoelace/dist/components/input/input';
|
||||
|
||||
function MyComponent() {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return <SlInput value={value} onSlInput={event => setValue((event.target as SlInputElement).value)} />;
|
||||
}
|
||||
|
||||
export default MyComponent;
|
||||
```
|
||||
|
||||
## Testing with Jest
|
||||
|
||||
Testing with web components can be challenging if your test environment runs in a Node environment (i.e. it doesn't run in a real browser). Fortunately, [Jest](https://jestjs.io/) has made a number of strides to support web components and provide additional browser APIs. However, it's still not a complete replication of a browser environment.
|
||||
|
||||
Here are some tips that will help smooth things over if you're having trouble with Jest + Shoelace.
|
||||
|
||||
?> If you're looking for a fast, modern testing alternative, consider [Web Test Runner](https://modern-web.dev/docs/test-runner/overview/).
|
||||
|
||||
### Upgrade Jest
|
||||
|
||||
Jest underwent a major revamp and received support for web components in [version 26.5.0](https://github.com/facebook/jest/blob/main/CHANGELOG.md#2650) when it introduced [JSDOM 16.2.0](https://github.com/jsdom/jsdom/blob/master/Changelog.md#1620). This release also included a number of mocks for built-in browser functions such as `MutationObserver`, `document.createRange`, and others.
|
||||
|
||||
If you're using [Create React App](https://reactjs.org/docs/create-a-new-react-app.html#create-react-app), you can update `react-scripts` which will also update Jest.
|
||||
|
||||
```
|
||||
npm install react-scripts@latest
|
||||
```
|
||||
|
||||
### Mock Missing APIs
|
||||
|
||||
Some components use `window.matchMedia`, but this function isn't supported by JSDOM so you'll need to mock it yourself.
|
||||
|
||||
In `src/setupTests.js`, add the following.
|
||||
|
||||
```js
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn()
|
||||
}))
|
||||
});
|
||||
```
|
||||
|
||||
For more details, refer to Jest's [manual mocking](https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom) documentation.
|
||||
|
||||
### Transform ES Modules
|
||||
|
||||
ES Modules are a [well-supported browser standard](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/). This is how Shoelace is distributed, but most React apps expect CommonJS. As a result, you'll probably run into the following error.
|
||||
|
||||
```
|
||||
Error: Unable to import outside of a module
|
||||
```
|
||||
|
||||
To fix this, add the following to your `package.json` which tells the transpiler to process Shoelace modules.
|
||||
|
||||
```js
|
||||
{
|
||||
"jest": {
|
||||
"transformIgnorePatterns": ["node_modules/?!(@shoelace)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These instructions are for apps created via Create React App. If you're using Jest directly, you can add `transformIgnorePatterns` directly into `jest.config.js`.
|
||||
|
||||
For more details, refer to Jest's [`transformIgnorePatterns` customization](https://jestjs.io/docs/tutorial-react-native#transformignorepatterns-customization) documentation.
|
||||
|
||||
?> Are you using Shoelace with React? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/react.md)
|
||||
@@ -1,100 +0,0 @@
|
||||
# Vue
|
||||
|
||||
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements, so you can use Shoelace in your Vue apps with ease.
|
||||
|
||||
?> These instructions are for Vue 3 and above. If you're using Vue 2, please see the [Vue 2 instructions](/frameworks/vue-2).
|
||||
|
||||
## Installation
|
||||
|
||||
To add Shoelace to your Vue app, install the package from npm.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
|
||||
|
||||
```jsx
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
|
||||
|
||||
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/');
|
||||
```
|
||||
|
||||
?> If you'd rather not use the CDN for assets, you can create a build task that copies `node_modules/@shoelace-style/shoelace/dist/assets` into a public folder in your app. Then you can point the base path to that folder instead.
|
||||
|
||||
## Configuration
|
||||
|
||||
You'll need to tell Vue to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
|
||||
|
||||
```js
|
||||
import { fileURLToPath, URL } from 'url';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('sl-')
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Now you can start using Shoelace components in your app!
|
||||
|
||||
## Usage
|
||||
|
||||
### QR code generator example
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>QR code generator</h1>
|
||||
|
||||
<sl-input maxlength="255" clearable label="Value" v-model="qrCode"></sl-input>
|
||||
|
||||
<sl-qr-code :value="qrCode"></sl-qr-code>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import '@shoelace-style/shoelace/dist/components/qr-code/qr-code.js';
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
||||
|
||||
const qrCode = ref();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
sl-input {
|
||||
margin: var(--sl-spacing-large) 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Binding Complex Data
|
||||
|
||||
When binding complex data such as objects and arrays, use the `.prop` modifier to make Vue bind them as a property instead of an attribute.
|
||||
|
||||
```html
|
||||
<sl-color-picker :swatches.prop="mySwatches" />
|
||||
```
|
||||
|
||||
?> Are you using Shoelace with Vue? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/vue.md)
|
||||
@@ -1,500 +0,0 @@
|
||||
# Form Controls
|
||||
|
||||
Every Shoelace component makes use of a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate markup, styles, and behavior. One caveat of this approach is that native `<form>` elements do not recognize form controls located inside a shadow root.
|
||||
|
||||
Shoelace solves this problem by using the [`formdata`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event) event, which is [available in all modern browsers](https://caniuse.com/mdn-api_htmlformelement_formdata_event). This means, when a form is submitted, Shoelace form controls will automatically append their values to the `FormData` object that's used to submit the form. In most cases, things will "just work." However, if you're using a form serialization library, it might need to be adapted to recognize Shoelace form controls.
|
||||
|
||||
?> Shoelace uses event listeners to intercept the form's `formdata` and `submit` events. This allows it to inject data and trigger validation as necessary. If you're also attaching an event listener to the form, _you must attach it after Shoelace form controls are connected to the DOM_, otherwise your logic will run before Shoelace has a chance to inject form data and validate form controls.
|
||||
|
||||
## Data Serialization
|
||||
|
||||
Serialization is just a fancy word for collecting form data. If you're relying on standard form submissions, e.g. `<form action="...">`, you can probably skip this section. However, most modern apps use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or a library such as [axios](https://github.com/axios/axios) to submit forms using JavaScript.
|
||||
|
||||
The [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) interface offers a standard way to serialize forms in the browser. You can create a `FormData` object from any `<form>` element like this.
|
||||
|
||||
```js
|
||||
const form = document.querySelector('form');
|
||||
const data = new FormData(form);
|
||||
|
||||
// All form control data is available in a FormData object
|
||||
```
|
||||
|
||||
However, some folks find `FormData` tricky to work with or they need to pass a JSON payload to their server. To accommodate this, Shoelace offers a serialization utility that gathers form data and returns a simple JavaScript object instead.
|
||||
|
||||
```js
|
||||
import { serialize } from '@shoelace-style/shoelace/dist/utilities/form.js';
|
||||
|
||||
const form = document.querySelector('form');
|
||||
const data = serialize(form);
|
||||
|
||||
// All form control data is available in a plain object
|
||||
```
|
||||
|
||||
This results in an object with name/value pairs that map to each form control. If more than one form control shares the same name, the values will be passed as an array, e.g. `{ name: ['value1', 'value2'] }`.
|
||||
|
||||
## Constraint Validation
|
||||
|
||||
Client-side validation can be enabled through the browser's [Constraint Validation API](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation) for Shoelace form controls. You can activate it using attributes such as `required`, `pattern`, `minlength`, `maxlength`, etc. Shoelace implements many of the same attributes as native form controls, but check the documentation for a list of supported properties for each component.
|
||||
|
||||
If you don't want to use client-side validation, you can suppress this behavior by adding `novalidate` to the surrounding `<form>` element.
|
||||
|
||||
?> If this syntax looks unfamiliar, don't worry! Most of what you're learning on this page is platform knowledge that applies to regular form controls, too.
|
||||
|
||||
!> Client-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. **You should always validate and sanitize user input on the server!**
|
||||
|
||||
### Required Fields
|
||||
|
||||
To make a field required, use the `required` attribute. Required fields will automatically receive a `*` after their labels. This is configurable through the `--sl-input-required-content` custom property.
|
||||
|
||||
The form will not be submitted if a required field is incomplete.
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-required">
|
||||
<sl-input name="name" label="Name" required></sl-input>
|
||||
<br />
|
||||
<sl-select label="Favorite Animal" clearable required>
|
||||
<sl-option value="birds">Birds</sl-option>
|
||||
<sl-option value="cats">Cats</sl-option>
|
||||
<sl-option value="dogs">Dogs</sl-option>
|
||||
<sl-option value="other">Other</sl-option>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-textarea name="comment" label="Comment" required></sl-textarea>
|
||||
<br />
|
||||
<sl-checkbox required>Check me before submitting</sl-checkbox>
|
||||
<br /><br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
const form = document.querySelector('.input-validation-required');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlCheckbox, SlInput, SlMenuItem, SlSelect, SlTextarea } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SlInput name="name" label="Name" required />
|
||||
<br />
|
||||
<SlSelect label="Favorite Animal" clearable required>
|
||||
<SlMenuItem value="birds">Birds</SlMenuItem>
|
||||
<SlMenuItem value="cats">Cats</SlMenuItem>
|
||||
<SlMenuItem value="dogs">Dogs</SlMenuItem>
|
||||
<SlMenuItem value="other">Other</SlMenuItem>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlTextarea name="comment" label="Comment" required></SlTextarea>
|
||||
<br />
|
||||
<SlCheckbox required>Check me before submitting</SlCheckbox>
|
||||
<br />
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Input Patterns
|
||||
|
||||
To restrict a value to a specific [pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern), use the `pattern` attribute. This example only allows the letters A-Z, so the form will not submit if a number or symbol is entered. This only works with `<sl-input>` elements.
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-pattern">
|
||||
<sl-input name="letters" required label="Letters" pattern="[A-Za-z]+"></sl-input>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
const form = document.querySelector('.input-validation-pattern');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SlInput name="letters" required label="Letters" pattern="[A-Za-z]+" />
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Input Types
|
||||
|
||||
Some input types will automatically trigger constraints, such as `email` and `url`.
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-type">
|
||||
<sl-input type="email" label="Email" placeholder="you@example.com" required></sl-input>
|
||||
<br />
|
||||
<sl-input type="url" label="URL" placeholder="https://example.com/" required></sl-input>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
const form = document.querySelector('.input-validation-type');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SlInput type="email" label="Email" placeholder="you@example.com" required />
|
||||
<br />
|
||||
<SlInput type="url" label="URL" placeholder="https://example.com/" required />
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Error Messages
|
||||
|
||||
To create a custom validation error, pass a non-empty string to the `setCustomValidity()` method. This will override any existing validation constraints. The form will not be submitted when a custom validity is set and the browser will show a validation error when the containing form is submitted. To make the input valid again, call `setCustomValidity()` again with an empty string.
|
||||
|
||||
```html preview
|
||||
<form class="input-validation-custom">
|
||||
<sl-input label="Type “shoelace”" required></sl-input>
|
||||
<br />
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
const form = document.querySelector('.input-validation-custom');
|
||||
const input = form.querySelector('sl-input');
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
|
||||
input.addEventListener('sl-input', () => {
|
||||
if (input.value === 'shoelace') {
|
||||
input.setCustomValidity('');
|
||||
} else {
|
||||
input.setCustomValidity("Hey, you're supposed to type 'shoelace' before submitting this!");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useRef, useState } from 'react';
|
||||
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const input = useRef(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
function handleInput(event) {
|
||||
setValue(event.target.value);
|
||||
|
||||
if (event.target.value === 'shoelace') {
|
||||
input.current.setCustomValidity('');
|
||||
} else {
|
||||
input.current.setCustomValidity("Hey, you're supposed to type 'shoelace' before submitting this!");
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SlInput ref={input} label="Type 'shoelace'" required value={value} onSlInput={handleInput} />
|
||||
<br />
|
||||
<SlButton type="submit" variant="primary">
|
||||
Submit
|
||||
</SlButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
?> Custom validation can be applied to any form control that supports the `setCustomValidity()` method. It is not limited to inputs and textareas.
|
||||
|
||||
## Custom Validation Styles
|
||||
|
||||
Due to the many ways form controls are used, Shoelace doesn't provide out of the box validation styles for form controls as part of its default theme. Instead, the following attributes will be applied to reflect a control's validity as users interact with it. You can use them to create custom styles for any of the validation states you're interested in.
|
||||
|
||||
- `data-required` - the form control is required
|
||||
- `data-optional` - the form control is optional
|
||||
- `data-invalid` - the form control is currently invalid
|
||||
- `data-valid` - the form control is currently valid
|
||||
- `data-user-invalid` - the form control is currently invalid and the user has interacted with it
|
||||
- `data-user-valid` - the form control is currently valid and the user has interacted with it
|
||||
|
||||
These attributes map to the browser's built-in pseudo classes for validation: [`:required`](https://developer.mozilla.org/en-US/docs/Web/CSS/:required), [`:optional`](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional), [`:invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid), [`:valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:valid), and the proposed [`:user-invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid) and [`:user-valid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-valid).
|
||||
|
||||
?> In the future, data attributes will be replaced with custom pseudo classes such as `:--valid` and `:--invalid`. Shoelace is using data attributes as a workaround until browsers support custom states through [`ElementInternals.states`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states).
|
||||
|
||||
### Styling Invalid Form Controls
|
||||
|
||||
You can target validity using any of the aforementioned data attributes, but it's usually preferable to target `data-user-invalid` and `data-user-valid` since they get applied only after a user interaction such as typing or submitting. This prevents empty form controls from appearing invalid immediately, which often results in a poor user experience.
|
||||
|
||||
This example demonstrates custom validation styles using `data-user-invalid` and `data-user-valid`. Try Typing in the fields to see how validity changes with user input.
|
||||
|
||||
```html preview
|
||||
<form class="validity-styles">
|
||||
<sl-input
|
||||
name="name"
|
||||
label="Name"
|
||||
help-text="What would you like people to call you?"
|
||||
autocomplete="off"
|
||||
required
|
||||
></sl-input>
|
||||
|
||||
<sl-select name="animal" label="Favorite Animal" help-text="Select the best option." clearable required>
|
||||
<sl-option value="birds">Birds</sl-option>
|
||||
<sl-option value="cats">Cats</sl-option>
|
||||
<sl-option value="dogs">Dogs</sl-option>
|
||||
<sl-option value="other">Other</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<sl-checkbox value="accept" required>Accept terms and conditions</sl-checkbox>
|
||||
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
const form = document.querySelector('.validity-styles');
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
alert('All fields are valid!');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.validity-styles sl-input,
|
||||
.validity-styles sl-select,
|
||||
.validity-styles sl-checkbox {
|
||||
display: block;
|
||||
margin-bottom: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
/* user invalid styles */
|
||||
.validity-styles sl-input[data-user-invalid]::part(base),
|
||||
.validity-styles sl-select[data-user-invalid]::part(combobox),
|
||||
.validity-styles sl-checkbox[data-user-invalid]::part(control) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
.validity-styles [data-user-invalid]::part(form-control-label),
|
||||
.validity-styles [data-user-invalid]::part(form-control-help-text),
|
||||
.validity-styles sl-checkbox[data-user-invalid]::part(label) {
|
||||
color: var(--sl-color-danger-700);
|
||||
}
|
||||
|
||||
.validity-styles sl-checkbox[data-user-invalid]::part(control) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.validity-styles sl-input:focus-within[data-user-invalid]::part(base),
|
||||
.validity-styles sl-select:focus-within[data-user-invalid]::part(combobox),
|
||||
.validity-styles sl-checkbox:focus-within[data-user-invalid]::part(control) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-300);
|
||||
}
|
||||
|
||||
/* User valid styles */
|
||||
.validity-styles sl-input[data-user-valid]::part(base),
|
||||
.validity-styles sl-select[data-user-valid]::part(combobox),
|
||||
.validity-styles sl-checkbox[data-user-valid]::part(control) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
.validity-styles [data-user-valid]::part(form-control-label),
|
||||
.validity-styles [data-user-valid]::part(form-control-help-text),
|
||||
.validity-styles sl-checkbox[data-user-valid]::part(label) {
|
||||
color: var(--sl-color-success-700);
|
||||
}
|
||||
|
||||
.validity-styles sl-checkbox[data-user-valid]::part(control) {
|
||||
background-color: var(--sl-color-success-600);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.validity-styles sl-input:focus-within[data-user-valid]::part(base),
|
||||
.validity-styles sl-select:focus-within[data-user-valid]::part(combobox),
|
||||
.validity-styles sl-checkbox:focus-within[data-user-valid]::part(control) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Inline Form Validation
|
||||
|
||||
By default, Shoelace form controls use the browser's tooltip-style error messages. No mechanism is provided to show errors inline, as there are too many opinions on how that would work when combined with native form controls and other custom elements. You can, however, implement your own solution using the following technique.
|
||||
|
||||
To disable the browser's error messages, you need to cancel the `sl-invalid` event. Then you can apply your own inline validation errors. This example demonstrates a primitive way to do this.
|
||||
|
||||
```html preview
|
||||
<form class="inline-validation">
|
||||
<sl-input
|
||||
name="name"
|
||||
label="Name"
|
||||
help-text="What would you like people to call you?"
|
||||
autocomplete="off"
|
||||
required
|
||||
></sl-input>
|
||||
|
||||
<div id="name-error" aria-live="polite" hidden></div>
|
||||
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const form = document.querySelector('.inline-validation');
|
||||
const nameError = document.querySelector('#name-error');
|
||||
|
||||
// A form control is invalid
|
||||
form.addEventListener(
|
||||
'sl-invalid',
|
||||
event => {
|
||||
// Suppress the browser's constraint validation message
|
||||
event.preventDefault();
|
||||
|
||||
nameError.textContent = `Error: ${event.target.validationMessage}`;
|
||||
nameError.hidden = false;
|
||||
|
||||
event.target.focus();
|
||||
},
|
||||
{ capture: true } // you must use capture since sl-invalid doesn't bubble!
|
||||
);
|
||||
|
||||
// Handle form submit
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
nameError.hidden = true;
|
||||
nameError.textContent = '';
|
||||
setTimeout(() => alert('All fields are valid'), 50);
|
||||
});
|
||||
|
||||
// Handle form reset
|
||||
form.addEventListener('reset', event => {
|
||||
nameError.hidden = true;
|
||||
nameError.textContent = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#name-error {
|
||||
font-size: var(--sl-input-help-text-font-size-medium);
|
||||
color: var(--sl-color-danger-700);
|
||||
}
|
||||
|
||||
#name-error ~ sl-button {
|
||||
margin-top: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.inline-validation sl-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* user invalid styles */
|
||||
.inline-validation sl-input[data-user-invalid]::part(base) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
.inline-validation [data-user-invalid]::part(form-control-label),
|
||||
.inline-validation [data-user-invalid]::part(form-control-help-text) {
|
||||
color: var(--sl-color-danger-700);
|
||||
}
|
||||
|
||||
.inline-validation sl-input:focus-within[data-user-invalid]::part(base) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-300);
|
||||
}
|
||||
|
||||
/* User valid styles */
|
||||
.inline-validation sl-input[data-user-valid]::part(base) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
.inline-validation [data-user-valid]::part(form-control-label),
|
||||
.inline-validation [data-user-valid]::part(form-control-help-text) {
|
||||
color: var(--sl-color-success-700);
|
||||
}
|
||||
|
||||
.inline-validation sl-checkbox[data-user-valid]::part(control) {
|
||||
background-color: var(--sl-color-success-600);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-validation sl-input:focus-within[data-user-valid]::part(base) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
!> This example is meant to demonstrate the concept of providing your own error messages inline. It is not intended to scale to more complex forms. Users who want this functionality are encouraged to build a more appropriate validation solution using the techniques shown below. Depending on how you implement this feature, custom error messages may affect the accessibility of your form controls.
|
||||
|
||||
## Getting Associated Form Controls
|
||||
|
||||
At this time, using [`HTMLFormElement.elements`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements) will not return Shoelace form controls because the browser is unaware of their status as custom element form controls. Fortunately, Shoelace provides an `elements()` function that does something very similar. However, instead of returning an [`HTMLFormControlsCollection`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection), it returns an array of HTML and Shoelace form controls in the order they appear in the DOM.
|
||||
|
||||
```js
|
||||
import { getFormControls } from '@shoelace-style/shoelace/dist/utilities/form.js';
|
||||
|
||||
const form = document.querySelector('#my-form');
|
||||
const formControls = getFormControls(form);
|
||||
|
||||
console.log(formControls); // e.g. [input, sl-input, ...]
|
||||
```
|
||||
|
||||
?> You probably don't need this function! If you're gathering form data for submission, you probably want to use [Data Serialization](#data-serializing) instead.
|
||||
@@ -1,156 +0,0 @@
|
||||
# Installation
|
||||
|
||||
You can use Shoelace via CDN or by installing it locally. You can also [cherry pick](#cherry-picking) individual components for faster load times.
|
||||
|
||||
If you're using a framework, make sure to check out the pages for [React](/frameworks/react), [Vue](/frameworks/vue), and [Angular](/frameworks/angular).
|
||||
|
||||
## Autoloading (Experimental)
|
||||
|
||||
The autoloader is the simplest and most efficient way to use Shoelace. A lightweight script watches the DOM for unregistered Shoelace elements and lazy loads them for you. This works for elements already on the page and elements that get added later on.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace-autoloader.js"></script>
|
||||
```
|
||||
|
||||
?> While convenient, one caveat of autoloading is you may see a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
|
||||
|
||||
## CDN Installation
|
||||
|
||||
The easiest way to install Shoelace is with the CDN. Just add the following tags to your page to get all components and the default light theme.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
?> If you're only using a handful of components, it will be more efficient to [autoload](#autoloading-experimental) or [cherry pick](#cherry-picking) the ones you need.
|
||||
|
||||
### Dark Theme
|
||||
|
||||
If you prefer to use the [dark theme](/getting-started/themes#dark-theme) instead, use this code and add `<html class="sl-theme-dark">` to the page.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
### Light & Dark Theme
|
||||
|
||||
If you want to load the light or dark theme based on the user's `prefers-color-scheme` setting, use this. The `media` attributes ensure that only the user's preferred theme stylesheet loads and the `onload` attribute sets the appropriate [theme class](/getting-started/themes) on the `<html>` element.
|
||||
|
||||
```html
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="(prefers-color-scheme:light)"
|
||||
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="(prefers-color-scheme:dark)"
|
||||
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css"
|
||||
onload="document.documentElement.classList.add('sl-theme-dark');"
|
||||
/>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
Now you can [start using Shoelace!](/getting-started/usage)
|
||||
|
||||
## Local Installation
|
||||
|
||||
If you don't want to use the CDN, you can install Shoelace locally with the following command.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
It's up to you to make the source files available to your app. One way to do this is to create a route in your app called `/shoelace` that serves static files from `node_modules/@shoelace-style/shoelace`.
|
||||
|
||||
Once you've done that, add the following tags to your page. Make sure to update `href` and `src` so they point to the route you created.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/shoelace/dist/themes/light.css" />
|
||||
<script type="module" src="/shoelace/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
Alternatively, [you can use a bundler](#bundling).
|
||||
|
||||
?> For clarity, the docs will usually show imports from `@shoelace-style/shoelace`. If you're not using a module resolver or bundler, you'll need to adjust these paths to point to the folder Shoelace is in.
|
||||
|
||||
## Setting the Base Path
|
||||
|
||||
Some components rely on assets (icons, images, etc.) and Shoelace needs to know where they're located. For convenience, Shoelace will try to auto-detect the correct location based on the script you've loaded it from. This assumes assets are colocated with `shoelace.js` or `shoelace-autoloader.js` and will "just work" for most users.
|
||||
|
||||
However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Shoelace, you'll need to set the base path. You can do this one of two ways.
|
||||
|
||||
```html
|
||||
<!-- Option 1: the data-shoelace attribute -->
|
||||
<script src="bundle.js" data-shoelace="/path/to/shoelace/dist"></script>
|
||||
|
||||
<!-- Option 2: the setBasePath() method -->
|
||||
<script src="bundle.js"></script>
|
||||
<script type="module">
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js';
|
||||
setBasePath('/path/to/shoelace/dist');
|
||||
</script>
|
||||
```
|
||||
|
||||
?> The library also exports a `getBasePath()` method you can use to reference assets.
|
||||
|
||||
## Cherry Picking
|
||||
|
||||
The previous approach is the _easiest_ way to load Shoelace, but easy isn't always efficient. You'll incur the full size of the library even if you only use a handful of components. This is convenient for prototyping or if you're using most of the components, but it may result in longer load times in production. To improve this, you can cherry pick the components you need.
|
||||
|
||||
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load components manually.
|
||||
|
||||
Here's an example that loads only the button component. Again, if you're not using a module resolver, you'll need to adjust the path to point to the folder Shoelace is in.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="/path/to/shoelace/dist/themes/light.css" />
|
||||
|
||||
<script type="module" data-shoelace="/path/to/shoelace/dist">
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js';
|
||||
|
||||
// <sl-button> is ready to use!
|
||||
</script>
|
||||
```
|
||||
|
||||
You can copy and paste the code to import a component from the "Importing" section of the component's documentation. Note that some components have dependencies that are automatically imported when you cherry pick. If a component has dependencies, they will be listed in the "Dependencies" section of its docs.
|
||||
|
||||
!> Never cherry pick components or utilities from `shoelace.js` as this will cause the browser to load the entire library. Instead, cherry pick from specific modules as shown above.
|
||||
|
||||
!> You will see files named `chunk.[hash].js` in the `chunks` directory. Never import these files directly, as they are generated and change from version to version.
|
||||
|
||||
## Bundling
|
||||
|
||||
Shoelace is distributed as a collection of standard ES modules that [all modern browsers can understand](https://caniuse.com/es6-module). However, importing a lot of modules can result in a lot of HTTP requests and potentially longer load times. Using a CDN can alleviate this, but some users may wish to further optimize their imports with a bundler.
|
||||
|
||||
To use Shoelace with a bundler, first install Shoelace along with your bundler of choice.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
Now it's time to configure your bundler. Configurations vary for each tool, but here are some examples to help you get started.
|
||||
|
||||
- [Example webpack config](https://github.com/shoelace-style/webpack-example/blob/master/webpack.config.js)
|
||||
- [Example Rollup config](https://github.com/shoelace-style/rollup-example/blob/master/rollup.config.js)
|
||||
|
||||
Once your bundler is configured, you'll be able to import Shoelace components and utilities.
|
||||
|
||||
```js
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js';
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
||||
import '@shoelace-style/shoelace/dist/components/rating/rating.js';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js';
|
||||
|
||||
// Set the base path to the folder you copied Shoelace's assets to
|
||||
setBasePath('/path/to/shoelace/dist');
|
||||
|
||||
// <sl-button>, <sl-icon>, <sl-input>, and <sl-rating> are ready to use!
|
||||
```
|
||||
|
||||
!> Component modules include side effects for registration purposes. Because of this, importing directly from `@shoelace-style/shoelace` may result in a larger bundle size than necessary. For optimal tree shaking, always cherry pick, i.e. import components and utilities from their respective files, as shown above.
|
||||