kopia lustrzana https://github.com/shoelace-style/shoelace
Porównaj commity
142 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 | |
Mitch Ray | a2b7816010 | |
Mitch Ray | 468b0b9e66 |
|
@ -1,8 +1,6 @@
|
|||
_site
|
||||
.cache
|
||||
.DS_Store
|
||||
package.json
|
||||
package-lock.json
|
||||
cdn
|
||||
dist
|
||||
docs/assets/images/sprite.svg
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
"monospace",
|
||||
"mousedown",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseup",
|
||||
"multiselectable",
|
||||
"nextjs",
|
||||
|
|
|
@ -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,6 +208,7 @@ export default {
|
|||
}
|
||||
]
|
||||
}),
|
||||
|
||||
customElementJetBrainsPlugin({
|
||||
outdir: './dist',
|
||||
excludeCss: true,
|
||||
|
@ -212,6 +219,12 @@ export default {
|
|||
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;
|
||||
};
|
||||
|
|
|
@ -1059,7 +1059,6 @@ html.sidebar-open #menu-toggle {
|
|||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: 250ms scale ease;
|
||||
}
|
||||
|
||||
#theme-selector:not(:defined) {
|
||||
|
@ -1102,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;
|
||||
|
@ -1420,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}`;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
@ -126,7 +114,7 @@ Are you using Shoelace with Vue? [Help us improve this page!](https://github.com
|
|||
|
||||
### 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:
|
||||
|
||||
|
|
|
@ -12,6 +12,81 @@ 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]
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.12.0",
|
||||
"version": "2.15.1",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
|
@ -49,6 +49,7 @@
|
|||
"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 --log-level=warn .",
|
||||
"prettier:check": "prettier --check --log-level=warn .",
|
||||
|
@ -96,6 +97,7 @@
|
|||
"cspell": "^6.18.1",
|
||||
"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.19.4",
|
||||
|
|
|
@ -25,10 +25,10 @@ for await (const component of components) {
|
|||
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 '../../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 '../../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/react';` : ``;
|
||||
const events = (component.events || [])
|
||||
|
|
|
@ -47,9 +47,19 @@ files.forEach(async 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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,7 +39,7 @@ 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
|
||||
|
|
|
@ -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;
|
||||
|
@ -549,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)
|
||||
)
|
||||
|
@ -587,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;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,7 +16,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
*
|
||||
*/
|
||||
export default class SlCarouselItem extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -3,14 +3,15 @@ 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 SlIcon from '../icon/icon.component.js';
|
||||
import styles from './carousel.styles.js';
|
||||
|
@ -47,7 +48,7 @@ import type SlCarouselItem from '../carousel-item/carousel-item.component.js';
|
|||
* 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. */
|
||||
|
@ -86,11 +87,11 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
// 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 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,35 +99,10 @@ 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();
|
||||
}
|
||||
|
||||
|
@ -177,7 +153,7 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
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');
|
||||
|
@ -216,24 +192,126 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
}
|
||||
}
|
||||
|
||||
private handleScrollEnd() {
|
||||
const slides = this.getSlides();
|
||||
const entries = [...this.intersectionObserverEntries.values()];
|
||||
private handleMouseDragStart(event: PointerEvent) {
|
||||
const canDrag = this.mouseDragging && event.button === 0;
|
||||
if (canDrag) {
|
||||
event.preventDefault();
|
||||
|
||||
const firstIntersecting: IntersectionObserverEntry | undefined = entries.find(entry => entry.isIntersecting);
|
||||
document.addEventListener('pointermove', this.handleMouseDrag, { capture: true, passive: true });
|
||||
document.addEventListener('pointerup', this.handleMouseDragEnd, { capture: true, once: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.loop && firstIntersecting?.target.hasAttribute('data-clone')) {
|
||||
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, 'auto');
|
||||
} else if (firstIntersecting) {
|
||||
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() {
|
||||
if (!this.scrolling || this.dragging) return;
|
||||
|
||||
this.synchronizeSlides();
|
||||
|
||||
this.scrolling = false;
|
||||
}
|
||||
|
||||
private isCarouselItem(node: Node): node is SlCarouselItem {
|
||||
return node instanceof Element && node.tagName.toLowerCase() === 'sl-carousel-item';
|
||||
|
@ -257,14 +335,8 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
@watch('loop', { waitUntilFirstUpdate: true })
|
||||
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
|
||||
initializeSlides() {
|
||||
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));
|
||||
|
@ -281,9 +353,7 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
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');
|
||||
|
@ -350,11 +420,6 @@ export default class SlCarousel extends ShoelaceElement {
|
|||
}
|
||||
}
|
||||
|
||||
@watch('mouseDragging')
|
||||
handleMouseDraggingChange() {
|
||||
this.scrollController.mouseDragging = this.mouseDragging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the carousel backward by `slides-per-move` slides.
|
||||
*
|
||||
|
@ -380,7 +445,7 @@ 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 });
|
||||
|
@ -399,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, slidesPerMove } = this;
|
||||
const { slidesPerMove, scrolling } = this;
|
||||
const pagesCount = this.getPageCount();
|
||||
const currentPage = this.getCurrentPage();
|
||||
const prevEnabled = this.canScrollPrev();
|
||||
const nextEnabled = this.canScrollNext();
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const isLtr = this.matches(':dir(ltr)');
|
||||
|
||||
return html`
|
||||
<div part="base" class="carousel">
|
||||
|
@ -425,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>
|
||||
|
|
|
@ -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,12 +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`
|
||||
|
@ -29,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`
|
||||
|
@ -47,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;
|
||||
|
||||
|
@ -68,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;
|
||||
|
||||
|
@ -91,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;
|
||||
|
||||
|
@ -178,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
|
||||
|
@ -304,6 +329,7 @@ describe('<sl-carousel>', () => {
|
|||
await clickOnElement(nextButton);
|
||||
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
@ -330,13 +356,19 @@ describe('<sl-carousel>', () => {
|
|||
|
||||
// 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
|
||||
|
@ -409,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 () => {
|
||||
|
@ -421,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;
|
||||
|
||||
|
@ -444,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
|
||||
|
@ -483,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);
|
||||
|
@ -508,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;
|
||||
|
||||
|
@ -532,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
|
||||
|
@ -566,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);
|
||||
|
@ -580,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-page="2" 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -600,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-page="2" 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -631,6 +739,7 @@ describe('<sl-carousel>', () => {
|
|||
// Act
|
||||
el.goToSlide(2);
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await intersectionObserverCallbacks();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
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;
|
||||
|
||||
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('scrollend', this.handleScrollEnd, true);
|
||||
scrollContainer.addEventListener('pointerdown', this.handlePointerDown);
|
||||
scrollContainer.addEventListener('pointerup', this.handlePointerUp);
|
||||
scrollContainer.addEventListener('pointercancel', this.handlePointerUp);
|
||||
}
|
||||
|
||||
hostDisconnected(): void {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
scrollContainer.removeEventListener('scroll', this.handleScroll);
|
||||
scrollContainer.removeEventListener('scrollend', this.handleScrollEnd, true);
|
||||
scrollContainer.removeEventListener('pointerdown', this.handlePointerDown);
|
||||
scrollContainer.removeEventListener('pointerup', this.handlePointerUp);
|
||||
scrollContainer.removeEventListener('pointercancel', this.handlePointerUp);
|
||||
}
|
||||
|
||||
handleScroll = () => {
|
||||
if (!this.scrolling) {
|
||||
this.scrolling = true;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
handleScrollEnd = () => {
|
||||
if (this.scrolling && !this.dragging) {
|
||||
this.scrolling = false;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
handlePointerDown = (event: PointerEvent) => {
|
||||
// Do not handle drag for touch interactions as scroll is natively supported
|
||||
if (event.pointerType === 'touch') {
|
||||
return;
|
||||
}
|
||||
|
||||
const canDrag = this.mouseDragging && 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.host.scrollContainer.releasePointerCapture(event.pointerId);
|
||||
|
||||
this.handleDragEnd();
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
handleDragEnd() {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
scrollContainer.removeEventListener('pointermove', this.handlePointerMove);
|
||||
|
||||
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' });
|
||||
|
||||
// Wait for scroll to be applied
|
||||
requestAnimationFrame(async () => {
|
||||
if (startLeft !== finalLeft || startTop !== finalTop) {
|
||||
await waitForEvent(scrollContainer, 'scrollend');
|
||||
}
|
||||
|
||||
scrollContainer.style.removeProperty('scroll-snap-type');
|
||||
|
||||
this.dragging = false;
|
||||
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,12 +187,24 @@ 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`
|
||||
<div
|
||||
class=${classMap({
|
||||
'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
|
||||
})}
|
||||
>
|
||||
<label
|
||||
part="base"
|
||||
class=${classMap({
|
||||
|
@ -208,6 +229,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
.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}
|
||||
|
@ -216,7 +238,9 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
/>
|
||||
|
||||
<span
|
||||
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate ? ' control--indeterminate' : ''}"
|
||||
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate
|
||||
? ' control--indeterminate'
|
||||
: ''}"
|
||||
class="checkbox__control"
|
||||
>
|
||||
${this.checked
|
||||
|
@ -240,6 +264,16 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
|||
<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>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -118,6 +115,7 @@ export default css`
|
|||
|
||||
: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,
|
||||
|
@ -487,6 +488,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||
this.formControlController.emitInvalidEvent(event);
|
||||
}
|
||||
|
||||
@eventOptions({ passive: false })
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -667,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;
|
||||
|
|
|
@ -385,8 +385,7 @@ describe('<sl-color-picker>', () => {
|
|||
expect(blurHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
// NOTE: Firefox is failing locally for me even though manual tests show this is working fine
|
||||
it.skip('should emit sl-focus when rendered inline and focused', async () => {
|
||||
it('should emit sl-focus when rendered inline and focused', async () => {
|
||||
const el = await fixture<SlColorPicker>(html`
|
||||
<div>
|
||||
<sl-color-picker inline></sl-color-picker>
|
||||
|
@ -410,8 +409,7 @@ describe('<sl-color-picker>', () => {
|
|||
expect(blurHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
// NOTE: Firefox is failing locally for me even though manual tests show this is working fine
|
||||
it.skip('should focus and blur when calling focus() and blur() and rendered as a dropdown', async () => {
|
||||
it('should focus and blur when calling focus() and blur() and rendered as a dropdown', async () => {
|
||||
const colorPicker = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const focusHandler = sinon.spy();
|
||||
const blurHandler = sinon.spy();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -186,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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -66,7 +67,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* 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
|
||||
};
|
||||
|
@ -75,6 +76,7 @@ export default class SlDialog extends ShoelaceElement {
|
|||
private readonly localize = new LocalizeController(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
public modal = new Modal(this);
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.dialog') dialog: HTMLElement;
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
|
@ -112,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') {
|
||||
|
@ -130,10 +133,17 @@ export default class SlDialog extends ShoelaceElement {
|
|||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -300,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 () => {
|
||||
|
@ -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';
|
||||
|
@ -74,13 +75,14 @@ import type { CSSResultGroup } from 'lit';
|
|||
* 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 originalTrigger: HTMLElement | null;
|
||||
public modal = new Modal(this);
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.drawer') drawer: HTMLElement;
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
|
@ -129,6 +131,7 @@ export default class SlDrawer extends ShoelaceElement {
|
|||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
unlockBodyScrolling(this);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
|
||||
|
@ -147,11 +150,20 @@ export default class SlDrawer extends ShoelaceElement {
|
|||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
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 () => {
|
||||
|
|
|
@ -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);
|
||||
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 ', () => {
|
||||
|
|
|
@ -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 ', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
color: var(--sl-color-neutral-600);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlIconButton from './icon-button';
|
||||
import type SlIconButton from './icon-button.js';
|
||||
|
||||
type LinkTarget = '_self' | '_blank' | '_parent' | '_top';
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ import { html } from 'lit';
|
|||
import { isTemplateResult } from 'lit/directive-helpers.js';
|
||||
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 styles from './icon.styles.js';
|
||||
|
||||
import type { CSSResultGroup, HTMLTemplateResult } from 'lit';
|
||||
|
||||
const CACHEABLE_ERROR = Symbol();
|
||||
|
@ -33,7 +33,7 @@ interface IconSource {
|
|||
* @csspart use - The <use> element generated when using `spriteSheet: true`
|
||||
*/
|
||||
export default class SlIcon extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
private initialRender = false;
|
||||
|
||||
|
@ -42,9 +42,21 @@ export default class SlIcon extends ShoelaceElement {
|
|||
let fileData: Response;
|
||||
|
||||
if (library?.spriteSheet) {
|
||||
return html`<svg part="svg">
|
||||
this.svg = html`<svg part="svg">
|
||||
<use part="use" href="${url}"></use>
|
||||
</svg>`;
|
||||
|
||||
// Using a templateResult requires the SVG to be written to the DOM first before we can grab the SVGElement
|
||||
// to be passed to the library's mutator function.
|
||||
await this.updateComplete;
|
||||
|
||||
const svg = this.shadowRoot!.querySelector("[part='svg']")!;
|
||||
|
||||
if (typeof library.mutator === 'function') {
|
||||
library.mutator(svg as SVGElement);
|
||||
}
|
||||
|
||||
return this.svg;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { registerIconLibrary } from '../../../dist/shoelace.js';
|
||||
import type { SlErrorEvent } from '../../events/sl-error';
|
||||
import type { SlLoadEvent } from '../../events/sl-load';
|
||||
import type SlIcon from './icon';
|
||||
import type { SlErrorEvent } from '../../events/sl-error.js';
|
||||
import type { SlLoadEvent } from '../../events/sl-load.js';
|
||||
import type SlIcon from './icon.js';
|
||||
|
||||
const testLibraryIcons = {
|
||||
'test-icon1': `
|
||||
|
@ -204,6 +204,10 @@ describe('<sl-icon>', () => {
|
|||
const rect = use?.getBoundingClientRect();
|
||||
expect(rect?.width).to.equal(0);
|
||||
expect(rect?.width).to.equal(0);
|
||||
|
||||
// Make sure the mutator is applied.
|
||||
// https://github.com/shoelace-style/shoelace/issues/1925
|
||||
expect(svg?.getAttribute('fill')).to.equal('currentColor');
|
||||
});
|
||||
|
||||
// TODO: <use> svg icons don't emit a "load" or "error" event...if we can figure out how to get the event to emit errors.
|
||||
|
|
|
@ -2,10 +2,10 @@ import { clamp } from '../../internal/math.js';
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { drag } from '../../internal/drag.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.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 './image-comparer.styles.js';
|
||||
|
@ -35,11 +35,9 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @cssproperty --handle-size - The size of the compare handle.
|
||||
*/
|
||||
export default class SlImageComparer extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static scopedElement = { 'sl-icon': SlIcon };
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.image-comparer') base: HTMLElement;
|
||||
@query('.image-comparer__handle') handle: HTMLElement;
|
||||
|
||||
|
@ -48,7 +46,7 @@ export default class SlImageComparer extends ShoelaceElement {
|
|||
|
||||
private handleDrag(event: PointerEvent) {
|
||||
const { width } = this.base.getBoundingClientRect();
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const isRtl = this.matches(':dir(rtl)');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
|
@ -62,8 +60,8 @@ export default class SlImageComparer extends ShoelaceElement {
|
|||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const isLtr = this.matches(':dir(ltr)');
|
||||
const isRtl = this.matches(':dir(rtl)');
|
||||
|
||||
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
||||
const incr = event.shiftKey ? 10 : 1;
|
||||
|
@ -95,7 +93,7 @@ export default class SlImageComparer extends ShoelaceElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const isRtl = this.matches(':dir(rtl)');
|
||||
|
||||
return html`
|
||||
<div
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--divider-width: 2px;
|
||||
--handle-size: 2.5rem;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlImageComparer from './image-comparer';
|
||||
import type SlImageComparer from './image-comparer.js';
|
||||
|
||||
describe('<sl-image-comparer>', () => {
|
||||
it('should render a basic before/after', async () => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { html } from 'lit';
|
|||
import { property } from 'lit/decorators.js';
|
||||
import { requestInclude } from './request.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import styles from './include.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -16,7 +17,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @event {{ status: number }} sl-error - Emitted when the included file fails to load due to an error.
|
||||
*/
|
||||
export default class SlInclude extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
/**
|
||||
* The location of the HTML file to include. Be sure you trust the content you are including as it will be executed as
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlInclude from './include';
|
||||
import type SlInclude from './include.js';
|
||||
|
||||
const stubbedFetchResponse: Response = {
|
||||
headers: new Headers(),
|
||||
|
|
|
@ -8,6 +8,8 @@ import { live } from 'lit/directives/live.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 formControlStyles from '../../styles/form-control.styles.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import styles from './input.styles.js';
|
||||
|
@ -49,7 +51,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
|||
* @csspart suffix - The container that wraps the suffix.
|
||||
*/
|
||||
export default class SlInput 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, {
|
||||
|
@ -249,13 +251,16 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
|||
}
|
||||
|
||||
private handleClearClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.value !== '') {
|
||||
this.value = '';
|
||||
this.emit('sl-clear');
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
|
@ -347,10 +352,12 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
|||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: 'select' | 'start' | 'end' | 'preserve'
|
||||
selectMode: 'select' | 'start' | 'end' | 'preserve' = 'preserve'
|
||||
) {
|
||||
// @ts-expect-error - start, end, and selectMode are optional
|
||||
this.input.setRangeText(replacement, start, end, selectMode);
|
||||
const selectionStart = start ?? this.input.selectionStart!;
|
||||
const selectionEnd = end ?? this.input.selectionEnd!;
|
||||
|
||||
this.input.setRangeText(replacement, selectionStart, selectionEnd, selectMode);
|
||||
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
|
@ -489,14 +496,11 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
|||
@blur=${this.handleBlur}
|
||||
/>
|
||||
|
||||
${hasClearIcon
|
||||
${isClearIconVisible
|
||||
? html`
|
||||
<button
|
||||
part="clear-button"
|
||||
class=${classMap({
|
||||
input__clear: true,
|
||||
'input__clear--visible': isClearIconVisible
|
||||
})}
|
||||
class="input__clear"
|
||||
type="button"
|
||||
aria-label=${this.localize.term('clearEntry')}
|
||||
@click=${this.handleClearClick}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
import formControlStyles from '../../styles/form-control.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
${formControlStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
@ -252,10 +247,6 @@ export default css`
|
|||
* Clearable + Password Toggle
|
||||
*/
|
||||
|
||||
.input__clear:not(.input__clear--visible) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.input__clear,
|
||||
.input__password-toggle {
|
||||
display: inline-flex;
|
||||
|
@ -280,10 +271,6 @@ export default css`
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.input--empty .input__clear {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Don't show the browser's password toggle in Edge */
|
||||
::-ms-reveal {
|
||||
display: none;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getFormControls, serialize } from '../../../dist/shoelace.js';
|
|||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
|
||||
import { sendKeys } from '@web/test-runner-commands'; // must come from the same module
|
||||
import sinon from 'sinon';
|
||||
import type SlInput from './input';
|
||||
import type SlInput from './input.js';
|
||||
|
||||
describe('<sl-input>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
|
@ -545,5 +545,17 @@ describe('<sl-input>', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when using the setRangeText() function', () => {
|
||||
it('should set replacement text in the correct location', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input value="test"></sl-input> `);
|
||||
|
||||
el.focus();
|
||||
el.setSelectionRange(1, 3);
|
||||
el.setRangeText('boom');
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal('tboomt'); // cspell:disable-line
|
||||
});
|
||||
});
|
||||
|
||||
runFormControlBaseTests('sl-input');
|
||||
});
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { getTextContent, HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { SubmenuController } from './submenu-controller.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 SlPopup from '../popup/popup.component.js';
|
||||
import SlSpinner from '../spinner/spinner.component.js';
|
||||
import styles from './menu-item.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
|
@ -19,6 +20,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
*
|
||||
* @dependency sl-icon
|
||||
* @dependency sl-popup
|
||||
* @dependency sl-spinner
|
||||
*
|
||||
* @slot - The menu item's label.
|
||||
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
||||
|
@ -30,15 +32,18 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart prefix - The prefix container.
|
||||
* @csspart label - The menu item label.
|
||||
* @csspart suffix - The suffix container.
|
||||
* @csspart spinner - The spinner that shows when the menu item is in the loading state.
|
||||
* @csspart spinner__base - The spinner's base part.
|
||||
* @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
|
||||
*
|
||||
* @cssproperty [--submenu-offset=-2px] - The distance submenus shift to overlap the parent menu.
|
||||
*/
|
||||
export default class SlMenuItem extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
static dependencies = {
|
||||
'sl-icon': SlIcon,
|
||||
'sl-popup': SlPopup
|
||||
'sl-popup': SlPopup,
|
||||
'sl-spinner': SlSpinner
|
||||
};
|
||||
|
||||
private cachedTextLabel: string;
|
||||
|
@ -55,12 +60,14 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||
/** A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */
|
||||
@property() value = '';
|
||||
|
||||
/** Draws the menu item in a loading state. */
|
||||
@property({ type: Boolean, reflect: true }) loading = false;
|
||||
|
||||
/** Draws the menu item in a disabled state, preventing selection. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'submenu');
|
||||
private submenuController: SubmenuController = new SubmenuController(this, this.hasSlotController, this.localize);
|
||||
private submenuController: SubmenuController = new SubmenuController(this, this.hasSlotController);
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
@ -146,7 +153,7 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
const isRtl = this.matches(':dir(rtl)');
|
||||
const isSubmenuExpanded = this.submenuController.isExpanded();
|
||||
|
||||
return html`
|
||||
|
@ -158,6 +165,7 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||
'menu-item--rtl': isRtl,
|
||||
'menu-item--checked': this.checked,
|
||||
'menu-item--disabled': this.disabled,
|
||||
'menu-item--loading': this.loading,
|
||||
'menu-item--has-submenu': this.isSubmenu(),
|
||||
'menu-item--submenu-expanded': isSubmenuExpanded
|
||||
})}
|
||||
|
@ -179,6 +187,7 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||
</span>
|
||||
|
||||
${this.submenuController.renderSubmenu()}
|
||||
${this.loading ? html` <sl-spinner part="spinner" exportparts="base:spinner__base"></sl-spinner> ` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--submenu-offset: -2px;
|
||||
|
||||
/* Private */
|
||||
--safe-triangle-cursor-x: 0;
|
||||
--safe-triangle-cursor-y: 0;
|
||||
--safe-triangle-submenu-start-x: 0;
|
||||
--safe-triangle-submenu-start-y: 0;
|
||||
--safe-triangle-submenu-end-x: 0;
|
||||
--safe-triangle-submenu-end-y: 0;
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@ -46,6 +35,25 @@ export default css`
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.menu-item.menu-item--loading {
|
||||
outline: none;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.menu-item.menu-item--loading *:not(sl-spinner) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menu-item--loading sl-spinner {
|
||||
--indicator-color: currentColor;
|
||||
--track-width: 1px;
|
||||
position: absolute;
|
||||
font-size: 0.75em;
|
||||
top: calc(50% - 0.5em);
|
||||
left: 0.65rem;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-item .menu-item__label {
|
||||
flex: 1 1 auto;
|
||||
display: inline-block;
|
||||
|
@ -83,9 +91,9 @@ export default css`
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
clip-path: polygon(
|
||||
var(--safe-triangle-cursor-x) var(--safe-triangle-cursor-y),
|
||||
var(--safe-triangle-submenu-start-x) var(--safe-triangle-submenu-start-y),
|
||||
var(--safe-triangle-submenu-end-x) var(--safe-triangle-submenu-end-y)
|
||||
var(--safe-triangle-cursor-x, 0) var(--safe-triangle-cursor-y, 0),
|
||||
var(--safe-triangle-submenu-start-x, 0) var(--safe-triangle-submenu-start-y, 0),
|
||||
var(--safe-triangle-submenu-end-x, 0) var(--safe-triangle-submenu-end-y, 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -139,4 +147,9 @@ export default css`
|
|||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(sl-menu) {
|
||||
max-width: var(--auto-size-available-width) !important;
|
||||
max-height: var(--auto-size-available-height) !important;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -2,8 +2,8 @@ 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 { SlSelectEvent } from '../../events/sl-select';
|
||||
import type SlMenuItem from './menu-item';
|
||||
import type { SlSelectEvent } from '../../events/sl-select.js';
|
||||
import type SlMenuItem from './menu-item.js';
|
||||
|
||||
describe('<sl-menu-item>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
|
@ -40,6 +40,7 @@ describe('<sl-menu-item>', () => {
|
|||
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.loading).to.equal(false);
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
});
|
||||
|
||||
|
@ -48,6 +49,13 @@ describe('<sl-menu-item>', () => {
|
|||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
it('should have a spinner present', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item loading>Menu Item Label</sl-menu-item> `);
|
||||
expect(el.shadowRoot!.querySelector('sl-spinner')).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a text label when calling getTextLabel()', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
||||
expect(el.getTextLabel()).to.equal('Test');
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||
import { type HasSlotController } from '../../internal/slot.js';
|
||||
import { html } from 'lit';
|
||||
import { type LocalizeController } from '../../utilities/localize.js';
|
||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
import type SlMenuItem from './menu-item.js';
|
||||
import type SlPopup from '../popup/popup.js';
|
||||
|
@ -15,17 +14,11 @@ export class SubmenuController implements ReactiveController {
|
|||
private isPopupConnected = false;
|
||||
private skidding = 0;
|
||||
private readonly hasSlotController: HasSlotController;
|
||||
private readonly localize: LocalizeController;
|
||||
private readonly submenuOpenDelay = 100;
|
||||
|
||||
constructor(
|
||||
host: ReactiveControllerHost & SlMenuItem,
|
||||
hasSlotController: HasSlotController,
|
||||
localize: LocalizeController
|
||||
) {
|
||||
constructor(host: ReactiveControllerHost & SlMenuItem, hasSlotController: HasSlotController) {
|
||||
(this.host = host).addController(this);
|
||||
this.hasSlotController = hasSlotController;
|
||||
this.localize = localize;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
|
@ -202,8 +195,7 @@ export class SubmenuController implements ReactiveController {
|
|||
private handlePopupReposition = () => {
|
||||
const submenuSlot: HTMLSlotElement | null = this.host.renderRoot.querySelector("slot[name='submenu']");
|
||||
const menu = submenuSlot?.assignedElements({ flatten: true }).filter(el => el.localName === 'sl-menu')[0];
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
|
||||
const isRtl = this.host.matches(':dir(rtl)');
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
@ -229,6 +221,7 @@ export class SubmenuController implements ReactiveController {
|
|||
// newly opened menu.
|
||||
private enableSubmenu(delay = true) {
|
||||
if (delay) {
|
||||
window.clearTimeout(this.enableSubmenuTimer);
|
||||
this.enableSubmenuTimer = window.setTimeout(() => {
|
||||
this.setSubmenuState(true);
|
||||
}, this.submenuOpenDelay);
|
||||
|
@ -238,7 +231,7 @@ export class SubmenuController implements ReactiveController {
|
|||
}
|
||||
|
||||
private disableSubmenu() {
|
||||
clearTimeout(this.enableSubmenuTimer);
|
||||
window.clearTimeout(this.enableSubmenuTimer);
|
||||
this.setSubmenuState(false);
|
||||
}
|
||||
|
||||
|
@ -266,7 +259,7 @@ export class SubmenuController implements ReactiveController {
|
|||
}
|
||||
|
||||
renderSubmenu() {
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const isRtl = this.host.matches(':dir(rtl)');
|
||||
|
||||
// Always render the slot, but conditionally render the outer <sl-popup>
|
||||
if (!this.isConnected) {
|
||||
|
@ -276,12 +269,14 @@ export class SubmenuController implements ReactiveController {
|
|||
return html`
|
||||
<sl-popup
|
||||
${ref(this.popupRef)}
|
||||
placement=${isLtr ? 'right-start' : 'left-start'}
|
||||
placement=${isRtl ? 'left-start' : 'right-start'}
|
||||
anchor="anchor"
|
||||
flip
|
||||
flip-fallback-strategy="best-fit"
|
||||
skidding="${this.skidding}"
|
||||
strategy="fixed"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="10"
|
||||
>
|
||||
<slot name="submenu"></slot>
|
||||
</sl-popup>
|
||||
|
|
|
@ -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 './menu-label.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
@ -14,7 +15,7 @@ import type { CSSResultGroup } from 'lit';
|
|||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
export default class SlMenuLabel extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static styles: CSSResultGroup = [componentStyles, styles];
|
||||
|
||||
render() {
|
||||
return html` <slot part="base" class="menu-label"></slot> `;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue