Compare commits

..

77 Commits

Author SHA1 Message Date
Cory LaViska
8ee519d40a fix positioning 2021-10-30 15:54:56 -04:00
Cory LaViska
6bc17d48c3 update examples 2021-10-29 18:35:24 -04:00
Cory LaViska
a1263f1b9d add experimental context menu 2021-10-29 18:32:45 -04:00
Cory LaViska
d69ebab765 update examples 2021-10-29 18:32:26 -04:00
Cory LaViska
0504946dac refactor popper creation 2021-10-29 18:32:12 -04:00
Cory LaViska
fbd6691711 improve search panel color 2021-10-29 14:42:17 -04:00
Cory LaViska
aec17da6b0 improve trigger border color in dark mode 2021-10-29 14:35:57 -04:00
Cory LaViska
639533662d update changelog 2021-10-26 09:35:59 -04:00
Cory LaViska
a340ce4a68 document part 2021-10-26 09:35:46 -04:00
Cory LaViska
6e5fe64e8b add eye dropper 2021-10-26 09:35:07 -04:00
Cory LaViska
84bdbb84b8 update lit 2021-10-26 09:34:48 -04:00
Cory LaViska
f91ffb6cb4 fix border radius on single button groups 2021-10-26 09:34:33 -04:00
Cory LaViska
13815199a3 fix dark theme link 2021-10-22 10:57:07 -04:00
Cory LaViska
98c20ff551 2.0.0-beta.58 2021-10-22 10:52:27 -04:00
Cory LaViska
479b6b9081 bundle back up for now 2021-10-22 10:51:17 -04:00
Cory LaViska
c640d2ea77 add stack overflow section 2021-10-19 10:51:41 -04:00
Cory LaViska
715547d2fd update changelog 2021-10-19 09:56:41 -04:00
Cory LaViska
8a914a536b fix cssproperty docs 2021-10-19 09:56:28 -04:00
Cory LaViska
f56b6c0648 remove RAFs 2021-10-19 09:52:41 -04:00
Denis Korablev
25aa8318d9 fix(sl-range): add value change handler (#572) 2021-10-19 09:48:39 -04:00
Cory LaViska
72f2cbe9e8 fix aspect ratio bug 2021-10-19 09:43:16 -04:00
Cory LaViska
fc7836084a add tooltip guard 2021-10-18 17:54:29 -04:00
Cory LaViska
60d9d9202b update bootstrap-icons to 1.6.1 2021-10-18 17:07:52 -04:00
Cory LaViska
a9df468282 fixes #563 2021-10-18 17:07:07 -04:00
Yuki Nishijima
0bba773c3e Bring the divider back to the Shadow DOM (#568) 2021-10-18 09:14:13 -04:00
Cory LaViska
7be03ae623 fix metadata plugin 2021-10-18 08:58:50 -04:00
Cory LaViska
d4741532f5 fix build dir 2021-10-16 10:35:42 -04:00
Cory LaViska
10f31efefa fix comment parser 2021-10-16 10:28:51 -04:00
Cory LaViska
be662ddf32 add animated-image 2021-10-16 08:29:25 -04:00
Cory LaViska
ff84beaade use :enabled 2021-10-14 09:02:00 -04:00
Cory LaViska
8dba8fa5fb fix tooltip bug 2021-10-14 09:01:37 -04:00
Cory LaViska
3a3f5552a7 fix test:watch 2021-10-14 08:39:17 -04:00
Cory LaViska
88cba353c0 add labels examples 2021-10-14 08:34:54 -04:00
Cory LaViska
a2851370bb revert styles 2021-10-14 08:32:11 -04:00
Cory LaViska
7c0ef7dcf0 Merge branch 'christoshrousis-test/progress' into next 2021-10-14 08:25:42 -04:00
Cory LaViska
fb6d5d89b8 use label attrib 2021-10-14 08:24:38 -04:00
Cory LaViska
45ceff4c08 Merge branch 'test/progress' of https://github.com/christoshrousis/shoelace into christoshrousis-test/progress 2021-10-14 08:10:57 -04:00
Cory LaViska
6169abc700 update bootstrap icons 2021-10-14 07:21:27 -04:00
Cory LaViska
c09e12d13e 2.0.0-beta.57 2021-10-13 17:34:04 -04:00
Cory LaViska
6152e15e10 fix esm links 2021-10-13 17:30:13 -04:00
Cory LaViska
79910b2ae8 2.0.0-beta.56 2021-10-13 17:14:22 -04:00
Cory LaViska
c347df7c17 rework build to support bare specifiers 2021-10-13 17:12:50 -04:00
Cory LaViska
e9e2b35c59 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2021-10-13 08:30:16 -04:00
Denis Korablev
8ae753c396 feat: add hoist attribute for sl-tooltip (#565) 2021-10-13 08:29:38 -04:00
Cory LaViska
d2c94321f2 update docs 2021-10-12 10:26:26 -04:00
Yuki Nishijima
4c10f8a537 Update the CSS path in the Integrating with Rails (#561) 2021-10-11 08:25:09 -04:00
Christos Hrousis
9a19cc2173 revert: misunderstood part/slot definition. 2021-10-10 13:37:32 +11:00
Christos Hrousis
c4cbc894f5 revert: misunderstood part/slot definition. 2021-10-10 13:36:41 +11:00
Christos Hrousis
449f5e6c7f style: typo. 2021-10-10 13:24:28 +11:00
Christos Hrousis
34447a3f2f test: migrate progress-ring tests to progress-bar
- Match coverage with progress-ring
- Attached titles/label/labelledby
- Value '' on aria-valuenow is does not pass AXE
2021-10-10 13:24:01 +11:00
Christos Hrousis
eee97d7dba test: cover progress-ring
- Add title to make ring accessibly hoverable.
- Add label/labelledby as aria options.
- Remove ununsed label slot.
2021-10-10 13:05:40 +11:00
Christos Hrousis
f16392947a docs: ring uses css prop for track-width. 2021-10-10 13:00:41 +11:00
Cory LaViska
c3adf92b49 2.0.0-beta.55 2021-10-08 17:06:12 -04:00
Cory LaViska
a6580b018d updat3e changelog 2021-10-08 17:05:53 -04:00
Cory LaViska
00c843c7ce revert unbundling 2021-10-08 17:04:30 -04:00
Cory LaViska
5fe55a4db9 2.0.0-beta.54 2021-10-08 16:55:09 -04:00
Cory LaViska
4ca998c346 revert 2021-10-08 16:54:57 -04:00
Cory LaViska
42b3e2cc11 update changelog 2021-10-08 16:53:31 -04:00
Cory LaViska
59fb8db6be unbundle select deps 2021-10-08 16:52:01 -04:00
Cory LaViska
664beafefa 2.0.0-beta.54 2021-10-08 16:42:53 -04:00
Cory LaViska
04443a64e2 update changelog 2021-10-08 16:39:10 -04:00
Cory LaViska
92dedf3386 ship bare module specifiers for prod 2021-10-08 10:11:12 -04:00
Cory LaViska
2ba5fb9820 update command line args 2021-10-08 10:10:26 -04:00
Cory LaViska
222235159b rename clear => remove 2021-10-07 09:52:23 -04:00
Cory LaViska
1061bd5e0d add disabled prop 2021-10-07 09:31:20 -04:00
Cory LaViska
ccec9a8348 fix initial disabled state 2021-10-07 09:30:47 -04:00
Cory LaViska
bf9e06e67d remove attr column 2021-10-05 10:41:52 -04:00
Cory LaViska
1e03d222c5 fix code in tables 2021-10-05 10:41:39 -04:00
Cory LaViska
3722f46b8e Merge branch 'kanoni4567-next' into next 2021-10-05 09:03:51 -04:00
Cory LaViska
8d7bf97127 Merge branch 'next' of https://github.com/kanoni4567/shoelace into kanoni4567-next 2021-10-05 09:01:35 -04:00
Cory LaViska
d26c1a6407 update docs 2021-10-05 08:58:41 -04:00
Rich Klein
f296ff8476 Create integrating-with-laravel.md (#553)
* Create integrating-with-laravel.md

Instructions for using Shoelace components in a Laravel 8.x application.

* Update integrating-with-laravel.md

Added a section for setting the base path and switched to using the full import path for each component. Also included a full `bootstrap.js` example.
2021-10-05 08:54:12 -04:00
Christos Hrousis
a75a71994a test: component/spinner (#556)
- covers accessibility
- provides explainer for aria-busy and aria-live tags.
2021-10-05 08:53:28 -04:00
Shanyu Cui
7d1373a1d1 radio: emit sl-change when toggled via arrow keys 2021-10-04 23:27:28 -07:00
Shanyu Cui
23abc50015 Merge branch 'shoelace-style:next' into next 2021-10-04 22:39:37 -07:00
Shanyu Cui
17df0e3cd3 Merge branch 'next' of github.com:shoelace-style/shoelace into next 2021-07-16 02:04:07 -07:00
Shanyu Cui
76df2fd204 update hasSlot selector to search top-level only 2021-07-16 02:03:02 -07:00
59 changed files with 1591 additions and 301 deletions

View File

@@ -21,6 +21,7 @@
- [Card](/components/card)
- [Checkbox](/components/checkbox)
- [Color Picker](/components/color-picker)
- [Context Menu](/components/context-menu)
- [Details](/components/details)
- [Dialog](/components/dialog)
- [Divider](/components/divider)
@@ -54,6 +55,7 @@
<!--plop:component-->
- Utilities
- [Animated Image](/components/animated-image)
- [Animation](/components/animation)
- [Format Bytes](/components/format-bytes)
- [Format Date](/components/format-date)
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

View File

@@ -39,7 +39,7 @@ body.site-search-visible {
flex-direction: column;
max-width: 460px;
max-height: calc(100vh - 20rem);
background-color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-surface-base-alt));
border-radius: var(--sl-border-radius-large);
box-shadow: var(--sl-shadow-x-large);
margin: 10rem auto;

View File

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

View File

@@ -0,0 +1,63 @@
# Animated Image
[component-header:sl-animated-image]
A component for displaying animated GIFs and WEBPs that play and pause on interaction.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
></sl-animated-image>
```
## Examples
### WEBP Images
Both GIF and WEBP images are supported.
```html preview
<sl-animated-image
src="/assets/images/tie.webp"
alt="Animation of a shoe being tied"
></sl-animated-image>
```
### Setting a Width and Height
To set a custom size, apply a width and/or height to the host element.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
style="width: 150px; height: 200px;"
>
</sl-animated-image>
```
### Customizing the Control Box
You can change the appearance and location of the control box by targeting the `control-box` part in your styles.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
class="animated-image-custom-control-box"
></sl-animated-image>
<style>
.animated-image-custom-control-box::part(control-box) {
top: auto;
right: auto;
bottom: 1rem;
left: 1rem;
background-color: deeppink;
color: white;
}
</style>
```
[component-metadata:sl-animated-image]

View File

@@ -0,0 +1,140 @@
# Context Menu
[component-header:sl-context-menu]
Context menus offer additional options through a menu that opens at the pointer's location, usually activated by a right-click.
Context menus are designed to work with [menus](/components/menu) and [menu items](/components/menu-item). The menu must include `slot="menu"`. Other content you provide will be part of the context menu's target area.
```html preview
<sl-context-menu>
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
## Examples
### Handling Selections
The [menu component](/components/menu) emits an `sl-select` event when a menu item is selected. You can use this to handle selections. The selected item will be available in `event.detail.item`.
```html preview
<div class="context-menu-selections">
<sl-context-menu>
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
</div>
<script>
const container = document.querySelector('.context-menu-selections');
const menu = container.querySelector('sl-menu');
const result = container.querySelector('.result');
menu.addEventListener('sl-select', event => {
console.log(`You selected: ${event.detail.item.value}`);
});
</script>
```
### Inline
The context menu uses `display: contents`, so it will assume the shape of the content you slot in.
```html preview
<sl-context-menu>
<span style="background: rgb(var(--sl-color-neutral-100)); padding: .5rem 1rem;">
Right-click here
</span>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
### Placement
The preferred placement of the context menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the menu remains in the viewport.
```html preview
<sl-context-menu placement="top-end">
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
### Detecting the Target Item
A single context menu can wrap a number of items. To detect the item that activated the context menu...
TODO
```html preview
<div class="context-menu-detecting">
<sl-context-menu>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
</div>
<style>
.context-menu-detecting ul {
max-width: 300px;
list-style: none;
padding: 0;
margin: 0;
}
.context-menu-detecting li {
background: rgb(var(--sl-color-neutral-100));
padding: .5rem 1rem;
margin: 0 0 2px 0;
}
</style>
```
[component-metadata:sl-context-menu]

View File

@@ -33,6 +33,33 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
## Examples
### Getting the Selected Item
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
```html preview
<div class="dropdown-selection">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection');
const dropdown = container.querySelector('sl-dropdown');
dropdown.addEventListener('sl-select', event => {
const selectedItem = event.detail.item;
console.log(selectedItem.value);
});
</script>
```
### Placement
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
@@ -121,57 +148,4 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
</style>
```
### Getting the Selected Item
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
```html preview
<div class="dropdown-selection">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection');
const dropdown = container.querySelector('sl-dropdown');
dropdown.addEventListener('sl-select', event => {
const selectedItem = event.detail.item;
console.log(selectedItem.value);
});
</script>
```
Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event.
```html preview
<div class="dropdown-selection-alt">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection-alt');
const cut = container.querySelector('sl-menu-item[value="cut"]');
const copy = container.querySelector('sl-menu-item[value="copy"]');
const paste = container.querySelector('sl-menu-item[value="paste"]');
cut.addEventListener('click', () => console.log('cut'));
copy.addEventListener('click', () => console.log('copy'));
paste.addEventListener('click', () => console.log('paste'));
</script>
```
[component-metadata:sl-dropdown]

View File

@@ -20,10 +20,18 @@ Use the `--height` custom property to set the progress bar's height.
### Labels
Use the default slot to show a label.
Use the `label` attribute to label the progress bar and tell assistive devices how to announce it.
```html preview
<sl-progress-bar value="50" class="progress-bar-labels">50%</sl-progress-bar>
<sl-progress-bar value="50" label="Upload progress"></sl-progress-bar>
```
### Showing Values
Use the default slot to show a value.
```html preview
<sl-progress-bar value="50" class="progress-bar-values">50%</sl-progress-bar>
<br>
@@ -31,7 +39,7 @@ Use the default slot to show a label.
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
<script>
const progressBar = document.querySelector('.progress-bar-labels');
const progressBar = document.querySelector('.progress-bar-values');
const subtractButton = progressBar.nextElementSibling.nextElementSibling;
const addButton = subtractButton.nextElementSibling;

View File

@@ -20,10 +20,10 @@ Use the `--size` custom property to set the diameter of the progress ring.
### Track Width
Use the `track-width` attribute to set the width of the progress ring's track.
Use the `--track-width` custom property to set the width of the progress ring's track.
```html preview
<sl-progress-ring value="50" stroke-width="10"></sl-progress-ring>
<sl-progress-ring value="50" style="--track-width: 10px;"></sl-progress-ring>
```
### Colors
@@ -42,10 +42,18 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
### Labels
Use the `label` attribute to label the progress ring and tell assistive devices how to announce it.
```html preview
<sl-progress-ring value="50" label="Upload progress"></sl-progress-ring>
```
### Showing Values
Use the default slot to show a label.
```html preview
<sl-progress-ring value="50" class="progress-ring-labels" style="margin-bottom: .5rem;">50%</sl-progress-ring>
<sl-progress-ring value="50" class="progress-ring-values" style="margin-bottom: .5rem;">50%</sl-progress-ring>
<br>
@@ -53,7 +61,7 @@ Use the default slot to show a label.
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
<script>
const progressRing = document.querySelector('.progress-ring-labels');
const progressRing = document.querySelector('.progress-ring-values');
const subtractButton = progressRing.nextElementSibling.nextElementSibling;
const addButton = subtractButton.nextElementSibling;

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) i
- Learn more about the project, its values, and its roadmap
<sl-button type="primary" href="https://github.com/shoelace-style/shoelace/discussions" target="_blank">
<sl-icon name="github" slot="prefix"></sl-icon>
Join the Discussion
</sl-button>
@@ -27,9 +28,19 @@ The [community chat](https://discord.gg/mg8f26C) is open to the public and power
- Chat live with other designers, developers, and Shoelace fans
<sl-button type="primary" href="https://discord.gg/mg8f26C" target="_blank">
<sl-icon name="discord" slot="prefix"></sl-icon>
Join the Chat
</sl-button>
## Stack Overflow
You can post questions on Stack Overflow using [the "shoelace" tag](https://stackoverflow.com/questions/tagged/shoelace). This is a public forum where talented developers answer questions. It's a great way to get help, but it is not maintained or actively monitored by the Shoelace author.
<sl-button type="primary" href="https://stackoverflow.com/questions/ask?tags=shoelace" target="_blank">
<sl-icon name="stack-overflow" slot="prefix"></sl-icon>
Ask for Help
</sl-button>
## Twitter
Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for general updates and announcements about Shoelace. This is a great place to say "hi" or to share something you're working on. You're also welcome to follow [@claviska](https://twitter.com/claviska), the creator, for tweets about web components, web development, and life.
@@ -37,5 +48,6 @@ Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for gene
**Please avoid using Twitter for support questions.** The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) is a much better place to share code snippets, screenshots, and other troubleshooting info. You'll have much better luck there, as more users will have a chance to help you.
<sl-button type="primary" href="https://twitter.com/shoelace_style" target="_blank">
<sl-icon name="twitter" slot="prefix"></sl-icon>
Follow on Twitter
</sl-button>

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
export default css`
${componentStyles}
:host {
--control-box-size: 2.5rem;
--icon-size: calc(var(--control-box-size) * 0.625);
display: inline-flex;
position: relative;
cursor: pointer;
}
img {
display: block;
width: 100%;
height: 100%;
}
img[aria-hidden='true'] {
display: none;
}
.animated-image__control-box {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
top: calc(50% - var(--control-box-size) / 2);
right: calc(50% - var(--control-box-size) / 2);
width: var(--control-box-size);
height: var(--control-box-size);
font-size: var(--icon-size);
background: none;
border: none;
background-color: rgb(var(--sl-color-neutral-1000) / 50%);
border-radius: var(--sl-border-radius-circle);
color: rgb(var(--sl-color-neutral-0));
pointer-events: none;
transition: var(--sl-transition-fast) opacity;
}
:host([play]:hover) .animated-image__control-box {
opacity: 1;
transform: scale(1);
}
:host([play]:not(:hover)) .animated-image__control-box {
opacity: 0;
}
`;

View File

@@ -0,0 +1,13 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlAnimatedImage from './animated-image';
describe('<sl-animated-image>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-animated-image></sl-animated-image> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,120 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch';
import { emit } from '../../internal/event';
import styles from './animated-image.styles';
import '../icon/icon';
/**
* @since 2.0
* @status experimental
*
* @dependency sl-icon
*
* @event sl-load - Emitted when the image loads successfully.
* @event sl-error - Emitted when the image fails to load.
*
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
* @part - play-icon - The icon to use for the play button.
* @part - pause-icon - The icon to use for the pause button.
*
* @cssproperty --control-box-size - The size of the icon box.
* @cssproperty --icon-size - The size of the play/pause icons.
*/
@customElement('sl-animated-image')
export default class SlAnimatedImage extends LitElement {
static styles = styles;
@state() frozenFrame: string;
@state() isLoaded = false;
@query('.animated-image__animated') animatedImage: HTMLImageElement;
/** The image's src attribute. */
@property() src: string;
/** The image's alt attribute. */
@property() alt: string;
/** When set, the image will animate. Otherwise, it will be paused. */
@property({ type: Boolean, reflect: true }) play: boolean;
handleClick() {
this.play = !this.play;
}
handleLoad() {
const canvas = document.createElement('canvas');
const { width, height } = this.animatedImage;
canvas.width = width;
canvas.height = height;
canvas.getContext('2d')!.drawImage(this.animatedImage, 0, 0, width, height);
this.frozenFrame = canvas.toDataURL('image/gif');
if (!this.isLoaded) {
emit(this, 'sl-load');
this.isLoaded = true;
}
}
handleError() {
emit(this, 'sl-error');
}
@watch('play')
async handlePlayChange() {
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
// won't trigger another request.
if (this.play) {
this.animatedImage.src = '';
this.animatedImage.src = this.src;
}
}
@watch('src')
handleSrcChange() {
this.isLoaded = false;
}
render() {
return html`
<div class="animated-image">
<img
class="animated-image__animated"
src=${this.src}
alt=${this.alt}
crossorigin="anonymous"
aria-hidden=${this.play ? 'false' : 'true'}
@click=${this.handleClick}
@load=${this.handleLoad}
@error=${this.handleError}
/>
${this.isLoaded
? html`
<img
class="animated-image__frozen"
src=${this.frozenFrame}
alt=${this.alt}
aria-hidden=${this.play ? 'true' : 'false'}
@click=${this.handleClick}
/>
<div part="control-box" class="animated-image__control-box">
${this.play
? html`<sl-icon part="pause-icon" name="pause-fill" library="system"></sl-icon>`
: html`<sl-icon part="play-icon" name="play-fill" library="system"></sl-icon>`}
</div>
`
: ''}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'sl-animated-image': SlAnimatedImage;
}
}

View File

@@ -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() {

View File

@@ -598,7 +598,7 @@ export default css`
* buttons and we style them here instead.
*/
:host(.sl-button-group__button--first) .button {
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
@@ -607,7 +607,7 @@ export default css`
border-radius: 0;
}
:host(.sl-button-group__button--last) .button {
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View File

@@ -191,11 +191,14 @@ export default css`
flex: 1 1 auto;
}
.color-picker__user-input sl-button-group {
margin-left: var(--sl-spacing-small);
}
.color-picker__user-input sl-button {
min-width: 3.25rem;
max-width: 3.25rem;
font-size: 1rem;
margin-left: var(--sl-spacing-small);
}
.color-picker__swatches {
@@ -299,7 +302,7 @@ export default css`
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
box-shadow: inset 0 0 0 1px rgb(var(--sl-color-neutral-1000) / 25%);
transition: inherit;
}

View File

@@ -13,15 +13,19 @@ import color from 'color';
import styles from './color-picker.styles';
import '../button/button';
import '../button-group/button-group';
import '../dropdown/dropdown';
import '../icon/icon';
import '../input/input';
const hasEyeDropper = 'EyeDropper' in window;
/**
* @since 2.0
* @status stable
*
* @dependency sl-button
* @dependency sl-button-group
* @dependency sl-dropdown
* @dependency sl-input
*
@@ -39,6 +43,7 @@ import '../input/input';
* @csspart slider-handle - Hue and opacity slider handles.
* @csspart preview - The preview color.
* @csspart input - The text input.
* @csspart eye-dropper-button - The toggle format button's base.
* @csspart format-button - The toggle format button's base.
*
* @cssproperty --grid-width - The width of the color grid.
@@ -570,6 +575,22 @@ export default class SlColorPicker extends LitElement {
this.previewButton.classList.remove('color-picker__preview-color--copied');
}
handleEyeDropper() {
if (!hasEyeDropper) {
return;
}
// @ts-ignore
const eyeDropper = new EyeDropper();
eyeDropper
.open()
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex))
.catch(() => {
// The user canceled, do nothing
});
}
@watch('format')
handleFormatChange() {
this.syncValues();
@@ -606,6 +627,7 @@ export default class SlColorPicker extends LitElement {
const x = this.saturation;
const y = 100 - this.lightness;
// TODO - i18n for format, copy, and eye dropper buttons
const colorPicker = html`
<div
part="base"
@@ -708,6 +730,7 @@ export default class SlColorPicker extends LitElement {
type="button"
part="preview"
class="color-picker__preview color-picker__transparent-bg"
aria-label="Copy"
style=${styleMap({
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
})}
@@ -730,13 +753,26 @@ export default class SlColorPicker extends LitElement {
@sl-change=${this.handleInputChange}
></sl-input>
${!this.noFormatToggle
? html`
<sl-button exportparts="base:format-button" @click=${this.handleFormatToggle}>
${this.setLetterCase(this.format)}
</sl-button>
`
: ''}
<sl-button-group>
${!this.noFormatToggle
? html`
<sl-button
aria-label="Change format"
exportparts="base:format-button"
@click=${this.handleFormatToggle}
>
${this.setLetterCase(this.format)}
</sl-button>
`
: ''}
${hasEyeDropper
? html`
<sl-button exportparts="base:eye-dropper-button" @click=${this.handleEyeDropper}>
<sl-icon library="system" name="eyedropper" label="Select a color from the screen"></sl-icon>
</sl-button>
`
: ''}
</sl-button-group>
</div>
${this.swatches

View File

@@ -0,0 +1,43 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
export default css`
${componentStyles}
:host {
display: contents;
}
::slotted(sl-menu) {
min-width: 180px;
background: rgb(var(--sl-panel-background-color));
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
border-radius: var(--sl-border-radius-medium);
box-shadow: var(--sl-shadow-large);
}
.context-menu {
position: relative;
z-index: var(--sl-z-index-dropdown);
}
.context-menu__locater {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
pointer-events: none;
}
.dropdown__positioner {
position: absolute;
}
.context-menu__menu {
position: relative;
top: 0;
left: 0;
pointer-events: all;
}
`;

View File

@@ -0,0 +1,13 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlContextMenu from './context-menu';
describe('<sl-context-menu>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-context-menu></sl-context-menu> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,292 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { emit, waitForEvent } from '../../internal/event';
import { watch } from '../../internal/watch';
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
import { animateTo, stopAnimations } from '../../internal/animate';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import type SlMenu from '../menu/menu';
import styles from './context-menu.styles';
import '../menu/menu';
/**
* @since 2.0
* @status experimental
*
* @dependency sl-menu
*
* @event sl-event-name - Emitted as an example.
*
* @slot - Content that will activate the context menu when right-clicked.
* @slot menu - The menu to show when the context menu is activated, an `<sl-menu>` element.
*
* @event sl-show - Emitted when the context menu opens.
* @event sl-after-show - Emitted after the context menu opens and all animations are complete.
* @event sl-hide - Emitted when the context menu closes.
* @event sl-after-hide - Emitted after the context menu closes and all animations are complete.
*
* @animation contextMenu.show - The animation to use when showing the context menu.
* @animation contextMenu.hide - The animation to use when hiding the context menu.
*/
@customElement('sl-context-menu')
export default class SlContextMenu extends LitElement {
static styles = styles;
@query('.context-menu') wrapper: HTMLElement;
@query('.context-menu__locater') locater: HTMLElement;
@query('.context-menu__menu') menu: HTMLSlotElement;
@query('.context-menu__positioner') positioner: HTMLElement;
private popover: PopperInstance;
/**
* The preferred placement of the context menu. Note that the actual placement may vary as needed to keep the menu
* inside of the viewport.
*/
@property() placement:
| 'top'
| 'top-start'
| 'top-end'
| 'right'
| 'right-start'
| 'right-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end' = 'bottom-start';
/** Disables the context menu so it won't show when triggered. */
@property({ type: Boolean, reflect: true }) disabled = false;
/** The distance in pixels from which to offset the context menu away from its target. */
@property({ type: Number }) distance = 0;
/** Indicates whether or not the context menu is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false;
/** The distance in pixels from which to offset the context menu along its target. */
@property({ type: Number }) skidding = 0;
/**
* Enable this option to prevent the menu from being clipped when the component is placed inside a container with
* `overflow: auto|hidden|scroll`.
*/
@property({ type: Boolean }) hoist = false;
connectedCallback() {
super.connectedCallback();
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
firstUpdated() {
this.menu.hidden = !this.open;
}
getMenu() {
const slot = this.menu.querySelector('slot')!;
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
}
async handleContextMenu(event: MouseEvent) {
const target = event.target as HTMLElement;
const targetRect = target.getBoundingClientRect();
const wrapperRect = this.wrapper.getBoundingClientRect();
const { offsetX, offsetY } = event;
const x = targetRect.left + offsetX - wrapperRect.left;
const y = targetRect.top + offsetY - wrapperRect.top;
event.preventDefault();
if (this.open) {
await this.hide();
}
this.show(x, y);
}
handleDocumentKeyDown(event: KeyboardEvent) {
const menu = this.getMenu();
const menuItems = menu ? menu.getAllItems() : [];
const firstMenuItem = menuItems[0];
const lastMenuItem = menuItems[menuItems.length - 1];
// Close when escape is pressed
if (event.key === 'Escape') {
this.hide();
return;
}
// Forward key presses that don't originate from the menu to allow keyboard selection and type-to-select
if (menu && !event.composedPath().includes(this.menu)) {
// Focus on a menu item
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
event.preventDefault();
const menu = this.getMenu();
menu.setCurrentItem(firstMenuItem);
firstMenuItem.focus();
return;
}
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
event.preventDefault();
menu.setCurrentItem(lastMenuItem);
lastMenuItem.focus();
return;
}
// Other keys bring focus to the menu and initiate type-to-select behavior
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
if (!ignoredKeys.includes(event.key)) {
menu.typeToSelect(event.key);
return;
}
}
}
handleDocumentMouseDown(event: MouseEvent) {
const path = event.composedPath() as Array<EventTarget>;
//
// Close the context menu when clicking outside of it. We use a setTimeout here because mousedown fires before
// contextmenu and, if the menu is already open and the user-right clicks again, we want the menu to re-open in the
// new position instead of closing.
//
setTimeout(() => {
if (this.open && !path.includes(this.menu)) {
this.hide();
return;
}
});
}
handleMenuSelect() {
// Close the context menu when a menu item is selected
this.hide();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.disabled) {
return;
}
if (this.open) {
// Show
emit(this, 'sl-show');
document.addEventListener('keydown', this.handleDocumentKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this);
this.popover = createPopper(this.locater, this.positioner, {
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
this.menu.hidden = false;
const { keyframes, options } = getAnimation(this, 'contextMenu.show');
await animateTo(this.menu, keyframes, options);
emit(this, 'sl-after-show');
} else {
// Hide
emit(this, 'sl-hide');
document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this);
const { keyframes, options } = getAnimation(this, 'contextMenu.hide');
await animateTo(this.menu, keyframes, options);
this.menu.hidden = true;
this.locater.style.top = '0px';
this.locater.style.left = '0px';
this.popover.destroy();
emit(this, 'sl-after-hide');
}
}
/** Shows the context menu */
async show(offsetX?: number, offsetY?: number) {
if (this.open) {
return;
}
this.locater.style.top = `${offsetY || 0}px`;
this.locater.style.left = `${offsetX || 0}px`;
this.open = true;
return waitForEvent(this, 'sl-after-show');
}
/** Hides the dropdown panel */
async hide() {
if (!this.open) {
return;
}
this.open = false;
return waitForEvent(this, 'sl-after-hide');
}
render() {
return html`
<div class="context-menu">
<slot @contextmenu=${this.handleContextMenu}></slot>
<div class="context-menu__locater"></div>
<!-- Position the menu with a wrapper since the popover makes use of translate. This let's us add animations
on the menu without interfering with the position. -->
<div class="context-menu__positioner">
<div class="context-menu__menu" hidden @sl-select=${this.handleMenuSelect}>
<slot name="menu"></slot>
</div>
</div>
</div>
`;
}
}
setDefaultAnimation('contextMenu.show', {
keyframes: [
{ opacity: 0, transform: 'scale(0.9)' },
{ opacity: 1, transform: 'scale(1)' }
],
options: { duration: 50, easing: 'ease' }
});
setDefaultAnimation('contextMenu.hide', {
keyframes: [
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.9)' }
],
options: { duration: 150, easing: 'ease' }
});
declare global {
interface HTMLElementTagNameMap {
'sl-context-menu': SlContextMenu;
}
}

View File

@@ -10,13 +10,17 @@ export default css`
--spacing: var(--sl-spacing-medium);
}
:host(:not([vertical])) {
:host(:not([vertical])) .menu-divider {
display: block;
border-top: solid var(--width) var(--color);
margin: var(--spacing) 0;
}
:host([vertical]) {
height: 100%;
}
:host([vertical]) .menu-divider {
display: inline-block;
height: 100%;
border-left: solid var(--width) var(--color);

View File

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

View File

@@ -100,28 +100,6 @@ export default class SlDropdown extends LitElement {
if (!this.containingElement) {
this.containingElement = this;
}
// Create the popover after render
this.updateComplete.then(() => {
this.popover = createPopper(this.trigger, this.positioner, {
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
});
}
firstUpdated() {
@@ -131,7 +109,6 @@ export default class SlDropdown extends LitElement {
disconnectedCallback() {
super.disconnectedCallback();
this.hide();
this.popover.destroy();
}
focusOnTrigger() {
@@ -207,40 +184,13 @@ export default class SlDropdown extends LitElement {
}
}
@watch('distance')
@watch('hoist')
@watch('placement')
@watch('skidding')
handlePopoverOptionsChange() {
if (this.popover) {
this.popover.setOptions({
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
}
}
handleTriggerClick() {
this.open ? this.hide() : this.show();
}
handleTriggerKeyDown(event: KeyboardEvent) {
const menu = this.getMenu();
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
const menuItems = menu ? menu.getAllItems() : [];
const firstMenuItem = menuItems[0];
const lastMenuItem = menuItems[menuItems.length - 1];
@@ -262,7 +212,7 @@ export default class SlDropdown extends LitElement {
// When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a
// selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for
// faster navigation.
if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
event.preventDefault();
// Show the menu if it's not already open
@@ -271,14 +221,14 @@ export default class SlDropdown extends LitElement {
}
// Focus on a menu item
if (event.key === 'ArrowDown' && firstMenuItem) {
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
const menu = this.getMenu();
menu.setCurrentItem(firstMenuItem);
firstMenuItem.focus();
return;
}
if (event.key === 'ArrowUp' && lastMenuItem) {
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
menu.setCurrentItem(lastMenuItem);
lastMenuItem.focus();
return;
@@ -327,7 +277,7 @@ export default class SlDropdown extends LitElement {
}
}
/** Shows the dropdown panel. */
/** Shows the dropdown panel */
async show() {
if (this.open) {
return;
@@ -352,11 +302,9 @@ export default class SlDropdown extends LitElement {
* is activated.
*/
reposition() {
if (!this.open) {
return;
if (this.popover) {
this.popover.update();
}
this.popover.update();
}
@watch('open', { waitUntilFirstUpdate: true })
@@ -376,7 +324,26 @@ export default class SlDropdown extends LitElement {
document.addEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this);
this.popover.update();
this.popover = createPopper(this.trigger, this.positioner, {
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
this.panel.hidden = false;
const { keyframes, options } = getAnimation(this, 'dropdown.show');
await animateTo(this.panel, keyframes, options);

View File

@@ -41,6 +41,11 @@ const icons = {
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/>
</svg>
`,
eyedropper: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eyedropper" viewBox="0 0 16 16">
<path d="M13.354.646a1.207 1.207 0 0 0-1.708 0L8.5 3.793l-.646-.647a.5.5 0 1 0-.708.708L8.293 5l-7.147 7.146A.5.5 0 0 0 1 12.5v1.793l-.854.853a.5.5 0 1 0 .708.707L1.707 15H3.5a.5.5 0 0 0 .354-.146L11 7.707l1.146 1.147a.5.5 0 0 0 .708-.708l-.647-.646 3.147-3.146a1.207 1.207 0 0 0 0-1.708l-2-2zM2 12.707l7-7L10.293 7l-7 7H2v-1.293z"></path>
</svg>
`,
'grip-vertical': `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-grip-vertical" viewBox="0 0 16 16">
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
@@ -51,6 +56,16 @@ const icons = {
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
`,
'play-fill': `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"></path>
</svg>
`,
'pause-fill': `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"></path>
</svg>
`,
'star-fill': `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>

View File

@@ -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() {

View File

@@ -0,0 +1,89 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlProgressBar from './progress-bar';
describe('<sl-progress-bar>', () => {
let el: SlProgressBar;
describe('when provided just a value parameter', async () => {
before(async () => {
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
});
describe('when provided a title, and value parameter', async () => {
let base: HTMLDivElement;
let indicator: HTMLDivElement;
before(async () => {
el = await fixture<SlProgressBar>(
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
);
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement;
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('uses the value parameter on the base, as aria-valuenow', async () => {
expect(base).attribute('aria-valuenow', '25');
});
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', async () => {
expect(indicator).attribute('style', 'width:25%;');
});
});
describe('when provided an indeterminate parameter', async () => {
let base: HTMLDivElement;
before(async () => {
el = await fixture<SlProgressBar>(
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
);
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('should append a progress-bar--indeterminate class to the "base" part.', async () => {
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
});
});
describe('when provided a ariaLabel, and value parameter', async () => {
before(async () => {
el = await fixture<SlProgressBar>(
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
});
describe('when provided a ariaLabelledBy, and value parameter', async () => {
before(async () => {
el = await fixture<SlProgressBar>(
html`
<label id="labelledby">Progress Ring Label</label>
<sl-progress-bar ariaLabelledBy="labelledby" value="25"></sl-progress-bar>
`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
});
});

View File

@@ -1,5 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import styles from './progress-bar.styles';
@@ -29,6 +30,9 @@ export default class SlProgressBar extends LitElement {
/** When true, percentage is ignored, the label is hidden, and the progress bar is drawn in an indeterminate state. */
@property({ type: Boolean, reflect: true }) indeterminate = false;
/** The progress bar's aria label. */
@property() label = 'Progress'; // TODO - i18n
render() {
return html`
<div
@@ -38,9 +42,11 @@ export default class SlProgressBar extends LitElement {
'progress-bar--indeterminate': this.indeterminate
})}
role="progressbar"
title=${ifDefined(this.title)}
aria-label=${ifDefined(this.label)}
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="${this.indeterminate ? '' : this.value}"
aria-valuenow=${this.indeterminate ? 0 : this.value}
>
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.value + '%' })}>
${!this.indeterminate

View File

@@ -0,0 +1,68 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlProgressRing from './progress-ring';
describe('<sl-progress-ring>', () => {
let el: SlProgressRing;
describe('when provided just a value parameter', async () => {
before(async () => {
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
});
describe('when provided a title, and value parameter', async () => {
let base: HTMLDivElement;
before(async () => {
el = await fixture<SlProgressRing>(
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
);
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('uses the value parameter on the base, as aria-valuenow', async () => {
expect(base).attribute('aria-valuenow', '25');
});
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', async () => {
expect(base).attribute('style', '--percentage: 0.25');
});
});
describe('when provided a ariaLabel, and value parameter', async () => {
before(async () => {
el = await fixture<SlProgressRing>(
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
});
describe('when provided a ariaLabelledBy, and value parameter', async () => {
before(async () => {
el = await fixture<SlProgressRing>(
html`
<label id="labelledby">Progress Ring Label</label>
<sl-progress-ring ariaLabelledBy="labelledby" value="25"></sl-progress-ring>
`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
});
});

View File

@@ -1,5 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import styles from './progress-ring.styles';
/**
@@ -27,6 +28,9 @@ export default class SlProgressRing extends LitElement {
/** The current progress, 0 to 100. */
@property({ type: Number, reflect: true }) value = 0;
/** The progress ring's aria label. */
@property() label = 'Progress'; // TODO - i18n
updated(changedProps: Map<string, any>) {
super.updated(changedProps);
@@ -50,6 +54,7 @@ export default class SlProgressRing extends LitElement {
part="base"
class="progress-ring"
role="progressbar"
aria-label=${ifDefined(this.label)}
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="${this.value}"

View File

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

View File

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

View File

@@ -53,18 +53,18 @@ export default css`
cursor: pointer;
}
.range__control:not(:disabled)::-webkit-slider-thumb:hover {
.range__control:enabled::-webkit-slider-thumb:hover {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
}
.range__control:not(:disabled)${focusVisibleSelector}::-webkit-slider-thumb {
.range__control:enabled${focusVisibleSelector}::-webkit-slider-thumb {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
box-shadow: var(--sl-focus-ring);
}
.range__control:not(:disabled)::-webkit-slider-thumb:active {
.range__control:enabled::-webkit-slider-thumb:active {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
cursor: grabbing;
@@ -101,18 +101,18 @@ export default css`
cursor: pointer;
}
.range__control:not(:disabled)::-moz-range-thumb:hover {
.range__control:enabled::-moz-range-thumb:hover {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
}
.range__control:not(:disabled)${focusVisibleSelector}::-moz-range-thumb {
.range__control:enabled${focusVisibleSelector}::-moz-range-thumb {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
box-shadow: var(--sl-focus-ring);
}
.range__control:not(:disabled)::-moz-range-thumb:active {
.range__control:enabled::-moz-range-thumb:active {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
cursor: grabbing;

View File

@@ -29,8 +29,8 @@ let id = 0;
* @cssproperty --thumb-size - The size of the thumb.
* @cssproperty --tooltip-offset - The vertical distance the tooltip is offset from the track.
* @cssproperty --track-color-active - The color of the portion of the track that represents the current value.
* @cssproperty --track-color-inactive: The of the portion of the track that represents the remaining value.
* @cssproperty --track-height: The height of the track.
* @cssproperty --track-color-inactive - The of the portion of the track that represents the remaining value.
* @cssproperty --track-height - The height of the track.
*/
@customElement('sl-range')
export default class SlRange extends LitElement {
@@ -129,7 +129,7 @@ export default class SlRange extends LitElement {
this.value = Number(this.input.value);
emit(this, 'sl-change');
requestAnimationFrame(() => this.syncRange());
this.syncRange();
}
handleBlur() {
@@ -138,6 +138,17 @@ export default class SlRange extends LitElement {
emit(this, 'sl-blur');
}
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
this.value = Number(this.value);
if (this.input) {
this.invalid = !this.input.checkValidity();
}
this.syncRange();
}
@watch('disabled')
handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes
@@ -185,13 +196,15 @@ export default class SlRange extends LitElement {
}
syncTooltip(percent: number) {
const inputWidth = this.input.offsetWidth;
const tooltipWidth = this.output.offsetWidth;
const thumbSize = getComputedStyle(this.input).getPropertyValue('--thumb-size');
const x = `calc(${inputWidth * percent}px - calc(calc(${percent} * ${thumbSize}) - calc(${thumbSize} / 2)))`;
if (this.output) {
const inputWidth = this.input.offsetWidth;
const tooltipWidth = this.output.offsetWidth;
const thumbSize = getComputedStyle(this.input).getPropertyValue('--thumb-size');
const x = `calc(${inputWidth * percent}px - calc(calc(${percent} * ${thumbSize}) - calc(${thumbSize} / 2)))`;
this.output.style.transform = `translateX(${x})`;
this.output.style.marginLeft = `-${tooltipWidth / 2}px`;
this.output.style.transform = `translateX(${x})`;
this.output.style.marginLeft = `-${tooltipWidth / 2}px`;
}
}
render() {
@@ -247,7 +260,7 @@ export default class SlRange extends LitElement {
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
${this.tooltip !== 'none'
${this.tooltip !== 'none' && !this.disabled
? html` <output part="tooltip" class="range__tooltip"> ${this.tooltipFormatter(this.value)} </output> `
: ''}
</div>

View File

@@ -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() {

View File

@@ -24,8 +24,8 @@ export default class SlResponsiveMedia extends LitElement {
render() {
const split = this.aspectRatio.split(':');
const x = parseInt(split[0]);
const y = parseInt(split[1]);
const x = parseFloat(split[0]);
const y = parseFloat(split[1]);
const paddingBottom = x && y ? `${(y / x) * 100}%` : '0';
return html`

View File

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

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

View File

@@ -354,7 +354,7 @@ export default class SlTabGroup extends LitElement {
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
>
<div class="tab-group__nav-container">
<div class="tab-group__nav-container" part="nav">
${this.hasScrollControls
? html`
<sl-icon-button
@@ -367,7 +367,7 @@ export default class SlTabGroup extends LitElement {
`
: ''}
<div part="nav" class="tab-group__nav">
<div class="tab-group__nav">
<div part="tabs" class="tab-group__tabs" role="tablist">
<div part="active-tab-indicator" class="tab-group__indicator"></div>
<slot name="nav" @slotchange=${this.syncTabsAndPanels}></slot>

View File

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

View File

@@ -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>
`
: ''}

View File

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

View File

@@ -1,5 +1,6 @@
// Components
export { default as SlAlert } from './components/alert/alert';
export { default as SlAnimatedImage } from './components/animated-image/animated-image';
export { default as SlAnimation } from './components/animation/animation';
export { default as SlAvatar } from './components/avatar/avatar';
export { default as SlBadge } from './components/badge/badge';
@@ -10,6 +11,7 @@ export { default as SlButtonGroup } from './components/button-group/button-group
export { default as SlCard } from './components/card/card';
export { default as SlCheckbox } from './components/checkbox/checkbox';
export { default as SlColorPicker } from './components/color-picker/color-picker';
export { default as SlContextMenu } from './components/context-menu/context-menu';
export { default as SlDetails } from './components/details/details';
export { default as SlDialog } from './components/dialog/dialog';
export { default as SlDivider } from './components/divider/divider';