mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df25f8617b | ||
|
|
ad2099a27f | ||
|
|
708127f96d | ||
|
|
9deb51e95a | ||
|
|
67852ea657 | ||
|
|
7240f4f8f4 | ||
|
|
17ee89a5e8 | ||
|
|
f2177dccaf | ||
|
|
6aaf17b81a | ||
|
|
d113d13792 | ||
|
|
ab9cb5f185 | ||
|
|
76fd7aa28d | ||
|
|
8f17bf4e9d | ||
|
|
0f0f71af9b | ||
|
|
e624701022 | ||
|
|
4cedfc3201 | ||
|
|
d88d9fc81a | ||
|
|
051baa4ff5 | ||
|
|
57c3d7009b | ||
|
|
a27fd4d2e9 | ||
|
|
79ac425e2b | ||
|
|
857f318f9c | ||
|
|
c0966bf767 | ||
|
|
86cecc9e30 | ||
|
|
ec036d8e61 | ||
|
|
77b25f4581 | ||
|
|
a8d59b3329 | ||
|
|
2371c5490f | ||
|
|
a127b8722e | ||
|
|
9c573fb454 | ||
|
|
a346d18930 | ||
|
|
a32488baeb | ||
|
|
a4131caeda | ||
|
|
6c62a4f4c0 |
@@ -9,6 +9,9 @@
|
||||
"atrule",
|
||||
"autocorrect",
|
||||
"autofix",
|
||||
"autoload",
|
||||
"autoloader",
|
||||
"autoloading",
|
||||
"autoplay",
|
||||
"bezier",
|
||||
"boxicons",
|
||||
@@ -36,6 +39,7 @@
|
||||
"datetime",
|
||||
"describedby",
|
||||
"Docsify",
|
||||
"dogfood",
|
||||
"dropdowns",
|
||||
"easings",
|
||||
"enterkeyhint",
|
||||
|
||||
@@ -436,6 +436,9 @@
|
||||
result += `
|
||||
## Importing
|
||||
|
||||
If you're using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to
|
||||
use any of the following snippets to [cherry pick](getting-started/installation#cherry-picking) this component.
|
||||
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="script">Script</sl-tab>
|
||||
<sl-tab slot="nav" panel="import">Import</sl-tab>
|
||||
|
||||
@@ -8,6 +8,17 @@ html {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
/* Show custom elements only after they're registered */
|
||||
:not(:defined),
|
||||
:not(:defined) * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:defined {
|
||||
opacity: 1;
|
||||
transition: 0.1s opacity;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
|
||||
@@ -533,7 +533,7 @@ const App = () => (
|
||||
|
||||
### Adding and Removing Slides
|
||||
|
||||
The content of the carousel can be changed by appending or removing carousel items. The carousel will update itself automatically.
|
||||
The content of the carousel can be changed by adding or removing carousel items. The carousel will update itself automatically.
|
||||
|
||||
```html preview
|
||||
<sl-carousel class="dynamic-carousel" pagination navigation>
|
||||
@@ -582,12 +582,19 @@ The content of the carousel can be changed by appending or removing carousel ite
|
||||
slide.innerText = `Slide ${dynamicCarousel.children.length + 1}`;
|
||||
slide.style.setProperty('background', `var(--sl-color-${color}-200)`);
|
||||
dynamicCarousel.appendChild(slide);
|
||||
dynamicRemove.disabled = false;
|
||||
};
|
||||
|
||||
const removeSlide = () => {
|
||||
const slide = dynamicCarousel.children[dynamicCarousel.children.length - 1];
|
||||
slide.remove();
|
||||
colorIndex--;
|
||||
const numSlides = dynamicCarousel.querySelectorAll('sl-carousel-item').length;
|
||||
|
||||
if (numSlides > 1) {
|
||||
slide.remove();
|
||||
colorIndex--;
|
||||
}
|
||||
|
||||
dynamicRemove.disabled = numSlides - 1 <= 1;
|
||||
};
|
||||
|
||||
dynamicAdd.addEventListener('click', addSlide);
|
||||
@@ -656,9 +663,7 @@ const App = () => {
|
||||
|
||||
### Vertical Scrolling
|
||||
|
||||
Setting the `orientation` attribute to `vertical`, will make the carousel laying out vertically, making it
|
||||
possible for the user to scroll it up and down. In case of heterogeneous content, for example images of different sizes,
|
||||
it's important to specify a predefined height to the carousel through CSS.
|
||||
Setting the `orientation` attribute to `vertical` will render the carousel in a vertical layout. If the content of your slides vary in height, you will need to set amn explicit `height` or `max-height` on the carousel using CSS.
|
||||
|
||||
```html preview
|
||||
<sl-carousel class="vertical" pagination orientation="vertical">
|
||||
@@ -895,7 +900,7 @@ const App = () => {
|
||||
|
||||
### Scroll Hint
|
||||
|
||||
Use the `--scroll-hint` attribute to add inline padding in horizontal carousels and block padding in vertical carousels. Setting a padding will make the closest slides slightly visible, hinting that there are more items in the carousel.
|
||||
Use the `--scroll-hint` custom property to add inline padding in horizontal carousels and block padding in vertical carousels. This will make the closest slides slightly visible, hinting that there are more items in the carousel.
|
||||
|
||||
```html preview
|
||||
<sl-carousel class="scroll-hint" pagination style="--scroll-hint: 10%;">
|
||||
|
||||
@@ -100,7 +100,8 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
|
||||
const errorMessage = `Don't forget to check me!`;
|
||||
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-checkbox').then(() => {
|
||||
customElements.whenDefined('sl-checkbox').then(async () => {
|
||||
await checkbox.updateComplete;
|
||||
checkbox.setCustomValidity(errorMessage);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ QR codes are useful for providing small pieces of information to users who can q
|
||||
const qrCode = container.querySelector('sl-qr-code');
|
||||
const input = container.querySelector('sl-input');
|
||||
|
||||
input.value = qrCode.value;
|
||||
input.addEventListener('sl-input', () => (qrCode.value = input.value));
|
||||
customElements.whenDefined('sl-qr-code').then(() => {
|
||||
input.value = qrCode.value;
|
||||
input.addEventListener('sl-input', () => (qrCode.value = input.value));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -175,7 +175,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
|
||||
const errorMessage = 'You must choose the last option';
|
||||
|
||||
// Set initial validity as soon as the element is defined
|
||||
customElements.whenDefined('sl-radio-group').then(() => {
|
||||
customElements.whenDefined('sl-radio').then(() => {
|
||||
radioGroup.setCustomValidity(errorMessage);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,32 +1,50 @@
|
||||
# Installation
|
||||
|
||||
You can use Shoelace via CDN or by installing it locally. You can also [cherry pick](#cherry-picking) individual components for faster load times.
|
||||
|
||||
If you're using a framework, make sure to check out the pages for [React](/frameworks/react), [Vue](/frameworks/vue), and [Angular](/frameworks/angular).
|
||||
You can load Shoelace via CDN or by installing it locally. If you're using a framework, make sure to check out the pages for [React](/frameworks/react), [Vue](/frameworks/vue), and [Angular](/frameworks/angular) for additional information.
|
||||
|
||||
## CDN Installation (Easiest)
|
||||
|
||||
The easiest way to install Shoelace is with the CDN. Just add the following tags to your page to get all components and the default light theme.
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="autoloader" active>Autoloader</sl-tab>
|
||||
<sl-tab slot="nav" panel="traditional">Traditional Loader</sl-tab>
|
||||
|
||||
<sl-tab-panel name="autoloader">
|
||||
|
||||
The experimental autoloader is the easiest and most efficient way to use Shoelace. A lightweight script watches the DOM for unregistered Shoelace elements and lazy loads them for you — even if they're added dynamically.
|
||||
|
||||
While convenient, autoloading may lead to a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/). The linked article describes some ways to alleviate it.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace-autoloader.js"></script>
|
||||
```
|
||||
|
||||
</sl-tab-panel>
|
||||
|
||||
<sl-tab-panel name="traditional">
|
||||
|
||||
The traditional CDN loader registers all Shoelace elements up front. Note that, if you're only using a handful of components, it will be much more efficient to stick with the autoloader. However, you can also [cherry pick](#cherry-picking) components if you want to load specific ones up front.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
?> If you're only using a handful of components, it will be more efficient to [cherry pick](#cherry-picking) the ones you need.
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
|
||||
### Dark Theme
|
||||
|
||||
If you prefer to use the [dark theme](/getting-started/themes#dark-theme) instead, use this code and add `<html class="sl-theme-dark">` to the page.
|
||||
The code above will load the light theme. If you want to use the [dark theme](/getting-started/themes#dark-theme) instead, update the stylesheet as shown below and add `<html class="sl-theme-dark">` to your page.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
### Light & Dark Theme
|
||||
|
||||
If you want to load the light or dark theme based on the user's `prefers-color-scheme` setting, use this. The `media` attributes ensure that only the user's preferred theme stylesheet loads and the `onload` attribute sets the appropriate [theme class](/getting-started/themes) on the `<html>` element.
|
||||
If you want to load the light or dark theme based on the user's `prefers-color-scheme` setting, use the stylesheets below. The `media` attributes ensure that only the user's preferred theme stylesheet loads and the `onload` attribute sets the appropriate [theme class](/getting-started/themes) on the `<html>` element.
|
||||
|
||||
```html
|
||||
<link
|
||||
@@ -40,7 +58,6 @@ If you want to load the light or dark theme based on the user's `prefers-color-s
|
||||
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css"
|
||||
onload="document.documentElement.classList.add('sl-theme-dark');"
|
||||
/>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
Now you can [start using Shoelace!](/getting-started/usage)
|
||||
@@ -68,7 +85,7 @@ Alternatively, [you can use a bundler](#bundling).
|
||||
|
||||
## Setting the Base Path
|
||||
|
||||
Some components rely on assets (icons, images, etc.) and Shoelace needs to know where they're located. For convenience, Shoelace will try to auto-detect the correct location based on the script you've loaded it from. This assumes assets are colocated with `shoelace.js` and will "just work" for most users.
|
||||
Some components rely on assets (icons, images, etc.) and Shoelace needs to know where they're located. For convenience, Shoelace will try to auto-detect the correct location based on the script you've loaded it from. This assumes assets are colocated with `shoelace.js` or `shoelace-autoloader.js` and will "just work" for most users.
|
||||
|
||||
However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Shoelace, you'll need to set the base path. You can do this one of two ways.
|
||||
|
||||
@@ -88,9 +105,7 @@ However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Sh
|
||||
|
||||
## Cherry Picking
|
||||
|
||||
The previous approach is the _easiest_ way to load Shoelace, but easy isn't always efficient. You'll incur the full size of the library even if you only use a handful of components. This is convenient for prototyping or if you're using most of the components, but it may result in longer load times in production. To improve this, you can cherry pick the components you need.
|
||||
|
||||
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load components manually.
|
||||
Cherry picking can be done from [the CDN](#cdn-installation-easiest) or your [local installation](#local-installation). This approach will load only the components you need up front, while limiting the number of files the browser has to download. The disadvantage is that you need to import each individual component.
|
||||
|
||||
Here's an example that loads only the button component. Again, if you're not using a module resolver, you'll need to adjust the path to point to the folder Shoelace is in.
|
||||
|
||||
|
||||
@@ -32,9 +32,10 @@ Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
|
||||
|
||||
Add the following code to your page.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace-autoloader.js"></script>
|
||||
```
|
||||
|
||||
Now you have access to all of Shoelace's components! Try adding a button:
|
||||
@@ -43,7 +44,7 @@ Now you have access to all of Shoelace's components! Try adding a button:
|
||||
<sl-button>Click me</sl-button>
|
||||
```
|
||||
|
||||
?> This will load all of Shoelace's components, but you should probably only load the ones you're actually using. To learn how, or for other ways to install Shoelace, refer to the [installation instructions](getting-started/installation).
|
||||
?> This will activate Shoelace's experimental autoloader, which registers components on the fly as you use them. To learn more about it, or for other ways to install Shoelace, refer to the [installation instructions](getting-started/installation).
|
||||
|
||||
## New to Web Components?
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<!-- Import Shoelace -->
|
||||
<link rel="stylesheet" href="/dist/themes/light.css" />
|
||||
<link rel="stylesheet" href="/dist/themes/dark.css" />
|
||||
<script type="module" src="/dist/shoelace.js"></script>
|
||||
<script type="module" src="/dist/shoelace-autoloader.js"></script>
|
||||
</head>
|
||||
<body data-shoelace="/dist">
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
// Set the initial theme to prevent flashing
|
||||
|
||||
@@ -6,11 +6,24 @@ 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).
|
||||
|
||||
## 2.3.0
|
||||
|
||||
- Added an experimental autoloader
|
||||
- Added the `subpath` argument to `getBasePath()` to make it easier to generate full paths to any file
|
||||
- Added `custom-elements.json` to package exports
|
||||
- Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `<sl-select>`
|
||||
- Fixed a bug in `<sl-rating>` that allowed the `sl-change` event to be emitted when disabled [#1220](https://github.com/shoelace-style/shoelace/issues/1220)
|
||||
- Fixed a regression in `<sl-input>` that caused `min` and `max` to stop working when `type="date"` [#1224](https://github.com/shoelace-style/shoelace/issues/1224)
|
||||
- Improved accessibility of `<sl-carousel>` [#1218](https://github.com/shoelace-style/shoelace/pull/1218)
|
||||
- Improved `<sl-option>` so it converts non-string values to strings for convenience [#1226](https://github.com/shoelace-style/shoelace/issues/1226)
|
||||
- Updated the docs to dogfood the autoloader
|
||||
|
||||
## 2.2.0
|
||||
|
||||
- Added TypeScript types to all custom events [#1183](https://github.com/shoelace-style/shoelace/pull/1183)
|
||||
- Added the `svg` part to `<sl-icon>`
|
||||
- Added the `getForm()` method to all form controls [#1180](https://github.com/shoelace-style/shoelace/issues/1180)
|
||||
- Added the experimental carousel component [#851](https://github.com/shoelace-style/shoelace/pull/851)
|
||||
- Fixed a bug in `<sl-select>` that caused the display label to render incorrectly in Chrome after form validation [#1197](https://github.com/shoelace-style/shoelace/discussions/1197)
|
||||
- Fixed a bug in `<sl-input>` that prevented users from applying their own value for `autocapitalize`, `autocomplete`, and `autocorrect` when using `type="password` [#1205](https://github.com/shoelace-style/shoelace/issues/1205)
|
||||
- Fixed a bug in `<sl-tab-group>` that prevented scroll controls from showing when dynamically adding tabs [#1208](https://github.com/shoelace-style/shoelace/issues/1208)
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -14,6 +14,7 @@
|
||||
"types": "./dist/shoelace.d.ts",
|
||||
"import": "./dist/shoelace.js"
|
||||
},
|
||||
"./dist/custom-elements.json": "./dist/custom-elements.json",
|
||||
"./dist/themes/*": "./dist/themes/*",
|
||||
"./dist/components/*": "./dist/components/*",
|
||||
"./dist/utilities/*": "./dist/utilities/*",
|
||||
|
||||
@@ -51,6 +51,8 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
//
|
||||
// The whole shebang
|
||||
'./src/shoelace.ts',
|
||||
// The auto-loader
|
||||
'./src/shoelace-autoloader.ts',
|
||||
// Components
|
||||
...(await globby('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Translations
|
||||
@@ -120,6 +122,22 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
routes: {
|
||||
'/dist': './dist'
|
||||
}
|
||||
},
|
||||
//
|
||||
// Suppress Chrome's document.write() warning
|
||||
//
|
||||
// More info: https://github.com/BrowserSync/browser-sync/issues/1600)
|
||||
//
|
||||
snippetOptions: {
|
||||
rule: {
|
||||
match: /<\/head>/u,
|
||||
fn: (snippet, match) => {
|
||||
const {
|
||||
groups: { src }
|
||||
} = /src='(?<src>[^']+)'/u.exec(snippet);
|
||||
return `<script src="${src}" async></script>${match}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import type SlAvatar from './avatar';
|
||||
|
||||
// The default avatar background just misses AA contrast, but the next step up is way too dark. Since avatars aren't
|
||||
@@ -113,23 +112,20 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('should not render the image when the image fails to load', async () => {
|
||||
const errorHandler = sinon.spy();
|
||||
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar></sl-avatar>`);
|
||||
el.addEventListener('error', errorHandler);
|
||||
el.image = 'bad_image';
|
||||
waitUntil(() => errorHandler.calledOnce);
|
||||
|
||||
await aTimeout(0);
|
||||
|
||||
await waitUntil(() => el.shadowRoot!.querySelector('img') === null);
|
||||
expect(el.shadowRoot!.querySelector('img')).to.be.null;
|
||||
});
|
||||
|
||||
it('should show a valid image after being passed an invalid image initially', async () => {
|
||||
const errorHandler = sinon.spy();
|
||||
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar></sl-avatar>`);
|
||||
el.addEventListener('error', errorHandler);
|
||||
el.image = 'bad_image';
|
||||
waitUntil(() => errorHandler.calledOnce);
|
||||
|
||||
await aTimeout(0);
|
||||
await waitUntil(() => el.shadowRoot!.querySelector('img') === null);
|
||||
|
||||
el.image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
await el.updateComplete;
|
||||
|
||||
@@ -25,8 +25,7 @@ export default class SlCarouselItem extends ShoelaceElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'listitem');
|
||||
this.setAttribute('aria-roledescription', 'slide');
|
||||
this.setAttribute('role', 'group');
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -8,6 +8,7 @@ export class AutoplayController implements ReactiveController {
|
||||
private host: ReactiveElement;
|
||||
private timerId = 0;
|
||||
private tickCallback: () => void;
|
||||
private activeInteractions = 0;
|
||||
|
||||
paused = false;
|
||||
stopped = true;
|
||||
@@ -57,12 +58,16 @@ export class AutoplayController implements ReactiveController {
|
||||
}
|
||||
|
||||
pause = () => {
|
||||
this.paused = true;
|
||||
this.host.requestUpdate();
|
||||
if (!this.activeInteractions++) {
|
||||
this.paused = true;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
resume = () => {
|
||||
this.paused = false;
|
||||
this.host.requestUpdate();
|
||||
if (!--this.activeInteractions) {
|
||||
this.paused = false;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ export default css`
|
||||
|
||||
.carousel__pagination {
|
||||
grid-area: pagination;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--sl-spacing-small);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export default css`
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: none;
|
||||
aspect-ratio: calc(var(--aspect-ratio) * var(--slides-per-page));
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
|
||||
--slide-size: calc((100% - (var(--slides-per-page) - 1) * var(--slide-gap)) / var(--slides-per-page));
|
||||
}
|
||||
@@ -64,6 +65,7 @@ export default css`
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-padding-inline: var(--scroll-hint);
|
||||
padding-inline: var(--scroll-hint);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.carousel__slides--vertical {
|
||||
@@ -74,6 +76,7 @@ export default css`
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-padding-block: var(--scroll-hint);
|
||||
padding-block: var(--scroll-hint);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.carousel__slides--dragging,
|
||||
@@ -101,7 +104,7 @@ export default css`
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
font-size: inherit;
|
||||
color: var(--sl-color-neutral-600);
|
||||
padding: var(--sl-spacing-x-small);
|
||||
@@ -138,12 +141,20 @@ export default css`
|
||||
width: var(--sl-spacing-small);
|
||||
height: var(--sl-spacing-small);
|
||||
background-color: var(--sl-color-neutral-300);
|
||||
will-change: transform;
|
||||
transition: var(--sl-transition-fast) ease-in;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.carousel__pagination-item--active {
|
||||
background-color: var(--sl-color-neutral-600);
|
||||
background-color: var(--sl-color-neutral-700);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
.carousel__slides:focus-visible,
|
||||
.carousel__navigation-button:focus-visible,
|
||||
.carousel__pagination-item:focus-visible {
|
||||
outline: var(--sl-focus-ring);
|
||||
outline-offset: var(--sl-focus-ring-offset);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('<sl-carousel>', () => {
|
||||
// Assert
|
||||
expect(el).to.exist;
|
||||
expect(el).to.have.attribute('role', 'region');
|
||||
expect(el).to.have.attribute('aria-roledescription', 'carousel');
|
||||
expect(el).to.have.attribute('aria-label', 'Carousel');
|
||||
expect(el.shadowRoot!.querySelector('.carousel__navigation')).not.to.exist;
|
||||
expect(el.shadowRoot!.querySelector('.carousel__pagination')).not.to.exist;
|
||||
});
|
||||
@@ -78,6 +78,34 @@ describe('<sl-carousel>', () => {
|
||||
// Assert
|
||||
expect(el.next).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should not resume if the user is still interacting', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel autoplay autoplay-interval="10">
|
||||
<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, 'next');
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
el.dispatchEvent(new Event('mouseenter'));
|
||||
el.dispatchEvent(new Event('focusin'));
|
||||
await el.updateComplete;
|
||||
|
||||
el.dispatchEvent(new Event('mouseleave'));
|
||||
await el.updateComplete;
|
||||
|
||||
clock.next();
|
||||
clock.next();
|
||||
|
||||
// Assert
|
||||
expect(el.next).not.to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `loop` attribute is provided', () => {
|
||||
@@ -511,7 +539,6 @@ describe('<sl-carousel>', () => {
|
||||
|
||||
// Assert
|
||||
expect(el.scrollContainer).to.have.attribute('aria-busy', 'false');
|
||||
expect(el.scrollContainer).to.have.attribute('aria-live', 'polite');
|
||||
expect(el.scrollContainer).to.have.attribute('aria-atomic', 'true');
|
||||
|
||||
expect(pagination).to.have.attribute('role', 'tablist');
|
||||
@@ -557,45 +584,5 @@ describe('<sl-carousel>', () => {
|
||||
expect(el.scrollContainer).to.have.attribute('aria-busy', 'false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when autoplay is active', () => {
|
||||
it('should disable live announcement', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel autoplay>
|
||||
<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>
|
||||
`);
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(el.scrollContainer).to.have.attribute('aria-live', 'off');
|
||||
});
|
||||
|
||||
describe('and user is interacting with the carousel', () => {
|
||||
it('should enable live announcement', async () => {
|
||||
// Arrange
|
||||
const el = await fixture<SlCarousel>(html`
|
||||
<sl-carousel autoplay>
|
||||
<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>
|
||||
`);
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Act
|
||||
el.dispatchEvent(new Event('focusin'));
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(el.scrollContainer).to.have.attribute('aria-live', 'polite');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import { prefersReducedMotion } from '../../internal/animate';
|
||||
import { range } from 'lit/directives/range.js';
|
||||
import { ScrollController } from './scroll-controller';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import SlCarouselItem from '../carousel-item/carousel-item';
|
||||
import styles from './carousel.styles';
|
||||
@@ -98,7 +97,7 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'region');
|
||||
this.setAttribute('aria-roledescription', 'carousel');
|
||||
this.setAttribute('aria-label', this.localize.term('carousel'));
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
@@ -137,63 +136,61 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
this.mutationObserver.observe(this, { childList: true, subtree: false });
|
||||
}
|
||||
|
||||
private getPageCount() {
|
||||
return Math.ceil(this.getSlides().length / this.slidesPerPage);
|
||||
}
|
||||
|
||||
private getCurrentPage() {
|
||||
return Math.floor(this.activeSlide / this.slidesPerPage);
|
||||
}
|
||||
|
||||
private getSlides({ excludeClones = true }: { excludeClones?: boolean } = {}) {
|
||||
return [...this.slides].filter(slide => !excludeClones || !slide.hasAttribute('data-clone'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the carousel backward by `slides-per-move` slides.
|
||||
*
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
previous(behavior: ScrollBehavior = 'smooth') {
|
||||
this.goToSlide(this.activeSlide - this.slidesPerMove, behavior);
|
||||
}
|
||||
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 isFocusInPagination = target.closest('[part~="pagination-item"]') !== null;
|
||||
const isNext =
|
||||
event.key === 'ArrowDown' || (!isRtl && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft');
|
||||
const isPrevious =
|
||||
event.key === 'ArrowUp' || (!isRtl && event.key === 'ArrowLeft') || (isRtl && event.key === 'ArrowRight');
|
||||
|
||||
/**
|
||||
* Move the carousel forward by `slides-per-move` slides.
|
||||
*
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
next(behavior: ScrollBehavior = 'smooth') {
|
||||
this.goToSlide(this.activeSlide + this.slidesPerMove, behavior);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
/**
|
||||
* Scrolls the carousel to the slide specified by `index`.
|
||||
*
|
||||
* @param index - The slide index.
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
goToSlide(index: number, behavior: ScrollBehavior = 'smooth') {
|
||||
const { slidesPerPage, loop } = this;
|
||||
if (isPrevious) {
|
||||
this.previous();
|
||||
}
|
||||
|
||||
const slidesWithClones = this.getSlides({ excludeClones: false });
|
||||
const normalizedIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1);
|
||||
const slide = slidesWithClones[normalizedIndex];
|
||||
if (isNext) {
|
||||
this.next();
|
||||
}
|
||||
|
||||
this.scrollContainer.scrollTo({
|
||||
left: slide.offsetLeft,
|
||||
top: slide.offsetTop,
|
||||
behavior: prefersReducedMotion() ? 'auto' : behavior
|
||||
});
|
||||
}
|
||||
if (event.key === 'Home') {
|
||||
this.goToSlide(0);
|
||||
}
|
||||
|
||||
handleSlotChange(mutations: MutationRecord[]) {
|
||||
const needsInitialization = mutations.some(mutation =>
|
||||
[...mutation.addedNodes, ...mutation.removedNodes].some(
|
||||
node => SlCarouselItem.isCarouselItem(node) && !(node as HTMLElement).hasAttribute('data-clone')
|
||||
)
|
||||
);
|
||||
if (event.key === 'End') {
|
||||
this.goToSlide(this.getSlides().length - 1);
|
||||
}
|
||||
|
||||
// Reinitialize the carousel if a carousel item has been added or removed
|
||||
if (needsInitialization) {
|
||||
this.initializeSlides();
|
||||
this.requestUpdate();
|
||||
if (isFocusInPagination) {
|
||||
this.updateComplete.then(() => {
|
||||
const activePaginationItem = this.shadowRoot?.querySelector<HTMLButtonElement>(
|
||||
'[part~="pagination-item--active"]'
|
||||
);
|
||||
|
||||
if (activePaginationItem) {
|
||||
activePaginationItem.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollEnd() {
|
||||
private handleScrollEnd() {
|
||||
const slides = this.getSlides();
|
||||
const entries = [...this.intersectionObserverEntries.values()];
|
||||
|
||||
@@ -214,6 +211,20 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
private handleSlotChange(mutations: MutationRecord[]) {
|
||||
const needsInitialization = mutations.some(mutation =>
|
||||
[...mutation.addedNodes, ...mutation.removedNodes].some(
|
||||
node => SlCarouselItem.isCarouselItem(node) && !(node as HTMLElement).hasAttribute('data-clone')
|
||||
)
|
||||
);
|
||||
|
||||
// Reinitialize the carousel if a carousel item has been added or removed
|
||||
if (needsInitialization) {
|
||||
this.initializeSlides();
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@watch('loop', { waitUntilFirstUpdate: true })
|
||||
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
|
||||
initializeSlides() {
|
||||
@@ -223,11 +234,12 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
this.intersectionObserverEntries.clear();
|
||||
|
||||
// Removes all the cloned elements from the carousel
|
||||
this.getSlides({ excludeClones: false }).forEach(slide => {
|
||||
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('slide_num', index + 1));
|
||||
|
||||
if (slide.hasAttribute('data-clone')) {
|
||||
slide.remove();
|
||||
@@ -307,85 +319,59 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
this.scrollController.mouseDragging = this.mouseDragging;
|
||||
}
|
||||
|
||||
private renderPagination = () => {
|
||||
/**
|
||||
* Move the carousel backward by `slides-per-move` slides.
|
||||
*
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
previous(behavior: ScrollBehavior = 'smooth') {
|
||||
this.goToSlide(this.activeSlide - this.slidesPerMove, behavior);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the carousel forward by `slides-per-move` slides.
|
||||
*
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
next(behavior: ScrollBehavior = 'smooth') {
|
||||
this.goToSlide(this.activeSlide + this.slidesPerMove, behavior);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the carousel to the slide specified by `index`.
|
||||
*
|
||||
* @param index - The slide index.
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
goToSlide(index: number, behavior: ScrollBehavior = 'smooth') {
|
||||
const { slidesPerPage, loop } = this;
|
||||
|
||||
const slides = this.getSlides();
|
||||
const slidesCount = slides.length;
|
||||
const slidesWithClones = this.getSlides({ excludeClones: false });
|
||||
|
||||
const { activeSlide, slidesPerPage } = this;
|
||||
const pagesCount = Math.ceil(slidesCount / slidesPerPage);
|
||||
const currentPage = Math.floor(activeSlide / slidesPerPage);
|
||||
// Sets the next index without taking into account clones, if any.
|
||||
const newActiveSlide = (index + slides.length) % slides.length;
|
||||
this.activeSlide = newActiveSlide;
|
||||
|
||||
return html`
|
||||
<nav part="pagination" role="tablist" class="carousel__pagination" aria-controls="scroll-container">
|
||||
${map(range(pagesCount), index => {
|
||||
const isActive = index === currentPage;
|
||||
return html`
|
||||
<button
|
||||
part="pagination-item ${isActive ? 'pagination-item--active' : ''}"
|
||||
class="${classMap({
|
||||
'carousel__pagination-item': true,
|
||||
'carousel__pagination-item--active': isActive
|
||||
})}"
|
||||
aria-selected="${isActive ? 'true' : 'false'}"
|
||||
aria-label="${this.localize.term('goToSlide', index + 1, pagesCount)}"
|
||||
role="tab"
|
||||
@click="${() => this.goToSlide(index * slidesPerPage)}"
|
||||
></button>
|
||||
`;
|
||||
})}
|
||||
</nav>
|
||||
`;
|
||||
};
|
||||
// Get the index of the next slide. For looping carousel it adds `slidesPerPage`
|
||||
// to normalize the starting index in order to ignore the first nth clones.
|
||||
const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1);
|
||||
const nextSlide = slidesWithClones[nextSlideIndex];
|
||||
|
||||
private renderNavigation = () => {
|
||||
const { loop, activeSlide } = this;
|
||||
const slides = this.getSlides();
|
||||
const slidesCount = slides.length;
|
||||
const prevEnabled = loop || activeSlide > 0;
|
||||
const nextEnabled = loop || activeSlide < slidesCount - 1;
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
|
||||
return html`
|
||||
<nav part="navigation" class="carousel__navigation">
|
||||
<button
|
||||
@click="${prevEnabled ? () => this.previous() : null}"
|
||||
aria-disabled="${prevEnabled ? 'false' : 'true'}"
|
||||
aria-controls="scroll-container"
|
||||
class="${classMap({
|
||||
'carousel__navigation-button': true,
|
||||
'carousel__navigation-button--previous': true,
|
||||
'carousel__navigation-button--disabled': !prevEnabled
|
||||
})}"
|
||||
aria-label="${this.localize.term('previousSlide')}"
|
||||
part="navigation-button navigation-button--previous"
|
||||
>
|
||||
<slot name="previous-icon">
|
||||
<sl-icon library="system" name="${isLtr ? 'chevron-left' : 'chevron-right'}"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="${nextEnabled ? () => this.next() : null}"
|
||||
aria-disabled="${nextEnabled ? 'false' : 'true'}"
|
||||
aria-controls="scroll-container"
|
||||
class="${classMap({
|
||||
'carousel__navigation-button': true,
|
||||
'carousel__navigation-button--next': true,
|
||||
'carousel__navigation-button--disabled': !nextEnabled
|
||||
})}"
|
||||
aria-label="${this.localize.term('nextSlide')}"
|
||||
part="navigation-button navigation-button--next"
|
||||
>
|
||||
<slot name="next-icon">
|
||||
<sl-icon library="system" name="${isLtr ? 'chevron-right' : 'chevron-left'}"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
};
|
||||
this.scrollContainer.scrollTo({
|
||||
left: nextSlide.offsetLeft,
|
||||
top: nextSlide.offsetTop,
|
||||
behavior: prefersReducedMotion() ? 'auto' : behavior
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { autoplayController, scrollController } = this;
|
||||
const { scrollController, slidesPerPage } = this;
|
||||
const pagesCount = this.getPageCount();
|
||||
const currentPage = this.getCurrentPage();
|
||||
const prevEnabled = this.loop || currentPage > 0;
|
||||
const nextEnabled = this.loop || currentPage < pagesCount - 1;
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
|
||||
return html`
|
||||
<div part="base" class="carousel">
|
||||
@@ -397,18 +383,79 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
'carousel__slides--horizontal': this.orientation === 'horizontal',
|
||||
'carousel__slides--vertical': this.orientation === 'vertical'
|
||||
})}"
|
||||
@scrollend="${this.handleScrollEnd}"
|
||||
role="list"
|
||||
tabindex="0"
|
||||
style="--slides-per-page: ${this.slidesPerPage};"
|
||||
aria-live="${!autoplayController.stopped && !autoplayController.paused ? 'off' : 'polite'}"
|
||||
aria-busy="${scrollController.scrolling ? 'true' : 'false'}"
|
||||
aria-atomic="true"
|
||||
tabindex="0"
|
||||
@keydown=${this.handleKeyDown}
|
||||
@scrollend=${this.handleScrollEnd}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
${when(this.navigation, this.renderNavigation)} ${when(this.pagination, this.renderPagination)}
|
||||
${this.navigation
|
||||
? html`
|
||||
<div part="navigation" class="carousel__navigation">
|
||||
<button
|
||||
part="navigation-button navigation-button--previous"
|
||||
class="${classMap({
|
||||
'carousel__navigation-button': true,
|
||||
'carousel__navigation-button--previous': true,
|
||||
'carousel__navigation-button--disabled': !prevEnabled
|
||||
})}"
|
||||
aria-label="${this.localize.term('previousSlide')}"
|
||||
aria-controls="scroll-container"
|
||||
aria-disabled="${prevEnabled ? 'false' : 'true'}"
|
||||
@click=${prevEnabled ? () => this.previous() : null}
|
||||
>
|
||||
<slot name="previous-icon">
|
||||
<sl-icon library="system" name="${isLtr ? 'chevron-left' : 'chevron-right'}"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<button
|
||||
part="navigation-button navigation-button--next"
|
||||
class=${classMap({
|
||||
'carousel__navigation-button': true,
|
||||
'carousel__navigation-button--next': true,
|
||||
'carousel__navigation-button--disabled': !nextEnabled
|
||||
})}
|
||||
aria-label="${this.localize.term('nextSlide')}"
|
||||
aria-controls="scroll-container"
|
||||
aria-disabled="${nextEnabled ? 'false' : 'true'}"
|
||||
@click=${nextEnabled ? () => this.next() : null}
|
||||
>
|
||||
<slot name="next-icon">
|
||||
<sl-icon library="system" name="${isLtr ? 'chevron-right' : 'chevron-left'}"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this.pagination
|
||||
? html`
|
||||
<div part="pagination" role="tablist" class="carousel__pagination" aria-controls="scroll-container">
|
||||
${map(range(pagesCount), index => {
|
||||
const isActive = index === currentPage;
|
||||
return html`
|
||||
<button
|
||||
part="pagination-item ${isActive ? 'pagination-item--active' : ''}"
|
||||
class="${classMap({
|
||||
'carousel__pagination-item': true,
|
||||
'carousel__pagination-item--active': isActive
|
||||
})}"
|
||||
role="tab"
|
||||
aria-selected="${isActive ? 'true' : 'false'}"
|
||||
aria-label="${this.localize.term('goToSlide', index + 1, pagesCount)}"
|
||||
tabindex=${isActive ? '0' : '-1'}
|
||||
@click=${() => this.goToSlide(index * slidesPerPage)}
|
||||
@keydown=${this.handleKeyDown}
|
||||
></button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
})
|
||||
);
|
||||
this.host.requestUpdate();
|
||||
} else {
|
||||
this.handleScrollEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { IconLibrary } from './library';
|
||||
|
||||
const library: IconLibrary = {
|
||||
name: 'default',
|
||||
resolver: name => `${getBasePath()}/assets/icons/${name}.svg`
|
||||
resolver: name => getBasePath(`assets/icons/${name}.svg`)
|
||||
};
|
||||
|
||||
export default library;
|
||||
|
||||
@@ -144,10 +144,10 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
@property({ type: Number }) maxlength: number;
|
||||
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property({ type: Number }) min: number;
|
||||
@property() min: number | string;
|
||||
|
||||
/** The input's maximum value. Only applies to date and number input types. */
|
||||
@property({ type: Number }) max: number;
|
||||
@property() max: number | string;
|
||||
|
||||
/**
|
||||
* Specifies the granularity that the value must adhere to, or the special value `any` which means no stepping is
|
||||
|
||||
@@ -41,4 +41,14 @@ describe('<sl-option>', () => {
|
||||
|
||||
expect(slotChangeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should convert non-string values to string', async () => {
|
||||
const el = await fixture<SlOption>(html` <sl-option>Text</sl-option> `);
|
||||
|
||||
// @ts-expect-error - intentional
|
||||
el.value = 10;
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal('10');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,12 @@ export default class SlOption extends ShoelaceElement {
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
// Ensure the value is a string. This ensures the next line doesn't error and allows framework users to pass numbers
|
||||
// instead of requiring them to cast the value to a string.
|
||||
if (typeof this.value !== 'string') {
|
||||
this.value = String(this.value);
|
||||
}
|
||||
|
||||
if (this.value.includes(' ')) {
|
||||
console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this);
|
||||
this.value = this.value.replace(/ /g, '_');
|
||||
|
||||
@@ -72,7 +72,7 @@ export default class SlQrCode extends ShoelaceElement {
|
||||
part="base"
|
||||
class="qr-code"
|
||||
role="img"
|
||||
aria-label=${this.label.length > 0 ? this.label : this.value}
|
||||
aria-label=${this.label?.length > 0 ? this.label : this.value}
|
||||
style=${styleMap({
|
||||
width: `${this.size}px`,
|
||||
height: `${this.size}px`
|
||||
|
||||
@@ -197,27 +197,35 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
||||
}
|
||||
|
||||
private handleSlotChange() {
|
||||
const radios = this.getAllRadios();
|
||||
if (customElements.get('sl-radio') || customElements.get('sl-radio-button')) {
|
||||
const radios = this.getAllRadios();
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
|
||||
radios.forEach(radio => (radio.checked = radio.value === this.value));
|
||||
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'sl-radio-button');
|
||||
|
||||
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'sl-radio-button');
|
||||
if (!radios.some(radio => radio.checked)) {
|
||||
if (this.hasButtonGroup) {
|
||||
const buttonRadio = radios[0].shadowRoot?.querySelector('button');
|
||||
|
||||
if (buttonRadio) {
|
||||
buttonRadio.tabIndex = 0;
|
||||
}
|
||||
} else {
|
||||
radios[0].tabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!radios.some(radio => radio.checked)) {
|
||||
if (this.hasButtonGroup) {
|
||||
const buttonRadio = radios[0].shadowRoot!.querySelector('button')!;
|
||||
buttonRadio.tabIndex = 0;
|
||||
} else {
|
||||
radios[0].tabIndex = 0;
|
||||
}
|
||||
}
|
||||
const buttonGroup = this.shadowRoot?.querySelector('sl-button-group');
|
||||
|
||||
if (this.hasButtonGroup) {
|
||||
const buttonGroup = this.shadowRoot?.querySelector('sl-button-group');
|
||||
|
||||
if (buttonGroup) {
|
||||
buttonGroup.disableRole = true;
|
||||
if (buttonGroup) {
|
||||
buttonGroup.disableRole = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Rerun this handler when <sl-radio> or <sl-radio-button> is registered
|
||||
customElements.whenDefined('sl-radio').then(() => this.handleSlotChange());
|
||||
customElements.whenDefined('sl-radio-button').then(() => this.handleSlotChange());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, eventOptions, property, query, state } from 'lit/decorators.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
@@ -156,6 +156,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleThumbDragStart() {
|
||||
this.hasTooltip = true;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,20 @@ describe('<sl-rating>', () => {
|
||||
expect(el.value).to.equal(1);
|
||||
});
|
||||
|
||||
it('should not emit sl-change when disabled', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating value="5" disabled></sl-rating> `);
|
||||
const lastSymbol = el.shadowRoot!.querySelector<HTMLSpanElement>('.rating__symbol:last-child')!;
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
|
||||
await clickOnElement(lastSymbol);
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.not.have.been.called;
|
||||
expect(el.value).to.equal(5);
|
||||
});
|
||||
|
||||
it('should not emit sl-change when the value is changed programmatically', async () => {
|
||||
const el = await fixture<SlRating>(html` <sl-rating label="Test" value="1"></sl-rating> `);
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change incorrectly emitted'));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '../icon/icon';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, eventOptions, property, query, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
@@ -89,6 +89,10 @@ export default class SlRating extends ShoelaceElement {
|
||||
}
|
||||
|
||||
private handleClick(event: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setValue(this.getValueFromMousePosition(event));
|
||||
this.emit('sl-change');
|
||||
}
|
||||
@@ -159,6 +163,7 @@ export default class SlRating extends ShoelaceElement {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
this.hoverValue = this.getValueFromTouchPosition(event);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ import type SlRemoveEvent from '../../events/sl-remove';
|
||||
* @csspart listbox - The listbox container where options are slotted.
|
||||
* @csspart tags - The container that houses option tags when `multiselect` is used.
|
||||
* @csspart tag - The individual tags that represent each multiselect option.
|
||||
* @csspart tag__base - The tag's base part.
|
||||
* @csspart tag__content - The tag's content part.
|
||||
* @csspart tag__remove-button - The tag's remove button.
|
||||
* @csspart tag__remove-button__base - The tag's remove button base part.
|
||||
* @csspart clear-button - The clear button.
|
||||
* @csspart expand-icon - The container that wraps the expand icon.
|
||||
*/
|
||||
@@ -433,18 +437,15 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
const values: string[] = [];
|
||||
|
||||
// Check for duplicate values in menu items
|
||||
allOptions.forEach(option => {
|
||||
if (values.includes(option.value)) {
|
||||
console.error(
|
||||
`An option with a duplicate value of "${option.value}" has been found in <sl-select>. All options must have unique values.`,
|
||||
option
|
||||
);
|
||||
}
|
||||
values.push(option.value);
|
||||
});
|
||||
if (customElements.get('sl-option')) {
|
||||
allOptions.forEach(option => values.push(option.value));
|
||||
|
||||
// Select only the options that match the new value
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||
// Select only the options that match the new value
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||
} else {
|
||||
// Rerun this handler when <sl-option> is registered
|
||||
customElements.whenDefined('sl-option').then(() => this.handleDefaultSlotChange());
|
||||
}
|
||||
}
|
||||
|
||||
private handleTagRemove(event: SlRemoveEvent, option: SlOption) {
|
||||
@@ -761,6 +762,12 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
return html`
|
||||
<sl-tag
|
||||
part="tag"
|
||||
exportparts="
|
||||
base:tag__base,
|
||||
content:tag__content,
|
||||
remove-button:tag__remove-button,
|
||||
remove-button__base:tag__remove-button__base
|
||||
"
|
||||
?pill=${this.pill}
|
||||
size=${this.size}
|
||||
removable
|
||||
|
||||
@@ -271,7 +271,7 @@ export class FormControlController implements ReactiveController {
|
||||
el.requestUpdate();
|
||||
}
|
||||
|
||||
private doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) {
|
||||
private doAction(type: 'submit' | 'reset', submitter?: HTMLInputElement | SlButton) {
|
||||
if (this.form) {
|
||||
const button = document.createElement('button');
|
||||
button.type = type;
|
||||
@@ -283,13 +283,13 @@ export class FormControlController implements ReactiveController {
|
||||
button.style.whiteSpace = 'nowrap';
|
||||
|
||||
// Pass name, value, and form attributes through to the temporary button
|
||||
if (invoker) {
|
||||
button.name = invoker.name;
|
||||
button.value = invoker.value;
|
||||
if (submitter) {
|
||||
button.name = submitter.name;
|
||||
button.value = submitter.value;
|
||||
|
||||
['formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => {
|
||||
if (invoker.hasAttribute(attr)) {
|
||||
button.setAttribute(attr, invoker.getAttribute(attr)!);
|
||||
if (submitter.hasAttribute(attr)) {
|
||||
button.setAttribute(attr, submitter.getAttribute(attr)!);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -306,15 +306,15 @@ export class FormControlController implements ReactiveController {
|
||||
}
|
||||
|
||||
/** Resets the form, restoring all the control to their default value */
|
||||
reset(invoker?: HTMLInputElement | SlButton) {
|
||||
this.doAction('reset', invoker);
|
||||
reset(submitter?: HTMLInputElement | SlButton) {
|
||||
this.doAction('reset', submitter);
|
||||
}
|
||||
|
||||
/** Submits the form, triggering validation and form data injection. */
|
||||
submit(invoker?: HTMLInputElement | SlButton) {
|
||||
submit(submitter?: HTMLInputElement | SlButton) {
|
||||
// Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a
|
||||
// native submit button into the form, "click" it, then remove it to simulate a standard form submission.
|
||||
this.doAction('submit', invoker);
|
||||
this.doAction('submit', submitter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -105,8 +105,8 @@ export interface ShoelaceFormControl extends ShoelaceElement {
|
||||
|
||||
// Constraint validation attributes
|
||||
pattern?: string;
|
||||
min?: number | Date;
|
||||
max?: number | Date;
|
||||
min?: number | string | Date;
|
||||
max?: number | string | Date;
|
||||
step?: number | 'any';
|
||||
required?: boolean;
|
||||
minlength?: number;
|
||||
|
||||
58
src/shoelace-autoloader.ts
Normal file
58
src/shoelace-autoloader.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getBasePath } from './utilities/base-path';
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
for (const { addedNodes } of mutations) {
|
||||
for (const node of addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
discover(node as Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks a node for undefined elements and attempts to register them.
|
||||
*/
|
||||
async function discover(root: Element) {
|
||||
const rootTagName = root.tagName.toLowerCase();
|
||||
const rootIsCustomElement = rootTagName.includes('-');
|
||||
const tags = [...root.querySelectorAll(':not(:defined)')]
|
||||
.map(el => el.tagName.toLowerCase())
|
||||
.filter(tag => tag.startsWith('sl-'));
|
||||
|
||||
// If the root element is an undefined custom element, add it to the list
|
||||
if (rootIsCustomElement && !customElements.get(rootTagName)) {
|
||||
tags.push(root.tagName.toLowerCase());
|
||||
}
|
||||
|
||||
// Make the list unique
|
||||
const tagsToRegister = [...new Set(tags)];
|
||||
|
||||
await Promise.allSettled(tagsToRegister.map(tagName => register(tagName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an element by tag name.
|
||||
*/
|
||||
function register(tagName: string): Promise<void> {
|
||||
const tagWithoutPrefix = tagName.replace(/^sl-/i, '');
|
||||
const path = getBasePath(`components/${tagWithoutPrefix}/${tagWithoutPrefix}.js`);
|
||||
|
||||
// If the element is already defined, there's nothing more to do
|
||||
if (customElements.get(tagName)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Register it
|
||||
return new Promise((resolve, reject) => {
|
||||
import(path)
|
||||
.then(() => resolve())
|
||||
.catch(() => reject(new Error(`Unable to automatically load<${tagName}> from ${path}`)));
|
||||
});
|
||||
}
|
||||
|
||||
// Initial discovery
|
||||
discover(document.body);
|
||||
|
||||
// Listen for new undefined elements
|
||||
observer.observe(document.body, { subtree: true, childList: true });
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Dansk',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Karrusel',
|
||||
clearEntry: 'Ryd indtastning',
|
||||
close: 'Luk',
|
||||
copy: 'Kopier',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Scroll til start',
|
||||
selectAColorFromTheScreen: 'Vælg en farve fra skærmen',
|
||||
showPassword: 'Vis adgangskode',
|
||||
slide_num: slide => `Slide ${slide}`,
|
||||
toggleColorFormat: 'Skift farveformat'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Deutsch',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Karussell',
|
||||
clearEntry: 'Eingabe löschen',
|
||||
close: 'Schließen',
|
||||
copy: 'Kopieren',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Zum Anfang scrollen',
|
||||
selectAColorFromTheScreen: 'Wähle eine Farbe vom Bildschirm',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
slide_num: slide => `Gleiten ${slide}`,
|
||||
toggleColorFormat: 'Farbformat umschalten'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'English',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Carousel',
|
||||
clearEntry: 'Clear entry',
|
||||
close: 'Close',
|
||||
copy: 'Copy',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Scroll to start',
|
||||
selectAColorFromTheScreen: 'Select a color from the screen',
|
||||
showPassword: 'Show password',
|
||||
slide_num: slide => `Slide ${slide}`,
|
||||
toggleColorFormat: 'Toggle color format'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Español',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Carrusel',
|
||||
clearEntry: 'Borrar entrada',
|
||||
close: 'Cerrar',
|
||||
copy: 'Copiar',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Desplazarse al inicio',
|
||||
selectAColorFromTheScreen: 'Seleccione un color de la pantalla',
|
||||
showPassword: 'Mostrar contraseña',
|
||||
slide_num: slide => `Diapositiva ${slide}`,
|
||||
toggleColorFormat: 'Alternar formato de color'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'فارسی',
|
||||
$dir: 'rtl',
|
||||
|
||||
carousel: 'چرخ فلک',
|
||||
clearEntry: 'پاک کردن ورودی',
|
||||
close: 'بستن',
|
||||
copy: 'رونوشت',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'پیمایش به ابتدا',
|
||||
selectAColorFromTheScreen: 'انتخاب یک رنگ از صفحه نمایش',
|
||||
showPassword: 'نمایش رمز',
|
||||
slide_num: slide => `اسلاید ${slide}`,
|
||||
toggleColorFormat: 'تغییر قالب رنگ'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Français',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Carrousel',
|
||||
clearEntry: `Effacer l'entrée`,
|
||||
close: 'Fermer',
|
||||
copy: 'Copier',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: `Faire défiler jusqu'au début`,
|
||||
selectAColorFromTheScreen: `Sélectionnez une couleur à l'écran`,
|
||||
showPassword: 'Montrer le mot de passe',
|
||||
slide_num: slide => `Diapositive ${slide}`,
|
||||
toggleColorFormat: 'Changer le format de couleur'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'עברית',
|
||||
$dir: 'rtl',
|
||||
|
||||
carousel: 'קרוסלה',
|
||||
clearEntry: 'נקה קלט',
|
||||
close: 'סגור',
|
||||
copy: 'העתק',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'גלול להתחלה',
|
||||
selectAColorFromTheScreen: 'בחור צבע מהמסך',
|
||||
showPassword: 'הראה סיסמה',
|
||||
slide_num: slide => `שקופית ${slide}`,
|
||||
toggleColorFormat: 'החלף פורמט צבע'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Magyar',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Körhinta',
|
||||
clearEntry: 'Bejegyzés törlése',
|
||||
close: 'Bezárás',
|
||||
copy: 'Másolás',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Görgessen az elejére',
|
||||
selectAColorFromTheScreen: 'Szín választása a képernyőről',
|
||||
showPassword: 'Jelszó megjelenítése',
|
||||
slide_num: slide => `${slide}. dia`,
|
||||
toggleColorFormat: 'Színformátum változtatása'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: '日本語',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'カルーセル',
|
||||
clearEntry: 'クリアエントリ',
|
||||
close: '閉じる',
|
||||
copy: 'コピー',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: '最初にスクロールする',
|
||||
selectAColorFromTheScreen: '画面から色を選択してください',
|
||||
showPassword: 'パスワードを表示',
|
||||
slide_num: slide => `スライド ${slide}`,
|
||||
toggleColorFormat: '色のフォーマットを切り替える'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Nederlands',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Carrousel',
|
||||
clearEntry: 'Invoer wissen',
|
||||
close: 'Sluiten',
|
||||
copy: 'Kopiëren',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Scroll naar begin',
|
||||
selectAColorFromTheScreen: 'Selecteer een kleur van het scherm',
|
||||
showPassword: 'Laat wachtwoord zien',
|
||||
slide_num: slide => `Schuif ${slide}`,
|
||||
toggleColorFormat: 'Wissel kleurnotatie'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Polski',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Karuzela',
|
||||
clearEntry: 'Wyczyść wpis',
|
||||
close: 'Zamknij',
|
||||
copy: 'Kopiuj',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Przewiń do początku',
|
||||
selectAColorFromTheScreen: 'Próbkuj z ekranu',
|
||||
showPassword: 'Pokaż hasło',
|
||||
slide_num: slide => `Slajd ${slide}`,
|
||||
toggleColorFormat: 'Przełącz format'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Português',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Carrossel',
|
||||
clearEntry: 'Limpar entrada',
|
||||
close: 'Fechar',
|
||||
copy: 'Copiar',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Rolar até o começo',
|
||||
selectAColorFromTheScreen: 'Selecionar uma cor da tela',
|
||||
showPassword: 'Mostrar senhaShow password',
|
||||
slide_num: slide => `Diapositivo ${slide}`,
|
||||
toggleColorFormat: 'Trocar o formato de cor'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Русский',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Карусель',
|
||||
clearEntry: 'Очистить запись',
|
||||
close: 'Закрыть',
|
||||
copy: 'Скопировать',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Пролистать к началу',
|
||||
selectAColorFromTheScreen: 'Выберите цвет на экране',
|
||||
showPassword: 'Показать пароль',
|
||||
slide_num: slide => `Слайд ${slide}`,
|
||||
toggleColorFormat: 'Переключить цветовую модель'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Svenska',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Karusell',
|
||||
clearEntry: 'Återställ val',
|
||||
close: 'Stäng',
|
||||
copy: 'Kopiera',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Skrolla till början',
|
||||
selectAColorFromTheScreen: 'Välj en färg från skärmen',
|
||||
showPassword: 'Visa lösenord',
|
||||
slide_num: slide => `Bild ${slide}`,
|
||||
toggleColorFormat: 'Växla färgformat'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: 'Türkçe',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: 'Atlıkarınca',
|
||||
clearEntry: 'Girişi sil',
|
||||
close: 'Kapat',
|
||||
copy: 'Kopya',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Başa kay',
|
||||
selectAColorFromTheScreen: 'Ekrandan bir renk seçin',
|
||||
showPassword: 'Şifreyi göster',
|
||||
slide_num: slide => `Slayt ${slide}`,
|
||||
toggleColorFormat: 'Renk biçimini değiştir'
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const translation: Translation = {
|
||||
$name: '正體中文',
|
||||
$dir: 'ltr',
|
||||
|
||||
carousel: '旋轉木馬',
|
||||
clearEntry: '清空',
|
||||
close: '關閉',
|
||||
copy: '複製',
|
||||
@@ -27,6 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: '捲至頁首',
|
||||
selectAColorFromTheScreen: '從螢幕中選擇一種顏色',
|
||||
showPassword: '顯示密碼',
|
||||
slide_num: slide => `幻燈片 ${slide}`,
|
||||
toggleColorFormat: '切換顏色格式'
|
||||
};
|
||||
|
||||
|
||||
@@ -9,16 +9,18 @@ export function setBasePath(path: string) {
|
||||
* Gets the library's base path.
|
||||
*
|
||||
* The base path is used to load assets such as icons and images, so it needs to be set for components to work properly.
|
||||
* By default, this script will look for a script ending in shoelace.js and set the base path to the directory that
|
||||
* contains that file. To override this behavior, you can add the data-shoelace attribute to any script on the page
|
||||
* (it probably makes the most sense to attach it to the Shoelace script, but it could also be on a bundle). The value
|
||||
* can be a local folder or it can point to a CORS-enabled endpoint such as a CDN.
|
||||
* By default, this script will look for a script ending in shoelace.js or shoelace-autoloader.js and set the base path
|
||||
* to the directory that contains that file. To override this behavior, you can add the data-shoelace attribute to any
|
||||
* script on the page (it probably makes the most sense to attach it to the Shoelace script, but it could also be on a
|
||||
* bundle). The value can be a local folder or it can point to a CORS-enabled endpoint such as a CDN.
|
||||
*
|
||||
* <script src="bundle.js" data-shoelace="/custom/base/path"></script>
|
||||
*
|
||||
* Alternatively, you can set the base path manually using the exported setBasePath() function.
|
||||
*
|
||||
* @param subpath - An optional path to append to the base path.
|
||||
*/
|
||||
export function getBasePath() {
|
||||
export function getBasePath(subpath = '') {
|
||||
if (!basePath) {
|
||||
const scripts = [...document.getElementsByTagName('script')] as HTMLScriptElement[];
|
||||
const configScript = scripts.find(script => script.hasAttribute('data-shoelace'));
|
||||
@@ -27,7 +29,9 @@ export function getBasePath() {
|
||||
// Use the data-shoelace attribute
|
||||
setBasePath(configScript.getAttribute('data-shoelace')!);
|
||||
} else {
|
||||
const fallbackScript = scripts.find(s => /shoelace(\.min)?\.js($|\?)/.test(s.src));
|
||||
const fallbackScript = scripts.find(s => {
|
||||
return /shoelace(\.min)?\.js($|\?)/.test(s.src) || /shoelace-autoloader(\.min)?\.js($|\?)/.test(s.src);
|
||||
});
|
||||
let path = '';
|
||||
|
||||
if (fallbackScript) {
|
||||
@@ -38,5 +42,6 @@ export function getBasePath() {
|
||||
}
|
||||
}
|
||||
|
||||
return basePath.replace(/\/$/, '');
|
||||
// Return the base path without a trailing slash. If one exists, append the subpath separated by a slash.
|
||||
return basePath.replace(/\/$/, '') + (subpath ? `/${subpath.replace(/^\//, '')}` : ``);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface Translation extends DefaultTranslation {
|
||||
$name: string; // e.g. English, Español
|
||||
$dir: 'ltr' | 'rtl';
|
||||
|
||||
carousel: string;
|
||||
clearEntry: string;
|
||||
close: string;
|
||||
copy: string;
|
||||
@@ -30,5 +31,6 @@ export interface Translation extends DefaultTranslation {
|
||||
scrollToStart: string;
|
||||
selectAColorFromTheScreen: string;
|
||||
showPassword: string;
|
||||
slide_num: (slide: number) => string;
|
||||
toggleColorFormat: string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
"target": "es2017",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.Iterable",
|
||||
|
||||
Reference in New Issue
Block a user