mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
77 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 | ||
|
|
79910b2ae8 | ||
|
|
c347df7c17 | ||
|
|
e9e2b35c59 | ||
|
|
8ae753c396 | ||
|
|
d2c94321f2 | ||
|
|
4c10f8a537 | ||
|
|
9a19cc2173 | ||
|
|
c4cbc894f5 | ||
|
|
449f5e6c7f | ||
|
|
34447a3f2f | ||
|
|
eee97d7dba | ||
|
|
f16392947a | ||
|
|
c3adf92b49 | ||
|
|
a6580b018d | ||
|
|
00c843c7ce | ||
|
|
5fe55a4db9 | ||
|
|
4ca998c346 | ||
|
|
42b3e2cc11 | ||
|
|
59fb8db6be | ||
|
|
664beafefa | ||
|
|
04443a64e2 | ||
|
|
92dedf3386 | ||
|
|
2ba5fb9820 | ||
|
|
222235159b | ||
|
|
1061bd5e0d | ||
|
|
ccec9a8348 | ||
|
|
bf9e06e67d | ||
|
|
1e03d222c5 | ||
|
|
3722f46b8e | ||
|
|
8d7bf97127 | ||
|
|
d26c1a6407 | ||
|
|
f296ff8476 | ||
|
|
a75a71994a | ||
|
|
7d1373a1d1 | ||
|
|
23abc50015 | ||
|
|
17df0e3cd3 | ||
|
|
76df2fd204 |
@@ -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)
|
||||
@@ -74,5 +76,6 @@
|
||||
- [Z-index](/tokens/z-index)
|
||||
|
||||
- Tutorials
|
||||
- [Integrating with Laravel](/tutorials/integrating-with-laravel)
|
||||
- [Integrating with NextJS](/tutorials/integrating-with-nextjs)
|
||||
- [Integrating with Rails](/tutorials/integrating-with-rails)
|
||||
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 |
@@ -11,7 +11,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Attribute</th>
|
||||
<th>Description</th>
|
||||
<th>Reflects</th>
|
||||
<th>Type</th>
|
||||
@@ -21,13 +20,33 @@
|
||||
<tbody>
|
||||
${props
|
||||
.map(prop => {
|
||||
const hasAttribute = !!prop.attribute;
|
||||
const isAttributeDifferent = prop.attribute !== prop.name;
|
||||
let attributeInfo = '';
|
||||
|
||||
if (!hasAttribute) {
|
||||
attributeInfo = `<br><small>(property only)</small>`;
|
||||
} else if (isAttributeDifferent) {
|
||||
attributeInfo = `
|
||||
<br>
|
||||
<sl-tooltip content="This attribute is different than the property">
|
||||
<small>
|
||||
<code class="nowrap">
|
||||
${escapeHtml(prop.attribute)}
|
||||
</code>
|
||||
</small>
|
||||
</sl-tooltip>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td class="nowrap">
|
||||
<code>${escapeHtml(prop.name)}</code>
|
||||
<td>
|
||||
<code class="nowrap">${escapeHtml(prop.name)}</code>
|
||||
${attributeInfo}
|
||||
</td>
|
||||
<td>
|
||||
${escapeHtml(prop.description)}
|
||||
</td>
|
||||
<td class="nowrap">${prop.attribute ? `<code>${escapeHtml(prop.attribute)}</code>` : '-'}</td>
|
||||
<td>${escapeHtml(prop.description)}</td>
|
||||
<td style="text-align: center;">${
|
||||
prop.reflects ? '<sl-icon label="yes" name="check"></sl-icon>' : ''
|
||||
}</td>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -291,6 +291,10 @@ strong {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.markdown-section tr:nth-child(2n) code {
|
||||
background-color: rgb(var(--sl-color-neutral-100));
|
||||
}
|
||||
|
||||
kbd,
|
||||
.markdown-section kbd {
|
||||
font-family: var(--sl-font-mono);
|
||||
@@ -429,6 +433,11 @@ kbd,
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.markdown-section table sl-tooltip code {
|
||||
border-bottom: dashed 1px rgb(var(--sl-color-neutral-300));
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Iframes */
|
||||
.markdown-section iframe {
|
||||
border: none;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -34,21 +34,21 @@ Use the `pill` attribute to give tabs rounded edges.
|
||||
<sl-tag size="large" pill>Large</sl-tag>
|
||||
```
|
||||
|
||||
### Clearable
|
||||
### Removable
|
||||
|
||||
Use the `clearable` attribute to add a clear button to the tag.
|
||||
Use the `removable` attribute to add a remove button to the tag.
|
||||
|
||||
```html preview
|
||||
<div class="tags-clearable">
|
||||
<sl-tag size="small" clearable>Small</sl-tag>
|
||||
<sl-tag size="medium" clearable>Medium</sl-tag>
|
||||
<sl-tag size="large" clearable>Large</sl-tag>
|
||||
<div class="tags-removable">
|
||||
<sl-tag size="small" removable>Small</sl-tag>
|
||||
<sl-tag size="medium" removable>Medium</sl-tag>
|
||||
<sl-tag size="large" removable>Large</sl-tag>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const div = document.querySelector('.tags-clearable');
|
||||
const div = document.querySelector('.tags-removable');
|
||||
|
||||
div.addEventListener('sl-clear', event => {
|
||||
div.addEventListener('sl-remove', event => {
|
||||
const tag = event.target;
|
||||
tag.style.opacity = '0';
|
||||
setTimeout(() => tag.style.opacity = '1', 2000);
|
||||
@@ -56,7 +56,7 @@ Use the `clearable` attribute to add a clear button to the tag.
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tags-clearable sl-tag {
|
||||
.tags-removable sl-tag {
|
||||
transition: var(--sl-transition-medium) opacity;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -174,4 +174,29 @@ Use the `content` slot to create tooltips with HTML content. Tooltips are design
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
### Hoisting
|
||||
|
||||
Tooltips will be clipped if they're inside a container that has `overflow: auto|hidden|scroll`. The `hoist` attribute forces the tooltip to use a fixed positioning strategy, allowing it to break out of the container. In this case, the tooltip will be positioned relative to its containing block, which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
|
||||
|
||||
```html preview
|
||||
<div class="tooltip-hoist">
|
||||
<sl-tooltip content="This is a tooltip">
|
||||
<sl-button>No Hoist</sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="This is a tooltip" hoist>
|
||||
<sl-button>Hoist</sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tooltip-hoist {
|
||||
border: solid 2px rgb(var(--sl-panel-border-color));
|
||||
overflow: hidden;
|
||||
padding: var(--sl-spacing-medium);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-tooltip]
|
||||
|
||||
@@ -16,7 +16,7 @@ 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">
|
||||
<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>
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -6,6 +6,55 @@ 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.
|
||||
|
||||
- Added the `hoist` attribute to `<sl-tooltip>` [#564](https://github.com/shoelace-style/shoelace/issues/564)
|
||||
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
|
||||
|
||||
## 2.0.0-beta.55
|
||||
|
||||
- Revert unbundling due to issues with the CDN not handling bare module specifiers as expected
|
||||
|
||||
## 2.0.0-beta.54
|
||||
|
||||
Shoelace doesn't have a lot of dependencies, but this release unbundles most of them so you can potentially save some extra kilobytes. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected.
|
||||
|
||||
- 🚨 BREAKING: renamed the `sl-clear` event to `sl-remove`, the `clear-button` part to `remove-button`, and the `clearable` property to `removable` in `<sl-tag>`
|
||||
- Added the `disabled` prop to `<sl-resize-observer>`
|
||||
- Fixed a bug in `<sl-mutation-observer>` where setting `disabled` initially didn't work
|
||||
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
|
||||
|
||||
## 2.0.0-beta.53
|
||||
|
||||
- 🚨 BREAKING: removed `<sl-menu-divider>` (use `<sl-divider>` instead)
|
||||
|
||||
@@ -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>
|
||||
|
||||
114
docs/tutorials/integrating-with-laravel.md
Normal file
114
docs/tutorials/integrating-with-laravel.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Integrating with Laravel
|
||||
|
||||
This page explains how to integrate Shoelace with a [Laravel](https://laravel.com) app using a local Webpack bundle. This is a community-maintained document. For questions about this integration, please [ask the community](/resources/community).
|
||||
|
||||
## Requirements
|
||||
|
||||
This integration has been tested with the following:
|
||||
|
||||
- Laravel >= 8
|
||||
- Node >= 14
|
||||
- Laravel Mix >= 6
|
||||
|
||||
## Instructions
|
||||
|
||||
These instructions assume an out-of-the-box [Laravel 8+ install](https://laravel.com/docs/8.x/installation) that uses [Laravel Mix](https://laravel.com/docs/8.x/mix) to compile assets.
|
||||
Be sure to run `npm install` to install the default Laravel front-end dependencies before installing Shoelace.
|
||||
|
||||
### Install the Shoelace package
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
### Import the Default Theme
|
||||
|
||||
Import Shoelace's default theme (stylesheet) in `/resources/css/app.css`:
|
||||
|
||||
```css
|
||||
@import "/node_modules/@shoelace-style/shoelace/dist/themes/light.css";
|
||||
```
|
||||
|
||||
### Import Your Shoelace Components
|
||||
|
||||
Import each Shoelace component you plan to use in `/resources/js/boostrap.js`. Since [Laravel Mix](https://laravel.com/docs/8.x/mix) uses Webpack, use the full path to each component -- as outlined in the [Cherry Picking instructions](https://shoelace.style/getting-started/installation?id=cherry-picking). You can find the full import statement for a component in the *Importing* section of the component's documentation (use the *Bundler* import). Your imports should look similar to:
|
||||
|
||||
```js
|
||||
import "@shoelace-style/shoelace/dist/components/button/button.js";
|
||||
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
|
||||
import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
|
||||
```
|
||||
|
||||
### Set the Base Path
|
||||
|
||||
Add the base path to your Shoelace assets (icons, images, etc.) in `/resources/js/boostrap.js`. The path must point to the same folder where you copy assets to in the next step.
|
||||
|
||||
```js
|
||||
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
|
||||
setBasePath("/");
|
||||
```
|
||||
|
||||
Here's an example `/resources/js/boostrap.js` file, after importing and setting the base path and components.
|
||||
|
||||
```js
|
||||
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
|
||||
setBasePath("/assets");
|
||||
|
||||
import "@shoelace-style/shoelace/dist/components/button/button.js";
|
||||
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
|
||||
import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
|
||||
```
|
||||
|
||||
|
||||
### Configure Laravel Mix
|
||||
|
||||
[Laravel Mix](https://laravel.com/docs/8.x/mix) is a wrapper around Webpack that simplifies configuration. Mix is used by default for compiling front-end assets in Laravel.
|
||||
|
||||
Modify `webpack.mix.js` to add Shoelace's assets to Webpack's build process:
|
||||
```js
|
||||
mix.js("resources/js/app.js", "public/js")
|
||||
.postCss("resources/css/app.css", "public/css", [])
|
||||
.copy("node_modules/@shoelace-style/shoelace/dist/assets", "public/assets")
|
||||
```
|
||||
|
||||
Consider [extracting vendor libraries](https://laravel.com/docs/8.x/mix#vendor-extraction) to a separate file. This splits frequently updated vendor libraries (like Shoelace) from your front-end application code -- for better long-term caching.
|
||||
Here's an example `webpack.mix.js` file that compiles and splits your JS into `app.js` and `vendor.js` files, and builds an optimized CSS bundle using PostCSS.
|
||||
|
||||
```js
|
||||
mix.js("resources/js/app.js", "public/js")
|
||||
.postCss("resources/css/app.css", "public/css", [])
|
||||
.copy("node_modules/@shoelace-style/shoelace/dist/assets", "public/assets")
|
||||
.extract(); // extracts libraries in node_modules to vendor.js
|
||||
```
|
||||
|
||||
### Compile Front-End Assets
|
||||
|
||||
Run the [Laravel Mix](https://laravel.com/docs/8.x/mix) npm scripts to build your application's CSS and JavaScript code.
|
||||
|
||||
```bash
|
||||
## build a development bundle
|
||||
npm run dev
|
||||
|
||||
## build a production bundle
|
||||
npm run prod
|
||||
```
|
||||
|
||||
### Include Front-End Assets in Your Layout File
|
||||
|
||||
Most full-stack Laravel applications use [layouts](https://laravel.com/docs/8.x/blade#building-layouts) to define the basic structure of a page.
|
||||
After compiling your front-end assets (above), include them in your top-level layouts/templates. The following example uses the [Laravel asset helper](https://laravel.com/docs/8.x/helpers#method-asset) to generate a full URL.
|
||||
|
||||
```html
|
||||
<script defer src="{{ asset('js/manifest.js') }}"></script>
|
||||
<script defer src="{{ asset('js/vendor.js') }}"></script>
|
||||
<script defer src="{{ asset('/js/app.js') }}"></script>
|
||||
|
||||
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
Have fun using Shoelace components in your Laravel app!
|
||||
|
||||
@@ -23,9 +23,12 @@ yarn add @shoelace-style/shoelace copy-webpack-plugin
|
||||
The next step is to import Shoelace's default theme (stylesheet) in `app/javascript/stylesheets/application.scss`.
|
||||
|
||||
```css
|
||||
@import '~@shoelace-style/shoelace/dist/themes/base';
|
||||
@import '~@shoelace-style/shoelace/dist/themes/light';
|
||||
@import '~@shoelace-style/shoelace/dist/themes/dark'; // Optional dark theme
|
||||
```
|
||||
|
||||
Fore more details about themes, please refer to [Theme Basics](/getting-started/themes?id=theme-basics).
|
||||
|
||||
### Importing Required Scripts
|
||||
|
||||
After importing the theme, you'll need to import the JavaScript files for Shoelace. Add the following code to `app/javascript/packs/application.js`.
|
||||
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.0.0-beta.53",
|
||||
"version": "2.0.0-beta.58",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.0.0-beta.53",
|
||||
"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,10 +22,10 @@
|
||||
"@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.1.1",
|
||||
"command-line-args": "^5.2.0",
|
||||
"comment-parser": "^1.1.5",
|
||||
"concurrently": "^5.3.0",
|
||||
"del": "^6.0.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"
|
||||
@@ -2425,12 +2427,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz",
|
||||
"integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-back": "^3.0.1",
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
@@ -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": {
|
||||
@@ -13209,12 +13216,12 @@
|
||||
}
|
||||
},
|
||||
"command-line-args": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz",
|
||||
"integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-back": "^3.0.1",
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.0.0-beta.53",
|
||||
"version": "2.0.0-beta.58",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -30,8 +30,8 @@
|
||||
"url": "https://github.com/sponsors/claviska"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/build.js --dev",
|
||||
"build": "node scripts/build.js",
|
||||
"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",
|
||||
@@ -42,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": {
|
||||
@@ -53,10 +52,10 @@
|
||||
"@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.1.1",
|
||||
"command-line-args": "^5.2.0",
|
||||
"comment-parser": "^1.1.5",
|
||||
"concurrently": "^5.3.0",
|
||||
"del": "^6.0.0",
|
||||
@@ -66,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",
|
||||
|
||||
105
scripts/build.js
105
scripts/build.js
@@ -1,6 +1,3 @@
|
||||
//
|
||||
// Builds the project. To spin up a dev server, pass the --serve flag.
|
||||
//
|
||||
import browserSync from 'browser-sync';
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
@@ -10,52 +7,67 @@ import esbuild from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import getPort from 'get-port';
|
||||
import glob from 'globby';
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
import { URL } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const build = esbuild.build;
|
||||
const bs = browserSync.create();
|
||||
const { dev } = commandLineArgs({ name: 'dev', type: Boolean });
|
||||
|
||||
del.sync('./dist');
|
||||
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: 'types', type: Boolean }
|
||||
]);
|
||||
|
||||
try {
|
||||
if (!dev) execSync('tsc', { stdio: 'inherit' }); // for type declarations
|
||||
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-search.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-vscode-data.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-css.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-icons.js', { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
}
|
||||
const outdir = dir;
|
||||
|
||||
del.sync(outdir);
|
||||
mkdirp.sync(outdir);
|
||||
|
||||
(async () => {
|
||||
const entryPoints = [
|
||||
// The whole shebang dist
|
||||
'./src/shoelace.ts',
|
||||
// Components
|
||||
...(await glob('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Public utilities
|
||||
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
|
||||
// Theme stylesheets
|
||||
...(await glob('./src/themes/**/!(*.test).ts'))
|
||||
];
|
||||
try {
|
||||
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const buildResult = await esbuild
|
||||
.build({
|
||||
format: 'esm',
|
||||
target: 'es2017',
|
||||
entryPoints,
|
||||
outdir: './dist',
|
||||
entryPoints: [
|
||||
// The whole shebang
|
||||
'./src/shoelace.ts',
|
||||
// Components
|
||||
...(await glob('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Public utilities
|
||||
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
|
||||
// Theme stylesheets
|
||||
...(await glob('./src/themes/**/!(*.test).ts'))
|
||||
],
|
||||
outdir,
|
||||
chunkNames: 'chunks/[name].[hash]',
|
||||
incremental: dev,
|
||||
incremental: serve,
|
||||
define: {
|
||||
// Popper.js expects this to be set
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
bundle: true,
|
||||
//
|
||||
// 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,
|
||||
plugins: []
|
||||
})
|
||||
@@ -64,20 +76,23 @@ try {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the docs distribution by copying dist into the docs folder. This is what powers the website. It doesn't need
|
||||
// to exist in dev because Browser Sync routes it virtually.
|
||||
await del('./docs/dist');
|
||||
if (!dev) {
|
||||
await Promise.all([copy('./dist', './docs/dist')]);
|
||||
// Copy the build output to an additional directory
|
||||
if (copydir) {
|
||||
del.sync(copydir);
|
||||
copy(outdir, copydir);
|
||||
}
|
||||
|
||||
console.log(chalk.green('The build has finished! 📦\n'));
|
||||
console.log(chalk.green(`The build has been generated at ${outdir} 📦\n`));
|
||||
|
||||
if (dev) {
|
||||
// Dev server
|
||||
if (serve) {
|
||||
const port = await getPort({
|
||||
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
|
||||
@@ -104,10 +119,10 @@ try {
|
||||
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', { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
@@ -116,20 +131,22 @@ try {
|
||||
return;
|
||||
}
|
||||
|
||||
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
})
|
||||
.then(() => {
|
||||
bs.reload();
|
||||
})
|
||||
.then(() => bs.reload())
|
||||
.catch(err => console.error(chalk.red(err)));
|
||||
});
|
||||
|
||||
// Reload without rebuilding when the docs change
|
||||
bs.watch(['docs/**/*.md']).on('change', filename => {
|
||||
console.log(`Docs file changed - ${filename}`);
|
||||
execSync('node scripts/make-search.js', { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
bs.reload();
|
||||
});
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGTERM', () => buildResult.rebuild.dispose());
|
||||
}
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGTERM', () => buildResult.rebuild.dispose());
|
||||
})();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// This script generates stylesheets from all *.styles.ts files in src/themes
|
||||
//
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import esbuild from 'esbuild';
|
||||
import fs from 'fs/promises';
|
||||
import glob from 'globby';
|
||||
@@ -10,12 +11,13 @@ import path from 'path';
|
||||
import prettier from 'prettier';
|
||||
import stripComments from 'strip-css-comments';
|
||||
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const files = glob.sync('./src/themes/**/*.styles.ts');
|
||||
const outdir = './dist/themes';
|
||||
const themesDir = path.join(outdir, 'themes');
|
||||
|
||||
console.log('Generating stylesheets');
|
||||
|
||||
mkdirp.sync(outdir);
|
||||
mkdirp.sync(themesDir);
|
||||
|
||||
try {
|
||||
files.map(async file => {
|
||||
@@ -32,7 +34,7 @@ try {
|
||||
|
||||
const formattedStyles = prettier.format(stripComments(css), { parser: 'css' });
|
||||
const filename = path.basename(file).replace('.styles.ts', '.css');
|
||||
const outfile = path.join(outdir, filename);
|
||||
const outfile = path.join(themesDir, filename);
|
||||
await fs.writeFile(outfile, formattedStyles, 'utf8');
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
import Promise from 'bluebird';
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import copy from 'recursive-copy';
|
||||
import del from 'del';
|
||||
import download from 'download';
|
||||
@@ -13,7 +14,9 @@ import { stat, readFile, writeFile } from 'fs/promises';
|
||||
import glob from 'globby';
|
||||
import path from 'path';
|
||||
|
||||
const iconDir = './dist/assets/icons';
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const iconDir = path.join(outdir, '/assets/icons');
|
||||
|
||||
const iconPackageData = JSON.parse(readFileSync('./node_modules/bootstrap-icons/package.json', 'utf8'));
|
||||
let numIcons = 0;
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
|
||||
//
|
||||
import chalk from 'chalk';
|
||||
import mkdirp from 'mkdirp';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
mkdirp.sync('./dist');
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
|
||||
// Run the analyzer
|
||||
console.log('Generating component metadata');
|
||||
execSync('cem analyze --litelement --outdir dist', { stdio: 'inherit' });
|
||||
execSync(`cem analyze --litelement --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import glob from 'globby';
|
||||
import lunr from 'lunr';
|
||||
import { getAllComponents } from './shared.js';
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync('./dist/custom-elements.json', 'utf8'));
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
|
||||
|
||||
console.log('Generating search index for documentation');
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
// You must generate dist/custom-elements.json before running this script.
|
||||
//
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getAllComponents } from './shared.js';
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync('./dist/custom-elements.json', 'utf8'));
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
|
||||
|
||||
console.log('Generating custom data for VS Code');
|
||||
|
||||
@@ -53,4 +56,4 @@ components.map(component => {
|
||||
vscode.tags.push({ name, attributes });
|
||||
});
|
||||
|
||||
fs.writeFileSync('./dist/vscode.html-custom-data.json', JSON.stringify(vscode, null, 2), 'utf8');
|
||||
fs.writeFileSync(path.join(outdir, 'vscode.html-custom-data.json'), JSON.stringify(vscode, null, 2), 'utf8');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -28,15 +28,16 @@ export default class SlBreadcrumbItem extends LitElement {
|
||||
@state() hasPrefix = false;
|
||||
@state() hasSuffix = false;
|
||||
|
||||
/** Optional link to direct the user to when the breadcrumb item is activated. */
|
||||
/**
|
||||
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
|
||||
* internally. When unset, a button will be rendered instead.
|
||||
*/
|
||||
@property() href: string;
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** Optionally allows the user to determine how the link should talk to the browser.
|
||||
* ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types
|
||||
*/
|
||||
/** The `rel` attribute to use on the link. Only used when `href` is set. */
|
||||
@property() rel: string = 'noreferrer noopener';
|
||||
|
||||
handleSlotChange() {
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -44,7 +44,10 @@ export default class SlMutationObserver extends LitElement {
|
||||
this.handleMutation = this.handleMutation.bind(this);
|
||||
|
||||
this.mutationObserver = new MutationObserver(this.handleMutation);
|
||||
this.startObserver();
|
||||
|
||||
if (!this.disabled) {
|
||||
this.startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
||||
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}"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { sendKeys } from '@web/test-runner-commands';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlRadio from './radio';
|
||||
import type SlRadioGroup from '../radio-group/radio-group';
|
||||
|
||||
describe('<sl-radio>', () => {
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
@@ -26,7 +27,7 @@ describe('<sl-radio>', () => {
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled via keyboard', async () => {
|
||||
it('should fire sl-change when toggled via keyboard - space', async () => {
|
||||
const el = await fixture<SlRadio>(html` <sl-radio></sl-radio> `);
|
||||
const input = el.shadowRoot?.querySelector('input');
|
||||
input.focus();
|
||||
@@ -36,6 +37,23 @@ describe('<sl-radio>', () => {
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled via keyboard - arrow key', async () => {
|
||||
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||
<sl-radio-group>
|
||||
<sl-radio id="radio-1"></sl-radio>
|
||||
<sl-radio id="radio-2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1');
|
||||
const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2');
|
||||
const input1 = radio1.shadowRoot?.querySelector('input');
|
||||
input1.focus();
|
||||
setTimeout(() => sendKeys({ press: 'ArrowRight' }));
|
||||
const event = await oneEvent(radio2, 'sl-change');
|
||||
expect(event.target).to.equal(radio2);
|
||||
expect(radio2.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should not fire sl-change when checked is set by javascript', async () => {
|
||||
const el = await fixture<SlRadio>(html` <sl-radio></sl-radio> `);
|
||||
el.addEventListener('sl-change', () => expect.fail('event fired'));
|
||||
|
||||
@@ -136,6 +136,7 @@ export default class SlRadio extends LitElement {
|
||||
this.getAllRadios().map(radio => (radio.checked = false));
|
||||
radios[index].focus();
|
||||
radios[index].checked = true;
|
||||
emit(radios[index], 'sl-change');
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './resize-observer.styles';
|
||||
|
||||
/**
|
||||
@@ -18,31 +19,60 @@ export default class SlResizeObserver extends LitElement {
|
||||
private resizeObserver: ResizeObserver;
|
||||
private observedElements: HTMLElement[] = [];
|
||||
|
||||
/** Disables the observer. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
emit(this, 'sl-resize', { detail: { entries } });
|
||||
});
|
||||
|
||||
if (!this.disabled) {
|
||||
this.startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.disconnect();
|
||||
this.stopObserver();
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
if (!this.disabled) {
|
||||
this.startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
startObserver() {
|
||||
const slot = this.shadowRoot!.querySelector('slot')!;
|
||||
const elements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
|
||||
// Unwatch previous elements
|
||||
this.observedElements.map(el => this.resizeObserver.unobserve(el));
|
||||
this.observedElements = [];
|
||||
if (slot) {
|
||||
const elements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
|
||||
// Watch new elements
|
||||
elements.map(el => {
|
||||
this.resizeObserver.observe(el);
|
||||
this.observedElements.push(el);
|
||||
});
|
||||
// Unwatch previous elements
|
||||
this.observedElements.map(el => this.resizeObserver.unobserve(el));
|
||||
this.observedElements = [];
|
||||
|
||||
// Watch new elements
|
||||
elements.map(el => {
|
||||
this.resizeObserver.observe(el);
|
||||
this.observedElements.push(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stopObserver() {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
if (this.disabled) {
|
||||
this.stopObserver();
|
||||
} else {
|
||||
this.startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -51,8 +51,8 @@ let id = 0;
|
||||
* @csspart prefix - The select's prefix.
|
||||
* @csspart label - The select's label.
|
||||
* @csspart suffix - The select's suffix.
|
||||
* @csspart menu - The select menu, a <sl-menu> element.
|
||||
* @csspart tag - The multiselect option, a <sl-tag> element.
|
||||
* @csspart menu - The select menu, an <sl-menu> element.
|
||||
* @csspart tag - The multiselect option, an <sl-tag> element.
|
||||
* @csspart tags - The container in which multiselect options are rendered.
|
||||
*/
|
||||
@customElement('sl-select')
|
||||
@@ -336,7 +336,7 @@ export default class SlSelect extends LitElement {
|
||||
const clearButton = path.find((el: SlIconButton) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
const element = el as HTMLElement;
|
||||
return element.classList.contains('tag__clear');
|
||||
return element.classList.contains('tag__remove');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -382,10 +382,10 @@ export default class SlSelect extends LitElement {
|
||||
type="neutral"
|
||||
size=${this.size}
|
||||
?pill=${this.pill}
|
||||
clearable
|
||||
removable
|
||||
@click=${this.handleTagInteraction}
|
||||
@keydown=${this.handleTagInteraction}
|
||||
@sl-clear=${(event: CustomEvent) => {
|
||||
@sl-remove=${(event: CustomEvent) => {
|
||||
event.stopPropagation();
|
||||
if (!this.disabled) {
|
||||
item.checked = false;
|
||||
|
||||
25
src/components/spinner/spinner.test.ts
Normal file
25
src/components/spinner/spinner.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlSpinner from './spinner';
|
||||
|
||||
describe('<sl-spinner>', () => {
|
||||
let el: SlSpinner;
|
||||
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlSpinner>(html` <sl-spinner></sl-spinner> `);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should defer updates to screen reader users via aria-live="polite".', async () => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as SVGElement;
|
||||
await expect(base).have.attribute('aria-busy', 'true');
|
||||
await expect(base).have.attribute('aria-live', 'polite');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default css`
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tag__clear::part(base) {
|
||||
.tag__remove::part(base) {
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default css`
|
||||
padding: 0 var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
.tag--small .tag__clear {
|
||||
.tag--small .tag__remove {
|
||||
margin-left: var(--sl-spacing-2x-small);
|
||||
margin-right: calc(-1 * var(--sl-spacing-3x-small));
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export default css`
|
||||
padding: 0 var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.tag__clear {
|
||||
.tag__remove {
|
||||
margin-left: var(--sl-spacing-2x-small);
|
||||
margin-right: calc(-1 * var(--sl-spacing-2x-small));
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export default css`
|
||||
padding: 0 var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.tag__clear {
|
||||
.tag__remove {
|
||||
margin-left: var(--sl-spacing-2x-small);
|
||||
margin-right: calc(-1 * var(--sl-spacing-x-small));
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import '../icon-button/icon-button';
|
||||
*
|
||||
* @slot - The tag's content.
|
||||
*
|
||||
* @event sl-clear - Emitted when the clear button is activated.
|
||||
* @event sl-remove - Emitted when the remove button is activated.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart content - The tag content.
|
||||
* @csspart clear-button - The clear button.
|
||||
* @csspart remove-button - The remove button.
|
||||
*/
|
||||
@customElement('sl-tag')
|
||||
export default class SlTag extends LitElement {
|
||||
@@ -33,11 +33,11 @@ export default class SlTag extends LitElement {
|
||||
/** Draws a pill-style tag with rounded edges. */
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
/** Makes the tag clearable. */
|
||||
@property({ type: Boolean }) clearable = false;
|
||||
/** Makes the tag removable. */
|
||||
@property({ type: Boolean }) removable = false;
|
||||
|
||||
handleClearClick() {
|
||||
emit(this, 'sl-clear');
|
||||
handleRemoveClick() {
|
||||
emit(this, 'sl-remove');
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -62,21 +62,21 @@ export default class SlTag extends LitElement {
|
||||
|
||||
// Modifers
|
||||
'tag--pill': this.pill,
|
||||
'tag--clearable': this.clearable
|
||||
'tag--removable': this.removable
|
||||
})}
|
||||
>
|
||||
<span part="content" class="tag__content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
|
||||
${this.clearable
|
||||
${this.removable
|
||||
? html`
|
||||
<sl-icon-button
|
||||
exportparts="base:clear-button"
|
||||
exportparts="base:remove-button"
|
||||
name="x"
|
||||
library="system"
|
||||
class="tag__clear"
|
||||
@click=${this.handleClearClick}
|
||||
class="tag__remove"
|
||||
@click=${this.handleRemoveClick}
|
||||
></sl-icon-button>
|
||||
`
|
||||
: ''}
|
||||
|
||||
@@ -83,6 +83,12 @@ export default class SlTooltip extends LitElement {
|
||||
*/
|
||||
@property() trigger = 'hover focus';
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the tooltip 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.handleBlur = this.handleBlur.bind(this);
|
||||
@@ -216,7 +222,7 @@ export default class SlTooltip extends LitElement {
|
||||
|
||||
this.popover = createPopper(this.target, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: 'absolute',
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
@@ -258,6 +264,7 @@ export default class SlTooltip extends LitElement {
|
||||
@watch('placement')
|
||||
@watch('distance')
|
||||
@watch('skidding')
|
||||
@watch('hoist')
|
||||
handleOptionsChange() {
|
||||
this.syncOptions();
|
||||
}
|
||||
@@ -290,7 +297,7 @@ export default class SlTooltip extends LitElement {
|
||||
if (this.popover) {
|
||||
this.popover.setOptions({
|
||||
placement: this.placement,
|
||||
strategy: 'absolute',
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
|
||||
@@ -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