kopia lustrzana https://github.com/shoelace-style/shoelace
Porównaj commity
256 Commity
Autor | SHA1 | Data |
---|---|---|
HappyXiaoAnAn | 6092796850 | |
Cory LaViska | 4120988079 | |
Enrico Gruner | 77c482ed16 | |
Cory LaViska | 9399df6e19 | |
Ahmad Alfy | b98deb877e | |
Konnor Rogers | f256d7aa8a | |
Cory LaViska | f757d514e4 | |
Cory LaViska | 07d2144395 | |
Cory LaViska | c042c8fe34 | |
Cory LaViska | 975115a923 | |
Cory LaViska | c31d4f5855 | |
Cory LaViska | 75e20e0672 | |
Uaena_Alex_John | 7ece400a30 | |
cptKNJO | 06a27dd899 | |
Konnor Rogers | 9767e84d26 | |
Cory LaViska | 8726910160 | |
Cory LaViska | d478ccb2da | |
Cory LaViska | d94acc6e06 | |
Fiqri Syah Redha | eb42671ef3 | |
Christian Schilling | 3ad6364678 | |
Konnor Rogers | 64996b2d35 | |
Susanne Kirchner | 0daa5d8dee | |
Konnor Rogers | 16d5575307 | |
Konnor Rogers | a427433701 | |
Danny Andrews | c6da4f5b14 | |
Cory LaViska | d0b71adb81 | |
Cory LaViska | ae66483671 | |
Cory LaViska | 537fd87497 | |
Cory LaViska | 1534f47d34 | |
Cory LaViska | eb08be0fce | |
Cory LaViska | dfc4cb6248 | |
Cory LaViska | c1eda83e5b | |
Cory LaViska | 0e5048989d | |
Konnor Rogers | ff2e0486b4 | |
Sebi | 4aa5e9c1f2 | |
Cory LaViska | 0b7e70bccf | |
Alessandro | 31f2600816 | |
Cory LaViska | f6d5344c44 | |
Nic Newdigate | 5a89439e14 | |
Cory LaViska | 2878957ef5 | |
Konnor Rogers | cd5a6486da | |
Cory LaViska | 77d6f27248 | |
Konnor Rogers | 7a62a87b9b | |
Cory LaViska | 0ac61a6a22 | |
Matt Walkland | acf76cf359 | |
Cory LaViska | 3451ec753c | |
cyantree | 2a4b3ee2e9 | |
Konnor Rogers | 3bc8495874 | |
Cory LaViska | 7f87887477 | |
Amadej Glasenčnik | c88b38f194 | |
Cory LaViska | e2bce65c02 | |
Susanne Kirchner | 9cbb0b8a95 | |
Cory LaViska | 2128e62109 | |
Cory LaViska | 12ce0217e5 | |
RoyDust | 1a2969a74b | |
Cory LaViska | 033fec9471 | |
Cory LaViska | 8272619663 | |
cyantree | 298892b10a | |
Cory LaViska | e1102ba9cf | |
Cory LaViska | b589938443 | |
Cory LaViska | 07b13d489a | |
Cory LaViska | e9405d33a8 | |
Cory LaViska | f2a42565e2 | |
Cory LaViska | 23f09dfa79 | |
stefanholzapfel | 6e288a80a3 | |
Cory LaViska | 9b19c4c782 | |
cyantree | 6440387432 | |
Cory LaViska | f3be76840f | |
Cory LaViska | 1056a10f8e | |
Cory LaViska | f9a73567f7 | |
Burton Smith | 6bc06d5d95 | |
Cory LaViska | 7571f8c534 | |
Cory LaViska | 1bf3e5a2b7 | |
Cory LaViska | 02ce4dbf4e | |
Cory LaViska | 775f30107f | |
Cory LaViska | 9ee1617696 | |
Alessandro | 7e38e93ab2 | |
Cory LaViska | dafb35c6e2 | |
Cory LaViska | a36bbe2fc4 | |
Ahmad Alfy | 4185430989 | |
Cory LaViska | e6d3d8317a | |
clintcs | 9451c3b8de | |
Konnor Rogers | a5e9b942e3 | |
Cory LaViska | 380d56fa40 | |
Cory LaViska | 83fe1ff28e | |
Cory LaViska | a4c49e95a9 | |
Cory LaViska | beea96b373 | |
Cory LaViska | 6751b21283 | |
Cory LaViska | e37139b7cf | |
Cory LaViska | 1f87f429ed | |
Cory LaViska | afc6dc1923 | |
Cory LaViska | e2a64486d0 | |
Cory LaViska | 8473d06822 | |
Cory LaViska | cb15749500 | |
Luke Warlow | 0a319c3646 | |
Matin | 1d626c1357 | |
YassSSH | caf47069c0 | |
Konnor Rogers | 773255881b | |
Cory LaViska | 478c8bdf69 | |
Alessandro | 9f640aa0a2 | |
Burton Smith | b1908d73dc | |
clintcs | 1a77e603f8 | |
Cory LaViska | ac5e2d2d43 | |
Alessandro | 95881b8cf8 | |
Cory LaViska | eb39610a46 | |
Cory LaViska | e231f8a4a1 | |
Cory LaViska | 6b9e78f05d | |
Cory LaViska | b79c72725b | |
Cory LaViska | 92bde9c66b | |
Cory LaViska | dd483c0a04 | |
Cory LaViska | f5f4f9ae43 | |
Cory LaViska | a21ab1d044 | |
Cory LaViska | 75c45a2aa7 | |
Michael Warren | d909f4f73d | |
Konnor Rogers | 7891dbef93 | |
Konnor Rogers | b4ed398240 | |
Cory LaViska | 1710cfb8bc | |
Cory LaViska | 0080ff9c60 | |
Cory LaViska | caae94119c | |
Cory LaViska | 59ef323f38 | |
Cory LaViska | e1417b8e1a | |
Cory LaViska | bb20126b17 | |
Ryan | 3de99eee0a | |
Cory LaViska | 0d043767ec | |
Cory LaViska | b7eccb1bff | |
Konnor Rogers | dd27db5196 | |
Cory LaViska | 3e38da210e | |
Cory LaViska | 4864ab808d | |
Cory LaViska | e2b7327d98 | |
Mitch Ray | 1a8403b9b2 | |
Konnor Rogers | bfa7c4cda9 | |
Cory LaViska | 7fae62b806 | |
Cory LaViska | 15c6733949 | |
Cory LaViska | 1e57a632d9 | |
Cory LaViska | ffe492c503 | |
Cory LaViska | 21e2c7a473 | |
Cory LaViska | b6c9b64ec0 | |
Cory LaViska | 00435ac682 | |
Matt Obee | 4699f99107 | |
Rikard Kling | 025da5e59f | |
Cory LaViska | d99b90dee1 | |
Cory LaViska | 66c5e4cba2 | |
Cory LaViska | d7d9242d58 | |
Cory LaViska | 02ad181775 | |
Cory LaViska | 024c6e2e48 | |
Cory LaViska | 3fdbefa2d4 | |
Cory LaViska | 2b45c546e8 | |
Cory LaViska | a36ae4e482 | |
Cory LaViska | 3b2eb9bb5c | |
Cory LaViska | 1bf490aed0 | |
Cory LaViska | 1564df829b | |
Cory LaViska | facb5504a4 | |
Cory LaViska | ee18f3a449 | |
folini96 | c3c770b0e0 | |
Nick Lemmon | a1888c628f | |
Cory LaViska | 13c3e88384 | |
Cory LaViska | e0701fe3fc | |
Konnor Rogers | 35c2ad886d | |
Coridyn | e786aa86b5 | |
Konnor Rogers | 5221419816 | |
Konnor Rogers | f015dc9169 | |
Mitch Ray | a2b7816010 | |
Cory LaViska | 2a1f48c332 | |
Henry Wilkinson | 8ddef1a0bd | |
Mitch Ray | 468b0b9e66 | |
Cory LaViska | 6590dd4004 | |
Konnor Rogers | 12a45eb65d | |
Konnor Rogers | 5e620a8bb3 | |
Cory LaViska | b9fa2a60fe | |
Cory LaViska | b7a4a228d6 | |
Alessandro | 597a06c97c | |
Cory LaViska | 1087fe23f7 | |
Cory LaViska | 207a660738 | |
Cory LaViska | 296a24c74a | |
Cory LaViska | 265ef71e6d | |
Cory LaViska | 224bba2532 | |
Cory LaViska | d07f8e01ad | |
Alessandro | 58bf05451d | |
floflausch | f53309b04a | |
Cory LaViska | 49b42c3b90 | |
Cory LaViska | 762d0b0098 | |
Cory LaViska | e297633bd7 | |
Cory LaViska | f28ea9b834 | |
Cory LaViska | 0272e3dcff | |
floflausch | 8d42e9fd7e | |
Cory LaViska | e5da26fe6d | |
Cory LaViska | eb96e3db4b | |
Cory LaViska | b1b54a5a34 | |
Cory LaViska | a5404ecab0 | |
Cory LaViska | afe7778f89 | |
fountainpen | 88f3009cf4 | |
Cory LaViska | 8c9f8e69fc | |
Cory LaViska | 2e2d0349d6 | |
Cory LaViska | 6e9abc0226 | |
Cory LaViska | 4b03675116 | |
Cory LaViska | db66bbe5a1 | |
Cory LaViska | 54923edd22 | |
Cory LaViska | 86df7f6053 | |
Cory LaViska | 3882eb151d | |
John F Morton | 6ef246c575 | |
Cory LaViska | db333e1b81 | |
Cory LaViska | 5155f02dbf | |
Konnor Rogers | 2643e4ff9e | |
Cory LaViska | ff94ea2e0c | |
Cory LaViska | 39a0fafdc3 | |
Cory LaViska | 28cc38a90b | |
Christian Schilling | 28da45c2de | |
Christian Schilling | 7041357bf5 | |
Cory LaViska | d5ab0fef22 | |
Cory LaViska | c7b53cff47 | |
Cory LaViska | 85f91b7785 | |
Cory LaViska | 620fda6e79 | |
Cory LaViska | 7bf90b64ed | |
Konnor Rogers | ad9ca8fdb5 | |
Cory LaViska | 236fbd7109 | |
Cory LaViska | a697b356ac | |
jarviszheng | eb6966a6cf | |
Burton Smith | 79e939e929 | |
Konnor Rogers | 7500cabc58 | |
Cory LaViska | 8748394f54 | |
Cory LaViska | 87d82639f8 | |
Cam Skene | 566f0e41a4 | |
Cory LaViska | cf85d6af41 | |
Cory LaViska | 789ba7a13c | |
Cory LaViska | fac6e12b4e | |
Cory LaViska | d56fbb6197 | |
Cory LaViska | 1f2407d673 | |
Cory LaViska | ed7949261e | |
Cory LaViska | c9f810ac3e | |
Cory LaViska | c900c2a9ca | |
Cory LaViska | b7107ace1b | |
Cory LaViska | cbd4336773 | |
Cory LaViska | 9b969339a1 | |
Cory LaViska | 24f7b190f7 | |
mfocqueteau | a41e4e8928 | |
Yehuda Ringler | 25dd15b92c | |
Cory LaViska | 2ed5a4ff97 | |
Cory LaViska | 4d3297937a | |
Christian Schuller | 1d28e1bbc5 | |
Alan Chambers | 3b77c3b99f | |
Cory LaViska | c858bc3723 | |
Wes | 317d567fe8 | |
Cory LaViska | e6db8c953a | |
Cory LaViska | aa2cf24be5 | |
Cory LaViska | 42f881806b | |
Mario Hamann | 2b5e8286df | |
Cory LaViska | 32342f803c | |
Burton Smith | 7d6f770cd9 | |
Burton Smith | 8d86f374f9 | |
Burton Smith | 242e8e92ae | |
Konnor Rogers | 883cb161ec | |
Cory LaViska | a2fbe121c3 | |
Cory LaViska | ab770c566e | |
Konnor Rogers | 1867603225 | |
Cory LaViska | cf195da424 | |
Cory LaViska | 0cb6aa5d12 |
|
@ -1,4 +1,7 @@
|
|||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://github.com/shoelace-style/shoelace/discussions/categories/ideas
|
||||
about: All requests for new features should go here.
|
||||
- name: Help & Support
|
||||
url: https://github.com/shoelace-style/shoelace/discussions/categories/help
|
||||
about: Please don't create issues for personal help requests. Instead, ask your question on the discussion forum.
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project.
|
||||
title: ''
|
||||
labels: feature
|
||||
---
|
||||
|
||||
### What issue are you having?
|
||||
Provide a clear and concise description of the problem you're facing.
|
||||
|
||||
### Describe the solution you'd like
|
||||
How would you like to see the library solve it?
|
||||
|
||||
### Describe alternatives you've considered
|
||||
In what ways have you tried to solve this with the current version?
|
|
@ -25,6 +25,6 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npx playwright install-deps
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm ci
|
||||
- run: npm run verify
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
_site
|
||||
.cache
|
||||
.DS_Store
|
||||
cdn
|
||||
dist
|
||||
docs/assets/images/sprite.svg
|
||||
node_modules
|
||||
src/react
|
||||
cdn
|
||||
web-types.json
|
|
@ -2,6 +2,6 @@
|
|||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
|
|
12
README.md
12
README.md
|
@ -77,16 +77,6 @@ Shoelace is an open source project and contributions are encouraged! If you're i
|
|||
|
||||
## 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)
|
||||
Shoelace was created by [Cory LaViska](https://twitter.com/claviska) and is available under the terms of the MIT license.
|
||||
|
||||
Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
"monospace",
|
||||
"mousedown",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseup",
|
||||
"multiselectable",
|
||||
"nextjs",
|
||||
|
@ -109,6 +110,7 @@
|
|||
"novalidate",
|
||||
"npmdir",
|
||||
"Numberish",
|
||||
"onscrollend",
|
||||
"outdir",
|
||||
"ParamagicDev",
|
||||
"peta",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as path from 'path';
|
||||
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
|
||||
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
|
||||
import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
|
||||
import { parse } from 'comment-parser';
|
||||
import { pascalCase } from 'pascal-case';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
|
@ -38,6 +39,7 @@ export default {
|
|||
customElementsManifest.package = { name, description, version, author, homepage, license };
|
||||
}
|
||||
},
|
||||
|
||||
// Infer tag names because we no longer use @customElement decorators.
|
||||
{
|
||||
name: 'shoelace-infer-tag-names',
|
||||
|
@ -66,6 +68,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Parse custom jsDoc tags
|
||||
{
|
||||
name: 'shoelace-custom-tags',
|
||||
|
@ -137,6 +140,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'shoelace-react-event-names',
|
||||
analyzePhase({ ts, node, moduleDoc }) {
|
||||
|
@ -155,6 +159,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'shoelace-translate-module-paths',
|
||||
packageLinkPhase({ customElementsManifest }) {
|
||||
|
@ -191,6 +196,7 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Generate custom VS Code data
|
||||
customElementVsCodePlugin({
|
||||
outdir,
|
||||
|
@ -202,14 +208,23 @@ export default {
|
|||
}
|
||||
]
|
||||
}),
|
||||
|
||||
customElementJetBrainsPlugin({
|
||||
outdir: './dist',
|
||||
excludeCss: true,
|
||||
packageJson: false,
|
||||
referencesTemplate: (_, tag) => {
|
||||
return {
|
||||
name: 'Documentation',
|
||||
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
customElementVuejsPlugin({
|
||||
outdir: './dist/types/vue',
|
||||
fileName: 'index.d.ts',
|
||||
componentTypePath: (_, tag) => `../../components/${tag.replace('sl-', '')}/${tag.replace('sl-', '')}.component.js`
|
||||
})
|
||||
]
|
||||
};
|
||||
|
|
|
@ -160,7 +160,7 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if prop.type.text %}
|
||||
<code>{{ prop.type.text | markdownInline | safe }}</code>
|
||||
<code>{{ prop.type.text | trimPipes | markdownInline | safe }}</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
|
@ -211,7 +211,7 @@
|
|||
<td>{{ event.description | markdownInline | safe }}</td>
|
||||
<td>
|
||||
{% if event.type.text %}
|
||||
<code>{{ event.type.text }}</code>
|
||||
<code>{{ event.type.text | trimPipes }}</code>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
|
@ -245,7 +245,7 @@
|
|||
{% if method.parameters.length %}
|
||||
<code>
|
||||
{% for param in method.parameters %}
|
||||
{{ param.name }}: {{ param.type.text }}{% if not loop.last %},{% endif %}
|
||||
{{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</code>
|
||||
{% else %}
|
||||
|
|
|
@ -95,6 +95,23 @@
|
|||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="ks-banner{% if toc %} with-toc{% endif %}"
|
||||
href="https://www.kickstarter.com/projects/fontawesome/web-awesome?ref=71ihfk"
|
||||
target="_blank"
|
||||
>
|
||||
<span>
|
||||
<svg viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#f36944" d="M11.63 1.625C11.63 2.27911 11.2435 2.84296 10.6865 3.10064L14 6L17.2622 5.34755C17.0968 5.10642 17 4.81452 17 4.5C17 3.67157 17.6716 3 18.5 3C19.3284 3 20 3.67157 20 4.5C20 5.31157 19.3555 5.9726 18.5504 5.99917L15.0307 13.8207C14.7077 14.5384 13.9939 15 13.2068 15H6.79317C6.00615 15 5.29229 14.5384 4.96933 13.8207L1.44963 5.99917C0.64452 5.9726 0 5.31157 0 4.5C0 3.67157 0.671573 3 1.5 3C2.32843 3 3 3.67157 3 4.5C3 4.81452 2.9032 5.10642 2.73777 5.34755L6 6L9.31702 3.09761C8.76346 2.83855 8.38 2.27656 8.38 1.625C8.38 0.727537 9.10754 0 10.005 0C10.9025 0 11.63 0.727537 11.63 1.625Z"/>
|
||||
</svg>
|
||||
<span>
|
||||
<strong style="white-space: nowrap;">Get ready for more awesome!</strong>
|
||||
Web Awesome, the next iteration of Shoelace, is on Kickstarter.
|
||||
</span>
|
||||
</span>
|
||||
<span class="faux-button">Read Our Story</span>
|
||||
</a>
|
||||
|
||||
<aside id="sidebar" data-preserve-scroll>
|
||||
<header>
|
||||
<a href="/">
|
||||
|
|
|
@ -9,11 +9,14 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* @param {Document} content
|
||||
* @param {String} rawContent
|
||||
* @param {Replacements} replacements
|
||||
*/
|
||||
module.exports = function (content, replacements) {
|
||||
module.exports = function (rawContent, replacements) {
|
||||
let content = rawContent;
|
||||
replacements.forEach(replacement => {
|
||||
content.body.innerHTML = content.body.innerHTML.replaceAll(replacement.pattern, replacement.replacement);
|
||||
content = content.replaceAll(replacement.pattern, replacement.replacement);
|
||||
});
|
||||
|
||||
return content;
|
||||
};
|
||||
|
|
|
@ -373,4 +373,12 @@
|
|||
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());
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -235,7 +235,9 @@ code {
|
|||
kbd {
|
||||
background: var(--sl-color-neutral-100);
|
||||
border: solid 1px var(--sl-color-neutral-200);
|
||||
box-shadow: inset 0 1px 0 0 var(--sl-color-neutral-0), inset 0 -1px 0 0 var(--sl-color-neutral-200);
|
||||
box-shadow:
|
||||
inset 0 1px 0 0 var(--sl-color-neutral-0),
|
||||
inset 0 -1px 0 0 var(--sl-color-neutral-200);
|
||||
font-family: var(--sl-font-mono);
|
||||
font-size: 0.9125em;
|
||||
border-radius: var(--docs-border-radius);
|
||||
|
@ -511,7 +513,9 @@ pre .token.italic {
|
|||
right: 0;
|
||||
white-space: normal;
|
||||
color: var(--sl-color-neutral-800);
|
||||
transition: 150ms opacity, 150ms scale;
|
||||
transition:
|
||||
150ms opacity,
|
||||
150ms scale;
|
||||
}
|
||||
|
||||
.copy-code-button::part(button) {
|
||||
|
@ -659,6 +663,7 @@ pre:hover .copy-code-button,
|
|||
margin: -1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.content details summary span {
|
||||
|
@ -982,7 +987,9 @@ main {
|
|||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: 250ms scale ease, 250ms rotate ease;
|
||||
transition:
|
||||
250ms scale ease,
|
||||
250ms rotate ease;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
|
@ -1052,7 +1059,6 @@ html.sidebar-open #menu-toggle {
|
|||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: 250ms scale ease;
|
||||
}
|
||||
|
||||
#theme-selector:not(:defined) {
|
||||
|
@ -1095,12 +1101,6 @@ html.sidebar-open #menu-toggle {
|
|||
color: var(--sl-color-neutral-1000);
|
||||
}
|
||||
|
||||
#icon-toolbar button:hover,
|
||||
#icon-toolbar a:hover,
|
||||
#theme-selector sl-button:hover {
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
#icon-toolbar a:not(:last-child),
|
||||
#icon-toolbar button:not(:last-child) {
|
||||
margin-right: 0.25rem;
|
||||
|
@ -1413,3 +1413,95 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
|
|||
grid-column-start: span 6;
|
||||
}
|
||||
}
|
||||
|
||||
.ks-banner {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
width: 950px;
|
||||
left: calc(50% - 475px);
|
||||
font-size: 0.9375rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #1a3256;
|
||||
border-radius: var(--sl-border-radius-large);
|
||||
padding: 1rem 1.25rem;
|
||||
color: #fdfdfd;
|
||||
text-decoration: none;
|
||||
line-height: 1.4;
|
||||
z-index: 2;
|
||||
margin-left: 160px;
|
||||
}
|
||||
|
||||
.ks-banner:hover {
|
||||
color: #fdfdfd;
|
||||
}
|
||||
|
||||
.ks-banner > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ks-banner svg {
|
||||
flex: 0 0 1.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.ks-banner .faux-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
background: white;
|
||||
border: solid 1px #d4d4d4;
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
font-size: 0.8375rem;
|
||||
color: #353439;
|
||||
padding: 0.5rem 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ks-banner.with-toc {
|
||||
width: 1100px;
|
||||
left: calc(50% - 550px);
|
||||
margin-left: 140px;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 70px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1650px) {
|
||||
.ks-banner,
|
||||
.ks-banner.with-toc {
|
||||
width: 540px !important;
|
||||
top: 50px;
|
||||
left: calc(50% - 270px);
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.ks-banner,
|
||||
.ks-banner.with-toc {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
.ks-banner,
|
||||
.ks-banner.with-toc {
|
||||
width: calc(100% - 2rem) !important;
|
||||
left: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 150px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,12 @@ module.exports = function (eleventyConfig) {
|
|||
return shoelaceFlavoredMarkdown.renderInline(content);
|
||||
});
|
||||
|
||||
// Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited.
|
||||
// With Prettier 3, this means a leading pipe will exist if the line wraps.
|
||||
eleventyConfig.addFilter('trimPipes', content => {
|
||||
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
|
||||
});
|
||||
|
||||
eleventyConfig.addFilter('classNameToComponentName', className => {
|
||||
let name = capitalCase(className.replace(/^Sl/, ''));
|
||||
if (name === 'Qr Code') name = 'QR Code'; // manual override
|
||||
|
@ -109,7 +115,13 @@ module.exports = function (eleventyConfig) {
|
|||
//
|
||||
// Transforms
|
||||
//
|
||||
eleventyConfig.addTransform('html-transform', function (content) {
|
||||
eleventyConfig.addTransform('html-transform', function (rawContent) {
|
||||
let content = replacer(rawContent, [
|
||||
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
|
||||
{ pattern: '%CDNDIR%', replacement: cdndir },
|
||||
{ pattern: '%NPMDIR%', replacement: npmdir }
|
||||
]);
|
||||
|
||||
// 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
|
||||
|
@ -134,11 +146,6 @@ module.exports = function (eleventyConfig) {
|
|||
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}`;
|
||||
|
|
|
@ -54,7 +54,7 @@ Set the `variant` attribute to change the alert's variant.
|
|||
<sl-alert variant="neutral" open>
|
||||
<sl-icon slot="icon" name="gear"></sl-icon>
|
||||
<strong>Your settings have been updated</strong><br />
|
||||
Settings will take affect on next login.
|
||||
Settings will take effect on next login.
|
||||
</sl-alert>
|
||||
|
||||
<br />
|
||||
|
@ -102,7 +102,7 @@ const App = () => (
|
|||
<SlIcon slot="icon" name="gear" />
|
||||
<strong>Your settings have been updated</strong>
|
||||
<br />
|
||||
Settings will take affect on next login.
|
||||
Settings will take effect on next login.
|
||||
</SlAlert>
|
||||
|
||||
<br />
|
||||
|
@ -276,7 +276,7 @@ You should always use the `closable` attribute so users can dismiss the notifica
|
|||
<sl-alert variant="neutral" duration="3000" closable>
|
||||
<sl-icon slot="icon" name="gear"></sl-icon>
|
||||
<strong>Your settings have been updated</strong><br />
|
||||
Settings will take affect on next login.
|
||||
Settings will take effect on next login.
|
||||
</sl-alert>
|
||||
|
||||
<sl-alert variant="warning" duration="3000" closable>
|
||||
|
@ -361,7 +361,7 @@ const App = () => {
|
|||
<SlIcon slot="icon" name="gear" />
|
||||
<strong>Your settings have been updated</strong>
|
||||
<br />
|
||||
Settings will take affect on next login.
|
||||
Settings will take effect on next login.
|
||||
</SlAlert>
|
||||
|
||||
<SlAlert ref={warning} variant="warning" duration="3000" closable>
|
||||
|
|
|
@ -236,7 +236,7 @@ When a `target` is set, the link will receive `rel="noreferrer noopener"` for [s
|
|||
|
||||
### 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.
|
||||
As expected, buttons can be given a custom width by passing inline styles to the component (or using a class). 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>
|
||||
|
@ -417,7 +417,7 @@ const App = () => (
|
|||
|
||||
### 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.
|
||||
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around.
|
||||
|
||||
```html:preview
|
||||
<sl-button variant="default" loading>Default</sl-button>
|
||||
|
|
|
@ -803,7 +803,7 @@ const App = () => (
|
|||
|
||||
### Aspect Ratio
|
||||
|
||||
Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport.
|
||||
Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport from the default value of 16/9.
|
||||
|
||||
```html:preview
|
||||
<sl-carousel class="aspect-ratio" navigation pagination style="--aspect-ratio: 3/2;">
|
||||
|
@ -1246,7 +1246,7 @@ const App = () => {
|
|||
<img
|
||||
alt={`Thumbnail by ${i + 1}`}
|
||||
className={`thumbnails__image ${i === currentSlide ? 'active' : ''}`}
|
||||
onCLick={() => handleThumbnailClick(i)}
|
||||
onClick={() => handleThumbnailClick(i)}
|
||||
src={src}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -89,6 +89,20 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html:preview
|
||||
<sl-checkbox help-text="What should the user know about the checkbox?">Label</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
|
||||
|
||||
const App = () => <SlCheckbox help-text="What should the user know about the switch?">Label</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.
|
||||
|
|
|
@ -123,7 +123,9 @@ Details are designed to function independently, but you can simulate a group or
|
|||
|
||||
// Close all other details when one is shown
|
||||
container.addEventListener('sl-show', event => {
|
||||
[...container.querySelectorAll('sl-details')].map(details => (details.open = event.target === details));
|
||||
if (event.target.localName === 'sl-details') {
|
||||
[...container.querySelectorAll('sl-details')].map(details => (details.open = event.target === details));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -60,35 +60,6 @@ const App = () => (
|
|||
|
||||
## 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>
|
||||
```
|
||||
|
||||
{% raw %}
|
||||
|
||||
```jsx:react
|
||||
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>Option 1</SlMenuItem>
|
||||
<SlMenuItem disabled>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
@ -151,6 +122,64 @@ const App = () => (
|
|||
|
||||
{% endraw %}
|
||||
|
||||
### 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>
|
||||
```
|
||||
|
||||
{% raw %}
|
||||
|
||||
```jsx:react
|
||||
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>Option 1</SlMenuItem>
|
||||
<SlMenuItem disabled>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
### Loading
|
||||
|
||||
Use the `loading` attribute to indicate that a menu item is busy. Like a disabled menu item, clicks will be suppressed until the loading state is removed.
|
||||
|
||||
```html:preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item>Option 1</sl-menu-item>
|
||||
<sl-menu-item loading>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
{% raw %}
|
||||
|
||||
```jsx:react
|
||||
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>Option 1</SlMenuItem>
|
||||
<SlMenuItem loading>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
{% endraw %}
|
||||
|
||||
### 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.
|
||||
|
|
|
@ -1530,6 +1530,140 @@ const App = () => {
|
|||
};
|
||||
```
|
||||
|
||||
### Hover Bridge
|
||||
|
||||
When a gap exists between the anchor and the popup element, this option will add a "hover bridge" that fills the gap using an invisible element. This makes listening for events such as `mouseover` and `mouseout` more sane because the pointer never technically leaves the element. The hover bridge will only be drawn when the popover is active. For demonstration purposes, the bridge in this example is shown in orange.
|
||||
|
||||
```html:preview
|
||||
<div class="popup-hover-bridge">
|
||||
<sl-popup placement="top" hover-bridge distance="10" skidding="0" active>
|
||||
<span slot="anchor"></span>
|
||||
<div class="box"></div>
|
||||
</sl-popup>
|
||||
|
||||
<br>
|
||||
<sl-switch checked>Hover Bridge</sl-switch><br>
|
||||
<sl-range min="0" max="50" step="1" value="10" label="Distance"></sl-range>
|
||||
<sl-range min="-50" max="50" step="1" value="0" label="Skidding"></sl-range>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.popup-hover-bridge span[slot='anchor'] {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: dashed 2px var(--sl-color-neutral-600);
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.popup-hover-bridge .box {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background: var(--sl-color-primary-600);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
}
|
||||
|
||||
.popup-hover-bridge sl-range {
|
||||
max-width: 260px;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.popup-hover-bridge sl-popup::part(hover-bridge) {
|
||||
background: tomato;
|
||||
opacity: .5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.popup-hover-bridge');
|
||||
const popup = container.querySelector('sl-popup');
|
||||
const hoverBridge = container.querySelector('sl-switch');
|
||||
const distance = container.querySelector('sl-range[label="Distance"]');
|
||||
const skidding = container.querySelector('sl-range[label="Skidding"]');
|
||||
|
||||
distance.addEventListener('sl-input', () => (popup.distance = distance.value));
|
||||
skidding.addEventListener('sl-input', () => (popup.skidding = skidding.value));
|
||||
hoverBridge.addEventListener('sl-change', () => (popup.hoverBridge = hoverBridge.checked));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { useState } from 'react';
|
||||
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
|
||||
import SlRange from '@shoelace-style/shoelace/dist/react/range';
|
||||
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
|
||||
|
||||
const css = `
|
||||
.popup-hover-bridge span[slot='anchor'] {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: dashed 2px var(--sl-color-neutral-600);
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.popup-hover-bridge .box {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
background: var(--sl-color-primary-600);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
}
|
||||
|
||||
.popup-hover-bridge sl-range {
|
||||
max-width: 260px;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.popup-hover-bridge sl-popup::part(hover-bridge) {
|
||||
background: tomato;
|
||||
opacity: .5;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
const [hoverBridge, setHoverBridge] = useState(true);
|
||||
const [distance, setDistance] = useState(10);
|
||||
const [skidding, setSkidding] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="popup-hover-bridge">
|
||||
<SlPopup placement="top" hover-bridge={hoverBridge} distance={distance} skidding={skidding} active>
|
||||
<span slot="anchor" />
|
||||
<div class="box" />
|
||||
</SlPopup>
|
||||
|
||||
<br />
|
||||
<SlSwitch
|
||||
checked={hoverBridge}
|
||||
onSlChange={event => setHoverBridge(event.target.checked)}
|
||||
>
|
||||
Hover Bridge
|
||||
</SlSwitch><br />
|
||||
<SlRange
|
||||
min="0"
|
||||
max="50"
|
||||
step="1"
|
||||
value={distance}
|
||||
label="Distance"
|
||||
onSlInput={event => setDistance(event.target.value)}
|
||||
/>
|
||||
<SlRange
|
||||
min="-50"
|
||||
max="50"
|
||||
step="1"
|
||||
value={skidding}
|
||||
label="Skidding"
|
||||
onSlInput={event => setSkidding(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Virtual Elements
|
||||
|
||||
In most cases, popups are anchored to an actual element. Sometimes, it can be useful to anchor them to a non-element. To do this, you can pass a `VirtualElement` to the anchor property. A virtual element must contain a function called `getBoundingClientRect()` that returns a [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) object as shown below.
|
||||
|
@ -1705,3 +1839,15 @@ const App = () => {
|
|||
);
|
||||
};
|
||||
```
|
||||
|
||||
Sometimes the `getBoundingClientRects` might be derived from a real element. In this case provide the anchor element as context to ensure clipping and position updates for the popup work well.
|
||||
|
||||
```ts
|
||||
const virtualElement = {
|
||||
getBoundingClientRect() {
|
||||
// ...
|
||||
return { width, height, x, y, top, left, right, bottom };
|
||||
},
|
||||
contextElement: anchorElement
|
||||
};
|
||||
```
|
||||
|
|
|
@ -87,26 +87,26 @@ const App = () => (
|
|||
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 size="small" 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>
|
||||
|
||||
<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 size="medium" 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>
|
||||
|
||||
<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 size="large" 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>
|
||||
```
|
||||
|
||||
|
@ -115,26 +115,26 @@ import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
|
|||
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
|
||||
|
||||
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 size="small" 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>
|
||||
|
||||
<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 size="medium" 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>
|
||||
|
||||
<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 size="large" 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>
|
||||
);
|
||||
```
|
||||
|
@ -144,26 +144,26 @@ const App = () => (
|
|||
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 size="small" label="Select an option" name="a" value="1">
|
||||
<sl-radio-button pill value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button pill value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill 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 size="medium" label="Select an option" name="a" value="1">
|
||||
<sl-radio-button pill value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button pill value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill 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 size="large" label="Select an option" name="a" value="1">
|
||||
<sl-radio-button pill value="1">Option 1</sl-radio-button>
|
||||
<sl-radio-button pill value="2">Option 2</sl-radio-button>
|
||||
<sl-radio-button pill value="3">Option 3</sl-radio-button>
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
|
@ -172,26 +172,26 @@ import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
|
|||
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
|
||||
|
||||
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 size="small" label="Select an option" name="a" value="1">
|
||||
<SlRadioButton pill value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton pill value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill 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 size="medium" label="Select an option" name="a" value="1">
|
||||
<SlRadioButton pill value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton pill value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill 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 size="large" label="Select an option" name="a" value="1">
|
||||
<SlRadioButton pill value="1">Option 1</SlRadioButton>
|
||||
<SlRadioButton pill value="2">Option 2</SlRadioButton>
|
||||
<SlRadioButton pill value="3">Option 3</SlRadioButton>
|
||||
</SlRadioGroup>
|
||||
);
|
||||
```
|
||||
|
|
|
@ -233,7 +233,7 @@ import SlOption from '@shoelace-style/shoelace/dist/react/option';
|
|||
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
|
||||
<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>
|
||||
|
@ -269,7 +269,7 @@ import SlOption from '@shoelace-style/shoelace/dist/react/option';
|
|||
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect value="option-1 option-2" multiple clearable>
|
||||
<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>
|
||||
|
|
|
@ -75,6 +75,20 @@ const App = () => (
|
|||
);
|
||||
```
|
||||
|
||||
### Help Text
|
||||
|
||||
Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
|
||||
|
||||
```html:preview
|
||||
<sl-switch help-text="What should the user know about the switch?">Label</sl-switch>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import SlSwitch from '@shoelace-style/shoelace/dist/react/checkbox';
|
||||
|
||||
const App = () => <SlSwitch help-text="What should the user know about the switch?">Label</SlSwitch>;
|
||||
```
|
||||
|
||||
### Custom Styles
|
||||
|
||||
Use the available custom properties to change how the switch is styled.
|
||||
|
|
|
@ -5,26 +5,6 @@ meta:
|
|||
layout: component
|
||||
---
|
||||
|
||||
```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/tab';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlTab>Tab</SlTab>
|
||||
<SlTab active>Active</SlTab>
|
||||
<SlTab closable>Closable</SlTab>
|
||||
<SlTab disabled>Disabled</SlTab>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
:::tip
|
||||
Additional demonstrations can be found in the [tab group examples](/components/tab-group).
|
||||
:::
|
||||
|
|
|
@ -249,7 +249,7 @@ const App = () => (
|
|||
|
||||
### 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.
|
||||
Tooltips can be controlled 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>
|
||||
|
|
|
@ -10,16 +10,41 @@ Angular [plays nice](https://custom-elements-everywhere.com/#angular) with custo
|
|||
|
||||
## Installation
|
||||
|
||||
### Download the npm package
|
||||
|
||||
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.
|
||||
### Update the Angular Configuration
|
||||
|
||||
Next, [include a theme](/getting-started/themes). In this example, we'll import the light theme.
|
||||
|
||||
Its also important to load the components by using a `<script>` tag into the index.html file. However, the Angular way to do it is by adding a script configurations into your angular.json file as follows:
|
||||
|
||||
```json
|
||||
"architect": {
|
||||
"build": {
|
||||
...
|
||||
"options": {
|
||||
...
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"@shoelace-style/shoelace/dist/themes/light.css"
|
||||
],
|
||||
"scripts": [
|
||||
"@shoelace-style/shoelace/dist/shoelace.js"
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
### Setting up the base path
|
||||
|
||||
Next, set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets in the `main.ts`. In this example, we'll use the CDN as a base path.
|
||||
|
||||
```jsx
|
||||
import '@shoelace-style/shoelace/%NPMDIR%/themes/light.css';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path';
|
||||
|
||||
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/%CDNDIR%/');
|
||||
|
|
|
@ -32,6 +32,10 @@ If you'd rather not use the CDN for assets, you can create a [build task](https:
|
|||
|
||||
Now you can start using components!
|
||||
|
||||
### Preact
|
||||
|
||||
Preact users facing type errors using components may benefit from setting "paths" in their tsconfig.json so that react types will instead resolve to preact/compat as described in [Preact's typescript documentation](https://preactjs.com/guide/v10/typescript/#typescript-preactcompat-configuration).
|
||||
|
||||
## Usage
|
||||
|
||||
### Importing Components
|
||||
|
|
|
@ -23,6 +23,7 @@ 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
|
||||
// main.js or main.ts
|
||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
|
||||
|
||||
|
@ -35,35 +36,22 @@ If you'd rather not use the CDN for assets, you can create a build task that cop
|
|||
|
||||
## 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))
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
If you haven't configured your Vue.js project to work with custom elements/web components, follow [the instructions here](https://vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue) based on your project type to ensure your project will not throw an error when it encounters a custom element.
|
||||
|
||||
Now you can start using Shoelace components in your app!
|
||||
|
||||
## Types
|
||||
|
||||
Once you have configured your application for custom elements, you should be able to use Shoelace in your application without it causing any errors. Unfortunately, this doesn't register the custom elements to behave like components built using Vue. To provide autocomplete information and type safety for your components, you can import the Shoelace Vue types into your `tsconfig.json` to get better integration in your standard Vue and JSX templates.
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["@shoelace-style/shoelace/dist/types/vue"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### QR code generator example
|
||||
|
@ -107,13 +95,26 @@ When binding complex data such as objects and arrays, use the `.prop` modifier t
|
|||
<sl-color-picker :swatches.prop="mySwatches" />
|
||||
```
|
||||
|
||||
### Two-way Binding
|
||||
|
||||
One caveat is there's currently [no support for v-model on custom elements](https://github.com/vuejs/vue/issues/7830), but you can still achieve two-way binding manually.
|
||||
|
||||
```html
|
||||
<!-- This doesn't work -->
|
||||
<sl-input v-model="name"></sl-input>
|
||||
<!-- This works, but it's a bit longer -->
|
||||
<sl-input :value="name" @input="name = $event.target.value"></sl-input>
|
||||
```
|
||||
|
||||
If that's too verbose for your liking, you can use a custom directive instead. [This utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) adds a custom directive that will work just like `v-model` but for Shoelace components.
|
||||
|
||||
:::tip
|
||||
Are you using Shoelace with Vue? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/vue.md)
|
||||
:::
|
||||
|
||||
### Slots
|
||||
|
||||
To use Shoelace components with slots, follow the Vue documentation on using [slots with custom elements](https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue).
|
||||
Slots in Shoelace/web components are functionally the same as basic slots in Vue. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
|
||||
|
||||
Here is an example:
|
||||
|
||||
|
|
|
@ -208,13 +208,34 @@ Shoelace ships with a file called `vscode.html-custom-data.json` that can be use
|
|||
}
|
||||
```
|
||||
|
||||
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take affect.
|
||||
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take effect.
|
||||
|
||||
## JetBrains IDEs
|
||||
### JetBrains IDEs
|
||||
|
||||
If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Shoelace from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor.
|
||||
|
||||
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/cdn/web-types.json) and add it to the root of your project.
|
||||
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/dist/web-types.json) and add it to the root of your project. Be sure to add a reference to the `web-types.json` file in your `package.json` in order for your editor to properly detect it.
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"web-types": "./web-types.json"
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
If you are using types from multiple projects, you can add an array of references.
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"web-types": [
|
||||
...,
|
||||
"./web-types.json"
|
||||
]
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Other Editors
|
||||
|
||||
|
|
|
@ -86,12 +86,10 @@ With Shoelace, you can:
|
|||
- Incrementally adopt components as needed (no need to ditch your framework)
|
||||
- Upgrade or switch frameworks without rebuilding foundational components
|
||||
|
||||
If your organization is looking to build a design system, [Shoelace will save you thousands of dollars](https://medium.com/eightshapes-llc/and-you-thought-buttons-were-easy-26eb5b5c1871).\* All the foundational components you need are right here, ready to be customized for your brand. And since it's built on web standards, browsers will continue to support it for many years to come.
|
||||
If your organization is looking to build a design system, [Shoelace will save you thousands of dollars](https://medium.com/eightshapes-llc/and-you-thought-buttons-were-easy-26eb5b5c1871). All the foundational components you need are right here, ready to be customized for your brand. And since it's built on web standards, browsers will continue to support it for many years to come.
|
||||
|
||||
Whether you use Shoelace as a starting point for your organization's design system or for a fun personal project, there's no limit to what you can do with it.
|
||||
|
||||
<small>\*Please consider giving back some of what you save by [supporting this project with a sponsorship](https://github.com/sponsors/claviska).</small>
|
||||
|
||||
## Browser Support
|
||||
|
||||
Shoelace is tested in the latest two versions of the following browsers.
|
||||
|
|
|
@ -12,6 +12,133 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
|
|||
|
||||
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
|
||||
|
||||
## Next
|
||||
|
||||
- `<sl-tab>` `closable` property now reflects. [#2041]
|
||||
- `<sl-tab-group>` now implements a proper "roving tabindex" and `<sl-tab>` is no longer tabbable by default. This aligns closer to the APG pattern for tabs. [#2041]
|
||||
- Fixed a bug in the submenu controller that prevented submenus from rendering in RTL without explicitly setting `dir` on the parent menu item [#1992]
|
||||
|
||||
## 2.15.1
|
||||
|
||||
- Fixed a bug in `<sl-radio-group>` where if a click did not contain a `<sl-radio>` it would show a console error. [#2009]
|
||||
- Fixed a bug in `<sl-split-panel>` that caused it not to recalculate it's position when going from being `display: none;` to its original display value. [#1942]
|
||||
- Fixed a bug in `<dialog>` where when it showed it would cause a layout shift. [#1967]
|
||||
- Fixed a bug in `<sl-tooltip>` that allowed unwanted text properties to leak in [#1947]
|
||||
- Fixed a bug in `<sl-button-group>` classes [#1974]
|
||||
- Fixed a bug in `<sl-textarea>` that may throw errors on `disconnectedCallback` in test environments [#1985]
|
||||
- Fixed a bug in `<sl-color-picker>` that would log a non-passive event listener warning [#2005]
|
||||
- Fixed a bug in the submenu controller that allowed submenus to go offscreen and not be scrollable [#2001]
|
||||
- Fixed a bug in `<sl-range>` that caused the tooltip position to be incorrect in some cases [#1979]
|
||||
|
||||
## 2.15.0
|
||||
|
||||
- Added the Slovenian translation [#1893]
|
||||
- Added support for `contextElement` to `VirtualElements` in `<sl-popup>` [#1874]
|
||||
- Added the `spinner` and `spinner__base` parts to `<sl-tree-item>` [#1937]
|
||||
- Added the `sync` property to `<sl-dropdown>` so the menu can easily sync sizes with the trigger element [#1935]
|
||||
- Fixed a bug in `<sl-icon>` that did not properly apply mutators to spritesheets [#1927]
|
||||
- Fixed a bug in `.sl-scroll-lock` causing layout shifts [#1895]
|
||||
- Fixed a bug in `<sl-rating>` that caused the rating to not reset in some circumstances [#1877]
|
||||
- Fixed a bug in `<sl-select>` that caused the menu to not close when rendered in a shadow root [#1878]
|
||||
- Fixed a bug in `<sl-tree>` that caused a new stacking context resulting in tooltips being clipped [#1709]
|
||||
- Fixed a bug in `<sl-tab-group>` that caused the scroll controls to toggle indefinitely when zoomed in Safari [#1839]
|
||||
- Fixed a bug in the submenu controller that allowed two submenus to be open at the same time [#1880]
|
||||
- Fixed a bug in `<sl-select>` where the tag size wouldn't update with the control's size [#1886]
|
||||
- Fixed a bug in `<sl-checkbox>` and `<sl-switch>` where the color of the required content wasn't applying correctly
|
||||
- Fixed a bug in `<sl-checkbox>` where help text was incorrectly styled [#1897]
|
||||
- Fixed a bug in `<sl-input>` that prevented the control from receiving focus when clicking over the clear button
|
||||
- Fixed a bug in `<sl-carousel>` that caused the carousel to be out of sync when used with reduced motion settings [#1887]
|
||||
- Fixed a bug in `<sl-button-group>` that caused styles to stop working when using `className` on buttons in React [#1926]
|
||||
|
||||
## 2.14.0
|
||||
|
||||
- Added the Arabic translation [#1852]
|
||||
- Added help text to `<sl-checkbox>` [#1860]
|
||||
- Added help text to `<sl-switch>` [#1800]
|
||||
- Fixed a bug in `<sl-option>` that caused HTML tags to be included in `getTextLabel()`
|
||||
- Fixed a bug in `<sl-carousel>` that caused slides to not switch correctly [#1862]
|
||||
- Refactored component styles to be consumed more efficiently [#1692]
|
||||
|
||||
## 2.13.1
|
||||
|
||||
- Fixed a bug where the safe triangle was always visible when selecting nested `<sl-menu>` elements [#1835]
|
||||
|
||||
## 2.13.0
|
||||
|
||||
- Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734]
|
||||
- Added the `loading` attribute and the `spinner` and `spinner__base` part to `<sl-menu-item>` [#1700]
|
||||
- Fixed files that did not have `.js` extensions. [#1770]
|
||||
- Fixed a bug in `<sl-tree>` when providing custom expand / collapse icons [#1922]
|
||||
- Fixed `<sl-dialog>` not accounting for elements with hidden dialog controls like `<video>` [#1755]
|
||||
- Fixed focus trapping not scrolling elements into view. [#1750]
|
||||
- Fixed more performance issues with focus trapping performance. [#1750]
|
||||
- Fixed a bug in `<sl-input>` and `<sl-textarea>` that made it work differently from `<input>` and `<textarea>` when using defaults [#1746]
|
||||
- Fixed a bug in `<sl-select>` that prevented it from closing when tabbing to another select inside a shadow root [#1763]
|
||||
- Fixed a bug in `<sl-spinner>` that caused the animation to appear strange in certain circumstances [#1787]
|
||||
- Fixed a bug in `<sl-dialog>` with focus trapping [#1813]
|
||||
- Fixed a bug that caused form controls to submit even after they were removed from the DOM [#1823]
|
||||
- Fixed a bug that caused empty `<sl-radio-group>` elements to log an error in the console [#1795]
|
||||
- Fixed a bug that caused modal scroll locking to conflict with the `scrollbar-gutter` property [#1805]
|
||||
- Fixed a bug in `<sl-option>` that caused slotted content to show up when calling `getTextLabel()` [#1730]
|
||||
- Fixed a bug in `<sl-color-picker>` that caused picker values to not match the preview color [#1831]
|
||||
- Fixed a bug in `<sl-carousel>` where pagination dots don't update when swiping slide in iOS Safari [#1748]
|
||||
- Fixed a bug in`<sl-carousel>` where trying to swipe doesn't change the slide in Firefox for Android [#1748]
|
||||
- Improved the accessibility of `<sl-tooltip>` so they persist when hovering over the tooltip and dismiss when pressing [[Esc]] [#1734]
|
||||
- Improved "close" behavior of multiple components in supportive browsers using the `CloseWatcher` API [#1788]
|
||||
- Removed the scroll controller from the experimental `<sl-carousel>` and moved all mouse related logic into the component [#1748]
|
||||
|
||||
## 2.12.0
|
||||
|
||||
- Added the Italian translation [#1727]
|
||||
- Added the ability to call `form.checkValidity()` and it will use Shoelace's custom `checkValidity()` handler. [#1708]
|
||||
- Fixed a bug where nested dialogs were not properly trapping focus. [#1711]
|
||||
- Fixed a bug with form controls removing the custom validity handlers from the form. [#1708]
|
||||
- Fixed a bug in form control components that used a `form` property, but not an attribute. [#1707]
|
||||
- Fixed a bug with bundled components using CDN builds not having translations on initial connect [#1696]
|
||||
- Fixed a bug where the `"sl-change"` event would always fire simultaneously with `"sl-input"` event in `<sl-color-picker>`. The `<sl-change>` event now only fires when a user stops dragging a slider or stops dragging on the color canvas. [#1689]
|
||||
- Updated the copy icon in the system library [#1702]
|
||||
|
||||
## 2.11.2
|
||||
|
||||
- Fixed a bug in `<sl-carousel>` component that caused an error to be thrown when rendered with Lit [#1684]
|
||||
|
||||
## 2.11.1
|
||||
|
||||
- Improved the experimental `<sl-carousel>` component [#1605]
|
||||
|
||||
## 2.11.0
|
||||
|
||||
- Added the Croatian translation [#1656]
|
||||
- Fixed a bug that caused the [[Escape]] key to stop propagating when tooltips are disabled [#1607]
|
||||
- Fixed a bug that made it impossible to style placeholders in `<sl-select>` [#1667]
|
||||
- Fixed a bug that caused `dist/react/index.js` to be blank [#1659]
|
||||
|
||||
## 2.10.0
|
||||
|
||||
- Added the Simplified Chinese translation [#1604]
|
||||
- Fixed a bug [in the localize dependency](https://github.com/shoelace-style/localize/issues/20) that caused underscores in language codes to throw a `RangeError`
|
||||
- Fixed a bug in the focus trapping utility used by modals that caused unexpected focus behavior. [#1583]
|
||||
- Fixed a bug in `<sl-copy-button>` that prevented exported tooltip parts from being styled [#1586]
|
||||
- Fixed a bug in `<sl-menu>` that caused it not to fire the `sl-select` event if you clicked an element inside of a `<sl-menu-item>` [#1599]
|
||||
- Fixed a bug that caused focus trap logic to hang the browser in certain circumstances [#1612]
|
||||
- Improved submenu selection by implementing the [safe triangle](https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/) method [#1550]
|
||||
- Updated `@shoelace-style/localize` to 3.1.0
|
||||
- Updated `@lib-labs/react` to stable `@lit/react`
|
||||
- Updated Bootstrap Icons to 1.11.1
|
||||
- Updated Lit to 3.0.0
|
||||
- Updated TypeScript to 5.2.2
|
||||
- Updated all other dependencies to latest versions
|
||||
|
||||
## 2.9.0
|
||||
|
||||
- Added the `modal` property to `<sl-dialog>` and `<sl-drawer>` to support third-party modals [#1571]
|
||||
- Fixed a bug in the autoloader causing it to register non-Shoelace elements [#1563]
|
||||
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
|
||||
- Fixed a bug in `<sl-icon>` that caused icons to not load when the default library used a sprite sheet [#1572]
|
||||
- Removed error when a missing popup anchor is provided [#1548]
|
||||
- Updated `@ctrl/tinycolor` to 4.0.1 [#1542]
|
||||
- Updated Bootstrap Icons to 1.11.0
|
||||
|
||||
## 2.8.0
|
||||
|
||||
- Added `--isolatedModules` and `--verbatimModuleSyntax` to `tsconfig.json`. For anyone directly importing event types, they no longer provide a default export due to these options being enabled. For people using the `events/event.js` file directly, there is no change.
|
||||
|
@ -23,7 +150,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
|
||||
- Improved `<sl-menu-item>` so labels truncate properly instead of getting chopped and overflowing
|
||||
- Removed the extra `React.Component` around `@lit-labs/react` wrapper. [#1531]
|
||||
- Upgrade `@lit-labs/react` to v2.0.1. [#1531]
|
||||
- Updated `@lit-labs/react` to v2.0.1. [#1531]
|
||||
|
||||
## 2.7.0
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ I realize that one cannot reasonably enforce this any more than one can enforce
|
|||
The [issue tracker](https://github.com/shoelace-style/shoelace/issues) is for bug reports, feature requests, and pull requests.
|
||||
|
||||
- Please **do not** use the issue tracker for personal support requests. Use [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/help) instead.
|
||||
- Please **do not** use the issue tracker for feature requests. Use [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/ideas) instead.
|
||||
- Please **do not** derail, hijack, or troll issues. Keep the discussion on topic and be respectful of others.
|
||||
- Please **do not** post comments with "+1" or "👍". Use [reactions](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) instead.
|
||||
- Please **do** use the issue tracker for feature requests, bug reports, and pull requests.
|
||||
|
@ -44,15 +45,13 @@ Issues that do not follow these guidelines are subject to closure. There simply
|
|||
|
||||
### Feature Requests
|
||||
|
||||
Feature requests can be added using the issue tracker.
|
||||
Feature requests can be added using [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/ideas).
|
||||
|
||||
- Please **do** search for an existing request before suggesting a new feature.
|
||||
- Please **do** use the "👍" reaction to vote for a feature.
|
||||
- Please **do** use the voting buttons to vote for a feature.
|
||||
- Please **do** share substantial use cases and perspective that support new features if they haven't already been mentioned.
|
||||
- Please **do not** bump, spam, or ping contributors to prioritize your own feature.
|
||||
|
||||
If you would like your feature prioritized, please consider [sponsoring the project](https://github.com/sponsors/claviska).
|
||||
|
||||
### Bug Reports
|
||||
|
||||
A bug is _a demonstrable problem_ caused by code in the library. Bug reports are an important contribution to the quality of the project. When submitting a bug report, there are a few steps you can take to make sure your issues gets attention quickly.
|
||||
|
@ -65,8 +64,6 @@ A bug is _a demonstrable problem_ caused by code in the library. Bug reports are
|
|||
|
||||
**A minimal test case is critical to a successful bug report.** It demonstrates that the bug exists in the library and not in surrounding code. Contributors should be able to understand the bug without studying your code, otherwise they'll probably move on to another bug.
|
||||
|
||||
If you would like your bug prioritized, please consider [sponsoring the project](https://github.com/sponsors/claviska).
|
||||
|
||||
### Pull Requests
|
||||
|
||||
To keep the project on track, please consider the following guidelines before submitting a PR.
|
||||
|
|
|
@ -88,7 +88,7 @@ module.exports = environment;
|
|||
The final step is to add the corresponding `pack_tags` to the page. You should have the following `tags` in the `<head>` section of `app/views/layouts/application.html.erb`.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- ... -->
|
||||
|
|
Plik diff jest za duży
Load Diff
87
package.json
87
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.8.0",
|
||||
"version": "2.15.1",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
"customElements": "dist/custom-elements.json",
|
||||
"web-types": "./web-types.json",
|
||||
"web-types": "./dist/web-types.json",
|
||||
"type": "module",
|
||||
"types": "dist/shoelace.d.ts",
|
||||
"jsdelivr": "./cdn/shoelace-autoloader.js",
|
||||
|
@ -49,12 +49,12 @@
|
|||
"start": "node scripts/build.js --serve",
|
||||
"build": "node scripts/build.js",
|
||||
"verify": "npm run prettier:check && npm run lint && npm run build && npm run test",
|
||||
"prepare": "npx playwright install",
|
||||
"prepublishOnly": "npm run verify",
|
||||
"prettier": "prettier --write --loglevel warn .",
|
||||
"prettier:check": "prettier --check --loglevel warn .",
|
||||
"prettier": "prettier --write --log-level=warn .",
|
||||
"prettier:check": "prettier --check --log-level=warn .",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"lint:fix": "eslint src --max-warnings 0 --fix",
|
||||
"ts-check": "tsc --noEmit --project ./tsconfig.json",
|
||||
"create": "plop --plopfile scripts/plop/plopfile.js",
|
||||
"test": "web-test-runner --group default",
|
||||
"test:component": "web-test-runner -- --watch --group",
|
||||
|
@ -67,77 +67,78 @@
|
|||
"node": ">=14.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@floating-ui/dom": "^1.2.1",
|
||||
"@lit-labs/react": "^2.0.1",
|
||||
"@ctrl/tinycolor": "^4.0.2",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@lit/react": "^1.0.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.1.1",
|
||||
"@shoelace-style/localize": "^3.1.2",
|
||||
"composed-offset-position": "^0.0.4",
|
||||
"lit": "^2.7.5",
|
||||
"lit": "^3.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^2.0.1",
|
||||
"@custom-elements-manifest/analyzer": "^0.8.3",
|
||||
"@open-wc/testing": "^3.1.7",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@web/dev-server-esbuild": "^0.3.3",
|
||||
"@web/test-runner": "^0.15.0",
|
||||
"@web/test-runner-commands": "^0.6.5",
|
||||
"@web/test-runner-playwright": "^0.9.0",
|
||||
"bootstrap-icons": "^1.10.5",
|
||||
"@custom-elements-manifest/analyzer": "^0.8.4",
|
||||
"@open-wc/testing": "^3.2.0",
|
||||
"@types/mocha": "^10.0.2",
|
||||
"@types/react": "^18.2.28",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
||||
"@typescript-eslint/parser": "^6.7.5",
|
||||
"@web/dev-server-esbuild": "^0.3.6",
|
||||
"@web/test-runner": "^0.18.0",
|
||||
"@web/test-runner-commands": "^0.9.0",
|
||||
"@web/test-runner-playwright": "^0.11.0",
|
||||
"bootstrap-icons": "^1.11.1",
|
||||
"browser-sync": "^2.29.3",
|
||||
"chalk": "^5.2.0",
|
||||
"chalk": "^5.3.0",
|
||||
"change-case": "^4.1.2",
|
||||
"command-line-args": "^5.2.1",
|
||||
"comment-parser": "^1.3.1",
|
||||
"comment-parser": "^1.4.0",
|
||||
"cspell": "^6.18.1",
|
||||
"custom-element-jet-brains-integration": "^1.1.0",
|
||||
"custom-element-vs-code-integration": "^1.1.0",
|
||||
"del": "^7.0.0",
|
||||
"custom-element-jet-brains-integration": "^1.4.0",
|
||||
"custom-element-vs-code-integration": "^1.2.1",
|
||||
"custom-element-vuejs-integration": "^1.0.0",
|
||||
"del": "^7.1.0",
|
||||
"download": "^8.0.0",
|
||||
"esbuild": "^0.18.2",
|
||||
"esbuild": "^0.19.4",
|
||||
"esbuild-plugin-replace": "^1.4.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-plugin-chai-expect": "^3.0.0",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-lit": "^1.8.3",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-lit": "^1.9.1",
|
||||
"eslint-plugin-lit-a11y": "^4.1.0",
|
||||
"eslint-plugin-markdown": "^3.0.0",
|
||||
"eslint-plugin-markdown": "^3.0.1",
|
||||
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
|
||||
"eslint-plugin-wc": "^1.5.0",
|
||||
"eslint-plugin-wc": "^2.0.4",
|
||||
"front-matter": "^4.0.2",
|
||||
"get-port": "^7.0.0",
|
||||
"globby": "^13.1.3",
|
||||
"globby": "^13.2.2",
|
||||
"husky": "^8.0.3",
|
||||
"jsdom": "^22.1.0",
|
||||
"jsonata": "^2.0.1",
|
||||
"lint-staged": "^13.1.0",
|
||||
"jsonata": "^2.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"lunr": "^2.3.9",
|
||||
"markdown-it-container": "^3.0.0",
|
||||
"markdown-it-ins": "^3.0.1",
|
||||
"markdown-it-kbd": "^2.2.2",
|
||||
"markdown-it-mark": "^3.0.1",
|
||||
"markdown-it-replace-it": "^1.0.0",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"ora": "^6.3.1",
|
||||
"npm-check-updates": "^16.14.6",
|
||||
"ora": "^7.0.1",
|
||||
"pascal-case": "^3.1.2",
|
||||
"plop": "^3.1.1",
|
||||
"prettier": "^2.8.8",
|
||||
"plop": "^4.0.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon": "^16.1.0",
|
||||
"smartquotes": "^2.3.2",
|
||||
"source-map": "^0.7.4",
|
||||
"strip-css-comments": "^5.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.1.3",
|
||||
"user-agent-data-types": "^0.3.0"
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"user-agent-data-types": "^0.3.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,js}": [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSpacing: true,
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
|
@ -16,3 +16,5 @@ module.exports = {
|
|||
trailingComma: 'none',
|
||||
useTabs: false
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -34,6 +34,9 @@ const shoelaceVersion = JSON.stringify(packageData.version.toString());
|
|||
async function buildTheDocs(watch = false) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const afterSignal = '[eleventy.after]';
|
||||
|
||||
// Totally non-scientific way to handle errors. Perhaps its just better to resolve on stderr? :shrug:
|
||||
const errorSignal = 'Original error stack trace:';
|
||||
const args = ['@11ty/eleventy', '--quiet'];
|
||||
const output = [];
|
||||
|
||||
|
@ -53,6 +56,10 @@ async function buildTheDocs(watch = false) {
|
|||
output.push(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on('data', data => {
|
||||
output.push(data.toString());
|
||||
});
|
||||
|
||||
if (watch) {
|
||||
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
|
||||
// tells us when the first build completes.
|
||||
|
@ -61,6 +68,13 @@ async function buildTheDocs(watch = false) {
|
|||
resolve({ child, output });
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', data => {
|
||||
if (data.includes(errorSignal)) {
|
||||
// This closes the dev server, not sure if thats what we want?
|
||||
reject(output);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
child.on('close', () => {
|
||||
resolve({ child, output });
|
||||
|
@ -73,7 +87,7 @@ async function buildTheDocs(watch = false) {
|
|||
// Builds the source with esbuild.
|
||||
//
|
||||
async function buildTheSource() {
|
||||
const alwaysExternal = ['@lit-labs/react', 'react'];
|
||||
const alwaysExternal = ['@lit/react', 'react'];
|
||||
|
||||
const cdnConfig = {
|
||||
format: 'esm',
|
||||
|
@ -108,7 +122,7 @@ async function buildTheSource() {
|
|||
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
|
||||
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
|
||||
//
|
||||
// We never bundle React or @lit-labs/react though!
|
||||
// We never bundle React or @lit/react though!
|
||||
//
|
||||
external: alwaysExternal,
|
||||
splitting: true,
|
||||
|
@ -201,9 +215,8 @@ await nextTask('Running the TypeScript compiler', () => {
|
|||
});
|
||||
|
||||
// Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing.
|
||||
await nextTask(`Copying Web Types, Themes, Icons, and TS Types to "${cdndir}"`, async () => {
|
||||
await nextTask(`Themes, Icons, and TS Types to "${cdndir}"`, async () => {
|
||||
await deleteAsync(cdndir);
|
||||
await copy('./web-types.json', `${outdir}/web-types.json`);
|
||||
await copy(outdir, cdndir);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import commandLineArgs from 'command-line-args';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { deleteSync } from 'del';
|
||||
import prettier from 'prettier';
|
||||
import prettierConfig from '../prettier.config.cjs';
|
||||
import { default as prettierConfig } from '../prettier.config.js';
|
||||
import { getAllComponents } from './shared.js';
|
||||
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
|
@ -20,19 +19,18 @@ const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.j
|
|||
const components = getAllComponents(metadata);
|
||||
const index = [];
|
||||
|
||||
components.map(component => {
|
||||
for await (const component of components) {
|
||||
const tagWithoutPrefix = component.tagName.replace(/^sl-/, '');
|
||||
const componentDir = path.join(reactDir, tagWithoutPrefix);
|
||||
const componentFile = path.join(componentDir, 'index.ts');
|
||||
const importPath = component.path.replace(/\.js$/, '.component.js');
|
||||
const eventImports = (component.events || [])
|
||||
.map(event => `import type { ${event.eventName} } from '../../../src/events/events';`)
|
||||
.map(event => `import type { ${event.eventName} } from '../../events/events.js';`)
|
||||
.join('\n');
|
||||
const eventExports = (component.events || [])
|
||||
.map(event => `export type { ${event.eventName} } from '../../../src/events/events';`)
|
||||
.map(event => `export type { ${event.eventName} } from '../../events/events.js';`)
|
||||
.join('\n');
|
||||
const eventNameImport =
|
||||
(component.events || []).length > 0 ? `import { type EventName } from '@lit-labs/react';` : ``;
|
||||
const eventNameImport = (component.events || []).length > 0 ? `import { type EventName } from '@lit/react';` : ``;
|
||||
const events = (component.events || [])
|
||||
.map(event => `${event.reactName}: '${event.name}' as EventName<${event.eventName}>`)
|
||||
.join(',\n');
|
||||
|
@ -41,10 +39,10 @@ components.map(component => {
|
|||
|
||||
const jsDoc = component.jsDoc || '';
|
||||
|
||||
const source = prettier.format(
|
||||
const source = await prettier.format(
|
||||
`
|
||||
import * as React from 'react';
|
||||
import { createComponent } from '@lit-labs/react';
|
||||
import { createComponent } from '@lit/react';
|
||||
import Component from '../../${importPath}';
|
||||
|
||||
${eventNameImport}
|
||||
|
@ -75,7 +73,7 @@ components.map(component => {
|
|||
index.push(`export { default as ${component.name} } from './${tagWithoutPrefix}/index.js';`);
|
||||
|
||||
fs.writeFileSync(componentFile, source, 'utf8');
|
||||
});
|
||||
}
|
||||
|
||||
// Generate the index file
|
||||
fs.writeFileSync(path.join(reactDir, 'index.ts'), index.join('\n'), 'utf8');
|
||||
|
|
|
@ -24,7 +24,7 @@ filesToEmbed.forEach(file => {
|
|||
});
|
||||
|
||||
// Loop through each theme file, copying the .css and generating a .js version for Lit users
|
||||
files.forEach(file => {
|
||||
files.forEach(async file => {
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
|
||||
// If the source has "/* _filename.css */" in it, replace it with the embedded styles
|
||||
|
@ -32,11 +32,11 @@ files.forEach(file => {
|
|||
source = source.replace(`/* ${key} */`, embeds[key]);
|
||||
});
|
||||
|
||||
const css = prettier.format(stripComments(source), {
|
||||
const css = await prettier.format(stripComments(source), {
|
||||
parser: 'css'
|
||||
});
|
||||
|
||||
let js = prettier.format(
|
||||
let js = await prettier.format(
|
||||
`
|
||||
import { css } from 'lit';
|
||||
|
||||
|
@ -47,9 +47,19 @@ files.forEach(file => {
|
|||
{ parser: 'babel-ts' }
|
||||
);
|
||||
|
||||
let dTs = await prettier.format(
|
||||
`
|
||||
declare const _default: import("lit").CSSResult;
|
||||
export default _default;
|
||||
`,
|
||||
{ parser: 'babel-ts' }
|
||||
);
|
||||
|
||||
const cssFile = path.join(themesDir, path.basename(file));
|
||||
const jsFile = path.join(themesDir, path.basename(file).replace('.css', '.styles.js'));
|
||||
const dTsFile = path.join(themesDir, path.basename(file).replace('.css', '.styles.d.ts'));
|
||||
|
||||
fs.writeFileSync(cssFile, css, 'utf8');
|
||||
fs.writeFileSync(jsFile, js, 'utf8');
|
||||
fs.writeFileSync(dTsFile, dTs, 'utf8');
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import { property } from 'lit/decorators.js';
|
|||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './{{ tagWithoutPrefix tag }}.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -24,7 +25,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @cssproperty --example - An example CSS custom property.
|
||||
*/
|
||||
export default class {{ properCase tag }} extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { LocalizeController } from '../../utilities/localize.js';
|
|||
import { property, query } from 'lit/decorators.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIconButton from '../icon-button/icon-button.component.js';
|
||||
import styles from './alert.styles.js';
|
||||
|
@ -40,7 +41,7 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
|
|||
* @animation alert.hide - The animation to use when hiding the alert.
|
||||
*/
|
||||
export default class SlAlert extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-icon-button': SlIconButton };
|
||||
|
||||
private autoHideTimeout: number;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: contents;
|
||||
|
||||
|
|
|
@ -210,10 +210,12 @@ describe('<sl-alert>', () => {
|
|||
};
|
||||
|
||||
it('deletes the toast stack after the last alert is done', async () => {
|
||||
const container = await fixture<HTMLElement>(html`<div>
|
||||
<sl-alert data-testid="alert1" closable>alert 1</sl-alert>
|
||||
<sl-alert data-testid="alert2" closable>alert 2</sl-alert>
|
||||
</div>`);
|
||||
const container = await fixture<HTMLElement>(
|
||||
html`<div>
|
||||
<sl-alert data-testid="alert1" closable>alert 1</sl-alert>
|
||||
<sl-alert data-testid="alert2" closable>alert 2</sl-alert>
|
||||
</div>`
|
||||
);
|
||||
|
||||
const alert1 = queryByTestId<SlAlert>(container, 'alert1');
|
||||
const alert2 = queryByTestId<SlAlert>(container, 'alert2');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { html } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './animated-image.styles.js';
|
||||
|
@ -20,13 +21,13 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
* @part control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
*
|
||||
* @cssproperty --control-box-size - The size of the icon box.
|
||||
* @cssproperty --icon-size - The size of the play/pause icons.
|
||||
*/
|
||||
export default class SlAnimatedImage extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-icon': SlIcon };
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--control-box-size: 3rem;
|
||||
--icon-size: calc(var(--control-box-size) * 0.625);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { animations } from './animations.js';
|
|||
import { html } from 'lit';
|
||||
import { property, queryAsync } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './animation.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -20,7 +21,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* animate multiple elements, either wrap them in a single container or use multiple `<sl-animation>` elements.
|
||||
*/
|
||||
export default class SlAnimation extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
private animation?: Animation;
|
||||
private hasStarted = false;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, oneEvent } from '@open-wc/testing';
|
||||
import { html } from 'lit';
|
||||
import type SlAnimation from './animation.js';
|
||||
|
||||
describe('<sl-animation>', () => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||
import { html } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './avatar.styles.js';
|
||||
|
@ -25,7 +26,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @cssproperty --size - The size of the avatar.
|
||||
*/
|
||||
export default class SlAvatar extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = {
|
||||
'sl-icon': SlIcon
|
||||
};
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
||||
|
@ -23,6 +20,7 @@ export default css`
|
|||
font-weight: var(--sl-font-weight-normal);
|
||||
color: var(--sl-color-neutral-0);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './badge.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -16,7 +17,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
export default class SlBadge extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
/** The badge's theme variant. */
|
||||
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
@ -21,6 +18,7 @@ export default css`
|
|||
white-space: nowrap;
|
||||
padding: 0.35em 0.6em;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { HasSlotController } from '../../internal/slot.js';
|
|||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './breadcrumb-item.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -26,7 +27,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart separator - The container that wraps the separator.
|
||||
*/
|
||||
export default class SlBreadcrumbItem extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
@ -84,5 +81,6 @@ export default css`
|
|||
align-items: center;
|
||||
margin: 0 var(--sl-spacing-x-small);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './breadcrumb.styles.js';
|
||||
|
@ -21,7 +22,7 @@ import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item.js';
|
|||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
export default class SlBreadcrumb extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-icon': SlIcon };
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { html } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './button-group.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -15,7 +16,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
export default class SlButtonGroup extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
|
||||
|
@ -29,22 +30,22 @@ export default class SlButtonGroup extends ShoelaceElement {
|
|||
|
||||
private handleFocus(event: Event) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.add('sl-button-group__button--focus');
|
||||
button?.toggleAttribute('data-sl-button-group__button--focus', true);
|
||||
}
|
||||
|
||||
private handleBlur(event: Event) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.remove('sl-button-group__button--focus');
|
||||
button?.toggleAttribute('data-sl-button-group__button--focus', false);
|
||||
}
|
||||
|
||||
private handleMouseOver(event: Event) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.add('sl-button-group__button--hover');
|
||||
button?.toggleAttribute('data-sl-button-group__button--hover', true);
|
||||
}
|
||||
|
||||
private handleMouseOut(event: Event) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.remove('sl-button-group__button--hover');
|
||||
button?.toggleAttribute('data-sl-button-group__button--hover', false);
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
|
@ -55,11 +56,14 @@ export default class SlButtonGroup extends ShoelaceElement {
|
|||
const button = findButton(el);
|
||||
|
||||
if (button) {
|
||||
button.classList.add('sl-button-group__button');
|
||||
button.classList.toggle('sl-button-group__button--first', index === 0);
|
||||
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
||||
button.classList.toggle('sl-button-group__button--last', index === slottedElements.length - 1);
|
||||
button.classList.toggle('sl-button-group__button--radio', button.tagName.toLowerCase() === 'sl-radio-button');
|
||||
button.toggleAttribute('data-sl-button-group__button', true);
|
||||
button.toggleAttribute('data-sl-button-group__button--first', index === 0);
|
||||
button.toggleAttribute('data-sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
||||
button.toggleAttribute('data-sl-button-group__button--last', index === slottedElements.length - 1);
|
||||
button.toggleAttribute(
|
||||
'data-sl-button-group__button--radio',
|
||||
button.tagName.toLowerCase() === 'sl-radio-button'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ describe('<sl-button-group>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('slotted button classes', () => {
|
||||
it('slotted buttons have the right classes applied based on their order', async () => {
|
||||
describe('slotted button data attributes', () => {
|
||||
it('slotted buttons have the right data attributes applied based on their order', async () => {
|
||||
const group = await fixture<SlButtonGroup>(html`
|
||||
<sl-button-group>
|
||||
<sl-button>Button 1 Label</sl-button>
|
||||
|
@ -38,19 +38,19 @@ describe('<sl-button-group>', () => {
|
|||
`);
|
||||
|
||||
const allButtons = group.querySelectorAll('sl-button');
|
||||
const hasGroupClass = Array.from(allButtons).every(button =>
|
||||
button.classList.contains('sl-button-group__button')
|
||||
const hasGroupAttrib = Array.from(allButtons).every(button =>
|
||||
button.hasAttribute('data-sl-button-group__button')
|
||||
);
|
||||
expect(hasGroupClass).to.be.true;
|
||||
expect(hasGroupAttrib).to.be.true;
|
||||
|
||||
expect(allButtons[0]).to.have.class('sl-button-group__button--first');
|
||||
expect(allButtons[1]).to.have.class('sl-button-group__button--inner');
|
||||
expect(allButtons[2]).to.have.class('sl-button-group__button--last');
|
||||
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--first');
|
||||
expect(allButtons[1]).to.have.attribute('data-sl-button-group__button--inner');
|
||||
expect(allButtons[2]).to.have.attribute('data-sl-button-group__button--last');
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus and blur events', () => {
|
||||
it('toggles focus class to slotted buttons on focus/blur', async () => {
|
||||
it('toggles focus data attribute to slotted buttons on focus/blur', async () => {
|
||||
const group = await fixture<SlButtonGroup>(html`
|
||||
<sl-button-group>
|
||||
<sl-button>Button 1 Label</sl-button>
|
||||
|
@ -63,16 +63,16 @@ describe('<sl-button-group>', () => {
|
|||
allButtons[0].dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
|
||||
|
||||
await elementUpdated(allButtons[0]);
|
||||
expect(allButtons[0].classList.contains('sl-button-group__button--focus')).to.be.true;
|
||||
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--focus');
|
||||
|
||||
allButtons[0].dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
||||
await elementUpdated(allButtons[0]);
|
||||
expect(allButtons[0].classList.contains('sl-button-group__button--focus')).not.to.be.true;
|
||||
expect(allButtons[0]).to.not.have.attribute('data-sl-button-group__button--focus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mouseover and mouseout events', () => {
|
||||
it('toggles hover class to slotted buttons on mouseover/mouseout', async () => {
|
||||
it('toggles hover data attribute to slotted buttons on mouseover/mouseout', async () => {
|
||||
const group = await fixture<SlButtonGroup>(html`
|
||||
<sl-button-group>
|
||||
<sl-button>Button 1 Label</sl-button>
|
||||
|
@ -85,11 +85,12 @@ describe('<sl-button-group>', () => {
|
|||
|
||||
allButtons[0].dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
await elementUpdated(allButtons[0]);
|
||||
expect(allButtons[0].classList.contains('sl-button-group__button--hover')).to.be.true;
|
||||
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--hover');
|
||||
|
||||
allButtons[0].dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
|
||||
await elementUpdated(allButtons[0]);
|
||||
expect(allButtons[0].classList.contains('sl-button-group__button--hover')).not.to.be.true;
|
||||
console.log(allButtons[0]);
|
||||
expect(allButtons[0]).to.not.have.attribute('data-sl-button-group__button--hover');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
|||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import SlSpinner from '../spinner/spinner.component.js';
|
||||
|
@ -38,27 +39,16 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
|||
* @csspart spinner - The spinner that shows when the button is in the loading state.
|
||||
*/
|
||||
export default class SlButton extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = {
|
||||
'sl-icon': SlIcon,
|
||||
'sl-spinner': SlSpinner
|
||||
};
|
||||
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
form: input => {
|
||||
// Buttons support a form attribute that points to an arbitrary form, so if this attribute is set we need to query
|
||||
// the form from the same root using its id
|
||||
if (input.hasAttribute('form')) {
|
||||
const doc = input.getRootNode() as Document | ShadowRoot;
|
||||
const formId = input.getAttribute('form')!;
|
||||
return doc.getElementById(formId) as HTMLFormElement;
|
||||
}
|
||||
|
||||
// Fall back to the closest containing form
|
||||
return input.closest('form');
|
||||
},
|
||||
assumeInteractionOn: ['click']
|
||||
});
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
@ -22,11 +19,15 @@ export default css`
|
|||
font-weight: var(--sl-font-weight-semibold);
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
transition: var(--sl-transition-x-fast) background-color, var(--sl-transition-x-fast) color,
|
||||
var(--sl-transition-x-fast) border, var(--sl-transition-x-fast) box-shadow;
|
||||
transition:
|
||||
var(--sl-transition-x-fast) background-color,
|
||||
var(--sl-transition-x-fast) color,
|
||||
var(--sl-transition-x-fast) border,
|
||||
var(--sl-transition-x-fast) box-shadow;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
|
@ -545,30 +546,30 @@ export default css`
|
|||
* buttons and we style them here instead.
|
||||
*/
|
||||
|
||||
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
|
||||
:host([data-sl-button-group__button--first]:not([data-sl-button-group__button--last])) .button {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
:host(.sl-button-group__button--inner) .button {
|
||||
:host([data-sl-button-group__button--inner]) .button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
|
||||
:host([data-sl-button-group__button--last]:not([data-sl-button-group__button--first])) .button {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
/* All except the first */
|
||||
:host(.sl-button-group__button:not(.sl-button-group__button--first)) {
|
||||
:host([data-sl-button-group__button]:not([data-sl-button-group__button--first])) {
|
||||
margin-inline-start: calc(-1 * var(--sl-input-border-width));
|
||||
}
|
||||
|
||||
/* Add a visual separator between solid buttons */
|
||||
:host(
|
||||
.sl-button-group__button:not(
|
||||
.sl-button-group__button--first,
|
||||
.sl-button-group__button--radio,
|
||||
[data-sl-button-group__button]:not(
|
||||
[data-sl-button-group__button--first],
|
||||
[data-sl-button-group__button--radio],
|
||||
[variant='default']
|
||||
):not(:hover)
|
||||
)
|
||||
|
@ -583,13 +584,13 @@ export default css`
|
|||
}
|
||||
|
||||
/* Bump hovered, focused, and checked buttons up so their focus ring isn't clipped */
|
||||
:host(.sl-button-group__button--hover) {
|
||||
:host([data-sl-button-group__button--hover]) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Focus and checked are always on top */
|
||||
:host(.sl-button-group__button--focus),
|
||||
:host(.sl-button-group__button[checked]) {
|
||||
:host([data-sl-button-group__button--focus]),
|
||||
:host([data-sl-button-group__button][checked]) {
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -99,25 +99,25 @@ describe('<sl-button>', () => {
|
|||
});
|
||||
|
||||
it('should render a link with rel="noreferrer noopener" when target is set and rel is not', async () => {
|
||||
const el = await fixture<SlButton>(
|
||||
html` <sl-button href="https://example.com/" target="_blank">Link</sl-button> `
|
||||
);
|
||||
const el = await fixture<SlButton>(html`
|
||||
<sl-button href="https://example.com/" target="_blank">Link</sl-button>
|
||||
`);
|
||||
const link = el.shadowRoot!.querySelector('a')!;
|
||||
expect(link?.getAttribute('rel')).to.equal('noreferrer noopener');
|
||||
});
|
||||
|
||||
it('should render a link with rel="" when a target is provided and rel is empty', async () => {
|
||||
const el = await fixture<SlButton>(
|
||||
html` <sl-button href="https://example.com/" target="_blank" rel="">Link</sl-button> `
|
||||
);
|
||||
const el = await fixture<SlButton>(html`
|
||||
<sl-button href="https://example.com/" target="_blank" rel="">Link</sl-button>
|
||||
`);
|
||||
const link = el.shadowRoot!.querySelector('a')!;
|
||||
expect(link?.getAttribute('rel')).to.equal('');
|
||||
});
|
||||
|
||||
it(`should render a link with a custom rel when a custom rel is provided`, async () => {
|
||||
const el = await fixture<SlButton>(
|
||||
html` <sl-button href="https://example.com/" target="_blank" rel="1">Link</sl-button> `
|
||||
);
|
||||
const el = await fixture<SlButton>(html`
|
||||
<sl-button href="https://example.com/" target="_blank" rel="1">Link</sl-button>
|
||||
`);
|
||||
const link = el.shadowRoot!.querySelector('a')!;
|
||||
expect(link?.getAttribute('rel')).to.equal('1');
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './card.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -28,7 +29,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @cssproperty --padding - The padding to use for the card's sections.
|
||||
*/
|
||||
export default class SlCard extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--border-color: var(--sl-color-neutral-200);
|
||||
--border-radius: var(--sl-border-radius-medium);
|
||||
|
|
|
@ -7,9 +7,9 @@ describe('<sl-card>', () => {
|
|||
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
|
||||
);
|
||||
el = await fixture<SlCard>(html`
|
||||
<sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { html } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './carousel-item.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -15,11 +16,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
*
|
||||
*/
|
||||
export default class SlCarouselItem extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
static isCarouselItem(node: Node) {
|
||||
return node instanceof Element && node.getAttribute('aria-roledescription') === 'slide';
|
||||
}
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--aspect-ratio: inherit;
|
||||
|
||||
|
@ -19,8 +16,8 @@ export default css`
|
|||
}
|
||||
|
||||
::slotted(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('<sl-carousel-item>', () => {
|
|||
|
||||
it('should pass accessibility tests', async () => {
|
||||
// Arrange
|
||||
const el = await fixture(html` <div role="list"><sl-carousel-item></sl-carousel-item></div> `);
|
||||
const el = await fixture(html` <sl-carousel-item></sl-carousel-item> `);
|
||||
|
||||
// Assert
|
||||
await expect(el).to.be.accessible();
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import '../../internal/scrollend-polyfill.js';
|
||||
|
||||
import { AutoplayController } from './autoplay-controller.js';
|
||||
import { clamp } from '../../internal/math.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { eventOptions, property, query, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { map } from 'lit/directives/map.js';
|
||||
import { prefersReducedMotion } from '../../internal/animate.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { range } from 'lit/directives/range.js';
|
||||
import { ScrollController } from './scroll-controller.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlCarouselItem from '../carousel-item/carousel-item.component.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './carousel.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { CSSResultGroup, PropertyValueMap } from 'lit';
|
||||
import type SlCarouselItem from '../carousel-item/carousel-item.component.js';
|
||||
|
||||
/**
|
||||
* @summary Carousels display an arbitrary number of content slides along a horizontal or vertical axis.
|
||||
|
@ -40,12 +43,12 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart navigation-button--next - Applied to the next button.
|
||||
*
|
||||
* @cssproperty --slide-gap - The space between each slide.
|
||||
* @cssproperty --aspect-ratio - The aspect ratio of each slide.
|
||||
* @cssproperty [--aspect-ratio=16/9] - The aspect ratio of each slide.
|
||||
* @cssproperty --scroll-hint - The amount of padding to apply to the scroll area, allowing adjacent slides to become
|
||||
* partially visible as a scroll hint.
|
||||
*/
|
||||
export default class SlCarousel extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-icon': SlIcon };
|
||||
|
||||
/** When set, allows the user to navigate the carousel in the same direction indefinitely. */
|
||||
|
@ -68,7 +71,7 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
|
||||
/**
|
||||
* Specifies the number of slides the carousel will advance when scrolling, useful when specifying a `slides-per-page`
|
||||
* greater than one.
|
||||
* greater than one. It can't be higher than `slides-per-page`.
|
||||
*/
|
||||
@property({ type: Number, attribute: 'slides-per-move' }) slidesPerMove = 1;
|
||||
|
||||
|
@ -78,19 +81,17 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
/** When set, it is possible to scroll through the slides by dragging them with the mouse. */
|
||||
@property({ type: Boolean, reflect: true, attribute: 'mouse-dragging' }) mouseDragging = false;
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.carousel__slides') scrollContainer: HTMLElement;
|
||||
@query('.carousel__pagination') paginationContainer: HTMLElement;
|
||||
|
||||
// The index of the active slide
|
||||
@state() activeSlide = 0;
|
||||
|
||||
@state() scrolling = false;
|
||||
|
||||
@state() dragging = false;
|
||||
|
||||
private autoplayController = new AutoplayController(this, () => this.next());
|
||||
private scrollController = new ScrollController(this);
|
||||
private readonly slides = this.getElementsByTagName('sl-carousel-item');
|
||||
private intersectionObserver: IntersectionObserver; // determines which slide is displayed
|
||||
// A map containing the state of all the slides
|
||||
private readonly intersectionObserverEntries = new Map<Element, IntersectionObserverEntry>();
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private mutationObserver: MutationObserver;
|
||||
|
||||
|
@ -98,60 +99,61 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
super.connectedCallback();
|
||||
this.setAttribute('role', 'region');
|
||||
this.setAttribute('aria-label', this.localize.term('carousel'));
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach(entry => {
|
||||
// Store all the entries in a map to be processed when scrolling ends
|
||||
this.intersectionObserverEntries.set(entry.target, entry);
|
||||
|
||||
const slide = entry.target;
|
||||
slide.toggleAttribute('inert', !entry.isIntersecting);
|
||||
slide.classList.toggle('--in-view', entry.isIntersecting);
|
||||
slide.setAttribute('aria-hidden', entry.isIntersecting ? 'false' : 'true');
|
||||
});
|
||||
},
|
||||
{
|
||||
root: this,
|
||||
threshold: 0.6
|
||||
}
|
||||
);
|
||||
this.intersectionObserver = intersectionObserver;
|
||||
|
||||
// Store the initial state of each slide
|
||||
intersectionObserver.takeRecords().forEach(entry => {
|
||||
this.intersectionObserverEntries.set(entry.target, entry);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.intersectionObserver.disconnect();
|
||||
this.mutationObserver.disconnect();
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this.initializeSlides();
|
||||
this.mutationObserver = new MutationObserver(this.handleSlotChange);
|
||||
this.mutationObserver.observe(this, { childList: true, subtree: false });
|
||||
this.mutationObserver.observe(this, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValueMap<SlCarousel> | Map<PropertyKey, unknown>): void {
|
||||
// Ensure the slidesPerMove is never higher than the slidesPerPage
|
||||
if (changedProperties.has('slidesPerMove') || changedProperties.has('slidesPerPage')) {
|
||||
this.slidesPerMove = Math.min(this.slidesPerMove, this.slidesPerPage);
|
||||
}
|
||||
}
|
||||
|
||||
private getPageCount() {
|
||||
return Math.ceil(this.getSlides().length / this.slidesPerPage);
|
||||
const slidesCount = this.getSlides().length;
|
||||
const { slidesPerPage, slidesPerMove, loop } = this;
|
||||
|
||||
const pages = loop ? slidesCount / slidesPerMove : (slidesCount - slidesPerPage) / slidesPerMove + 1;
|
||||
|
||||
return Math.ceil(pages);
|
||||
}
|
||||
|
||||
private getCurrentPage() {
|
||||
return Math.ceil(this.activeSlide / this.slidesPerPage);
|
||||
return Math.ceil(this.activeSlide / this.slidesPerMove);
|
||||
}
|
||||
|
||||
private canScrollNext(): boolean {
|
||||
return this.loop || this.getCurrentPage() < this.getPageCount() - 1;
|
||||
}
|
||||
|
||||
private canScrollPrev(): boolean {
|
||||
return this.loop || this.getCurrentPage() > 0;
|
||||
}
|
||||
|
||||
/** @internal Gets all carousel items. */
|
||||
private getSlides({ excludeClones = true }: { excludeClones?: boolean } = {}) {
|
||||
return [...this.slides].filter(slide => !excludeClones || !slide.hasAttribute('data-clone'));
|
||||
return [...this.children].filter(
|
||||
(el: HTMLElement) => this.isCarouselItem(el) && (!excludeClones || !el.hasAttribute('data-clone'))
|
||||
) as SlCarouselItem[];
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
|
||||
const target = event.target as HTMLElement;
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const isRtl = this.matches(':dir(rtl)');
|
||||
const isFocusInPagination = target.closest('[part~="pagination-item"]') !== null;
|
||||
const isNext =
|
||||
event.key === 'ArrowDown' || (!isRtl && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft');
|
||||
|
@ -190,31 +192,135 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
}
|
||||
}
|
||||
|
||||
private handleMouseDragStart(event: PointerEvent) {
|
||||
const canDrag = this.mouseDragging && event.button === 0;
|
||||
if (canDrag) {
|
||||
event.preventDefault();
|
||||
|
||||
document.addEventListener('pointermove', this.handleMouseDrag, { capture: true, passive: true });
|
||||
document.addEventListener('pointerup', this.handleMouseDragEnd, { capture: true, once: true });
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseDrag = (event: PointerEvent) => {
|
||||
if (!this.dragging) {
|
||||
// Start dragging if it hasn't yet
|
||||
this.scrollContainer.style.setProperty('scroll-snap-type', 'none');
|
||||
this.dragging = true;
|
||||
}
|
||||
|
||||
this.scrollContainer.scrollBy({
|
||||
left: -event.movementX,
|
||||
top: -event.movementY,
|
||||
behavior: 'instant'
|
||||
});
|
||||
};
|
||||
|
||||
private handleMouseDragEnd = () => {
|
||||
const scrollContainer = this.scrollContainer;
|
||||
|
||||
document.removeEventListener('pointermove', this.handleMouseDrag, { capture: true });
|
||||
|
||||
// get the current scroll position
|
||||
const startLeft = scrollContainer.scrollLeft;
|
||||
const startTop = scrollContainer.scrollTop;
|
||||
|
||||
// remove the scroll-snap-type property so that the browser will snap the slide to the correct position
|
||||
scrollContainer.style.removeProperty('scroll-snap-type');
|
||||
|
||||
// fix(safari): forcing a style recalculation doesn't seem to immediately update the scroll
|
||||
// position in Safari. Setting "overflow" to "hidden" should force this behavior.
|
||||
scrollContainer.style.setProperty('overflow', 'hidden');
|
||||
|
||||
// get the final scroll position to the slide snapped by the browser
|
||||
const finalLeft = scrollContainer.scrollLeft;
|
||||
const finalTop = scrollContainer.scrollTop;
|
||||
|
||||
// restore the scroll position to the original one, so that it can be smoothly animated if needed
|
||||
scrollContainer.style.removeProperty('overflow');
|
||||
scrollContainer.style.setProperty('scroll-snap-type', 'none');
|
||||
scrollContainer.scrollTo({ left: startLeft, top: startTop, behavior: 'instant' });
|
||||
|
||||
requestAnimationFrame(async () => {
|
||||
if (startLeft !== finalLeft || startTop !== finalTop) {
|
||||
scrollContainer.scrollTo({
|
||||
left: finalLeft,
|
||||
top: finalTop,
|
||||
behavior: prefersReducedMotion() ? 'auto' : 'smooth'
|
||||
});
|
||||
await waitForEvent(scrollContainer, 'scrollend');
|
||||
}
|
||||
|
||||
scrollContainer.style.removeProperty('scroll-snap-type');
|
||||
|
||||
this.dragging = false;
|
||||
this.handleScrollEnd();
|
||||
});
|
||||
};
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleScroll() {
|
||||
this.scrolling = true;
|
||||
}
|
||||
|
||||
/** @internal Synchronizes the slides with the IntersectionObserver API. */
|
||||
private synchronizeSlides() {
|
||||
const io = new IntersectionObserver(
|
||||
entries => {
|
||||
io.disconnect();
|
||||
|
||||
for (const entry of entries) {
|
||||
const slide = entry.target;
|
||||
slide.toggleAttribute('inert', !entry.isIntersecting);
|
||||
slide.classList.toggle('--in-view', entry.isIntersecting);
|
||||
slide.setAttribute('aria-hidden', entry.isIntersecting ? 'false' : 'true');
|
||||
}
|
||||
|
||||
const firstIntersecting = entries.find(entry => entry.isIntersecting);
|
||||
|
||||
if (firstIntersecting) {
|
||||
if (this.loop && firstIntersecting.target.hasAttribute('data-clone')) {
|
||||
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
|
||||
|
||||
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
|
||||
this.goToSlide(clonePosition, 'instant');
|
||||
} else {
|
||||
const slides = this.getSlides();
|
||||
|
||||
// Update the current index based on the first visible slide
|
||||
const slideIndex = slides.indexOf(firstIntersecting.target as SlCarouselItem);
|
||||
// Set the index to the first "snappable" slide
|
||||
this.activeSlide = Math.ceil(slideIndex / this.slidesPerMove) * this.slidesPerMove;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: this.scrollContainer,
|
||||
threshold: 0.6
|
||||
}
|
||||
);
|
||||
|
||||
this.getSlides({ excludeClones: false }).forEach(slide => {
|
||||
io.observe(slide);
|
||||
});
|
||||
}
|
||||
|
||||
private handleScrollEnd() {
|
||||
const slides = this.getSlides();
|
||||
const entries = [...this.intersectionObserverEntries.values()];
|
||||
if (!this.scrolling || this.dragging) return;
|
||||
|
||||
const firstIntersecting: IntersectionObserverEntry | undefined = entries.find(entry => entry.isIntersecting);
|
||||
this.synchronizeSlides();
|
||||
|
||||
if (this.loop && firstIntersecting?.target.hasAttribute('data-clone')) {
|
||||
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
|
||||
this.scrolling = false;
|
||||
}
|
||||
|
||||
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
|
||||
this.goToSlide(clonePosition, 'auto');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate the first intersecting slide
|
||||
if (firstIntersecting) {
|
||||
this.activeSlide = slides.indexOf(firstIntersecting.target as SlCarouselItem);
|
||||
}
|
||||
private isCarouselItem(node: Node): node is SlCarouselItem {
|
||||
return node instanceof Element && node.tagName.toLowerCase() === 'sl-carousel-item';
|
||||
}
|
||||
|
||||
private handleSlotChange = (mutations: MutationRecord[]) => {
|
||||
const needsInitialization = mutations.some(mutation =>
|
||||
[...mutation.addedNodes, ...mutation.removedNodes].some(
|
||||
node => SlCarouselItem.isCarouselItem(node) && !(node as HTMLElement).hasAttribute('data-clone')
|
||||
(el: HTMLElement) => this.isCarouselItem(el) && !el.hasAttribute('data-clone')
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -222,21 +328,15 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
if (needsInitialization) {
|
||||
this.initializeSlides();
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
@watch('loop', { waitUntilFirstUpdate: true })
|
||||
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
|
||||
initializeSlides() {
|
||||
const slides = this.getSlides();
|
||||
const intersectionObserver = this.intersectionObserver;
|
||||
|
||||
this.intersectionObserverEntries.clear();
|
||||
|
||||
// Removes all the cloned elements from the carousel
|
||||
this.getSlides({ excludeClones: false }).forEach((slide, index) => {
|
||||
intersectionObserver.unobserve(slide);
|
||||
|
||||
slide.classList.remove('--in-view');
|
||||
slide.classList.remove('--is-active');
|
||||
slide.setAttribute('aria-label', this.localize.term('slideNum', index + 1));
|
||||
|
@ -246,33 +346,39 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
}
|
||||
});
|
||||
|
||||
this.updateSlidesSnap();
|
||||
|
||||
if (this.loop) {
|
||||
// Creates clones to be placed before and after the original elements to simulate infinite scrolling
|
||||
const slidesPerPage = this.slidesPerPage;
|
||||
const lastSlides = slides.slice(-slidesPerPage);
|
||||
const firstSlides = slides.slice(0, slidesPerPage);
|
||||
|
||||
lastSlides.reverse().forEach((slide, i) => {
|
||||
const clone = slide.cloneNode(true) as HTMLElement;
|
||||
clone.setAttribute('data-clone', String(slides.length - i - 1));
|
||||
this.prepend(clone);
|
||||
});
|
||||
|
||||
firstSlides.forEach((slide, i) => {
|
||||
const clone = slide.cloneNode(true) as HTMLElement;
|
||||
clone.setAttribute('data-clone', String(i));
|
||||
this.append(clone);
|
||||
});
|
||||
this.createClones();
|
||||
}
|
||||
|
||||
this.getSlides({ excludeClones: false }).forEach(slide => {
|
||||
intersectionObserver.observe(slide);
|
||||
});
|
||||
this.synchronizeSlides();
|
||||
|
||||
// Because the DOM may be changed, restore the scroll position to the active slide
|
||||
this.goToSlide(this.activeSlide, 'auto');
|
||||
}
|
||||
|
||||
private createClones() {
|
||||
const slides = this.getSlides();
|
||||
|
||||
const slidesPerPage = this.slidesPerPage;
|
||||
const lastSlides = slides.slice(-slidesPerPage);
|
||||
const firstSlides = slides.slice(0, slidesPerPage);
|
||||
|
||||
lastSlides.reverse().forEach((slide, i) => {
|
||||
const clone = slide.cloneNode(true) as HTMLElement;
|
||||
clone.setAttribute('data-clone', String(slides.length - i - 1));
|
||||
this.prepend(clone);
|
||||
});
|
||||
|
||||
firstSlides.forEach((slide, i) => {
|
||||
const clone = slide.cloneNode(true) as HTMLElement;
|
||||
clone.setAttribute('data-clone', String(i));
|
||||
this.append(clone);
|
||||
});
|
||||
}
|
||||
|
||||
@watch('activeSlide')
|
||||
handelSlideChange() {
|
||||
const slides = this.getSlides();
|
||||
|
@ -292,12 +398,12 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
}
|
||||
|
||||
@watch('slidesPerMove')
|
||||
handleSlidesPerMoveChange() {
|
||||
const slides = this.getSlides({ excludeClones: false });
|
||||
updateSlidesSnap() {
|
||||
const slides = this.getSlides();
|
||||
|
||||
const slidesPerMove = this.slidesPerMove;
|
||||
slides.forEach((slide, i) => {
|
||||
const shouldSnap = Math.abs(i - slidesPerMove) % slidesPerMove === 0;
|
||||
const shouldSnap = (i + slidesPerMove) % slidesPerMove === 0;
|
||||
if (shouldSnap) {
|
||||
slide.style.removeProperty('scroll-snap-align');
|
||||
} else {
|
||||
|
@ -314,26 +420,13 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
}
|
||||
}
|
||||
|
||||
@watch('mouseDragging')
|
||||
handleMouseDraggingChange() {
|
||||
this.scrollController.mouseDragging = this.mouseDragging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the carousel backward by `slides-per-move` slides.
|
||||
*
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
previous(behavior: ScrollBehavior = 'smooth') {
|
||||
let previousIndex = this.activeSlide || this.activeSlide - this.slidesPerMove;
|
||||
let canSnap = false;
|
||||
|
||||
while (!canSnap && previousIndex > 0) {
|
||||
previousIndex -= 1;
|
||||
canSnap = Math.abs(previousIndex - this.slidesPerMove) % this.slidesPerMove === 0;
|
||||
}
|
||||
|
||||
this.goToSlide(previousIndex, behavior);
|
||||
this.goToSlide(this.activeSlide - this.slidesPerMove, behavior);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -352,13 +445,18 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
goToSlide(index: number, behavior: ScrollBehavior = 'smooth') {
|
||||
const { slidesPerPage, loop, scrollContainer } = this;
|
||||
const { slidesPerPage, loop } = this;
|
||||
|
||||
const slides = this.getSlides();
|
||||
const slidesWithClones = this.getSlides({ excludeClones: false });
|
||||
|
||||
// No need to do anything in case there are no items in the carousel
|
||||
if (!slides.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sets the next index without taking into account clones, if any.
|
||||
const newActiveSlide = (index + slides.length) % slides.length;
|
||||
const newActiveSlide = loop ? (index + slides.length) % slides.length : clamp(index, 0, slides.length - 1);
|
||||
this.activeSlide = newActiveSlide;
|
||||
|
||||
// Get the index of the next slide. For looping carousel it adds `slidesPerPage`
|
||||
|
@ -366,23 +464,31 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1);
|
||||
const nextSlide = slidesWithClones[nextSlideIndex];
|
||||
|
||||
this.scrollToSlide(nextSlide, prefersReducedMotion() ? 'auto' : behavior);
|
||||
}
|
||||
|
||||
private scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') {
|
||||
const scrollContainer = this.scrollContainer;
|
||||
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const nextSlideRect = nextSlide.getBoundingClientRect();
|
||||
const nextSlideRect = slide.getBoundingClientRect();
|
||||
|
||||
const nextLeft = nextSlideRect.left - scrollContainerRect.left;
|
||||
const nextTop = nextSlideRect.top - scrollContainerRect.top;
|
||||
|
||||
scrollContainer.scrollTo({
|
||||
left: nextSlideRect.left - scrollContainerRect.left + scrollContainer.scrollLeft,
|
||||
top: nextSlideRect.top - scrollContainerRect.top + scrollContainer.scrollTop,
|
||||
behavior: prefersReducedMotion() ? 'auto' : behavior
|
||||
left: nextLeft + scrollContainer.scrollLeft,
|
||||
top: nextTop + scrollContainer.scrollTop,
|
||||
behavior
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { scrollController, slidesPerPage } = this;
|
||||
const { slidesPerMove, scrolling } = this;
|
||||
const pagesCount = this.getPageCount();
|
||||
const currentPage = this.getCurrentPage();
|
||||
const prevEnabled = this.loop || currentPage > 0;
|
||||
const nextEnabled = this.loop || currentPage < pagesCount - 1;
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const prevEnabled = this.canScrollPrev();
|
||||
const nextEnabled = this.canScrollNext();
|
||||
const isLtr = this.matches(':dir(ltr)');
|
||||
|
||||
return html`
|
||||
<div part="base" class="carousel">
|
||||
|
@ -392,13 +498,16 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
class="${classMap({
|
||||
carousel__slides: true,
|
||||
'carousel__slides--horizontal': this.orientation === 'horizontal',
|
||||
'carousel__slides--vertical': this.orientation === 'vertical'
|
||||
'carousel__slides--vertical': this.orientation === 'vertical',
|
||||
'carousel__slides--dragging': this.dragging
|
||||
})}"
|
||||
style="--slides-per-page: ${this.slidesPerPage};"
|
||||
aria-busy="${scrollController.scrolling ? 'true' : 'false'}"
|
||||
aria-busy="${scrolling ? 'true' : 'false'}"
|
||||
aria-atomic="true"
|
||||
tabindex="0"
|
||||
@keydown=${this.handleKeyDown}
|
||||
@mousedown="${this.handleMouseDragStart}"
|
||||
@scroll="${this.handleScroll}"
|
||||
@scrollend=${this.handleScrollEnd}
|
||||
>
|
||||
<slot></slot>
|
||||
|
@ -459,7 +568,7 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
aria-selected="${isActive ? 'true' : 'false'}"
|
||||
aria-label="${this.localize.term('goToSlide', index + 1, pagesCount)}"
|
||||
tabindex=${isActive ? '0' : '-1'}
|
||||
@click=${() => this.goToSlide(index * slidesPerPage)}
|
||||
@click=${() => this.goToSlide(index * slidesPerMove)}
|
||||
@keydown=${this.handleKeyDown}
|
||||
></button>
|
||||
`;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--slide-gap: var(--sl-spacing-medium, 1rem);
|
||||
--aspect-ratio: 16 / 9;
|
||||
|
@ -79,9 +76,7 @@ export default css`
|
|||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.carousel__slides--dragging,
|
||||
.carousel__slides--dropping {
|
||||
scroll-snap-type: unset;
|
||||
.carousel__slides--dragging {
|
||||
}
|
||||
|
||||
:host([vertical]) ::slotted(sl-carousel-item) {
|
||||
|
|
|
@ -1,10 +1,41 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { clickOnElement } from '../../internal/test.js';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, nextFrame, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { clickOnElement, dragElement, moveMouseOnElement } from '../../internal/test.js';
|
||||
import { map } from 'lit/directives/map.js';
|
||||
import { range } from 'lit/directives/range.js';
|
||||
import { resetMouse } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type { SinonStub } from 'sinon';
|
||||
import type SlCarousel from './carousel.js';
|
||||
|
||||
describe('<sl-carousel>', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
const ioCallbacks = new Map<IntersectionObserver, SinonStub>();
|
||||
const intersectionObserverCallbacks = () => {
|
||||
const callbacks = [...ioCallbacks.values()];
|
||||
return waitUntil(() => callbacks.every(callback => callback.called));
|
||||
};
|
||||
const OriginalIntersectionObserver = globalThis.IntersectionObserver;
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.IntersectionObserver = class IntersectionObserverMock extends OriginalIntersectionObserver {
|
||||
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
|
||||
const stubCallback = sandbox.stub().callsFake(callback);
|
||||
|
||||
super(stubCallback, options);
|
||||
|
||||
ioCallbacks.set(this, stubCallback);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await resetMouse();
|
||||
sandbox.restore();
|
||||
globalThis.IntersectionObserver = OriginalIntersectionObserver;
|
||||
ioCallbacks.clear();
|
||||
});
|
||||
|
||||
it('should render a carousel with default configuration', async () => {
|
||||
// Arrange
|
||||
const el = await fixture(html`
|
||||
|
@ -27,15 +58,11 @@ describe('<sl-carousel>', () => {
|
|||
let clock: sinon.SinonFakeTimers;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers({
|
||||
clock = sandbox.useFakeTimers({
|
||||
now: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('should scroll forwards every `autoplay-interval` milliseconds', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
|
@ -45,7 +72,7 @@ describe('<sl-carousel>', () => {
|
|||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
sinon.stub(el, 'next');
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
|
@ -66,7 +93,7 @@ describe('<sl-carousel>', () => {
|
|||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
sinon.stub(el, 'next');
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
|
@ -89,7 +116,7 @@ describe('<sl-carousel>', () => {
|
|||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
sinon.stub(el, 'next');
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
|
@ -176,7 +203,7 @@ describe('<sl-carousel>', () => {
|
|||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
sinon.stub(el, 'goToSlide');
|
||||
sandbox.stub(el, 'goToSlide');
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
|
@ -223,6 +250,36 @@ describe('<sl-carousel>', () => {
|
|||
// Assert
|
||||
expect(el.scrollContainer.style.getPropertyValue('--slides-per-page').trim()).to.be.equal('2');
|
||||
});
|
||||
|
||||
[
|
||||
[7, 2, 1, false, 6],
|
||||
[5, 3, 3, false, 2],
|
||||
[10, 2, 2, false, 5],
|
||||
[7, 2, 1, true, 7],
|
||||
[5, 3, 3, true, 2],
|
||||
[10, 2, 2, true, 5]
|
||||
].forEach(([slides, slidesPerPage, slidesPerMove, loop, expected]: [number, number, number, boolean, number]) => {
|
||||
it(`should display ${expected} pages for ${slides} slides grouped by ${slidesPerPage} and scrolled by ${slidesPerMove}${
|
||||
loop ? ' (loop)' : ''
|
||||
}`, async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel
|
||||
pagination
|
||||
navigation
|
||||
slides-per-page="${slidesPerPage}"
|
||||
slides-per-move="${slidesPerMove}"
|
||||
?loop=${loop}
|
||||
>
|
||||
${map(range(slides), i => html`<sl-carousel-item>${i}</sl-carousel-item>`)}
|
||||
</sl-carousel>
|
||||
`);
|
||||
|
||||
// Assert
|
||||
const paginationItems = el.shadowRoot!.querySelectorAll('.carousel__pagination-item');
|
||||
expect(paginationItems.length).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `slides-per-move` attribute is provided', () => {
|
||||
|
@ -230,7 +287,7 @@ describe('<sl-carousel>', () => {
|
|||
// Arrange
|
||||
const expectedSnapGranularity = 2;
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel slides-per-move="${expectedSnapGranularity}">
|
||||
<sl-carousel slides-per-page="${expectedSnapGranularity}" slides-per-move="${expectedSnapGranularity}">
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
|
@ -252,6 +309,96 @@ describe('<sl-carousel>', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should be possible to move by the given number of slides at a time', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel navigation slides-per-move="2" slides-per-page="2">
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item class="expected">Node 3</sl-carousel-item>
|
||||
<sl-carousel-item class="expected">Node 4</sl-carousel-item>
|
||||
<sl-carousel-item>Node 5</sl-carousel-item>
|
||||
<sl-carousel-item>Node 6</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
const expectedSlides = el.querySelectorAll('.expected')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
|
||||
// Act
|
||||
await clickOnElement(nextButton);
|
||||
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
for (const expectedSlide of expectedSlides) {
|
||||
expect(expectedSlide).to.have.class('--in-view');
|
||||
expect(expectedSlide).to.be.visible;
|
||||
}
|
||||
});
|
||||
|
||||
it('should be possible to move by a number that is less than the displayed number', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel navigation slides-per-move="1" slides-per-page="2">
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
<sl-carousel-item>Node 4</sl-carousel-item>
|
||||
<sl-carousel-item class="expected">Node 5</sl-carousel-item>
|
||||
<sl-carousel-item class="expected">Node 6</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
const expectedSlides = el.querySelectorAll('.expected')!;
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
|
||||
// Act
|
||||
await clickOnElement(nextButton);
|
||||
await aTimeout(50);
|
||||
await clickOnElement(nextButton);
|
||||
await aTimeout(50);
|
||||
await clickOnElement(nextButton);
|
||||
await aTimeout(50);
|
||||
await clickOnElement(nextButton);
|
||||
await aTimeout(50);
|
||||
await clickOnElement(nextButton);
|
||||
await aTimeout(50);
|
||||
await clickOnElement(nextButton);
|
||||
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
for (const expectedSlide of expectedSlides) {
|
||||
expect(expectedSlide).to.have.class('--in-view');
|
||||
expect(expectedSlide).to.be.visible;
|
||||
}
|
||||
});
|
||||
|
||||
it('should not be possible to move by a number that is greater than the displayed number', async () => {
|
||||
// Arrange
|
||||
const expectedSlidesPerMove = 2;
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel slides-per-page="${expectedSlidesPerMove}">
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
<sl-carousel-item>Node 4</sl-carousel-item>
|
||||
<sl-carousel-item>Node 5</sl-carousel-item>
|
||||
<sl-carousel-item>Node 6</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
|
||||
// Act
|
||||
el.slidesPerMove = 3;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(el.slidesPerMove).to.be.equal(expectedSlidesPerMove);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `orientation` attribute is provided', () => {
|
||||
|
@ -294,6 +441,53 @@ describe('<sl-carousel>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when `mouse-dragging` attribute is provided', () => {
|
||||
// TODO(alenaksu): skipping because failing in webkit, PointerEvent.movementX and PointerEvent.movementY seem to return incorrect values
|
||||
it.skip('should be possible to drag the carousel using the mouse', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel mouse-dragging>
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
|
||||
// Act
|
||||
await dragElement(el, -Math.round(el.offsetWidth * 0.75));
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await dragElement(el, -Math.round(el.offsetWidth * 0.75));
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(el.activeSlide).to.be.equal(2);
|
||||
});
|
||||
|
||||
it('should be possible to interact with clickable elements', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel mouse-dragging>
|
||||
<sl-carousel-item><button>click me</button></sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
const button = el.querySelector('button')!;
|
||||
|
||||
const clickSpy = sinon.spy();
|
||||
button.addEventListener('click', clickSpy);
|
||||
|
||||
// Act
|
||||
await moveMouseOnElement(button);
|
||||
await clickOnElement(button);
|
||||
|
||||
// Assert
|
||||
expect(clickSpy).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation controls', () => {
|
||||
describe('when the user clicks the next button', () => {
|
||||
it('should scroll to the next slide', async () => {
|
||||
|
@ -306,7 +500,7 @@ describe('<sl-carousel>', () => {
|
|||
</sl-carousel>
|
||||
`);
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
sinon.stub(el, 'next');
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
|
@ -329,10 +523,11 @@ describe('<sl-carousel>', () => {
|
|||
</sl-carousel>
|
||||
`);
|
||||
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
|
||||
sinon.stub(el, 'next');
|
||||
sandbox.stub(el, 'next');
|
||||
|
||||
el.goToSlide(2, 'auto');
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
|
@ -368,6 +563,9 @@ describe('<sl-carousel>', () => {
|
|||
// wait scroll to actual item
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(nextButton).to.have.attribute('aria-disabled', 'false');
|
||||
expect(el.activeSlide).to.be.equal(0);
|
||||
|
@ -393,7 +591,7 @@ describe('<sl-carousel>', () => {
|
|||
await el.updateComplete;
|
||||
|
||||
const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!;
|
||||
sinon.stub(el, 'previous');
|
||||
sandbox.stub(el, 'previous');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
|
@ -417,7 +615,7 @@ describe('<sl-carousel>', () => {
|
|||
`);
|
||||
|
||||
const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!;
|
||||
sinon.stub(el, 'previous');
|
||||
sandbox.stub(el, 'previous');
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
|
@ -451,6 +649,8 @@ describe('<sl-carousel>', () => {
|
|||
// wait scroll to actual item
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
|
||||
await intersectionObserverCallbacks();
|
||||
|
||||
// Assert
|
||||
expect(previousButton).to.have.attribute('aria-disabled', 'false');
|
||||
expect(el.activeSlide).to.be.equal(2);
|
||||
|
@ -465,19 +665,27 @@ describe('<sl-carousel>', () => {
|
|||
it('should scroll the carousel to the next slide', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel slides-per-move="2">
|
||||
<sl-carousel>
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
sinon.stub(el, 'goToSlide');
|
||||
await el.updateComplete;
|
||||
sandbox.spy(el, 'goToSlide');
|
||||
const expectedCarouselItem: HTMLElement = el.querySelector('sl-carousel-item:nth-child(2)')!;
|
||||
|
||||
// Act
|
||||
el.next();
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.goToSlide).to.have.been.calledWith(2);
|
||||
const containerRect = el.scrollContainer.getBoundingClientRect();
|
||||
const itemRect = expectedCarouselItem.getBoundingClientRect();
|
||||
|
||||
// Assert
|
||||
expect(el.goToSlide).to.have.been.calledWith(1);
|
||||
expect(itemRect.top).to.be.equal(containerRect.top);
|
||||
expect(itemRect.left).to.be.equal(containerRect.left);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -485,19 +693,34 @@ describe('<sl-carousel>', () => {
|
|||
it('should scroll the carousel to the previous slide', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel slides-per-move="2">
|
||||
<sl-carousel>
|
||||
<sl-carousel-item>Node 1</sl-carousel-item>
|
||||
<sl-carousel-item>Node 2</sl-carousel-item>
|
||||
<sl-carousel-item>Node 3</sl-carousel-item>
|
||||
</sl-carousel>
|
||||
`);
|
||||
sinon.stub(el, 'goToSlide');
|
||||
await el.updateComplete;
|
||||
const expectedCarouselItem: HTMLElement = el.querySelector('sl-carousel-item:nth-child(1)')!;
|
||||
|
||||
el.goToSlide(1);
|
||||
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await nextFrame();
|
||||
|
||||
sandbox.spy(el, 'goToSlide');
|
||||
|
||||
// Act
|
||||
el.previous();
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
|
||||
expect(el.goToSlide).to.have.been.calledWith(-2);
|
||||
const containerRect = el.scrollContainer.getBoundingClientRect();
|
||||
const itemRect = expectedCarouselItem.getBoundingClientRect();
|
||||
|
||||
// Assert
|
||||
expect(el.goToSlide).to.have.been.calledWith(0);
|
||||
expect(itemRect.top).to.be.equal(containerRect.top);
|
||||
expect(itemRect.left).to.be.equal(containerRect.left);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -516,6 +739,7 @@ describe('<sl-carousel>', () => {
|
|||
// Act
|
||||
el.goToSlide(2);
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
import { debounce } from '../../internal/debounce.js';
|
||||
import { prefersReducedMotion } from '../../internal/animate.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import type { ReactiveController, ReactiveElement } from 'lit';
|
||||
|
||||
interface ScrollHost extends ReactiveElement {
|
||||
scrollContainer: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* A controller for handling scrolling and mouse dragging.
|
||||
*/
|
||||
export class ScrollController<T extends ScrollHost> implements ReactiveController {
|
||||
private host: T;
|
||||
private pointers = new Set();
|
||||
|
||||
dragging = false;
|
||||
scrolling = false;
|
||||
mouseDragging = false;
|
||||
|
||||
constructor(host: T) {
|
||||
this.host = host;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
async hostConnected() {
|
||||
const host = this.host;
|
||||
await host.updateComplete;
|
||||
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
scrollContainer.addEventListener('scroll', this.handleScroll, { passive: true });
|
||||
scrollContainer.addEventListener('pointerdown', this.handlePointerDown);
|
||||
scrollContainer.addEventListener('pointerup', this.handlePointerUp);
|
||||
scrollContainer.addEventListener('pointercancel', this.handlePointerUp);
|
||||
scrollContainer.addEventListener('touchstart', this.handleTouchStart, { passive: true });
|
||||
scrollContainer.addEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
|
||||
hostDisconnected(): void {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
scrollContainer.removeEventListener('scroll', this.handleScroll);
|
||||
scrollContainer.removeEventListener('pointerdown', this.handlePointerDown);
|
||||
scrollContainer.removeEventListener('pointerup', this.handlePointerUp);
|
||||
scrollContainer.removeEventListener('pointercancel', this.handlePointerUp);
|
||||
scrollContainer.removeEventListener('touchstart', this.handleTouchStart);
|
||||
scrollContainer.removeEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
|
||||
handleScroll = () => {
|
||||
if (!this.scrolling) {
|
||||
this.scrolling = true;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
this.handleScrollEnd();
|
||||
};
|
||||
|
||||
@debounce(100)
|
||||
handleScrollEnd() {
|
||||
if (!this.pointers.size) {
|
||||
// If no pointer is active in the scroll area then the scroll has ended
|
||||
this.scrolling = false;
|
||||
this.host.scrollContainer.dispatchEvent(
|
||||
new CustomEvent('scrollend', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
})
|
||||
);
|
||||
this.host.requestUpdate();
|
||||
} else {
|
||||
// otherwise let's wait a bit more
|
||||
this.handleScrollEnd();
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerDown = (event: PointerEvent) => {
|
||||
if (event.pointerType === 'touch') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pointers.add(event.pointerId);
|
||||
|
||||
const canDrag = this.mouseDragging && !this.dragging && event.button === 0;
|
||||
if (canDrag) {
|
||||
event.preventDefault();
|
||||
|
||||
this.host.scrollContainer.addEventListener('pointermove', this.handlePointerMove);
|
||||
}
|
||||
};
|
||||
|
||||
handlePointerMove = (event: PointerEvent) => {
|
||||
const scrollContainer = this.host.scrollContainer;
|
||||
|
||||
const hasMoved = !!event.movementX || !!event.movementY;
|
||||
if (!this.dragging && hasMoved) {
|
||||
// Start dragging if it hasn't yet
|
||||
scrollContainer.setPointerCapture(event.pointerId);
|
||||
this.handleDragStart();
|
||||
} else if (scrollContainer.hasPointerCapture(event.pointerId)) {
|
||||
// Ignore pointers that we are not tracking
|
||||
this.handleDrag(event);
|
||||
}
|
||||
};
|
||||
|
||||
handlePointerUp = (event: PointerEvent) => {
|
||||
this.pointers.delete(event.pointerId);
|
||||
this.host.scrollContainer.releasePointerCapture(event.pointerId);
|
||||
|
||||
if (this.pointers.size === 0) {
|
||||
this.handleDragEnd();
|
||||
}
|
||||
};
|
||||
|
||||
handleTouchEnd = (event: TouchEvent) => {
|
||||
for (const touch of event.changedTouches) {
|
||||
this.pointers.delete(touch.identifier);
|
||||
}
|
||||
};
|
||||
|
||||
handleTouchStart = (event: TouchEvent) => {
|
||||
for (const touch of event.touches) {
|
||||
this.pointers.add(touch.identifier);
|
||||
}
|
||||
};
|
||||
|
||||
handleDragStart() {
|
||||
const host = this.host;
|
||||
|
||||
this.dragging = true;
|
||||
host.scrollContainer.style.setProperty('scroll-snap-type', 'unset');
|
||||
host.requestUpdate();
|
||||
}
|
||||
|
||||
handleDrag(event: PointerEvent) {
|
||||
this.host.scrollContainer.scrollBy({
|
||||
left: -event.movementX,
|
||||
top: -event.movementY
|
||||
});
|
||||
}
|
||||
|
||||
async handleDragEnd() {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
scrollContainer.removeEventListener('pointermove', this.handlePointerMove);
|
||||
this.dragging = false;
|
||||
|
||||
const startLeft = scrollContainer.scrollLeft;
|
||||
const startTop = scrollContainer.scrollTop;
|
||||
|
||||
scrollContainer.style.removeProperty('scroll-snap-type');
|
||||
const finalLeft = scrollContainer.scrollLeft;
|
||||
const finalTop = scrollContainer.scrollTop;
|
||||
|
||||
scrollContainer.style.setProperty('scroll-snap-type', 'unset');
|
||||
scrollContainer.scrollTo({ left: startLeft, top: startTop, behavior: 'auto' });
|
||||
scrollContainer.scrollTo({ left: finalLeft, top: finalTop, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
|
||||
|
||||
if (this.scrolling) {
|
||||
await waitForEvent(scrollContainer, 'scrollend');
|
||||
}
|
||||
|
||||
scrollContainer.style.removeProperty('scroll-snap-type');
|
||||
|
||||
host.requestUpdate();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { defaultValue } from '../../internal/default-value.js';
|
||||
import { FormControlController } from '../../internal/form.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import formControlStyles from '../../styles/form-control.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './checkbox.styles.js';
|
||||
|
@ -21,6 +24,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
|||
* @dependency sl-icon
|
||||
*
|
||||
* @slot - The checkbox's label.
|
||||
* @slot help-text - Text that describes how to use the checkbox. Alternatively, you can use the `help-text` attribute.
|
||||
*
|
||||
* @event sl-blur - Emitted when the checkbox loses focus.
|
||||
* @event sl-change - Emitted when the checked state changes.
|
||||
|
@ -35,9 +39,10 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
|||
* @csspart checked-icon - The checked icon, an `<sl-icon>` element.
|
||||
* @csspart indeterminate-icon - The indeterminate icon, an `<sl-icon>` element.
|
||||
* @csspart label - The container that wraps the checkbox's label.
|
||||
* @csspart form-control-help-text - The help text's wrapper.
|
||||
*/
|
||||
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, formControlStyles, styles];
|
||||
static dependencies = { 'sl-icon': SlIcon };
|
||||
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
|
@ -45,6 +50,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
defaultValue: (control: SlCheckbox) => control.defaultChecked,
|
||||
setValue: (control: SlCheckbox, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text');
|
||||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
|
@ -86,6 +92,9 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
/** Makes the checkbox a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** The checkbox's help text. If you need to display HTML, use the `help-text` slot instead. */
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
|
||||
/** Gets the validity state object */
|
||||
get validity() {
|
||||
return this.input.validity;
|
||||
|
@ -178,68 +187,93 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
}
|
||||
|
||||
render() {
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||
|
||||
//
|
||||
// NOTE: we use a <div> around the label slot because of this Chrome bug.
|
||||
//
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1413733
|
||||
//
|
||||
return html`
|
||||
<label
|
||||
part="base"
|
||||
<div
|
||||
class=${classMap({
|
||||
checkbox: true,
|
||||
'checkbox--checked': this.checked,
|
||||
'checkbox--disabled': this.disabled,
|
||||
'checkbox--focused': this.hasFocus,
|
||||
'checkbox--indeterminate': this.indeterminate,
|
||||
'checkbox--small': this.size === 'small',
|
||||
'checkbox--medium': this.size === 'medium',
|
||||
'checkbox--large': this.size === 'large'
|
||||
'form-control': true,
|
||||
'form-control--small': this.size === 'small',
|
||||
'form-control--medium': this.size === 'medium',
|
||||
'form-control--large': this.size === 'large',
|
||||
'form-control--has-help-text': hasHelpText
|
||||
})}
|
||||
>
|
||||
<input
|
||||
class="checkbox__input"
|
||||
type="checkbox"
|
||||
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
|
||||
name=${this.name}
|
||||
value=${ifDefined(this.value)}
|
||||
.indeterminate=${live(this.indeterminate)}
|
||||
.checked=${live(this.checked)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
/>
|
||||
|
||||
<span
|
||||
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate ? ' control--indeterminate' : ''}"
|
||||
class="checkbox__control"
|
||||
<label
|
||||
part="base"
|
||||
class=${classMap({
|
||||
checkbox: true,
|
||||
'checkbox--checked': this.checked,
|
||||
'checkbox--disabled': this.disabled,
|
||||
'checkbox--focused': this.hasFocus,
|
||||
'checkbox--indeterminate': this.indeterminate,
|
||||
'checkbox--small': this.size === 'small',
|
||||
'checkbox--medium': this.size === 'medium',
|
||||
'checkbox--large': this.size === 'large'
|
||||
})}
|
||||
>
|
||||
${this.checked
|
||||
? html`
|
||||
<sl-icon part="checked-icon" class="checkbox__checked-icon" library="system" name="check"></sl-icon>
|
||||
`
|
||||
: ''}
|
||||
${!this.checked && this.indeterminate
|
||||
? html`
|
||||
<sl-icon
|
||||
part="indeterminate-icon"
|
||||
class="checkbox__indeterminate-icon"
|
||||
library="system"
|
||||
name="indeterminate"
|
||||
></sl-icon>
|
||||
`
|
||||
: ''}
|
||||
</span>
|
||||
<input
|
||||
class="checkbox__input"
|
||||
type="checkbox"
|
||||
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
|
||||
name=${this.name}
|
||||
value=${ifDefined(this.value)}
|
||||
.indeterminate=${live(this.indeterminate)}
|
||||
.checked=${live(this.checked)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
aria-describedby="help-text"
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
/>
|
||||
|
||||
<div part="label" class="checkbox__label">
|
||||
<slot></slot>
|
||||
<span
|
||||
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate
|
||||
? ' control--indeterminate'
|
||||
: ''}"
|
||||
class="checkbox__control"
|
||||
>
|
||||
${this.checked
|
||||
? html`
|
||||
<sl-icon part="checked-icon" class="checkbox__checked-icon" library="system" name="check"></sl-icon>
|
||||
`
|
||||
: ''}
|
||||
${!this.checked && this.indeterminate
|
||||
? html`
|
||||
<sl-icon
|
||||
part="indeterminate-icon"
|
||||
class="checkbox__indeterminate-icon"
|
||||
library="system"
|
||||
name="indeterminate"
|
||||
></sl-icon>
|
||||
`
|
||||
: ''}
|
||||
</span>
|
||||
|
||||
<div part="label" class="checkbox__label">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div
|
||||
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
||||
class="form-control__help-text"
|
||||
id="help-text"
|
||||
part="form-control-help-text"
|
||||
>
|
||||
<slot name="help-text">${this.helpText}</slot>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -46,8 +43,11 @@ export default css`
|
|||
border-radius: 2px;
|
||||
background-color: var(--sl-input-background-color);
|
||||
color: var(--sl-color-neutral-0);
|
||||
transition: var(--sl-transition-fast) border-color, var(--sl-transition-fast) background-color,
|
||||
var(--sl-transition-fast) color, var(--sl-transition-fast) box-shadow;
|
||||
transition:
|
||||
var(--sl-transition-fast) border-color,
|
||||
var(--sl-transition-fast) background-color,
|
||||
var(--sl-transition-fast) color,
|
||||
var(--sl-transition-fast) box-shadow;
|
||||
}
|
||||
|
||||
.checkbox__input {
|
||||
|
@ -110,10 +110,12 @@ export default css`
|
|||
line-height: var(--toggle-size);
|
||||
margin-inline-start: 0.5em;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
:host([required]) .checkbox__label::after {
|
||||
content: var(--sl-input-required-content);
|
||||
color: var(--sl-input-required-content-color);
|
||||
margin-inline-start: var(--sl-input-required-content-offset);
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('<sl-checkbox>', () => {
|
|||
expect(el.checked).to.be.false;
|
||||
expect(el.indeterminate).to.be.false;
|
||||
expect(el.defaultChecked).to.be.false;
|
||||
expect(el.helpText).to.equal('');
|
||||
});
|
||||
|
||||
it('should have title if title attribute is set', async () => {
|
||||
|
|
|
@ -2,14 +2,15 @@ import { clamp } from '../../internal/math.js';
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { defaultValue } from '../../internal/default-value.js';
|
||||
import { drag } from '../../internal/drag.js';
|
||||
import { eventOptions, property, query, state } from 'lit/decorators.js';
|
||||
import { FormControlController } from '../../internal/form.js';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlButton from '../button/button.component.js';
|
||||
import SlButtonGroup from '../button-group/button-group.component.js';
|
||||
|
@ -90,7 +91,7 @@ declare const EyeDropper: EyeDropperConstructor;
|
|||
* @cssproperty --swatch-size - The size of each predefined color swatch.
|
||||
*/
|
||||
export default class SlColorPicker extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
static dependencies = {
|
||||
'sl-button-group': SlButtonGroup,
|
||||
|
@ -243,7 +244,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
let initialValue = this.value;
|
||||
let currentValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
@ -253,12 +255,17 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.alpha = clamp((x / width) * 100, 0, 100);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
if (this.value !== currentValue) {
|
||||
currentValue = this.value;
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
onStop: () => {
|
||||
if (this.value !== initialValue) {
|
||||
initialValue = this.value;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
}
|
||||
|
@ -267,7 +274,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
let initialValue = this.value;
|
||||
let currentValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
@ -277,12 +285,17 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.hue = clamp((x / width) * 360, 0, 360);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
if (this.value !== currentValue) {
|
||||
currentValue = this.value;
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
onStop: () => {
|
||||
if (this.value !== initialValue) {
|
||||
initialValue = this.value;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
}
|
||||
|
@ -291,7 +304,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
|
||||
const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
|
||||
const { width, height } = grid.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
let initialValue = this.value;
|
||||
let currentValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
@ -304,13 +318,18 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.brightness = clamp(100 - (y / height) * 100, 0, 100);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
if (this.value !== currentValue) {
|
||||
currentValue = this.value;
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
onStop: () => (this.isDraggingGridHandle = false),
|
||||
onStop: () => {
|
||||
this.isDraggingGridHandle = false;
|
||||
if (this.value !== initialValue) {
|
||||
initialValue = this.value;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
}
|
||||
|
@ -469,6 +488,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
@eventOptions({ passive: false })
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -649,7 +669,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
|
||||
/** Generates a hex string from HSV values. Hue must be 0-360. All other arguments must be 0-100. */
|
||||
private getHexString(hue: number, saturation: number, brightness: number, alpha = 100) {
|
||||
const color = new TinyColor(`hsva(${hue}, ${saturation}, ${brightness}, ${alpha / 100})`);
|
||||
const color = new TinyColor(`hsva(${hue}, ${saturation}%, ${brightness}%, ${alpha / 100})`);
|
||||
if (!color.isValid) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--grid-width: 280px;
|
||||
--grid-height: 200px;
|
||||
|
@ -24,6 +21,7 @@ export default css`
|
|||
background-color: var(--sl-panel-background-color);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.color-picker--inline {
|
||||
|
@ -245,7 +243,11 @@ export default css`
|
|||
linear-gradient(45deg, transparent 75%, var(--sl-color-neutral-300) 75%),
|
||||
linear-gradient(45deg, var(--sl-color-neutral-300) 25%, transparent 25%);
|
||||
background-size: 10px 10px;
|
||||
background-position: 0 0, 0 0, -5px -5px, 5px 5px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 0,
|
||||
-5px -5px,
|
||||
5px 5px;
|
||||
}
|
||||
|
||||
.color-picker--disabled {
|
||||
|
@ -311,7 +313,9 @@ export default css`
|
|||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background-color: currentColor;
|
||||
box-shadow: inset 0 0 0 2px var(--sl-input-border-color), inset 0 0 0 4px var(--sl-color-neutral-0);
|
||||
box-shadow:
|
||||
inset 0 0 0 2px var(--sl-input-border-color),
|
||||
inset 0 0 0 4px var(--sl-color-neutral-0);
|
||||
}
|
||||
|
||||
.color-dropdown__trigger--empty:before {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test.js';
|
||||
import { clickOnElement, dragElement } from '../../internal/test.js';
|
||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form.js';
|
||||
|
@ -31,11 +31,22 @@ describe('<sl-color-picker>', () => {
|
|||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(grid); // click on the grid
|
||||
|
||||
// Simulate a drag event. "sl-change" should not fire until we stop dragging.
|
||||
await dragElement(grid, 2, 0, {
|
||||
afterMouseDown: () => {
|
||||
expect(changeHandler).to.have.not.been.called;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
},
|
||||
afterMouseMove: () => {
|
||||
expect(inputHandler).to.have.been.calledTwice;
|
||||
}
|
||||
});
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when the hue slider is moved', async () => {
|
||||
|
@ -50,10 +61,22 @@ describe('<sl-color-picker>', () => {
|
|||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(slider); // click on the hue slider
|
||||
// Simulate a drag event. "sl-change" should not fire until we stop dragging.
|
||||
await dragElement(slider, 20, 0, {
|
||||
afterMouseDown: () => {
|
||||
expect(changeHandler).to.have.not.been.called;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
},
|
||||
afterMouseMove: () => {
|
||||
// It's not twice because you can't change the hue of white!
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
}
|
||||
});
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
// It's not twice because you can't change the hue of white!
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
|
@ -69,11 +92,22 @@ describe('<sl-color-picker>', () => {
|
|||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(slider); // click on the opacity slider
|
||||
|
||||
// Simulate a drag event. "sl-change" should not fire until we stop dragging.
|
||||
await dragElement(slider, 2, 0, {
|
||||
afterMouseDown: () => {
|
||||
expect(changeHandler).to.have.not.been.called;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
},
|
||||
afterMouseMove: () => {
|
||||
expect(inputHandler).to.have.been.calledTwice;
|
||||
}
|
||||
});
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when toggling the format', async () => {
|
||||
|
@ -97,9 +131,9 @@ describe('<sl-color-picker>', () => {
|
|||
});
|
||||
|
||||
it('should render the correct swatches when passing a string of color values', async () => {
|
||||
const el = await fixture<SlColorPicker>(
|
||||
html` <sl-color-picker swatches="red; #008000; rgb(0,0,255);"></sl-color-picker> `
|
||||
);
|
||||
const el = await fixture<SlColorPicker>(html`
|
||||
<sl-color-picker swatches="red; #008000; rgb(0,0,255);"></sl-color-picker>
|
||||
`);
|
||||
const swatches = [...el.shadowRoot!.querySelectorAll('[part~="swatch"] > div')];
|
||||
|
||||
expect(swatches.length).to.equal(3);
|
||||
|
@ -326,7 +360,7 @@ describe('<sl-color-picker>', () => {
|
|||
expect(previewColor).to.equal('#ff000050');
|
||||
});
|
||||
|
||||
it('should emit sl-focus when rendered as a dropdown and focused', async () => {
|
||||
it.skip('should emit sl-focus when rendered as a dropdown and focused', async () => {
|
||||
const el = await fixture<SlColorPicker>(html`
|
||||
<div>
|
||||
<sl-color-picker></sl-color-picker>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { getAnimation, setDefaultAnimation } from '../../utilities/animation-reg
|
|||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import SlTooltip from '../tooltip/tooltip.component.js';
|
||||
|
@ -41,7 +42,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @animation copy.out - The animation to use when feedback icons animate out.
|
||||
*/
|
||||
export default class SlCopyButton extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = {
|
||||
'sl-icon': SlIcon,
|
||||
'sl-tooltip': SlTooltip
|
||||
|
@ -206,9 +207,9 @@ export default class SlCopyButton extends ShoelaceElement {
|
|||
?disabled=${this.disabled}
|
||||
?hoist=${this.hoist}
|
||||
exportparts="
|
||||
base:tooltip__base
|
||||
base__popup:tooltip__base__popup
|
||||
base__arrow:tooltip__base__arrow
|
||||
base:tooltip__base,
|
||||
base__popup:tooltip__base__popup,
|
||||
base__arrow:tooltip__base__arrow,
|
||||
body:tooltip__body
|
||||
"
|
||||
>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--error-color: var(--sl-color-danger-600);
|
||||
--success-color: var(--sl-color-success-600);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { LocalizeController } from '../../utilities/localize.js';
|
|||
import { property, query } from 'lit/decorators.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './details.styles.js';
|
||||
|
@ -39,7 +40,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @animation details.hide - The animation to use when hiding details. You can use `height: auto` with this animation.
|
||||
*/
|
||||
export default class SlDetails extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
static dependencies = {
|
||||
'sl-icon': SlIcon
|
||||
|
@ -87,6 +88,7 @@ export default class SlDetails extends ShoelaceElement {
|
|||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.detailsObserver.disconnect();
|
||||
}
|
||||
|
||||
|
@ -185,7 +187,7 @@ export default class SlDetails extends ShoelaceElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const isRtl = this.matches(':dir(rtl)');
|
||||
|
||||
return html`
|
||||
<details
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
@ -25,6 +22,7 @@ export default css`
|
|||
border-radius: inherit;
|
||||
padding: var(--sl-spacing-medium);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import '../../../dist/shoelace.js';
|
|||
// cspell:dictionaries lorem-ipsum
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type { SlHideEvent } from '../../events/sl-hide';
|
||||
import type { SlShowEvent } from '../../events/sl-show';
|
||||
import type SlDetails from './details';
|
||||
import type { SlHideEvent } from '../../events/sl-hide.js';
|
||||
import type { SlShowEvent } from '../../events/sl-show.js';
|
||||
import type SlDetails from './details.js';
|
||||
|
||||
describe('<sl-details>', () => {
|
||||
describe('accessibility', () => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js
|
|||
import { property, query } from 'lit/decorators.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import Modal from '../../internal/modal.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIconButton from '../icon-button/icon-button.component.js';
|
||||
|
@ -60,17 +61,22 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @animation dialog.denyClose - The animation to use when a request to close the dialog is denied.
|
||||
* @animation dialog.overlay.show - The animation to use when showing the dialog's overlay.
|
||||
* @animation dialog.overlay.hide - The animation to use when hiding the dialog's overlay.
|
||||
*
|
||||
* @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus
|
||||
* trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when
|
||||
* the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping.
|
||||
*/
|
||||
export default class SlDialog extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = {
|
||||
'sl-icon-button': SlIconButton
|
||||
};
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private modal = new Modal(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
public modal = new Modal(this);
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.dialog') dialog: HTMLElement;
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
|
@ -108,6 +114,7 @@ export default class SlDialog extends ShoelaceElement {
|
|||
super.disconnectedCallback();
|
||||
this.modal.deactivate();
|
||||
unlockBodyScrolling(this);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
|
||||
|
@ -126,10 +133,17 @@ export default class SlDialog extends ShoelaceElement {
|
|||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => this.requestClose('keyboard');
|
||||
} else {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
this.closeWatcher?.destroy();
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
|
@ -296,9 +310,9 @@ export default class SlDialog extends ShoelaceElement {
|
|||
`
|
||||
: ''}
|
||||
${
|
||||
'' /* The tabindex="-1" is here because the body is technically scrollable if overflowing. However, if there's no focusable elements inside, you won't actually be able to scroll it via keyboard. */
|
||||
'' /* The tabindex="-1" is here because the body is technically scrollable if overflowing. However, if there's no focusable elements inside, you won't actually be able to scroll it via keyboard. Previously this was just a <slot>, but tabindex="-1" on the slot causes children to not be focusable. https://github.com/shoelace-style/shoelace/issues/1753#issuecomment-1836803277 */
|
||||
}
|
||||
<slot part="body" class="dialog__body" tabindex="-1"></slot>
|
||||
<div part="body" class="dialog__body" tabindex="-1"><slot></slot></div>
|
||||
|
||||
<footer part="footer" class="dialog__footer">
|
||||
<slot name="footer"></slot>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--width: 31rem;
|
||||
--header-spacing: var(--sl-spacing-large);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
// cspell:dictionaries lorem-ipsum
|
||||
import { aTimeout, elementUpdated, expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { LitElement } from 'lit';
|
||||
import { aTimeout, elementUpdated, expect, fixture, waitUntil } from '@open-wc/testing';
|
||||
import { html, LitElement } from 'lit';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlDialog from './dialog';
|
||||
import type SlDialog from './dialog.js';
|
||||
|
||||
describe('<sl-dialog>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
|
@ -17,9 +17,9 @@ describe('<sl-dialog>', () => {
|
|||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture<SlDialog>(
|
||||
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
|
||||
);
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
|
@ -211,7 +211,7 @@ describe('<sl-dialog>', () => {
|
|||
// Opens modal.
|
||||
const openModalButton = container.shadowRoot?.querySelector('sl-button');
|
||||
|
||||
if (openModalButton) openModalButton.click();
|
||||
openModalButton!.click();
|
||||
|
||||
// Test tab cycling
|
||||
await pressTab();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { property } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './divider.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -15,7 +16,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @cssproperty --spacing - The spacing of the divider.
|
||||
*/
|
||||
export default class SlDivider extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
/** Draws the divider in a vertical orientation. */
|
||||
@property({ type: Boolean, reflect: true }) vertical = false;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--color: var(--sl-panel-border-color);
|
||||
--width: var(--sl-panel-border-width);
|
||||
|
|
|
@ -10,6 +10,7 @@ import { property, query } from 'lit/decorators.js';
|
|||
import { uppercaseFirstLetter } from '../../internal/string.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import Modal from '../../internal/modal.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIconButton from '../icon-button/icon-button.component.js';
|
||||
|
@ -68,15 +69,20 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @animation drawer.denyClose - The animation to use when a request to close the drawer is denied.
|
||||
* @animation drawer.overlay.show - The animation to use when showing the drawer's overlay.
|
||||
* @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay.
|
||||
*
|
||||
* @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus
|
||||
* trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when
|
||||
* the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping.
|
||||
*/
|
||||
export default class SlDrawer extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-icon-button': SlIconButton };
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private modal = new Modal(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
public modal = new Modal(this);
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.drawer') drawer: HTMLElement;
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
|
@ -125,6 +131,7 @@ export default class SlDrawer extends ShoelaceElement {
|
|||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
unlockBodyScrolling(this);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
|
||||
|
@ -143,11 +150,20 @@ export default class SlDrawer extends ShoelaceElement {
|
|||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
if (!this.contained) {
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => this.requestClose('keyboard');
|
||||
}
|
||||
} else {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
private handleDocumentKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--size: 25rem;
|
||||
--header-spacing: var(--sl-spacing-large);
|
||||
|
|
|
@ -3,7 +3,7 @@ import '../../../dist/shoelace.js';
|
|||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlDrawer from './drawer';
|
||||
import type SlDrawer from './drawer.js';
|
||||
|
||||
describe('<sl-drawer>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
|
@ -16,9 +16,9 @@ describe('<sl-drawer>', () => {
|
|||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture<SlDrawer>(
|
||||
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
|
||||
);
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
|
|
|
@ -3,10 +3,12 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
|
||||
import { getTabbableBoundary } from '../../internal/tabbable.js';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlPopup from '../popup/popup.component.js';
|
||||
import styles from './dropdown.styles.js';
|
||||
|
@ -40,7 +42,7 @@ import type SlMenu from '../menu/menu.js';
|
|||
* @animation dropdown.hide - The animation to use when hiding the dropdown.
|
||||
*/
|
||||
export default class SlDropdown extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-popup': SlPopup };
|
||||
|
||||
@query('.dropdown') popup: SlPopup;
|
||||
|
@ -48,6 +50,7 @@ export default class SlDropdown extends ShoelaceElement {
|
|||
@query('.dropdown__panel') panel: HTMLSlotElement;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
|
||||
|
@ -100,6 +103,11 @@ export default class SlDropdown extends ShoelaceElement {
|
|||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
/**
|
||||
* Syncs the popup width or height to that of the trigger element.
|
||||
*/
|
||||
@property({ reflect: true }) sync: 'width' | 'height' | 'both' | undefined = undefined;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
|
@ -149,7 +157,7 @@ export default class SlDropdown extends ShoelaceElement {
|
|||
|
||||
private handleDocumentKeyDown = (event: KeyboardEvent) => {
|
||||
// Close when escape or tab is pressed
|
||||
if (event.key === 'Escape' && this.open) {
|
||||
if (event.key === 'Escape' && this.open && !this.closeWatcher) {
|
||||
event.stopPropagation();
|
||||
this.focusOnTrigger();
|
||||
this.hide();
|
||||
|
@ -334,7 +342,16 @@ export default class SlDropdown extends ShoelaceElement {
|
|||
|
||||
addOpenListeners() {
|
||||
this.panel.addEventListener('sl-select', this.handlePanelSelect);
|
||||
this.panel.addEventListener('keydown', this.handleKeyDown);
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.hide();
|
||||
this.focusOnTrigger();
|
||||
};
|
||||
} else {
|
||||
this.panel.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
|
@ -346,6 +363,7 @@ export default class SlDropdown extends ShoelaceElement {
|
|||
}
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
|
@ -397,6 +415,7 @@ export default class SlDropdown extends ShoelaceElement {
|
|||
shift
|
||||
auto-size="vertical"
|
||||
auto-size-padding="10"
|
||||
sync=${ifDefined(this.sync ? this.sync : undefined)}
|
||||
class=${classMap({
|
||||
dropdown: true,
|
||||
'dropdown--open': this.open
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
|
@ -354,27 +354,4 @@ describe('<sl-dropdown>', () => {
|
|||
|
||||
expect(el.open).to.be.false;
|
||||
});
|
||||
|
||||
it('should close and stop propagating the keydown event when Escape is pressed and the dropdown is open ', async () => {
|
||||
const el = await fixture<SlDropdown>(html`
|
||||
<sl-dropdown open>
|
||||
<sl-button slot="trigger" caret>Toggle</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-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const firstMenuItem = el.querySelector('sl-menu-item')!;
|
||||
const hideHandler = sinon.spy();
|
||||
|
||||
document.body.addEventListener('keydown', hideHandler);
|
||||
firstMenuItem.focus();
|
||||
await sendKeys({ press: 'Escape' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
expect(hideHandler).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlFormatBytes from './format-bytes';
|
||||
import type SlFormatBytes from './format-bytes.js';
|
||||
|
||||
describe('<sl-format-bytes>', () => {
|
||||
describe('defaults ', () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlFormatDate from './format-date';
|
||||
import type SlFormatDate from './format-date.js';
|
||||
|
||||
describe('<sl-format-date>', () => {
|
||||
describe('defaults ', () => {
|
||||
|
@ -52,11 +52,9 @@ describe('<sl-format-date>', () => {
|
|||
];
|
||||
results.forEach(setup => {
|
||||
it(`date has correct language format: ${setup.lang}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" lang="${setup.lang}"></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" lang="${setup.lang}"></sl-format-date>
|
||||
`);
|
||||
expect(el.shadowRoot?.textContent?.trim()).to.equal(setup.result);
|
||||
});
|
||||
});
|
||||
|
@ -66,14 +64,12 @@ describe('<sl-format-date>', () => {
|
|||
const weekdays = ['narrow', 'short', 'long'];
|
||||
weekdays.forEach((weekdayFormat: 'narrow' | 'short' | 'long') => {
|
||||
it(`date has correct weekday format: ${weekdayFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
weekday="${weekdayFormat}"
|
||||
></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
weekday="${weekdayFormat}"
|
||||
></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { weekday: weekdayFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -87,11 +83,9 @@ describe('<sl-format-date>', () => {
|
|||
const eras = ['narrow', 'short', 'long'];
|
||||
eras.forEach((eraFormat: 'narrow' | 'short' | 'long') => {
|
||||
it(`date has correct era format: ${eraFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" era="${eraFormat}"></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" era="${eraFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { era: eraFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -105,11 +99,9 @@ describe('<sl-format-date>', () => {
|
|||
const yearFormats = ['numeric', '2-digit'];
|
||||
yearFormats.forEach((yearFormat: 'numeric' | '2-digit') => {
|
||||
it(`date has correct year format: ${yearFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" year="${yearFormat}"></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" year="${yearFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { year: yearFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -123,11 +115,9 @@ describe('<sl-format-date>', () => {
|
|||
const monthFormats = ['numeric', '2-digit', 'narrow', 'short', 'long'];
|
||||
monthFormats.forEach((monthFormat: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long') => {
|
||||
it(`date has correct month format: ${monthFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" month="${monthFormat}"></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" month="${monthFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { month: monthFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -141,11 +131,9 @@ describe('<sl-format-date>', () => {
|
|||
const dayFormats = ['numeric', '2-digit'];
|
||||
dayFormats.forEach((dayFormat: 'numeric' | '2-digit') => {
|
||||
it(`date has correct day format: ${dayFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" day="${dayFormat}"></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" day="${dayFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { day: dayFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -159,11 +147,9 @@ describe('<sl-format-date>', () => {
|
|||
const hourFormats = ['numeric', '2-digit'];
|
||||
hourFormats.forEach((hourFormat: 'numeric' | '2-digit') => {
|
||||
it(`date has correct hour format: ${hourFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" hour="${hourFormat}"></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" hour="${hourFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { hour: hourFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -177,14 +163,9 @@ describe('<sl-format-date>', () => {
|
|||
const minuteFormats = ['numeric', '2-digit'];
|
||||
minuteFormats.forEach((minuteFormat: 'numeric' | '2-digit') => {
|
||||
it(`date has correct minute format: ${minuteFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
minute="${minuteFormat}"
|
||||
></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" minute="${minuteFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { minute: minuteFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -198,14 +179,9 @@ describe('<sl-format-date>', () => {
|
|||
const secondFormats = ['numeric', '2-digit'];
|
||||
secondFormats.forEach((secondFormat: 'numeric' | '2-digit') => {
|
||||
it(`date has correct second format: ${secondFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
second="${secondFormat}"
|
||||
></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" second="${secondFormat}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { second: secondFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -219,14 +195,12 @@ describe('<sl-format-date>', () => {
|
|||
const timeZoneNameFormats = ['short', 'long'];
|
||||
timeZoneNameFormats.forEach((timeZoneNameFormat: 'short' | 'long') => {
|
||||
it(`date has correct timeZoneName format: ${timeZoneNameFormat}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
time-zone-name="${timeZoneNameFormat}"
|
||||
></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
time-zone-name="${timeZoneNameFormat}"
|
||||
></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { timeZoneName: timeZoneNameFormat }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -240,14 +214,9 @@ describe('<sl-format-date>', () => {
|
|||
const timeZones = ['America/New_York', 'America/Los_Angeles', 'Europe/Zurich'];
|
||||
timeZones.forEach(timeZone => {
|
||||
it(`date has correct timeZoneName format: ${timeZone}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
time-zone="${timeZone}"
|
||||
></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" time-zone="${timeZone}"></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', { timeZone: timeZone }).format(
|
||||
new Date(new Date().getFullYear(), 0, 1)
|
||||
|
@ -261,14 +230,12 @@ describe('<sl-format-date>', () => {
|
|||
const hourFormatValues = ['auto', '12', '24'];
|
||||
hourFormatValues.forEach(hourFormatValue => {
|
||||
it(`date has correct hourFormat format: ${hourFormatValue}`, async () => {
|
||||
const el = await fixture<SlFormatDate>(
|
||||
html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
hour-format="${hourFormatValue as 'auto' | '12' | '24'}"
|
||||
></sl-format-date>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatDate>(html`
|
||||
<sl-format-date
|
||||
.date="${new Date(new Date().getFullYear(), 0, 1)}"
|
||||
hour-format="${hourFormatValue as 'auto' | '12' | '24'}"
|
||||
></sl-format-date>
|
||||
`);
|
||||
|
||||
const expected = new Intl.DateTimeFormat('en-US', {
|
||||
hour12: hourFormatValue === 'auto' ? undefined : hourFormatValue === '12'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlFormatNumber from './format-number';
|
||||
import type SlFormatNumber from './format-number.js';
|
||||
|
||||
describe('<sl-format-number>', () => {
|
||||
describe('defaults ', () => {
|
||||
|
@ -24,9 +24,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('lang property', () => {
|
||||
['de', 'de-CH', 'fr', 'es', 'he', 'ja', 'nl', 'pl', 'pt', 'ru'].forEach(lang => {
|
||||
it(`number has correct language format: ${lang}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" lang="${lang}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" lang="${lang}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat(lang, { style: 'decimal', useGrouping: true }).format(1000);
|
||||
expect(el.shadowRoot?.textContent).to.equal(expected);
|
||||
});
|
||||
|
@ -36,9 +36,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('type property', () => {
|
||||
['currency', 'decimal', 'percent'].forEach(type => {
|
||||
it(`number has correct type format: ${type}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" type="${type}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" type="${type}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', { style: type, currency: 'USD' }).format(1000);
|
||||
expect(el.shadowRoot?.textContent).to.equal(expected);
|
||||
});
|
||||
|
@ -62,9 +62,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('currency property', () => {
|
||||
['USD', 'CAD', 'AUD', 'UAH'].forEach(currency => {
|
||||
it(`number has correct type format: ${currency}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" currency="${currency}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" currency="${currency}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', { style: 'decimal', currency: currency }).format(1000);
|
||||
expect(el.shadowRoot?.textContent).to.equal(expected);
|
||||
});
|
||||
|
@ -74,9 +74,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('currencyDisplay property', () => {
|
||||
['symbol', 'narrowSymbol', 'code', 'name'].forEach(currencyDisplay => {
|
||||
it(`number has correct type format: ${currencyDisplay}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" currency-display="${currencyDisplay}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" currency-display="${currencyDisplay}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', { style: 'decimal', currencyDisplay: currencyDisplay }).format(
|
||||
1000
|
||||
);
|
||||
|
@ -88,9 +88,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('minimumIntegerDigits property', () => {
|
||||
[4, 5, 6].forEach(minDigits => {
|
||||
it(`number has correct type format: ${minDigits}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" minimum-integer-digits="${minDigits}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" minimum-integer-digits="${minDigits}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', {
|
||||
style: 'decimal',
|
||||
currencyDisplay: 'symbol',
|
||||
|
@ -104,9 +104,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('minimumFractionDigits property', () => {
|
||||
[4, 5, 6].forEach(minFractionDigits => {
|
||||
it(`number has correct type format: ${minFractionDigits}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" minimum-fraction-digits="${minFractionDigits}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" minimum-fraction-digits="${minFractionDigits}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', {
|
||||
style: 'decimal',
|
||||
currencyDisplay: 'symbol',
|
||||
|
@ -120,9 +120,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('maximumFractionDigits property', () => {
|
||||
[4, 5, 6].forEach(maxFractionDigits => {
|
||||
it(`number has correct type format: ${maxFractionDigits}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html` <sl-format-number value="1000" maximum-fraction-digits="${maxFractionDigits}"></sl-format-number> `
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" maximum-fraction-digits="${maxFractionDigits}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', {
|
||||
style: 'decimal',
|
||||
currencyDisplay: 'symbol',
|
||||
|
@ -136,11 +136,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('minimumSignificantDigits property', () => {
|
||||
[4, 5, 6].forEach(minSignificantDigits => {
|
||||
it(`number has correct type format: ${minSignificantDigits}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html`
|
||||
<sl-format-number value="1000" minimum-significant-digits="${minSignificantDigits}"></sl-format-number>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" minimum-significant-digits="${minSignificantDigits}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', {
|
||||
style: 'decimal',
|
||||
currencyDisplay: 'symbol',
|
||||
|
@ -154,11 +152,9 @@ describe('<sl-format-number>', () => {
|
|||
describe('maximumSignificantDigits property', () => {
|
||||
[4, 5, 6].forEach(maxSignificantDigits => {
|
||||
it(`number has correct type format: ${maxSignificantDigits}`, async () => {
|
||||
const el = await fixture<SlFormatNumber>(
|
||||
html`
|
||||
<sl-format-number value="1000" maximum-significant-digits="${maxSignificantDigits}"></sl-format-number>
|
||||
`
|
||||
);
|
||||
const el = await fixture<SlFormatNumber>(html`
|
||||
<sl-format-number value="1000" maximum-significant-digits="${maxSignificantDigits}"></sl-format-number>
|
||||
`);
|
||||
const expected = new Intl.NumberFormat('en-US', {
|
||||
style: 'decimal',
|
||||
currencyDisplay: 'symbol',
|
||||
|
|
|
@ -2,6 +2,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||
import { html, literal } from 'lit/static-html.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './icon-button.styles.js';
|
||||
|
@ -21,7 +22,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
export default class SlIconButton extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = { 'sl-icon': SlIcon };
|
||||
|
||||
@query('.icon-button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue