Compare commits

..

46 Commits

Author SHA1 Message Date
Cory LaViska
8ee519d40a fix positioning 2021-10-30 15:54:56 -04:00
Cory LaViska
6bc17d48c3 update examples 2021-10-29 18:35:24 -04:00
Cory LaViska
a1263f1b9d add experimental context menu 2021-10-29 18:32:45 -04:00
Cory LaViska
d69ebab765 update examples 2021-10-29 18:32:26 -04:00
Cory LaViska
0504946dac refactor popper creation 2021-10-29 18:32:12 -04:00
Cory LaViska
fbd6691711 improve search panel color 2021-10-29 14:42:17 -04:00
Cory LaViska
aec17da6b0 improve trigger border color in dark mode 2021-10-29 14:35:57 -04:00
Cory LaViska
639533662d update changelog 2021-10-26 09:35:59 -04:00
Cory LaViska
a340ce4a68 document part 2021-10-26 09:35:46 -04:00
Cory LaViska
6e5fe64e8b add eye dropper 2021-10-26 09:35:07 -04:00
Cory LaViska
84bdbb84b8 update lit 2021-10-26 09:34:48 -04:00
Cory LaViska
f91ffb6cb4 fix border radius on single button groups 2021-10-26 09:34:33 -04:00
Cory LaViska
13815199a3 fix dark theme link 2021-10-22 10:57:07 -04:00
Cory LaViska
98c20ff551 2.0.0-beta.58 2021-10-22 10:52:27 -04:00
Cory LaViska
479b6b9081 bundle back up for now 2021-10-22 10:51:17 -04:00
Cory LaViska
c640d2ea77 add stack overflow section 2021-10-19 10:51:41 -04:00
Cory LaViska
715547d2fd update changelog 2021-10-19 09:56:41 -04:00
Cory LaViska
8a914a536b fix cssproperty docs 2021-10-19 09:56:28 -04:00
Cory LaViska
f56b6c0648 remove RAFs 2021-10-19 09:52:41 -04:00
Denis Korablev
25aa8318d9 fix(sl-range): add value change handler (#572) 2021-10-19 09:48:39 -04:00
Cory LaViska
72f2cbe9e8 fix aspect ratio bug 2021-10-19 09:43:16 -04:00
Cory LaViska
fc7836084a add tooltip guard 2021-10-18 17:54:29 -04:00
Cory LaViska
60d9d9202b update bootstrap-icons to 1.6.1 2021-10-18 17:07:52 -04:00
Cory LaViska
a9df468282 fixes #563 2021-10-18 17:07:07 -04:00
Yuki Nishijima
0bba773c3e Bring the divider back to the Shadow DOM (#568) 2021-10-18 09:14:13 -04:00
Cory LaViska
7be03ae623 fix metadata plugin 2021-10-18 08:58:50 -04:00
Cory LaViska
d4741532f5 fix build dir 2021-10-16 10:35:42 -04:00
Cory LaViska
10f31efefa fix comment parser 2021-10-16 10:28:51 -04:00
Cory LaViska
be662ddf32 add animated-image 2021-10-16 08:29:25 -04:00
Cory LaViska
ff84beaade use :enabled 2021-10-14 09:02:00 -04:00
Cory LaViska
8dba8fa5fb fix tooltip bug 2021-10-14 09:01:37 -04:00
Cory LaViska
3a3f5552a7 fix test:watch 2021-10-14 08:39:17 -04:00
Cory LaViska
88cba353c0 add labels examples 2021-10-14 08:34:54 -04:00
Cory LaViska
a2851370bb revert styles 2021-10-14 08:32:11 -04:00
Cory LaViska
7c0ef7dcf0 Merge branch 'christoshrousis-test/progress' into next 2021-10-14 08:25:42 -04:00
Cory LaViska
fb6d5d89b8 use label attrib 2021-10-14 08:24:38 -04:00
Cory LaViska
45ceff4c08 Merge branch 'test/progress' of https://github.com/christoshrousis/shoelace into christoshrousis-test/progress 2021-10-14 08:10:57 -04:00
Cory LaViska
6169abc700 update bootstrap icons 2021-10-14 07:21:27 -04:00
Cory LaViska
c09e12d13e 2.0.0-beta.57 2021-10-13 17:34:04 -04:00
Cory LaViska
6152e15e10 fix esm links 2021-10-13 17:30:13 -04:00
Christos Hrousis
9a19cc2173 revert: misunderstood part/slot definition. 2021-10-10 13:37:32 +11:00
Christos Hrousis
c4cbc894f5 revert: misunderstood part/slot definition. 2021-10-10 13:36:41 +11:00
Christos Hrousis
449f5e6c7f style: typo. 2021-10-10 13:24:28 +11:00
Christos Hrousis
34447a3f2f test: migrate progress-ring tests to progress-bar
- Match coverage with progress-ring
- Attached titles/label/labelledby
- Value '' on aria-valuenow is does not pass AXE
2021-10-10 13:24:01 +11:00
Christos Hrousis
eee97d7dba test: cover progress-ring
- Add title to make ring accessibly hoverable.
- Add label/labelledby as aria options.
- Remove ununsed label slot.
2021-10-10 13:05:40 +11:00
Christos Hrousis
f16392947a docs: ring uses css prop for track-width. 2021-10-10 13:00:41 +11:00
39 changed files with 1199 additions and 204 deletions

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/assets/images/walk.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -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;

View 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]

View 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]

View File

@@ -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]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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:

View File

@@ -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.

View File

@@ -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
View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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)));
});

View 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;
}
`;

View 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;
});
});

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View 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;
}
`;

View 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;
});
});

View 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;
}
}

View File

@@ -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);

View File

@@ -30,7 +30,7 @@ export default class SlDivider extends LitElement {
}
render() {
return html``;
return html` <div part="base" class="menu-divider"></div> `;
}
}

View File

@@ -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);

View File

@@ -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"/>

View 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();
});
});
});

View File

@@ -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

View 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();
});
});
});

View File

@@ -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}"

View File

@@ -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;

View File

@@ -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>

View File

@@ -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`

View File

@@ -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>

View File

@@ -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';