mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Compare commits
62 Commits
autoload
...
alenaksu/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4fc4eb9a | ||
|
|
a226db0071 | ||
|
|
e4a525c5c4 | ||
|
|
0e7487257b | ||
|
|
eab0e3219f | ||
|
|
cf89c901a2 | ||
|
|
902b08cc0f | ||
|
|
caf9a09efa | ||
|
|
65734dc993 | ||
|
|
0f02fffc3a | ||
|
|
931ecad8c5 | ||
|
|
c137f83df6 | ||
|
|
d3a0a38dce | ||
|
|
b76af1aa21 | ||
|
|
5cf6a37ee2 | ||
|
|
63194abf93 | ||
|
|
e196b0915a | ||
|
|
d2369d1de8 | ||
|
|
a9bbcc5556 | ||
|
|
8d9430e7a2 | ||
|
|
0411754949 | ||
|
|
91ffaa1a2d | ||
|
|
ae9972a91a | ||
|
|
478fa6f2bb | ||
|
|
6a52a04591 | ||
|
|
a8f87e0d5e | ||
|
|
cbc96fdf5c | ||
|
|
b4d24dd9af | ||
|
|
4b66cc2acb | ||
|
|
3766d5ce27 | ||
|
|
4b7d686754 | ||
|
|
b948a07a4d | ||
|
|
6d3505aefa | ||
|
|
b22650ff51 | ||
|
|
23a7f65b49 | ||
|
|
f4fba8eab4 | ||
|
|
1734bf54a7 | ||
|
|
88efec7815 | ||
|
|
e335189bb8 | ||
|
|
d03ca4ab95 | ||
|
|
257407758f | ||
|
|
2443c046aa | ||
|
|
d710eb3947 | ||
|
|
7b2f6f230d | ||
|
|
07cb6070cc | ||
|
|
bd7dc2a7be | ||
|
|
db931c12be | ||
|
|
765b311a08 | ||
|
|
ce198d9c0b | ||
|
|
8f5893931b | ||
|
|
221be48589 | ||
|
|
234ff2619d | ||
|
|
b37be46ba3 | ||
|
|
6e2ea508db | ||
|
|
0e6e2abd28 | ||
|
|
db1bdfbf65 | ||
|
|
7bf0f647b3 | ||
|
|
df25f8617b | ||
|
|
ad2099a27f | ||
|
|
708127f96d | ||
|
|
9deb51e95a | ||
|
|
67852ea657 |
@@ -15,6 +15,7 @@
|
||||
"autoplay",
|
||||
"bezier",
|
||||
"boxicons",
|
||||
"CACHEABLE",
|
||||
"callout",
|
||||
"callouts",
|
||||
"chatbubble",
|
||||
@@ -39,6 +40,7 @@
|
||||
"datetime",
|
||||
"describedby",
|
||||
"Docsify",
|
||||
"dogfood",
|
||||
"dropdowns",
|
||||
"easings",
|
||||
"enterkeyhint",
|
||||
@@ -110,6 +112,8 @@
|
||||
"reregister",
|
||||
"resizer",
|
||||
"resizers",
|
||||
"retargeted",
|
||||
"RETRYABLE",
|
||||
"rgba",
|
||||
"roadmap",
|
||||
"Roboto",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -41,6 +41,38 @@ import { AppComponent } from './app.component';
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## Reference Shoelace components in your Angular component code
|
||||
|
||||
```js
|
||||
import { SlDrawer } from '@shoelace-style/shoelace';
|
||||
|
||||
@Component({
|
||||
selector: 'app-drawer-example',
|
||||
template: '<div id="page"><button (click)="showDrawer()">Show drawer</button><sl-drawer #drawer label="Drawer" class="drawer-focus" style="--size: 50vw"><p>Drawer content</p></sl-drawer></div>'
|
||||
})
|
||||
export class DrawerExampleComponent implements OnInit {
|
||||
|
||||
// use @ViewChild to get a reference to the #drawer element within component template
|
||||
@ViewChild('drawer')
|
||||
drawer?: ElementRef<SlDrawer>;
|
||||
|
||||
...
|
||||
|
||||
constructor(...) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
showDrawer() {
|
||||
// use nativeElement to access Shoelace components
|
||||
this.drawer?.nativeElement.show();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can start using Shoelace components in your app!
|
||||
|
||||
?> Are you using Shoelace with Angular? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/angular.md)
|
||||
|
||||
@@ -58,7 +58,7 @@ Now you can start using Shoelace components in your app!
|
||||
|
||||
### QR code generator example
|
||||
|
||||
```vue
|
||||
```html
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>QR code generator</h1>
|
||||
@@ -70,22 +70,22 @@ Now you can start using Shoelace components in your app!
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import '@shoelace-style/shoelace/dist/components/qr-code/qr-code.js';
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
||||
import { ref } from 'vue';
|
||||
import '@shoelace-style/shoelace/dist/components/qr-code/qr-code.js';
|
||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
||||
|
||||
const qrCode = ref();
|
||||
const qrCode = ref();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
sl-input {
|
||||
margin: var(--sl-spacing-large) 0;
|
||||
}
|
||||
sl-input {
|
||||
margin: var(--sl-spacing-large) 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -98,3 +98,18 @@ When binding complex data such as objects and arrays, use the `.prop` modifier t
|
||||
```
|
||||
|
||||
?> Are you using Shoelace with Vue? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/vue.md)
|
||||
|
||||
### Slots
|
||||
|
||||
To use Shoelace components with slots, follow the Vue documentation on using [slots with custom elements](https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue).
|
||||
|
||||
Here is an example:
|
||||
|
||||
```html
|
||||
<sl-drawer label="Drawer" placement="start" class="drawer-placement-start" :open="drawerIsOpen">
|
||||
This drawer slides in from the start.
|
||||
<div slot="footer">
|
||||
<sl-button variant="primary" @click=" drawerIsOpen = false">Close</sl-button>
|
||||
</div>
|
||||
</sl-drawer>
|
||||
```
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
# 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.
|
||||
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.
|
||||
|
||||
If you're using a framework, make sure to check out the pages for [React](/frameworks/react), [Vue](/frameworks/vue), and [Angular](/frameworks/angular).
|
||||
## CDN Installation (Easiest)
|
||||
|
||||
## Autoloading (Experimental)
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel="autoloader" active>Autoloader</sl-tab>
|
||||
<sl-tab slot="nav" panel="traditional">Traditional Loader</sl-tab>
|
||||
|
||||
The autoloader is the simplest and most efficient way to use Shoelace. A lightweight script watches the DOM for unregistered Shoelace elements and lazy loads them for you. This works for elements already on the page and elements that get added later on.
|
||||
<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
|
||||
@@ -14,31 +20,31 @@ The autoloader is the simplest and most efficient way to use Shoelace. A lightwe
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace-autoloader.js"></script>
|
||||
```
|
||||
|
||||
?> While convenient, one caveat of autoloading is you may see a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
|
||||
</sl-tab-panel>
|
||||
|
||||
## CDN Installation
|
||||
<sl-tab-panel name="traditional">
|
||||
|
||||
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.
|
||||
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 [autoload](#autoloading-experimental) or [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
|
||||
@@ -52,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)
|
||||
@@ -100,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?
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ Refer to a component's documentation for a complete list of its properties.
|
||||
|
||||
## Events
|
||||
|
||||
You can listen for standard events such as `click`, `mouseover`, etc. as you normally would. In addition, some components emit custom events. These work the same way as standard events, but are prefixed with `sl-` to prevent collisions with standard events and other libraries.
|
||||
You can listen for standard events such as `click`, `mouseover`, etc. as you normally would. However, it's important to note that many events emitted within a component's shadow root will be [retargeted](https://dom.spec.whatwg.org/#retarget) to the host element. This may result in, for example, multiple `click` handlers executing even if the user clicks just once. Furthermore, `event.target` will point to the host element, making things even more confusing.
|
||||
|
||||
As a result, you should almost always listen for custom events instead. For example, instead of listening to `click` to determine when an `<sl-checkbox>` gets toggled, listen to `sl-change`.
|
||||
|
||||
```html
|
||||
<sl-checkbox>Check me</sl-checkbox>
|
||||
@@ -46,7 +48,7 @@ You can listen for standard events such as `click`, `mouseover`, etc. as you nor
|
||||
</script>
|
||||
```
|
||||
|
||||
Refer to a component's documentation for a complete list of its custom events.
|
||||
All custom events are prefixed with `sl-` to prevent collisions with standard events and other libraries. Refer to a component's documentation for a complete list of its custom events.
|
||||
|
||||
## Methods
|
||||
|
||||
|
||||
@@ -8,6 +8,24 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
|
||||
## Next
|
||||
|
||||
- Added the `discover()` function to the experimental autoloader's exports [#1236](https://github.com/shoelace-style/shoelace/pull/1236)
|
||||
- Added more tests for `<sl-animated-image>` [#1246](https://github.com/shoelace-style/shoelace/pull/1246)
|
||||
- Added tests for `<sl-animation>` [#1274](https://github.com/shoelace-style/shoelace/pull/1274)
|
||||
- Fixed a bug in `<sl-tree-item>` that prevented long labels from wrapping [#1243](https://github.com/shoelace-style/shoelace/issues/1243)
|
||||
- Fixed a bug in `<sl-tree-item>` that caused labels to be misaligned when text wraps [#1244](https://github.com/shoelace-style/shoelace/issues/1244)
|
||||
- Fixed an incorrect CSS property value in `<sl-checkbox>` [#1272](https://github.com/shoelace-style/shoelace/pull/1272)
|
||||
- Fixed a bug in `<sl-avatar>` that caused the initials to show up behind images with transparency [#1260](https://github.com/shoelace-style/shoelace/pull/1260)
|
||||
- Fixed a bug in `<sl-split-panel>` that prevented the divider from being focusable in some browsers [#1288](https://github.com/shoelace-style/shoelace/issues/1288)
|
||||
- Fixed a bug that caused `<sl-tab-group>` to affect scrolling when initializing [#1292](https://github.com/shoelace-style/shoelace/issues/1292)
|
||||
- Fixed a bug in `<sl-menu-item>` that allowed the hover state to show when focused [#1282](https://github.com/shoelace-style/shoelace/issues/1282)
|
||||
- Fixed a bug in `<sl-carousel>` that prevented interactive elements from receiving clicks [#1262](https://github.com/shoelace-style/shoelace/issues/1262)
|
||||
- Improved the behavior of `<sl-carousel>` when used inside a flex container [#1235](https://github.com/shoelace-style/shoelace/pull/1235)
|
||||
- Improved the behavior of `<sl-tree-item>` to support buttons and other interactive elements [#1234](https://github.com/shoelace-style/shoelace/issues/1234)
|
||||
- Improved the performance of `<sl-include>` to prevent an apparent memory leak in some browsers [#1284](https://github.com/shoelace-style/shoelace/pull/1284)
|
||||
- Improved the accessibility of `<sl-select>`, `<sl-split-panel>`, and `<sl-details>` by ensuring slots don't have roles [#1287](https://github.com/shoelace-style/shoelace/issues/1287)
|
||||
|
||||
## 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
|
||||
@@ -16,6 +34,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
- 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
|
||||
|
||||
@@ -26,7 +45,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||
- 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)
|
||||
- Fixed a big in `<sl-input>` that caused the calendar icon to be clipped in Firefox [#1213](https://github.com/shoelace-style/shoelace/pull/1213)
|
||||
- Fixed a bug in `<sl-input>` that caused the calendar icon to be clipped in Firefox [#1213](https://github.com/shoelace-style/shoelace/pull/1213)
|
||||
- Fixed a bug in `<sl-tab>` that caused `sl-tab-show` to be emitted when activating the close button
|
||||
- Fixed a bug in `<sl-spinner>` that caused `--track-color` to be invisible with certain colors
|
||||
- Fixed a bug in `<sl-menu-item>` that caused the focus color to show when selecting menu items with a mouse or touch device
|
||||
|
||||
@@ -363,3 +363,26 @@ Avoid inlining SVG icons inside of templates. If a component requires an icon, m
|
||||
```
|
||||
|
||||
This will render the icons instantly whereas the default library will fetch them from a remote source. If an icon isn't available in the system library, you will need to add it to `library.system.ts`. Using the system library ensures that all icons load instantly and are customizable by users who wish to provide a custom resolver for the system library.
|
||||
|
||||
### Writing tests
|
||||
|
||||
What to test for a given component:
|
||||
|
||||
- Start with a simple test that checks that the default version of the component still renders.
|
||||
- Add at least one accessibility test (The accessibility check only covers the parts of the DOM which are currently visible and rendered. Depending on the component, more than one accessibility test is required to cover all scenarios.):
|
||||
|
||||
```ts
|
||||
const myComponent = await fixture<SlAlert>(html`<sl-my-component>SomeContent</sl-my-component>`);
|
||||
|
||||
await expect(myComponent).to.be.accessible();
|
||||
```
|
||||
|
||||
- Try to cover all features advertised in the component's description
|
||||
|
||||
Guidelines for writing tests:
|
||||
|
||||
- Each test should declare its own, hand crafted hml fixture for the component. Do not try to write one big component to match all tests. This helps keeping each test understandable in isolation.
|
||||
- Tests should not produce log lines. Note that sometimes this cannot be prevented as the test runner might log errors (e.g. 404s).
|
||||
- Try keeping the main test readable: Extract more complicated sets of selectors/commands/assertions into separate functions.
|
||||
- Try to aim testing the user facing features of the component instead of the internal workings of the component.
|
||||
- Group multiple tests for one feature into describe blocks.
|
||||
|
||||
5
package-lock.json
generated
5
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",
|
||||
@@ -56,7 +56,6 @@
|
||||
"lint-staged": "^13.1.0",
|
||||
"lunr": "^2.3.9",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"open": "^8.4.0",
|
||||
"pascal-case": "^3.1.2",
|
||||
"plop": "^3.1.1",
|
||||
"prettier": "^2.8.2",
|
||||
|
||||
@@ -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",
|
||||
@@ -110,7 +110,6 @@
|
||||
"lint-staged": "^13.1.0",
|
||||
"lunr": "^2.3.9",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"open": "^8.4.0",
|
||||
"pascal-case": "^3.1.2",
|
||||
"plop": "^3.1.1",
|
||||
"prettier": "^2.8.2",
|
||||
|
||||
@@ -7,7 +7,6 @@ import esbuild from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { globby } from 'globby';
|
||||
import open from 'open';
|
||||
import copy from 'recursive-copy';
|
||||
|
||||
const { bundle, copydir, dir, serve, types } = commandLineArgs([
|
||||
@@ -108,7 +107,6 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
deleteSync('docs/dist');
|
||||
|
||||
const browserSyncConfig = {
|
||||
open: false,
|
||||
startPath: '/',
|
||||
port,
|
||||
logLevel: 'silent',
|
||||
@@ -145,7 +143,6 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
bs.init(browserSyncConfig, () => {
|
||||
const url = `http://localhost:${port}`;
|
||||
console.log(chalk.cyan(`Launched the Shoelace dev server at ${url} 🥾\n`));
|
||||
open(url);
|
||||
});
|
||||
|
||||
// Rebuild and reload when source files change
|
||||
|
||||
@@ -1,9 +1,70 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import type SlAnimatedImage from './animated-image';
|
||||
|
||||
describe('<sl-animated-image>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-animated-image></sl-animated-image> `);
|
||||
const animatedImage = await fixture(html` <sl-animated-image></sl-animated-image> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
expect(animatedImage).to.exist;
|
||||
});
|
||||
|
||||
it('should render be accessible', async () => {
|
||||
const animatedImage = await fixture(html` <sl-animated-image></sl-animated-image> `);
|
||||
|
||||
await expect(animatedImage).to.be.accessible();
|
||||
});
|
||||
|
||||
const files = ['docs/assets/images/walk.gif', 'docs/assets/images/tie.webp'];
|
||||
|
||||
files.forEach((file: string) => {
|
||||
it(`should load a ${file} without errors`, async () => {
|
||||
const animatedImage = await fixture<SlAnimatedImage>(html` <sl-animated-image></sl-animated-image> `);
|
||||
let errorCount = 0;
|
||||
oneEvent(animatedImage, 'sl-error').then(() => errorCount++);
|
||||
await loadImage(animatedImage, file);
|
||||
|
||||
expect(errorCount).to.be.equal(0);
|
||||
});
|
||||
|
||||
it(`should play ${file} on click`, async () => {
|
||||
const animatedImage = await fixture<SlAnimatedImage>(html` <sl-animated-image></sl-animated-image> `);
|
||||
await loadImage(animatedImage, file);
|
||||
|
||||
expect(animatedImage.play).not.to.be.true;
|
||||
|
||||
await clickOnElement(animatedImage);
|
||||
|
||||
expect(animatedImage.play).to.be.true;
|
||||
});
|
||||
|
||||
it(`should pause and resume ${file} on click`, async () => {
|
||||
const animatedImage = await fixture<SlAnimatedImage>(html` <sl-animated-image></sl-animated-image> `);
|
||||
await loadImage(animatedImage, file);
|
||||
|
||||
animatedImage.play = true;
|
||||
|
||||
await clickOnElement(animatedImage);
|
||||
|
||||
expect(animatedImage.play).to.be.false;
|
||||
|
||||
await clickOnElement(animatedImage);
|
||||
|
||||
expect(animatedImage.play).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit an error event on invalid url', async () => {
|
||||
const animatedImage = await fixture<SlAnimatedImage>(html` <sl-animated-image></sl-animated-image> `);
|
||||
|
||||
const errorPromise = oneEvent(animatedImage, 'sl-error');
|
||||
animatedImage.src = 'completelyWrong';
|
||||
|
||||
await errorPromise;
|
||||
});
|
||||
});
|
||||
async function loadImage(animatedImage: SlAnimatedImage, file: string) {
|
||||
const loadingPromise = oneEvent(animatedImage, 'sl-load');
|
||||
animatedImage.src = file;
|
||||
await loadingPromise;
|
||||
}
|
||||
|
||||
81
src/components/animation/animation.test.ts
Normal file
81
src/components/animation/animation.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import type SlAnimation from './animation';
|
||||
|
||||
describe('<sl-animation>', () => {
|
||||
const boxToAnimate = html`<div style="width: 10px; height: 10px;" data-testid="animated-box"></div>`;
|
||||
|
||||
it('renders', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(html`<sl-animation>${boxToAnimate}</sl-animation>`);
|
||||
|
||||
expect(animationContainer).to.exist;
|
||||
});
|
||||
|
||||
it('is accessible', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(html`<sl-animation>${boxToAnimate}</sl-animation>`);
|
||||
|
||||
await expect(animationContainer).to.be.accessible();
|
||||
});
|
||||
|
||||
describe('animation start', () => {
|
||||
it('does not start the animation by default', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(
|
||||
html`<sl-animation name="bounce" easing="ease-in-out" duration="10">${boxToAnimate}</sl-animation>`
|
||||
);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(animationContainer.play).to.be.false;
|
||||
});
|
||||
|
||||
it('emits the correct event on animation start', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(
|
||||
html`<sl-animation name="bounce" easing="ease-in-out" duration="10">${boxToAnimate}</sl-animation>`
|
||||
);
|
||||
|
||||
const startPromise = oneEvent(animationContainer, 'sl-start');
|
||||
animationContainer.play = true;
|
||||
return startPromise;
|
||||
});
|
||||
});
|
||||
|
||||
it('emits the correct event on animation end', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(
|
||||
html`<sl-animation name="bounce" easing="ease-in-out" duration="1">${boxToAnimate}</sl-animation>`
|
||||
);
|
||||
|
||||
const endPromise = oneEvent(animationContainer, 'sl-finish');
|
||||
animationContainer.iterations = 1;
|
||||
animationContainer.play = true;
|
||||
return endPromise;
|
||||
});
|
||||
|
||||
it('can be finished by hand', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(
|
||||
html`<sl-animation name="bounce" easing="ease-in-out" duration="1000">${boxToAnimate}</sl-animation>`
|
||||
);
|
||||
|
||||
const endPromise = oneEvent(animationContainer, 'sl-finish');
|
||||
animationContainer.iterations = 1;
|
||||
animationContainer.play = true;
|
||||
|
||||
await aTimeout(0);
|
||||
|
||||
animationContainer.finish();
|
||||
return endPromise;
|
||||
});
|
||||
|
||||
it('can be cancelled', async () => {
|
||||
const animationContainer = await fixture<SlAnimation>(
|
||||
html`<sl-animation name="bounce" easing="ease-in-out" duration="1">${boxToAnimate}</sl-animation>`
|
||||
);
|
||||
let animationHasFinished = false;
|
||||
oneEvent(animationContainer, 'sl-finish').then(() => (animationHasFinished = true));
|
||||
const cancelPromise = oneEvent(animationContainer, 'sl-cancel');
|
||||
animationContainer.play = true;
|
||||
|
||||
await aTimeout(0);
|
||||
animationContainer.cancel();
|
||||
|
||||
await cancelPromise;
|
||||
expect(animationHasFinished).to.be.false;
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,46 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when image is present, the initials or icon part should not render', () => {
|
||||
const initials = 'SL';
|
||||
const image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
const label = 'Small transparent square';
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(
|
||||
html`<sl-avatar image="${image}" label="${label}" initials="${initials}"></sl-avatar>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
/**
|
||||
* The image element itself is ancillary, because it's parent container contains the
|
||||
* aria-label which dictates what "sl-avatar" is. This also implies that label text will
|
||||
* resolve to "" when not provided and ignored by readers. This is why we use alt="" on
|
||||
* the image element to pass accessibility.
|
||||
* https://html.spec.whatwg.org/multipage/images.html#ancillary-images
|
||||
*/
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('renders "image" part, with src and a role of presentation', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part~="image"]')!;
|
||||
|
||||
expect(part.getAttribute('src')).to.eq(image);
|
||||
});
|
||||
|
||||
it('should not render the initials part', () => {
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part~="initials"]')!;
|
||||
|
||||
expect(part).to.not.exist;
|
||||
});
|
||||
|
||||
it('should not render the icon part', () => {
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=icon]')!;
|
||||
|
||||
expect(slot).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
['square', 'rounded', 'circle'].forEach(shape => {
|
||||
describe(`when passed a shape attribute ${shape}`, () => {
|
||||
before(async () => {
|
||||
|
||||
@@ -52,6 +52,29 @@ export default class SlAvatar extends ShoelaceElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const avatarWithImage = html`
|
||||
<img
|
||||
part="image"
|
||||
class="avatar__image"
|
||||
src="${this.image}"
|
||||
loading="${this.loading}"
|
||||
alt=""
|
||||
@error="${() => (this.hasError = true)}"
|
||||
/>
|
||||
`;
|
||||
|
||||
let avatarWithoutImage = html``;
|
||||
|
||||
if (this.initials) {
|
||||
avatarWithoutImage = html`<div part="initials" class="avatar__initials">${this.initials}</div>`;
|
||||
} else {
|
||||
avatarWithoutImage = html`
|
||||
<slot name="icon" part="icon" class="avatar__icon" aria-hidden="true">
|
||||
<sl-icon name="person-fill" library="system"></sl-icon>
|
||||
</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
part="base"
|
||||
@@ -64,25 +87,7 @@ export default class SlAvatar extends ShoelaceElement {
|
||||
role="img"
|
||||
aria-label=${this.label}
|
||||
>
|
||||
${this.initials
|
||||
? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
|
||||
: html`
|
||||
<slot name="icon" part="icon" class="avatar__icon" aria-hidden="true">
|
||||
<sl-icon name="person-fill" library="system"></sl-icon>
|
||||
</slot>
|
||||
`}
|
||||
${this.image && !this.hasError
|
||||
? html`
|
||||
<img
|
||||
part="image"
|
||||
class="avatar__image"
|
||||
src="${this.image}"
|
||||
loading="${this.loading}"
|
||||
alt=""
|
||||
@error="${() => (this.hasError = true)}"
|
||||
/>
|
||||
`
|
||||
: ''}
|
||||
${this.image && !this.hasError ? avatarWithImage : avatarWithoutImage}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { clickOnElement, moveMouseOnElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { resetMouse, sendMouse } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlCarousel from './carousel';
|
||||
|
||||
describe('<sl-carousel>', () => {
|
||||
afterEach(async () => {
|
||||
await resetMouse();
|
||||
});
|
||||
|
||||
it('should render a carousel with default configuration', async () => {
|
||||
// Arrange
|
||||
const el = await fixture(html`
|
||||
@@ -293,6 +298,63 @@ describe('<sl-carousel>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `mouse-dragging` attribute is provided', () => {
|
||||
it('should be possible to drag the carousel using 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>
|
||||
`);
|
||||
const carouselItem = el.querySelector('sl-carousel-item') as HTMLElement;
|
||||
|
||||
// Act
|
||||
await moveMouseOnElement(carouselItem, 'right');
|
||||
await sendMouse({
|
||||
type: 'down'
|
||||
});
|
||||
|
||||
// For some reason it seems necessary to move the mouse back and forth to trigger a move event
|
||||
await moveMouseOnElement(carouselItem, 'left');
|
||||
await moveMouseOnElement(carouselItem, 'right');
|
||||
await moveMouseOnElement(carouselItem, 'left');
|
||||
|
||||
await sendMouse({
|
||||
type: 'up'
|
||||
});
|
||||
|
||||
await oneEvent(el.scrollContainer, 'scrollend');
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
expect(el.activeSlide).to.be.equal(1);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
/**
|
||||
* @summary Carousels display an arbitrary number of content slides along a horizontal or vertical axis.
|
||||
*
|
||||
* @since 2.0
|
||||
* @since 2.2
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-icon
|
||||
@@ -239,7 +239,7 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
|
||||
slide.classList.remove('--in-view');
|
||||
slide.classList.remove('--is-active');
|
||||
slide.setAttribute('aria-label', this.localize.term('slide_num', index + 1));
|
||||
slide.setAttribute('aria-label', this.localize.term('slideNum', index + 1));
|
||||
|
||||
if (slide.hasAttribute('data-clone')) {
|
||||
slide.remove();
|
||||
@@ -344,7 +344,7 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
* @param behavior - The behavior used for scrolling.
|
||||
*/
|
||||
goToSlide(index: number, behavior: ScrollBehavior = 'smooth') {
|
||||
const { slidesPerPage, loop } = this;
|
||||
const { slidesPerPage, loop, scrollContainer } = this;
|
||||
|
||||
const slides = this.getSlides();
|
||||
const slidesWithClones = this.getSlides({ excludeClones: false });
|
||||
@@ -358,9 +358,12 @@ export default class SlCarousel extends ShoelaceElement {
|
||||
const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1);
|
||||
const nextSlide = slidesWithClones[nextSlideIndex];
|
||||
|
||||
this.scrollContainer.scrollTo({
|
||||
left: nextSlide.offsetLeft,
|
||||
top: nextSlide.offsetTop,
|
||||
const scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const nextSlideRect = nextSlide.getBoundingClientRect();
|
||||
|
||||
scrollContainer.scrollTo({
|
||||
left: nextSlideRect.left - scrollContainerRect.left + scrollContainer.scrollLeft,
|
||||
top: nextSlideRect.top - scrollContainerRect.top + scrollContainer.scrollTop,
|
||||
behavior: prefersReducedMotion() ? 'auto' : behavior
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
@debounce(100)
|
||||
handleScrollEnd() {
|
||||
if (!this.pointers.size) {
|
||||
// If no pointer is active in the scroll area then the scroll has ended
|
||||
this.scrolling = false;
|
||||
this.host.scrollContainer.dispatchEvent(
|
||||
new CustomEvent('scrollend', {
|
||||
@@ -78,6 +79,7 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
);
|
||||
this.host.requestUpdate();
|
||||
} else {
|
||||
// otherwise let's wait a bit more
|
||||
this.handleScrollEnd();
|
||||
}
|
||||
}
|
||||
@@ -87,35 +89,33 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollContainer = this.host.scrollContainer;
|
||||
this.pointers.add(event.pointerId);
|
||||
scrollContainer.setPointerCapture(event.pointerId);
|
||||
|
||||
if (this.mouseDragging && this.pointers.size === 1) {
|
||||
const canDrag = this.mouseDragging && !this.dragging && event.button === 0;
|
||||
if (canDrag) {
|
||||
event.preventDefault();
|
||||
scrollContainer.addEventListener('pointermove', this.handlePointerMove);
|
||||
|
||||
this.host.scrollContainer.addEventListener('pointermove', this.handlePointerMove);
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerMove(event: PointerEvent) {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
if (scrollContainer.hasPointerCapture(event.pointerId)) {
|
||||
if (!this.dragging) {
|
||||
this.handleDragStart();
|
||||
}
|
||||
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) {
|
||||
const host = this.host;
|
||||
const scrollContainer = host.scrollContainer;
|
||||
|
||||
this.pointers.delete(event.pointerId);
|
||||
scrollContainer.releasePointerCapture(event.pointerId);
|
||||
this.host.scrollContainer.releasePointerCapture(event.pointerId);
|
||||
|
||||
if (this.pointers.size === 0) {
|
||||
this.handleDragEnd();
|
||||
|
||||
@@ -11,7 +11,7 @@ export default css`
|
||||
.checkbox {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: top;
|
||||
align-items: flex-start;
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-weight: var(--sl-input-font-weight);
|
||||
color: var(--sl-input-label-color);
|
||||
|
||||
@@ -6,6 +6,20 @@ import type SlHideEvent from '../../events/sl-hide';
|
||||
import type SlShowEvent from '../../events/sl-show';
|
||||
|
||||
describe('<sl-details>', () => {
|
||||
describe('accessibility', () => {
|
||||
it('should be accessible when closed', async () => {
|
||||
const details = await fixture<SlDetails>(html`<sl-details summary="Test"> Test text </sl-details>`);
|
||||
|
||||
await expect(details).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should be accessible when open', async () => {
|
||||
const details = await fixture<SlDetails>(html`<sl-details open summary="Test">Test text</sl-details>`);
|
||||
|
||||
await expect(details).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture<SlDetails>(html`
|
||||
<sl-details open>
|
||||
|
||||
@@ -170,7 +170,7 @@ export default class SlDetails extends ShoelaceElement {
|
||||
'details--rtl': isRtl
|
||||
})}
|
||||
>
|
||||
<header
|
||||
<div
|
||||
part="header"
|
||||
id="header"
|
||||
class="details__header"
|
||||
@@ -192,10 +192,10 @@ export default class SlDetails extends ShoelaceElement {
|
||||
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
|
||||
</slot>
|
||||
</span>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="details__body">
|
||||
<slot part="content" id="content" class="details__content" role="region" aria-labelledby="header"></slot>
|
||||
<div class="details__body" role="region" aria-labelledby="header">
|
||||
<slot part="content" id="content" class="details__content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { getIconLibrary, unwatchIcon, watchIcon } from './library';
|
||||
import { html } from 'lit';
|
||||
import { requestIcon } from './request';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './icon.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
const CACHEABLE_ERROR = Symbol();
|
||||
const RETRYABLE_ERROR = Symbol();
|
||||
type SVGResult = SVGSVGElement | typeof RETRYABLE_ERROR | typeof CACHEABLE_ERROR;
|
||||
|
||||
let parser: DOMParser;
|
||||
const iconCache = new Map<string, Promise<SVGResult>>();
|
||||
|
||||
/**
|
||||
* @summary Icons are symbols that can be used to represent various options within an application.
|
||||
@@ -25,7 +27,37 @@ let parser: DOMParser;
|
||||
export default class SlIcon extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@state() private svg = '';
|
||||
/** Given a URL, this function returns the resulting SVG element or an appropriate error symbol. */
|
||||
private static async resolveIcon(url: string): Promise<SVGResult> {
|
||||
let fileData: Response;
|
||||
try {
|
||||
fileData = await fetch(url, { mode: 'cors' });
|
||||
if (!fileData.ok) return fileData.status === 410 ? CACHEABLE_ERROR : RETRYABLE_ERROR;
|
||||
} catch {
|
||||
return RETRYABLE_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = await fileData.text();
|
||||
|
||||
const svg = div.firstElementChild;
|
||||
if (svg?.tagName?.toLowerCase() !== 'svg') return CACHEABLE_ERROR;
|
||||
|
||||
if (!parser) parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svg.outerHTML, 'text/html');
|
||||
|
||||
const svgEl = doc.body.querySelector('svg');
|
||||
if (!svgEl) return CACHEABLE_ERROR;
|
||||
|
||||
svgEl.part.add('svg');
|
||||
return document.adoptNode(svgEl);
|
||||
} catch {
|
||||
return CACHEABLE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@state() private svg: SVGElement | null = null;
|
||||
|
||||
/** The name of the icon to draw. Available names depend on the icon library being used. */
|
||||
@property({ reflect: true }) name?: string;
|
||||
@@ -87,46 +119,42 @@ export default class SlIcon extends ShoelaceElement {
|
||||
const library = getIconLibrary(this.library);
|
||||
const url = this.getUrl();
|
||||
|
||||
// Create an instance of the DOM parser. We do it here instead of top-level to support SSR while maintaining a
|
||||
// single parser instance for optimal performance.
|
||||
if (!parser) {
|
||||
parser = new DOMParser();
|
||||
if (!url) {
|
||||
this.svg = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
const file = await requestIcon(url);
|
||||
if (url !== this.getUrl()) {
|
||||
// If the url has changed while fetching the icon, ignore this request
|
||||
return;
|
||||
} else if (file.ok) {
|
||||
const doc = parser.parseFromString(file.svg, 'text/html');
|
||||
const svgEl = doc.body.querySelector('svg');
|
||||
let iconResolver = iconCache.get(url);
|
||||
if (!iconResolver) {
|
||||
iconResolver = SlIcon.resolveIcon(url);
|
||||
iconCache.set(url, iconResolver);
|
||||
}
|
||||
|
||||
if (svgEl !== null) {
|
||||
svgEl.part.add('svg');
|
||||
library?.mutator?.(svgEl);
|
||||
this.svg = svgEl.outerHTML;
|
||||
this.emit('sl-load');
|
||||
} else {
|
||||
this.svg = '';
|
||||
this.emit('sl-error');
|
||||
}
|
||||
} else {
|
||||
this.svg = '';
|
||||
this.emit('sl-error');
|
||||
}
|
||||
} catch {
|
||||
const svg = await iconResolver;
|
||||
if (svg === RETRYABLE_ERROR) {
|
||||
iconCache.delete(url);
|
||||
}
|
||||
|
||||
if (url !== this.getUrl()) {
|
||||
// If the url has changed while fetching the icon, ignore this request
|
||||
return;
|
||||
}
|
||||
|
||||
switch (svg) {
|
||||
case RETRYABLE_ERROR:
|
||||
case CACHEABLE_ERROR:
|
||||
this.svg = null;
|
||||
this.emit('sl-error');
|
||||
}
|
||||
} else if (this.svg.length > 0) {
|
||||
// If we can't resolve a URL and an icon was previously set, remove it
|
||||
this.svg = '';
|
||||
break;
|
||||
default:
|
||||
this.svg = svg.cloneNode(true) as SVGElement;
|
||||
library?.mutator?.(this.svg);
|
||||
this.emit('sl-load');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${unsafeSVG(this.svg)} `;
|
||||
return this.svg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { requestInclude } from '../include/request';
|
||||
|
||||
type IconFile =
|
||||
| {
|
||||
ok: true;
|
||||
status: number;
|
||||
svg: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
status: number;
|
||||
svg: null;
|
||||
};
|
||||
|
||||
interface IconFileUnknown {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
svg: string | null;
|
||||
}
|
||||
|
||||
const iconFiles = new Map<string, IconFile>();
|
||||
|
||||
export async function requestIcon(url: string): Promise<IconFile> {
|
||||
if (iconFiles.has(url)) {
|
||||
return iconFiles.get(url)!;
|
||||
}
|
||||
const fileData = await requestInclude(url);
|
||||
const iconFileData: IconFileUnknown = {
|
||||
ok: fileData.ok,
|
||||
status: fileData.status,
|
||||
svg: null
|
||||
};
|
||||
if (fileData.ok) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = fileData.html;
|
||||
const svg = div.firstElementChild;
|
||||
iconFileData.svg = svg?.tagName.toLowerCase() === 'svg' ? svg.outerHTML : '';
|
||||
}
|
||||
|
||||
iconFiles.set(url, iconFileData as IconFile);
|
||||
return iconFileData as IconFile;
|
||||
}
|
||||
@@ -4,20 +4,26 @@ interface IncludeFile {
|
||||
html: string;
|
||||
}
|
||||
|
||||
const includeFiles = new Map<string, Promise<IncludeFile>>();
|
||||
const includeFiles = new Map<string, IncludeFile | Promise<IncludeFile>>();
|
||||
|
||||
/** Fetches an include file from a remote source. Caching is enabled so the origin is only pinged once. */
|
||||
export function requestInclude(src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors'): Promise<IncludeFile> {
|
||||
if (includeFiles.has(src)) {
|
||||
return includeFiles.get(src)!;
|
||||
const prev = includeFiles.get(src);
|
||||
if (prev !== undefined) {
|
||||
// Promise.resolve() transparently unboxes prev if it was a promise.
|
||||
return Promise.resolve(prev);
|
||||
}
|
||||
const fileDataPromise = fetch(src, { mode: mode }).then(async response => {
|
||||
return {
|
||||
const res = {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
html: await response.text()
|
||||
};
|
||||
// Replace the cached promise with its result to avoid having buggy browser Promises retain memory as mentioned in #1284 and #1249
|
||||
includeFiles.set(src, res);
|
||||
return res;
|
||||
});
|
||||
// Cache the promise to only fetch() once per src
|
||||
includeFiles.set(src, fileDataPromise);
|
||||
return fileDataPromise;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default css`
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host(:hover:not([aria-disabled='true'])) .menu-item {
|
||||
:host(:hover:not([aria-disabled='true'], :focus-visible)) .menu-item {
|
||||
background-color: var(--sl-color-neutral-100);
|
||||
color: var(--sl-color-neutral-1000);
|
||||
}
|
||||
|
||||
@@ -8,15 +8,31 @@ import type SlOption from '../option/option';
|
||||
import type SlSelect from './select';
|
||||
|
||||
describe('<sl-select>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<SlSelect>(html`
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
await expect(el).to.be.accessible();
|
||||
describe('accessibility', () => {
|
||||
it('should pass accessibility tests when closed', async () => {
|
||||
const select = await fixture<SlSelect>(html`
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
await expect(select).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should pass accessibility tests when open', async () => {
|
||||
const select = await fixture<SlSelect>(html`
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
`);
|
||||
|
||||
await select.show();
|
||||
|
||||
await expect(select).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
|
||||
@@ -821,7 +821,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<slot
|
||||
<div
|
||||
id="listbox"
|
||||
role="listbox"
|
||||
aria-expanded=${this.open ? 'true' : 'false'}
|
||||
@@ -832,7 +832,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
||||
tabindex="-1"
|
||||
@mouseup=${this.handleOptionClick}
|
||||
@slotchange=${this.handleDefaultSlotChange}
|
||||
></slot>
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</sl-popup>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,8 +2,17 @@ import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('<sl-split-panel>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-split-panel></sl-split-panel> `);
|
||||
const splitPanel = await fixture(html` <sl-split-panel></sl-split-panel> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
expect(splitPanel).to.exist;
|
||||
});
|
||||
|
||||
it('should be accessible', async () => {
|
||||
const splitPanel = await fixture(html`<sl-split-panel>
|
||||
<div slot="start">Start</div>
|
||||
<div slot="end">End</div>
|
||||
</sl-split-panel>`);
|
||||
|
||||
await expect(splitPanel).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -249,17 +249,21 @@ export default class SlSplitPanel extends ShoelaceElement {
|
||||
return html`
|
||||
<slot name="start" part="panel start" class="start"></slot>
|
||||
|
||||
<slot
|
||||
name="divider"
|
||||
<div
|
||||
part="divider"
|
||||
class="divider"
|
||||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||
role="separator"
|
||||
aria-valuenow=${this.position}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-label=${this.localize.term('resize')}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@mousedown=${this.handleDrag}
|
||||
@touchstart=${this.handleDrag}
|
||||
></slot>
|
||||
>
|
||||
<slot name="divider"></slot>
|
||||
</div>
|
||||
|
||||
<slot name="end" part="panel end" class="end"></slot>
|
||||
`;
|
||||
|
||||
@@ -70,6 +70,11 @@ export default class SlTabGroup extends ShoelaceElement {
|
||||
@property({ attribute: 'no-scroll-controls', type: Boolean }) noScrollControls = false;
|
||||
|
||||
connectedCallback() {
|
||||
const whenAllDefined = Promise.allSettled([
|
||||
customElements.whenDefined('sl-tab'),
|
||||
customElements.whenDefined('sl-tab-panel')
|
||||
]);
|
||||
|
||||
super.connectedCallback();
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
@@ -89,20 +94,24 @@ export default class SlTabGroup extends ShoelaceElement {
|
||||
}
|
||||
});
|
||||
|
||||
// After the first update...
|
||||
this.updateComplete.then(() => {
|
||||
this.syncTabsAndPanels();
|
||||
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
|
||||
this.resizeObserver.observe(this.nav);
|
||||
|
||||
// Set initial tab state when the tabs first become visible
|
||||
const intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (entries[0].intersectionRatio > 0) {
|
||||
this.setAriaLabels();
|
||||
this.setActiveTab(this.getActiveTab() ?? this.tabs[0], { emitEvents: false });
|
||||
observer.unobserve(entries[0].target);
|
||||
}
|
||||
// Wait for tabs and tab panels to be registered
|
||||
whenAllDefined.then(() => {
|
||||
// Set initial tab state when the tabs become visible
|
||||
const intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||
if (entries[0].intersectionRatio > 0) {
|
||||
this.setAriaLabels();
|
||||
this.setActiveTab(this.getActiveTab() ?? this.tabs[0], { emitEvents: false });
|
||||
observer.unobserve(entries[0].target);
|
||||
}
|
||||
});
|
||||
intersectionObserver.observe(this.tabGroup);
|
||||
});
|
||||
intersectionObserver.observe(this.tabGroup);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export default css`
|
||||
color: var(--sl-color-neutral-700);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-item__checkbox {
|
||||
@@ -39,7 +38,7 @@ export default css`
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
line-height: var(--sl-line-height-normal);
|
||||
line-height: var(--sl-line-height-dense);
|
||||
letter-spacing: var(--sl-letter-spacing-normal);
|
||||
}
|
||||
|
||||
@@ -63,6 +62,7 @@ export default css`
|
||||
padding: var(--sl-spacing-x-small);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -263,12 +263,11 @@ export default class SlTreeItem extends ShoelaceElement {
|
||||
?disabled="${this.disabled}"
|
||||
?checked="${live(this.selected)}"
|
||||
?indeterminate="${this.indeterminate}"
|
||||
>
|
||||
<slot class="tree-item__label" part="label"></slot>
|
||||
</sl-checkbox>
|
||||
`,
|
||||
() => html` <slot class="tree-item__label" part="label"></slot> `
|
||||
></sl-checkbox>
|
||||
`
|
||||
)}
|
||||
|
||||
<slot class="tree-item__label" part="label"></slot>
|
||||
</div>
|
||||
|
||||
<slot
|
||||
|
||||
@@ -19,7 +19,7 @@ const reportValidityOverloads: WeakMap<HTMLFormElement, () => boolean> = new Wea
|
||||
// We store a Set of controls that users have interacted with. This allows us to determine the interaction state
|
||||
// without littering the DOM with additional data attributes.
|
||||
//
|
||||
const userInteractedControls: Set<ShoelaceFormControl> = new Set();
|
||||
const userInteractedControls: WeakSet<ShoelaceFormControl> = new WeakSet();
|
||||
|
||||
//
|
||||
// We store a WeakMap of interactions for each form control so we can track when all conditions are met for validation.
|
||||
|
||||
@@ -32,7 +32,7 @@ export function unlockBodyScrolling(lockingEl: HTMLElement) {
|
||||
|
||||
if (locks.size === 0) {
|
||||
document.body.classList.remove('sl-scroll-lock');
|
||||
document.body.style.removeProperty('--sl-scrollbar-width');
|
||||
document.body.style.removeProperty('--sl-scroll-lock-size');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,16 @@ const observer = new MutationObserver(mutations => {
|
||||
/**
|
||||
* 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('-');
|
||||
export async function discover(root: Element | ShadowRoot) {
|
||||
const rootTagName = root instanceof Element ? 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());
|
||||
tags.push(rootTagName);
|
||||
}
|
||||
|
||||
// Make the list unique
|
||||
|
||||
@@ -28,7 +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}`,
|
||||
slideNum: slide => `Slide ${slide}`,
|
||||
toggleColorFormat: 'Skift farveformat'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Zum Anfang scrollen',
|
||||
selectAColorFromTheScreen: 'Wähle eine Farbe vom Bildschirm',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
slide_num: slide => `Gleiten ${slide}`,
|
||||
slideNum: slide => `Gleiten ${slide}`,
|
||||
toggleColorFormat: 'Farbformat umschalten'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Scroll to start',
|
||||
selectAColorFromTheScreen: 'Select a color from the screen',
|
||||
showPassword: 'Show password',
|
||||
slide_num: slide => `Slide ${slide}`,
|
||||
slideNum: slide => `Slide ${slide}`,
|
||||
toggleColorFormat: 'Toggle color format'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +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}`,
|
||||
slideNum: slide => `Diapositiva ${slide}`,
|
||||
toggleColorFormat: 'Alternar formato de color'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'پیمایش به ابتدا',
|
||||
selectAColorFromTheScreen: 'انتخاب یک رنگ از صفحه نمایش',
|
||||
showPassword: 'نمایش رمز',
|
||||
slide_num: slide => `اسلاید ${slide}`,
|
||||
slideNum: slide => `اسلاید ${slide}`,
|
||||
toggleColorFormat: 'تغییر قالب رنگ'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +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 => `Glisser ${slide}`,
|
||||
slideNum: slide => `Diapositive ${slide}`,
|
||||
toggleColorFormat: 'Changer le format de couleur'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'גלול להתחלה',
|
||||
selectAColorFromTheScreen: 'בחור צבע מהמסך',
|
||||
showPassword: 'הראה סיסמה',
|
||||
slide_num: slide => `שקופית ${slide}`,
|
||||
slideNum: slide => `שקופית ${slide}`,
|
||||
toggleColorFormat: 'החלף פורמט צבע'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +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`,
|
||||
slideNum: slide => `${slide}. dia`,
|
||||
toggleColorFormat: 'Színformátum változtatása'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: '最初にスクロールする',
|
||||
selectAColorFromTheScreen: '画面から色を選択してください',
|
||||
showPassword: 'パスワードを表示',
|
||||
slide_num: slide => `スライド ${slide}`,
|
||||
slideNum: slide => `スライド ${slide}`,
|
||||
toggleColorFormat: '色のフォーマットを切り替える'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +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}`,
|
||||
slideNum: slide => `Schuif ${slide}`,
|
||||
toggleColorFormat: 'Wissel kleurnotatie'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Przewiń do początku',
|
||||
selectAColorFromTheScreen: 'Próbkuj z ekranu',
|
||||
showPassword: 'Pokaż hasło',
|
||||
slide_num: slide => `Slajd ${slide}`,
|
||||
slideNum: slide => `Slajd ${slide}`,
|
||||
toggleColorFormat: 'Przełącz format'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +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}`,
|
||||
slideNum: slide => `Diapositivo ${slide}`,
|
||||
toggleColorFormat: 'Trocar o formato de cor'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Пролистать к началу',
|
||||
selectAColorFromTheScreen: 'Выберите цвет на экране',
|
||||
showPassword: 'Показать пароль',
|
||||
slide_num: slide => `Слайд ${slide}`,
|
||||
slideNum: slide => `Слайд ${slide}`,
|
||||
toggleColorFormat: 'Переключить цветовую модель'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +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}`,
|
||||
slideNum: slide => `Bild ${slide}`,
|
||||
toggleColorFormat: 'Växla färgformat'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: 'Başa kay',
|
||||
selectAColorFromTheScreen: 'Ekrandan bir renk seçin',
|
||||
showPassword: 'Şifreyi göster',
|
||||
slide_num: slide => `Slayt ${slide}`,
|
||||
slideNum: slide => `Slayt ${slide}`,
|
||||
toggleColorFormat: 'Renk biçimini değiştir'
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const translation: Translation = {
|
||||
scrollToStart: '捲至頁首',
|
||||
selectAColorFromTheScreen: '從螢幕中選擇一種顏色',
|
||||
showPassword: '顯示密碼',
|
||||
slide_num: slide => `幻燈片 ${slide}`,
|
||||
slideNum: slide => `幻燈片 ${slide}`,
|
||||
toggleColorFormat: '切換顏色格式'
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,6 @@ export interface Translation extends DefaultTranslation {
|
||||
scrollToStart: string;
|
||||
selectAColorFromTheScreen: string;
|
||||
showPassword: string;
|
||||
slide_num: (slide: number) => string;
|
||||
slideNum: (slide: number) => string;
|
||||
toggleColorFormat: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user