mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
46 Commits
v2.0.0-bet
...
context-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee519d40a | ||
|
|
6bc17d48c3 | ||
|
|
a1263f1b9d | ||
|
|
d69ebab765 | ||
|
|
0504946dac | ||
|
|
fbd6691711 | ||
|
|
aec17da6b0 | ||
|
|
639533662d | ||
|
|
a340ce4a68 | ||
|
|
6e5fe64e8b | ||
|
|
84bdbb84b8 | ||
|
|
f91ffb6cb4 | ||
|
|
13815199a3 | ||
|
|
98c20ff551 | ||
|
|
479b6b9081 | ||
|
|
c640d2ea77 | ||
|
|
715547d2fd | ||
|
|
8a914a536b | ||
|
|
f56b6c0648 | ||
|
|
25aa8318d9 | ||
|
|
72f2cbe9e8 | ||
|
|
fc7836084a | ||
|
|
60d9d9202b | ||
|
|
a9df468282 | ||
|
|
0bba773c3e | ||
|
|
7be03ae623 | ||
|
|
d4741532f5 | ||
|
|
10f31efefa | ||
|
|
be662ddf32 | ||
|
|
ff84beaade | ||
|
|
8dba8fa5fb | ||
|
|
3a3f5552a7 | ||
|
|
88cba353c0 | ||
|
|
a2851370bb | ||
|
|
7c0ef7dcf0 | ||
|
|
fb6d5d89b8 | ||
|
|
45ceff4c08 | ||
|
|
6169abc700 | ||
|
|
c09e12d13e | ||
|
|
6152e15e10 | ||
|
|
9a19cc2173 | ||
|
|
c4cbc894f5 | ||
|
|
449f5e6c7f | ||
|
|
34447a3f2f | ||
|
|
eee97d7dba | ||
|
|
f16392947a |
@@ -21,6 +21,7 @@
|
||||
- [Card](/components/card)
|
||||
- [Checkbox](/components/checkbox)
|
||||
- [Color Picker](/components/color-picker)
|
||||
- [Context Menu](/components/context-menu)
|
||||
- [Details](/components/details)
|
||||
- [Dialog](/components/dialog)
|
||||
- [Divider](/components/divider)
|
||||
@@ -54,6 +55,7 @@
|
||||
<!--plop:component-->
|
||||
|
||||
- Utilities
|
||||
- [Animated Image](/components/animated-image)
|
||||
- [Animation](/components/animation)
|
||||
- [Format Bytes](/components/format-bytes)
|
||||
- [Format Date](/components/format-date)
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 709 KiB After Width: | Height: | Size: 688 KiB |
BIN
docs/assets/images/tie.webp
Normal file
BIN
docs/assets/images/tie.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/assets/images/walk.gif
Normal file
BIN
docs/assets/images/walk.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -39,7 +39,7 @@ body.site-search-visible {
|
||||
flex-direction: column;
|
||||
max-width: 460px;
|
||||
max-height: calc(100vh - 20rem);
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
background-color: rgb(var(--sl-surface-base-alt));
|
||||
border-radius: var(--sl-border-radius-large);
|
||||
box-shadow: var(--sl-shadow-x-large);
|
||||
margin: 10rem auto;
|
||||
|
||||
63
docs/components/animated-image.md
Normal file
63
docs/components/animated-image.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Animated Image
|
||||
|
||||
[component-header:sl-animated-image]
|
||||
|
||||
A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/walk.gif"
|
||||
alt="Animation of untied shoes walking on pavement"
|
||||
></sl-animated-image>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### WEBP Images
|
||||
|
||||
Both GIF and WEBP images are supported.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/tie.webp"
|
||||
alt="Animation of a shoe being tied"
|
||||
></sl-animated-image>
|
||||
```
|
||||
|
||||
### Setting a Width and Height
|
||||
|
||||
To set a custom size, apply a width and/or height to the host element.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/walk.gif"
|
||||
alt="Animation of untied shoes walking on pavement"
|
||||
style="width: 150px; height: 200px;"
|
||||
>
|
||||
</sl-animated-image>
|
||||
```
|
||||
|
||||
### Customizing the Control Box
|
||||
|
||||
You can change the appearance and location of the control box by targeting the `control-box` part in your styles.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/walk.gif"
|
||||
alt="Animation of untied shoes walking on pavement"
|
||||
class="animated-image-custom-control-box"
|
||||
></sl-animated-image>
|
||||
|
||||
<style>
|
||||
.animated-image-custom-control-box::part(control-box) {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
background-color: deeppink;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-animated-image]
|
||||
140
docs/components/context-menu.md
Normal file
140
docs/components/context-menu.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Context Menu
|
||||
|
||||
[component-header:sl-context-menu]
|
||||
|
||||
Context menus offer additional options through a menu that opens at the pointer's location, usually activated by a right-click.
|
||||
|
||||
Context menus are designed to work with [menus](/components/menu) and [menu items](/components/menu-item). The menu must include `slot="menu"`. Other content you provide will be part of the context menu's target area.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu>
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Handling Selections
|
||||
|
||||
The [menu component](/components/menu) emits an `sl-select` event when a menu item is selected. You can use this to handle selections. The selected item will be available in `event.detail.item`.
|
||||
|
||||
```html preview
|
||||
<div class="context-menu-selections">
|
||||
<sl-context-menu>
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.context-menu-selections');
|
||||
const menu = container.querySelector('sl-menu');
|
||||
const result = container.querySelector('.result');
|
||||
|
||||
menu.addEventListener('sl-select', event => {
|
||||
console.log(`You selected: ${event.detail.item.value}`);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Inline
|
||||
|
||||
The context menu uses `display: contents`, so it will assume the shape of the content you slot in.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu>
|
||||
<span style="background: rgb(var(--sl-color-neutral-100)); padding: .5rem 1rem;">
|
||||
Right-click here
|
||||
</span>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the context menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the menu remains in the viewport.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu placement="top-end">
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
### Detecting the Target Item
|
||||
|
||||
A single context menu can wrap a number of items. To detect the item that activated the context menu...
|
||||
|
||||
TODO
|
||||
|
||||
```html preview
|
||||
<div class="context-menu-detecting">
|
||||
<sl-context-menu>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
<li>Item 4</li>
|
||||
<li>Item 5</li>
|
||||
</ul>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context-menu-detecting ul {
|
||||
max-width: 300px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.context-menu-detecting li {
|
||||
background: rgb(var(--sl-color-neutral-100));
|
||||
padding: .5rem 1rem;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-context-menu]
|
||||
@@ -33,6 +33,33 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
|
||||
|
||||
## Examples
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
|
||||
@@ -121,57 +148,4 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
|
||||
</style>
|
||||
```
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection-alt">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection-alt');
|
||||
const cut = container.querySelector('sl-menu-item[value="cut"]');
|
||||
const copy = container.querySelector('sl-menu-item[value="copy"]');
|
||||
const paste = container.querySelector('sl-menu-item[value="paste"]');
|
||||
|
||||
cut.addEventListener('click', () => console.log('cut'));
|
||||
copy.addEventListener('click', () => console.log('copy'));
|
||||
paste.addEventListener('click', () => console.log('paste'));
|
||||
</script>
|
||||
```
|
||||
|
||||
[component-metadata:sl-dropdown]
|
||||
|
||||
@@ -20,10 +20,18 @@ Use the `--height` custom property to set the progress bar's height.
|
||||
|
||||
### Labels
|
||||
|
||||
Use the default slot to show a label.
|
||||
Use the `label` attribute to label the progress bar and tell assistive devices how to announce it.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50" class="progress-bar-labels">50%</sl-progress-bar>
|
||||
<sl-progress-bar value="50" label="Upload progress"></sl-progress-bar>
|
||||
```
|
||||
|
||||
### Showing Values
|
||||
|
||||
Use the default slot to show a value.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50" class="progress-bar-values">50%</sl-progress-bar>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -31,7 +39,7 @@ Use the default slot to show a label.
|
||||
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
|
||||
|
||||
<script>
|
||||
const progressBar = document.querySelector('.progress-bar-labels');
|
||||
const progressBar = document.querySelector('.progress-bar-values');
|
||||
const subtractButton = progressBar.nextElementSibling.nextElementSibling;
|
||||
const addButton = subtractButton.nextElementSibling;
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ Use the `--size` custom property to set the diameter of the progress ring.
|
||||
|
||||
### Track Width
|
||||
|
||||
Use the `track-width` attribute to set the width of the progress ring's track.
|
||||
Use the `--track-width` custom property to set the width of the progress ring's track.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" stroke-width="10"></sl-progress-ring>
|
||||
<sl-progress-ring value="50" style="--track-width: 10px;"></sl-progress-ring>
|
||||
```
|
||||
|
||||
### Colors
|
||||
@@ -42,10 +42,18 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to label the progress ring and tell assistive devices how to announce it.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" label="Upload progress"></sl-progress-ring>
|
||||
```
|
||||
|
||||
### Showing Values
|
||||
|
||||
Use the default slot to show a label.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" class="progress-ring-labels" style="margin-bottom: .5rem;">50%</sl-progress-ring>
|
||||
<sl-progress-ring value="50" class="progress-ring-values" style="margin-bottom: .5rem;">50%</sl-progress-ring>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -53,7 +61,7 @@ Use the default slot to show a label.
|
||||
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
|
||||
|
||||
<script>
|
||||
const progressRing = document.querySelector('.progress-ring-labels');
|
||||
const progressRing = document.querySelector('.progress-ring-values');
|
||||
const subtractButton = progressRing.nextElementSibling.nextElementSibling;
|
||||
const addButton = subtractButton.nextElementSibling;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The easiest way to install Shoelace is with the CDN. Just add the following tags
|
||||
|
||||
```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/+esm"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
### Dark Theme
|
||||
@@ -16,8 +16,8 @@ The easiest way to install Shoelace is with the CDN. Just add the following tags
|
||||
If you prefer to use the dark theme instead, use this. Note the `sl-theme-dark` class on the `<html>` element. [Learn more about the Dark Theme.](/getting-started/themes#dark-theme)
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css">
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js/+esm"></script>
|
||||
<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
|
||||
@@ -29,7 +29,7 @@ If you want to load the light or dark theme based on the user's `prefers-color-s
|
||||
<link rel="stylesheet" media="(prefers-color-scheme:dark)"
|
||||
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css"
|
||||
onload="document.documentElement.classList.add('sl-theme-dark');">
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js/+esm"></script>
|
||||
<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)
|
||||
@@ -79,7 +79,7 @@ However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Sh
|
||||
|
||||
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, but 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 and register each component manually.
|
||||
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 component manually.
|
||||
|
||||
Here's an example that loads only the button component. Again, if you're not using a module resolver, you'll need to adjust the path to point to the folder Shoelace is in.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Add the following code to your page.
|
||||
|
||||
```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/+esm"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
Now you have access to all of Shoelace's components! Try adding a button:
|
||||
|
||||
@@ -6,6 +6,35 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge
|
||||
|
||||
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
|
||||
|
||||
## Next
|
||||
|
||||
- Added experimental `<sl-context-menu>` component
|
||||
- Added eye dropper to `<sl-color-picker>` when the browser supports the [EyeDropper API](https://wicg.github.io/eyedropper-api/)
|
||||
- Fixed a bug in `<sl-button-group>` where buttons groups with only one button would have an incorrect border radius
|
||||
- Improved the `<sl-color-picker>` trigger's border in dark mode
|
||||
- Refactored positioning logic in `<sl-dropdown>` so Popper is only active when the menu is open
|
||||
- Updated to Lit 2.0.2
|
||||
|
||||
## 2.0.0-beta.58
|
||||
|
||||
This version once again restores the bundled distribution because the unbundled + CDN approach is currently confusing and [not working properly](https://github.com/shoelace-style/shoelace/issues/559#issuecomment-949662331). Unbundling the few dependencies Shoelace has is still a goal of the project, but [this jsDelivr bug](https://github.com/jsdelivr/jsdelivr/issues/18337) needs to be resolved before we can achieve it.
|
||||
|
||||
I sincerely apologize for the instability of the last few beta releases as a result of this effort.
|
||||
|
||||
- Added experimental `<sl-animated-image>` component
|
||||
- Added `label` attribute to `<sl-progress-bar>` and `<sl-progress-ring>` to improve a11y
|
||||
- Fixed a bug where the tooltip would show briefly when clicking a disabled `<sl-range>`
|
||||
- Fixed a bug that caused a console error when `<sl-range>` was used
|
||||
- Fixed a bug where the `nav` part in `<sl-tab-group>` was on the incorrect element [#563](https://github.com/shoelace-style/shoelace/pull/563)
|
||||
- Fixed a bug where non-integer aspect ratios were calculated incorrectly in `<sl-responsive-media>`
|
||||
- Fixed a bug in `<sl-range>` where setting `value` wouldn't update the active and inactive portion of the track [#572](https://github.com/shoelace-style/shoelace/pull/572)
|
||||
- Reverted to publishing the bundled dist and removed `/+esm` links from the docs
|
||||
- Updated to Bootstrap Icons to 1.6.1
|
||||
|
||||
## 2.0.0-beta.57
|
||||
|
||||
- Fix CodePen links and CDN links
|
||||
|
||||
## 2.0.0-beta.56
|
||||
|
||||
This release is the second attempt at unbundling dependencies. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected, but note the URLs for modules on the CDN must have the `/+esm` now.
|
||||
|
||||
@@ -14,6 +14,7 @@ The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) i
|
||||
- Learn more about the project, its values, and its roadmap
|
||||
|
||||
<sl-button type="primary" href="https://github.com/shoelace-style/shoelace/discussions" target="_blank">
|
||||
<sl-icon name="github" slot="prefix"></sl-icon>
|
||||
Join the Discussion
|
||||
</sl-button>
|
||||
|
||||
@@ -27,9 +28,19 @@ The [community chat](https://discord.gg/mg8f26C) is open to the public and power
|
||||
- Chat live with other designers, developers, and Shoelace fans
|
||||
|
||||
<sl-button type="primary" href="https://discord.gg/mg8f26C" target="_blank">
|
||||
<sl-icon name="discord" slot="prefix"></sl-icon>
|
||||
Join the Chat
|
||||
</sl-button>
|
||||
|
||||
## Stack Overflow
|
||||
|
||||
You can post questions on Stack Overflow using [the "shoelace" tag](https://stackoverflow.com/questions/tagged/shoelace). This is a public forum where talented developers answer questions. It's a great way to get help, but it is not maintained or actively monitored by the Shoelace author.
|
||||
|
||||
<sl-button type="primary" href="https://stackoverflow.com/questions/ask?tags=shoelace" target="_blank">
|
||||
<sl-icon name="stack-overflow" slot="prefix"></sl-icon>
|
||||
Ask for Help
|
||||
</sl-button>
|
||||
|
||||
## Twitter
|
||||
|
||||
Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for general updates and announcements about Shoelace. This is a great place to say "hi" or to share something you're working on. You're also welcome to follow [@claviska](https://twitter.com/claviska), the creator, for tweets about web components, web development, and life.
|
||||
@@ -37,5 +48,6 @@ Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for gene
|
||||
**Please avoid using Twitter for support questions.** The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) is a much better place to share code snippets, screenshots, and other troubleshooting info. You'll have much better luck there, as more users will have a chance to help you.
|
||||
|
||||
<sl-button type="primary" href="https://twitter.com/shoelace_style" target="_blank">
|
||||
<sl-icon name="twitter" slot="prefix"></sl-icon>
|
||||
Follow on Twitter
|
||||
</sl-button>
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.0.0-beta.56",
|
||||
"version": "2.0.0-beta.58",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.0.0-beta.56",
|
||||
"version": "2.0.0-beta.58",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.7.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"color": "^3.1.3",
|
||||
"lit": "^2.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -23,7 +22,7 @@
|
||||
"@web/test-runner": "^0.13.5",
|
||||
"@web/test-runner-puppeteer": "^0.10.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"bootstrap-icons": "^1.4.1",
|
||||
"bootstrap-icons": "^1.6.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
"chalk": "^4.1.0",
|
||||
"command-line-args": "^5.2.0",
|
||||
@@ -36,6 +35,7 @@
|
||||
"get-port": "^5.1.1",
|
||||
"globby": "^11.0.4",
|
||||
"husky": "^4.3.8",
|
||||
"lit": "^2.0.2",
|
||||
"lunr": "^2.3.9",
|
||||
"mkdirp": "^0.5.5",
|
||||
"plop": "^2.7.4",
|
||||
@@ -176,7 +176,8 @@
|
||||
"node_modules/@lit/reactive-element": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA=="
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@mdn/browser-compat-data": {
|
||||
"version": "3.3.5",
|
||||
@@ -801,7 +802,8 @@
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
@@ -1663,9 +1665,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bootstrap-icons": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
|
||||
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.6.1.tgz",
|
||||
"integrity": "sha512-MNpF89+njCdVJePDRbCd2DrUusqIyNsPlBrdKqBEXAvFZpwb+Gc8k2VlyF2ueiDQn1PoeTSg9UqQNgx8tGqHAA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -6144,9 +6146,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0.tgz",
|
||||
"integrity": "sha512-pqi5O/wVzQ9Bn4ERRoYQlt1EAUWyY5Wv888vzpoArbtChc+zfUv1XohRqSdtQZYCogl0eHKd+MQwymg2XJfECg==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.2.tgz",
|
||||
"integrity": "sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-element": "^3.0.0",
|
||||
@@ -6157,6 +6160,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0.tgz",
|
||||
"integrity": "sha512-oPqRhhBBhs+AlI62QLwtWQNU/bNK/h2L1jI3IDroqZubo6XVAkyNy2dW3CRfjij8mrNlY7wULOfyyKKOnfEePA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-html": "^2.0.0"
|
||||
@@ -6166,6 +6170,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0.tgz",
|
||||
"integrity": "sha512-tJsCapCmc0vtLj6harqd6HfCxnlt/RSkgowtz4SC9dFE3nSL38Tb33I5HMDiyJsRjQZRTgpVsahrnDrR9wg27w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
@@ -11291,7 +11296,8 @@
|
||||
"@lit/reactive-element": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA=="
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA==",
|
||||
"dev": true
|
||||
},
|
||||
"@mdn/browser-compat-data": {
|
||||
"version": "3.3.5",
|
||||
@@ -11899,7 +11905,8 @@
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
@@ -12572,9 +12579,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"bootstrap-icons": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
|
||||
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.6.1.tgz",
|
||||
"integrity": "sha512-MNpF89+njCdVJePDRbCd2DrUusqIyNsPlBrdKqBEXAvFZpwb+Gc8k2VlyF2ueiDQn1PoeTSg9UqQNgx8tGqHAA==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
@@ -16166,9 +16173,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"lit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0.tgz",
|
||||
"integrity": "sha512-pqi5O/wVzQ9Bn4ERRoYQlt1EAUWyY5Wv888vzpoArbtChc+zfUv1XohRqSdtQZYCogl0eHKd+MQwymg2XJfECg==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.2.tgz",
|
||||
"integrity": "sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-element": "^3.0.0",
|
||||
@@ -16179,6 +16187,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0.tgz",
|
||||
"integrity": "sha512-oPqRhhBBhs+AlI62QLwtWQNU/bNK/h2L1jI3IDroqZubo6XVAkyNy2dW3CRfjij8mrNlY7wULOfyyKKOnfEePA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-html": "^2.0.0"
|
||||
@@ -16188,6 +16197,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0.tgz",
|
||||
"integrity": "sha512-tJsCapCmc0vtLj6harqd6HfCxnlt/RSkgowtz4SC9dFE3nSL38Tb33I5HMDiyJsRjQZRTgpVsahrnDrR9wg27w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
|
||||
15
package.json
15
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.0.0-beta.56",
|
||||
"version": "2.0.0-beta.58",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
"main": "dist/shoelace.js",
|
||||
"module": "dist/shoelace.js",
|
||||
"customElements": "docs/dist/custom-elements.json",
|
||||
"customElements": "dist/custom-elements.json",
|
||||
"type": "module",
|
||||
"types": "dist/shoelace.d.ts",
|
||||
"files": [
|
||||
@@ -30,10 +30,9 @@
|
||||
"url": "https://github.com/sponsors/claviska"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/build.js --dir docs/dist --bundle --serve",
|
||||
"build": "node scripts/build.js --types",
|
||||
"build-docs": "node scripts/build.js --dir docs/dist --bundle",
|
||||
"prepublishOnly": "npm run build && npm run build-docs && npm run test",
|
||||
"start": "node scripts/build.js --bundle --serve",
|
||||
"build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"",
|
||||
"prepublishOnly": "npm run build && npm run test",
|
||||
"prettier": "prettier --write --loglevel warn .",
|
||||
"create": "plop --plopfile scripts/plop/plopfile.cjs",
|
||||
"test": "web-test-runner \"src/**/*.test.ts\" --node-resolve --puppeteer",
|
||||
@@ -43,7 +42,6 @@
|
||||
"@popperjs/core": "^2.7.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"color": "^3.1.3",
|
||||
"lit": "^2.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -54,7 +52,7 @@
|
||||
"@web/test-runner": "^0.13.5",
|
||||
"@web/test-runner-puppeteer": "^0.10.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"bootstrap-icons": "^1.4.1",
|
||||
"bootstrap-icons": "^1.6.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
"chalk": "^4.1.0",
|
||||
"command-line-args": "^5.2.0",
|
||||
@@ -67,6 +65,7 @@
|
||||
"get-port": "^5.1.1",
|
||||
"globby": "^11.0.4",
|
||||
"husky": "^4.3.8",
|
||||
"lit": "^2.0.2",
|
||||
"lunr": "^2.3.9",
|
||||
"mkdirp": "^0.5.5",
|
||||
"plop": "^2.7.4",
|
||||
|
||||
@@ -15,16 +15,15 @@ import { execSync } from 'child_process';
|
||||
const build = esbuild.build;
|
||||
const bs = browserSync.create();
|
||||
|
||||
const { bundle, dir, serve, types } = commandLineArgs([
|
||||
const { bundle, copydir, dir, serve, types } = commandLineArgs([
|
||||
{ name: 'bundle', type: Boolean },
|
||||
{ name: 'copydir', type: String },
|
||||
{ name: 'dir', type: String, defaultValue: 'dist' },
|
||||
{ name: 'serve', type: Boolean },
|
||||
{ name: 'bundle', type: Boolean },
|
||||
{ name: 'types', type: Boolean }
|
||||
]);
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
const rootDir = path.dirname(__dirname);
|
||||
const outdir = path.relative(rootDir, dir);
|
||||
const outdir = dir;
|
||||
|
||||
del.sync(outdir);
|
||||
mkdirp.sync(outdir);
|
||||
@@ -65,12 +64,8 @@ mkdirp.sync(outdir);
|
||||
},
|
||||
bundle: true,
|
||||
//
|
||||
// We don't bundle certain dependencies in the production build. This ensures the dist ships with bare module
|
||||
// specifiers, allowing end users to optimize better. jsDelivr understands this if you add /+esm to the URL. Note
|
||||
// that we can't bundle packages that don't ship ESM. https://github.com/jsdelivr/jsdelivr/issues/18263
|
||||
//
|
||||
// We still bundle for the dev environment and the docs build since we don't use a CDN for those. Once import maps
|
||||
// are better supported, we can adjust for that and use the same build again. https://caniuse.com/import-maps
|
||||
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
|
||||
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
|
||||
//
|
||||
external: bundle ? undefined : ['@popperjs/core', '@shoelace-style/animations', 'lit', 'qr-creator'],
|
||||
splitting: true,
|
||||
@@ -81,6 +76,12 @@ mkdirp.sync(outdir);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Copy the build output to an additional directory
|
||||
if (copydir) {
|
||||
del.sync(copydir);
|
||||
copy(outdir, copydir);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`The build has been generated at ${outdir} 📦\n`));
|
||||
|
||||
// Dev server
|
||||
@@ -89,6 +90,9 @@ mkdirp.sync(outdir);
|
||||
port: getPort.makeRange(4000, 4999)
|
||||
});
|
||||
|
||||
// Make sure docs/dist is empty since we're serving it virtually
|
||||
del.sync('docs/dist');
|
||||
|
||||
console.log(chalk.cyan(`Launching the Shoelace dev server at http://localhost:${port}! 🥾\n`));
|
||||
|
||||
// Launch browser sync
|
||||
@@ -102,7 +106,10 @@ mkdirp.sync(outdir);
|
||||
single: true,
|
||||
ghostMode: false,
|
||||
server: {
|
||||
baseDir: 'docs'
|
||||
baseDir: 'docs',
|
||||
routes: {
|
||||
'/dist': './dist'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,7 +119,7 @@ mkdirp.sync(outdir);
|
||||
buildResult
|
||||
// Rebuild and reload
|
||||
.rebuild()
|
||||
.then(async () => {
|
||||
.then(() => {
|
||||
// Rebuild stylesheets when a theme file changes
|
||||
if (/^src\/themes/.test(filename)) {
|
||||
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
@@ -126,7 +133,9 @@ mkdirp.sync(outdir);
|
||||
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
})
|
||||
.then(() => bs.reload())
|
||||
.then(() => {
|
||||
bs.reload();
|
||||
})
|
||||
.catch(err => console.error(chalk.red(err)));
|
||||
});
|
||||
|
||||
|
||||
52
src/components/animated-image/animated-image.styles.ts
Normal file
52
src/components/animated-image/animated-image.styles.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--control-box-size: 2.5rem;
|
||||
--icon-size: calc(var(--control-box-size) * 0.625);
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img[aria-hidden='true'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.animated-image__control-box {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: calc(50% - var(--control-box-size) / 2);
|
||||
right: calc(50% - var(--control-box-size) / 2);
|
||||
width: var(--control-box-size);
|
||||
height: var(--control-box-size);
|
||||
font-size: var(--icon-size);
|
||||
background: none;
|
||||
border: none;
|
||||
background-color: rgb(var(--sl-color-neutral-1000) / 50%);
|
||||
border-radius: var(--sl-border-radius-circle);
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
pointer-events: none;
|
||||
transition: var(--sl-transition-fast) opacity;
|
||||
}
|
||||
|
||||
:host([play]:hover) .animated-image__control-box {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:host([play]:not(:hover)) .animated-image__control-box {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
13
src/components/animated-image/animated-image.test.ts
Normal file
13
src/components/animated-image/animated-image.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
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> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
120
src/components/animated-image/animated-image.ts
Normal file
120
src/components/animated-image/animated-image.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { emit } from '../../internal/event';
|
||||
import styles from './animated-image.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-load - Emitted when the image loads successfully.
|
||||
* @event sl-error - Emitted when the image fails to load.
|
||||
*
|
||||
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
* @part - play-icon - The icon to use for the play button.
|
||||
* @part - pause-icon - The icon to use for the pause button.
|
||||
*
|
||||
* @cssproperty --control-box-size - The size of the icon box.
|
||||
* @cssproperty --icon-size - The size of the play/pause icons.
|
||||
*/
|
||||
@customElement('sl-animated-image')
|
||||
export default class SlAnimatedImage extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@state() frozenFrame: string;
|
||||
@state() isLoaded = false;
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
||||
/** The image's src attribute. */
|
||||
@property() src: string;
|
||||
|
||||
/** The image's alt attribute. */
|
||||
@property() alt: string;
|
||||
|
||||
/** When set, the image will animate. Otherwise, it will be paused. */
|
||||
@property({ type: Boolean, reflect: true }) play: boolean;
|
||||
|
||||
handleClick() {
|
||||
this.play = !this.play;
|
||||
}
|
||||
|
||||
handleLoad() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const { width, height } = this.animatedImage;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.getContext('2d')!.drawImage(this.animatedImage, 0, 0, width, height);
|
||||
this.frozenFrame = canvas.toDataURL('image/gif');
|
||||
|
||||
if (!this.isLoaded) {
|
||||
emit(this, 'sl-load');
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleError() {
|
||||
emit(this, 'sl-error');
|
||||
}
|
||||
|
||||
@watch('play')
|
||||
async handlePlayChange() {
|
||||
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
||||
// won't trigger another request.
|
||||
if (this.play) {
|
||||
this.animatedImage.src = '';
|
||||
this.animatedImage.src = this.src;
|
||||
}
|
||||
}
|
||||
|
||||
@watch('src')
|
||||
handleSrcChange() {
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="animated-image">
|
||||
<img
|
||||
class="animated-image__animated"
|
||||
src=${this.src}
|
||||
alt=${this.alt}
|
||||
crossorigin="anonymous"
|
||||
aria-hidden=${this.play ? 'false' : 'true'}
|
||||
@click=${this.handleClick}
|
||||
@load=${this.handleLoad}
|
||||
@error=${this.handleError}
|
||||
/>
|
||||
|
||||
${this.isLoaded
|
||||
? html`
|
||||
<img
|
||||
class="animated-image__frozen"
|
||||
src=${this.frozenFrame}
|
||||
alt=${this.alt}
|
||||
aria-hidden=${this.play ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
/>
|
||||
|
||||
<div part="control-box" class="animated-image__control-box">
|
||||
${this.play
|
||||
? html`<sl-icon part="pause-icon" name="pause-fill" library="system"></sl-icon>`
|
||||
: html`<sl-icon part="play-icon" name="play-fill" library="system"></sl-icon>`}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-animated-image': SlAnimatedImage;
|
||||
}
|
||||
}
|
||||
@@ -598,7 +598,7 @@ export default css`
|
||||
* buttons and we style them here instead.
|
||||
*/
|
||||
|
||||
:host(.sl-button-group__button--first) .button {
|
||||
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
@@ -607,7 +607,7 @@ export default css`
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:host(.sl-button-group__button--last) .button {
|
||||
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
@@ -191,11 +191,14 @@ export default css`
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.color-picker__user-input sl-button-group {
|
||||
margin-left: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.color-picker__user-input sl-button {
|
||||
min-width: 3.25rem;
|
||||
max-width: 3.25rem;
|
||||
font-size: 1rem;
|
||||
margin-left: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.color-picker__swatches {
|
||||
@@ -299,7 +302,7 @@ export default css`
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background-color: currentColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: inset 0 0 0 1px rgb(var(--sl-color-neutral-1000) / 25%);
|
||||
transition: inherit;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,15 +13,19 @@ import color from 'color';
|
||||
import styles from './color-picker.styles';
|
||||
|
||||
import '../button/button';
|
||||
import '../button-group/button-group';
|
||||
import '../dropdown/dropdown';
|
||||
import '../icon/icon';
|
||||
import '../input/input';
|
||||
|
||||
const hasEyeDropper = 'EyeDropper' in window;
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @dependency sl-button
|
||||
* @dependency sl-button-group
|
||||
* @dependency sl-dropdown
|
||||
* @dependency sl-input
|
||||
*
|
||||
@@ -39,6 +43,7 @@ import '../input/input';
|
||||
* @csspart slider-handle - Hue and opacity slider handles.
|
||||
* @csspart preview - The preview color.
|
||||
* @csspart input - The text input.
|
||||
* @csspart eye-dropper-button - The toggle format button's base.
|
||||
* @csspart format-button - The toggle format button's base.
|
||||
*
|
||||
* @cssproperty --grid-width - The width of the color grid.
|
||||
@@ -570,6 +575,22 @@ export default class SlColorPicker extends LitElement {
|
||||
this.previewButton.classList.remove('color-picker__preview-color--copied');
|
||||
}
|
||||
|
||||
handleEyeDropper() {
|
||||
if (!hasEyeDropper) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const eyeDropper = new EyeDropper();
|
||||
|
||||
eyeDropper
|
||||
.open()
|
||||
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex))
|
||||
.catch(() => {
|
||||
// The user canceled, do nothing
|
||||
});
|
||||
}
|
||||
|
||||
@watch('format')
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
@@ -606,6 +627,7 @@ export default class SlColorPicker extends LitElement {
|
||||
const x = this.saturation;
|
||||
const y = 100 - this.lightness;
|
||||
|
||||
// TODO - i18n for format, copy, and eye dropper buttons
|
||||
const colorPicker = html`
|
||||
<div
|
||||
part="base"
|
||||
@@ -708,6 +730,7 @@ export default class SlColorPicker extends LitElement {
|
||||
type="button"
|
||||
part="preview"
|
||||
class="color-picker__preview color-picker__transparent-bg"
|
||||
aria-label="Copy"
|
||||
style=${styleMap({
|
||||
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
})}
|
||||
@@ -730,13 +753,26 @@ export default class SlColorPicker extends LitElement {
|
||||
@sl-change=${this.handleInputChange}
|
||||
></sl-input>
|
||||
|
||||
${!this.noFormatToggle
|
||||
? html`
|
||||
<sl-button exportparts="base:format-button" @click=${this.handleFormatToggle}>
|
||||
${this.setLetterCase(this.format)}
|
||||
</sl-button>
|
||||
`
|
||||
: ''}
|
||||
<sl-button-group>
|
||||
${!this.noFormatToggle
|
||||
? html`
|
||||
<sl-button
|
||||
aria-label="Change format"
|
||||
exportparts="base:format-button"
|
||||
@click=${this.handleFormatToggle}
|
||||
>
|
||||
${this.setLetterCase(this.format)}
|
||||
</sl-button>
|
||||
`
|
||||
: ''}
|
||||
${hasEyeDropper
|
||||
? html`
|
||||
<sl-button exportparts="base:eye-dropper-button" @click=${this.handleEyeDropper}>
|
||||
<sl-icon library="system" name="eyedropper" label="Select a color from the screen"></sl-icon>
|
||||
</sl-button>
|
||||
`
|
||||
: ''}
|
||||
</sl-button-group>
|
||||
</div>
|
||||
|
||||
${this.swatches
|
||||
|
||||
43
src/components/context-menu/context-menu.styles.ts
Normal file
43
src/components/context-menu/context-menu.styles.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
::slotted(sl-menu) {
|
||||
min-width: 180px;
|
||||
background: rgb(var(--sl-panel-background-color));
|
||||
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--sl-shadow-large);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: relative;
|
||||
z-index: var(--sl-z-index-dropdown);
|
||||
}
|
||||
|
||||
.context-menu__locater {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown__positioner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.context-menu__menu {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: all;
|
||||
}
|
||||
`;
|
||||
13
src/components/context-menu/context-menu.test.ts
Normal file
13
src/components/context-menu/context-menu.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlContextMenu from './context-menu';
|
||||
|
||||
describe('<sl-context-menu>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-context-menu></sl-context-menu> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
292
src/components/context-menu/context-menu.ts
Normal file
292
src/components/context-menu/context-menu.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { emit, waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import styles from './context-menu.styles';
|
||||
|
||||
import '../menu/menu';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-menu
|
||||
*
|
||||
* @event sl-event-name - Emitted as an example.
|
||||
*
|
||||
* @slot - Content that will activate the context menu when right-clicked.
|
||||
* @slot menu - The menu to show when the context menu is activated, an `<sl-menu>` element.
|
||||
*
|
||||
* @event sl-show - Emitted when the context menu opens.
|
||||
* @event sl-after-show - Emitted after the context menu opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the context menu closes.
|
||||
* @event sl-after-hide - Emitted after the context menu closes and all animations are complete.
|
||||
*
|
||||
* @animation contextMenu.show - The animation to use when showing the context menu.
|
||||
* @animation contextMenu.hide - The animation to use when hiding the context menu.
|
||||
*/
|
||||
@customElement('sl-context-menu')
|
||||
export default class SlContextMenu extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@query('.context-menu') wrapper: HTMLElement;
|
||||
@query('.context-menu__locater') locater: HTMLElement;
|
||||
@query('.context-menu__menu') menu: HTMLSlotElement;
|
||||
@query('.context-menu__positioner') positioner: HTMLElement;
|
||||
|
||||
private popover: PopperInstance;
|
||||
|
||||
/**
|
||||
* The preferred placement of the context menu. Note that the actual placement may vary as needed to keep the menu
|
||||
* inside of the viewport.
|
||||
*/
|
||||
@property() placement:
|
||||
| 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end' = 'bottom-start';
|
||||
|
||||
/** Disables the context menu so it won't show when triggered. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The distance in pixels from which to offset the context menu away from its target. */
|
||||
@property({ type: Number }) distance = 0;
|
||||
|
||||
/** Indicates whether or not the context menu is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** The distance in pixels from which to offset the context menu along its target. */
|
||||
@property({ type: Number }) skidding = 0;
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the menu from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|hidden|scroll`.
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
||||
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.menu.hidden = !this.open;
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
const slot = this.menu.querySelector('slot')!;
|
||||
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
|
||||
}
|
||||
|
||||
async handleContextMenu(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const wrapperRect = this.wrapper.getBoundingClientRect();
|
||||
const { offsetX, offsetY } = event;
|
||||
const x = targetRect.left + offsetX - wrapperRect.left;
|
||||
const y = targetRect.top + offsetY - wrapperRect.top;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (this.open) {
|
||||
await this.hide();
|
||||
}
|
||||
|
||||
this.show(x, y);
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? menu.getAllItems() : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
// Close when escape is pressed
|
||||
if (event.key === 'Escape') {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward key presses that don't originate from the menu to allow keyboard selection and type-to-select
|
||||
if (menu && !event.composedPath().includes(this.menu)) {
|
||||
// Focus on a menu item
|
||||
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||
event.preventDefault();
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||
event.preventDefault();
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Other keys bring focus to the menu and initiate type-to-select behavior
|
||||
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
||||
if (!ignoredKeys.includes(event.key)) {
|
||||
menu.typeToSelect(event.key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentMouseDown(event: MouseEvent) {
|
||||
const path = event.composedPath() as Array<EventTarget>;
|
||||
|
||||
//
|
||||
// Close the context menu when clicking outside of it. We use a setTimeout here because mousedown fires before
|
||||
// contextmenu and, if the menu is already open and the user-right clicks again, we want the menu to re-open in the
|
||||
// new position instead of closing.
|
||||
//
|
||||
setTimeout(() => {
|
||||
if (this.open && !path.includes(this.menu)) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleMenuSelect() {
|
||||
// Close the context menu when a menu item is selected
|
||||
this.hide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
emit(this, 'sl-show');
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
|
||||
this.popover = createPopper(this.locater, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.menu.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'contextMenu.show');
|
||||
await animateTo(this.menu, keyframes, options);
|
||||
|
||||
emit(this, 'sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
emit(this, 'sl-hide');
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
const { keyframes, options } = getAnimation(this, 'contextMenu.hide');
|
||||
await animateTo(this.menu, keyframes, options);
|
||||
|
||||
this.menu.hidden = true;
|
||||
this.locater.style.top = '0px';
|
||||
this.locater.style.left = '0px';
|
||||
this.popover.destroy();
|
||||
|
||||
emit(this, 'sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the context menu */
|
||||
async show(offsetX?: number, offsetY?: number) {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.locater.style.top = `${offsetY || 0}px`;
|
||||
this.locater.style.left = `${offsetX || 0}px`;
|
||||
this.open = true;
|
||||
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dropdown panel */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="context-menu">
|
||||
<slot @contextmenu=${this.handleContextMenu}></slot>
|
||||
|
||||
<div class="context-menu__locater"></div>
|
||||
|
||||
<!-- Position the menu with a wrapper since the popover makes use of translate. This let's us add animations
|
||||
on the menu without interfering with the position. -->
|
||||
<div class="context-menu__positioner">
|
||||
<div class="context-menu__menu" hidden @sl-select=${this.handleMenuSelect}>
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('contextMenu.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.9)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
options: { duration: 50, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('contextMenu.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.9)' }
|
||||
],
|
||||
options: { duration: 150, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-context-menu': SlContextMenu;
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,17 @@ export default css`
|
||||
--spacing: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
:host(:not([vertical])) {
|
||||
:host(:not([vertical])) .menu-divider {
|
||||
display: block;
|
||||
border-top: solid var(--width) var(--color);
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
:host([vertical]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([vertical]) .menu-divider {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
border-left: solid var(--width) var(--color);
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class SlDivider extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html``;
|
||||
return html` <div part="base" class="menu-divider"></div> `;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,28 +100,6 @@ export default class SlDropdown extends LitElement {
|
||||
if (!this.containingElement) {
|
||||
this.containingElement = this;
|
||||
}
|
||||
|
||||
// Create the popover after render
|
||||
this.updateComplete.then(() => {
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@@ -131,7 +109,6 @@ export default class SlDropdown extends LitElement {
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.hide();
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
focusOnTrigger() {
|
||||
@@ -207,40 +184,13 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('distance')
|
||||
@watch('hoist')
|
||||
@watch('placement')
|
||||
@watch('skidding')
|
||||
handlePopoverOptionsChange() {
|
||||
if (this.popover) {
|
||||
this.popover.setOptions({
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleTriggerClick() {
|
||||
this.open ? this.hide() : this.show();
|
||||
}
|
||||
|
||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
||||
const menuItems = menu ? menu.getAllItems() : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
@@ -262,7 +212,7 @@ export default class SlDropdown extends LitElement {
|
||||
// When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a
|
||||
// selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for
|
||||
// faster navigation.
|
||||
if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
|
||||
// Show the menu if it's not already open
|
||||
@@ -271,14 +221,14 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
|
||||
// Focus on a menu item
|
||||
if (event.key === 'ArrowDown' && firstMenuItem) {
|
||||
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && lastMenuItem) {
|
||||
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
@@ -327,7 +277,7 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the dropdown panel. */
|
||||
/** Shows the dropdown panel */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
@@ -352,11 +302,9 @@ export default class SlDropdown extends LitElement {
|
||||
* is activated.
|
||||
*/
|
||||
reposition() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
if (this.popover) {
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
@@ -376,7 +324,26 @@ export default class SlDropdown extends LitElement {
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.popover.update();
|
||||
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
@@ -41,6 +41,11 @@ const icons = {
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
`,
|
||||
eyedropper: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eyedropper" viewBox="0 0 16 16">
|
||||
<path d="M13.354.646a1.207 1.207 0 0 0-1.708 0L8.5 3.793l-.646-.647a.5.5 0 1 0-.708.708L8.293 5l-7.147 7.146A.5.5 0 0 0 1 12.5v1.793l-.854.853a.5.5 0 1 0 .708.707L1.707 15H3.5a.5.5 0 0 0 .354-.146L11 7.707l1.146 1.147a.5.5 0 0 0 .708-.708l-.647-.646 3.147-3.146a1.207 1.207 0 0 0 0-1.708l-2-2zM2 12.707l7-7L10.293 7l-7 7H2v-1.293z"></path>
|
||||
</svg>
|
||||
`,
|
||||
'grip-vertical': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-grip-vertical" viewBox="0 0 16 16">
|
||||
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
|
||||
@@ -51,6 +56,16 @@ const icons = {
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
`,
|
||||
'play-fill': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
|
||||
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"></path>
|
||||
</svg>
|
||||
`,
|
||||
'pause-fill': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
|
||||
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"></path>
|
||||
</svg>
|
||||
`,
|
||||
'star-fill': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
|
||||
|
||||
89
src/components/progress-bar/progress-bar.test.ts
Normal file
89
src/components/progress-bar/progress-bar.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlProgressBar from './progress-bar';
|
||||
|
||||
describe('<sl-progress-bar>', () => {
|
||||
let el: SlProgressBar;
|
||||
|
||||
describe('when provided just a value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a title, and value parameter', async () => {
|
||||
let base: HTMLDivElement;
|
||||
let indicator: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
||||
expect(base).attribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', async () => {
|
||||
expect(indicator).attribute('style', 'width:25%;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an indeterminate parameter', async () => {
|
||||
let base: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should append a progress-bar--indeterminate class to the "base" part.', async () => {
|
||||
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`
|
||||
<label id="labelledby">Progress Ring Label</label>
|
||||
<sl-progress-bar ariaLabelledBy="labelledby" value="25"></sl-progress-bar>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import styles from './progress-bar.styles';
|
||||
@@ -29,6 +30,9 @@ export default class SlProgressBar extends LitElement {
|
||||
/** When true, percentage is ignored, the label is hidden, and the progress bar is drawn in an indeterminate state. */
|
||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||
|
||||
/** The progress bar's aria label. */
|
||||
@property() label = 'Progress'; // TODO - i18n
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -38,9 +42,11 @@ export default class SlProgressBar extends LitElement {
|
||||
'progress-bar--indeterminate': this.indeterminate
|
||||
})}
|
||||
role="progressbar"
|
||||
title=${ifDefined(this.title)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.indeterminate ? '' : this.value}"
|
||||
aria-valuenow=${this.indeterminate ? 0 : this.value}
|
||||
>
|
||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.value + '%' })}>
|
||||
${!this.indeterminate
|
||||
|
||||
68
src/components/progress-ring/progress-ring.test.ts
Normal file
68
src/components/progress-ring/progress-ring.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlProgressRing from './progress-ring';
|
||||
|
||||
describe('<sl-progress-ring>', () => {
|
||||
let el: SlProgressRing;
|
||||
|
||||
describe('when provided just a value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a title, and value parameter', async () => {
|
||||
let base: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
||||
expect(base).attribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', async () => {
|
||||
expect(base).attribute('style', '--percentage: 0.25');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`
|
||||
<label id="labelledby">Progress Ring Label</label>
|
||||
<sl-progress-ring ariaLabelledBy="labelledby" value="25"></sl-progress-ring>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import styles from './progress-ring.styles';
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,9 @@ export default class SlProgressRing extends LitElement {
|
||||
/** The current progress, 0 to 100. */
|
||||
@property({ type: Number, reflect: true }) value = 0;
|
||||
|
||||
/** The progress ring's aria label. */
|
||||
@property() label = 'Progress'; // TODO - i18n
|
||||
|
||||
updated(changedProps: Map<string, any>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
@@ -50,6 +54,7 @@ export default class SlProgressRing extends LitElement {
|
||||
part="base"
|
||||
class="progress-ring"
|
||||
role="progressbar"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.value}"
|
||||
|
||||
@@ -53,18 +53,18 @@ export default css`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range__control:not(:disabled)::-webkit-slider-thumb:hover {
|
||||
.range__control:enabled::-webkit-slider-thumb:hover {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
}
|
||||
|
||||
.range__control:not(:disabled)${focusVisibleSelector}::-webkit-slider-thumb {
|
||||
.range__control:enabled${focusVisibleSelector}::-webkit-slider-thumb {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
box-shadow: var(--sl-focus-ring);
|
||||
}
|
||||
|
||||
.range__control:not(:disabled)::-webkit-slider-thumb:active {
|
||||
.range__control:enabled::-webkit-slider-thumb:active {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
cursor: grabbing;
|
||||
@@ -101,18 +101,18 @@ export default css`
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range__control:not(:disabled)::-moz-range-thumb:hover {
|
||||
.range__control:enabled::-moz-range-thumb:hover {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
}
|
||||
|
||||
.range__control:not(:disabled)${focusVisibleSelector}::-moz-range-thumb {
|
||||
.range__control:enabled${focusVisibleSelector}::-moz-range-thumb {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
box-shadow: var(--sl-focus-ring);
|
||||
}
|
||||
|
||||
.range__control:not(:disabled)::-moz-range-thumb:active {
|
||||
.range__control:enabled::-moz-range-thumb:active {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
cursor: grabbing;
|
||||
|
||||
@@ -29,8 +29,8 @@ let id = 0;
|
||||
* @cssproperty --thumb-size - The size of the thumb.
|
||||
* @cssproperty --tooltip-offset - The vertical distance the tooltip is offset from the track.
|
||||
* @cssproperty --track-color-active - The color of the portion of the track that represents the current value.
|
||||
* @cssproperty --track-color-inactive: The of the portion of the track that represents the remaining value.
|
||||
* @cssproperty --track-height: The height of the track.
|
||||
* @cssproperty --track-color-inactive - The of the portion of the track that represents the remaining value.
|
||||
* @cssproperty --track-height - The height of the track.
|
||||
*/
|
||||
@customElement('sl-range')
|
||||
export default class SlRange extends LitElement {
|
||||
@@ -129,7 +129,7 @@ export default class SlRange extends LitElement {
|
||||
this.value = Number(this.input.value);
|
||||
emit(this, 'sl-change');
|
||||
|
||||
requestAnimationFrame(() => this.syncRange());
|
||||
this.syncRange();
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
@@ -138,6 +138,17 @@ export default class SlRange extends LitElement {
|
||||
emit(this, 'sl-blur');
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.value = Number(this.value);
|
||||
|
||||
if (this.input) {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
this.syncRange();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
@@ -185,13 +196,15 @@ export default class SlRange extends LitElement {
|
||||
}
|
||||
|
||||
syncTooltip(percent: number) {
|
||||
const inputWidth = this.input.offsetWidth;
|
||||
const tooltipWidth = this.output.offsetWidth;
|
||||
const thumbSize = getComputedStyle(this.input).getPropertyValue('--thumb-size');
|
||||
const x = `calc(${inputWidth * percent}px - calc(calc(${percent} * ${thumbSize}) - calc(${thumbSize} / 2)))`;
|
||||
if (this.output) {
|
||||
const inputWidth = this.input.offsetWidth;
|
||||
const tooltipWidth = this.output.offsetWidth;
|
||||
const thumbSize = getComputedStyle(this.input).getPropertyValue('--thumb-size');
|
||||
const x = `calc(${inputWidth * percent}px - calc(calc(${percent} * ${thumbSize}) - calc(${thumbSize} / 2)))`;
|
||||
|
||||
this.output.style.transform = `translateX(${x})`;
|
||||
this.output.style.marginLeft = `-${tooltipWidth / 2}px`;
|
||||
this.output.style.transform = `translateX(${x})`;
|
||||
this.output.style.marginLeft = `-${tooltipWidth / 2}px`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -247,7 +260,7 @@ export default class SlRange extends LitElement {
|
||||
@focus=${this.handleFocus}
|
||||
@blur=${this.handleBlur}
|
||||
/>
|
||||
${this.tooltip !== 'none'
|
||||
${this.tooltip !== 'none' && !this.disabled
|
||||
? html` <output part="tooltip" class="range__tooltip"> ${this.tooltipFormatter(this.value)} </output> `
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,8 @@ export default class SlResponsiveMedia extends LitElement {
|
||||
|
||||
render() {
|
||||
const split = this.aspectRatio.split(':');
|
||||
const x = parseInt(split[0]);
|
||||
const y = parseInt(split[1]);
|
||||
const x = parseFloat(split[0]);
|
||||
const y = parseFloat(split[1]);
|
||||
const paddingBottom = x && y ? `${(y / x) * 100}%` : '0';
|
||||
|
||||
return html`
|
||||
|
||||
@@ -354,7 +354,7 @@ export default class SlTabGroup extends LitElement {
|
||||
@click=${this.handleClick}
|
||||
@keydown=${this.handleKeyDown}
|
||||
>
|
||||
<div class="tab-group__nav-container">
|
||||
<div class="tab-group__nav-container" part="nav">
|
||||
${this.hasScrollControls
|
||||
? html`
|
||||
<sl-icon-button
|
||||
@@ -367,7 +367,7 @@ export default class SlTabGroup extends LitElement {
|
||||
`
|
||||
: ''}
|
||||
|
||||
<div part="nav" class="tab-group__nav">
|
||||
<div class="tab-group__nav">
|
||||
<div part="tabs" class="tab-group__tabs" role="tablist">
|
||||
<div part="active-tab-indicator" class="tab-group__indicator"></div>
|
||||
<slot name="nav" @slotchange=${this.syncTabsAndPanels}></slot>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Components
|
||||
export { default as SlAlert } from './components/alert/alert';
|
||||
export { default as SlAnimatedImage } from './components/animated-image/animated-image';
|
||||
export { default as SlAnimation } from './components/animation/animation';
|
||||
export { default as SlAvatar } from './components/avatar/avatar';
|
||||
export { default as SlBadge } from './components/badge/badge';
|
||||
@@ -10,6 +11,7 @@ export { default as SlButtonGroup } from './components/button-group/button-group
|
||||
export { default as SlCard } from './components/card/card';
|
||||
export { default as SlCheckbox } from './components/checkbox/checkbox';
|
||||
export { default as SlColorPicker } from './components/color-picker/color-picker';
|
||||
export { default as SlContextMenu } from './components/context-menu/context-menu';
|
||||
export { default as SlDetails } from './components/details/details';
|
||||
export { default as SlDialog } from './components/dialog/dialog';
|
||||
export { default as SlDivider } from './components/divider/divider';
|
||||
|
||||
Reference in New Issue
Block a user