mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
118 Commits
v2.0.0-bet
...
v2.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
227ecc0193 | ||
|
|
ee260e671f | ||
|
|
1a954c5b25 | ||
|
|
a14dd95c21 | ||
|
|
19f7918435 | ||
|
|
1e4de5f821 | ||
|
|
45f4b33eb1 | ||
|
|
16e7287c24 | ||
|
|
3b2b5eed5a | ||
|
|
0521740824 | ||
|
|
1d2033953b | ||
|
|
e20edefc61 | ||
|
|
a9287d9d80 | ||
|
|
ba029db24e | ||
|
|
529c187bc4 | ||
|
|
714914ffe5 | ||
|
|
cd23b9ebfe | ||
|
|
35ce68f4f6 | ||
|
|
f7bcd89b97 | ||
|
|
0b44fba68c | ||
|
|
501869c7aa | ||
|
|
15cb1cb746 | ||
|
|
7454cc12a1 | ||
|
|
52d52810b9 | ||
|
|
936039f7a7 | ||
|
|
ba0e8f7973 | ||
|
|
8449a99418 | ||
|
|
28c9dbab1f | ||
|
|
81753cd44b | ||
|
|
9970bc84ff | ||
|
|
499bc4c4cd | ||
|
|
b0921b5be0 | ||
|
|
afc4dfaf50 | ||
|
|
9dda3a9323 | ||
|
|
115e80dce0 | ||
|
|
9f405686ec | ||
|
|
e7d7469c4e | ||
|
|
0dbb72efe9 | ||
|
|
9b21d5a619 | ||
|
|
8ffcdebffc | ||
|
|
e089184a14 | ||
|
|
a2a059962c | ||
|
|
3eb42321d5 | ||
|
|
474484b059 | ||
|
|
998e255636 | ||
|
|
3938644442 | ||
|
|
b13e637593 | ||
|
|
95e3f5e0e8 | ||
|
|
d5ee79fe1e | ||
|
|
51f003d5fd | ||
|
|
dd89657f1e | ||
|
|
1e280608d3 | ||
|
|
c6e5bedd3c | ||
|
|
0ff5b46799 | ||
|
|
e34090a87b | ||
|
|
f690b24c68 | ||
|
|
10f045fe6e | ||
|
|
8d8b77ca07 | ||
|
|
234d2380ef | ||
|
|
4de659d5bb | ||
|
|
2aabe4e11c | ||
|
|
171e55ce6d | ||
|
|
297e6c8872 | ||
|
|
2b39d613b7 | ||
|
|
2432fd1d85 | ||
|
|
3cc3d4997b | ||
|
|
9c0189f8be | ||
|
|
fc5a21f57d | ||
|
|
ee9ce8a87b | ||
|
|
d720121044 | ||
|
|
3fce846a8d | ||
|
|
8776c3f4a8 | ||
|
|
189ad7889d | ||
|
|
79a15e1470 | ||
|
|
dfd0d0ed30 | ||
|
|
d7bf0bd653 | ||
|
|
6044190019 | ||
|
|
89b8d0ef67 | ||
|
|
489d713fa2 | ||
|
|
8d984d8dac | ||
|
|
cadbae85a5 | ||
|
|
01bb476023 | ||
|
|
d5c37f7b29 | ||
|
|
99181cf5c6 | ||
|
|
7836b8229a | ||
|
|
3cff627b22 | ||
|
|
4263899bc0 | ||
|
|
0f4bb2b24b | ||
|
|
7bd7b421b8 | ||
|
|
49eb9bbcf8 | ||
|
|
a87596b3a1 | ||
|
|
1ca890b1e9 | ||
|
|
52aba14ae9 | ||
|
|
916ee07265 | ||
|
|
1e67c7411c | ||
|
|
62ef8e17c7 | ||
|
|
327ef6b06c | ||
|
|
5dff7b2855 | ||
|
|
22b359a612 | ||
|
|
7bf3a8d8f7 | ||
|
|
4bd73ac374 | ||
|
|
cae50866f9 | ||
|
|
ebd1b95ba0 | ||
|
|
703cb4dc7a | ||
|
|
e274904d28 | ||
|
|
b3bcfc9934 | ||
|
|
a3beaafbcc | ||
|
|
5c619b87b6 | ||
|
|
86fc6b85d6 | ||
|
|
4c1e077833 | ||
|
|
c8e94ea098 | ||
|
|
cf38478cd5 | ||
|
|
e3ca914eac | ||
|
|
18a933bf6b | ||
|
|
7554f47258 | ||
|
|
e579330177 | ||
|
|
360cfa43d8 | ||
|
|
f66535f7d4 |
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html",
|
||||
"bashmish.es6-string-css"
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
124
docs/_sidebar.md
124
docs/_sidebar.md
@@ -1,74 +1,74 @@
|
||||
- Getting Started
|
||||
- [Overview](/)
|
||||
- [Installation](/getting-started/installation.md)
|
||||
- [Usage](/getting-started/usage.md)
|
||||
- [Customizing](/getting-started/customizing.md)
|
||||
- [Themes](/getting-started/themes.md)
|
||||
- [Installation](/getting-started/installation)
|
||||
- [Usage](/getting-started/usage)
|
||||
- [Customizing](/getting-started/customizing)
|
||||
- [Themes](/getting-started/themes)
|
||||
|
||||
- Resources
|
||||
- [Community](/resources/community.md)
|
||||
- [Contributing](/resources/contributing.md)
|
||||
- [Changelog](/resources/changelog.md)
|
||||
- [Community](/resources/community)
|
||||
- [Contributing](/resources/contributing)
|
||||
- [Changelog](/resources/changelog)
|
||||
|
||||
- Components
|
||||
- [Alert](/components/alert.md)
|
||||
- [Avatar](/components/avatar.md)
|
||||
- [Badge](/components/badge.md)
|
||||
- [Button](/components/button.md)
|
||||
- [Button Group](/components/button-group.md)
|
||||
- [Card](/components/card.md)
|
||||
- [Checkbox](/components/checkbox.md)
|
||||
- [Color Picker](/components/color-picker.md)
|
||||
- [Details](/components/details.md)
|
||||
- [Dialog](/components/dialog.md)
|
||||
- [Drawer](/components/drawer.md)
|
||||
- [Dropdown](/components/dropdown.md)
|
||||
- [Form](/components/form.md)
|
||||
- [Icon](/components/icon.md)
|
||||
- [Icon Button](/components/icon-button.md)
|
||||
- [Image Comparer](/components/image-comparer.md)
|
||||
- [Input](/components/input.md)
|
||||
- [Menu](/components/menu.md)
|
||||
- [Menu Divider](/components/menu-divider.md)
|
||||
- [Menu Item](/components/menu-item.md)
|
||||
- [Menu Label](/components/menu-label.md)
|
||||
- [Progress Bar](/components/progress-bar.md)
|
||||
- [Progress Ring](/components/progress-ring.md)
|
||||
- [Radio](/components/radio.md)
|
||||
- [Radio Group](/components/radio-group.md)
|
||||
- [Range](/components/range.md)
|
||||
- [Rating](/components/rating.md)
|
||||
- [Responsive Embed](/components/responsive-embed.md)
|
||||
- [Select](/components/select.md)
|
||||
- [Skeleton](/components/skeleton.md)
|
||||
- [Spinner](/components/spinner.md)
|
||||
- [Switch](/components/switch.md)
|
||||
- [Tab Group](/components/tab-group.md)
|
||||
- [Tab](/components/tab.md)
|
||||
- [Tab Panel](/components/tab-panel.md)
|
||||
- [Tag](/components/tag.md)
|
||||
- [Textarea](/components/textarea.md)
|
||||
- [Tooltip](/components/tooltip.md)
|
||||
- [Alert](/components/alert)
|
||||
- [Avatar](/components/avatar)
|
||||
- [Badge](/components/badge)
|
||||
- [Button](/components/button)
|
||||
- [Button Group](/components/button-group)
|
||||
- [Card](/components/card)
|
||||
- [Checkbox](/components/checkbox)
|
||||
- [Color Picker](/components/color-picker)
|
||||
- [Details](/components/details)
|
||||
- [Dialog](/components/dialog)
|
||||
- [Drawer](/components/drawer)
|
||||
- [Dropdown](/components/dropdown)
|
||||
- [Form](/components/form)
|
||||
- [Icon](/components/icon)
|
||||
- [Icon Button](/components/icon-button)
|
||||
- [Image Comparer](/components/image-comparer)
|
||||
- [Input](/components/input)
|
||||
- [Menu](/components/menu)
|
||||
- [Menu Divider](/components/menu-divider)
|
||||
- [Menu Item](/components/menu-item)
|
||||
- [Menu Label](/components/menu-label)
|
||||
- [Progress Bar](/components/progress-bar)
|
||||
- [Progress Ring](/components/progress-ring)
|
||||
- [Radio](/components/radio)
|
||||
- [Radio Group](/components/radio-group)
|
||||
- [Range](/components/range)
|
||||
- [Rating](/components/rating)
|
||||
- [Select](/components/select)
|
||||
- [Skeleton](/components/skeleton)
|
||||
- [Spinner](/components/spinner)
|
||||
- [Switch](/components/switch)
|
||||
- [Tab Group](/components/tab-group)
|
||||
- [Tab](/components/tab)
|
||||
- [Tab Panel](/components/tab-panel)
|
||||
- [Tag](/components/tag)
|
||||
- [Textarea](/components/textarea)
|
||||
- [Tooltip](/components/tooltip)
|
||||
|
||||
- Utilities
|
||||
- [Animation](/components/animation.md)
|
||||
- [Format Bytes](/components/format-bytes.md)
|
||||
- [Format Date](/components/format-date.md)
|
||||
- [Format Number](/components/format-number.md)
|
||||
- [Include](/components/include.md)
|
||||
- [QR Code](/components/qr-code.md)
|
||||
- [Relative Time](/components/relative-time.md)
|
||||
- [Resize Observer](/components/resize-observer.md)
|
||||
- [Animation](/components/animation)
|
||||
- [Format Bytes](/components/format-bytes)
|
||||
- [Format Date](/components/format-date)
|
||||
- [Format Number](/components/format-number)
|
||||
- [Include](/components/include)
|
||||
- [QR Code](/components/qr-code)
|
||||
- [Relative Time](/components/relative-time)
|
||||
- [Resize Observer](/components/resize-observer)
|
||||
- [Responsive Media](/components/responsive-media)
|
||||
|
||||
- Design Tokens
|
||||
- [Typography](/tokens/typography.md)
|
||||
- [Color](/tokens/color.md)
|
||||
- [Spacing](/tokens/spacing.md)
|
||||
- [Elevation](/tokens/elevation.md)
|
||||
- [Border Radius](/tokens/border-radius.md)
|
||||
- [Transition](/tokens/transition.md)
|
||||
- [Z-index](/tokens/z-index.md)
|
||||
- [Typography](/tokens/typography)
|
||||
- [Color](/tokens/color)
|
||||
- [Spacing](/tokens/spacing)
|
||||
- [Elevation](/tokens/elevation)
|
||||
- [Border Radius](/tokens/border-radius)
|
||||
- [Transition](/tokens/transition)
|
||||
- [Z-index](/tokens/z-index)
|
||||
|
||||
- Tutorials
|
||||
- [Integrating with NextJS](/tutorials/integrating-with-nextjs.md)
|
||||
- [Integrating with Rails](/tutorials/integrating-with-rails.md)
|
||||
- [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: 677 KiB After Width: | Height: | Size: 709 KiB |
@@ -51,7 +51,7 @@
|
||||
|
||||
${pre.outerHTML}
|
||||
|
||||
<div class="code-block__toggle" aria-expanded="false" aria-controls="${preId}">
|
||||
<button type="button" class="code-block__toggle" aria-expanded="false" aria-controls="${preId}">
|
||||
Source
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@@ -63,7 +63,7 @@
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
(() => {
|
||||
let metadataStore;
|
||||
|
||||
function getAttrName(propName) {
|
||||
return propName.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`).replace(/^-/, '');
|
||||
}
|
||||
|
||||
function createPropsTable(props) {
|
||||
const table = document.createElement('table');
|
||||
table.innerHTML = `
|
||||
@@ -19,18 +15,17 @@
|
||||
<tbody>
|
||||
${props
|
||||
.map(prop => {
|
||||
const attr = getAttrName(prop.name);
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<code>${escapeHtml(prop.name)}</code>
|
||||
${
|
||||
prop.name !== attr
|
||||
prop.attribute && prop.name !== prop.attribute
|
||||
? `
|
||||
<br>
|
||||
<small>
|
||||
<sl-tooltip content="Use this attribute in your HTML">
|
||||
<code class="attribute-tooltip">${escapeHtml(attr)}</code>
|
||||
<sl-tooltip content="This is the corresponding HTML attribute">
|
||||
<code class="attribute-tooltip">${escapeHtml(prop.attribute)}</code>
|
||||
</sl-tooltip>
|
||||
</small>`
|
||||
: ''
|
||||
@@ -99,7 +94,9 @@
|
||||
method.params.length
|
||||
? `
|
||||
<code style="white-space: normal;">${escapeHtml(
|
||||
method.params.map(param => `${param.name}: ${param.type}`).join(', ')
|
||||
method.params
|
||||
.map(param => `${param.name}${param.isOptional ? '?' : ''}: ${param.type}`)
|
||||
.join(', ')
|
||||
)}</code>
|
||||
`
|
||||
: ''
|
||||
@@ -193,6 +190,32 @@
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createAnimationsTable(animations) {
|
||||
const table = document.createElement('table');
|
||||
table.innerHTML = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${animations
|
||||
.map(
|
||||
animation => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(animation.name)}</code></td>
|
||||
<td>${escapeHtml(animation.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
}
|
||||
|
||||
function createDependenciesList(targetComponent, allComponents) {
|
||||
const ul = document.createElement('ul');
|
||||
const dependencies = [];
|
||||
@@ -295,7 +318,7 @@
|
||||
|
||||
if (!component) {
|
||||
console.error('Component not found in metadata: ' + tag);
|
||||
next(content);
|
||||
return next(content);
|
||||
}
|
||||
|
||||
let badgeType = 'info';
|
||||
@@ -332,7 +355,7 @@
|
||||
|
||||
if (!component) {
|
||||
console.error('Component not found in metadata: ' + tag);
|
||||
next(content);
|
||||
return next(content);
|
||||
}
|
||||
|
||||
if (component.props.length) {
|
||||
@@ -377,6 +400,15 @@
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.animations.length) {
|
||||
result += `
|
||||
## Animations
|
||||
${createAnimationsTable(component.animations)}
|
||||
|
||||
Learn how to [customize animations](/getting-started/customizing#animations).
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.dependencies.length) {
|
||||
result += `
|
||||
## Dependencies
|
||||
|
||||
@@ -47,7 +47,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { getAnimationNames, getEasingNames } from '/dist/shoelace.js';
|
||||
import { getAnimationNames, getEasingNames } from '/dist/utilities/animation.js';
|
||||
|
||||
const container = document.querySelector('.animation-sandbox');
|
||||
const animation = container.querySelector('sl-animation');
|
||||
|
||||
@@ -8,7 +8,7 @@ Checkboxes allow the user to toggle an option on or off.
|
||||
<sl-checkbox>Checkbox</sl-checkbox>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -24,20 +24,20 @@ Drawers slide in from a container to expose additional options and information.
|
||||
|
||||
## Examples
|
||||
|
||||
### Slide in From Left
|
||||
### Slide in From Start
|
||||
|
||||
To make the drawer slide in from the left, set the `placement` attribute to `left`.
|
||||
By default, drawers slide in from the end. To make the drawer slide in from the start, set the `placement` attribute to `start`.
|
||||
|
||||
```html preview
|
||||
<sl-drawer label="Drawer" placement="left" class="drawer-placement-left">
|
||||
This drawer slides in from the left.
|
||||
<sl-drawer label="Drawer" placement="start" class="drawer-placement-start">
|
||||
This drawer slides in from the start.
|
||||
<sl-button slot="footer" type="primary">Close</sl-button>
|
||||
</sl-drawer>
|
||||
|
||||
<sl-button>Open Drawer</sl-button>
|
||||
|
||||
<script>
|
||||
const drawer = document.querySelector('.drawer-placement-left');
|
||||
const drawer = document.querySelector('.drawer-placement-start');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
const closeButton = drawer.querySelector('sl-button[type="primary"]');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Dropdowns expose additional content that "drops down" in a panel.
|
||||
|
||||
Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it.
|
||||
|
||||
Dropdowns are designed to work well with [menus](/components/menu.md) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker.md) and [select](/components/select.md)). The API gives you complete control over showing, hiding, and positioning the panel.
|
||||
Dropdowns are designed to work well with [menus](/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker) and [select](/components/select)). The API gives you complete control over showing, hiding, and positioning the panel.
|
||||
|
||||
```html preview
|
||||
<sl-dropdown>
|
||||
@@ -123,7 +123,7 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu.md), 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.
|
||||
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">
|
||||
|
||||
@@ -75,8 +75,6 @@ When a form control is invalid, the containing form will not be submitted. Inste
|
||||
|
||||
All form controls support validation, but not all validation props are available for every component. Refer to a component's documentation to see which validation props it supports.
|
||||
|
||||
Note that validity is not checked until the user interacts with the control or its containing form is submitted. This prevents required controls from being rendered as invalid right away, which can result in a poor user experience. If you need this behavior, set the `invalid` attribute initially.
|
||||
|
||||
!> Client-side validation can be used to improve the UX of forms, but it is not a replacement for server-side validation. **You should always validate and sanitize user input on the server!**
|
||||
|
||||
### Required Fields
|
||||
|
||||
@@ -76,7 +76,7 @@ Here's an example that registers an icon library located in the `/assets/icons`
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/shoelace/dist/utilities/icon-library.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('my-icons', {
|
||||
resolver: name => `/assets/icons/${name}.svg`,
|
||||
@@ -104,7 +104,7 @@ Icons in this library are licensed under the [Creative Commons 4.0 License](http
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('boxicons', {
|
||||
resolver: name => {
|
||||
@@ -158,7 +158,7 @@ Icons in this library are licensed under the [MIT License](https://github.com/fe
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('feather', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/icons/${name}.svg`
|
||||
@@ -174,7 +174,7 @@ Icons in this library are licensed under the [Font Awesome Free License](https:/
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('fa', {
|
||||
resolver: name => {
|
||||
@@ -220,7 +220,7 @@ Icons in this library are licensed under the [MIT License](https://github.com/ta
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('heroicons', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/heroicons@0.4.2/outline/${name}.svg`
|
||||
@@ -237,6 +237,31 @@ Icons in this library are licensed under the [MIT License](https://github.com/ta
|
||||
</div>
|
||||
```
|
||||
|
||||
### Iconoir
|
||||
|
||||
This will register the [Iconoir](https://iconoir.com/) library using the jsDelivr CDN.
|
||||
|
||||
Icons in this library are licensed under the [MIT License](https://github.com/lucaburgio/iconoir/blob/master/LICENSE).
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('iconoir', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/gh/lucaburgio/iconoir@latest/icons/${name}.svg`
|
||||
});
|
||||
</script>
|
||||
|
||||
<div style="font-size: 24px;">
|
||||
<sl-icon library="iconoir" name="check-circled-outline"></sl-icon>
|
||||
<sl-icon library="iconoir" name="drawer"></sl-icon>
|
||||
<sl-icon library="iconoir" name="keyframes"></sl-icon>
|
||||
<sl-icon library="iconoir" name="headset-help"></sl-icon>
|
||||
<sl-icon library="iconoir" name="color-picker"></sl-icon>
|
||||
<sl-icon library="iconoir" name="wifi"></sl-icon>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Ionicons
|
||||
|
||||
This will register the [Ionicons](https://ionicons.com/) library using the jsDelivr CDN. This library has three variations: outline (default), filled (`*-filled`), and sharp (`*-sharp`). A mutator function is required to polyfill a handful of styles we're not including.
|
||||
@@ -245,11 +270,11 @@ Icons in this library are licensed under the [MIT License](https://github.com/io
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('ionicons', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/ionicons@5.1.2/dist/ionicons/svg/${name}.svg`,
|
||||
mutator: svg => {
|
||||
mutator: svg => {
|
||||
svg.setAttribute('fill', 'currentColor');
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
[...svg.querySelectorAll('.ionicon-fill-none')].map(el => el.setAttribute('fill', 'none'));
|
||||
@@ -290,7 +315,7 @@ Icons in this library are licensed under the [MIT License](https://github.com/mi
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('jam', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/jam-icons@2.0.0/svg/${name}.svg`,
|
||||
@@ -323,7 +348,7 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('material', {
|
||||
resolver: name => {
|
||||
@@ -366,7 +391,7 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('remixicon', {
|
||||
resolver: name => {
|
||||
@@ -403,7 +428,7 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
|
||||
```html preview
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/shoelace.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('unicons', {
|
||||
resolver: name => {
|
||||
@@ -439,7 +464,7 @@ This example will load the same set of icons from the jsDelivr CDN instead of yo
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/shoelace/dist/utilities/icon-library.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('default', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/bootstrap-icons@1.0.0/icons/${name}.svg`
|
||||
@@ -455,7 +480,7 @@ If you want to change the icons Shoelace uses internally, you can register an ic
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/shoelace/dist/utilities/icon-library.js';
|
||||
import { registerIconLibrary } from '/dist/utilities/icon-library.js';
|
||||
|
||||
registerIconLibrary('system', {
|
||||
resolver: name => `/path/to/custom/icons/${name}.svg`
|
||||
@@ -475,6 +500,7 @@ If you want to change the icons Shoelace uses internally, you can register an ic
|
||||
const loader = container.querySelector('.icon-loader');
|
||||
const list = container.querySelector('.icon-list');
|
||||
const queue = [];
|
||||
let inputTimeout;
|
||||
|
||||
// Generate icons
|
||||
icons.map(i => {
|
||||
@@ -505,15 +531,18 @@ If you want to change the icons Shoelace uses internally, you can register an ic
|
||||
|
||||
// Filter as the user types
|
||||
input.addEventListener('sl-input', () => {
|
||||
[...list.querySelectorAll('.icon-list-item')].map(item => {
|
||||
const filter = input.value.toLowerCase();
|
||||
if (filter === '') {
|
||||
item.hidden = false;
|
||||
} else {
|
||||
const terms = item.getAttribute('data-terms').toLowerCase();
|
||||
item.hidden = terms.indexOf(filter) < 0;
|
||||
}
|
||||
});
|
||||
clearTimeout(inputTimeout);
|
||||
inputTimeout = setTimeout(() => {
|
||||
[...list.querySelectorAll('.icon-list-item')].map(item => {
|
||||
const filter = input.value.toLowerCase();
|
||||
if (filter === '') {
|
||||
item.hidden = false;
|
||||
} else {
|
||||
const terms = item.getAttribute('data-terms').toLowerCase();
|
||||
item.hidden = terms.indexOf(filter) < 0;
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Sort by type and remember preference
|
||||
@@ -534,6 +563,10 @@ If you want to change the icons Shoelace uses internally, you can register an ic
|
||||
padding: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
.icon-search [hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon-search-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Inputs collect data from the user.
|
||||
<sl-input></sl-input>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Menus provide a list of options for the user to choose from.
|
||||
|
||||
You can use [menu items](/components/menu-item.md), [menu dividers](/components/menu-divider.md), and [menu labels](/components/menu-label.md) to compose a menu.
|
||||
You can use [menu items](/components/menu-item), [menu dividers](/components/menu-divider), and [menu labels](/components/menu-label) to compose a menu.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px; border: solid 1px var(--sl-panel-border-color); border-radius: var(--sl-border-radius-medium);">
|
||||
|
||||
@@ -14,7 +14,7 @@ Radios are designed to be used with [radio groups](/components/radio-group). As
|
||||
</sl-radio-group>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Ranges allow the user to select a single value within a given range using a slid
|
||||
<sl-range min="0" max="100" step="1"></sl-range>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Responsive Embed
|
||||
|
||||
[component-header:sl-responsive-embed]
|
||||
|
||||
Displays embedded media in a responsive manner based on its aspect ratio.
|
||||
|
||||
You can embed any element of the `<iframe>`, `<embed>`, or `<object>` type. The default aspect ratio is `16:9`.
|
||||
|
||||
```html preview
|
||||
<sl-responsive-embed>
|
||||
<iframe src="https://player.vimeo.com/video/1053647?title=0&byline=0&portrait=0" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>
|
||||
</sl-responsive-embed>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Aspect Ratio
|
||||
|
||||
To set the aspect ratio, use the `aspect-ratio` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-responsive-embed aspect-ratio="4:3">
|
||||
<iframe src="https://www.youtube.com/embed/mM5_T-F1Yn4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</sl-responsive-embed>
|
||||
```
|
||||
|
||||
[component-metadata:sl-responsive-embed]
|
||||
37
docs/components/responsive-media.md
Normal file
37
docs/components/responsive-media.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Responsive Media
|
||||
|
||||
[component-header:sl-responsive-media]
|
||||
|
||||
Displays media in the desired aspect ratio.
|
||||
|
||||
You can slot in any [replaced element](https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element), including `<iframe>`, `<img>`, and `<video>`. As the element's width changes, its height will resize proportionally. Only one element should be slotted into the container. The default aspect ratio is `16:9`.
|
||||
|
||||
```html preview
|
||||
<sl-responsive-media>
|
||||
<img src="https://images.unsplash.com/photo-1541427468627-a89a96e5ca1d?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1800&q=80" alt="A train riding through autumn foliage with mountains in the distance.">
|
||||
</sl-responsive-media>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Responsive Images
|
||||
|
||||
The following image maintains a `4:3` aspect ratio as its container is resized.
|
||||
|
||||
```html preview
|
||||
<sl-responsive-media aspect-ratio="4:3">
|
||||
<img src="https://images.unsplash.com/photo-1473186578172-c141e6798cf4?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1800&q=80" alt="Two blue chairs on a sandy beach.">
|
||||
</sl-responsive-media>
|
||||
```
|
||||
|
||||
### Responsive Videos
|
||||
|
||||
The following video is embedded using an `iframe` and maintains a `16:9` aspect ratio as its container is resized.
|
||||
|
||||
```html preview
|
||||
<sl-responsive-media aspect-ratio="16:9">
|
||||
<iframe src="https://player.vimeo.com/video/1053647?title=0&byline=0&portrait=0" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>
|
||||
</sl-responsive-media>
|
||||
```
|
||||
|
||||
[component-metadata:sl-responsive-media]
|
||||
@@ -16,7 +16,7 @@ Selects allow you to choose one or more items from a dropdown menu.
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Switches allow the user to toggle an option on or off.
|
||||
<sl-switch>Switch</sl-switch>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Tab groups organize content into a container that shows one section at a time.
|
||||
|
||||
Tab groups make use of [tabs](/components/tab.md) and [tab panels](/components/tab-panel.md). Each tab must be slotted into the `nav` slot and its `panel` must refer to a tab panel of the same name.
|
||||
Tab groups make use of [tabs](/components/tab) and [tab panels](/components/tab-panel). Each tab must be slotted into the `nav` slot and its `panel` must refer to a tab panel of the same name.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group>
|
||||
@@ -40,12 +40,12 @@ Tabs can be shown on the bottom by setting `placement` to `bottom`.
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
### Tabs on Left
|
||||
### Tabs on Start
|
||||
|
||||
Tabs can be shown on the left by setting `placement` to `left`.
|
||||
Tabs can be shown on the starting side by setting `placement` to `start`.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group placement="left">
|
||||
<sl-tab-group placement="start">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
@@ -58,12 +58,12 @@ Tabs can be shown on the left by setting `placement` to `left`.
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
### Tabs on Right
|
||||
### Tabs on End
|
||||
|
||||
Tabs can be shown on the right by setting `placement` to `right`.
|
||||
Tabs can be shown on the ending side by setting `placement` to `end`.
|
||||
|
||||
```html preview
|
||||
<sl-tab-group placement="right">
|
||||
<sl-tab-group placement="end">
|
||||
<sl-tab slot="nav" panel="general">General</sl-tab>
|
||||
<sl-tab slot="nav" panel="custom">Custom</sl-tab>
|
||||
<sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
|
||||
|
||||
@@ -18,6 +18,6 @@ Tab panels are used inside tab groups to display content.
|
||||
</sl-tab-group>
|
||||
```
|
||||
|
||||
?> Additional demonstrations can be found in the [tab group examples](/components/tab-group.md).
|
||||
?> Additional demonstrations can be found in the [tab group examples](/components/tab-group).
|
||||
|
||||
[component-metadata:sl-tab-panel]
|
||||
|
||||
@@ -11,6 +11,6 @@ Tabs are used inside tab groups to represent tab panels.
|
||||
<sl-tab disabled>Disabled</sl-tab>
|
||||
```
|
||||
|
||||
?> Additional demonstrations can be found in the [tab group examples](/components/tab-group.md).
|
||||
?> Additional demonstrations can be found in the [tab group examples](/components/tab-group).
|
||||
|
||||
[component-metadata:sl-tab]
|
||||
|
||||
@@ -8,7 +8,7 @@ Textareas collect data from the user and allow multiple lines of text.
|
||||
<sl-textarea></sl-textarea>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form.md) instead.
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
|
||||
?> Please refer to the section on [form control validation](/components/form?id=form-control-validation) to learn how to do client-side validation.
|
||||
|
||||
|
||||
@@ -106,3 +106,51 @@ Alternatively, you can set them inline directly on the element.
|
||||
```
|
||||
|
||||
Not all components expose CSS custom properties. For those that do, they can be found in the component's API documentation.
|
||||
|
||||
## Animations
|
||||
|
||||
Some components use animation, such as when a dialog is shown or hidden. Animations are performed using the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) rather than CSS. However, you can still customize them through Shoelace's animation registry. If a component has customizable animations, they'll be listed in the "Animation" section of its documentation.
|
||||
|
||||
To customize a default animation, use the `setDefaultAnimation()` method. The function accepts an animation name (found in the component's docs) and an object with `keyframes` and `options` or `null` to disable the animation.
|
||||
|
||||
This example will make all dialogs use a custom show animation.
|
||||
|
||||
```js
|
||||
import { setDefaultAnimation } from '@shoelace-style/shoelace/dist/utilities/animation-registry.js';
|
||||
|
||||
// Change the default animation for all dialogs
|
||||
setDefaultAnimation('dialog.show', {
|
||||
keyframes: [
|
||||
{ transform: 'rotate(-10deg) scale(0.5)', opacity: '0' },
|
||||
{ transform: 'rotate(0deg) scale(1)', opacity: '1' }
|
||||
],
|
||||
options: {
|
||||
duration: 500
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you only want to target a single component, use the `setAnimation()` method instead. This function accepts an element, an animation name, and an object comprised of animation `keyframes` and `options`.
|
||||
|
||||
In this example, only the target dialog will use a custom show animation.
|
||||
|
||||
```js
|
||||
import { setAnimation } from '@shoelace-style/shoelace/dist/utilities/animation-registry.js';
|
||||
|
||||
// Change the animation for a single dialog
|
||||
const dialog = document.querySelector('#my-dialog');
|
||||
|
||||
setAnimation(dialog, 'dialog.show', {
|
||||
keyframes: [
|
||||
{ transform: 'rotate(-10deg) scale(0.5)', opacity: '0' },
|
||||
{ transform: 'rotate(0deg) scale(1)', opacity: '1' }
|
||||
],
|
||||
options: {
|
||||
duration: 500
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To learn more about creating Web Animations, refer to the documentation for [`Element.animate()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate).
|
||||
|
||||
?> Animations respect the users `prefers-reduced-motion` setting. When this setting is enabled, animations will not be played. To disable animations for all users, set `options.duration` to `0`.
|
||||
|
||||
@@ -11,7 +11,7 @@ The easiest way to install Shoelace is with the CDN. Just add the following tags
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
Now you can [start using Shoelace!](/getting-started/usage.md)
|
||||
Now you can [start using Shoelace!](/getting-started/usage)
|
||||
|
||||
## Local Installation
|
||||
|
||||
@@ -30,6 +30,8 @@ Once you've done that, add the following tags to your page. Make sure to update
|
||||
<script type="module" src="/scripts/shoelace/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
?> For clarity, the docs will usually show imports from `@shoelace-style/shoelace`. If you're not using a module resolver or bundler, you'll need to adjust these paths to point to the folder Shoelace is in.
|
||||
|
||||
## Setting the Base Path
|
||||
|
||||
Some components rely on assets (icons, images, etc.) and Shoelace needs to know where they're located. For convenience, Shoelace will try to auto-detect the correct location based on the script you've loaded it from. This assumes assets are colocated with `shoelace.js` and will "just work" for most users.
|
||||
@@ -56,15 +58,15 @@ The previous approach is the _easiest_ way to load Shoelace, but easy isn't alwa
|
||||
|
||||
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, including its dependencies.
|
||||
|
||||
Here's an example that loads only the button component and its dependencies. Again, we're assuming you're serving Shoelace's `dist` directory from `/scripts/shoelace`.
|
||||
Here's an example that loads only the button component and its dependencies. Again, if you're not using a module resolver, you'll need to adjust the path to point to the folder Shoelace is in.
|
||||
|
||||
```html
|
||||
<!-- The base stylesheet is always required -->
|
||||
<link rel="stylesheet" href="/scripts/shoelace/dist/themes/base.css">
|
||||
<link rel="stylesheet" href="@shoelace-style/shoelace/dist/themes/base.css">
|
||||
|
||||
<script type="module" data-shoelace="/scripts/shoelace">
|
||||
import SlButton from '/scripts/shoelace/dist/components/button/button.js';
|
||||
import SlSpinner from '/scripts/shoelace/dist/components/spinner/spinner.js';
|
||||
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.js';
|
||||
import SlSpinner from '@shoelace-style/shoelace/dist/components/spinner/spinner.js';
|
||||
|
||||
// <sl-button> and <sl-spinner> are ready to use!
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<div class="splash">
|
||||
<div class="splash-start">
|
||||
<img class="splash-logo" src="/assets/images/wordmark.svg" alt="Shoelace">
|
||||
<p><strong>A forward-thinking library of web components.</strong></p>
|
||||
<ul>
|
||||
<li>Works with all frameworks 🧩</li>
|
||||
<li>Works with CDNs 🚛</li>
|
||||
<li>Fully customizable with CSS 🎨</li>
|
||||
<li>Includes an official dark theme 🌛</li>
|
||||
<li>Built with accessibility in mind ♿️</li>
|
||||
<li>Open source 😸</li>
|
||||
</ul>
|
||||
<p>Designed in New Hampshire by <a href="https://twitter.com/claviska" rel="noopener" target="_blank">Cory LaViska</a>.</p>
|
||||
</div>
|
||||
<div class="splash-end">
|
||||
<img class="splash-image" src="/assets/images/undraw-content-team.svg" alt="Cartoon of people assembling components while standing on a giant laptop.">
|
||||
</div>
|
||||
<div class="splash-start">
|
||||
<img class="splash-logo" src="/assets/images/wordmark.svg" alt="Shoelace">
|
||||
|
||||
**A forward-thinking library of web components.**
|
||||
|
||||
- Works with all frameworks 🧩
|
||||
- Works with CDNs 🚛
|
||||
- Fully customizable with CSS 🎨
|
||||
- Includes an official dark theme 🌛
|
||||
- Built with accessibility in mind ♿️
|
||||
- First-party [React wrappers](/getting-started/usage#react)
|
||||
- Open source 😸
|
||||
|
||||
Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
|
||||
</div>
|
||||
<div class="splash-end">
|
||||
<img class="splash-image" src="/assets/images/undraw-content-team.svg" alt="Cartoon of people assembling components while standing on a giant laptop.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace)
|
||||
@@ -39,7 +41,7 @@ Now you have access to all of Shoelace's components! Try adding a button:
|
||||
<sl-button>Click me</sl-button>
|
||||
```
|
||||
|
||||
See the [installation instructions](getting-started/installation.md) for more details and other ways to install Shoelace.
|
||||
See the [installation instructions](getting-started/installation) for more details and other ways to install Shoelace.
|
||||
|
||||
## New to Web Components?
|
||||
|
||||
@@ -120,5 +122,6 @@ Special thanks to the following projects and individuals that helped make Shoela
|
||||
- Icons are courtesy of [Bootstrap Icons](https://icons.getbootstrap.com/)
|
||||
- The homepage illustration is courtesy of [unDraw](https://undraw.co/)
|
||||
- Positioning of menus, tooltips, et al is handled by [Popper.js](https://popper.js.org/)
|
||||
- The animation component was inspired by [animatable-component](https://github.com/proyecto26/animatable-component)
|
||||
- Animations are courtesy of [animate.css](https://animate.style/)
|
||||
- QR codes are generated with [qr-creator](https://github.com/nimiq/qr-creator)
|
||||
- The Shoelace logo was designed with a single shoelace by [Adam K Olson](https://twitter.com/adamkolson)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Shoelace ships with a dark theme that complements the default light theme. You can even take things a step further by designing your own custom theme.
|
||||
|
||||
The default theme is included as part of `themse/base.css` and should always be loaded first, even if you want to use another theme exclusively. The default theme contains important base tokens and utilities that many components rely on.
|
||||
The default theme is included as part of `themes/base.css` and should always be loaded first, even if you want to use another theme exclusively. The default theme contains important base tokens and utilities that many components rely on.
|
||||
|
||||
## Dark Mode
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ For example, `<button>` and `<sl-button>` both have a `type` attribute, but it d
|
||||
|
||||
Shoelace ships with a file called `vscode.html-custom-data.json` that can be used to describe its components to Visual Studio Code. This enables code completion for Shoelace components (also known as "code hinting" or "IntelliSense"). To enable it, you need to tell VS Code where the file is.
|
||||
|
||||
1. [Install Shoelace locally](/getting-started/installation.md#local-installation)
|
||||
1. [Install Shoelace locally](/getting-started/installation#local-installation)
|
||||
2. Create a folder called `.vscode` at the root of your project
|
||||
3. Create a file inside the folder called `settings.json`
|
||||
4. Add the following to the file
|
||||
@@ -139,39 +139,51 @@ Event handling can also be cumbersome.
|
||||
|
||||
> Because React implements its own synthetic event system, it cannot listen for DOM events coming from Custom Elements without the use of a workaround. Developers will need to reference their Custom Elements using a ref and manually attach event listeners with addEventListener. This makes working with Custom Elements cumbersome.
|
||||
|
||||
Fortunately, there's a utility that will wrap Shoelace components so you can use them as if they were React components. 👇
|
||||
|
||||
?> If you're starting a new project, consider using [Preact](https://preactjs.com/) as an alternative. It shares the same API as React and [handles custom elements quite well](https://custom-elements-everywhere.com/#preact).
|
||||
|
||||
### Wrapping Components
|
||||
|
||||
You can use [this utility](https://www.npmjs.com/package/@shoelace-style/react-wrapper) to wrap Shoelace components so they work like regular React components. To install it, use this command.
|
||||
Fortunately, there's a package called [@shoelace-style/react](https://www.npmjs.com/package/@shoelace-style/react) that will let you use Shoelace components as if they were React components. You can install it using this command.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/react-wrapper
|
||||
npm install @shoelace-style/react
|
||||
```
|
||||
|
||||
Now you can "import" Shoelace components as React components! Remember to [install Shoelace](/getting-started/installation.md) first, otherwise this won't work.
|
||||
|
||||
```js
|
||||
import wrapCustomElement from '@shoelace-style/react-wrapper';
|
||||
|
||||
const SlButton = wrapCustomElement('sl-button');
|
||||
|
||||
return <SlButton type="primary">Click me</SlButton>;
|
||||
```
|
||||
|
||||
A reference ("ref") to the underlying custom element is exposed through the `element` property so you can access it directly. This is useful for calling methods.
|
||||
Include the base theme and any components you want to use in your app.
|
||||
|
||||
```jsx
|
||||
<SlButton
|
||||
ref={el => this.button = el}
|
||||
onClick={() => this.button.element.current.blur()}
|
||||
>
|
||||
Click me
|
||||
</SlButton>
|
||||
import '@shoelace-style/shoelace/dist/themes/base.css';
|
||||
|
||||
import SlButton from '@shoelace-style/react/dist/button';
|
||||
import SlSpinner from '@shoelace-style/react/dist/spinner';
|
||||
|
||||
// ...
|
||||
|
||||
const MyComponent = (props) => {
|
||||
return (
|
||||
<SlButton type="primary">
|
||||
Click me
|
||||
</SlButton>
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
Some components depend on other components internally. For example, `<sl-button>` requires you to load `<sl-spinner>` because it's used internally for its loading state. If a component has dependencies, they'll be listed in the "Dependencies" section of its documentation. These are always Shoelace components, not third-party libraries.
|
||||
|
||||
Since dependencies are just components, you can load them the same way.
|
||||
|
||||
```jsx
|
||||
import SlButton from '@shoelace-style/react/dist/button';
|
||||
import SlSpinner from '@shoelace-style/react/dist/spinner';
|
||||
```
|
||||
|
||||
However, this may cause your linter to complain (e.g. "SlButton is defined but never used"). If you're not going to use the dependent components in your JSX, you can import them as side effects instead.
|
||||
|
||||
```jsx
|
||||
import '@shoelace-style/react/dist/button';
|
||||
import '@shoelace-style/react/dist/spinner';
|
||||
```
|
||||
|
||||
This extra step is required for dependencies to ensure they get registered with the browser as custom elements.
|
||||
|
||||
## Vue
|
||||
|
||||
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements. You just have to tell it to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
|
||||
@@ -202,7 +214,7 @@ One caveat is there's currently [no support for v-model on custom elements](http
|
||||
<sl-input :value="name" @input="name = $event.target.value">
|
||||
```
|
||||
|
||||
If that's too verbose, you can use a custom directive instead. 👇
|
||||
If that's too verbose, you can use a custom directive instead.
|
||||
|
||||
### Using a Custom Directive
|
||||
|
||||
|
||||
@@ -6,12 +6,92 @@ 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._ 🐛
|
||||
|
||||
## 2.0.0-beta.44
|
||||
|
||||
- 🚨 BREAKING: all `invalid` props on form controls now reflect validity before interaction [#455](https://github.com/shoelace-style/shoelace/issues/455)
|
||||
- Allow `null` to be passed to disable animations in `setDefaultAnimation()` and `setAnimation()`
|
||||
- Converted build scripts to ESM
|
||||
- Fixed a bug in `sl-checkbox` where `invalid` did not update properly
|
||||
- Fixed a bug in `sl-dropdown` where a `keydown` listener wasn't cleaned up properly
|
||||
- Fixed a bug in `sl-select` where `sl-blur` was emitted prematurely [#456](https://github.com/shoelace-style/shoelace/issues/456)
|
||||
- Fixed a bug in `sl-select` where no selection with `multiple` resulted in an incorrect value [#457](https://github.com/shoelace-style/shoelace/issues/457)
|
||||
- Fixed a bug in `sl-select` where `sl-change` was emitted immediately after connecting to the DOM [#458](https://github.com/shoelace-style/shoelace/issues/458)
|
||||
- Fixed a bug in `sl-select` where non-printable keys would cause the menu to open
|
||||
- Fixed a bug in `sl-select` where `invalid` was not always updated properly
|
||||
- Reworked the `@watch` decorator to use `update` instead of `updated` resulting in better performance and flexibility
|
||||
|
||||
## 2.0.0-beta.43
|
||||
|
||||
- Added `?` to optional arguments in methods tables in the docs
|
||||
- Added the `scrollPosition()` method to `sl-textarea` to get/set scroll position
|
||||
- Added intial tests for `sl-dialog`, `sl-drawer`, `sl-dropdown`, and `sl-tooltip`
|
||||
- Fixed a bug in `sl-tab-group` where scrollable tab icons were not displaying correctly
|
||||
- Fixed a bug in `sl-dialog` and `sl-drawer` where preventing clicks on the overlay no longer worked as described [#452](https://github.com/shoelace-style/shoelace/issues/452)
|
||||
- Fixed a bug in `sl-dialog` and `sl-drawer` where setting initial focus no longer worked as described [#453](https://github.com/shoelace-style/shoelace/issues/453)
|
||||
- Fixed a bug in `sl-card` where the `slotchange` listener wasn't attached correctly [#454](https://github.com/shoelace-style/shoelace/issues/454)
|
||||
- Fixed lifecycle bugs in a number of components [#451](https://github.com/shoelace-style/shoelace/issues/451)
|
||||
- Removed `fill: both` from internal animate utility so styles won't "stick" by default [#450](https://github.com/shoelace-style/shoelace/issues/450)
|
||||
|
||||
## 2.0.0-beta.42
|
||||
|
||||
This release addresses an issue with the `open` prop no longer working in a number of components, as a result of the changes in beta.41. It also removes a small but controversial feature that complicated show/hide logic and led to a poor experience for developers and end users.
|
||||
|
||||
There are two ways to show/hide affected components: by calling `show() | hide()` and by toggling the `open` prop. Previously, it was possible to call `event.preventDefault()` in an `sl-show | sl-hide ` handler to stop the component from showing/hiding. The problem becomes obvious when you set `el.open = false`, the event gets canceled, and in the next cycle `el.open` has reverted to `true`. Not only is this unexpected, but it also doesn't play nicely with frameworks. Additionally, this made it impossible to await `show() | hide()` since there was a chance they'd never resolve.
|
||||
|
||||
Technical reasons aside, canceling these events seldom led to a good user experience, so the decision was made to no longer allow `sl-show | sl-hide` to be cancelable.
|
||||
|
||||
- 🚨 BREAKING: `sl-show` and `sl-hide` events are no longer cancelable
|
||||
- Added Iconoir example to the icon docs
|
||||
- Added Web Test Runner
|
||||
- Added intial tests for `sl-alert` and `sl-details`
|
||||
- Changed the `cancelable` default to `false` for the internal `@event` decorator
|
||||
- Fixed a bug where toggling `open` stopped working in `sl-alert`, `sl-dialog`, `sl-drawer`, `sl-dropdown`, and `sl-tooltip`
|
||||
- Fixed a bug in `sl-range` where setting a value outside the default `min` or `max` would clamp the value [#448](https://github.com/shoelace-style/shoelace/issues/448)
|
||||
- Fixed a bug in `sl-dropdown` where placement wouldn't adjust properly when shown [#447](https://github.com/shoelace-style/shoelace/issues/447)
|
||||
- Fixed a bug in the internal `shimKeyframesHeightAuto` utility that caused `sl-details` to measure heights incorrectly [#445](https://github.com/shoelace-style/shoelace/issues/445)
|
||||
- Fixed a number of imports that should have been type imports
|
||||
- Updated Lit to 2.0.0-rc.2
|
||||
- Updated esbuild to 0.12.4
|
||||
|
||||
## 2.0.0-beta.41
|
||||
|
||||
This release changes how components animate. In previous versions, CSS transitions were used for most show/hide animations. Transitions are problematic due to the way `transitionend` works. This event fires once _per transition_, and it's impossible to know which transition to look for when users can customize any possible CSS property. Because of this, components previously required the `opacity` property to transition. If a user were to prevent `opacity` from transitioning, the component wouldn't hide properly and the `sl-after-show|hide` events would never emit.
|
||||
|
||||
CSS animations, on the other hand, have a more reliable `animationend` event. Alas, `@keyframes` don't cascade and can't be injected into a shadow DOM via CSS, so there would be no good way to customize them.
|
||||
|
||||
The most elegant solution I found was to use the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API), which offers more control over animations at the expense of customizations being done in JavaScript. Fortunately, through the [Animation Registry](/getting-started/customizing#animations), you can customize animations globally and/or per component with a minimal amount of code.
|
||||
|
||||
- 🚨 BREAKING: changed `left` and `right` placements to `start` and `end` in `sl-drawer`
|
||||
- 🚨 BREAKING: changed `left` and `right` placements to `start` and `end` in `sl-tab-group`
|
||||
- 🚨 BREAKING: removed `--hide-duration`, `--hide-timing-function`, `--show-duration`, and `--show-timing-function` custom properties from `sl-tooltip` (use the Animation Registry instead)
|
||||
- Added the Animation Registry
|
||||
- Fixed a bug where removing `sl-dropdown` from the DOM and adding it back destroyed the popover reference [#443](https://github.com/shoelace-style/shoelace/issues/443)
|
||||
- Updated animations for `sl-alert`, `sl-dialog`, `sl-drawer`, `sl-dropdown`, and `sl-tooltip` to use the Animation Registry instead of CSS transitions
|
||||
- Improved a11y by respecting `prefers-reduced-motion` for all show/hide animations
|
||||
- Improved `--show-delay` and `--hide-delay` behavior in `sl-tooltip` so they only apply on hover
|
||||
- Removed the internal popover utility
|
||||
|
||||
## 2.0.0-beta.40
|
||||
|
||||
- 🚨 BREAKING: renamed `sl-responsive-embed` to `sl-responsive-media` and added support for images and videos [#436](https://github.com/shoelace-style/shoelace/issues/436)
|
||||
- Fixed a bug where setting properties before an element was defined would render incorrectly [#425](https://github.com/shoelace-style/shoelace/issues/425)
|
||||
- Fixed a bug that caused all modules to be imported when cherry picking certain components [#439](https://github.com/shoelace-style/shoelace/issues/439)
|
||||
- Fixed a bug where the scrollbar would reposition `sl-dialog` on hide causing it to jump [#424](https://github.com/shoelace-style/shoelace/issues/424)
|
||||
- Fixed a bug that prevented the project from being built in a Windows environment
|
||||
- Improved a11y in `sl-progress-ring`
|
||||
- Removed `src/utilities/index.ts` to prevent tree-shaking confusion (please import utilities directly from their respective modules)
|
||||
- Removed global `[hidden]` styles so they don't affect anything outside of components
|
||||
- Updated to Bootstrap Icons 1.5.0
|
||||
- Updated React docs to use [`@shoelace-style/react`](https://github.com/shoelace-style/react)
|
||||
- Updated NextJS docs [#434](https://github.com/shoelace-style/shoelace/pull/434)
|
||||
- Updated TypeScript to 4.2.4
|
||||
|
||||
## 2.0.0-beta.39
|
||||
|
||||
- Added experimental `sl-qr-code` component
|
||||
- Added `system` icon library and updated all components to use this instead of the default icon library [#420](https://github.com/shoelace-style/shoelace/issues/420)
|
||||
- Updated to esbuild 0.8.57
|
||||
- Updated to lit 2.0.0-rc.1 and lit-html 2.0.0-rc.2
|
||||
- Updated to Lit 2.0.0-rc.1 and lit-html 2.0.0-rc.2
|
||||
|
||||
## 2.0.0-beta.38
|
||||
|
||||
|
||||
@@ -108,6 +108,29 @@ There is currently no hot module reloading (HMR), as browsers don't provide a wa
|
||||
|
||||
For more information about running and building the project locally, refer to `README.md` in the project's root.
|
||||
|
||||
### Testing
|
||||
|
||||
Shoelace uses [Web Test Runner](https://modern-web.dev/guides/test-runner/getting-started/) for testing. To launch the test runner during development, open a terminal and launch the dev server.
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
In a second terminal window, launch the test runner.
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
Follow the on-screen instructions to work with the test runner. Tests will automatically re-run as you make changes.
|
||||
|
||||
To run tests only once, make sure to build the project first.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the project.
|
||||
@@ -188,16 +211,25 @@ This convention avoids the problem of browsers lowercasing attributes, causing s
|
||||
|
||||
### CSS Custom Properties
|
||||
|
||||
To expose custom properties as part of a component's API, scope them to the `:host` block and use the following syntax for comments so they appear in the generated docs. Do not use the `--sl-` prefix, as that is reserved for design tokens that live in the global scope.
|
||||
To expose custom properties as part of a component's API, scope them to the `:host` block.
|
||||
|
||||
```css
|
||||
/**
|
||||
* @prop --color: The component's text color.
|
||||
* @prop --background-color: The component's background color.
|
||||
*/
|
||||
```scss
|
||||
:host {
|
||||
--color: white;
|
||||
--background-color: tomato;
|
||||
--color: var(--sl-color-primary-500);
|
||||
--background-color: var(--sl-color-gray-100);
|
||||
}
|
||||
```
|
||||
|
||||
Then use the following syntax for comments so they appear in the generated docs. Do not use the `--sl-` prefix, as that is reserved for design tokens that live in the global scope.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @customProperty --color: The component's text color.
|
||||
* @customProperty --background-color: The component's background color.
|
||||
*/
|
||||
@customElement('sl-example')
|
||||
export default class SlExample {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -232,7 +264,3 @@ Form controls should support validation through the following conventions:
|
||||
- All form controls must have a `reportValidity()` method that report their validity during form submission
|
||||
- All form controls should mirror their native validation attributes such as `required`, `pattern`, `minlength`, `maxlength`, etc. when possible
|
||||
- All form controls must be serialized by `<sl-form>`
|
||||
|
||||
### Positioning Popovers
|
||||
|
||||
Shoelace uses an internal popover utility for dropdowns, tooltips, etc. This is a light abstraction of Popper.js designed to make positioning and transitioning things easy and consistent throughout the library. When possible, use this utility instead of relying on Popper directly. See `src/utilities/popover.ts` for details.
|
||||
|
||||
@@ -14,7 +14,7 @@ This integration has been tested with the following:
|
||||
To get started using Shoelace with NextJS, the following packages must be installed.
|
||||
|
||||
```bash
|
||||
yarn add @shoelace-style/shoelace @shoelace-style/react-wrapper copy-webpack-plugin
|
||||
yarn add @shoelace-style/shoelace @shoelace-style/react-wrapper copy-webpack-plugin next-compose-plugins next-transpile-modules
|
||||
```
|
||||
|
||||
### Importing the Default Theme
|
||||
@@ -22,7 +22,7 @@ yarn add @shoelace-style/shoelace @shoelace-style/react-wrapper copy-webpack-plu
|
||||
The next step is to import Shoelace's default theme (stylesheet) in your `_app.js` file:
|
||||
|
||||
```css
|
||||
@import '~@shoelace-style/shoelace/dist/themes/base';
|
||||
import '@shoelace-style/shoelace/dist/themes/base.css';
|
||||
```
|
||||
|
||||
### Defining Custom Elements
|
||||
@@ -40,12 +40,16 @@ function CustomEls({ URL }) {
|
||||
if (customEls.current) {
|
||||
return;
|
||||
}
|
||||
setBasePath(`${URL}/static/static`);
|
||||
|
||||
const { setBasePath } = require("@shoelace-style/shoelace/dist/utilities/base-path");
|
||||
|
||||
// Define the components you intend to use
|
||||
customElements.define("sl-alert", SlAlert);
|
||||
customElements.define("sl-button", SlButton);
|
||||
// ...
|
||||
setBasePath(`${URL}/static/static`);
|
||||
|
||||
// This imports all components
|
||||
require("@shoelace-style/shoelace/dist/shoelace");
|
||||
|
||||
// If you want to selectively import components, replace this line with your own definitions
|
||||
// require("@shoelace-style/shoelace/dist/components/button/button");
|
||||
|
||||
customEls.current = true;
|
||||
}, [URL, customEls]);
|
||||
@@ -90,7 +94,7 @@ MyApp.getInitialProps = async (context) => {
|
||||
const URL = process.env.BASE_URL;
|
||||
|
||||
return {
|
||||
URL,
|
||||
URL
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -104,31 +108,30 @@ Next we need to add Shoelace's assets to the final build output. To do this, mod
|
||||
```javascript
|
||||
const path = require("path");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')(['@shoelace-style/shoelace']);
|
||||
|
||||
module.exports = {
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(
|
||||
__dirname,
|
||||
"node_modules/@shoelace-style/shoelace/dist/assets"
|
||||
),
|
||||
to: path.resolve(__dirname, "static/assets"),
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
return config;
|
||||
},
|
||||
};
|
||||
module.exports = withPlugins([withTM], {
|
||||
webpack: config => {
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(__dirname, 'node_modules/@shoelace-style/shoelace/dist/assets/icons'),
|
||||
to: path.resolve(__dirname, 'static/icons')
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
return config;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
?> This will copy the files from `node_modules` into your `static` folder on every development serve or build. You may want to avoid commiting these into your repo. To do so, simply add `static/assets` into your `.gitignore` folder
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- There is a third-party [example repo](https://github.com/crutchcorn/nextjs-shoelace-example), courtesy of [crutchcorn](https://github.com/crutchcorn) available to help you get started.
|
||||
- There is a third-party [example repo](https://github.com/crutchcorn/nextjs-shoelace-example), courtesy of [crutchcorn](https://github.com/crutchcorn), available to help you get started.
|
||||
|
||||
|
||||
|
||||
6984
package-lock.json
generated
6984
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.0.0-beta.39",
|
||||
"version": "2.0.0-beta.44",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -29,46 +29,50 @@
|
||||
"url": "https://github.com/sponsors/claviska"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/build.cjs --dev",
|
||||
"build": "node scripts/build.cjs",
|
||||
"prepublishOnly": "npm run build",
|
||||
"start": "node scripts/build.js --dev",
|
||||
"build": "node scripts/build.js",
|
||||
"prepublishOnly": "npm run build && npm run test",
|
||||
"prettier": "prettier --write --loglevel warn .",
|
||||
"create": "node scripts/create-component.cjs"
|
||||
"create": "node scripts/create-component.js",
|
||||
"test": "web-test-runner \"src/**/*.test.ts\" --node-resolve --puppeteer",
|
||||
"test:watch": "web-test-runner \"src/**/*.test.ts\" --node-resolve --puppeteer --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.7.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"color": "^3.1.3",
|
||||
"lit": "^2.0.0-rc.1",
|
||||
"globby": "^11.0.3",
|
||||
"lit": "^2.0.0-rc.2",
|
||||
"lit-html": "^2.0.0-rc.2",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@open-wc/testing": "^2.5.33",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/resize-observer-browser": "^0.1.5",
|
||||
"@web/dev-server-esbuild": "^0.2.12",
|
||||
"@web/test-runner": "^0.13.5",
|
||||
"@web/test-runner-puppeteer": "^0.10.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"bootstrap-icons": "^1.4.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
"chalk": "^4.1.0",
|
||||
"command-line-args": "^5.1.1",
|
||||
"comment-parser": "^1.1.2",
|
||||
"concurrently": "^5.3.0",
|
||||
"del": "^6.0.0",
|
||||
"download": "^8.0.0",
|
||||
"esbuild": "^0.8.54",
|
||||
"esbuild": "^0.12.4",
|
||||
"esbuild-plugin-inline-import": "^1.0.0",
|
||||
"esbuild-plugin-sass": "^0.3.3",
|
||||
"front-matter": "^4.0.2",
|
||||
"get-port": "^5.1.1",
|
||||
"glob": "^7.1.6",
|
||||
"husky": "^4.3.8",
|
||||
"prettier": "^2.2.1",
|
||||
"recursive-copy": "^2.0.11",
|
||||
"sass": "^1.32.7",
|
||||
"tiny-glob": "^0.2.8",
|
||||
"tslib": "^2.1.0",
|
||||
"sinon": "^11.1.1",
|
||||
"tslib": "^2.2.0",
|
||||
"typedoc": "^0.20.28",
|
||||
"typescript": "4.1.5",
|
||||
"typescript": "^4.2.4",
|
||||
"wait-on": "^5.2.1"
|
||||
},
|
||||
"husky": {
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
//
|
||||
// Builds the project. To spin up a dev server, pass the --serve flag.
|
||||
//
|
||||
const bs = require('browser-sync').create();
|
||||
const chalk = require('chalk');
|
||||
const commandLineArgs = require('command-line-args');
|
||||
const copy = require('recursive-copy');
|
||||
const del = require('del');
|
||||
const esbuild = require('esbuild');
|
||||
const execSync = require('child_process').execSync;
|
||||
const getPort = require('get-port');
|
||||
const glob = require('tiny-glob');
|
||||
const inlineImportPlugin = require('esbuild-plugin-inline-import');
|
||||
const path = require('path');
|
||||
const sass = require('sass');
|
||||
const sassPlugin = require('esbuild-plugin-sass');
|
||||
const { build } = require('esbuild');
|
||||
import browserSync from 'browser-sync';
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import copy from 'recursive-copy';
|
||||
import del from 'del';
|
||||
import esbuild from 'esbuild';
|
||||
import { execSync } from 'child_process';
|
||||
import getPort from 'get-port';
|
||||
import glob from 'globby';
|
||||
import inlineImportPlugin from 'esbuild-plugin-inline-import';
|
||||
import path from 'path';
|
||||
import sass from 'sass';
|
||||
import sassPlugin from 'esbuild-plugin-sass';
|
||||
|
||||
const build = esbuild.build;
|
||||
const bs = browserSync.create();
|
||||
const { dev } = commandLineArgs({ name: 'dev', type: Boolean });
|
||||
|
||||
execSync(`rm -rf ./dist`, { stdio: 'inherit' });
|
||||
del.sync('./dist');
|
||||
|
||||
if (!dev) execSync('tsc', { stdio: 'inherit' }); // for type declarations
|
||||
execSync('node scripts/make-metadata.cjs', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-icons.cjs', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-icons.js', { stdio: 'inherit' });
|
||||
|
||||
(async () => {
|
||||
const entryPoints = [
|
||||
// The whole shebang dist
|
||||
'./src/shoelace.ts',
|
||||
// Components
|
||||
...(await glob('./src/components/**/*.ts')),
|
||||
...(await glob('./src/components/**/!(*.test).ts')),
|
||||
// Public utilities
|
||||
...(await glob('./src/utilities/**/*.ts')),
|
||||
...(await glob('./src/utilities/**/!(*.test).ts')),
|
||||
// Theme stylesheets
|
||||
...(await glob('./src/themes/**/*.ts'))
|
||||
...(await glob('./src/themes/**/!(*.test).ts'))
|
||||
];
|
||||
|
||||
const buildResult = await esbuild
|
||||
@@ -114,7 +116,7 @@ execSync('node scripts/make-icons.cjs', { stdio: 'inherit' });
|
||||
});
|
||||
|
||||
// Rebuild and reload when source files change
|
||||
bs.watch(['src/**/*']).on('change', async filename => {
|
||||
bs.watch(['src/**/!(*.test).*']).on('change', async filename => {
|
||||
console.log(`Source file changed - ${filename}`);
|
||||
|
||||
// NOTE: we don't run TypeDoc on every change because it's quite heavy, so changes to the docs won't be included
|
||||
@@ -1,17 +1,19 @@
|
||||
const args = process.argv.slice(2);
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs');
|
||||
const mkdirp = require('mkdirp');
|
||||
const path = require('path');
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const tagName = (args[0] + '').toLowerCase().trim();
|
||||
const tagNameWithoutPrefix = tagName.replace(/^sl-/, '');
|
||||
const className = tagName.replace(/(^\w|-\w)/g, string => string.replace(/-/, '').toUpperCase());
|
||||
const readableName = tagNameWithoutPrefix
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\w\S*/g, string => string.charAt(0).toUpperCase() + string.substr(1).toLowerCase());
|
||||
const version = require('../package.json').version;
|
||||
const minorVersion = version.split('.').slice(0, 2).join('.');
|
||||
|
||||
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
const minorVersion = packageData.version.split('.').slice(0, 2).join('.');
|
||||
|
||||
// Check for tag name
|
||||
if (!tagName) {
|
||||
@@ -34,7 +36,7 @@ if (fs.existsSync(componentFile)) {
|
||||
|
||||
const componentSource = `
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import styles from 'sass:./${tagNameWithoutPrefix}.scss';
|
||||
|
||||
/**
|
||||
@@ -48,7 +50,11 @@ import styles from 'sass:./${tagNameWithoutPrefix}.scss';
|
||||
* @slot example - A named slot called example.
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
* @part example - Another part called example.
|
||||
*
|
||||
* @customProperty example - An example custom property
|
||||
*
|
||||
* @animation example.show - An example animation.
|
||||
* @animation example.hide - An example animation.
|
||||
*/
|
||||
@customElement('${tagName}')
|
||||
export default class ${className} extends LitElement {
|
||||
@@ -75,9 +81,6 @@ const stylesFile = `src/components/${tagNameWithoutPrefix}/${tagNameWithoutPrefi
|
||||
const stylesSource = `
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --custom-property-here: Description here
|
||||
*/
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
//
|
||||
// This script downloads and generates icons and icon metadata.
|
||||
//
|
||||
const Promise = require('bluebird');
|
||||
const promisify = require('util').promisify;
|
||||
const chalk = require('chalk');
|
||||
const copy = require('recursive-copy');
|
||||
const del = require('del');
|
||||
const download = require('download');
|
||||
const mkdirp = require('mkdirp');
|
||||
const fm = require('front-matter');
|
||||
const fs = require('fs').promises;
|
||||
const glob = promisify(require('glob'));
|
||||
const path = require('path');
|
||||
import Promise from 'bluebird';
|
||||
import chalk from 'chalk';
|
||||
import copy from 'recursive-copy';
|
||||
import del from 'del';
|
||||
import download from 'download';
|
||||
import mkdirp from 'mkdirp';
|
||||
import fm from 'front-matter';
|
||||
import { readFileSync } from 'fs';
|
||||
import { stat, readFile, writeFile } from 'fs/promises';
|
||||
import glob from 'globby';
|
||||
import path from 'path';
|
||||
|
||||
const baseDir = path.dirname(__dirname);
|
||||
const iconDir = './dist/assets/icons';
|
||||
const iconPackageData = JSON.parse(readFileSync('./node_modules/bootstrap-icons/package.json', 'utf8'));
|
||||
let numIcons = 0;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const version = require('bootstrap-icons/package').version;
|
||||
const version = iconPackageData.version;
|
||||
const srcPath = `./.cache/icons/icons-${version}`;
|
||||
const url = `https://github.com/twbs/icons/archive/v${version}.zip`;
|
||||
|
||||
try {
|
||||
await fs.stat(`${srcPath}/LICENSE.md`);
|
||||
await stat(`${srcPath}/LICENSE.md`);
|
||||
console.log(chalk.cyan('Generating icons from cache'));
|
||||
} catch {
|
||||
// Download the source from GitHub (since not everything is published to NPM)
|
||||
@@ -48,7 +48,7 @@ let numIcons = 0;
|
||||
|
||||
const metadata = await Promise.map(files, async file => {
|
||||
const name = path.basename(file, path.extname(file));
|
||||
const data = fm(await fs.readFile(file, 'utf8')).attributes;
|
||||
const data = fm(await readFile(file, 'utf8')).attributes;
|
||||
numIcons++;
|
||||
|
||||
return {
|
||||
@@ -59,7 +59,7 @@ let numIcons = 0;
|
||||
};
|
||||
});
|
||||
|
||||
await fs.writeFile(path.join(iconDir, 'icons.json'), JSON.stringify(metadata, null, 2), 'utf8');
|
||||
await writeFile(path.join(iconDir, 'icons.json'), JSON.stringify(metadata, null, 2), 'utf8');
|
||||
|
||||
console.log(chalk.green(`Successfully processed ${numIcons} icons ✨\n`));
|
||||
} catch (err) {
|
||||
@@ -1,13 +1,13 @@
|
||||
//
|
||||
// This script runs TypeDoc and uses its output to generate metadata files used by the docs
|
||||
//
|
||||
const chalk = require('chalk');
|
||||
const execSync = require('child_process').execSync;
|
||||
const fs = require('fs');
|
||||
const mkdirp = require('mkdirp');
|
||||
const path = require('path');
|
||||
const package = require('../package.json');
|
||||
const { parse } = require('comment-parser/lib');
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
|
||||
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
|
||||
function getTagName(className) {
|
||||
return className.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`).replace(/^-/, '');
|
||||
@@ -32,6 +32,18 @@ function getTypeInfo(item) {
|
||||
});
|
||||
}
|
||||
|
||||
if (item.type.type === 'reflection' && item.type.declaration?.children) {
|
||||
const args = item.type.declaration.children.map(prop => {
|
||||
const name = prop.name;
|
||||
const type = prop.type.name;
|
||||
const isOptional = prop.flags.isOptional === true;
|
||||
return `${name}${isOptional ? '?' : ''}: ${type}`;
|
||||
});
|
||||
|
||||
// Display as an object
|
||||
type += `{ ${args.join(', ')} }`;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
values: values.length ? values : undefined
|
||||
@@ -67,12 +79,12 @@ const data = JSON.parse(fs.readFileSync('.cache/typedoc.json', 'utf8'));
|
||||
const modules = data.children;
|
||||
const components = modules.filter(module => module.kindString === 'Class');
|
||||
const metadata = {
|
||||
name: package.name,
|
||||
description: package.description,
|
||||
version: package.version,
|
||||
author: package.author,
|
||||
homepage: package.homepage,
|
||||
license: package.license,
|
||||
name: packageData.name,
|
||||
description: packageData.description,
|
||||
version: packageData.version,
|
||||
author: packageData.author,
|
||||
homepage: packageData.homepage,
|
||||
license: packageData.license,
|
||||
components: []
|
||||
};
|
||||
|
||||
@@ -98,12 +110,16 @@ components.map(async component => {
|
||||
const dependencies = tags.filter(item => item.tag === 'dependency');
|
||||
const slots = tags.filter(item => item.tag === 'slot');
|
||||
const parts = tags.filter(item => item.tag === 'part');
|
||||
const customProperties = tags.filter(item => item.tag === 'customproperty');
|
||||
const animations = tags.filter(item => item.tag === 'animation');
|
||||
|
||||
api.since = tags.find(item => item.tag === 'since').text.trim();
|
||||
api.status = tags.find(item => item.tag === 'status').text.trim();
|
||||
api.dependencies = dependencies.map(tag => tag.text.trim());
|
||||
api.slots = slots.map(tag => splitText(tag.text));
|
||||
api.parts = parts.map(tag => splitText(tag.text));
|
||||
api.cssCustomProperties = customProperties.map(tag => splitText(tag.text));
|
||||
api.animations = animations.map(tag => splitText(tag.text));
|
||||
} else {
|
||||
console.error(chalk.yellow(`Missing comment block for ${component.name} - skipping metadata`));
|
||||
}
|
||||
@@ -116,9 +132,29 @@ components.map(async component => {
|
||||
|
||||
props.map(prop => {
|
||||
const { type, values } = getTypeInfo(prop);
|
||||
let attribute;
|
||||
|
||||
// Look for an attribute in the @property decorator
|
||||
if (Array.isArray(prop.decorators)) {
|
||||
const decorator = prop.decorators.find(d => d.name === 'property');
|
||||
if (decorator) {
|
||||
try {
|
||||
// We trust TypeDoc <3
|
||||
const options = eval(`(${decorator.arguments.options})`);
|
||||
|
||||
// If an attribute is specified, it will always be a string
|
||||
if (options && typeof options.attribute === 'string') {
|
||||
attribute = options.attribute;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
api.props.push({
|
||||
name: prop.name,
|
||||
attribute: attribute,
|
||||
description: prop.comment.shortText,
|
||||
type,
|
||||
values,
|
||||
@@ -206,6 +242,7 @@ components.map(async component => {
|
||||
name: param.name,
|
||||
type,
|
||||
values,
|
||||
isOptional: param.flags?.isOptional,
|
||||
defaultValue: param.defaultValue
|
||||
};
|
||||
})
|
||||
@@ -218,17 +255,6 @@ components.map(async component => {
|
||||
});
|
||||
});
|
||||
|
||||
// CSS custom properties
|
||||
const stylesheet = path.resolve(path.dirname(api.file), path.parse(api.file).name + '.scss');
|
||||
if (fs.existsSync(stylesheet)) {
|
||||
const styles = fs.readFileSync(stylesheet, 'utf8');
|
||||
const parsed = parse(styles);
|
||||
const tags = parsed[0] ? parsed[0].tags : [];
|
||||
const cssCustomProperties = tags
|
||||
.filter(tag => tag.tag === 'prop')
|
||||
.map(tag => api.cssCustomProperties.push({ name: tag.name.slice(0, -1), description: tag.description }));
|
||||
}
|
||||
|
||||
metadata.components.push(api);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
@use '../../styles/component';
|
||||
@use '../../styles/mixins/hide';
|
||||
|
||||
/**
|
||||
* @prop --box-shadow: The alert's box shadow.
|
||||
*/
|
||||
:host {
|
||||
display: contents;
|
||||
|
||||
@@ -25,19 +22,7 @@
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
line-height: 1.6;
|
||||
color: var(--sl-color-gray-700);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: var(--sl-transition-medium) opacity ease, var(--sl-transition-medium) transform ease;
|
||||
margin: inherit;
|
||||
|
||||
&:not(.alert--visible) {
|
||||
@include hide.hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.alert--open {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.alert__icon {
|
||||
|
||||
93
src/components/alert/alert.test.ts
Normal file
93
src/components/alert/alert.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlAlert from './alert';
|
||||
|
||||
describe('<sl-alert>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = (await fixture(html` <sl-alert>I am an alert</sl-alert> `)) as SlAlert;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = (await fixture(html` <sl-alert open>I am an alert</sl-alert> `)) as SlAlert;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = (await fixture(html` <sl-alert>I am an alert</sl-alert> `)) as SlAlert;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.open = true;
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = (await fixture(html` <sl-alert open>I am an alert</sl-alert> `)) as SlAlert;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.open = false;
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./alert.scss';
|
||||
|
||||
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
|
||||
@@ -19,6 +22,11 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
|
||||
* @part icon - The container that wraps the alert icon.
|
||||
* @part message - The alert message.
|
||||
* @part close-button - The close button.
|
||||
*
|
||||
* @customProperty --box-shadow - The alert's box shadow.
|
||||
*
|
||||
* @animation alert.show - The animation to use when showing the alert.
|
||||
* @animation alert.hide - The animation to use when hiding the alert.
|
||||
*/
|
||||
@customElement('sl-alert')
|
||||
export default class SlAlert extends LitElement {
|
||||
@@ -26,7 +34,7 @@ export default class SlAlert extends LitElement {
|
||||
|
||||
private autoHideTimeout: any;
|
||||
|
||||
@state() private isVisible = false;
|
||||
@query('[part="base"]') base: HTMLElement;
|
||||
|
||||
/** Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
@@ -43,63 +51,40 @@ export default class SlAlert extends LitElement {
|
||||
*/
|
||||
@property({ type: Number }) duration = Infinity;
|
||||
|
||||
/** Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the alert opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the alert opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the alert closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the alert closes and all transitions are complete. */
|
||||
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Show on init if open
|
||||
if (this.open) {
|
||||
this.show();
|
||||
}
|
||||
firstUpdated() {
|
||||
this.base.hidden = !this.open;
|
||||
}
|
||||
|
||||
/** Shows the alert. */
|
||||
show() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (this.isVisible) {
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
this.open = true;
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.autoHideTimeout = setTimeout(() => this.hide(), this.duration);
|
||||
}
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the alert */
|
||||
hide() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,22 +137,38 @@ export default class SlAlert extends LitElement {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
handleTransitionEnd(event: TransitionEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
|
||||
// Ensure we only emit one event when the target element is no longer visible
|
||||
if (event.propertyName === 'opacity' && target.classList.contains('alert')) {
|
||||
this.isVisible = this.open;
|
||||
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show');
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide');
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
}
|
||||
|
||||
@watch('duration')
|
||||
@watch('duration', { waitUntilFirstUpdate: true })
|
||||
handleDurationChange() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
@@ -179,7 +180,6 @@ export default class SlAlert extends LitElement {
|
||||
class=${classMap({
|
||||
alert: true,
|
||||
'alert--open': this.open,
|
||||
'alert--visible': this.isVisible,
|
||||
'alert--closable': this.closable,
|
||||
'alert--primary': this.type === 'primary',
|
||||
'alert--success': this.type === 'success',
|
||||
@@ -192,7 +192,6 @@ export default class SlAlert extends LitElement {
|
||||
aria-atomic="true"
|
||||
aria-hidden=${this.open ? 'false' : 'true'}
|
||||
@mousemove=${this.handleMouseMove.bind(this)}
|
||||
@transitionend=${this.handleTransitionEnd.bind(this)}
|
||||
>
|
||||
<span part="icon" class="alert__icon">
|
||||
<slot name="icon"></slot>
|
||||
@@ -219,6 +218,22 @@ export default class SlAlert extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('alert.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.8)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('alert.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.8)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-alert': SlAlert;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, queryAsync } from 'lit/decorators';
|
||||
import { customElement, property, queryAsync } from 'lit/decorators.js';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./animation.scss';
|
||||
import { animations } from './animations';
|
||||
import styles from 'sass:./animation.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -93,7 +93,11 @@ export default class SlAnimation extends LitElement {
|
||||
@watch('iterations')
|
||||
@watch('iterationsStart')
|
||||
@watch('keyframes')
|
||||
handleAnimationChange() {
|
||||
async handleAnimationChange() {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createAnimation();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --size: The size of the avatar.
|
||||
*/
|
||||
:host {
|
||||
display: inline-block;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import styles from 'sass:./avatar.scss';
|
||||
|
||||
@@ -15,6 +15,8 @@ import styles from 'sass:./avatar.scss';
|
||||
* @part icon - The container that wraps the avatar icon.
|
||||
* @part initials - The container that wraps the avatar initials.
|
||||
* @part image - The avatar image.
|
||||
*
|
||||
* @customProperty --size - The size of the avatar.
|
||||
*/
|
||||
@customElement('sl-avatar')
|
||||
export default class SlAvatar extends LitElement {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import styles from 'sass:./badge.scss';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import styles from 'sass:./button-group.scss';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { event, EventEmitter } from '../../internal/decorators';
|
||||
import styles from 'sass:./button.scss';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from 'sass:./button.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --border-color: The card's border color, including borders that occur inside the card.
|
||||
* @prop --border-radius: The border radius for card edges.
|
||||
* @prop --border-width: The width of card borders.
|
||||
* @prop --padding: The padding to use for card sections.
|
||||
*/
|
||||
:host {
|
||||
--border-color: var(--sl-color-gray-200);
|
||||
--border-radius: var(--sl-border-radius-medium);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import styles from 'sass:./card.scss';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from 'sass:./card.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -18,6 +18,11 @@ import { hasSlot } from '../../internal/slot';
|
||||
* @part header - The card's header, if present.
|
||||
* @part body - The card's body.
|
||||
* @part footer - The card's footer, if present.
|
||||
*
|
||||
* @customProperty --border-color - The card's border color, including borders that occur inside the card.
|
||||
* @customProperty --border-radius - The border radius for card edges.
|
||||
* @customProperty --border-width - The width of card borders.
|
||||
* @customProperty --padding - The padding to use for card sections.*
|
||||
*/
|
||||
@customElement('sl-card')
|
||||
export default class SlCard extends LitElement {
|
||||
@@ -27,11 +32,6 @@ export default class SlCard extends LitElement {
|
||||
@state() private hasImage = false;
|
||||
@state() private hasHeader = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasFooter = hasSlot(this, 'footer');
|
||||
this.hasImage = hasSlot(this, 'image');
|
||||
@@ -50,11 +50,11 @@ export default class SlCard extends LitElement {
|
||||
})}
|
||||
>
|
||||
<div part="image" class="card__image">
|
||||
<slot name="image" onslotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="image" @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>
|
||||
|
||||
<div part="header" class="card__header">
|
||||
<slot name="header" onslotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="header" @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>
|
||||
|
||||
<div part="body" class="card__body">
|
||||
@@ -62,7 +62,7 @@ export default class SlCard extends LitElement {
|
||||
</div>
|
||||
|
||||
<div part="footer" class="card__footer">
|
||||
<slot name="footer" onslotchange=${this.handleSlotChange}></slot>
|
||||
<slot name="footer" @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
@@ -61,7 +61,7 @@ export default class SlCheckbox extends LitElement {
|
||||
@event('sl-focus') slFocus: EventEmitter<void>;
|
||||
|
||||
firstUpdated() {
|
||||
this.input.indeterminate = this.indeterminate;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Simulates a click on the checkbox. */
|
||||
@@ -111,11 +111,10 @@ export default class SlCheckbox extends LitElement {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
@watch('indeterminate')
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
@watch('indeterminate', { waitUntilFirstUpdate: true })
|
||||
handleStateChange() {
|
||||
this.input.checked = this.checked;
|
||||
this.input.indeterminate = this.indeterminate;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.slChange.emit();
|
||||
}
|
||||
|
||||
@@ -171,9 +170,10 @@ export default class SlCheckbox extends LitElement {
|
||||
type="checkbox"
|
||||
name=${ifDefined(this.name)}
|
||||
value=${ifDefined(this.value)}
|
||||
?checked=${this.checked}
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
.indeterminate=${this.indeterminate}
|
||||
.checked=${this.checked}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
role="checkbox"
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
aria-labelledby=${this.labelId}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --grid-width: The width of the color grid.
|
||||
* @prop --grid-height: The height of the color grid.
|
||||
* @prop --grid-handle-size: The size of the color grid's handle.
|
||||
* @prop --slider-height: The height of the hue and alpha sliders.
|
||||
* @prop --slider-handle-size: The diameter of the slider's handle.
|
||||
*/
|
||||
:host {
|
||||
--grid-width: 260px;
|
||||
--grid-height: 200px;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./color-picker.scss';
|
||||
import { SlDropdown, SlInput } from '../../shoelace';
|
||||
import color from 'color';
|
||||
import { clamp } from '../../internal/math';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlInput from '../input/input';
|
||||
import color from 'color';
|
||||
import styles from 'sass:./color-picker.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -31,6 +32,12 @@ import { clamp } from '../../internal/math';
|
||||
* @part preview - The preview color.
|
||||
* @part input - The text input.
|
||||
* @part format-button - The toggle format button's base.
|
||||
*
|
||||
* @customProperty --grid-width - The width of the color grid.
|
||||
* @customProperty --grid-height - The height of the color grid.
|
||||
* @customProperty --grid-handle-size - The size of the color grid's handle.
|
||||
* @customProperty --slider-height - The height of the hue and alpha sliders.
|
||||
* @customProperty --slider-handle-size - The diameter of the slider's handle.
|
||||
*/
|
||||
@customElement('sl-color-picker')
|
||||
export default class SlColorPicker extends LitElement {
|
||||
@@ -40,7 +47,6 @@ export default class SlColorPicker extends LitElement {
|
||||
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('.color-dropdown') dropdown: SlDropdown;
|
||||
|
||||
private bypassValueParse = false;
|
||||
private lastValueEmitted: string;
|
||||
|
||||
@state() private inputValue = '';
|
||||
@@ -374,12 +380,6 @@ export default class SlColorPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
handleDropdownShow(event: CustomEvent) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleDropdownAfterHide() {
|
||||
this.showCopyFeedback = false;
|
||||
}
|
||||
@@ -551,38 +551,31 @@ export default class SlColorPicker extends LitElement {
|
||||
this.inputValue = this.opacity ? currentColor.hexa : currentColor.hex;
|
||||
}
|
||||
|
||||
// Setting this.value will trigger the watcher which parses the new value. We want to bypass that behavior because
|
||||
// we've already parsed the color here and conversion/rounding can lead to values changing slightly. After the next
|
||||
// update, the usual behavior is restored.
|
||||
this.bypassValueParse = true;
|
||||
this.value = this.inputValue;
|
||||
this.updateComplete.then(() => (this.bypassValueParse = false));
|
||||
}
|
||||
|
||||
@watch('format')
|
||||
@watch('format', { waitUntilFirstUpdate: true })
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
@watch('opacity')
|
||||
@watch('opacity', { waitUntilFirstUpdate: true })
|
||||
handleOpacityChange() {
|
||||
this.alpha = 100;
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange(oldValue: string, newValue: string) {
|
||||
if (!this.bypassValueParse) {
|
||||
const newColor = this.parseColor(newValue);
|
||||
const newColor = this.parseColor(newValue);
|
||||
|
||||
if (newColor) {
|
||||
this.inputValue = this.value;
|
||||
this.hue = newColor.hsla.h;
|
||||
this.saturation = newColor.hsla.s;
|
||||
this.lightness = newColor.hsla.l;
|
||||
this.alpha = newColor.hsla.a * 100;
|
||||
} else {
|
||||
this.inputValue = oldValue;
|
||||
}
|
||||
if (newColor) {
|
||||
this.inputValue = this.value;
|
||||
this.hue = newColor.hsla.h;
|
||||
this.saturation = newColor.hsla.s;
|
||||
this.lightness = newColor.hsla.l;
|
||||
this.alpha = newColor.hsla.a * 100;
|
||||
} else {
|
||||
this.inputValue = oldValue;
|
||||
}
|
||||
|
||||
if (this.value !== this.lastValueEmitted) {
|
||||
@@ -775,8 +768,8 @@ export default class SlColorPicker extends LitElement {
|
||||
class="color-dropdown"
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
.containing-element=${this}
|
||||
?disabled=${this.disabled}
|
||||
?hoist=${this.hoist}
|
||||
@sl-show=${this.handleDropdownShow}
|
||||
@sl-after-hide=${this.handleDropdownAfterHide}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --hide-duration: The length of the hide transition.
|
||||
* @prop --hide-timing-function: The timing function (easing) to use for the hide transition.
|
||||
* @prop --show-duration: The length of the show transition.
|
||||
* @prop --show-timing-function: The timing function (easing) to use for the show transition.
|
||||
*/
|
||||
:host {
|
||||
--hide-duration: var(--sl-transition-medium);
|
||||
--hide-timing-function: ease;
|
||||
--show-duration: var(--sl-transition-medium);
|
||||
--show-timing-function: ease;
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -69,16 +58,7 @@
|
||||
}
|
||||
|
||||
.details__body {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition-property: height;
|
||||
transition-duration: var(--hide-duration);
|
||||
transition-timing-function: var(--hide-timing-function);
|
||||
}
|
||||
|
||||
.details--open .details__body {
|
||||
transition-duration: var(--show-duration);
|
||||
transition-timing-function: var(--show-timing-function);
|
||||
}
|
||||
|
||||
.details__content {
|
||||
|
||||
152
src/components/details/details.test.ts
Normal file
152
src/components/details/details.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDetails from './details';
|
||||
|
||||
describe('<sl-details>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<sl-details open>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
|
||||
expect(body.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<sl-details>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
|
||||
expect(body.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-details>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</sl-details>
|
||||
`)) as SlDetails;
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(body.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-details open>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</sl-details>
|
||||
`)) as SlDetails;
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(body.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-details>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</sl-details>
|
||||
`)) as SlDetails;
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.open = true;
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(body.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-details open>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</sl-details>
|
||||
`)) as SlDetails;
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.open = false;
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(body.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should be the correct size after opening more than one instance', async () => {
|
||||
const el = await fixture(html`
|
||||
<div>
|
||||
<sl-details>
|
||||
<div style="height: 200px;"></div>
|
||||
</sl-details>
|
||||
<sl-details>
|
||||
<div style="height: 400px;"></div>
|
||||
</sl-details>
|
||||
</div>
|
||||
`);
|
||||
const first = el.querySelectorAll('sl-details')[0];
|
||||
const second = el.querySelectorAll('sl-details')[1];
|
||||
const firstBody = first.shadowRoot?.querySelector('.details__body');
|
||||
const secondBody = second.shadowRoot?.querySelector('.details__body');
|
||||
|
||||
await first.show();
|
||||
await second.show();
|
||||
|
||||
expect(firstBody!.clientHeight).to.equal(200);
|
||||
expect(secondBody!.clientHeight).to.equal(400);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./details.scss';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { focusVisible } from '../../internal/focus-visible';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./details.scss';
|
||||
|
||||
let id = 0;
|
||||
|
||||
@@ -21,6 +24,9 @@ let id = 0;
|
||||
* @part summary - The details summary.
|
||||
* @part summary-icon - The expand/collapse summary icon.
|
||||
* @part content - The details content.
|
||||
*
|
||||
* @animation details.show - The animation to use when showing details. You can use `height: auto` with this animation.
|
||||
* @animation details.hide - The animation to use when hiding details. You can use `height: auto` with this animation.
|
||||
*/
|
||||
@customElement('sl-details')
|
||||
export default class SlDetails extends LitElement {
|
||||
@@ -31,7 +37,6 @@ export default class SlDetails extends LitElement {
|
||||
@query('.details__body') body: HTMLElement;
|
||||
|
||||
private componentId = `details-${++id}`;
|
||||
private isVisible = false;
|
||||
|
||||
/** Indicates whether or not the details is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
@@ -42,13 +47,13 @@ export default class SlDetails extends LitElement {
|
||||
/** Disables the details so it can't be toggled. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Emitted when the details opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the details opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the details opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the details closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the details closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the details closes and all transitions are complete. */
|
||||
@@ -56,12 +61,10 @@ export default class SlDetails extends LitElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.isVisible = this.open;
|
||||
this.updateComplete.then(() => focusVisible.observe(this.details));
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
focusVisible.observe(this.details);
|
||||
|
||||
this.body.hidden = !this.open;
|
||||
this.body.style.height = this.open ? 'auto' : '0';
|
||||
}
|
||||
@@ -71,71 +74,24 @@ export default class SlDetails extends LitElement {
|
||||
focusVisible.unobserve(this.details);
|
||||
}
|
||||
|
||||
/** Shows the alert. */
|
||||
show() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (this.isVisible || this.disabled) {
|
||||
/** Shows the details. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.body.hidden = false;
|
||||
|
||||
if (this.body.scrollHeight === 0) {
|
||||
// When the scroll height can't be measured, use auto. This prevents a borked open state when the details is open
|
||||
// intitially, but not immediately visible (i.e. in a tab panel).
|
||||
this.body.style.height = 'auto';
|
||||
this.body.style.overflow = 'visible';
|
||||
} else {
|
||||
this.body.style.height = `${this.body.scrollHeight}px`;
|
||||
this.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
this.isVisible = true;
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the alert */
|
||||
hide() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.isVisible || this.disabled) {
|
||||
/** Hides the details */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't transition out of `height: auto`, so let's set it to the current height first
|
||||
this.body.style.height = `${this.body.scrollHeight}px`;
|
||||
this.body.style.overflow = 'hidden';
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.body.clientWidth; // force a reflow
|
||||
this.body.style.height = '0';
|
||||
});
|
||||
|
||||
this.isVisible = false;
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
handleBodyTransitionEnd(event: TransitionEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Ensure we only emit one event when the target element is no longer visible
|
||||
if (event.propertyName === 'height' && target.classList.contains('details__body')) {
|
||||
this.body.style.overflow = this.open ? 'visible' : 'hidden';
|
||||
this.body.style.height = this.open ? 'auto' : '0';
|
||||
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
|
||||
this.body.hidden = !this.open;
|
||||
}
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleSummaryClick() {
|
||||
@@ -162,9 +118,33 @@ export default class SlDetails extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
|
||||
await stopAnimations(this);
|
||||
this.body.hidden = false;
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'details.show');
|
||||
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
|
||||
await stopAnimations(this);
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'details.hide');
|
||||
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
|
||||
this.body.hidden = true;
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -198,7 +178,7 @@ export default class SlDetails extends LitElement {
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="details__body" @transitionend=${this.handleBodyTransitionEnd}>
|
||||
<div class="details__body">
|
||||
<div
|
||||
part="content"
|
||||
id=${`${this.componentId}-content`}
|
||||
@@ -214,6 +194,22 @@ export default class SlDetails extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('details.show', {
|
||||
keyframes: [
|
||||
{ height: '0', opacity: '0' },
|
||||
{ height: 'auto', opacity: '1' }
|
||||
],
|
||||
options: { duration: 250, easing: 'linear' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('details.hide', {
|
||||
keyframes: [
|
||||
{ height: 'auto', opacity: '1' },
|
||||
{ height: '0', opacity: '0' }
|
||||
],
|
||||
options: { duration: 250, easing: 'linear' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-details': SlDetails;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
@use '../../styles/component';
|
||||
@use '../../styles/mixins/hide';
|
||||
|
||||
/**
|
||||
* @prop --width: The preferred width of the dialog. Note that the dialog will shrink to accommodate smaller screens.
|
||||
* @prop --header-spacing: The amount of padding to use for the header.
|
||||
* @prop --body-spacing: The amount of padding to use for the body.
|
||||
* @prop --footer-spacing: The amount of padding to use for the footer.
|
||||
*/
|
||||
:host {
|
||||
--width: 31rem;
|
||||
--header-spacing: var(--sl-spacing-large);
|
||||
@@ -26,10 +20,6 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--sl-z-index-dialog);
|
||||
|
||||
&:not(.dialog--visible) {
|
||||
@include hide.hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog__panel {
|
||||
@@ -42,9 +32,6 @@
|
||||
background-color: var(--sl-panel-background-color);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--sl-shadow-x-large);
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transition: var(--sl-transition-medium) opacity, var(--sl-transition-medium) transform;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
@@ -112,10 +99,4 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--sl-overlay-background-color);
|
||||
opacity: 0;
|
||||
transition: var(--sl-transition-medium) opacity;
|
||||
}
|
||||
|
||||
.dialog--open .dialog__overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
132
src/components/dialog/dialog.test.ts
Normal file
132
src/components/dialog/dialog.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDialog from './dialog';
|
||||
|
||||
describe('<sl-dialog>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture(html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`)) as SlDialog;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`)) as SlDialog;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`)) as SlDialog;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.open = true;
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`)) as SlDialog;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.open = false;
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should not close when sl-overlay-dismiss is prevented', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`)) as SlDialog;
|
||||
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement;
|
||||
|
||||
el.addEventListener('sl-overlay-dismiss', event => event.preventDefault());
|
||||
overlay.click();
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
|
||||
it('should allow initial focus to be set', async () => {
|
||||
const el = (await fixture(html` <sl-dialog><input /></sl-dialog> `)) as SlDialog;
|
||||
const input = el.querySelector('input');
|
||||
const initialFocusHandler = sinon.spy(event => {
|
||||
event.preventDefault();
|
||||
input.focus();
|
||||
});
|
||||
|
||||
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => initialFocusHandler.calledOnce);
|
||||
|
||||
expect(initialFocusHandler).to.have.been.calledOnce;
|
||||
expect(document.activeElement).to.equal(input);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,15 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
import Modal from '../../internal/modal';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./dialog.scss';
|
||||
|
||||
const hasPreventScroll = isPreventScrollSupported();
|
||||
@@ -31,6 +34,16 @@ let id = 0;
|
||||
* @part close-button - The close button.
|
||||
* @part body - The dialog body.
|
||||
* @part footer - The dialog footer.
|
||||
*
|
||||
* @customProperty --width - The preferred width of the dialog. Note that the dialog will shrink to accommodate smaller screens.
|
||||
* @customProperty --header-spacing - The amount of padding to use for the header.
|
||||
* @customProperty --body-spacing - The amount of padding to use for the body.
|
||||
* @customProperty --footer-spacing - The amount of padding to use for the footer.
|
||||
*
|
||||
* @animation dialog.show - The animation to use when showing the dialog.
|
||||
* @animation dialog.hide - The animation to use when hiding the dialog.
|
||||
* @animation dialog.overlay.show - The animation to use when showing the dialog's overlay.
|
||||
* @animation dialog.overlay.hide - The animation to use when hiding the dialog's overlay.
|
||||
*/
|
||||
@customElement('sl-dialog')
|
||||
export default class SlDialog extends LitElement {
|
||||
@@ -38,15 +51,13 @@ export default class SlDialog extends LitElement {
|
||||
|
||||
@query('.dialog') dialog: HTMLElement;
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
@query('.dialog__overlay') overlay: HTMLElement;
|
||||
|
||||
private componentId = `dialog-${++id}`;
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
private willShow = false;
|
||||
private willHide = false;
|
||||
|
||||
@state() private hasFooter = false;
|
||||
@state() private isVisible = false;
|
||||
|
||||
/** Indicates whether or not the dialog is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
@@ -63,13 +74,13 @@ export default class SlDialog extends LitElement {
|
||||
*/
|
||||
@property({ attribute: 'no-header', type: Boolean, reflect: true }) noHeader = false;
|
||||
|
||||
/** Emitted when the dialog opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the dialog opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dialog opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the dialog closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the dialog closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dialog closes and all transitions are complete. */
|
||||
@@ -89,11 +100,10 @@ export default class SlDialog extends LitElement {
|
||||
|
||||
this.modal = new Modal(this);
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
// Show on init if open
|
||||
if (this.open) {
|
||||
this.show();
|
||||
}
|
||||
firstUpdated() {
|
||||
this.dialog.hidden = !this.open;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -101,80 +111,24 @@ export default class SlDialog extends LitElement {
|
||||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the dialog */
|
||||
show() {
|
||||
if (this.willShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.willShow = true;
|
||||
this.isVisible = true;
|
||||
this.open = true;
|
||||
this.modal.activate();
|
||||
|
||||
lockBodyScrolling(this);
|
||||
|
||||
/** Shows the dialog. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
if (hasPreventScroll) {
|
||||
// Wait for the next frame before setting initial focus so the dialog is technically visible
|
||||
requestAnimationFrame(() => {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Once Safari supports { preventScroll: true } we can remove this nasty little hack, but until then we need to
|
||||
// wait for the transition to complete before setting focus, otherwise the panel may render in a buggy way
|
||||
// that's out of view initially.
|
||||
//
|
||||
// Fiddle: https://jsfiddle.net/g6buoafq/1/
|
||||
// Safari: https://bugs.webkit.org/show_bug.cgi?id=178583
|
||||
//
|
||||
this.dialog.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus();
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dialog */
|
||||
hide() {
|
||||
if (this.willHide) {
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.willHide = true;
|
||||
this.open = false;
|
||||
this.modal.deactivate();
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
unlockBodyScrolling(this);
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
@@ -188,13 +142,72 @@ export default class SlDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.modal.activate();
|
||||
|
||||
lockBodyScrolling(this);
|
||||
|
||||
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
|
||||
this.dialog.hidden = false;
|
||||
|
||||
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
|
||||
if (hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit({ cancelable: true });
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
const panelAnimation = getAnimation(this, 'dialog.show');
|
||||
const overlayAnimation = getAnimation(this, 'dialog.overlay.show');
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
]);
|
||||
|
||||
// Browsers that don't support el.focus({ preventScroll }) have to wait for the animation to finish before initial
|
||||
// focus to prevent scrolling issues. See: https://caniuse.com/mdn-api_htmlelement_focus_preventscroll_option
|
||||
if (!hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit({ cancelable: true });
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
this.modal.deactivate();
|
||||
|
||||
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
|
||||
const panelAnimation = getAnimation(this, 'dialog.hide');
|
||||
const overlayAnimation = getAnimation(this, 'dialog.overlay.hide');
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
]);
|
||||
this.dialog.hidden = true;
|
||||
|
||||
unlockBodyScrolling(this);
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleOverlayClick() {
|
||||
const slOverlayDismiss = this.slOverlayDismiss.emit();
|
||||
const slOverlayDismiss = this.slOverlayDismiss.emit({ cancelable: true });
|
||||
if (!slOverlayDismiss.defaultPrevented) {
|
||||
this.hide();
|
||||
}
|
||||
@@ -204,18 +217,6 @@ export default class SlDialog extends LitElement {
|
||||
this.hasFooter = hasSlot(this, 'footer');
|
||||
}
|
||||
|
||||
handleTransitionEnd(event: TransitionEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Ensure we only emit one event when the target element is no longer visible
|
||||
if (event.propertyName === 'opacity' && target.classList.contains('dialog__panel')) {
|
||||
this.isVisible = this.open;
|
||||
this.willShow = false;
|
||||
this.willHide = false;
|
||||
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -223,11 +224,9 @@ export default class SlDialog extends LitElement {
|
||||
class=${classMap({
|
||||
dialog: true,
|
||||
'dialog--open': this.open,
|
||||
'dialog--visible': this.isVisible,
|
||||
'dialog--has-footer': this.hasFooter
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@transitionend=${this.handleTransitionEnd}
|
||||
>
|
||||
<div part="overlay" class="dialog__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div>
|
||||
|
||||
@@ -271,6 +270,32 @@ export default class SlDialog extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('dialog.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.8)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dialog.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.8)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dialog.overlay.show', {
|
||||
keyframes: [{ opacity: 0 }, { opacity: 1 }],
|
||||
options: { duration: 250 }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dialog.overlay.hide', {
|
||||
keyframes: [{ opacity: 1 }, { opacity: 0 }],
|
||||
options: { duration: 250 }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-dialog': SlDialog;
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
@use '../../styles/component';
|
||||
@use '../../styles/mixins/hide';
|
||||
|
||||
/**
|
||||
* @prop --size: The preferred size of the drawer. This will be applied to the drawer's width or height depending on its
|
||||
* `placement`. Note that the drawer will shrink to accommodate smaller screens.
|
||||
* @prop --header-spacing: The amount of padding to use for the header.
|
||||
* @prop --body-spacing: The amount of padding to use for the body.
|
||||
* @prop --footer-spacing: The amount of padding to use for the footer.
|
||||
*
|
||||
*/
|
||||
:host {
|
||||
--size: 25rem;
|
||||
--header-spacing: var(--sl-spacing-large);
|
||||
@@ -25,10 +17,6 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
|
||||
&:not(.drawer--visible) {
|
||||
@include hide.hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer--contained {
|
||||
@@ -66,17 +54,15 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--size);
|
||||
transform: translate(0, -100%);
|
||||
}
|
||||
|
||||
.drawer--right .drawer__panel {
|
||||
.drawer--end .drawer__panel {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
width: var(--size);
|
||||
height: 100%;
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
|
||||
.drawer--bottom .drawer__panel {
|
||||
@@ -86,21 +72,15 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--size);
|
||||
transform: translate(0, 100%);
|
||||
}
|
||||
|
||||
.drawer--left .drawer__panel {
|
||||
.drawer--start .drawer__panel {
|
||||
top: 0;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
width: var(--size);
|
||||
height: 100%;
|
||||
transform: translate(-100%, 0);
|
||||
}
|
||||
|
||||
.drawer--open .drawer__panel {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.drawer__header {
|
||||
@@ -150,15 +130,9 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--sl-overlay-background-color);
|
||||
opacity: 0;
|
||||
transition: var(--sl-transition-medium) opacity;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.drawer--contained .drawer__overlay {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.drawer--open .drawer__overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
132
src/components/drawer/drawer.test.ts
Normal file
132
src/components/drawer/drawer.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDrawer from './drawer';
|
||||
|
||||
describe('<sl-drawer>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture(html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`)) as SlDrawer;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`)) as SlDrawer;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`)) as SlDrawer;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.open = true;
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`)) as SlDrawer;
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.open = false;
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should not close when sl-overlay-dismiss is prevented', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`)) as SlDrawer;
|
||||
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement;
|
||||
|
||||
el.addEventListener('sl-overlay-dismiss', event => event.preventDefault());
|
||||
overlay.click();
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
|
||||
it('should allow initial focus to be set', async () => {
|
||||
const el = (await fixture(html` <sl-drawer><input /></sl-drawer> `)) as SlDrawer;
|
||||
const input = el.querySelector('input');
|
||||
const initialFocusHandler = sinon.spy(event => {
|
||||
event.preventDefault();
|
||||
input.focus();
|
||||
});
|
||||
|
||||
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => initialFocusHandler.calledOnce);
|
||||
|
||||
expect(initialFocusHandler).to.have.been.calledOnce;
|
||||
expect(document.activeElement).to.equal(input);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,16 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { uppercaseFirstLetter } from '../../internal/string';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
import Modal from '../../internal/modal';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./drawer.scss';
|
||||
|
||||
const hasPreventScroll = isPreventScrollSupported();
|
||||
@@ -31,6 +35,23 @@ let id = 0;
|
||||
* @part close-button - The close button.
|
||||
* @part body - The drawer body.
|
||||
* @part footer - The drawer footer.
|
||||
*
|
||||
* @customProperty --size - The preferred size of the drawer. This will be applied to the drawer's width or height
|
||||
* depending on its `placement`. Note that the drawer will shrink to accommodate smaller screens.
|
||||
* @customProperty --header-spacing - The amount of padding to use for the header.
|
||||
* @customProperty --body-spacing - The amount of padding to use for the body.
|
||||
* @customProperty --footer-spacing - The amount of padding to use for the footer.
|
||||
*
|
||||
* @animation drawer.showTop - The animation to use when showing a drawer with `top` placement.
|
||||
* @animation drawer.showEnd - The animation to use when showing a drawer with `end` placement.
|
||||
* @animation drawer.showBottom - The animation to use when showing a drawer with `bottom` placement.
|
||||
* @animation drawer.showStart - The animation to use when showing a drawer with `start` placement.
|
||||
* @animation drawer.hideTop - The animation to use when hiding a drawer with `top` placement.
|
||||
* @animation drawer.hideEnd - The animation to use when hiding a drawer with `end` placement.
|
||||
* @animation drawer.hideBottom - The animation to use when hiding a drawer with `bottom` placement.
|
||||
* @animation drawer.hideStart - The animation to use when hiding a drawer with `start` placement.
|
||||
* @animation drawer.overlay.show - The animation to use when showing the drawer's overlay.
|
||||
* @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay.
|
||||
*/
|
||||
@customElement('sl-drawer')
|
||||
export default class SlDrawer extends LitElement {
|
||||
@@ -38,15 +59,13 @@ export default class SlDrawer extends LitElement {
|
||||
|
||||
@query('.drawer') drawer: HTMLElement;
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
@query('.drawer__overlay') overlay: HTMLElement;
|
||||
|
||||
private componentId = `drawer-${++id}`;
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
private willShow = false;
|
||||
private willHide = false;
|
||||
|
||||
@state() private hasFooter = false;
|
||||
@state() private isVisible = false;
|
||||
|
||||
/** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
@@ -58,7 +77,7 @@ export default class SlDrawer extends LitElement {
|
||||
@property({ reflect: true }) label = '';
|
||||
|
||||
/** The direction from which the drawer will open. */
|
||||
@property({ reflect: true }) placement: 'top' | 'right' | 'bottom' | 'left' = 'right';
|
||||
@property({ reflect: true }) placement: 'top' | 'end' | 'bottom' | 'start' = 'end';
|
||||
|
||||
/**
|
||||
* By default, the drawer slides out of its containing block (usually the viewport). To make the drawer slide out of
|
||||
@@ -72,13 +91,13 @@ export default class SlDrawer extends LitElement {
|
||||
*/
|
||||
@property({ attribute: 'no-header', type: Boolean, reflect: true }) noHeader = false;
|
||||
|
||||
/** Emitted when the drawer opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the drawer opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the drawer opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the drawer closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the drawer closes and all transitions are complete. */
|
||||
@@ -95,11 +114,10 @@ export default class SlDrawer extends LitElement {
|
||||
|
||||
this.modal = new Modal(this);
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
// Show on init if open
|
||||
if (this.open) {
|
||||
this.show();
|
||||
}
|
||||
firstUpdated() {
|
||||
this.drawer.hidden = !this.open;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -107,83 +125,24 @@ export default class SlDrawer extends LitElement {
|
||||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the drawer */
|
||||
show() {
|
||||
if (this.willShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.willShow = true;
|
||||
this.isVisible = true;
|
||||
this.open = true;
|
||||
|
||||
// Lock body scrolling only if the drawer isn't contained
|
||||
if (!this.contained) {
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the drawer. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
if (hasPreventScroll) {
|
||||
// Wait for the next frame before setting initial focus so the drawer is technically visible
|
||||
requestAnimationFrame(() => {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Once Safari supports { preventScroll: true } we can remove this nasty little hack, but until then we need to
|
||||
// wait for the transition to complete before setting focus, otherwise the panel may render in a buggy way its
|
||||
// out of view initially.
|
||||
//
|
||||
// Fiddle: https://jsfiddle.net/g6buoafq/1/
|
||||
// Safari: https://bugs.webkit.org/show_bug.cgi?id=178583
|
||||
//
|
||||
this.drawer.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus();
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the drawer */
|
||||
hide() {
|
||||
if (this.willHide) {
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.willHide = true;
|
||||
this.open = false;
|
||||
this.modal.deactivate();
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
unlockBodyScrolling(this);
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
@@ -197,13 +156,75 @@ export default class SlDrawer extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
|
||||
// Lock body scrolling only if the drawer isn't contained
|
||||
if (!this.contained) {
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
}
|
||||
|
||||
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
|
||||
this.drawer.hidden = false;
|
||||
|
||||
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
|
||||
if (hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit({ cancelable: true });
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`);
|
||||
const overlayAnimation = getAnimation(this, 'drawer.overlay.show');
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
]);
|
||||
|
||||
// Browsers that don't support el.focus({ preventScroll }) have to wait for the animation to finish before initial
|
||||
// focus to prevent scrolling issues. See: https://caniuse.com/mdn-api_htmlelement_focus_preventscroll_option
|
||||
if (!hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit({ cancelable: true });
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
this.modal.deactivate();
|
||||
unlockBodyScrolling(this);
|
||||
|
||||
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
|
||||
const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`);
|
||||
const overlayAnimation = getAnimation(this, 'drawer.overlay.hide');
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
]);
|
||||
|
||||
this.drawer.hidden = true;
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleOverlayClick() {
|
||||
const slOverlayDismiss = this.slOverlayDismiss.emit();
|
||||
const slOverlayDismiss = this.slOverlayDismiss.emit({ cancelable: true });
|
||||
if (!slOverlayDismiss.defaultPrevented) {
|
||||
this.hide();
|
||||
}
|
||||
@@ -213,18 +234,6 @@ export default class SlDrawer extends LitElement {
|
||||
this.hasFooter = hasSlot(this, 'footer');
|
||||
}
|
||||
|
||||
handleTransitionEnd(event: TransitionEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Ensure we only emit one event when the target element is no longer visible
|
||||
if (event.propertyName === 'transform' && target.classList.contains('drawer__panel')) {
|
||||
this.isVisible = this.open;
|
||||
this.willShow = false;
|
||||
this.willHide = false;
|
||||
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -232,17 +241,15 @@ export default class SlDrawer extends LitElement {
|
||||
class=${classMap({
|
||||
drawer: true,
|
||||
'drawer--open': this.open,
|
||||
'drawer--visible': this.isVisible,
|
||||
'drawer--top': this.placement === 'top',
|
||||
'drawer--right': this.placement === 'right',
|
||||
'drawer--end': this.placement === 'end',
|
||||
'drawer--bottom': this.placement === 'bottom',
|
||||
'drawer--left': this.placement === 'left',
|
||||
'drawer--start': this.placement === 'start',
|
||||
'drawer--contained': this.contained,
|
||||
'drawer--fixed': !this.contained,
|
||||
'drawer--has-footer': this.hasFooter
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@transitionend=${this.handleTransitionEnd}
|
||||
>
|
||||
<div part="overlay" class="drawer__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div>
|
||||
|
||||
@@ -287,6 +294,85 @@ export default class SlDrawer extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Top
|
||||
setDefaultAnimation('drawer.showTop', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateY(-100%)' },
|
||||
{ opacity: 1, transform: 'translateY(0)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideTop', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateY(0)' },
|
||||
{ opacity: 0, transform: 'translateY(-100%)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
// End
|
||||
setDefaultAnimation('drawer.showEnd', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateX(100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideEnd', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(100%)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
// Bottom
|
||||
setDefaultAnimation('drawer.showBottom', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateY(100%)' },
|
||||
{ opacity: 1, transform: 'translateY(0)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideBottom', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateY(0)' },
|
||||
{ opacity: 0, transform: 'translateY(100%)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
// Start
|
||||
setDefaultAnimation('drawer.showStart', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateX(-100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideStart', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(-100%)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
// Overlay
|
||||
setDefaultAnimation('drawer.overlay.show', {
|
||||
keyframes: [{ opacity: 0 }, { opacity: 1 }],
|
||||
options: { duration: 250 }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.overlay.hide', {
|
||||
keyframes: [{ opacity: 1 }, { opacity: 0 }],
|
||||
options: { duration: 250 }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-drawer': SlDrawer;
|
||||
|
||||
@@ -27,12 +27,13 @@
|
||||
border: solid 1px var(--sl-panel-border-color);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--sl-shadow-large);
|
||||
opacity: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: none;
|
||||
pointer-events: none;
|
||||
transform: scale(0.9);
|
||||
transition: var(--sl-transition-fast) opacity, var(--sl-transition-fast) transform;
|
||||
}
|
||||
|
||||
.dropdown--open .dropdown__panel {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.dropdown__positioner {
|
||||
@@ -51,10 +52,4 @@
|
||||
&[data-popper-placement^='right'] .dropdown__panel {
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
&.popover-visible .dropdown__panel {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
147
src/components/dropdown/dropdown.test.ts
Normal file
147
src/components/dropdown/dropdown.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDropdown from './dropdown';
|
||||
|
||||
describe('<sl-dropdown>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<sl-dropdown open>
|
||||
<sl-button slot="trigger" caret>Toggle</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Toggle</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Toggle</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`)) as SlDropdown;
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dropdown open>
|
||||
<sl-button slot="trigger" caret>Toggle</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`)) as SlDropdown;
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Toggle</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`)) as SlDropdown;
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.open = true;
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
|
||||
expect(showHandler).to.have.been.calledOnce;
|
||||
expect(afterShowHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-dropdown open>
|
||||
<sl-button slot="trigger" caret>Toggle</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`)) as SlDropdown;
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.open = false;
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
|
||||
expect(hideHandler).to.have.been.calledOnce;
|
||||
expect(afterHideHandler).to.have.been.calledOnce;
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,16 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./dropdown.scss';
|
||||
import { SlMenu, SlMenuItem } from '../../shoelace';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { scrollIntoView } from '../../internal/scroll';
|
||||
import { getNearestTabbableElement } from '../../internal/tabbable';
|
||||
import Popover from '../../internal/popover';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from 'sass:./dropdown.scss';
|
||||
|
||||
let id = 0;
|
||||
|
||||
@@ -20,6 +24,9 @@ let id = 0;
|
||||
* @part base - The component's base wrapper.
|
||||
* @part trigger - The container that wraps the trigger.
|
||||
* @part panel - The panel that gets shown when the dropdown is open.
|
||||
*
|
||||
* @animation dropdown.show - The animation to use when showing the dropdown.
|
||||
* @animation dropdown.hide - The animation to use when hiding the dropdown.
|
||||
*/
|
||||
@customElement('sl-dropdown')
|
||||
export default class SlDropdown extends LitElement {
|
||||
@@ -30,8 +37,7 @@ export default class SlDropdown extends LitElement {
|
||||
@query('.dropdown__positioner') positioner: HTMLElement;
|
||||
|
||||
private componentId = `dropdown-${++id}`;
|
||||
private isVisible = false;
|
||||
private popover: Popover;
|
||||
private popover: PopperInstance;
|
||||
|
||||
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
@@ -54,6 +60,9 @@ export default class SlDropdown extends LitElement {
|
||||
| 'left-start'
|
||||
| 'left-end' = 'bottom-start';
|
||||
|
||||
/** Disables the dropdown so the panel will not open. */
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/** Determines whether the dropdown should hide when a menu item is selected. */
|
||||
@property({ attribute: 'close-on-select', type: Boolean, reflect: true }) closeOnSelect = true;
|
||||
|
||||
@@ -72,16 +81,16 @@ export default class SlDropdown extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
/** Emitted when the dropdown opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the dropdown opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dropdown opens and all transitions are complete. */
|
||||
/** Emitted after the dropdown opens and all animations are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the dropdown closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the dropdown closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dropdown closes and all transitions are complete. */
|
||||
/** Emitted after the dropdown closes and all animations are complete. */
|
||||
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
|
||||
|
||||
connectedCallback() {
|
||||
@@ -94,28 +103,32 @@ 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() {
|
||||
this.popover = new Popover(this.trigger, this.positioner, {
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
placement: this.placement,
|
||||
distance: this.distance,
|
||||
skidding: this.skidding,
|
||||
transitionElement: this.panel,
|
||||
onAfterHide: () => this.slAfterHide.emit(),
|
||||
onAfterShow: () => this.slAfterShow.emit(),
|
||||
onTransitionEnd: () => {
|
||||
if (!this.open) {
|
||||
this.panel.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show on init if open
|
||||
if (this.open) {
|
||||
this.show();
|
||||
}
|
||||
this.panel.hidden = !this.open;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -204,10 +217,22 @@ export default class SlDropdown extends LitElement {
|
||||
handlePopoverOptionsChange() {
|
||||
if (this.popover) {
|
||||
this.popover.setOptions({
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
placement: this.placement,
|
||||
distance: this.distance,
|
||||
skidding: this.skidding
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -302,50 +327,24 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the dropdown panel */
|
||||
show() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (this.isVisible) {
|
||||
/** Shows the dropdown panel. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.panel.addEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.addEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
this.isVisible = true;
|
||||
this.open = true;
|
||||
this.popover.show();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dropdown panel */
|
||||
hide() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.isVisible) {
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.panel.removeEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.removeEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
this.isVisible = false;
|
||||
this.open = false;
|
||||
this.popover.hide();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,14 +356,47 @@ export default class SlDropdown extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.popover.reposition();
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateAccessibleTrigger();
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
this.panel.addEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.addEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.popover.update();
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
this.panel.removeEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.removeEventListener('sl-select', this.handlePanelSelect);
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.hide');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
this.panel.hidden = true;
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -387,7 +419,7 @@ export default class SlDropdown extends LitElement {
|
||||
<slot name="trigger" @slotchange=${this.handleTriggerSlotChange}></slot>
|
||||
</span>
|
||||
|
||||
<!-- Position the panel with a wrapper since the popover makes use of translate. This let's us add transitions
|
||||
<!-- Position the panel with a wrapper since the popover makes use of translate. This let's us add animations
|
||||
on the panel without interfering with the position. -->
|
||||
<div class="dropdown__positioner">
|
||||
<div
|
||||
@@ -405,6 +437,22 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('dropdown.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.9)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
options: { duration: 150, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dropdown.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.9)' }
|
||||
],
|
||||
options: { duration: 150, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-dropdown': SlDropdown;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { event, EventEmitter } from '../../internal/decorators';
|
||||
import type SlButton from '../button/button';
|
||||
import type SlCheckbox from '../checkbox/checkbox';
|
||||
import type SlColorPicker from '../color-picker/color-picker';
|
||||
import type SlInput from '../input/input';
|
||||
import type SlRadio from '../radio/radio';
|
||||
import type SlRange from '../range/range';
|
||||
import type SlSelect from '../select/select';
|
||||
import type SlSwitch from '../switch/switch';
|
||||
import type SlTextarea from '../textarea/textarea';
|
||||
import styles from 'sass:./form.scss';
|
||||
import {
|
||||
SlButton,
|
||||
SlCheckbox,
|
||||
SlColorPicker,
|
||||
SlInput,
|
||||
SlRadio,
|
||||
SlRange,
|
||||
SlSelect,
|
||||
SlSwitch,
|
||||
SlTextarea
|
||||
} from '../../shoelace';
|
||||
|
||||
interface FormControl {
|
||||
tag: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { formatBytes } from '../../internal/number';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import styles from 'sass:./icon-button.scss';
|
||||
import { focusVisible } from '../../internal/focus-visible';
|
||||
import styles from 'sass:./icon-button.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -37,8 +37,9 @@ export default class SlIconButton extends LitElement {
|
||||
/** Disables the button. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
firstUpdated() {
|
||||
focusVisible.observe(this.button);
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => focusVisible.observe(this.button));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { unsafeSVG } from 'lit-html/directives/unsafe-svg';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./icon.scss';
|
||||
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
|
||||
import { requestIcon } from './request';
|
||||
import styles from 'sass:./icon.scss';
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SlIcon } from '../../shoelace';
|
||||
import defaultLibrary from './library.default';
|
||||
import systemLibrary from './library.system';
|
||||
import type SlIcon from '../icon/icon';
|
||||
|
||||
export type IconLibraryResolver = (name: string) => string;
|
||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --divider-width: The width of the dividing line.
|
||||
* @prop --handle-size: The size of the compare handle.
|
||||
*/
|
||||
:host {
|
||||
--divider-width: 2px;
|
||||
--handle-size: 2.5rem;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./image-comparer.scss';
|
||||
import { clamp } from '../../internal/math';
|
||||
import styles from 'sass:./image-comparer.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -20,6 +20,9 @@ import { clamp } from '../../internal/math';
|
||||
* @part after - The container that holds the "after" image.
|
||||
* @part divider - The divider that separates the images.
|
||||
* @part handle - The handle that the user drags to expose the after image.
|
||||
*
|
||||
* @customProperty --divider-width - The width of the dividing line.
|
||||
* @customProperty --handle-size - The size of the compare handle.
|
||||
*/
|
||||
@customElement('sl-image-comparer')
|
||||
export default class SlImageComparer extends LitElement {
|
||||
@@ -89,7 +92,7 @@ export default class SlImageComparer extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('position')
|
||||
@watch('position', { waitUntilFirstUpdate: true })
|
||||
handlePositionChange() {
|
||||
this.slChange.emit();
|
||||
}
|
||||
|
||||
28
src/components/include/include.test.ts
Normal file
28
src/components/include/include.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlInclude from './include';
|
||||
|
||||
describe('<sl-include>', () => {
|
||||
it('should load content and emit sl-load', async () => {
|
||||
const el = await fixture(html` <sl-include src="https://jsonplaceholder.typicode.com/posts/1"></sl-include> `);
|
||||
const loadHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-load', loadHandler);
|
||||
await waitUntil(() => loadHandler.calledOnce);
|
||||
|
||||
expect(el.innerHTML).to.contain('"id": 1');
|
||||
expect(loadHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-error when content cannot be loaded', async () => {
|
||||
const el = await fixture(html` <sl-include src="https://404"></sl-include> `);
|
||||
const loadHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-error', loadHandler);
|
||||
await waitUntil(() => loadHandler.calledOnce);
|
||||
|
||||
expect(loadHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./include.scss';
|
||||
import { requestInclude } from './request';
|
||||
import styles from 'sass:./include.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -30,11 +30,6 @@ export default class SlInclude extends LitElement {
|
||||
/** Emitted when the included file fails to load due to an error. */
|
||||
@event('sl-error') slError: EventEmitter<{ status: number }>;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadSource();
|
||||
}
|
||||
|
||||
executeScript(script: HTMLScriptElement) {
|
||||
// Create a copy of the script and swap it out so the browser executes it
|
||||
const newScript = document.createElement('script');
|
||||
@@ -44,7 +39,7 @@ export default class SlInclude extends LitElement {
|
||||
}
|
||||
|
||||
@watch('src')
|
||||
async loadSource() {
|
||||
async handleSrcChange() {
|
||||
try {
|
||||
const src = this.src;
|
||||
const file = await requestInclude(src, this.mode);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
@use '../../styles/component';
|
||||
@use '../../styles/form-control';
|
||||
|
||||
/**
|
||||
* @prop --focus-ring: The focus ring style to use when the control receives focus, a `box-shadow` property.
|
||||
*/
|
||||
:host {
|
||||
--focus-ring: 0 0 0 var(--sl-focus-ring-width) var(--sl-focus-ring-color-primary);
|
||||
display: block;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./input.scss';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from 'sass:./input.scss';
|
||||
|
||||
let id = 0;
|
||||
|
||||
@@ -32,6 +32,8 @@ let id = 0;
|
||||
* @part password-toggle-button - The password toggle button.
|
||||
* @part suffix - The input suffix container.
|
||||
* @part help-text - The input help text.
|
||||
*
|
||||
* @customProperty --focus-ring - The focus ring style to use when the control receives focus, a `box-shadow` property.
|
||||
*/
|
||||
@customElement('sl-input')
|
||||
export default class SlInput extends LitElement {
|
||||
@@ -147,12 +149,11 @@ export default class SlInput extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.handleSlotChange();
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -258,10 +259,13 @@ export default class SlInput extends LitElement {
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
if (this.input) {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// NOTE - always bind value after min/max, otherwise it will be clamped
|
||||
return renderFormControl(
|
||||
{
|
||||
inputId: this.inputId,
|
||||
@@ -302,7 +306,6 @@ export default class SlInput extends LitElement {
|
||||
class="input__control"
|
||||
type=${this.type === 'password' && this.isPasswordVisible ? 'text' : this.type}
|
||||
name=${ifDefined(this.name)}
|
||||
.value=${this.value}
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
?required=${this.required}
|
||||
@@ -312,6 +315,7 @@ export default class SlInput extends LitElement {
|
||||
min=${ifDefined(this.min)}
|
||||
max=${ifDefined(this.max)}
|
||||
step=${ifDefined(this.step)}
|
||||
.value=${this.value}
|
||||
autocapitalize=${ifDefined(this.autocapitalize)}
|
||||
autocomplete=${ifDefined(this.autocomplete)}
|
||||
autocorrect=${ifDefined(this.autocorrect)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import styles from 'sass:./menu-divider.scss';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import styles from 'sass:./menu-item.scss';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import styles from 'sass:./menu-label.scss';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, query } from 'lit/decorators';
|
||||
import { customElement, query } from 'lit/decorators.js';
|
||||
import { event, EventEmitter } from '../../internal/decorators';
|
||||
import styles from 'sass:./menu.scss';
|
||||
import { SlMenuItem } from '../../shoelace';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from 'sass:./menu.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -49,7 +49,7 @@ export default class SlMenu extends LitElement {
|
||||
|
||||
syncItems() {
|
||||
this.items = [...this.defaultSlot.assignedElements({ flatten: true })].filter(
|
||||
(el: any) => el instanceof SlMenuItem && !el.disabled
|
||||
(el: any) => el.tagName.toLowerCase() === 'sl-menu-item' && !el.disabled
|
||||
) as [SlMenuItem];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --height: The progress bar's height.
|
||||
* @prop --track-color: The track color.
|
||||
* @prop --indicator-color: The indicator color.
|
||||
* @prop --label-color: The label color.
|
||||
*/
|
||||
:host {
|
||||
--height: 16px;
|
||||
--track-color: var(--sl-color-gray-200);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
import styles from 'sass:./progress-bar.scss';
|
||||
@@ -13,6 +13,11 @@ import styles from 'sass:./progress-bar.scss';
|
||||
* @part base - The component's base wrapper.
|
||||
* @part indicator - The progress bar indicator.
|
||||
* @part label - The progress bar label.
|
||||
*
|
||||
* @customProperty --height - The progress bar's height.
|
||||
* @customProperty --track-color - The track color.
|
||||
* @customProperty --indicator-color - The indicator color.
|
||||
* @customProperty --label-color - The label color.
|
||||
*/
|
||||
@customElement('sl-progress-bar')
|
||||
export default class SlProgressBar extends LitElement {
|
||||
@@ -35,7 +40,7 @@ export default class SlProgressBar extends LitElement {
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.indeterminate ? null : this.percentage}"
|
||||
aria-valuenow="${this.indeterminate ? '' : this.percentage}"
|
||||
>
|
||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.percentage + '%' })}>
|
||||
${!this.indeterminate
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --track-color: The track color.
|
||||
* @prop --indicator-color: The indicator color.
|
||||
*/
|
||||
:host {
|
||||
--track-color: var(--sl-color-gray-200);
|
||||
--indicator-color: var(--sl-color-primary-500);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./progress-ring.scss';
|
||||
|
||||
@@ -11,6 +11,9 @@ import styles from 'sass:./progress-ring.scss';
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
* @part label - The progress ring label.
|
||||
*
|
||||
* @customProperty --track-color - The track color.
|
||||
* @customProperty --indicator-color - The indicator color.
|
||||
*/
|
||||
@customElement('sl-progress-ring')
|
||||
export default class SlProgressRing extends LitElement {
|
||||
@@ -31,7 +34,7 @@ export default class SlProgressRing extends LitElement {
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
@watch('percentage')
|
||||
@watch('percentage', { waitUntilFirstUpdate: true })
|
||||
handlePercentageChange() {
|
||||
this.updateProgress();
|
||||
}
|
||||
@@ -47,7 +50,14 @@ export default class SlProgressRing extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div part="base" class="progress-ring">
|
||||
<div
|
||||
part="base"
|
||||
class="progress-ring"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.percentage}"
|
||||
>
|
||||
<svg class="progress-ring__image" width=${this.size} height=${this.size}>
|
||||
<circle
|
||||
class="progress-ring__track"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
import { watch } from '../../internal/decorators';
|
||||
import QrCreator from 'qr-creator';
|
||||
@@ -35,7 +35,7 @@ export default class SlQrCode extends LitElement {
|
||||
/** The edge radius of each module. Must be between 0 and 0.5. */
|
||||
@property({ type: Number }) radius = 0;
|
||||
|
||||
/* The level of error correction to use. */
|
||||
/** The level of error correction to use. */
|
||||
@property({ attribute: 'error-correction' }) errorCorrection: 'L' | 'M' | 'Q' | 'H' = 'H';
|
||||
|
||||
firstUpdated() {
|
||||
@@ -49,6 +49,10 @@ export default class SlQrCode extends LitElement {
|
||||
@watch('size')
|
||||
@watch('value')
|
||||
generate() {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
QrCreator.render(
|
||||
{
|
||||
text: this.value,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import styles from 'sass:./radio-group.scss';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
@@ -97,7 +97,7 @@ export default class SlRadio extends LitElement {
|
||||
return this.getAllRadios().filter(radio => radio !== this) as this[];
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
handleCheckedChange() {
|
||||
if (this.checked) {
|
||||
this.getSiblingRadios().map(radio => (radio.checked = false));
|
||||
@@ -172,8 +172,8 @@ export default class SlRadio extends LitElement {
|
||||
type="radio"
|
||||
name=${ifDefined(this.name)}
|
||||
value=${ifDefined(this.value)}
|
||||
?checked=${this.checked}
|
||||
?disabled=${this.disabled}
|
||||
.checked=${this.checked}
|
||||
.disabled=${this.disabled}
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
aria-labelledby=${this.labelId}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./range.scss';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from 'sass:./range.scss';
|
||||
|
||||
let id = 0;
|
||||
|
||||
@@ -85,6 +85,7 @@ export default class SlRange extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
this.resizeObserver = new ResizeObserver(() => this.syncTooltip());
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
|
||||
if (this.value === undefined || this.value === null) this.value = this.min;
|
||||
@@ -92,15 +93,16 @@ export default class SlRange extends LitElement {
|
||||
if (this.value > this.max) this.value = this.max;
|
||||
|
||||
this.handleSlotChange();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.syncTooltip();
|
||||
this.resizeObserver = new ResizeObserver(() => this.syncTooltip());
|
||||
this.updateComplete.then(() => {
|
||||
this.syncTooltip();
|
||||
this.resizeObserver.observe(this.input);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.unobserve(this.input);
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
@@ -131,14 +133,12 @@ export default class SlRange extends LitElement {
|
||||
this.hasFocus = false;
|
||||
this.hasTooltip = false;
|
||||
this.slBlur.emit();
|
||||
this.resizeObserver.unobserve(this.input);
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.hasTooltip = true;
|
||||
this.slFocus.emit();
|
||||
this.resizeObserver.observe(this.input);
|
||||
}
|
||||
|
||||
@watch('label')
|
||||
@@ -165,6 +165,7 @@ export default class SlRange extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
// NOTE - always bind value after min/max, otherwise it will be clamped
|
||||
return renderFormControl(
|
||||
{
|
||||
inputId: this.inputId,
|
||||
@@ -193,12 +194,12 @@ export default class SlRange extends LitElement {
|
||||
part="input"
|
||||
type="range"
|
||||
class="range__control"
|
||||
name=${this.name}
|
||||
.value=${this.value + ''}
|
||||
name=${ifDefined(this.name)}
|
||||
?disabled=${this.disabled}
|
||||
min=${ifDefined(this.min)}
|
||||
max=${ifDefined(this.max)}
|
||||
step=${ifDefined(this.step)}
|
||||
.value=${String(this.value)}
|
||||
aria-labelledby=${ifDefined(
|
||||
getLabelledBy({
|
||||
label: this.label,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
/**
|
||||
* @prop --symbol-color: The inactive color for symbols.
|
||||
* @prop --symbol-color-active: The active color for symbols.
|
||||
* @prop --symbol-size: The size of symbols.
|
||||
* @prop --symbol-spacing: The spacing to use around symbols.
|
||||
*/
|
||||
:host {
|
||||
--symbol-color: var(--sl-color-gray-300);
|
||||
--symbol-color-active: #ffbe00;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./rating.scss';
|
||||
import { focusVisible } from '../../internal/focus-visible';
|
||||
import { clamp } from '../../internal/math';
|
||||
import styles from 'sass:./rating.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -15,6 +15,11 @@ import { clamp } from '../../internal/math';
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
*
|
||||
* @customProperty --symbol-color - The inactive color for symbols.
|
||||
* @customProperty --symbol-color-active - The active color for symbols.
|
||||
* @customProperty --symbol-size - The size of symbols.
|
||||
* @customProperty --symbol-spacing - The spacing to use around symbols.
|
||||
*/
|
||||
@customElement('sl-rating')
|
||||
export default class SlRating extends LitElement {
|
||||
@@ -57,8 +62,9 @@ export default class SlRating extends LitElement {
|
||||
this.rating.blur();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
focusVisible.observe(this.rating);
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => focusVisible.observe(this.rating));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -157,7 +163,7 @@ export default class SlRating extends LitElement {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.slChange.emit();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/decorators';
|
||||
|
||||
/**
|
||||
@@ -32,10 +32,6 @@ export default class SlRelativeTime extends LitElement {
|
||||
/** Keep the displayed value up to date as time passes. */
|
||||
@property({ type: Boolean }) sync = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.updateTime();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearTimeout(this.updateTimeout);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { event, EventEmitter } from '../../internal/decorators';
|
||||
import styles from 'sass:./resize-observer.scss';
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.responsive-embed {
|
||||
position: relative;
|
||||
|
||||
::slotted(embed),
|
||||
::slotted(iframe),
|
||||
::slotted(object) {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import styles from 'sass:./responsive-embed.scss';
|
||||
import { watch } from '../../internal/decorators';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @part base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-responsive-embed')
|
||||
export default class SlResponsiveEmbed extends LitElement {
|
||||
static styles = unsafeCSS(styles);
|
||||
|
||||
@query('.responsive-embed') base: HTMLElement;
|
||||
|
||||
/**
|
||||
* The aspect ratio of the embedded media in the format of `width:height`, e.g. `16:9`, `4:3`, or `1:1`. Ratios not in
|
||||
* this format will be ignored.
|
||||
*/
|
||||
@property({ attribute: 'aspect-ratio' }) aspectRatio = '16:9';
|
||||
|
||||
@watch('aspectRatio')
|
||||
updateAspectRatio() {
|
||||
const split = this.aspectRatio.split(':');
|
||||
const x = parseInt(split[0]);
|
||||
const y = parseInt(split[1]);
|
||||
|
||||
this.base.style.paddingBottom = x && y ? `${(y / x) * 100}%` : '';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div part="base" class="responsive-embed">
|
||||
<slot @slotchange=${() => this.updateAspectRatio()}></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-responsive-embed': SlResponsiveEmbed;
|
||||
}
|
||||
}
|
||||
35
src/components/responsive-media/responsive-media.scss
Normal file
35
src/components/responsive-media/responsive-media.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
@use '../../styles/component';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.responsive-media {
|
||||
position: relative;
|
||||
|
||||
::slotted(*) {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-media--cover {
|
||||
::slotted(embed),
|
||||
::slotted(iframe),
|
||||
::slotted(img),
|
||||
::slotted(video) {
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-media--contain {
|
||||
::slotted(embed),
|
||||
::slotted(iframe),
|
||||
::slotted(img),
|
||||
::slotted(video) {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
}
|
||||
50
src/components/responsive-media/responsive-media.ts
Normal file
50
src/components/responsive-media/responsive-media.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import styles from 'sass:./responsive-media.scss';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @slot - The element to receive the aspect ratio. Should be a replaced element, such as `<img>`, `<iframe>`, or `<video>`.
|
||||
*/
|
||||
@customElement('sl-responsive-media')
|
||||
export default class SlResponsiveMedia extends LitElement {
|
||||
static styles = unsafeCSS(styles);
|
||||
|
||||
/**
|
||||
* The aspect ratio of the embedded media in the format of `width:height`, e.g. `16:9`, `4:3`, or `1:1`. Ratios not in
|
||||
* this format will be ignored.
|
||||
*/
|
||||
@property({ attribute: 'aspect-ratio' }) aspectRatio = '16:9';
|
||||
|
||||
/** Determines how content will be resized to fit its container. */
|
||||
@property() fit: 'cover' | 'contain' = 'cover';
|
||||
|
||||
render() {
|
||||
const split = this.aspectRatio.split(':');
|
||||
const x = parseInt(split[0]);
|
||||
const y = parseInt(split[1]);
|
||||
const paddingBottom = x && y ? `${(y / x) * 100}%` : '0';
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
'responsive-media': true,
|
||||
'responsive-media--cover': this.fit === 'cover',
|
||||
'responsive-media--contain': this.fit === 'contain'
|
||||
})}
|
||||
style="padding-bottom: ${paddingBottom}"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-responsive-media': SlResponsiveMedia;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
@use '../../styles/form-control';
|
||||
@use '../../styles/mixins/hide';
|
||||
|
||||
/**
|
||||
* @prop --focus-ring: The focus ring style to use when the control receives focus, a `box-shadow` property.
|
||||
*/
|
||||
:host {
|
||||
--focus-ring: 0 0 0 var(--sl-focus-ring-width) var(--sl-focus-ring-color-primary);
|
||||
|
||||
@@ -38,7 +35,7 @@
|
||||
color: var(--sl-input-color-hover);
|
||||
}
|
||||
|
||||
.select:not(.select--disabled) .select__box:focus {
|
||||
.select.select--focused:not(.select--disabled) .select__box {
|
||||
background-color: var(--sl-input-background-color-focus);
|
||||
border-color: var(--sl-input-border-color-focus);
|
||||
box-shadow: var(--focus-ring);
|
||||
|
||||
24
src/components/select/select.test.ts
Normal file
24
src/components/select/select.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlSelect from './select';
|
||||
|
||||
describe('<sl-select>', () => {
|
||||
it('should emit sl-change when the value changes', async () => {
|
||||
const el = (await fixture(html`
|
||||
<sl-select>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`)) as SlSelect;
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.value = 'option-2';
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,16 @@
|
||||
import { LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import styles from 'sass:./select.scss';
|
||||
import { SlDropdown, SlIconButton, SlMenu, SlMenuItem } from '../../shoelace';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from 'sass:./select.scss';
|
||||
|
||||
let id = 0;
|
||||
|
||||
@@ -34,12 +37,15 @@ let id = 0;
|
||||
* @part menu - The select menu, a <sl-menu> element.
|
||||
* @part tag - The multiselect option, a <sl-tag> element.
|
||||
* @part tags - The container in which multiselect options are rendered.
|
||||
*
|
||||
* @customProperty --focus-ring - The focus ring style to use when the control receives focus, a `box-shadow` property.
|
||||
*/
|
||||
@customElement('sl-select')
|
||||
export default class SlSelect extends LitElement {
|
||||
static styles = unsafeCSS(styles);
|
||||
|
||||
@query('.select') dropdown: SlDropdown;
|
||||
@query('.select__box') box: SlDropdown;
|
||||
@query('.select__hidden-select') input: HTMLInputElement;
|
||||
@query('.select__menu') menu: SlMenu;
|
||||
|
||||
@@ -118,18 +124,22 @@ export default class SlSelect extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleSlotChange = this.handleSlotChange.bind(this);
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
|
||||
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
this.handleSlotChange();
|
||||
this.updateComplete.then(() => {
|
||||
this.resizeObserver.observe(this);
|
||||
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
|
||||
this.syncItemsFromValue();
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
|
||||
this.syncItemsFromValue();
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.unobserve(this);
|
||||
this.shadowRoot!.removeEventListener('slotchange', this.handleSlotChange);
|
||||
}
|
||||
|
||||
@@ -154,12 +164,20 @@ export default class SlSelect extends LitElement {
|
||||
}
|
||||
|
||||
getValueAsArray() {
|
||||
// Single selects use '' as an empty selection value, so convert this to [] for an empty multi select
|
||||
if (this.multiple && this.value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.isArray(this.value) ? this.value : [this.value];
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.slBlur.emit();
|
||||
// Don't blur if the control is open. We'll move focus back once it closes.
|
||||
if (!this.isOpen) {
|
||||
this.hasFocus = false;
|
||||
this.slBlur.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleClearClick(event: MouseEvent) {
|
||||
@@ -169,7 +187,7 @@ export default class SlSelect extends LitElement {
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
if (this.disabled && this.isOpen) {
|
||||
this.dropdown.hide();
|
||||
@@ -177,8 +195,10 @@ export default class SlSelect extends LitElement {
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.slFocus.emit();
|
||||
if (!this.hasFocus) {
|
||||
this.hasFocus = true;
|
||||
this.slFocus.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -221,8 +241,8 @@ export default class SlSelect extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// All other keys open the menu and initiate type to select
|
||||
if (!this.isOpen) {
|
||||
// All other "printable" keys open the menu and initiate type to select
|
||||
if (!this.isOpen && event.key.length === 1) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.dropdown.show();
|
||||
@@ -249,23 +269,19 @@ export default class SlSelect extends LitElement {
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
handleMenuShow(event: CustomEvent) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
handleMenuShow() {
|
||||
this.resizeMenu();
|
||||
this.resizeObserver.observe(this);
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
handleMenuHide() {
|
||||
this.resizeObserver.unobserve(this);
|
||||
this.isOpen = false;
|
||||
|
||||
// Restore focus on the box after the menu is hidden
|
||||
this.box.focus();
|
||||
}
|
||||
|
||||
@watch('multiple')
|
||||
@watch('multiple', { waitUntilFirstUpdate: true })
|
||||
handleMultipleChange() {
|
||||
// Cast to array | string based on `this.multiple`
|
||||
const value = this.getValueAsArray();
|
||||
@@ -273,8 +289,8 @@ export default class SlSelect extends LitElement {
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
@watch('helpText')
|
||||
@watch('label')
|
||||
@watch('helpText', { waitUntilFirstUpdate: true })
|
||||
@watch('label', { waitUntilFirstUpdate: true })
|
||||
async handleSlotChange() {
|
||||
this.hasHelpTextSlot = hasSlot(this, 'help-text');
|
||||
this.hasLabelSlot = hasSlot(this, 'label');
|
||||
@@ -300,9 +316,11 @@ export default class SlSelect extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
handleValueChange() {
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
async handleValueChange() {
|
||||
this.syncItemsFromValue();
|
||||
await this.updateComplete;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.slChange.emit();
|
||||
}
|
||||
|
||||
@@ -399,6 +417,7 @@ export default class SlSelect extends LitElement {
|
||||
.hoist=${this.hoist}
|
||||
.closeOnSelect=${!this.multiple}
|
||||
.containingElement=${this}
|
||||
?disabled=${this.disabled}
|
||||
class=${classMap({
|
||||
select: true,
|
||||
'select--open': this.isOpen,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user